@outbuild-company/schedule-core 1.14.2 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -341,6 +341,7 @@ var DISPATCH_ACTION_KIND = {
341
341
  ACTIVITY_MOVE: "activity-move",
342
342
  ACTIVITY_INDENT: "activity-indent",
343
343
  ACTIVITY_OUTDENT: "activity-outdent",
344
+ ACTIVITY_TYPE_SET: "activity-type-set",
344
345
  ACTIVITY_SET_PROGRESS: "activity-set-progress",
345
346
  ACTIVITY_LOOKAHEAD_CONVERT: "activity-lookahead-convert",
346
347
  SELECTION_TOGGLE: "selection-toggle",
@@ -349,7 +350,8 @@ var DISPATCH_ACTION_KIND = {
349
350
  FILTER_SET: "filter-set",
350
351
  SORT_SET: "sort-set",
351
352
  SIR_SYNC: "sir-sync",
352
- ACTIVITY_LOOKAHEAD_SYNC: "activity-lookahead-sync"
353
+ ACTIVITY_LOOKAHEAD_SYNC: "activity-lookahead-sync",
354
+ SCHEDULE_AUTO_SCHEDULE: "schedule-auto-schedule"
353
355
  };
354
356
  var ACTIVITY_BATCH_ACTION_KIND = "activity-batch";
355
357
  var DISPATCH_LINK_OPERATION_KIND = LINK_OPERATION_KIND;
@@ -449,6 +451,8 @@ function batchLink(operation) {
449
451
  return {
450
452
  kind: DISPATCH_LINK_OPERATION_KIND.UPDATE,
451
453
  linkId: operation.linkId,
454
+ ...operation.sourceId === void 0 ? {} : { source: operation.sourceId },
455
+ ...operation.targetId === void 0 ? {} : { target: operation.targetId },
452
456
  ...operation.relationship === void 0 ? {} : { type: linkType(operation.relationship) },
453
457
  ...operation.lagDays === void 0 ? {} : { lag: operation.lagDays }
454
458
  };
@@ -505,6 +509,14 @@ function createActivitiesApi(engine) {
505
509
  },
506
510
  options
507
511
  ),
512
+ setType: (activityId, type, options) => engine.dispatch(
513
+ {
514
+ kind: DISPATCH_ACTION_KIND.ACTIVITY_TYPE_SET,
515
+ activityId,
516
+ type
517
+ },
518
+ options
519
+ ),
508
520
  applyDragBatch: (edits, options) => engine.dispatch(
509
521
  {
510
522
  kind: DISPATCH_ACTION_KIND.DATES_BATCH,
@@ -566,6 +578,13 @@ function createActivitiesApi(engine) {
566
578
  ...input,
567
579
  kind: DISPATCH_ACTION_KIND.ACTIVITY_LOOKAHEAD_SYNC
568
580
  }),
581
+ autoSchedule: (activityId, options) => engine.dispatch(
582
+ {
583
+ kind: DISPATCH_ACTION_KIND.SCHEDULE_AUTO_SCHEDULE,
584
+ ...activityId === void 0 ? {} : { activityId }
585
+ },
586
+ options
587
+ ),
569
588
  createDateGesturePreview: (gesture) => engine.createDateGesturePreview(gesture)
570
589
  };
571
590
  }
