@outbuild-company/schedule-core 1.9.1 → 1.9.3

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
@@ -3289,6 +3289,84 @@ function roundProgressPerLevel(value) {
3289
3289
  return Number(value.toFixed(2));
3290
3290
  }
3291
3291
 
3292
+ // src/propagations/upward/progress-rollup.ts
3293
+ function recomputeAllProgressRollup(adapter) {
3294
+ for (const parentId of parentsDeepestFirst(adapter)) {
3295
+ rollupParent(parentId, adapter);
3296
+ }
3297
+ }
3298
+ function recomputeProgressRollupForParents(parentIdsDeepestFirst, adapter) {
3299
+ for (const parentId of parentIdsDeepestFirst) {
3300
+ rollupParent(parentId, adapter);
3301
+ }
3302
+ }
3303
+ function parentsDeepestFirst(adapter) {
3304
+ const parents = /* @__PURE__ */ new Set();
3305
+ adapter.forEachActivity((_activity, id) => {
3306
+ if (adapter.getChildren(id).length > 0) parents.add(id);
3307
+ });
3308
+ const depthById = /* @__PURE__ */ new Map();
3309
+ for (const id of parents) depthById.set(id, depthOf(id, adapter));
3310
+ return [...parents].sort(
3311
+ (a, b) => (depthById.get(b) ?? 0) - (depthById.get(a) ?? 0)
3312
+ );
3313
+ }
3314
+ function depthOf(id, adapter) {
3315
+ let depth = 0;
3316
+ let current = id;
3317
+ while (current && current !== "0") {
3318
+ if (!adapter.getActivity(current)) break;
3319
+ depth += 1;
3320
+ current = adapter.getParentId(current);
3321
+ }
3322
+ return depth;
3323
+ }
3324
+ function rollupParent(parentId, adapter) {
3325
+ const childIds = adapter.getChildren(parentId);
3326
+ if (childIds.length === 0) return;
3327
+ const children = [];
3328
+ for (const childId of childIds) {
3329
+ const child = adapter.getActivity(childId);
3330
+ if (!child) continue;
3331
+ children.push({
3332
+ progress: Number(child.progress ?? 0),
3333
+ ponderator: Number(child.ponderator ?? 0)
3334
+ });
3335
+ }
3336
+ applyWeightedProgressRollup(parentId, children, adapter);
3337
+ }
3338
+ function applyWeightedProgressRollup(parentId, children, adapter) {
3339
+ const parent = adapter.getActivity(parentId);
3340
+ if (!parent) return;
3341
+ const rollup = computeWeightedProgressRollup(children);
3342
+ if (rollup === null) {
3343
+ const fallback = adapter.getProgressFallback(parentId);
3344
+ if (fallback !== null) {
3345
+ adapter.setActivityField(parentId, COLUMN.PROGRESS, fallback);
3346
+ }
3347
+ return;
3348
+ }
3349
+ if (adapter.getProgressFallback(parentId) === null) {
3350
+ adapter.setProgressFallback(parentId, Number(parent.progress ?? 0));
3351
+ }
3352
+ adapter.setActivityField(
3353
+ parentId,
3354
+ COLUMN.PROGRESS,
3355
+ roundProgressPerLevel(rollup)
3356
+ );
3357
+ }
3358
+ function setExplicitSummaryProgressFallbacks(activityId, progress, adapter) {
3359
+ const pending = [activityId];
3360
+ while (pending.length > 0) {
3361
+ const currentId = pending.pop();
3362
+ if (currentId === void 0) continue;
3363
+ const children = adapter.getChildren(currentId);
3364
+ if (children.length === 0) continue;
3365
+ adapter.setProgressFallback(currentId, progress);
3366
+ pending.push(...children);
3367
+ }
3368
+ }
3369
+
3292
3370
  // src/columns/real-work/compute.ts
