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