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