@@ -589,6 +608,8 @@ function createLinksApi(engine) {
589
608
  {
590
609
  kind: DISPATCH_ACTION_KIND.LINK_UPDATE,
591
610
  linkId,
611
+ ...patch.sourceId === void 0 ? {} : { source: patch.sourceId },
612
+ ...patch.targetId === void 0 ? {} : { target: patch.targetId },
592
613
  ...patch.relationship === void 0 ? {} : { type: linkType(patch.relationship) },
593
614
  ...patch.lagDays === void 0 ? {} : { lag: patch.lagDays }
594
615
  },
@@ -1902,6 +1923,87 @@ function assertNever(value, message) {
1902
1923
  throw new Error(message);
1903
1924
  }
1904
1925
 
1926
+ // src/activity-types/behavior.ts
1927
+ var PONDERATOR_WEIGHT = {
1928
+ OWN_BASELINE: "own-baseline",
1929
+ RECURSIVE_LEAF_SUM: "recursive-leaf-sum",
1930
+ ZERO: "zero"
1931
+ };
1932
+ var ACTIVITY_TYPE_BEHAVIOR = {
1933
+ [ACTIVITY_TYPE.PROJECT]: {
1934
+ alwaysSummary: true,
1935
+ holdsConstraint: false,
1936
+ carriesCustomId: false,
1937
+ forcedDuration: null,
1938
+ ponderatorWeight: PONDERATOR_WEIGHT.RECURSIVE_LEAF_SUM,
1939
+ promotesToProjectOnFirstChild: false
1940
+ },
1941
+ [ACTIVITY_TYPE.TASK]: {
1942
+ alwaysSummary: false,
1943
+ holdsConstraint: true,
1944
+ carriesCustomId: true,
1945
+ forcedDuration: null,
1946
+ ponderatorWeight: PONDERATOR_WEIGHT.OWN_BASELINE,
1947
+ promotesToProjectOnFirstChild: true
1948
+ },
1949
+ [ACTIVITY_TYPE.MILESTONE]: {
1950
+ alwaysSummary: false,
1951
+ holdsConstraint: true,
1952
+ carriesCustomId: true,
1953
+ forcedDuration: 0,
1954
+ ponderatorWeight: PONDERATOR_WEIGHT.ZERO,
1955
+ promotesToProjectOnFirstChild: true
1956
+ }
1957
+ };
1958
+
1959
+ // src/activity-types/predicates.ts
1960
+ function isSummary(type, hasChildren) {
1961
+ return hasChildren || isActivityType(type) && ACTIVITY_TYPE_BEHAVIOR[type].alwaysSummary;
1962
+ }
1963
+ function isMilestoneType(type) {
1964
+ return type === ACTIVITY_TYPE.MILESTONE;
1965
+ }
1966
+ function isPromotableLeafType(type) {
1967
+ return isActivityType(type) && ACTIVITY_TYPE_BEHAVIOR[type].promotesToProjectOnFirstChild;
1968
+ }
1969
+
1970
+ // src/activity-types/transitions.ts
1971
+ var PROMOTION_TARGET_TYPE = ACTIVITY_TYPE.PROJECT;
1972
+ var DEMOTION_TARGET_TYPE = ACTIVITY_TYPE.TASK;
1973
+ function typeForNewDuration(currentDuration, newDuration) {
1974
+ if (newDuration === 0) return ACTIVITY_TYPE.MILESTONE;
1975
+ if (currentDuration === 0 && newDuration > 0) return ACTIVITY_TYPE.TASK;
1976
+ return null;
1977
+ }
1978
+
1979
+ // src/constants/activity-properties.ts
1980
+ var ACTIVITY_PROPERTY = {
1981
+ PARENT: "parentId",
1982
+ TYPE: "type",
1983
+ START_DATE: "startDate",
1984
+ END_DATE: "endDate",
1985
+ DURATION_HOURS: "durationHours",
1986
+ CONSTRAINT_TYPE: "constraintType",
1987
+ CONSTRAINT_DATE: "constraintDate",
1988
+ PROGRESS: "progress",
1989
+ COST: "cost",
1990
+ USED_COST: "usedCost",
1991
+ REAL_COST: "realCost",
1992
+ WORK_HOURS: "workHours",
1993
+ REAL_WORK_HOURS: "realWorkHours",
1994
+ CORRELATIVE_ID: "correlativeId",
1995
+ CUSTOM_ID: "customId",
1996
+ NEW_ACTIVITIES_ARRAY: "newActivityIds",
1997
+ HAS_NEW_ACTIVITIES: "hasNewActivities",
1998
+ CALENDAR_DURATION: "calendarDuration"};
1999
+ var LINK_PROPERTY = {
2000
+ SOURCE: "source",
2001
+ TARGET: "target",
2002
+ TYPE: "type",
2003
+ LAG: "lag",
2004
+ FREE_SLACK_HOURS: "freeSlackHours",
2005
+ IS_CRITICAL: "isCritical"};
2006
+
1905
2007
  // src/autoscheduler/utils/cancellation.ts
1906
2008
  function createIsCurrent(id, getCurrentId) {
1907
2009
  return () => getCurrentId() === id;
@@ -2012,59 +2114,6 @@ function filterTopoOrder(order, scopedIds) {
2012
2114
  return filtered;
2013
2115
  }
2014
2116
 
2015
- // src/activity-types/behavior.ts
2016
- var PONDERATOR_WEIGHT = {
2017
- OWN_BASELINE: "own-baseline",
2018
- RECURSIVE_LEAF_SUM: "recursive-leaf-sum",
2019
- ZERO: "zero"
2020
- };
2021
- var ACTIVITY_TYPE_BEHAVIOR = {
2022
- [ACTIVITY_TYPE.PROJECT]: {
2023
- alwaysSummary: true,
2024
- holdsConstraint: false,
2025
- carriesCustomId: false,
2026
- forcedDuration: null,
2027
- ponderatorWeight: PONDERATOR_WEIGHT.RECURSIVE_LEAF_SUM,
2028
- promotesToProjectOnFirstChild: false
2029
- },
2030
- [ACTIVITY_TYPE.TASK]: {
2031
- alwaysSummary: false,
2032
- holdsConstraint: true,
2033
- carriesCustomId: true,
2034
- forcedDuration: null,
2035
- ponderatorWeight: PONDERATOR_WEIGHT.OWN_BASELINE,
2036
- promotesToProjectOnFirstChild: true
2037
- },
2038
- [ACTIVITY_TYPE.MILESTONE]: {
2039
- alwaysSummary: false,
2040
- holdsConstraint: true,
2041
- carriesCustomId: true,
2042
- forcedDuration: 0,
2043
- ponderatorWeight: PONDERATOR_WEIGHT.ZERO,
2044
- promotesToProjectOnFirstChild: true
2045
- }
2046
- };
2047
-
2048
- // src/activity-types/predicates.ts
2049
- function isSummary(type, hasChildren) {
2050
- return hasChildren || isActivityType(type) && ACTIVITY_TYPE_BEHAVIOR[type].alwaysSummary;
2051
- }
2052
- function isMilestoneType(type) {
2053
- return type === ACTIVITY_TYPE.MILESTONE;
2054
- }
2055
- function isPromotableLeafType(type) {
2056
- return isActivityType(type) && ACTIVITY_TYPE_BEHAVIOR[type].promotesToProjectOnFirstChild;
2057
- }
2058
-
2059
- // src/activity-types/transitions.ts
2060
- var PROMOTION_TARGET_TYPE = ACTIVITY_TYPE.PROJECT;
2061
- var DEMOTION_TARGET_TYPE = ACTIVITY_TYPE.TASK;
2062
- function typeForNewDuration(currentDuration, newDuration) {
2063
- if (newDuration === 0) return ACTIVITY_TYPE.MILESTONE;
2064
- if (currentDuration === 0 && newDuration > 0) return ACTIVITY_TYPE.TASK;
2065
- return null;
2066
- }
2067
-
2068
2117
  // src/autoscheduler/utils/parent-link-expansion.ts
2069
2118
  function isSummary2(id, adapter) {
2070
2119
  const activity = adapter.getActivity(id);
@@ -3538,6 +3587,53 @@ function applyLinkUpdate(op, port) {
3538
3587
  if (!link) {
3539
3588
  return { applied: false, rejected: LINK_REJECTION_REASON.LINK_MISSING };
3540
3589
  }
3590
+ const proposed = {
3591
+ ...link,
3592
+ source: op.source ?? link.source,
3593
+ target: op.target ?? link.target,
3594
+ type: op.type ?? link.type,
3595
+ lag: op.lag ?? link.lag
3596
+ };
3597
+ if (String(proposed.source) === String(proposed.target)) {
3598
+ return { applied: false, rejected: LINK_REJECTION_REASON.SELF_LINK };
3599
+ }
3600
+ if (!port.getActivity(proposed.source) || !port.getActivity(proposed.target)) {
3601
+ return {
3602
+ applied: false,
3603
+ rejected: LINK_REJECTION_REASON.ACTIVITY_MISSING
3604
+ };
3605
+ }
3606
+ if (isSummaryHierarchyLink(proposed.source, proposed.target, port)) {
3607
+ return {
3608
+ applied: false,
3609
+ rejected: LINK_REJECTION_REASON.HIERARCHY_LINK
3610
+ };
3611
+ }
3612
+ const currentLinks = port.getAllLinks();
3613
+ const duplicate = currentLinks.find(
3614
+ (candidate) => String(candidate.id) !== String(op.linkId) && linkKey(candidate) === linkKey(proposed)
3615
+ );
3616
+ if (duplicate) {
3617
+ return {
3618
+ applied: false,
3619
+ rejected: LINK_REJECTION_REASON.DUPLICATE,
3620
+ linkId: duplicate.id
3621
+ };
3622
+ }
3623
+ const proposedLinks = currentLinks.map(
3624
+ (candidate) => String(candidate.id) === String(op.linkId) ? proposed : candidate
3625
+ );
3626
+ const effectiveVerdict = validateTopologyCommit({
3627
+ port,
3628
+ currentLinks,
3629
+ proposedLinks
3630
+ });
3631
+ if (!effectiveVerdict.ok) {
3632
+ return { applied: false, rejected: LINK_REJECTION_REASON.CYCLE };
3633
+ }
3634
+ if (String(link.source) !== String(proposed.source) || String(link.target) !== String(proposed.target)) {
3635
+ port.setLinkEndpoints(op.linkId, proposed.source, proposed.target);
3636
+ }
3541
3637
  if (op.lag !== void 0) port.setLinkField(op.linkId, "lag", op.lag);
3542
3638
  if (op.type !== void 0) port.setLinkField(op.linkId, "type", op.type);
3543
3639
  return { applied: true, linkId: op.linkId };
@@ -4020,10 +4116,7 @@ function adjustLinkLagOnTaskMove(targetId, adapter, preEditTarget) {
4020
4116
  calendarApi
4021
4117
  );
4022
4118
  if (newLag === link.lag) continue;
4023
- applyLinkUpdate(
4024
- { kind: LINK_OPERATION_KIND.UPDATE, linkId: link.id, lag: newLag },
4025
- adapter
4026
- );
4119
+ adapter.setLinkField(link.id, LINK_PROPERTY.LAG, newLag);
4027
4120
  }
4028
4121
  }
4029
4122
 
@@ -5587,6 +5680,7 @@ var CONSTRAINT_WARNING_KIND = {
5587
5680
  VIOLATION: "constraint_violation"
5588
5681
  };
5589
5682
  var CHANGE_SET_SOURCE_KIND = {
5683
+ DISPATCH_MANY: "dispatch-many",
5590
5684
  INIT: "init",
5591
5685
  UNDO: "undo",
5592
5686
  REDO: "redo"
@@ -5636,32 +5730,6 @@ var COLUMN_WRITABLE_FIELDS = new Set(
5636
5730
  COLUMN_WRITABLE_FIELD_LIST
5637
5731
  );
5638
5732
 
5639
- // src/constants/activity-properties.ts
5640
- var ACTIVITY_PROPERTY = {
5641
- PARENT: "parentId",
5642
- TYPE: "type",
5643
- START_DATE: "startDate",
5644
- END_DATE: "endDate",
5645
- DURATION_HOURS: "durationHours",
5646
- CONSTRAINT_TYPE: "constraintType",
5647
- CONSTRAINT_DATE: "constraintDate",
5648
- PROGRESS: "progress",
5649
- COST: "cost",
5650
- USED_COST: "usedCost",
5651
- REAL_COST: "realCost",
5652
- WORK_HOURS: "workHours",
5653
- REAL_WORK_HOURS: "realWorkHours",
5654
- CORRELATIVE_ID: "correlativeId",
5655
- CUSTOM_ID: "customId",
5656
- NEW_ACTIVITIES_ARRAY: "newActivityIds",
5657
- HAS_NEW_ACTIVITIES: "hasNewActivities",
5658
- CALENDAR_DURATION: "calendarDuration"};
5659
- var LINK_PROPERTY = {
5660
- TYPE: "type",
5661
- LAG: "lag",
5662
- FREE_SLACK_HOURS: "freeSlackHours",
5663
- IS_CRITICAL: "isCritical"};
5664
-
5665
5733
  // src/internal/state/dynamic-writes.ts
5666
5734
  function setActivityFieldDynamic(state, activityId, field, value) {
5667
5735
  state.setActivityFieldDynamic(activityId, field, value);
@@ -10916,48 +10984,267 @@ function cloneLink(link) {
10916
10984
  return { ...link };
10917
10985
  }
10918
10986
 
10919
- // src/dispatch/shared/post-mutation.ts
10920
- var AUTOSCHEDULE_SCOPE = { ROOTS: "roots" };
10921
- async function runPostMutation(deps, args) {
10922
- const progressInvalidation = resolveProgressInvalidation(deps.adapter);
10923
- const passes = resolvePasses(args, progressInvalidation);
10924
- if (passes.has(DERIVED_PASS.AUTOSCHEDULE)) {
10925
- await refreshMutatedParentBounds(deps.adapter, args);
10926
- }
10927
- const schedule = passes.has(DERIVED_PASS.AUTOSCHEDULE) ? await runAutoschedule(
10928
- deps,
10929
- args.autoscheduleFrom,
10930
- args.preventSummaryTargetPullback
10931
- ) : emptySchedule();
10932
- await rollUpParentBounds(
10933
- deps.adapter,
10934
- args,
10935
- schedule.scheduledIds,
10936
- passes,
10937
- progressInvalidation
10938
- );
10939
- await runDerivedRecomputes(deps, passes, args.now);
10940
- return schedule;
10941
- }
10942
- function resolvePasses(args, progressInvalidation) {
10943
- const requested = args.autoscheduleFrom !== null;
10944
- const ran = !args.options.skipAutoSchedule && requested;
10945
- const passes = new Set(
10946
- resolveDerivedPasses(args.action, { ran }, args.options)
10947
- );
10948
- if (args.recomputeParentProgress || progressInvalidation.affectedParents.size > 0) {
10949
- passes.add(DERIVED_PASS.PARENT_PROGRESS);
10987
+ // src/dispatch/sir-auto-reject.ts
10988
+ function detectSirAutoReject(beforeSnap, adapter, touchedIds) {
10989
+ const effects = [];
10990
+ for (const id of touchedIds) {
10991
+ const before = beforeSnap.get(id);
10992
+ if (!before) continue;
10993
+ const pendingRequestIds = before.pendingRequestIds ?? [];
10994
+ if (!isFrozen({ pendingRequestIds })) continue;
10995
+ if (adapter.getActivity(id) != null) continue;
10996
+ for (const pending of pendingRequestIds) {
10997
+ effects.push({
10998
+ kind: SCHEDULE_EFFECT_KIND.SIR_AUTO_REJECT,
10999
+ activityId: id,
11000
+ sirId: pending,
11001
+ reason: SIR_AUTO_REJECT_REASON.ACTIVITY_DELETED
11002
+ });
11003
+ }
10950
11004
  }
10951
- return passes;
11005
+ return effects;
10952
11006
  }
10953
- function resolveProgressInvalidation(adapter) {
10954
- const affectedParents = /* @__PURE__ */ new Set();
10955
- const dirtySeeds = /* @__PURE__ */ new Set();
10956
- const journal = adapter.peekWriteCapture();
10957
- if (!journal) return { affectedParents, dirtySeeds };
10958
- const affectParent = (parentId) => {
10959
- if (parentId === null || isRootParent(parentId)) return;
10960
- const normalized2 = String(parentId);
11007
+
11008
+ // src/dispatch/conversion.ts
11009
+ function snapshotToLink(l) {
11010
+ const view = {
11011
+ id: String(l.id),
11012
+ source: String(l.source),
11013
+ target: String(l.target),
11014
+ type: l.type,
11015
+ lag: Number(l.lag),
11016
+ freeSlackHours: typeof l.freeSlackHours === "number" && Number.isFinite(l.freeSlackHours) ? l.freeSlackHours : null,
11017
+ isCritical: l.isCritical === true
11018
+ };
11019
+ if (l.proplannerId != null) view.proplannerId = Number(l.proplannerId);
11020
+ if (l.ganttId != null) view.ganttId = Number(l.ganttId);
11021
+ if (l.sectorId != null) view.sectorId = Number(l.sectorId);
11022
+ return view;
11023
+ }
11024
+
11025
+ // src/dispatch/shared/change-set.ts
11026
+ var SIR_DETECTION_PHASE = {
11027
+ BEFORE_DIFF: "before-diff",
11028
+ AFTER_DIFF: "after-diff"
11029
+ };
11030
+ async function assembleChangeSet(adapter, args) {
11031
+ const allTouched = collectChangeSetIds(adapter, args);
11032
+ const before = new Map(args.beforeSnap);
11033
+ const captured = adapter.peekWriteCapture();
11034
+ if (captured) {
11035
+ const insertedIds = collectInsertedIds(captured);
11036
+ mergeDirtyBeforeImages(adapter, captured, insertedIds, before, allTouched);
11037
+ }
11038
+ const sirDetection = args.sirDetection;
11039
+ const beforeDiffEffects = sirDetection === SIR_DETECTION_PHASE.BEFORE_DIFF ? detectSirAutoReject(before, adapter, allTouched) : [];
11040
+ const correlativeBefore = buildCorrelativeBeforeMap(
11041
+ args.correlativeShifts,
11042
+ allTouched
11043
+ );
11044
+ const activityChanges = await buildActivityChanges(
11045
+ adapter,
11046
+ before,
11047
+ allTouched,
11048
+ correlativeBefore
11049
+ );
11050
+ const effects = sirDetection === SIR_DETECTION_PHASE.AFTER_DIFF ? detectSirAutoReject(before, adapter, allTouched) : beforeDiffEffects;
11051
+ const warnings = args.warnings ?? [];
11052
+ const linkChanges = mergeLinkChanges(
11053
+ args.links ?? [],
11054
+ captured ? buildCapturedCriticalPathLinkChanges(adapter, captured) : []
11055
+ );
11056
+ return {
11057
+ source: args.source,
11058
+ activities: activityChanges,
11059
+ links: linkChanges,
11060
+ calendars: [],
11061
+ trackingEvents: args.trackingEvents ?? [],
11062
+ ...effects.length > 0 ? { effects } : {},
11063
+ ...warnings.length > 0 ? { warnings } : {}
11064
+ };
11065
+ }
11066
+ function buildCapturedCriticalPathLinkChanges(adapter, captured) {
11067
+ const changes = [];
11068
+ for (const operation of captured.ops) {
11069
+ if (operation.kind !== WRITE_OPERATION_KIND.LINK_FIELD) continue;
11070
+ const after = adapter.getLink(operation.linkId);
11071
+ if (!after) continue;
11072
+ const fields = {};
11073
+ for (const field of ["freeSlackHours", "isCritical"]) {
11074
+ if (!Object.is(operation.before[field], after[field])) {
11075
+ fields[field] = {
11076
+ before: operation.before[field],
11077
+ after: after[field]
11078
+ };
11079
+ }
11080
+ }
11081
+ if (Object.keys(fields).length === 0) continue;
11082
+ changes.push({
11083
+ id: operation.linkId,
11084
+ kind: ENTITY_CHANGE_KIND.UPDATED,
11085
+ fields,
11086
+ after: snapshotToLink(after)
11087
+ });
11088
+ }
11089
+ return changes;
11090
+ }
11091
+ function mergeLinkChanges(explicit, captured) {
11092
+ const merged = [];
11093
+ const indexById = /* @__PURE__ */ new Map();
11094
+ for (const change of explicit) {
11095
+ const index = indexById.get(change.id);
11096
+ if (index === void 0) {
11097
+ indexById.set(change.id, merged.length);
11098
+ merged.push(change);
11099
+ continue;
11100
+ }
11101
+ const current = merged[index];
11102
+ if ((current.kind === ENTITY_CHANGE_KIND.UPDATED || current.kind === ENTITY_CHANGE_KIND.CREATED) && change.kind === ENTITY_CHANGE_KIND.UPDATED) {
11103
+ merged[index] = {
11104
+ ...current,
11105
+ fields: { ...current.fields, ...change.fields },
11106
+ after: change.after
11107
+ };
11108
+ continue;
11109
+ }
11110
+ merged[index] = change;
11111
+ }
11112
+ for (const change of captured) {
11113
+ const index = indexById.get(change.id);
11114
+ if (index === void 0) {
11115
+ indexById.set(change.id, merged.length);
11116
+ merged.push(change);
11117
+ continue;
11118
+ }
11119
+ const current = merged[index];
11120
+ if (current.kind === ENTITY_CHANGE_KIND.DELETED) continue;
11121
+ const capturedFields = current.kind === ENTITY_CHANGE_KIND.CREATED ? Object.fromEntries(
11122
+ Object.entries(change.fields ?? {}).map(([field, value]) => [
11123
+ field,
11124
+ { before: void 0, after: value.after }
11125
+ ])
11126
+ ) : change.fields;
11127
+ merged[index] = {
11128
+ ...current,
11129
+ fields: { ...current.fields, ...capturedFields },
11130
+ after: change.after
11131
+ };
11132
+ }
11133
+ return merged;
11134
+ }
11135
+ function buildCorrelativeBeforeMap(shifts, touched) {
11136
+ if (!shifts || shifts.length === 0) return void 0;
11137
+ const correlativeBefore = /* @__PURE__ */ new Map();
11138
+ for (const shift of shifts) {
11139
+ correlativeBefore.set(shift.activityId, shift.before);
11140
+ touched.add(shift.activityId);
11141
+ }
11142
+ return correlativeBefore;
11143
+ }
11144
+ function collectChangeSetIds(adapter, args) {
11145
+ const ids = /* @__PURE__ */ new Set();
11146
+ for (const id of args.touchedIds) ids.add(id);
11147
+ for (const id of args.scheduledIds) ids.add(id);
11148
+ if (args.includeAllIds) {
11149
+ for (const id of adapter.getAllIds()) ids.add(id);
11150
+ }
11151
+ return ids;
11152
+ }
11153
+ function mergeDirtyBeforeImages(adapter, captured, insertedIds, before, touched) {
11154
+ for (const id of captured.dirty) {
11155
+ if (insertedIds.has(id) || before.has(id)) continue;
11156
+ const beforeImage = captured.before.get(id);
11157
+ const live = adapter.getActivity(id);
11158
+ if (!beforeImage || !live) continue;
11159
+ if (Object.keys(diffActivity(beforeImage, live)).length === 0) continue;
11160
+ before.set(id, beforeImage);
11161
+ touched.add(id);
11162
+ }
11163
+ }
11164
+ function collectInsertedIds(captured) {
11165
+ return new Set(
11166
+ captured.ops.filter((operation) => operation.kind === WRITE_OPERATION_KIND.INSERT).map((operation) => operation.activityId)
11167
+ );
11168
+ }
11169
+
11170
+ // src/dispatch/shared/post-mutation.ts
11171
+ var AUTOSCHEDULE_SCOPE = { ROOTS: "roots" };
11172
+ async function runPostMutation(deps, args) {
11173
+ const progressInvalidation = resolveProgressInvalidation(deps.adapter);
11174
+ const passes = resolvePasses(args, progressInvalidation);
11175
+ if (passes.has(DERIVED_PASS.AUTOSCHEDULE)) {
11176
+ await refreshMutatedParentBounds(deps.adapter, args);
11177
+ }
11178
+ const schedule = passes.has(DERIVED_PASS.AUTOSCHEDULE) ? await runAutoschedule(
11179
+ deps,
11180
+ args.autoscheduleFrom,
11181
+ args.preventSummaryTargetPullback
11182
+ ) : emptySchedule();
11183
+ await rollUpParentBounds(
11184
+ deps.adapter,
11185
+ args,
11186
+ schedule.scheduledIds,
11187
+ passes,
11188
+ progressInvalidation
11189
+ );
11190
+ await runDerivedRecomputes(deps, passes, args.now);
11191
+ return schedule;
11192
+ }
11193
+ async function dispatchScheduleAutoSchedule(action, options, deps) {
11194
+ if (action.activityId !== void 0 && !deps.adapter.activityExists(action.activityId)) {
11195
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
11196
+ }
11197
+ const beforeSnap = await snapshotActivities(
11198
+ deps.adapter,
11199
+ new Set(deps.adapter.getAllIds())
11200
+ );
11201
+ deps.scheduler.invalidateAllCaches();
11202
+ const recomputeParentsFrom = action.activityId === void 0 ? deps.adapter.getRootIds() : [action.activityId];
11203
+ const { scheduledIds } = await runPostMutation(
11204
+ {
11205
+ adapter: deps.adapter,
11206
+ scheduler: deps.scheduler,
11207
+ sector: deps.sector,
11208
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
11209
+ },
11210
+ {
11211
+ action,
11212
+ autoscheduleFrom: action.activityId ?? AUTOSCHEDULE_SCOPE.ROOTS,
11213
+ recomputeParentsFrom,
11214
+ now: deps.now,
11215
+ options
11216
+ }
11217
+ );
11218
+ return {
11219
+ ok: true,
11220
+ changes: await assembleChangeSet(deps.adapter, {
11221
+ source: action,
11222
+ beforeSnap,
11223
+ touchedIds: recomputeParentsFrom,
11224
+ scheduledIds,
11225
+ hoursPerDay: deps.sector.hoursPerDay
11226
+ })
11227
+ };
11228
+ }
11229
+ function resolvePasses(args, progressInvalidation) {
11230
+ const requested = args.autoscheduleFrom !== null;
11231
+ const ran = !args.options.skipAutoSchedule && requested;
11232
+ const passes = new Set(
11233
+ resolveDerivedPasses(args.action, { ran }, args.options)
11234
+ );
11235
+ if (args.recomputeParentProgress || progressInvalidation.affectedParents.size > 0) {
11236
+ passes.add(DERIVED_PASS.PARENT_PROGRESS);
11237
+ }
11238
+ return passes;
11239
+ }
11240
+ function resolveProgressInvalidation(adapter) {
11241
+ const affectedParents = /* @__PURE__ */ new Set();
11242
+ const dirtySeeds = /* @__PURE__ */ new Set();
11243
+ const journal = adapter.peekWriteCapture();
11244
+ if (!journal) return { affectedParents, dirtySeeds };
11245
+ const affectParent = (parentId) => {
11246
+ if (parentId === null || isRootParent(parentId)) return;
11247
+ const normalized2 = String(parentId);
10961
11248
  affectedParents.add(normalized2);
10962
11249
  const childId = adapter.getChildren(normalized2)[0];
10963
11250
  if (childId !== void 0) dirtySeeds.add(childId);
@@ -11006,8 +11293,27 @@ async function runAutoschedule(deps, autoscheduleFrom, preventSummaryTargetPullb
11006
11293
  }
11007
11294
  const result = await deps.scheduler.schedule(options);
11008
11295
  const outcome = captureScheduledRows(deps.adapter, result.updatedIds);
11296
+ const warnings = collectConstraintWarnings(deps.adapter, result);
11009
11297
  applyResults(deps.adapter, result);
11010
- return outcome;
11298
+ return { ...outcome, warnings };
11299
+ }
11300
+ function collectConstraintWarnings(adapter, result) {
11301
+ const warnings = [];
11302
+ for (const plan of result.plans.values()) {
11303
+ const projectedDate = plan.earliestSchedulingStart;
11304
+ if (!projectedDate || plan.startDate >= projectedDate) continue;
11305
+ const activity = adapter.getActivity(plan.activityId);
11306
+ if (!activity?.constraintType || !activity.constraintDate) continue;
11307
+ warnings.push({
11308
+ kind: CONSTRAINT_WARNING_KIND.VIOLATION,
11309
+ activityId: activity.id,
11310
+ constraintType: activity.constraintType,
11311
+ constraintDate: activity.constraintDate,
11312
+ projectedDate,
11313
+ messageKey: CONSTRAINT_WARNING_KIND.VIOLATION
11314
+ });
11315
+ }
11316
+ return warnings;
11011
11317
  }
11012
11318
  function captureScheduledRows(adapter, updatedIds) {
11013
11319
  const scheduledIds = [];
@@ -11019,10 +11325,10 @@ function captureScheduledRows(adapter, updatedIds) {
11019
11325
  adapter.noteScheduledBefore(activityId, snap);
11020
11326
  }
11021
11327
  }
11022
- return { scheduledIds };
11328
+ return { scheduledIds, warnings: [] };
11023
11329
  }
11024
11330
  function emptySchedule() {
11025
- return { scheduledIds: [] };
11331
+ return { scheduledIds: [], warnings: [] };
11026
11332
  }
11027
11333
  async function refreshMutatedParentBounds(adapter, args) {
11028
11334
  const mutated = args.collectDirty ? args.collectDirty([]) : collectDirtyForStructural(args.recomputeParentsFrom, []);
@@ -11132,241 +11438,58 @@ function asDate(value) {
11132
11438
  if (value instanceof Date) return value;
11133
11439
  if (typeof value === "string" || typeof value === "number") {
11134
11440
  const parsed2 = new Date(value);
11135
- return Number.isNaN(parsed2.getTime()) ? null : parsed2;
11136
- }
11137
- return null;
11138
- }
11139
- function sameDate(first, second) {
11140
- if (first === null && second === null) return true;
11141
- if (first === null || second === null) return false;
11142
- return first.getTime() === second.getTime();
11143
- }
11144
-
11145
- // src/dispatch/shared/constraint-settle.ts
11146
- async function settleConstraintEdits(deps, { postMutationArgs, revertTargets, revert }) {
11147
- let { scheduledIds } = await runPostMutation(
11148
- deps,
11149
- withCriticalPathSuppressed(postMutationArgs)
11150
- );
11151
- let someEditReverted = false;
11152
- for (const { activityId, priorConstraintDate } of revertTargets) {
11153
- const reverted = revert(activityId, priorConstraintDate);
11154
- someEditReverted = someEditReverted || reverted;
11155
- }
11156
- if (someEditReverted) {
11157
- const resettled = await runPostMutation(
11158
- deps,
11159
- withCriticalPathSuppressed(postMutationArgs)
11160
- );
11161
- scheduledIds = mergeScheduledIds(scheduledIds, resettled.scheduledIds);
11162
- }
11163
- if (resolveRunCriticalPath(postMutationArgs.action)) {
11164
- await runPostMutation(deps, criticalPathOnlyArgs(postMutationArgs));
11165
- }
11166
- return { scheduledIds };
11167
- }
11168
- function withCriticalPathSuppressed(args) {
11169
- return {
11170
- ...args,
11171
- options: { ...args.options, skipCriticalPath: true }
11172
- };
11173
- }
11174
- function criticalPathOnlyArgs(args) {
11175
- return {
11176
- action: args.action,
11177
- autoscheduleFrom: null,
11178
- recomputeParentsFrom: [],
11179
- now: null,
11180
- options: args.options
11181
- };
11182
- }
11183
- function mergeScheduledIds(firstSettleIds, secondSettleIds) {
11184
- const merged = new Set(firstSettleIds);
11185
- for (const scheduledId of secondSettleIds) merged.add(scheduledId);
11186
- return [...merged];
11187
- }
11188
-
11189
- // src/dispatch/sir-auto-reject.ts
11190
- function detectSirAutoReject(beforeSnap, adapter, touchedIds) {
11191
- const effects = [];
11192
- for (const id of touchedIds) {
11193
- const before = beforeSnap.get(id);
11194
- if (!before) continue;
11195
- const pendingRequestIds = before.pendingRequestIds ?? [];
11196
- if (!isFrozen({ pendingRequestIds })) continue;
11197
- if (adapter.getActivity(id) != null) continue;
11198
- for (const pending of pendingRequestIds) {
11199
- effects.push({
11200
- kind: SCHEDULE_EFFECT_KIND.SIR_AUTO_REJECT,
11201
- activityId: id,
11202
- sirId: pending,
11203
- reason: SIR_AUTO_REJECT_REASON.ACTIVITY_DELETED
11204
- });
11205
- }
11206
- }
11207
- return effects;
11208
- }
11209
-
11210
- // src/dispatch/conversion.ts
11211
- function snapshotToLink(l) {
11212
- const view = {
11213
- id: String(l.id),
11214
- source: String(l.source),
11215
- target: String(l.target),
11216
- type: l.type,
11217
- lag: Number(l.lag),
11218
- freeSlackHours: typeof l.freeSlackHours === "number" && Number.isFinite(l.freeSlackHours) ? l.freeSlackHours : null,
11219
- isCritical: l.isCritical === true
11220
- };
11221
- if (l.proplannerId != null) view.proplannerId = Number(l.proplannerId);
11222
- if (l.ganttId != null) view.ganttId = Number(l.ganttId);
11223
- if (l.sectorId != null) view.sectorId = Number(l.sectorId);
11224
- return view;
11225
- }
11226
-
11227
- // src/dispatch/shared/change-set.ts
11228
- var SIR_DETECTION_PHASE = {
11229
- BEFORE_DIFF: "before-diff",
11230
- AFTER_DIFF: "after-diff"
11231
- };
11232
- async function assembleChangeSet(adapter, args) {
11233
- const allTouched = collectChangeSetIds(adapter, args);
11234
- const before = new Map(args.beforeSnap);
11235
- const captured = adapter.peekWriteCapture();
11236
- if (captured) {
11237
- const insertedIds = collectInsertedIds(captured);
11238
- mergeDirtyBeforeImages(adapter, captured, insertedIds, before, allTouched);
11239
- }
11240
- const sirDetection = args.sirDetection;
11241
- const beforeDiffEffects = sirDetection === SIR_DETECTION_PHASE.BEFORE_DIFF ? detectSirAutoReject(before, adapter, allTouched) : [];
11242
- const correlativeBefore = buildCorrelativeBeforeMap(
11243
- args.correlativeShifts,
11244
- allTouched
11245
- );
11246
- const activityChanges = await buildActivityChanges(
11247
- adapter,
11248
- before,
11249
- allTouched,
11250
- correlativeBefore
11251
- );
11252
- const effects = sirDetection === SIR_DETECTION_PHASE.AFTER_DIFF ? detectSirAutoReject(before, adapter, allTouched) : beforeDiffEffects;
11253
- const warnings = args.warnings ?? [];
11254
- const linkChanges = mergeLinkChanges(
11255
- args.links ?? [],
11256
- captured ? buildCapturedCriticalPathLinkChanges(adapter, captured) : []
11257
- );
11258
- return {
11259
- source: args.source,
11260
- activities: activityChanges,
11261
- links: linkChanges,
11262
- calendars: [],
11263
- trackingEvents: args.trackingEvents ?? [],
11264
- ...effects.length > 0 ? { effects } : {},
11265
- ...warnings.length > 0 ? { warnings } : {}
11266
- };
11267
- }
11268
- function buildCapturedCriticalPathLinkChanges(adapter, captured) {
11269
- const changes = [];
11270
- for (const operation of captured.ops) {
11271
- if (operation.kind !== WRITE_OPERATION_KIND.LINK_FIELD) continue;
11272
- const after = adapter.getLink(operation.linkId);
11273
- if (!after) continue;
11274
- const fields = {};
11275
- for (const field of ["freeSlackHours", "isCritical"]) {
11276
- if (!Object.is(operation.before[field], after[field])) {
11277
- fields[field] = {
11278
- before: operation.before[field],
11279
- after: after[field]
11280
- };
11281
- }
11282
- }
11283
- if (Object.keys(fields).length === 0) continue;
11284
- changes.push({
11285
- id: operation.linkId,
11286
- kind: ENTITY_CHANGE_KIND.UPDATED,
11287
- fields,
11288
- after: snapshotToLink(after)
11289
- });
11290
- }
11291
- return changes;
11292
- }
11293
- function mergeLinkChanges(explicit, captured) {
11294
- const merged = [];
11295
- const indexById = /* @__PURE__ */ new Map();
11296
- for (const change of explicit) {
11297
- const index = indexById.get(change.id);
11298
- if (index === void 0) {
11299
- indexById.set(change.id, merged.length);
11300
- merged.push(change);
11301
- continue;
11302
- }
11303
- const current = merged[index];
11304
- if ((current.kind === ENTITY_CHANGE_KIND.UPDATED || current.kind === ENTITY_CHANGE_KIND.CREATED) && change.kind === ENTITY_CHANGE_KIND.UPDATED) {
11305
- merged[index] = {
11306
- ...current,
11307
- fields: { ...current.fields, ...change.fields },
11308
- after: change.after
11309
- };
11310
- continue;
11311
- }
11312
- merged[index] = change;
11313
- }
11314
- for (const change of captured) {
11315
- const index = indexById.get(change.id);
11316
- if (index === void 0) {
11317
- indexById.set(change.id, merged.length);
11318
- merged.push(change);
11319
- continue;
11320
- }
11321
- const current = merged[index];
11322
- if (current.kind === ENTITY_CHANGE_KIND.DELETED) continue;
11323
- const capturedFields = current.kind === ENTITY_CHANGE_KIND.CREATED ? Object.fromEntries(
11324
- Object.entries(change.fields ?? {}).map(([field, value]) => [
11325
- field,
11326
- { before: void 0, after: value.after }
11327
- ])
11328
- ) : change.fields;
11329
- merged[index] = {
11330
- ...current,
11331
- fields: { ...current.fields, ...capturedFields },
11332
- after: change.after
11333
- };
11441
+ return Number.isNaN(parsed2.getTime()) ? null : parsed2;
11334
11442
  }
11335
- return merged;
11443
+ return null;
11336
11444
  }
11337
- function buildCorrelativeBeforeMap(shifts, touched) {
11338
- if (!shifts || shifts.length === 0) return void 0;
11339
- const correlativeBefore = /* @__PURE__ */ new Map();
11340
- for (const shift of shifts) {
11341
- correlativeBefore.set(shift.activityId, shift.before);
11342
- touched.add(shift.activityId);
11343
- }
11344
- return correlativeBefore;
11445
+ function sameDate(first, second) {
11446
+ if (first === null && second === null) return true;
11447
+ if (first === null || second === null) return false;
11448
+ return first.getTime() === second.getTime();
11345
11449
  }
11346
- function collectChangeSetIds(adapter, args) {
11347
- const ids = /* @__PURE__ */ new Set();
11348
- for (const id of args.touchedIds) ids.add(id);
11349
- for (const id of args.scheduledIds) ids.add(id);
11350
- if (args.includeAllIds) {
11351
- for (const id of adapter.getAllIds()) ids.add(id);
11450
+
11451
+ // src/dispatch/shared/constraint-settle.ts
11452
+ async function settleConstraintEdits(deps, { postMutationArgs, revertTargets, revert }) {
11453
+ let { scheduledIds } = await runPostMutation(
11454
+ deps,
11455
+ withCriticalPathSuppressed(postMutationArgs)
11456
+ );
11457
+ let someEditReverted = false;
11458
+ for (const { activityId, priorConstraintDate } of revertTargets) {
11459
+ const reverted = revert(activityId, priorConstraintDate);
11460
+ someEditReverted = someEditReverted || reverted;
11352
11461
  }
11353
- return ids;
11354
- }
11355
- function mergeDirtyBeforeImages(adapter, captured, insertedIds, before, touched) {
11356
- for (const id of captured.dirty) {
11357
- if (insertedIds.has(id) || before.has(id)) continue;
11358
- const beforeImage = captured.before.get(id);
11359
- const live = adapter.getActivity(id);
11360
- if (!beforeImage || !live) continue;
11361
- if (Object.keys(diffActivity(beforeImage, live)).length === 0) continue;
11362
- before.set(id, beforeImage);
11363
- touched.add(id);
11462
+ if (someEditReverted) {
11463
+ const resettled = await runPostMutation(
11464
+ deps,
11465
+ withCriticalPathSuppressed(postMutationArgs)
11466
+ );
11467
+ scheduledIds = mergeScheduledIds(scheduledIds, resettled.scheduledIds);
11468
+ }
11469
+ if (resolveRunCriticalPath(postMutationArgs.action)) {
11470
+ await runPostMutation(deps, criticalPathOnlyArgs(postMutationArgs));
11364
11471
  }
11472
+ return { scheduledIds };
11365
11473
  }
11366
- function collectInsertedIds(captured) {
11367
- return new Set(
11368
- captured.ops.filter((operation) => operation.kind === WRITE_OPERATION_KIND.INSERT).map((operation) => operation.activityId)
11369
- );
11474
+ function withCriticalPathSuppressed(args) {
11475
+ return {
11476
+ ...args,
11477
+ options: { ...args.options, skipCriticalPath: true }
11478
+ };
11479
+ }
11480
+ function criticalPathOnlyArgs(args) {
11481
+ return {
11482
+ action: args.action,
11483
+ autoscheduleFrom: null,
11484
+ recomputeParentsFrom: [],
11485
+ now: null,
11486
+ options: args.options
11487
+ };
11488
+ }
11489
+ function mergeScheduledIds(firstSettleIds, secondSettleIds) {
11490
+ const merged = new Set(firstSettleIds);
11491
+ for (const scheduledId of secondSettleIds) merged.add(scheduledId);
11492
+ return [...merged];
11370
11493
  }
11371
11494
 
11372
11495
  // src/dispatch/shared/helpers.ts
@@ -11445,6 +11568,18 @@ function buildLinkChangesForBatch(applied, beforeLinks, adapter) {
11445
11568
  const afterLink = adapter.getLink(op.linkId);
11446
11569
  if (!afterLink) continue;
11447
11570
  const fields = {};
11571
+ if (beforeLink && String(beforeLink.source) !== String(afterLink.source)) {
11572
+ fields.source = {
11573
+ before: String(beforeLink.source),
11574
+ after: String(afterLink.source)
11575
+ };
11576
+ }
11577
+ if (beforeLink && String(beforeLink.target) !== String(afterLink.target)) {
11578
+ fields.target = {
11579
+ before: String(beforeLink.target),
11580
+ after: String(afterLink.target)
11581
+ };
11582
+ }
11448
11583
  if (beforeLink && beforeLink.type !== afterLink.type) {
11449
11584
  fields.type = { before: beforeLink.type, after: afterLink.type };
11450
11585
  }
@@ -11713,6 +11848,12 @@ function collectBatchContext(operations, adapter) {
11713
11848
  if (!link) continue;
11714
11849
  activityIds.add(String(link.source));
11715
11850
  activityIds.add(String(link.target));
11851
+ if (operation.kind === DISPATCH_LINK_OPERATION_KIND.UPDATE) {
11852
+ if (operation.source !== void 0)
11853
+ activityIds.add(String(operation.source));
11854
+ if (operation.target !== void 0)
11855
+ activityIds.add(String(operation.target));
11856
+ }
11716
11857
  links.set(operation.linkId, { ...link });
11717
11858
  }
11718
11859
  return { activityIds, links };
@@ -11798,6 +11939,8 @@ function batchOpToLinkOperation(op, hoursPerDay) {
11798
11939
  return {
11799
11940
  kind: DISPATCH_LINK_OPERATION_KIND.UPDATE,
11800
11941
  linkId: op.linkId,
11942
+ source: op.source,
11943
+ target: op.target,
11801
11944
  type: op.type,
11802
11945
  lag: op.lag === void 0 ? void 0 : lagDaysToHours(op.lag, hoursPerDay)
11803
11946
  };
@@ -11821,8 +11964,11 @@ async function dispatchLinkBatch(operations, source, options, deps, deterministi
11821
11964
  if (operations.length === 1 && applied[0] && applied[0].rejected !== null && source.kind !== DISPATCH_ACTION_KIND.INLINE_EDIT) {
11822
11965
  return { ok: false, reason: applied[0].rejected };
11823
11966
  }
11967
+ if (!options.skipAutoSchedule) {
11968
+ snapFreedMilestones(applied, beforeLinks, adapter);
11969
+ }
11824
11970
  const triggerId = pickAutoscheduleTrigger(applied, beforeLinks, adapter);
11825
- const { scheduledIds } = await runPostMutation(
11971
+ const { scheduledIds, warnings } = await runPostMutation(
11826
11972
  {
11827
11973
  adapter,
11828
11974
  scheduler,
@@ -11860,10 +12006,32 @@ async function dispatchLinkBatch(operations, source, options, deps, deterministi
11860
12006
  touchedIds: affectedActivityIds,
11861
12007
  scheduledIds,
11862
12008
  links: linkChanges,
12009
+ warnings,
11863
12010
  hoursPerDay: sector.hoursPerDay
11864
12011
  })
11865
12012
  };
11866
12013
  }
12014
+ function snapFreedMilestones(applied, beforeLinks, adapter) {
12015
+ for (const operation of applied) {
12016
+ if (operation.rejected !== null || operation.op.kind !== DISPATCH_LINK_OPERATION_KIND.DELETE) {
12017
+ continue;
12018
+ }
12019
+ const targetId = beforeLinks.get(operation.op.linkId)?.target;
12020
+ if (!targetId || adapter.getIncomingLinkIds(targetId).length > 0) continue;
12021
+ const activity = adapter.getActivity(targetId);
12022
+ if (!activity || activity.type !== ACTIVITY_TYPE.MILESTONE) continue;
12023
+ const startDate = adapter.getClosestWorkTime({
12024
+ date: activity.startDate,
12025
+ dir: WORK_TIME_DIRECTION.FUTURE,
12026
+ task: activity
12027
+ });
12028
+ if (startDate.getTime() === activity.startDate.getTime()) continue;
12029
+ adapter.setActivityFields(targetId, {
12030
+ startDate,
12031
+ endDate: new Date(startDate)
12032
+ });
12033
+ }
12034
+ }
11867
12035
  function pickAutoscheduleTrigger(applied, beforeLinks, adapter) {
11868
12036
  for (const operation of applied) {
11869
12037
  if (operation.rejected !== null) continue;
@@ -11896,6 +12064,8 @@ function toLinkOperation(action, hoursPerDay) {
11896
12064
  return {
11897
12065
  kind: DISPATCH_LINK_OPERATION_KIND.UPDATE,
11898
12066
  linkId: action.linkId,
12067
+ source: action.source,
12068
+ target: action.target,
11899
12069
  type: action.type,
11900
12070
  lag: action.lag === void 0 ? void 0 : lagDaysToHours(action.lag, hoursPerDay)
11901
12071
  };
@@ -14827,6 +14997,158 @@ async function dispatchActivityOutdent(action, options, deps) {
14827
14997
  };
14828
14998
  }
14829
14999
 
15000
+ // src/dispatch/handlers/activity-type-set.ts
15001
+ async function dispatchActivityTypeSet(action, options, deps) {
15002
+ const { adapter, scheduler, sector } = deps;
15003
+ const activity = adapter.getActivity(action.activityId);
15004
+ if (!activity) {
15005
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
15006
+ }
15007
+ if (!isActivityType(action.type)) {
15008
+ return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
15009
+ }
15010
+ const children = adapter.getChildren(action.activityId);
15011
+ const targetIsProject = action.type === ACTIVITY_TYPE.PROJECT;
15012
+ if (targetIsProject !== children.length > 0) {
15013
+ return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
15014
+ }
15015
+ const allIds = new Set(adapter.getAllIds());
15016
+ const beforeSnap = await snapshotActivities(adapter, allIds);
15017
+ scheduler.invalidateAllCaches();
15018
+ if (targetIsProject && activity.type !== ACTIVITY_TYPE.PROJECT) {
15019
+ applyPromotion(action.activityId, children[0], deps);
15020
+ } else if (!targetIsProject && activity.type === ACTIVITY_TYPE.PROJECT) {
15021
+ applyDemotion(action.activityId, deps);
15022
+ }
15023
+ if (action.type !== ACTIVITY_TYPE.PROJECT) {
15024
+ applyLeafType(action.activityId, action.type, deps);
15025
+ ensureLeafCustomId(action.activityId, deps);
15026
+ } else {
15027
+ recomputeRollupCascadesForParent(action.activityId, adapter);
15028
+ }
15029
+ const beforeLinks = removeInvalidHierarchyLinks(action.activityId, deps);
15030
+ const { scheduledIds } = await runPostMutation(
15031
+ {
15032
+ adapter,
15033
+ scheduler,
15034
+ sector,
15035
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
15036
+ },
15037
+ {
15038
+ action,
15039
+ autoscheduleFrom: action.activityId,
15040
+ recomputeParentsFrom: [action.activityId],
15041
+ recomputeParentProgress: true,
15042
+ now: deps.now,
15043
+ options
15044
+ }
15045
+ );
15046
+ return {
15047
+ ok: true,
15048
+ changes: await assembleChangeSet(adapter, {
15049
+ source: action,
15050
+ beforeSnap,
15051
+ touchedIds: allIds,
15052
+ scheduledIds,
15053
+ sirDetection: SIR_DETECTION_PHASE.BEFORE_DIFF,
15054
+ links: buildLinkDeletions(beforeLinks),
15055
+ hoursPerDay: sector.hoursPerDay
15056
+ }),
15057
+ __beforeLinks: new Map(beforeLinks)
15058
+ };
15059
+ }
15060
+ function applyPromotion(activityId, childId, deps) {
15061
+ const activity = deps.adapter.getActivity(activityId);
15062
+ const oldCustomId = activity?.customId ?? null;
15063
+ const promotion = buildParentMutations({
15064
+ parentId: activityId,
15065
+ newChildId: childId,
15066
+ isCurrentlyLeaf: true,
15067
+ getParent: (id) => deps.adapter.getActivity(id) ?? null
15068
+ });
15069
+ if (!promotion) return;
15070
+ for (const [field, value] of Object.entries(promotion.fields)) {
15071
+ setActivityFieldDynamic(deps.adapter, activityId, field, value);
15072
+ }
15073
+ if (promotion.promotionSnapshot) {
15074
+ deps.adapter.setPromotionSnapshot(activityId, promotion.promotionSnapshot);
15075
+ }
15076
+ if (promotion.fields.customId === null) {
15077
+ deps.customIdTracker.releaseCustomId(oldCustomId);
15078
+ }
15079
+ }
15080
+ function applyDemotion(activityId, deps) {
15081
+ const activity = deps.adapter.getActivity(activityId);
15082
+ if (!activity) return;
15083
+ const demotion = buildParentDemotionMutations({
15084
+ parent: activity,
15085
+ remainingChildIds: [],
15086
+ defaultDurationHours: deps.sector.hoursPerDay,
15087
+ promotionSnapshot: deps.adapter.getPromotionSnapshot(activityId),
15088
+ computeEndDate: (startDate, durationHours) => deps.adapter.calculateEndDate({
15089
+ startDate,
15090
+ durationHours,
15091
+ task: activity
15092
+ })
15093
+ });
15094
+ if (!demotion) return;
15095
+ for (const [field, value] of Object.entries(demotion.fields)) {
15096
+ setActivityFieldDynamic(deps.adapter, activityId, field, value);
15097
+ }
15098
+ }
15099
+ function applyLeafType(activityId, type, deps) {
15100
+ const activity = deps.adapter.getActivity(activityId);
15101
+ if (!activity) return;
15102
+ deps.adapter.setActivityField(activityId, ACTIVITY_PROPERTY.TYPE, type);
15103
+ const durationHours = type === ACTIVITY_TYPE.MILESTONE ? 0 : activity.durationHours === 0 ? deps.sector.hoursPerDay : activity.durationHours;
15104
+ if (durationHours !== activity.durationHours) {
15105
+ deps.adapter.setActivityField(
15106
+ activityId,
15107
+ ACTIVITY_PROPERTY.DURATION_HOURS,
15108
+ durationHours
15109
+ );
15110
+ }
15111
+ const current = deps.adapter.getActivity(activityId);
15112
+ if (!current) return;
15113
+ const endDate = deps.adapter.calculateEndDate({
15114
+ startDate: current.startDate,
15115
+ durationHours,
15116
+ task: current
15117
+ });
15118
+ deps.adapter.setActivityField(
15119
+ activityId,
15120
+ ACTIVITY_PROPERTY.END_DATE,
15121
+ endDate
15122
+ );
15123
+ }
15124
+ function ensureLeafCustomId(activityId, deps) {
15125
+ const activity = deps.adapter.getActivity(activityId);
15126
+ if (activity?.customId) return;
15127
+ const previousSiblingId = findPrevSiblingHelper(activityId, deps.adapter);
15128
+ const previousCustomId = previousSiblingId ? deps.adapter.getActivity(previousSiblingId)?.customId : null;
15129
+ const generated = deps.customIdTracker.generateCustomIdForRestoredChild(
15130
+ previousCustomId || null
15131
+ );
15132
+ deps.adapter.setActivityField(
15133
+ activityId,
15134
+ ACTIVITY_PROPERTY.CUSTOM_ID,
15135
+ generated
15136
+ );
15137
+ }
15138
+ function removeInvalidHierarchyLinks(activityId, deps) {
15139
+ const beforeLinks = /* @__PURE__ */ new Map();
15140
+ for (const linkId of collectInvalidLinkIdsForCycle(
15141
+ [activityId],
15142
+ deps.adapter
15143
+ )) {
15144
+ const link = deps.adapter.getLink(linkId);
15145
+ if (!link) continue;
15146
+ beforeLinks.set(String(linkId), { ...link });
15147
+ deps.adapter.removeLink(linkId);
15148
+ }
15149
+ return beforeLinks;
15150
+ }
15151
+
14830
15152
  // src/dispatch/handlers/set-progress.ts
14831
15153
  async function dispatchActivitySetProgress(action, options, deps) {
14832
15154
  const { adapter, ctx, scheduler, sector } = deps;
@@ -16145,6 +16467,9 @@ async function performDispatch(action, options, deps) {
16145
16467
  if (action.kind === DISPATCH_ACTION_KIND.ACTIVITY_OUTDENT) {
16146
16468
  return dispatchActivityOutdent(action, options, deps);
16147
16469
  }
16470
+ if (action.kind === DISPATCH_ACTION_KIND.ACTIVITY_TYPE_SET) {
16471
+ return dispatchActivityTypeSet(action, options, deps);
16472
+ }
16148
16473
  if (action.kind === DISPATCH_ACTION_KIND.ACTIVITY_SET_PROGRESS) {
16149
16474
  return dispatchActivitySetProgress(action, options, deps);
16150
16475
  }
@@ -16181,6 +16506,9 @@ async function performDispatch(action, options, deps) {
16181
16506
  if (action.kind === DISPATCH_ACTION_KIND.ACTIVITY_LOOKAHEAD_CONVERT) {
16182
16507
  return dispatchActivityLookaheadConvert(action, options, deps);
16183
16508
  }
16509
+ if (action.kind === DISPATCH_ACTION_KIND.SCHEDULE_AUTO_SCHEDULE) {
16510
+ return dispatchScheduleAutoSchedule(action, options, deps);
16511
+ }
16184
16512
  if (action.kind === DISPATCH_ACTION_KIND.INLINE_EDIT && (action.column === COLUMN.CUSTOM_PREDECESSORS || action.column === COLUMN.CUSTOM_SUCESSORS)) {
16185
16513
  return dispatchInlineEditLinks(action, options, deps);
16186
16514
  }
@@ -19594,16 +19922,59 @@ var ScheduleState = class _ScheduleState {
19594
19922
  if (incoming)
19595
19923
  this._incoming.set(String(link.target), detachLinkRef(incoming, link.id));
19596
19924
  }
19925
+ _removeLinkRef(index, activityId, linkId) {
19926
+ const refs = index.get(activityId);
19927
+ if (refs) index.set(activityId, detachLinkRef(refs, linkId));
19928
+ }
19929
+ _insertLinkRefInOrder(index, activityId, linkId) {
19930
+ const precedingIds = /* @__PURE__ */ new Set();
19931
+ for (const existingId of this._links.keys()) {
19932
+ if (String(existingId) === String(linkId)) break;
19933
+ precedingIds.add(String(existingId));
19934
+ }
19935
+ const refs = [...index.get(activityId) ?? []];
19936
+ const insertionIndex = refs.findIndex(
19937
+ (existingId) => !precedingIds.has(String(existingId))
19938
+ );
19939
+ refs.splice(insertionIndex < 0 ? refs.length : insertionIndex, 0, linkId);
19940
+ index.set(activityId, refs);
19941
+ }
19597
19942
  setLinkField(linkId, field, value) {
19598
19943
  const link = this.getLink(linkId);
19599
19944
  if (!link) return;
19600
19945
  this._writeCapture.noteLinkField(String(linkId), link);
19601
19946
  link[field] = value;
19602
19947
  }
19948
+ setLinkEndpoints(linkId, source, target) {
19949
+ const link = this.getLink(linkId);
19950
+ if (!link) return;
19951
+ const sourceChanged = String(link.source) !== String(source);
19952
+ const targetChanged = String(link.target) !== String(target);
19953
+ if (!sourceChanged && !targetChanged) return;
19954
+ this._writeCapture.noteLinkField(String(linkId), link);
19955
+ if (sourceChanged) {
19956
+ this._removeLinkRef(this._outgoing, String(link.source), link.id);
19957
+ link.source = source;
19958
+ this._insertLinkRefInOrder(this._outgoing, String(source), link.id);
19959
+ }
19960
+ if (targetChanged) {
19961
+ this._removeLinkRef(this._incoming, String(link.target), link.id);
19962
+ link.target = target;
19963
+ this._insertLinkRefInOrder(this._incoming, String(target), link.id);
19964
+ }
19965
+ }
19603
19966
  setLinkFieldDynamic(linkId, field, value) {
19604
- if (!isMutableLinkField(field)) return;
19605
19967
  const link = this.getLink(linkId);
19606
19968
  if (!link) return;
19969
+ if (field === LINK_PROPERTY.SOURCE) {
19970
+ this.setLinkEndpoints(linkId, String(value), link.target);
19971
+ return;
19972
+ }
19973
+ if (field === LINK_PROPERTY.TARGET) {
19974
+ this.setLinkEndpoints(linkId, link.source, String(value));
19975
+ return;
19976
+ }
19977
+ if (!isMutableLinkField(field)) return;
19607
19978
  this._writeCapture.noteLinkField(String(linkId), link);
19608
19979
  Reflect.set(link, field, value);
19609
19980
  }
@@ -19724,10 +20095,16 @@ var ScheduleState = class _ScheduleState {
19724
20095
  }
19725
20096
  }
19726
20097
  _restoreLinkFields(ops) {
19727
- for (const op of ops)
19728
- if (op.kind === WRITE_OPERATION_KIND.LINK_FIELD && this._links.has(op.linkId)) {
19729
- this._links.set(op.linkId, op.before);
20098
+ for (const op of ops) {
20099
+ if (op.kind !== WRITE_OPERATION_KIND.LINK_FIELD) continue;
20100
+ const live = this._links.get(op.linkId);
20101
+ if (!live) continue;
20102
+ this.setLinkEndpoints(op.linkId, op.before.source, op.before.target);
20103
+ for (const key of Object.keys(live)) {
20104
+ if (!Object.hasOwn(op.before, key)) Reflect.deleteProperty(live, key);
19730
20105
  }
20106
+ Object.assign(live, op.before);
20107
+ }
19731
20108
  }
19732
20109
  _restoreActivityFields(before) {
19733
20110
  for (const [id, snapshot] of before) {
@@ -20198,18 +20575,23 @@ function applyChangeSetSide(state, entry, side) {
20198
20575
  for (const ch of cs.links) {
20199
20576
  if (ch.kind === ENTITY_CHANGE_KIND.UPDATED) {
20200
20577
  if (!ch.fields) continue;
20201
- if (Object.hasOwn(ch.fields, "source") || Object.hasOwn(ch.fields, "target")) {
20202
- const snapshot = side === CHANGE_SET_SIDE.BEFORE ? entry.beforeLinks?.get(String(ch.id)) : entry.afterLinks?.get(String(ch.id));
20203
- if (!snapshot) {
20204
- throw new Error(
20205
- `${side === CHANGE_SET_SIDE.BEFORE ? "undo" : "redo"}: missing link snapshot for ${ch.id}`
20206
- );
20207
- }
20208
- state.removeLink(String(ch.id));
20209
- state.addLink({ ...snapshot });
20210
- continue;
20578
+ const liveLink = state.getLink(String(ch.id));
20579
+ const sourceChange = ch.fields[LINK_PROPERTY.SOURCE];
20580
+ const targetChange = ch.fields[LINK_PROPERTY.TARGET];
20581
+ if (liveLink && (sourceChange || targetChange)) {
20582
+ state.setLinkEndpoints(
20583
+ String(ch.id),
20584
+ String(
20585
+ sourceChange ? side === CHANGE_SET_SIDE.BEFORE ? sourceChange.before : sourceChange.after : liveLink.source
20586
+ ),
20587
+ String(
20588
+ targetChange ? side === CHANGE_SET_SIDE.BEFORE ? targetChange.before : targetChange.after : liveLink.target
20589
+ )
20590
+ );
20211
20591
  }
20212
20592
  for (const [field, fv] of Object.entries(ch.fields)) {
20593
+ if (field === LINK_PROPERTY.SOURCE || field === LINK_PROPERTY.TARGET)
20594
+ continue;
20213
20595
  setLinkFieldDynamic(
20214
20596
  state,
20215
20597
  String(ch.id),
@@ -22772,6 +23154,15 @@ function evaluateEnd(formula, proposedEdgeTimeMs) {
22772
23154
 
22773
23155
  // src/init/schedule-core.ts
22774
23156
  var FORK_SOURCE = /* @__PURE__ */ Symbol("ScheduleCore.fork.source");
23157
+ function emptyChangeSet(source) {
23158
+ return {
23159
+ source,
23160
+ activities: [],
23161
+ links: [],
23162
+ calendars: [],
23163
+ trackingEvents: []
23164
+ };
23165
+ }
22775
23166
  function acknowledgesPersistence(action) {
22776
23167
  return action.kind === DISPATCH_ACTION_KIND.PERSISTENCE_ACKNOWLEDGE || action.kind === DISPATCH_ACTION_KIND.ACTIVITY_LOOKAHEAD_SYNC && action.acknowledgePersistedLookahead === true;
22777
23168
  }
@@ -23297,15 +23688,20 @@ var ScheduleCore = class _ScheduleCore {
23297
23688
  createProjectionChangeSetFrom(source) {
23298
23689
  this.assertReady();
23299
23690
  source.assertReady();
23300
- const projectionOf = (core) => ({
23301
- activities: core.getAllActivitiesView(),
23302
- links: core.getAllLinksView(),
23303
- calendars: core.getAllCalendars(),
23304
- selectedIds: core.getSelectedActivityIds(),
23305
- hiddenIds: core.getHiddenActivityIds(),
23306
- order: collectBranchOrder(core.coreRuntime.state)
23307
- });
23308
- return createProjectionChangeSet(projectionOf(this), projectionOf(source));
23691
+ return createProjectionChangeSet(
23692
+ this._snapshotProjection(),
23693
+ source._snapshotProjection()
23694
+ );
23695
+ }
23696
+ _snapshotProjection() {
23697
+ return {
23698
+ activities: this.getAllActivitiesView(),
23699
+ links: this.getAllLinksView(),
23700
+ calendars: this.getAllCalendars(),
23701
+ selectedIds: this.getSelectedActivityIds(),
23702
+ hiddenIds: this.getHiddenActivityIds(),
23703
+ order: collectBranchOrder(this.coreRuntime.state)
23704
+ };
23309
23705
  }
23310
23706
  /** @returns Deleted persisted activity identities since the last save. */
23311
23707
  getDeletedActivitiesSinceLastSave() {
@@ -23367,6 +23763,17 @@ var ScheduleCore = class _ScheduleCore {
23367
23763
  const admitted = this._beginScheduleMutation(action);
23368
23764
  return this._enqueue(() => this._dispatchInner(action, options, admitted));
23369
23765
  }
23766
+ /**
23767
+ * Applies commands in order and runs deferred scheduling finalization once.
23768
+ *
23769
+ * Rejected commands do not roll back earlier accepted commands.
23770
+ */
23771
+ async dispatchMany(actions, options = {}) {
23772
+ const admitted = actions.some(dispatchChangesSchedulingState) ? this._admitScheduleMutation() : false;
23773
+ return this._enqueue(
23774
+ () => this._dispatchManyInner(actions, options, admitted)
23775
+ );
23776
+ }
23370
23777
  /**
23371
23778
  * Serializes and applies an activity creation batch.
23372
23779
  *
@@ -23404,7 +23811,125 @@ var ScheduleCore = class _ScheduleCore {
23404
23811
  this._settleScheduleMutation(admitted, changedState);
23405
23812
  }
23406
23813
  }
23407
- async _runDispatch(action, options) {
23814
+ async _dispatchManyInner(actions, options, admitted) {
23815
+ const projectionBefore = this._snapshotProjection();
23816
+ const deferredOptions = {
23817
+ ...options,
23818
+ skipAutoSchedule: true,
23819
+ skipCriticalPath: true
23820
+ };
23821
+ const results = [];
23822
+ const acceptedChangeSets = [];
23823
+ const acceptedActions = [];
23824
+ let changedSchedulingState = false;
23825
+ let finalizationRequested = false;
23826
+ let scopedActivityId;
23827
+ let requiresRootFinalization = false;
23828
+ let finalizationChangeSet = null;
23829
+ let autoScheduled = false;
23830
+ let completed = false;
23831
+ try {
23832
+ for (const action of actions) {
23833
+ if (action.kind === DISPATCH_ACTION_KIND.SCHEDULE_AUTO_SCHEDULE) {
23834
+ if (action.activityId !== void 0 && !this.coreRuntime.state.activityExists(action.activityId)) {
23835
+ results.push({
23836
+ ok: false,
23837
+ reason: REJECTION_REASON.ACTIVITY_NOT_FOUND
23838
+ });
23839
+ continue;
23840
+ }
23841
+ const changes = emptyChangeSet(action);
23842
+ results.push({ ok: true, changes });
23843
+ acceptedChangeSets.push(changes);
23844
+ acceptedActions.push(action);
23845
+ finalizationRequested = true;
23846
+ if (action.activityId === void 0 || scopedActivityId !== void 0 && scopedActivityId !== action.activityId) {
23847
+ requiresRootFinalization = true;
23848
+ } else {
23849
+ scopedActivityId = action.activityId;
23850
+ }
23851
+ continue;
23852
+ }
23853
+ const dispatched = await this._runDispatch(
23854
+ action,
23855
+ deferredOptions,
23856
+ true
23857
+ );
23858
+ results.push(dispatched.result);
23859
+ if (dispatched.result.ok) {
23860
+ acceptedChangeSets.push(dispatched.result.changes);
23861
+ acceptedActions.push(action);
23862
+ }
23863
+ if (dispatched.changedState) {
23864
+ changedSchedulingState = true;
23865
+ finalizationRequested = true;
23866
+ requiresRootFinalization = true;
23867
+ }
23868
+ }
23869
+ if (finalizationRequested) {
23870
+ const finalizationAction = requiresRootFinalization || scopedActivityId === void 0 ? { kind: DISPATCH_ACTION_KIND.SCHEDULE_AUTO_SCHEDULE } : {
23871
+ kind: DISPATCH_ACTION_KIND.SCHEDULE_AUTO_SCHEDULE,
23872
+ activityId: scopedActivityId
23873
+ };
23874
+ const finalized = await this._runDispatch(
23875
+ finalizationAction,
23876
+ options,
23877
+ true
23878
+ );
23879
+ if (finalized.result.ok) {
23880
+ finalizationChangeSet = finalized.result.changes;
23881
+ acceptedChangeSets.push(finalized.result.changes);
23882
+ autoScheduled = options.skipAutoSchedule !== true;
23883
+ }
23884
+ changedSchedulingState ||= finalized.changedState;
23885
+ }
23886
+ if (changedSchedulingState) {
23887
+ this._recordScheduleMutation(
23888
+ finalizationChangeSet !== null && options.skipAutoSchedule !== true && options.skipCriticalPath !== true
23889
+ );
23890
+ }
23891
+ completed = true;
23892
+ return {
23893
+ results,
23894
+ autoScheduled,
23895
+ changeSet: this._aggregateDispatchManyChangeSets(
23896
+ projectionBefore,
23897
+ acceptedChangeSets,
23898
+ acceptedActions
23899
+ )
23900
+ };
23901
+ } finally {
23902
+ if (!completed && changedSchedulingState) {
23903
+ this._recordScheduleMutation(false);
23904
+ }
23905
+ this._settleScheduleMutation(
23906
+ admitted,
23907
+ completed && changedSchedulingState
23908
+ );
23909
+ }
23910
+ }
23911
+ _aggregateDispatchManyChangeSets(before, changeSets, acceptedActions) {
23912
+ if (changeSets.length === 0) return null;
23913
+ const aggregate = createProjectionChangeSet(
23914
+ this._snapshotProjection(),
23915
+ before
23916
+ );
23917
+ const effects = changeSets.flatMap((changes) => changes.effects ?? []);
23918
+ const warnings = changeSets.flatMap((changes) => changes.warnings ?? []);
23919
+ return {
23920
+ ...aggregate,
23921
+ source: {
23922
+ kind: CHANGE_SET_SOURCE_KIND.DISPATCH_MANY,
23923
+ actions: acceptedActions
23924
+ },
23925
+ trackingEvents: changeSets.flatMap(
23926
+ (changes) => changes.trackingEvents
23927
+ ),
23928
+ ...effects.length === 0 ? {} : { effects },
23929
+ ...warnings.length === 0 ? {} : { warnings }
23930
+ };
23931
+ }
23932
+ async _runDispatch(action, options, deferScheduleRevision = false) {
23408
23933
  this.assertReady();
23409
23934
  if (this._isFork && acknowledgesPersistence(action)) {
23410
23935
  return {
@@ -23533,9 +24058,11 @@ var ScheduleCore = class _ScheduleCore {
23533
24058
  const hasSubstantiveChanges = changeSetIsSubstantive(result.changes);
23534
24059
  if (hasSubstantiveChanges) this._stateRevision++;
23535
24060
  if (dispatchChangesSchedulingState(action) && hasSubstantiveChanges) {
23536
- this._recordScheduleMutation(
23537
- options.skipAutoSchedule !== true && options.skipCriticalPath !== true && resolveRunCriticalPath(action)
23538
- );
24061
+ if (!deferScheduleRevision) {
24062
+ this._recordScheduleMutation(
24063
+ options.skipAutoSchedule !== true && options.skipCriticalPath !== true && resolveRunCriticalPath(action)
24064
+ );
24065
+ }
23539
24066
  return { result: toPublicDispatchResult(result), changedState: true };
23540
24067
  }
23541
24068
  return { result: toPublicDispatchResult(result), changedState: false };