3293
3371
  function computeLeafRealWork(workHours, progress) {
3294
3372
  return (Number(workHours) || 0) * (Number(progress) || 0) / 100;
@@ -3404,16 +3482,8 @@ function recomputeParentFromChildren(parentId, adapter, progressMode = "freeze-w
3404
3482
  adapter.setActivityField(parentId, "startDate", newStart);
3405
3483
  adapter.setActivityField(parentId, "endDate", newEnd);
3406
3484
  adapter.setActivityField(parentId, "durationHours", durationHours);
3407
- const progressRollup = computeWeightedProgressRollup(
3408
- aggregate.weightedChildren
3409
- );
3410
- const shouldWriteProgress = progressMode !== "skip" && (progressRollup !== null || progressMode === "zero-when-no-contribution");
3411
- if (shouldWriteProgress) {
3412
- adapter.setActivityField(
3413
- parentId,
3414
- "progress",
3415
- roundProgressPerLevel(progressRollup ?? 0)
3416
- );
3485
+ if (progressMode !== "skip") {
3486
+ applyWeightedProgressRollup(parentId, aggregate.weightedChildren, adapter);
3417
3487
  }
3418
3488
  if (recomputeRealWork) {
3419
3489
  adapter.setActivityField(parentId, "realWorkHours", aggregate.realWorkSum);
@@ -3436,10 +3506,10 @@ function collectParentIds(adapter) {
3436
3506
  }
3437
3507
  function computeDepths(ids, adapter) {
3438
3508
  const depths = /* @__PURE__ */ new Map();
3439
- for (const id of ids) depths.set(id, depthOf(id, adapter));
3509
+ for (const id of ids) depths.set(id, depthOf2(id, adapter));
3440
3510
  return depths;
3441
3511
  }
3442
- function depthOf(id, adapter) {
3512
+ function depthOf2(id, adapter) {
3443
3513
  let depth = 0;
3444
3514
  let current = id;
3445
3515
  while (current && !isRootParent(current)) {
@@ -3922,6 +3992,7 @@ function buildAncestorRollupCascade(activity, newValue, hierarchy) {
3922
3992
  const mutations = [];
3923
3993
  for (const id of chain) {
3924
3994
  const rollup = computeWeightedProgress(id, overlay, hierarchy);
3995
+ if (rollup === null) continue;
3925
3996
  overlay.set(id, rollup);
3926
3997
  mutations.push({
3927
3998
  activityId: id,
@@ -3935,7 +4006,8 @@ function computeWeightedProgress(parentId, overlay, hierarchy) {
3935
4006
  progress: overlay.has(child.id) ? overlay.get(child.id) : child.progress,
3936
4007
  ponderator: child.ponderator ?? 0
3937
4008
  }));
3938
- return Number((computeWeightedProgressRollup(children) ?? 0).toFixed(2));
4009
+ const rollup = computeWeightedProgressRollup(children);
4010
+ return rollup === null ? null : Number(rollup.toFixed(2));
3939
4011
  }
3940
4012
  function buildRecursiveTrackingEvents(activity, newValue) {
3941
4013
  return [
@@ -9886,24 +9958,82 @@ function resolveDerivedPasses(action, autoSchedule, options) {
9886
9958
 
9887
9959
  // src/dispatch/shared/post-mutation.ts
9888
9960
  async function runPostMutation(deps, args) {
9889
- const passes = resolvePasses(args);
9961
+ const progressInvalidation = resolveProgressInvalidation(deps.adapter);
9962
+ const passes = resolvePasses(args, progressInvalidation);
9890
9963
  if (passes.has(DERIVED_PASS.AUTOSCHEDULE)) {
9891
9964
  await refreshMutatedParentBounds(deps.adapter, args);
9892
9965
  }
9893
9966
  const schedule = passes.has(DERIVED_PASS.AUTOSCHEDULE) ? await runAutoschedule(deps, args.autoscheduleFrom) : emptySchedule();
9894
- await rollUpParentBounds(deps.adapter, args, schedule.scheduledIds, passes);
9967
+ await rollUpParentBounds(
9968
+ deps.adapter,
9969
+ args,
9970
+ schedule.scheduledIds,
9971
+ passes,
9972
+ progressInvalidation
9973
+ );
9895
9974
  await runDerivedRecomputes(deps, passes, args.now);
9896
9975
  return schedule;
9897
9976
  }
9898
- function resolvePasses(args) {
9977
+ function resolvePasses(args, progressInvalidation) {
9899
9978
  const requested = args.autoscheduleFrom !== null;
9900
9979
  const ran = !args.options.skipAutoSchedule && requested;
9901
9980
  const passes = new Set(
9902
9981
  resolveDerivedPasses(args.action, { ran }, args.options)
9903
9982
  );
9904
- if (args.recomputeParentProgress) passes.add(DERIVED_PASS.PARENT_PROGRESS);
9983
+ if (args.recomputeParentProgress || progressInvalidation.affectedParents.size > 0) {
9984
+ passes.add(DERIVED_PASS.PARENT_PROGRESS);
9985
+ }
9905
9986
  return passes;
9906
9987
  }
9988
+ function resolveProgressInvalidation(adapter) {
9989
+ const affectedParents = /* @__PURE__ */ new Set();
9990
+ const dirtySeeds = /* @__PURE__ */ new Set();
9991
+ const journal = adapter.peekWriteCapture();
9992
+ if (!journal) return { affectedParents, dirtySeeds };
9993
+ const affectParent = (parentId) => {
9994
+ if (parentId === null || isRootParent(parentId)) return;
9995
+ const normalized2 = String(parentId);
9996
+ affectedParents.add(normalized2);
9997
+ const childId = adapter.getChildren(normalized2)[0];
9998
+ if (childId !== void 0) dirtySeeds.add(childId);
9999
+ };
10000
+ collectStructuralProgressInvalidations(adapter, journal.ops, affectParent);
10001
+ collectFieldProgressInvalidations(
10002
+ adapter,
10003
+ journal.before,
10004
+ dirtySeeds,
10005
+ affectParent
10006
+ );
10007
+ return { affectedParents, dirtySeeds };
10008
+ }
10009
+ function collectStructuralProgressInvalidations(adapter, operations, affectParent) {
10010
+ for (const operation of operations) {
10011
+ if (operation.kind === "insert") {
10012
+ const inserted = adapter.getActivity(operation.activityId);
10013
+ if (inserted) affectParent(inserted.parentId);
10014
+ } else if (operation.kind === "remove") {
10015
+ affectParent(operation.before.parentId);
10016
+ }
10017
+ }
10018
+ }
10019
+ function collectFieldProgressInvalidations(adapter, beforeById, dirtySeeds, affectParent) {
10020
+ for (const [activityId, before] of beforeById) {
10021
+ const after = adapter.getActivity(activityId);
10022
+ if (!after) continue;
10023
+ const progressChanged = !Object.is(before.progress, after.progress);
10024
+ const ponderatorChanged = !Object.is(before.ponderator, after.ponderator);
10025
+ const parentChanged = !Object.is(before.parentId, after.parentId);
10026
+ if (progressChanged || ponderatorChanged) {
10027
+ affectParent(after.parentId);
10028
+ dirtySeeds.add(String(activityId));
10029
+ }
10030
+ if (parentChanged) {
10031
+ affectParent(before.parentId);
10032
+ affectParent(after.parentId);
10033
+ dirtySeeds.add(String(activityId));
10034
+ }
10035
+ }
10036
+ }
9907
10037
  async function runAutoschedule(deps, autoscheduleFrom) {
9908
10038
  const options = autoscheduleFrom === "roots" ? {} : { triggerId: autoscheduleFrom };
9909
10039
  const result = await deps.scheduler.schedule(options);
@@ -9931,18 +10061,36 @@ async function refreshMutatedParentBounds(adapter, args) {
9931
10061
  if (mutated.size === 0) return;
9932
10062
  await updateParentBoundsFromChildren(adapter, mutated, false);
9933
10063
  }
9934
- async function rollUpParentBounds(adapter, args, scheduledIds, passes) {
10064
+ async function rollUpParentBounds(adapter, args, scheduledIds, passes, progressInvalidation) {
10065
+ if (passes.has(DERIVED_PASS.PARENT_PROGRESS)) {
10066
+ restoreDemotedProgressFallbacks(
10067
+ adapter,
10068
+ progressInvalidation.affectedParents
10069
+ );
10070
+ }
9935
10071
  const dirty = args.collectDirty ? args.collectDirty(scheduledIds) : collectDirtyForStructural(args.recomputeParentsFrom, scheduledIds);
10072
+ for (const activityId of progressInvalidation.dirtySeeds) {
10073
+ dirty.add(activityId);
10074
+ }
9936
10075
  await updateParentBoundsFromChildren(
9937
10076
  adapter,
9938
10077
  dirty,
9939
10078
  passes.has(DERIVED_PASS.PARENT_PROGRESS),
9940
10079
  {
9941
10080
  recomputeRealWork: passes.has(DERIVED_PASS.REAL_WORK),
9942
- progressMode: args.recomputeParentProgress ? "zero-when-no-contribution" : "freeze-when-no-contribution"
10081
+ progressMode: "freeze-when-no-contribution"
9943
10082
  }
9944
10083
  );
9945
10084
  }
10085
+ function restoreDemotedProgressFallbacks(adapter, affectedParents) {
10086
+ for (const parentId of affectedParents) {
10087
+ if (adapter.hasChildren(parentId)) continue;
10088
+ const fallback = adapter.getProgressFallback(parentId);
10089
+ if (fallback === null) continue;
10090
+ adapter.setActivityField(parentId, "progress", fallback);
10091
+ adapter.setProgressFallback(parentId, null);
10092
+ }
10093
+ }
9946
10094
  async function runDerivedRecomputes(deps, passes, now) {
9947
10095
  if (passes.has(DERIVED_PASS.EXPECTED_PROGRESS) && now !== null) {
9948
10096
  if (hasActiveBaseline(deps.adapter)) {
@@ -11171,7 +11319,6 @@ function buildParentMutations(input) {
11171
11319
  if ((input.promotionSource ?? "create") === "create") {
11172
11320
  fields.constraintType = CONSTRAINT_TYPE.ASAP;
11173
11321
  fields.constraintDate = null;
11174
- fields.progress = 0;
11175
11322
  }
11176
11323
  }
11177
11324
  return {
@@ -12324,60 +12471,6 @@ function dispatchSirSync(action, deps) {
12324
12471
  };
12325
12472
  }
12326
12473
 
12327
- // src/propagations/upward/progress-rollup.ts
12328
- function recomputeAllProgressRollup(adapter) {
12329
- for (const parentId of parentsDeepestFirst(adapter)) {
12330
- rollupParent(parentId, adapter);
12331
- }
12332
- }
12333
- function recomputeProgressRollupForParents(parentIdsDeepestFirst, adapter) {
12334
- for (const parentId of parentIdsDeepestFirst) {
12335
- rollupParent(parentId, adapter);
12336
- }
12337
- }
12338
- function parentsDeepestFirst(adapter) {
12339
- const parents = /* @__PURE__ */ new Set();
12340
- adapter.forEachActivity((_activity, id) => {
12341
- if (adapter.getChildren(id).length > 0) parents.add(id);
12342
- });
12343
- const depthById = /* @__PURE__ */ new Map();
12344
- for (const id of parents) depthById.set(id, depthOf2(id, adapter));
12345
- return [...parents].sort(
12346
- (a, b) => (depthById.get(b) ?? 0) - (depthById.get(a) ?? 0)
12347
- );
12348
- }
12349
- function depthOf2(id, adapter) {
12350
- let depth = 0;
12351
- let current = id;
12352
- while (current && current !== "0") {
12353
- if (!adapter.getActivity(current)) break;
12354
- depth += 1;
12355
- current = adapter.getParentId(current);
12356
- }
12357
- return depth;
12358
- }
12359
- function rollupParent(parentId, adapter) {
12360
- const childIds = adapter.getChildren(parentId);
12361
- if (childIds.length === 0) return;
12362
- const children = [];
12363
- for (const childId of childIds) {
12364
- const child = adapter.getActivity(childId);
12365
- if (!child) continue;
12366
- children.push({
12367
- progress: Number(child.progress ?? 0),
12368
- ponderator: Number(child.ponderator ?? 0)
12369
- });
12370
- }
12371
- const rollup = computeWeightedProgressRollup(children);
12372
- if (rollup !== null) {
12373
- adapter.setActivityField(
12374
- parentId,
12375
- COLUMN.PROGRESS,
12376
- roundProgressPerLevel(rollup)
12377
- );
12378
- }
12379
- }
12380
-
12381
12474
  // src/dispatch/handlers/persistence-acknowledge.ts
12382
12475
  function persistedOwnersOf(entities) {
12383
12476
  const owners = /* @__PURE__ */ new Map();
@@ -12450,28 +12543,6 @@ function assertValidAssignments(assignments, entityName, readCurrent, ownerByBac
12450
12543
  );
12451
12544
  }
12452
12545
  }
12453
- function assertValidReconciledProgress(entries, deps) {
12454
- const seen = /* @__PURE__ */ new Set();
12455
- for (const entry of entries) {
12456
- const id = String(entry.activityId);
12457
- if (id.length === 0 || seen.has(id)) {
12458
- throw new Error(
12459
- `persistence-acknowledge: duplicate or empty reconciled activity id "${id}"`
12460
- );
12461
- }
12462
- if (!Number.isFinite(entry.progress) || entry.progress < 0 || entry.progress > 100) {
12463
- throw new Error(
12464
- `persistence-acknowledge: invalid reconciled progress ${String(entry.progress)} for "${id}"`
12465
- );
12466
- }
12467
- if (!deps.adapter.getActivity(id)) {
12468
- throw new Error(
12469
- `persistence-acknowledge: unknown reconciled activity id "${id}"`
12470
- );
12471
- }
12472
- seen.add(id);
12473
- }
12474
- }
12475
12546
  async function applyReconciledProgress(entries, deps) {
12476
12547
  const progressToApply = entries.filter((entry) => {
12477
12548
  const current = deps.adapter.getActivity(String(entry.activityId));
@@ -12502,19 +12573,25 @@ async function applyReconciledProgress(entries, deps) {
12502
12573
  async function dispatchPersistenceAcknowledge(action, deps) {
12503
12574
  const activities = action.activities ?? [];
12504
12575
  const links = action.links ?? [];
12505
- const reconciledProgress = action.reconciledProgress ?? [];
12506
- assertValidReconciledProgress(reconciledProgress, deps);
12507
12576
  const currentActivities = deps.adapter.getAllActivities();
12508
12577
  const currentLinks = deps.adapter.getAllLinks();
12509
- const activityOwnerByBackendId = persistedOwnersOf(currentActivities);
12510
- const linkOwnerByBackendId = persistedOwnersOf(currentLinks);
12578
+ const checkpointActivities = action.checkpoint?.activities ?? currentActivities;
12579
+ const checkpointLinks = action.checkpoint?.links ?? currentLinks;
12580
+ const checkpointActivityById = new Map(
12581
+ checkpointActivities.map((activity) => [String(activity.id), activity])
12582
+ );
12583
+ const checkpointLinkById = new Map(
12584
+ checkpointLinks.map((link) => [String(link.id), link])
12585
+ );
12586
+ const activityOwnerByBackendId = persistedOwnersOf(checkpointActivities);
12587
+ const linkOwnerByBackendId = persistedOwnersOf(checkpointLinks);
12511
12588
  assertValidAssignments(
12512
12589
  activities,
12513
12590
  "activity",
12514
12591
  (id) => {
12515
- const activity = deps.adapter.getActivity(id);
12592
+ const activity = checkpointActivityById.get(id);
12516
12593
  return {
12517
- exists: activity !== null,
12594
+ exists: activity !== void 0,
12518
12595
  current: activity?.proplannerId ?? null
12519
12596
  };
12520
12597
  },
@@ -12524,17 +12601,21 @@ async function dispatchPersistenceAcknowledge(action, deps) {
12524
12601
  links,
12525
12602
  "link",
12526
12603
  (id) => {
12527
- const link = deps.adapter.getLink(id);
12528
- return { exists: link !== null, current: link?.proplannerId ?? null };
12604
+ const link = checkpointLinkById.get(id);
12605
+ return {
12606
+ exists: link !== void 0,
12607
+ current: link?.proplannerId ?? null
12608
+ };
12529
12609
  },
12530
12610
  linkOwnerByBackendId
12531
12611
  );
12532
- assertAllNewEntitiesAcknowledged(currentActivities, activities, "activity");
12533
- assertAllNewEntitiesAcknowledged(currentLinks, links, "link");
12534
- const activityChanges = await applyReconciledProgress(
12535
- reconciledProgress,
12536
- deps
12612
+ assertAllNewEntitiesAcknowledged(
12613
+ checkpointActivities,
12614
+ activities,
12615
+ "activity"
12537
12616
  );
12617
+ assertAllNewEntitiesAcknowledged(checkpointLinks, links, "link");
12618
+ const activityChanges = [];
12538
12619
  const linkChanges = [];
12539
12620
  deps.adapter.batchUpdate(() => {
12540
12621
  for (const assignment of activities) {
@@ -13444,6 +13525,11 @@ async function dispatchActivitySetProgress(action, options, deps) {
13444
13525
  const touchedIds = collectTouchedIds(action.activityId, changes);
13445
13526
  const beforeSnap = await snapshotActivities(adapter, touchedIds);
13446
13527
  applyFieldChanges(adapter, action.activityId, changes);
13528
+ setExplicitSummaryProgressFallbacks(
13529
+ action.activityId,
13530
+ action.newValue,
13531
+ adapter
13532
+ );
13447
13533
  runPostProcessorsOnAdapter(
13448
13534
  action.activityId,
13449
13535
  changes.postProcessors ?? [],
@@ -17307,7 +17393,8 @@ var WriteCapture = class {
17307
17393
  ops: [],
17308
17394
  linkFieldCaptured: /* @__PURE__ */ new Set(),
17309
17395
  correlativeBefore: /* @__PURE__ */ new Map(),
17310
- scheduledBefore: /* @__PURE__ */ new Map()
17396
+ scheduledBefore: /* @__PURE__ */ new Map(),
17397
+ progressFallbackBefore: /* @__PURE__ */ new Map()
17311
17398
  };
17312
17399
  }
17313
17400
  peek() {
@@ -17370,6 +17457,11 @@ var WriteCapture = class {
17370
17457
  before: cloneLink(current)
17371
17458
  });
17372
17459
  }
17460
+ noteProgressFallback(activityId, current) {
17461
+ const journal = this._journal;
17462
+ if (!journal || journal.progressFallbackBefore.has(activityId)) return;
17463
+ journal.progressFallbackBefore.set(activityId, current);
17464
+ }
17373
17465
  };
17374
17466
  function cloneLink(link) {
17375
17467
  return { ...link };
@@ -17615,6 +17707,7 @@ var ScheduleState = class _ScheduleState {
17615
17707
  _activities = /* @__PURE__ */ new Map();
17616
17708
  _lastStartDate = /* @__PURE__ */ new Map();
17617
17709
  _promotionSnapshot = /* @__PURE__ */ new Map();
17710
+ _progressFallback = /* @__PURE__ */ new Map();
17618
17711
  _links = /* @__PURE__ */ new Map();
17619
17712
  _outgoing = /* @__PURE__ */ new Map();
17620
17713
  _incoming = /* @__PURE__ */ new Map();
@@ -17675,6 +17768,7 @@ var ScheduleState = class _ScheduleState {
17675
17768
  cloneDomainValue(snapshot)
17676
17769
  ])
17677
17770
  );
17771
+ fork._progressFallback = new Map(this._progressFallback);
17678
17772
  fork._flags = cloneDomainValue(this._flags);
17679
17773
  fork._filterProjection = cloneDomainValue(this._filterProjection);
17680
17774
  return fork;
@@ -17728,6 +17822,17 @@ var ScheduleState = class _ScheduleState {
17728
17822
  this._capturePromotionSnapshotOnce();
17729
17823
  this._promotionSnapshot.set(String(activityId), snapshot);
17730
17824
  }
17825
+ getProgressFallback(activityId) {
17826
+ return this._progressFallback.get(String(activityId)) ?? null;
17827
+ }
17828
+ setProgressFallback(activityId, value) {
17829
+ const key = String(activityId);
17830
+ const current = this._progressFallback.get(key) ?? null;
17831
+ if (Object.is(current, value)) return;
17832
+ this._writeCapture.noteProgressFallback(key, current);
17833
+ if (value === null) this._progressFallback.delete(key);
17834
+ else this._progressFallback.set(key, value);
17835
+ }
17731
17836
  _capturePromotionSnapshotOnce() {
17732
17837
  if (this._writeCapture.peek() === null) return;
17733
17838
  if (this._promotionSnapshotBefore === null) {
@@ -17978,6 +18083,7 @@ var ScheduleState = class _ScheduleState {
17978
18083
  const activity = this.getActivity(activityId);
17979
18084
  if (activity) this._writeCapture.noteRemove(String(activityId), activity);
17980
18085
  const key = String(activityId);
18086
+ this.setProgressFallback(key, null);
17981
18087
  if (this._viewState.isChecked(key) || !this._viewState.isVisible(key)) {
17982
18088
  this._captureViewStateOnce();
17983
18089
  }
@@ -18002,7 +18108,13 @@ var ScheduleState = class _ScheduleState {
18002
18108
  restoreFromCapture() {
18003
18109
  const journal = this._writeCapture.peek();
18004
18110
  if (!journal) return;
18005
- const { ops, before, correlativeBefore, scheduledBefore } = journal;
18111
+ const {
18112
+ ops,
18113
+ before,
18114
+ correlativeBefore,
18115
+ scheduledBefore,
18116
+ progressFallbackBefore
18117
+ } = journal;
18006
18118
  this._undoLinkAdds(ops);
18007
18119
  this._undoInserts(ops);
18008
18120
  this._reInsertRemovedActivities(ops);
@@ -18011,6 +18123,7 @@ var ScheduleState = class _ScheduleState {
18011
18123
  this._restoreCorrelatives(correlativeBefore);
18012
18124
  this._restoreActivityFields(scheduledBefore);
18013
18125
  this._restoreActivityFields(before);
18126
+ this._restoreProgressFallbacks(progressFallbackBefore);
18014
18127
  if (this._viewStateBefore !== null) {
18015
18128
  this._viewState.restore(this._viewStateBefore);
18016
18129
  this._filterProjection = null;
@@ -18026,6 +18139,12 @@ var ScheduleState = class _ScheduleState {
18026
18139
  this._cache.visualOrderIds = null;
18027
18140
  this._hierarchy.markDirty();
18028
18141
  }
18142
+ _restoreProgressFallbacks(before) {
18143
+ for (const [activityId, value] of before) {
18144
+ if (value === null) this._progressFallback.delete(activityId);
18145
+ else this._progressFallback.set(activityId, value);
18146
+ }
18147
+ }
18029
18148
  _restoreCorrelatives(correlativeBefore) {
18030
18149
  for (const [id, before] of correlativeBefore) {
18031
18150
  const live = this._activities.get(id);
@@ -18239,13 +18358,15 @@ function buildHistoryChangeSet(changes) {
18239
18358
  links: linksWithoutAfter
18240
18359
  });
18241
18360
  }
18242
- function buildUndoEntry(changeSet, beforeSnap, beforeLinks, afterSnap, afterLinks) {
18361
+ function buildUndoEntry(changeSet, beforeSnap, beforeLinks, afterSnap, afterLinks, progressFallbackBefore, progressFallbackAfter) {
18243
18362
  return {
18244
18363
  changeSet,
18245
18364
  beforeSnap,
18246
18365
  beforeLinks: resetDerivedLinkSnapshots(beforeLinks),
18247
18366
  afterSnap,
18248
- afterLinks: resetDerivedLinkSnapshots(afterLinks)
18367
+ afterLinks: resetDerivedLinkSnapshots(afterLinks),
18368
+ progressFallbackBefore,
18369
+ progressFallbackAfter
18249
18370
  };
18250
18371
  }
18251
18372
  function resetDerivedLinkSnapshots(snapshots) {
@@ -18308,7 +18429,9 @@ function mergeCoalesced(top, next) {
18308
18429
  return buildUndoEntry(changeSet);
18309
18430
  }
18310
18431
  function getDispatchHistoryPolicy(action) {
18311
- if (action.kind === "persistence-acknowledge") return "clear-on-success";
18432
+ if (action.kind === "persistence-acknowledge") {
18433
+ return "checkpoint-on-success";
18434
+ }
18312
18435
  if (action.kind === "sir-sync" || action.kind === "activity-lookahead-sync" || action.kind === "baseline-apply" || action.kind === "ponderator-criterion-set" || action.kind === "status-criteria-set" || action.kind === "activity-clipboard-clear" || action.kind === "activity-copy" || action.kind === "selection-toggle" || action.kind === "selection-replace" || action.kind === "visibility-set" || action.kind === "filter-set" || action.kind === "sort-set") {
18313
18436
  return "skip";
18314
18437
  }
@@ -18548,6 +18671,10 @@ function applyChangeSetSide(state, entry, side) {
18548
18671
  );
18549
18672
  }
18550
18673
  }
18674
+ const progressFallbacks = side === "before" ? entry.progressFallbackBefore : entry.progressFallbackAfter;
18675
+ for (const [activityId, value] of progressFallbacks ?? []) {
18676
+ state.setProgressFallback(activityId, value);
18677
+ }
18551
18678
  }
18552
18679
 
18553
18680
  // src/dispatch/undo/undo-recorder.ts
@@ -18560,22 +18687,25 @@ var UndoRecorder = class _UndoRecorder {
18560
18687
  maxDepth;
18561
18688
  undoStack = [];
18562
18689
  redoStack = [];
18690
+ sequence = 0;
18563
18691
  _lastKey = null;
18564
18692
  _lastTime = 0;
18565
18693
  fork() {
18566
18694
  const fork = new _UndoRecorder(this.maxDepth);
18567
18695
  fork.undoStack.push(...cloneDomainValue(this.undoStack));
18568
18696
  fork.redoStack.push(...cloneDomainValue(this.redoStack));
18697
+ fork.sequence = this.sequence;
18569
18698
  fork._lastKey = this._lastKey;
18570
18699
  fork._lastTime = this._lastTime;
18571
18700
  return fork;
18572
18701
  }
18573
18702
  record(entry, coalesceKey = null, now = 0) {
18574
18703
  const top = this.undoStack[this.undoStack.length - 1];
18575
- if (coalesceKey != null && coalesceKey === this._lastKey && now - this._lastTime <= COALESCE_WINDOW_MS && top !== void 0 && canCoalesce(top, entry)) {
18576
- this.undoStack[this.undoStack.length - 1] = mergeCoalesced(top, entry);
18704
+ if (coalesceKey != null && coalesceKey === this._lastKey && now - this._lastTime <= COALESCE_WINDOW_MS && top !== void 0 && canCoalesce(top.entry, entry)) {
18705
+ top.entry = mergeCoalesced(top.entry, entry);
18706
+ top.touchedAt = this.touch();
18577
18707
  } else {
18578
- this.undoStack.push(entry);
18708
+ this.undoStack.push({ entry, touchedAt: this.touch() });
18579
18709
  if (this.undoStack.length > this.maxDepth) {
18580
18710
  this.undoStack.splice(0, this.undoStack.length - this.maxDepth);
18581
18711
  }
@@ -18588,16 +18718,16 @@ var UndoRecorder = class _UndoRecorder {
18588
18718
  this._lastKey = null;
18589
18719
  }
18590
18720
  takeUndo() {
18591
- return this.undoStack.pop() ?? null;
18721
+ return this.undoStack.pop()?.entry ?? null;
18592
18722
  }
18593
18723
  takeRedo() {
18594
- return this.redoStack.pop() ?? null;
18724
+ return this.redoStack.pop()?.entry ?? null;
18595
18725
  }
18596
18726
  pushRedo(entry) {
18597
- this.redoStack.push(entry);
18727
+ this.redoStack.push({ entry, touchedAt: this.touch() });
18598
18728
  }
18599
18729
  pushUndo(entry) {
18600
- this.undoStack.push(entry);
18730
+ this.undoStack.push({ entry, touchedAt: this.touch() });
18601
18731
  }
18602
18732
  canUndo() {
18603
18733
  return this.undoStack.length > 0;
@@ -18608,11 +18738,64 @@ var UndoRecorder = class _UndoRecorder {
18608
18738
  undoDepth() {
18609
18739
  return this.undoStack.length;
18610
18740
  }
18741
+ capturePersistenceBoundary() {
18742
+ this.resetCoalesce();
18743
+ return this.sequence;
18744
+ }
18745
+ discardThrough(boundary) {
18746
+ this.removeEntriesThrough(this.undoStack, boundary);
18747
+ this.removeEntriesThrough(this.redoStack, boundary);
18748
+ this.resetCoalesce();
18749
+ }
18750
+ applyPersistedIdentities(activities, links) {
18751
+ const activityIds = new Map(
18752
+ activities.map(({ id, proplannerId }) => [String(id), proplannerId])
18753
+ );
18754
+ const linkIds = new Map(
18755
+ links.map(({ id, proplannerId }) => [String(id), proplannerId])
18756
+ );
18757
+ for (const stored of [...this.undoStack, ...this.redoStack]) {
18758
+ stored.entry = this.withPersistedIdentities(
18759
+ stored.entry,
18760
+ activityIds,
18761
+ linkIds
18762
+ );
18763
+ }
18764
+ }
18611
18765
  clear() {
18612
18766
  this.undoStack.length = 0;
18613
18767
  this.redoStack.length = 0;
18614
18768
  this.resetCoalesce();
18615
18769
  }
18770
+ touch() {
18771
+ this.sequence += 1;
18772
+ return this.sequence;
18773
+ }
18774
+ removeEntriesThrough(stack, boundary) {
18775
+ const retained = stack.filter(({ touchedAt }) => touchedAt > boundary);
18776
+ stack.splice(0, stack.length, ...retained);
18777
+ }
18778
+ withPersistedIdentities(entry, activityIds, linkIds) {
18779
+ const patchActivities = (snapshots) => snapshots === void 0 ? void 0 : new Map(
18780
+ Array.from(snapshots, ([id, activity]) => [
18781
+ id,
18782
+ activityIds.has(id) ? { ...activity, proplannerId: activityIds.get(id) } : activity
18783
+ ])
18784
+ );
18785
+ const patchLinks = (snapshots) => snapshots === void 0 ? void 0 : new Map(
18786
+ Array.from(snapshots, ([id, link]) => [
18787
+ id,
18788
+ linkIds.has(id) ? { ...link, proplannerId: linkIds.get(id) } : link
18789
+ ])
18790
+ );
18791
+ return {
18792
+ ...entry,
18793
+ beforeSnap: patchActivities(entry.beforeSnap),
18794
+ afterSnap: patchActivities(entry.afterSnap),
18795
+ beforeLinks: patchLinks(entry.beforeLinks),
18796
+ afterLinks: patchLinks(entry.afterLinks)
18797
+ };
18798
+ }
18616
18799
  };
18617
18800
 
18618
18801
  // src/dispatch/shared/resequenced-parents.ts
@@ -18770,10 +18953,12 @@ function snapshotFieldValue(field, value) {
18770
18953
  return value;
18771
18954
  }
18772
18955
  function activityChangedSince(current, saved) {
18773
- for (const field of PERSISTABLE_ACTIVITY_FIELDS) {
18774
- if (!fieldEquals(field, current[field], saved[field])) return true;
18775
- }
18776
- return false;
18956
+ return changedPersistableActivityFields(current, saved).length > 0;
18957
+ }
18958
+ function changedPersistableActivityFields(current, saved) {
18959
+ return PERSISTABLE_ACTIVITY_FIELDS.filter(
18960
+ (field) => !fieldEquals(field, current[field], saved[field])
18961
+ );
18777
18962
  }
18778
18963
  function fieldEquals(field, currentValue, savedValue) {
18779
18964
  if (DATE_FIELDS2.has(field)) return dateEquals(currentValue, savedValue);
@@ -18882,11 +19067,60 @@ var SaveTracker = class _SaveTracker {
18882
19067
  }
18883
19068
  this.initialized = true;
18884
19069
  }
18885
- acknowledge(activities, links) {
19070
+ acknowledge(activities, links, activityIdentities = [], linkIdentities = [], currentActivities, currentLinks) {
18886
19071
  if (!this.trackingEnabled) return;
18887
- assertValidPersistedIdentities(activities, links);
18888
- this.snapshotActivities(activities);
18889
- this.snapshotLinks(links);
19072
+ const activityIds = new Map(
19073
+ activityIdentities.map(({ id, proplannerId }) => [
19074
+ String(id),
19075
+ proplannerId
19076
+ ])
19077
+ );
19078
+ const linkIds = new Map(
19079
+ linkIdentities.map(({ id, proplannerId }) => [String(id), proplannerId])
19080
+ );
19081
+ const persistedActivities = activities.map((activity) => ({
19082
+ ...activity,
19083
+ proplannerId: activityIds.get(String(activity.id)) ?? activity.proplannerId
19084
+ }));
19085
+ const persistedLinks = links.map((link) => {
19086
+ const proplannerId = linkIds.get(String(link.id));
19087
+ return proplannerId === void 0 ? { ...link } : { ...link, proplannerId };
19088
+ });
19089
+ assertValidPersistedIdentities(persistedActivities, persistedLinks);
19090
+ this.snapshotActivities(persistedActivities);
19091
+ this.snapshotLinks(persistedLinks);
19092
+ if (!currentActivities || !currentLinks) return;
19093
+ const currentActivityById = new Map(
19094
+ currentActivities.map((activity) => [String(activity.id), activity])
19095
+ );
19096
+ const currentLinkById = new Map(
19097
+ currentLinks.map((link) => [String(link.id), link])
19098
+ );
19099
+ for (const activity of persistedActivities) {
19100
+ const activityId = String(activity.id);
19101
+ if (this.activityIsDirty(
19102
+ activityId,
19103
+ currentActivityById.get(activityId) ?? null
19104
+ )) {
19105
+ this.dirtyActivityIds.add(activityId);
19106
+ }
19107
+ }
19108
+ for (const activity of currentActivities) {
19109
+ const activityId = String(activity.id);
19110
+ if (this.activityIsDirty(activityId, activity)) {
19111
+ this.dirtyActivityIds.add(activityId);
19112
+ }
19113
+ }
19114
+ for (const link of persistedLinks) {
19115
+ const linkId = String(link.id);
19116
+ if (this.linkIsDirty(linkId, currentLinkById.get(linkId) ?? null)) {
19117
+ this.dirtyLinkIds.add(linkId);
19118
+ }
19119
+ }
19120
+ for (const link of currentLinks) {
19121
+ const linkId = String(link.id);
19122
+ if (this.linkIsDirty(linkId, link)) this.dirtyLinkIds.add(linkId);
19123
+ }
18890
19124
  }
18891
19125
  snapshotActivities(activities) {
18892
19126
  if (!this.trackingEnabled) return;
@@ -19070,6 +19304,21 @@ var SaveTracker = class _SaveTracker {
19070
19304
  }
19071
19305
  return modified;
19072
19306
  }
19307
+ modifiedActivityFields(getActivity) {
19308
+ const result = /* @__PURE__ */ new Map();
19309
+ if (!this.trackingEnabled || !this.initialized) return result;
19310
+ for (const activityId of this.dirtyActivityIds) {
19311
+ const current = getActivity(activityId);
19312
+ const saved = this.activitiesAtLastSave.get(activityId);
19313
+ if (!current || !saved) continue;
19314
+ const fields = changedPersistableActivityFields(
19315
+ current,
19316
+ saved.persistable
19317
+ );
19318
+ if (fields.length > 0) result.set(activityId, fields);
19319
+ }
19320
+ return result;
19321
+ }
19073
19322
  modifiedLinks(getLink) {
19074
19323
  if (!this.trackingEnabled || !this.initialized) return [];
19075
19324
  const modified = [];
@@ -20439,6 +20688,7 @@ async function runInitialPasses(deps) {
20439
20688
  normalizeLoadedConstraintDates(state);
20440
20689
  recomputeCanonicalRealWork(state);
20441
20690
  if (skipAutoSchedule) {
20691
+ recomputeAllProgressRollup(state);
20442
20692
  if (now) {
20443
20693
  applyExpectedProgressLive(state, now);
20444
20694
  applyStatusPass(state, sector.statusCriteria);
@@ -20450,6 +20700,7 @@ async function runInitialPasses(deps) {
20450
20700
  normalizeMilestoneConstraintDatesForDisplay(state);
20451
20701
  recomputeSkippedLeafEnds(state);
20452
20702
  await updateParentBoundsFromChildren(state, void 0, false);
20703
+ recomputeAllProgressRollup(state);
20453
20704
  if (now) {
20454
20705
  applyExpectedProgressLive(state, now);
20455
20706
  applyStatusPass(state, sector.statusCriteria);
@@ -21160,6 +21411,14 @@ var ScheduleCore = class _ScheduleCore {
21160
21411
  (activityId) => this.coreRuntime.state.getActivity(activityId)
21161
21412
  ).map(cloneDomainValue);
21162
21413
  }
21414
+ getModifiedActivityFields() {
21415
+ this.assertReady();
21416
+ return cloneDomainValue(
21417
+ this._saveTracker.modifiedActivityFields(
21418
+ (activityId) => this.coreRuntime.state.getActivity(activityId)
21419
+ )
21420
+ );
21421
+ }
21163
21422
  getNewActivities() {
21164
21423
  this.assertReady();
21165
21424
  return this._saveTracker.newActivities(
@@ -21173,6 +21432,10 @@ var ScheduleCore = class _ScheduleCore {
21173
21432
  getScheduleRevision() {
21174
21433
  return this._scheduleRevision;
21175
21434
  }
21435
+ capturePersistenceHistoryBoundary() {
21436
+ this.assertReady();
21437
+ return this._undo.capturePersistenceBoundary();
21438
+ }
21176
21439
  getCanonicalDigest() {
21177
21440
  this.assertReady();
21178
21441
  const byId = (left, right) => String(left.id).localeCompare(String(right.id));
@@ -21284,6 +21547,7 @@ var ScheduleCore = class _ScheduleCore {
21284
21547
  this.coreRuntime.state.beginWriteCapture();
21285
21548
  this.coreRuntime.customIdTracker.beginCustomIdTransaction();
21286
21549
  let result;
21550
+ let progressFallbackBefore;
21287
21551
  try {
21288
21552
  result = await performDispatch(action, options, {
21289
21553
  adapter: this.coreRuntime.state,
@@ -21304,6 +21568,10 @@ var ScheduleCore = class _ScheduleCore {
21304
21568
  )?.id ?? null,
21305
21569
  now: this.coreRuntime.clock ? endOfLocalDay(this.coreRuntime.clock()) : null
21306
21570
  });
21571
+ const fallbackBefore = this.coreRuntime.state.peekWriteCapture()?.progressFallbackBefore;
21572
+ if (fallbackBefore && fallbackBefore.size > 0) {
21573
+ progressFallbackBefore = new Map(fallbackBefore);
21574
+ }
21307
21575
  if (!result.ok) this._rollback(generatorSnapshot);
21308
21576
  } catch (error) {
21309
21577
  this._rollback(generatorSnapshot);
@@ -21317,25 +21585,50 @@ var ScheduleCore = class _ScheduleCore {
21317
21585
  this.coreRuntime.state,
21318
21586
  result.changes
21319
21587
  );
21588
+ const progressFallbackAfter = progressFallbackBefore ? new Map(
21589
+ Array.from(progressFallbackBefore.keys(), (activityId) => [
21590
+ activityId,
21591
+ this.coreRuntime.state.getProgressFallback(activityId)
21592
+ ])
21593
+ ) : void 0;
21320
21594
  this._undo.record(
21321
21595
  buildUndoEntry(
21322
21596
  buildHistoryChangeSet(result.changes),
21323
21597
  result.__beforeSnap,
21324
21598
  result.__beforeLinks,
21325
21599
  created.afterSnap,
21326
- created.afterLinks
21600
+ created.afterLinks,
21601
+ progressFallbackBefore,
21602
+ progressFallbackAfter
21327
21603
  ),
21328
21604
  computeCoalesceKey(action),
21329
21605
  Date.now()
21330
21606
  );
21331
21607
  }
21332
21608
  if (result.ok) {
21333
- if (historyPolicy === "clear-on-success") {
21609
+ if (historyPolicy === "checkpoint-on-success") {
21610
+ if (action.kind !== "persistence-acknowledge") {
21611
+ throw new Error(
21612
+ "checkpoint-on-success requires persistence acknowledge"
21613
+ );
21614
+ }
21615
+ const currentActivities = this.coreRuntime.state.getAllActivities();
21616
+ const currentLinks = this.coreRuntime.state.getAllLinks();
21617
+ const checkpoint = action.checkpoint;
21618
+ const boundary = checkpoint?.historyBoundary ?? this._undo.capturePersistenceBoundary();
21334
21619
  this._saveTracker.acknowledge(
21335
- this.coreRuntime.state.getAllActivities(),
21336
- this.coreRuntime.state.getAllLinks()
21620
+ checkpoint?.activities ?? currentActivities,
21621
+ checkpoint?.links ?? currentLinks,
21622
+ action.activities,
21623
+ action.links,
21624
+ currentActivities,
21625
+ currentLinks
21626
+ );
21627
+ this._undo.discardThrough(boundary);
21628
+ this._undo.applyPersistedIdentities(
21629
+ action.activities ?? [],
21630
+ action.links ?? []
21337
21631
  );
21338
- this._undo.clear();
21339
21632
  } else if (action.kind === "activity-lookahead-sync" && action.acknowledgePersistedLookahead === true) {
21340
21633
  this._saveTracker.acknowledgePersistedLookahead(
21341
21634
  result.changes.activities,