@outbuild-company/schedule-core 1.6.3 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -132,6 +132,9 @@ var REJECTION_REASON = {
132
132
  ACTIVITY_NOT_FOUND: "activity_not_found",
133
133
  PARENT_NOT_FOUND: "parent_not_found",
134
134
  ACTIVITY_IDS_EMPTY: "activity_ids_empty",
135
+ NO_ACTIVITY_SELECTION: "no_activity_selection",
136
+ CLIPBOARD_EMPTY: "clipboard_empty",
137
+ ACTIVITY_PASTE_INVARIANT_FAILED: "activity_paste_invariant_failed",
135
138
  ANCHOR_SIBLING_CONFLICT: "anchor_sibling_conflict",
136
139
  ANCHOR_SIBLING_NOT_FOUND: "anchor_sibling_not_found",
137
140
  ANCHOR_SIBLING_WRONG_PARENT: "anchor_sibling_wrong_parent",
@@ -139,6 +142,8 @@ var REJECTION_REASON = {
139
142
  CANNOT_MOVE_INTO_OWN_DESCENDANT: "cannot_move_into_own_descendant",
140
143
  INDENT_NO_ELIGIBLE_SIBLING: "indent_no_eligible_sibling",
141
144
  OUTDENT_ROOT_LEVEL_NOT_EDITABLE: "outdent_root_level_not_editable",
145
+ HIERARCHY_CYCLE: "hierarchy_cycle",
146
+ SANDBOX_PERSISTENCE_FORBIDDEN: "sandbox_persistence_forbidden",
142
147
  INVALID_PROGRESS_VALUE: "invalid_progress_value_must_be_0_or_100"
143
148
  };
144
149
 
@@ -267,8 +272,17 @@ function toDays(hours, hoursPerDay) {
267
272
  return hours / hoursPerDay;
268
273
  }
269
274
  function calendarDaySpan(startDate, endDate) {
270
- const elapsed = endDate.getTime() - startDate.getTime();
271
- return Math.floor(elapsed / MILLISECONDS_PER_DAY) + 1;
275
+ const startDay = Date.UTC(
276
+ startDate.getUTCFullYear(),
277
+ startDate.getUTCMonth(),
278
+ startDate.getUTCDate()
279
+ );
280
+ const endDay = Date.UTC(
281
+ endDate.getUTCFullYear(),
282
+ endDate.getUTCMonth(),
283
+ endDate.getUTCDate()
284
+ );
285
+ return Math.floor((endDay - startDay) / MILLISECONDS_PER_DAY) + 1;
272
286
  }
273
287
  var FIELD_REGISTRY = {
274
288
  name: { valueKind: "string", extract: (activity) => activity.name },
@@ -283,7 +297,10 @@ var FIELD_REGISTRY = {
283
297
  },
284
298
  uniqueCorrelativeId: {
285
299
  valueKind: "number",
286
- extract: (activity) => Number(activity.uniqueCorrelativeId)
300
+ extract: (activity) => {
301
+ const value = Number(activity.uniqueCorrelativeId);
302
+ return Number.isFinite(value) ? value : null;
303
+ }
287
304
  },
288
305
  progress: { valueKind: "number", extract: (activity) => activity.progress },
289
306
  durationDays: {
@@ -292,7 +309,7 @@ var FIELD_REGISTRY = {
292
309
  },
293
310
  calendarDuration: {
294
311
  valueKind: "number",
295
- extract: (activity) => calendarDaySpan(activity.startDate, activity.endDate)
312
+ extract: (activity) => activity.type === "milestone" ? 0 : calendarDaySpan(activity.startDate, activity.endDate)
296
313
  },
297
314
  cost: { valueKind: "number", extract: (activity) => activity.cost },
298
315
  usedCost: { valueKind: "number", extract: (activity) => activity.usedCost },
@@ -658,34 +675,102 @@ var OPERATORS_BY_KIND = {
658
675
  boolean: /* @__PURE__ */ new Set(["someOf", "notSomeOf"]),
659
676
  reference: /* @__PURE__ */ new Set(["someOf", "notSomeOf"])
660
677
  };
661
- function validateCriterion(criterion) {
678
+ function normalizeDateRange(value) {
679
+ if (value === void 0) return { ok: true, dateRange: void 0 };
680
+ if (typeof value !== "object" || value === null) {
681
+ return { ok: false, reason: "invalid_value" };
682
+ }
683
+ const { start, end } = value;
684
+ if (!(start instanceof Date) || !(end instanceof Date)) {
685
+ return { ok: false, reason: "invalid_value" };
686
+ }
687
+ const startTime = start.getTime();
688
+ const endTime = end.getTime();
689
+ if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || startTime > endTime) {
690
+ return { ok: false, reason: "invalid_value" };
691
+ }
692
+ return {
693
+ ok: true,
694
+ dateRange: { start: new Date(startTime), end: new Date(endTime) }
695
+ };
696
+ }
697
+ function normalizeCriterion(criterion) {
662
698
  const descriptor = getFieldDescriptor(criterion.field);
663
- if (descriptor === null) return "unknown_field";
699
+ if (descriptor === null) return { ok: false, reason: "unknown_field" };
664
700
  if (!OPERATORS_BY_KIND[descriptor.valueKind].has(criterion.operator)) {
665
- return "unknown_operator";
701
+ return { ok: false, reason: "unknown_operator" };
702
+ }
703
+ if (descriptor.valueKind === "number") {
704
+ const value = normalizeNumberValue(criterion.value);
705
+ return value === null ? { ok: false, reason: "invalid_value" } : {
706
+ ok: true,
707
+ criterion: { ...criterion, value }
708
+ };
709
+ }
710
+ if (descriptor.valueKind === "date") {
711
+ const value = normalizeDateValue(criterion.value);
712
+ return value === null ? { ok: false, reason: "invalid_value" } : {
713
+ ok: true,
714
+ criterion: { ...criterion, value }
715
+ };
716
+ }
717
+ const rejection = validateValueForKind(criterion, descriptor.valueKind);
718
+ if (rejection !== null) return { ok: false, reason: rejection };
719
+ if (descriptor.valueKind === "boolean") {
720
+ return {
721
+ ok: true,
722
+ criterion: {
723
+ ...criterion,
724
+ value: [...new Set(criterion.value)]
725
+ }
726
+ };
727
+ }
728
+ if (descriptor.valueKind === "id-array" || descriptor.valueKind === "enum" || descriptor.valueKind === "reference") {
729
+ return {
730
+ ok: true,
731
+ criterion: {
732
+ ...criterion,
733
+ value: [
734
+ ...new Set(
735
+ criterion.value.map(String)
736
+ )
737
+ ]
738
+ }
739
+ };
666
740
  }
667
- return validateValueForKind(criterion, descriptor.valueKind);
741
+ return { ok: true, criterion };
668
742
  }
669
743
  function validateValueForKind(criterion, valueKind) {
670
744
  const { value } = criterion;
671
- if (valueKind === "id-array" || valueKind === "enum") {
672
- return Array.isArray(value) ? null : "invalid_value";
673
- }
674
- if (Array.isArray(value)) return "invalid_value";
675
- if (valueKind === "number") {
676
- return Number.isFinite(Number(value)) ? null : "invalid_value";
745
+ if (valueKind === "boolean") {
746
+ return Array.isArray(value) && value.every((item) => typeof item === "boolean") ? null : "invalid_value";
677
747
  }
678
- if (valueKind === "date") {
679
- return isValidDateValue(value) ? null : "invalid_value";
748
+ if (valueKind === "id-array" || valueKind === "enum" || valueKind === "reference") {
749
+ return Array.isArray(value) && value.every(
750
+ (item) => typeof item === "string" || typeof item === "number"
751
+ ) ? null : "invalid_value";
680
752
  }
753
+ if (Array.isArray(value)) return "invalid_value";
681
754
  return null;
682
755
  }
683
- function isValidDateValue(value) {
684
- if (value instanceof Date) return true;
685
- if (typeof value === "string" || typeof value === "number") {
686
- return !Number.isNaN(new Date(value).getTime());
687
- }
688
- return false;
756
+ function normalizeNumberValue(value) {
757
+ if (typeof value === "string" && value.trim() === "") return null;
758
+ if (typeof value !== "string" && typeof value !== "number") return null;
759
+ const parsed2 = typeof value === "number" ? value : Number(value);
760
+ return Number.isFinite(parsed2) ? parsed2 : null;
761
+ }
762
+ function normalizeDateValue(value) {
763
+ let date2;
764
+ if (value instanceof Date) date2 = new Date(value.getTime());
765
+ else if (typeof value === "string") date2 = new Date(value);
766
+ else if (typeof value === "number") date2 = new Date(value);
767
+ else return null;
768
+ return Number.isFinite(date2.getTime()) ? date2 : null;
769
+ }
770
+
771
+ // src/shared/clone-domain-value.ts
772
+ function cloneDomainValue(value) {
773
+ return structuredClone(value);
689
774
  }
690
775
 
691
776
  // src/dispatch/filter.ts
@@ -715,15 +800,21 @@ function resolveFilter(adapter, filter, hoursPerDay) {
715
800
  }
716
801
  function dispatchFilterSet(action, deps) {
717
802
  const { adapter, hoursPerDay } = deps;
803
+ const criteria = [];
718
804
  for (const criterion of action.criteria) {
719
- const rejection = validateCriterion(criterion);
720
- if (rejection !== null) return { ok: false, reason: rejection };
721
- }
722
- const filter = action.dateRange === void 0 ? { criteria: action.criteria, logic: action.logic } : {
723
- criteria: action.criteria,
724
- logic: action.logic,
725
- dateRange: action.dateRange
726
- };
805
+ const result = normalizeCriterion(criterion);
806
+ if (!result.ok) return { ok: false, reason: result.reason };
807
+ criteria.push(result.criterion);
808
+ }
809
+ const rangeResult = normalizeDateRange(action.dateRange);
810
+ if (!rangeResult.ok) return { ok: false, reason: rangeResult.reason };
811
+ const filter = cloneDomainValue(
812
+ rangeResult.dateRange === void 0 ? { criteria, logic: action.logic } : {
813
+ criteria,
814
+ logic: action.logic,
815
+ dateRange: rangeResult.dateRange
816
+ }
817
+ );
727
818
  adapter.setActiveFilter(isEmptyFilter(filter) ? null : filter);
728
819
  const { visibleIds, projection } = resolveFilter(
729
820
  adapter,
@@ -868,7 +959,7 @@ function dispatchSortSet(action, deps) {
868
959
  const rejection = validateRule(rule);
869
960
  if (rejection !== null) return { ok: false, reason: rejection };
870
961
  }
871
- const order = action.rules.length === 0 ? null : { rules: action.rules };
962
+ const order = action.rules.length === 0 ? null : cloneDomainValue({ rules: action.rules });
872
963
  adapter.setActiveOrder(order);
873
964
  const changes = {
874
965
  source: action,
@@ -1436,6 +1527,11 @@ function isDescendantOf(childId, ancestorId, adapter) {
1436
1527
  }
1437
1528
  return false;
1438
1529
  }
1530
+ function isSummaryHierarchyLink(source, target, adapter) {
1531
+ const sourceIsSummary = isSummary2(source, adapter);
1532
+ const targetIsSummary = isSummary2(target, adapter);
1533
+ return targetIsSummary && isDescendantOf(source, target, adapter) || sourceIsSummary && isDescendantOf(target, source, adapter);
1534
+ }
1439
1535
  function getLeafDescendants(parentId, adapter) {
1440
1536
  const leaves = [];
1441
1537
  const stack = [parentId];
@@ -1694,6 +1790,8 @@ function buildVirtualLink(originalLink, sourceId, targetId, counter, sourceLag,
1694
1790
  target: targetId,
1695
1791
  type: originalLink.type,
1696
1792
  lag: originalLink.lag,
1793
+ freeSlackHours: null,
1794
+ isCritical: false,
1697
1795
  _sourceLag: sourceLag,
1698
1796
  _targetLag: targetLag,
1699
1797
  _trueLag: trueLag,
@@ -2373,6 +2471,18 @@ var noopReporter = {
2373
2471
  }
2374
2472
  };
2375
2473
 
2474
+ // src/internal/state/calendar-duration.ts
2475
+ var CALENDAR_DAY_MS = 864e5;
2476
+ function computeCalendarDuration(activity) {
2477
+ if (activity.type === "milestone") return 0;
2478
+ const start = activity.startDate?.getTime();
2479
+ const end = activity.endDate?.getTime();
2480
+ if (start == null || end == null || Number.isNaN(start) || Number.isNaN(end)) {
2481
+ return null;
2482
+ }
2483
+ return Math.floor((end - start) / CALENDAR_DAY_MS) + 1;
2484
+ }
2485
+
2376
2486
  // src/autoscheduler/integration/apply-results.ts
2377
2487
  function applyResults(adapter, result) {
2378
2488
  if (result.cancelled || result.updatedIds.length === 0) return;
@@ -2384,6 +2494,7 @@ function applyResults(adapter, result) {
2384
2494
  if (!liveActivity) continue;
2385
2495
  liveActivity.startDate = plan.startDate;
2386
2496
  liveActivity.endDate = plan.endDate;
2497
+ liveActivity.calendarDuration = computeCalendarDuration(liveActivity);
2387
2498
  adapter.updateActivity(id);
2388
2499
  }
2389
2500
  });
@@ -2450,13 +2561,253 @@ function hasPath(adjacency, from, to) {
2450
2561
  return false;
2451
2562
  }
2452
2563
 
2564
+ // src/internal/dependency-graph/strongly-connected-components.ts
2565
+ function circularComponents(edges) {
2566
+ const walk = buildWalkState(edges);
2567
+ for (const node of walk.adjacency.keys()) {
2568
+ if (!walk.records.has(node)) collectFromRoot(node, walk);
2569
+ }
2570
+ return walk.components;
2571
+ }
2572
+ function buildWalkState(edges) {
2573
+ const adjacency = /* @__PURE__ */ new Map();
2574
+ const selfLooped = /* @__PURE__ */ new Set();
2575
+ for (const edge of edges) {
2576
+ const source = String(edge.source);
2577
+ const target = String(edge.target);
2578
+ const sourceNeighbors = neighborsOf(adjacency, source);
2579
+ neighborsOf(adjacency, target);
2580
+ if (source === target) {
2581
+ selfLooped.add(source);
2582
+ continue;
2583
+ }
2584
+ sourceNeighbors.push(target);
2585
+ }
2586
+ return {
2587
+ adjacency,
2588
+ selfLooped,
2589
+ records: /* @__PURE__ */ new Map(),
2590
+ componentStack: [],
2591
+ nextIndex: 0,
2592
+ components: []
2593
+ };
2594
+ }
2595
+ function neighborsOf(adjacency, node) {
2596
+ const existing = adjacency.get(node);
2597
+ if (existing) return existing;
2598
+ const created = [];
2599
+ adjacency.set(node, created);
2600
+ return created;
2601
+ }
2602
+ function collectFromRoot(root, walk) {
2603
+ const frames = [];
2604
+ frames.push({ node: root, record: enterNode(root, walk), neighborCursor: 0 });
2605
+ while (frames.length > 0) {
2606
+ const frame = frames[frames.length - 1];
2607
+ if (frame === void 0) break;
2608
+ const neighbors = walk.adjacency.get(frame.node) ?? [];
2609
+ if (frame.neighborCursor < neighbors.length) {
2610
+ const neighbor = neighbors[frame.neighborCursor];
2611
+ frame.neighborCursor += 1;
2612
+ if (neighbor === void 0) continue;
2613
+ visitNeighbor(frame, neighbor, frames, walk);
2614
+ continue;
2615
+ }
2616
+ frames.pop();
2617
+ const parentFrame = frames[frames.length - 1];
2618
+ if (parentFrame && frame.record.lowLink < parentFrame.record.lowLink) {
2619
+ parentFrame.record.lowLink = frame.record.lowLink;
2620
+ }
2621
+ const isComponentRoot = frame.record.lowLink === frame.record.visitIndex;
2622
+ if (isComponentRoot) emitComponent(frame.node, walk);
2623
+ }
2624
+ }
2625
+ function visitNeighbor(frame, neighbor, frames, walk) {
2626
+ const neighborRecord = walk.records.get(neighbor);
2627
+ if (!neighborRecord) {
2628
+ frames.push({
2629
+ node: neighbor,
2630
+ record: enterNode(neighbor, walk),
2631
+ neighborCursor: 0
2632
+ });
2633
+ return;
2634
+ }
2635
+ const lowersCurrent = neighborRecord.onStack && neighborRecord.visitIndex < frame.record.lowLink;
2636
+ if (lowersCurrent) frame.record.lowLink = neighborRecord.visitIndex;
2637
+ }
2638
+ function enterNode(node, walk) {
2639
+ const record = {
2640
+ visitIndex: walk.nextIndex,
2641
+ lowLink: walk.nextIndex,
2642
+ onStack: true
2643
+ };
2644
+ walk.nextIndex += 1;
2645
+ walk.records.set(node, record);
2646
+ walk.componentStack.push({ node, record });
2647
+ return record;
2648
+ }
2649
+ function emitComponent(rootNode, walk) {
2650
+ const component = [];
2651
+ for (; ; ) {
2652
+ const entry = walk.componentStack.pop();
2653
+ if (entry === void 0) break;
2654
+ entry.record.onStack = false;
2655
+ component.push(entry.node);
2656
+ if (entry.node === rootNode) break;
2657
+ }
2658
+ const firstMember = component[0];
2659
+ const isSingleSelfLoop = component.length === 1 && firstMember !== void 0 && walk.selfLooped.has(firstMember);
2660
+ const isCircular = component.length > 1 || isSingleSelfLoop;
2661
+ if (isCircular) walk.components.push(component);
2662
+ }
2663
+
2664
+ // src/internal/dependency-graph/effective-graph.ts
2665
+ function projectEffectiveEdges(port, links, proposal) {
2666
+ const projectionPort = proposal ? new ReparentedPort(port, proposal) : port;
2667
+ return expandParentLinks([...links], projectionPort);
2668
+ }
2669
+ var ReparentedPort = class {
2670
+ constructor(base, proposal) {
2671
+ this.base = base;
2672
+ for (const [childId, proposedParent] of proposal.reparented) {
2673
+ const childKey = String(childId);
2674
+ const newParent = proposedParent === null || isRootParent(proposedParent) ? null : proposedParent;
2675
+ const oldParent = base.getActivity(childId)?.parentId ?? null;
2676
+ const oldKey = oldParent === null || isRootParent(oldParent) ? null : String(oldParent);
2677
+ const newKey = newParent === null ? null : String(newParent);
2678
+ if (oldKey === newKey) continue;
2679
+ this.reparented.set(childKey, newParent);
2680
+ if (oldKey !== null) {
2681
+ const removed = this.movedOutByParent.get(oldKey) ?? /* @__PURE__ */ new Set();
2682
+ removed.add(childKey);
2683
+ this.movedOutByParent.set(oldKey, removed);
2684
+ }
2685
+ if (newKey !== null) {
2686
+ const gained = this.movedInByParent.get(newKey) ?? [];
2687
+ gained.push(childId);
2688
+ this.movedInByParent.set(newKey, gained);
2689
+ }
2690
+ }
2691
+ }
2692
+ base;
2693
+ reparented = /* @__PURE__ */ new Map();
2694
+ movedOutByParent = /* @__PURE__ */ new Map();
2695
+ movedInByParent = /* @__PURE__ */ new Map();
2696
+ withOverride(activity) {
2697
+ if (!activity) return null;
2698
+ const override = this.reparented.get(String(activity.id));
2699
+ if (override === void 0) return activity;
2700
+ return { ...activity, parentId: override };
2701
+ }
2702
+ getActivity(activityId) {
2703
+ return this.withOverride(this.base.getActivity(activityId));
2704
+ }
2705
+ getLiveActivity(activityId) {
2706
+ return this.withOverride(this.base.getLiveActivity(activityId));
2707
+ }
2708
+ getAllActivities() {
2709
+ return this.base.getAllActivities().map((activity) => this.withOverride(activity) ?? activity);
2710
+ }
2711
+ getChildren(parentId) {
2712
+ const parentKey = String(parentId);
2713
+ const removed = this.movedOutByParent.get(parentKey);
2714
+ const baseChildren = this.base.getChildren(parentId);
2715
+ const kept = removed ? baseChildren.filter((childId) => !removed.has(String(childId))) : [...baseChildren];
2716
+ const gained = this.movedInByParent.get(parentKey);
2717
+ if (gained) kept.push(...gained);
2718
+ return kept;
2719
+ }
2720
+ getParent(activityId) {
2721
+ const override = this.reparented.get(String(activityId));
2722
+ if (override === void 0) return this.base.getParent(activityId);
2723
+ if (override === null) return null;
2724
+ return this.getActivity(override);
2725
+ }
2726
+ isChildOf(childId, parentId) {
2727
+ const targetKey = String(parentId);
2728
+ const visited = /* @__PURE__ */ new Set();
2729
+ let current = this.getActivity(childId);
2730
+ while (current) {
2731
+ const parent = current.parentId;
2732
+ if (parent === null || isRootParent(parent)) return false;
2733
+ const parentKey = String(parent);
2734
+ if (parentKey === targetKey) return true;
2735
+ if (visited.has(parentKey)) return false;
2736
+ visited.add(parentKey);
2737
+ current = this.getActivity(parent);
2738
+ }
2739
+ return false;
2740
+ }
2741
+ getAllLinks() {
2742
+ return this.base.getAllLinks();
2743
+ }
2744
+ getAllIds() {
2745
+ return this.base.getAllIds();
2746
+ }
2747
+ getLink(linkId) {
2748
+ return this.base.getLink(linkId);
2749
+ }
2750
+ activityExists(activityId) {
2751
+ return this.base.activityExists(activityId);
2752
+ }
2753
+ getOutgoingLinkIds(activityId) {
2754
+ return this.base.getOutgoingLinkIds(activityId);
2755
+ }
2756
+ getIncomingLinkIds(activityId) {
2757
+ return this.base.getIncomingLinkIds(activityId);
2758
+ }
2759
+ getClosestWorkTime(params) {
2760
+ return this.base.getClosestWorkTime(params);
2761
+ }
2762
+ calculateEndDate(params) {
2763
+ return this.base.calculateEndDate(params);
2764
+ }
2765
+ calculateDuration(params) {
2766
+ return this.base.calculateDuration(params);
2767
+ }
2768
+ batchUpdate(runBatch) {
2769
+ this.base.batchUpdate(runBatch);
2770
+ }
2771
+ updateActivity(activityId) {
2772
+ this.base.updateActivity(activityId);
2773
+ }
2774
+ getFlags() {
2775
+ return this.base.getFlags();
2776
+ }
2777
+ getProjectEnd() {
2778
+ return this.base.getProjectEnd();
2779
+ }
2780
+ };
2781
+
2782
+ // src/internal/dependency-graph/effective-graph-validation.ts
2783
+ function validateTopologyCommit(input) {
2784
+ const { port, currentLinks, proposedLinks, proposal } = input;
2785
+ const proposedEdges = projectEffectiveEdges(port, proposedLinks, proposal);
2786
+ if (!hasDirectedCycle(proposedEdges)) return { ok: true };
2787
+ const currentEdges = projectEffectiveEdges(port, currentLinks);
2788
+ const corruptBefore = corruptActivityIds(currentEdges);
2789
+ const corruptAfter = corruptActivityIds(proposedEdges);
2790
+ const newlyCyclicActivityIds = [...corruptAfter].filter(
2791
+ (activityId) => !corruptBefore.has(activityId)
2792
+ );
2793
+ if (newlyCyclicActivityIds.length === 0) return { ok: true };
2794
+ return { ok: false, newlyCyclicActivityIds };
2795
+ }
2796
+ function corruptActivityIds(edges) {
2797
+ const corrupt = /* @__PURE__ */ new Set();
2798
+ for (const component of circularComponents(edges)) {
2799
+ for (const activityId of component) corrupt.add(activityId);
2800
+ }
2801
+ return corrupt;
2802
+ }
2803
+
2453
2804
  // src/autoscheduler/links/apply-link-operation.ts
2454
2805
  function applyLinkOperation(op, deps) {
2455
2806
  switch (op.kind) {
2456
2807
  case "create":
2457
2808
  return applyCreate(op, deps);
2458
2809
  case "update":
2459
- return applyUpdate(op, deps);
2810
+ return applyLinkUpdate(op, deps.port);
2460
2811
  case "delete":
2461
2812
  return applyDelete(op, deps);
2462
2813
  }
@@ -2482,6 +2833,10 @@ function applyLinkCreates(links, deps) {
2482
2833
  rejected = { applied: false, rejected: "activity-missing" };
2483
2834
  break;
2484
2835
  }
2836
+ if (isSummaryHierarchyLink(operation.source, operation.target, deps.port)) {
2837
+ rejected = { applied: false, rejected: "hierarchy-link" };
2838
+ break;
2839
+ }
2485
2840
  const key = linkKey(operation);
2486
2841
  const duplicateId = duplicateKeys.get(key);
2487
2842
  if (duplicateId !== void 0) {
@@ -2507,6 +2862,26 @@ function applyLinkCreates(links, deps) {
2507
2862
  return { applied: false, rejected: "cycle" };
2508
2863
  }
2509
2864
  if (rejected) return rejected;
2865
+ const proposedLinks = [
2866
+ ...existingLinks,
2867
+ ...links.map(({ operation, linkId }) => ({
2868
+ id: linkId,
2869
+ source: operation.source,
2870
+ target: operation.target,
2871
+ type: operation.type,
2872
+ lag: operation.lag,
2873
+ freeSlackHours: null,
2874
+ isCritical: false
2875
+ }))
2876
+ ];
2877
+ const effectiveVerdict = validateTopologyCommit({
2878
+ port: deps.port,
2879
+ currentLinks: existingLinks,
2880
+ proposedLinks
2881
+ });
2882
+ if (!effectiveVerdict.ok) {
2883
+ return { applied: false, rejected: "cycle" };
2884
+ }
2510
2885
  for (const { operation, linkId } of links) {
2511
2886
  const preserved = deps.preserved?.get(linkId);
2512
2887
  deps.port.addLink({
@@ -2515,6 +2890,8 @@ function applyLinkCreates(links, deps) {
2515
2890
  target: operation.target,
2516
2891
  type: operation.type,
2517
2892
  lag: operation.lag,
2893
+ freeSlackHours: null,
2894
+ isCritical: false,
2518
2895
  ...preserved?.ganttId === void 0 ? {} : { ganttId: preserved.ganttId },
2519
2896
  ...preserved?.sectorId === void 0 ? {} : { sectorId: preserved.sectorId },
2520
2897
  ...preserved?.proplannerId === void 0 ? {} : { proplannerId: preserved.proplannerId }
@@ -2535,6 +2912,9 @@ function applyCreate(op, deps) {
2535
2912
  if (!src || !tgt) {
2536
2913
  return { applied: false, rejected: "activity-missing" };
2537
2914
  }
2915
+ if (isSummaryHierarchyLink(op.source, op.target, port)) {
2916
+ return { applied: false, rejected: "hierarchy-link" };
2917
+ }
2538
2918
  const existingLinks = port.getAllLinks();
2539
2919
  for (const l of existingLinks) {
2540
2920
  if (String(l.source) === String(op.source) && String(l.target) === String(op.target) && l.type === op.type) {
@@ -2554,13 +2934,22 @@ function applyCreate(op, deps) {
2554
2934
  source: op.source,
2555
2935
  target: op.target,
2556
2936
  type: op.type,
2557
- lag: op.lag
2937
+ lag: op.lag,
2938
+ freeSlackHours: null,
2939
+ isCritical: false
2558
2940
  };
2941
+ const effectiveVerdict = validateTopologyCommit({
2942
+ port,
2943
+ currentLinks: existingLinks,
2944
+ proposedLinks: [...existingLinks, link]
2945
+ });
2946
+ if (!effectiveVerdict.ok) {
2947
+ return { applied: false, rejected: "cycle" };
2948
+ }
2559
2949
  port.addLink(link);
2560
2950
  return { applied: true, linkId: id };
2561
2951
  }
2562
- function applyUpdate(op, deps) {
2563
- const { port } = deps;
2952
+ function applyLinkUpdate(op, port) {
2564
2953
  const link = port.getLink(op.linkId);
2565
2954
  if (!link) return { applied: false, rejected: "link-missing" };
2566
2955
  if (op.lag !== void 0) port.setLinkField(op.linkId, "lag", op.lag);
@@ -2731,6 +3120,10 @@ function diffOutgoingLinks(input) {
2731
3120
  }
2732
3121
 
2733
3122
  // src/autoscheduler/links/lag-units.ts
3123
+ function roundLagDays(lagDays) {
3124
+ const rounded = Math.sign(lagDays) * Math.round(Math.abs(lagDays));
3125
+ return Object.is(rounded, -0) ? 0 : rounded;
3126
+ }
2734
3127
  function lagDaysToHours(lagDays, hoursPerDay) {
2735
3128
  return Math.sign(lagDays) * Math.round(Math.abs(lagDays) * hoursPerDay);
2736
3129
  }
@@ -2993,10 +3386,7 @@ function adjustLinkLagOnTaskMove(targetId, adapter, preEditTarget) {
2993
3386
  calendarApi
2994
3387
  );
2995
3388
  if (newLag === link.lag) continue;
2996
- applyLinkOperation(
2997
- { kind: "update", linkId: link.id, lag: newLag },
2998
- { port: adapter }
2999
- );
3389
+ applyLinkUpdate({ linkId: link.id, lag: newLag }, adapter);
3000
3390
  }
3001
3391
  }
3002
3392
 
@@ -3060,19 +3450,44 @@ async function recomputeParentsCooperatively(parents, adapter, progressMode, opt
3060
3450
  }
3061
3451
  function findAncestorsOfDirty(adapter, dirtyIds) {
3062
3452
  const ancestors = /* @__PURE__ */ new Set();
3063
- for (const id of dirtyIds) walkAncestors(id, adapter, ancestors);
3453
+ const parentById = /* @__PURE__ */ new Map();
3454
+ for (const id of dirtyIds) {
3455
+ walkAncestors(id, adapter, ancestors, parentById);
3456
+ }
3064
3457
  if (ancestors.size === 0) return [];
3065
3458
  const ids = Array.from(ancestors);
3066
- const depths = computeDepths(ids, adapter);
3459
+ const depths = computeKnownDepths(ids, parentById);
3067
3460
  return sortDeepestFirst(ids, depths);
3068
3461
  }
3069
- function walkAncestors(id, adapter, collector) {
3462
+ function walkAncestors(id, adapter, collector, parentById) {
3070
3463
  let current = adapter.getParentId(id);
3071
3464
  while (current && !isRootParent(current)) {
3072
3465
  if (collector.has(current)) return;
3073
3466
  collector.add(current);
3074
- current = adapter.getParentId(current);
3467
+ const parent = adapter.getParentId(current);
3468
+ parentById.set(current, parent);
3469
+ current = parent;
3470
+ }
3471
+ }
3472
+ function computeKnownDepths(ids, parentById) {
3473
+ const depths = /* @__PURE__ */ new Map();
3474
+ for (const id of ids) {
3475
+ if (depths.has(id)) continue;
3476
+ const path = [];
3477
+ const inPath = /* @__PURE__ */ new Set();
3478
+ let current = id;
3479
+ while (current && !isRootParent(current) && !depths.has(current) && !inPath.has(current)) {
3480
+ path.push(current);
3481
+ inPath.add(current);
3482
+ current = parentById.get(current) ?? null;
3483
+ }
3484
+ let depth = current ? depths.get(current) ?? 0 : 0;
3485
+ for (let index = path.length - 1; index >= 0; index -= 1) {
3486
+ depth += 1;
3487
+ depths.set(path[index], depth);
3488
+ }
3075
3489
  }
3490
+ return depths;
3076
3491
  }
3077
3492
  function aggregateChildren(childrenIds, adapter, includeRealWork) {
3078
3493
  let minStart = null;
@@ -3303,7 +3718,7 @@ var setEndDate = (value) => ({
3303
3718
 
3304
3719
  // src/columns/shared/date-normalize.ts
3305
3720
  function truncateToUtcDay(date2, tz) {
3306
- if (tz) {
3721
+ {
3307
3722
  const parts = new Intl.DateTimeFormat("en-CA", {
3308
3723
  timeZone: tz,
3309
3724
  year: "numeric",
@@ -3313,9 +3728,6 @@ function truncateToUtcDay(date2, tz) {
3313
3728
  const get = (kind) => Number(parts.find((p) => p.type === kind)?.value);
3314
3729
  return new Date(Date.UTC(get("year"), get("month") - 1, get("day")));
3315
3730
  }
3316
- return new Date(
3317
- Date.UTC(date2.getFullYear(), date2.getMonth(), date2.getDate())
3318
- );
3319
3731
  }
3320
3732
  function endOfLocalDay(date2, tz) {
3321
3733
  {
@@ -3325,11 +3737,11 @@ function endOfLocalDay(date2, tz) {
3325
3737
  }
3326
3738
  }
3327
3739
  function nextWorkingDay(calendar, domainDay) {
3328
- let cursor = truncateToUtcDay(domainDay);
3740
+ let cursor = truncateToUtcDay(domainDay, "UTC");
3329
3741
  while (!calendar.isWorkTime(cursor, CALENDAR_UNIT.DAY)) {
3330
3742
  cursor = new Date(cursor);
3331
3743
  cursor.setUTCDate(cursor.getUTCDate() + 1);
3332
- cursor = truncateToUtcDay(cursor);
3744
+ cursor = truncateToUtcDay(cursor, "UTC");
3333
3745
  }
3334
3746
  return cursor;
3335
3747
  }
@@ -3367,11 +3779,12 @@ var DATELESS_CONSTRAINTS = /* @__PURE__ */ new Set([
3367
3779
  ]);
3368
3780
  var IMPLIED_CONSTRAINT_ON_DATE_EDIT = "snet";
3369
3781
  function normalizeConstraintDate(typedDay, calendar, constraintType) {
3782
+ const constraintDay = truncateToUtcDay(typedDay, "UTC");
3370
3783
  if (!START_BASED_CONSTRAINTS2.has(constraintType) && !END_BASED_CONSTRAINTS.has(constraintType)) {
3371
- return typedDay;
3784
+ return constraintDay;
3372
3785
  }
3373
- const reference = START_BASED_CONSTRAINTS2.has(constraintType) ? shiftStart(calendar, typedDay) : shiftEnd(calendar, typedDay);
3374
- const result = new Date(typedDay);
3786
+ const reference = START_BASED_CONSTRAINTS2.has(constraintType) ? shiftStart(calendar, constraintDay) : shiftEnd(calendar, constraintDay);
3787
+ const result = new Date(constraintDay);
3375
3788
  result.setUTCHours(
3376
3789
  reference.getUTCHours(),
3377
3790
  reference.getUTCMinutes(),
@@ -3427,10 +3840,8 @@ var durationPipeline = {
3427
3840
  },
3428
3841
  parseInput(rawValue, _activity, ctx) {
3429
3842
  const numericValue = parseFloat(String(rawValue));
3430
- if (Number.isNaN(numericValue)) return parseError("not_a_number");
3431
- return parsed(
3432
- ctx.inputUnit === "hours" ? numericValue : durationToHours(numericValue, ctx.hoursPerDay)
3433
- );
3843
+ if (!Number.isFinite(numericValue)) return parseError("not_a_number");
3844
+ return parsed(durationToHours(numericValue, ctx.hoursPerDay));
3434
3845
  },
3435
3846
  validate(_activity, oldValue, newValue) {
3436
3847
  if (isNegativeDuration(newValue)) {
@@ -3879,17 +4290,7 @@ var startDatePipeline = {
3879
4290
  transform(activity, newValue, ctx) {
3880
4291
  const calendar = ctx.calendars.getCalendar(activity.calendarId);
3881
4292
  if (!calendar) {
3882
- return fieldChanges(
3883
- {
3884
- startDate: newValue,
3885
- ...setConstraintTypeImplied(),
3886
- ...setConstraintDate(newValue)
3887
- },
3888
- {
3889
- autoSchedule: true,
3890
- postProcessors: ["adjustLinkLagOnTaskMove"]
3891
- }
3892
- );
4293
+ throw new Error("startDatePipeline: no calendar resolved (integrity)");
3893
4294
  }
3894
4295
  const finalStart = shiftStart(calendar, newValue);
3895
4296
  const milestoneActivity = activity.type === "milestone";
@@ -3919,7 +4320,7 @@ function checkParentStartConstraint(activity, newDate, ctx) {
3919
4320
  activity.id
3920
4321
  );
3921
4322
  if (!parentStartDate) return null;
3922
- const parentDay = truncateToUtcDay(parentStartDate);
4323
+ const parentDay = truncateToUtcDay(parentStartDate, "UTC");
3923
4324
  if (newDate.getTime() < parentDay.getTime()) {
3924
4325
  return invalid("before_parent_start");
3925
4326
  }
@@ -3967,7 +4368,6 @@ var endDatePipeline = {
3967
4368
  const calendar = ctx.calendars.getCalendar(activity.calendarId);
3968
4369
  if (!calendar) return parsed(day);
3969
4370
  if (!calendar.isWorkTime(day, CALENDAR_UNIT.DAY)) {
3970
- if (ctx.dateFormat.split(" ").length > 1) return parsed(day);
3971
4371
  return parsed(
3972
4372
  calendar.getClosestWorkTime({
3973
4373
  direction: "past",
@@ -4150,7 +4550,12 @@ var constraintDatePipeline = {
4150
4550
  transform(activity, newValue, ctx) {
4151
4551
  const finalType = isUnconstrained(activity) ? IMPLIED_CONSTRAINT_ON_DATE_EDIT : effectiveConstraintType(activity);
4152
4552
  const calendar = ctx.calendars.getCalendar(activity.calendarId);
4153
- const storedDate = calendar ? normalizeConstraintDate(newValue, calendar, finalType) : newValue;
4553
+ if (!calendar) {
4554
+ throw new Error(
4555
+ "constraintDatePipeline: no calendar resolved (integrity)"
4556
+ );
4557
+ }
4558
+ const storedDate = normalizeConstraintDate(newValue, calendar, finalType);
4154
4559
  const patch = buildConstraintDateExtraFields(activity, storedDate);
4155
4560
  return fieldChanges(patch, {
4156
4561
  autoSchedule: true
@@ -4450,6 +4855,31 @@ var COLUMN_WRITABLE_FIELDS = new Set(
4450
4855
  COLUMN_WRITABLE_FIELD_LIST
4451
4856
  );
4452
4857
 
4858
+ // src/constants/activity-properties.ts
4859
+ var ACTIVITY_PROPERTY = {
4860
+ PARENT: "parentId",
4861
+ TYPE: "type",
4862
+ START_DATE: "startDate",
4863
+ END_DATE: "endDate",
4864
+ CONSTRAINT_TYPE: "constraintType",
4865
+ CONSTRAINT_DATE: "constraintDate",
4866
+ PROGRESS: "progress",
4867
+ COST: "cost",
4868
+ USED_COST: "usedCost",
4869
+ REAL_COST: "realCost",
4870
+ WORK_HOURS: "workHours",
4871
+ REAL_WORK_HOURS: "realWorkHours",
4872
+ CORRELATIVE_ID: "correlativeId",
4873
+ CUSTOM_ID: "customId",
4874
+ NEW_ACTIVITIES_ARRAY: "newActivityIds",
4875
+ HAS_NEW_ACTIVITIES: "hasNewActivities",
4876
+ CALENDAR_DURATION: "calendarDuration"};
4877
+ var LINK_PROPERTY = {
4878
+ TYPE: "type",
4879
+ LAG: "lag",
4880
+ FREE_SLACK_HOURS: "freeSlackHours",
4881
+ IS_CRITICAL: "isCritical"};
4882
+
4453
4883
  // src/internal/state/dynamic-writes.ts
4454
4884
  function setActivityFieldDynamic(state, activityId, field, value) {
4455
4885
  state.setActivityFieldDynamic(activityId, field, value);
@@ -4501,11 +4931,6 @@ function cloneCriticalPath(value) {
4501
4931
  };
4502
4932
  }
4503
4933
 
4504
- // src/shared/clone-domain-value.ts
4505
- function cloneDomainValue(value) {
4506
- return structuredClone(value);
4507
- }
4508
-
4509
4934
  // src/dispatch/shared/snapshots.ts
4510
4935
  var YIELD_EVERY_N_ENTRIES = 200;
4511
4936
  function collectTouchedIds(primary, changes) {
@@ -4617,6 +5042,7 @@ function diffActivity(before, after) {
4617
5042
  const beforeRec = before ?? {};
4618
5043
  const afterRec = after;
4619
5044
  const keys = /* @__PURE__ */ new Set([...Object.keys(beforeRec), ...Object.keys(afterRec)]);
5045
+ keys.delete(ACTIVITY_PROPERTY.CALENDAR_DURATION);
4620
5046
  for (const k of keys) {
4621
5047
  if (Object.hasOwn(beforeRec, k) !== Object.hasOwn(afterRec, k) || !fieldValueEqual(beforeRec[k], afterRec[k])) {
4622
5048
  fields[k] = {
@@ -5308,10 +5734,11 @@ function mutateTheDateToFutureOrPastInBaseRestriction(dateBaseToCalculate, restr
5308
5734
 
5309
5735
  // src/critical-path/legacy/base/parents-calculations/forward.js
5310
5736
  var CalculateForwardParentsWithLinks = class {
5311
- constructor(linkedActivitiesData, activity, gantt) {
5737
+ constructor(linkedActivitiesData, activity, gantt, linkCandidates) {
5312
5738
  this.linkedActivitiesData = linkedActivitiesData;
5313
5739
  this.activity = activity;
5314
5740
  this.gantt = gantt;
5741
+ this.linkCandidates = linkCandidates;
5315
5742
  this.activityCalendar = this.gantt.getCalendar(this.activity.calendar_id);
5316
5743
  if (!this.activityCalendar) {
5317
5744
  throw new Error(
@@ -5356,6 +5783,10 @@ var CalculateForwardParentsWithLinks = class {
5356
5783
  continue;
5357
5784
  }
5358
5785
  if (calculation) {
5786
+ this.linkCandidates?.set(linkedActivity.linkId, {
5787
+ targetId: String(this.activity.id),
5788
+ ...calculation
5789
+ });
5359
5790
  calculations.push(calculation);
5360
5791
  }
5361
5792
  }
@@ -5803,7 +6234,8 @@ function parentWithLinks(params) {
5803
6234
  linkDirection,
5804
6235
  calculations,
5805
6236
  gantt,
5806
- direction
6237
+ direction,
6238
+ linkCandidates
5807
6239
  } = params;
5808
6240
  const linksArray = parent[linkProperty];
5809
6241
  const parentData = extractParentData(parent);
@@ -5818,7 +6250,8 @@ function parentWithLinks(params) {
5818
6250
  calculation = new forward_default(
5819
6251
  linksWithActivityData,
5820
6252
  parent,
5821
- gantt
6253
+ gantt,
6254
+ linkCandidates
5822
6255
  );
5823
6256
  calculation = calculation.calculate();
5824
6257
  } else {
@@ -5863,6 +6296,7 @@ function getLinksWithActivityDataForward(linksArray, linkDirection, calculations
5863
6296
  return null;
5864
6297
  }
5865
6298
  return {
6299
+ linkId: String(linkData.id),
5866
6300
  es: calculationsOfLinkedActivity.es,
5867
6301
  ef: calculationsOfLinkedActivity.ef,
5868
6302
  linkLag: linkData.lag,
@@ -5963,6 +6397,7 @@ var CalculationOfParent = class {
5963
6397
  this.calculationOfParentsFromLinks = params.calculationOfParentsFromLinks;
5964
6398
  this.singleParents = params.singleParents;
5965
6399
  this.parentCalculationFromChildren = params.parentCalculationFromChildren;
6400
+ this.linkCandidates = params.linkCandidates;
5966
6401
  this.gantt = params.gantt;
5967
6402
  }
5968
6403
  handleParentWithLinks() {
@@ -6010,7 +6445,8 @@ var CalculationOfParent = class {
6010
6445
  linkDirection: this.linkDirection,
6011
6446
  gantt: this.gantt,
6012
6447
  calculations: this.calculations,
6013
- direction: this.direction
6448
+ direction: this.direction,
6449
+ linkCandidates: this.linkCandidates
6014
6450
  });
6015
6451
  this.calculationsOfParentThatImpactChildrens.set(activity.id, calculation);
6016
6452
  this.calculationOfParentsFromLinks.add(activity.id);
@@ -6659,6 +7095,7 @@ var CriticalPathHelpers = class {
6659
7095
  calculationOfParentsFromLinks: this.calculationOfParentsFromLinks,
6660
7096
  singleParents: this.singleParents,
6661
7097
  parentCalculationFromChildren: this.parentCalculationFromChildren,
7098
+ linkCandidates: this.linkCandidates,
6662
7099
  gantt: this.gantt
6663
7100
  });
6664
7101
  if (this.pendingParentsWithLinks.has(activity.id)) {
@@ -6726,6 +7163,7 @@ var LinksRulesCalculationsMethods = class {
6726
7163
  this.parentsWithLinks = calculationObject.parentsWithLinks;
6727
7164
  this.parentsCalculations = calculationObject.parentsCalculations;
6728
7165
  this.gantt = calculationObject.ganttInstance;
7166
+ this.linkCandidates = calculationObject.linkCandidates;
6729
7167
  }
6730
7168
  getEsAndEfFromPreviousCalculatedActivities(activity) {
6731
7169
  const isAlap = this.alapMap.has(activity);
@@ -6897,7 +7335,12 @@ var LinksConstraintCalculator = class extends LinksRulesCalculationsMethods_defa
6897
7335
  if (!linkData) {
6898
7336
  throw new Error("Link data not found", { cause: { linkId: link } });
6899
7337
  }
6900
- return this.calculateLink(linkData);
7338
+ const calculation = this.calculateLink(linkData);
7339
+ this.linkCandidates?.set(String(linkData.id), {
7340
+ targetId: String(this.referenceActivity.id),
7341
+ ...calculation
7342
+ });
7343
+ return calculation;
6901
7344
  });
6902
7345
  const parent = Number(this.referenceActivity.parent);
6903
7346
  if (this.parentsWithLinks.has(parent)) {
@@ -7163,6 +7606,7 @@ var ForwardPath = class extends generic_calculations_default {
7163
7606
  this.totalFloatSnltFnlt = /* @__PURE__ */ new Map();
7164
7607
  this.totalFloatForMsoMfo = /* @__PURE__ */ new Map();
7165
7608
  this.totalFloatForAsap = /* @__PURE__ */ new Map();
7609
+ this.linkCandidates = /* @__PURE__ */ new Map();
7166
7610
  this.calculationOfParentsFromLinks = /* @__PURE__ */ new Set();
7167
7611
  this.parentCalculationFromChildren = /* @__PURE__ */ new Set();
7168
7612
  this.forbiddenActivitiesLinkedToParentsWithLinks = /* @__PURE__ */ new Set();
@@ -7261,7 +7705,8 @@ var ForwardPath = class extends generic_calculations_default {
7261
7705
  constraint: constraintType,
7262
7706
  parentsWithLinks: this.pendingParentsWithLinks,
7263
7707
  parentsCalculations: this.calculationsOfParentThatImpactChildrens,
7264
- ganttInstance: this.gantt
7708
+ ganttInstance: this.gantt,
7709
+ linkCandidates: this.linkCandidates
7265
7710
  }).calculate();
7266
7711
  if (constraintType === CONSTRAINT_TYPES.ASAP) {
7267
7712
  this.totalFloatForAsap.set(activity, {
@@ -7279,7 +7724,8 @@ var ForwardPath = class extends generic_calculations_default {
7279
7724
  constraint: constraintType,
7280
7725
  parentsWithLinks: this.pendingParentsWithLinks,
7281
7726
  parentsCalculations: this.calculationsOfParentThatImpactChildrens,
7282
- ganttInstance: this.gantt
7727
+ ganttInstance: this.gantt,
7728
+ linkCandidates: this.linkCandidates
7283
7729
  }).calculate());
7284
7730
  }
7285
7731
  if (constraintType === CONSTRAINT_TYPES.ALAP) {
@@ -8316,7 +8762,6 @@ var BackwardPath = class extends generic_calculations_default {
8316
8762
  }
8317
8763
  }
8318
8764
  if ([CONSTRAINT_TYPES.SNLT].includes(activity.constraint_type)) {
8319
- const constraintDate = activity.constraint_date;
8320
8765
  const parentLinkCalcualation2 = this.calculationsOfParentThatImpactChildrens.get(parent);
8321
8766
  const lsToComparison = addDurationToDate(
8322
8767
  this.gantt.getCalendar(activity.calendar_id),
@@ -8324,6 +8769,7 @@ var BackwardPath = class extends generic_calculations_default {
8324
8769
  -activity.duration,
8325
8770
  activity
8326
8771
  );
8772
+ const constraintDate = activity.constraint_date ?? lsToComparison;
8327
8773
  ls = new Date(
8328
8774
  Math.min(constraintDate.getTime(), lsToComparison.getTime())
8329
8775
  );
@@ -8335,8 +8781,8 @@ var BackwardPath = class extends generic_calculations_default {
8335
8781
  );
8336
8782
  }
8337
8783
  if ([CONSTRAINT_TYPES.FNLT].includes(activity.constraint_type)) {
8338
- const constraintDate = activity.constraint_date;
8339
8784
  const parentLinkCalcualation2 = this.calculationsOfParentThatImpactChildrens.get(parent);
8785
+ const constraintDate = activity.constraint_date ?? parentLinkCalcualation2.lf;
8340
8786
  ls = new Date(
8341
8787
  Math.min(constraintDate.getTime(), parentLinkCalcualation2.lf.getTime())
8342
8788
  );
@@ -8720,18 +9166,20 @@ var FreeFloat = class {
8720
9166
  );
8721
9167
  }
8722
9168
  calculateFreeFloatForEndOfProject(constraint) {
9169
+ const calculation = constraint === CONSTRAINT_TYPES.ALAP ? this.backwardMap.get(this.activity.id) : this.forwardMap.get(this.activity.id);
8723
9170
  return this.parseHoursToDays(
8724
9171
  this.calculationWithCalendar(
8725
9172
  this.gantt.getSubtaskDates().end_date,
8726
- constraint === CONSTRAINT_TYPES.ALAP ? this.backwardMap.get(this.activity.id).lf : this.forwardMap.get(this.activity.id).ef
9173
+ constraint === CONSTRAINT_TYPES.ALAP ? calculation?.lf : calculation?.ef
8727
9174
  )
8728
9175
  );
8729
9176
  }
8730
9177
  calculateFreeFloatOfRestriction(constraint) {
9178
+ const calculation = this.forwardMap.get(this.activity.id);
8731
9179
  return this.parseHoursToDays(
8732
9180
  this.calculationWithCalendar(
8733
9181
  this.activity.constraint_date,
8734
- constraint === CONSTRAINT_TYPES.FNLT ? this.forwardMap.get(this.activity.id).ef : this.forwardMap.get(this.activity.id).es
9182
+ constraint === CONSTRAINT_TYPES.FNLT ? calculation?.ef : calculation?.es
8735
9183
  )
8736
9184
  );
8737
9185
  }
@@ -8749,111 +9197,33 @@ var FreeFloat = class {
8749
9197
  sucessorId,
8750
9198
  predecessorId
8751
9199
  }) {
8752
- const getPredecessorDate = ({
8753
- direction,
8754
- constraint: constraint2 = CONSTRAINT_TYPES.ASAP
8755
- }) => {
8756
- if (constraint2 === CONSTRAINT_TYPES.ALAP) {
8757
- return this.backwardMap.get(predecessorId)[direction];
9200
+ const successor = (isSuscessorAlap ? this.backwardMap : this.forwardMap).get(sucessorId);
9201
+ const predecessor = (constraint === CONSTRAINT_TYPES.ALAP ? this.backwardMap : this.forwardMap).get(predecessorId);
9202
+ const predecessorStart = constraint === CONSTRAINT_TYPES.ALAP ? predecessor?.ls : predecessor?.es;
9203
+ const predecessorFinish = constraint === CONSTRAINT_TYPES.ALAP ? predecessor?.lf : predecessor?.ef;
9204
+ return {
9205
+ fs: {
9206
+ sucessor: successor?.ls ?? successor?.es,
9207
+ predecessor: predecessorFinish
9208
+ },
9209
+ ss: {
9210
+ sucessor: successor?.ls ?? successor?.es,
9211
+ predecessor: predecessorStart
9212
+ },
9213
+ ff: {
9214
+ sucessor: successor?.lf ?? successor?.ef,
9215
+ predecessor: predecessorFinish
9216
+ },
9217
+ sf: {
9218
+ sucessor: successor?.lf ?? successor?.ef,
9219
+ predecessor: predecessorStart
8758
9220
  }
8759
- return this.forwardMap.get(predecessorId)[direction];
8760
9221
  };
8761
- if (constraint === CONSTRAINT_TYPES.ASAP || constraint === CONSTRAINT_TYPES.SNET || constraint === CONSTRAINT_TYPES.FNET || constraint === CONSTRAINT_TYPES.MSO || constraint === CONSTRAINT_TYPES.MFO) {
8762
- return {
8763
- fs: {
8764
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
8765
- predecessor: getPredecessorDate({ direction: "ef" })
8766
- },
8767
- ss: {
8768
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
8769
- predecessor: getPredecessorDate({ direction: "es" })
8770
- },
8771
- ff: {
8772
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
8773
- predecessor: getPredecessorDate({ direction: "ef" })
8774
- },
8775
- sf: {
8776
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
8777
- predecessor: getPredecessorDate({ direction: "es" })
8778
- }
8779
- };
8780
- }
8781
- if (constraint === CONSTRAINT_TYPES.ALAP) {
8782
- return {
8783
- fs: {
8784
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
8785
- predecessor: getPredecessorDate({
8786
- constraint: CONSTRAINT_TYPES.ALAP,
8787
- direction: "lf"
8788
- })
8789
- },
8790
- ss: {
8791
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
8792
- predecessor: getPredecessorDate({
8793
- constraint: CONSTRAINT_TYPES.ALAP,
8794
- direction: "ls"
8795
- })
8796
- },
8797
- ff: {
8798
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
8799
- predecessor: getPredecessorDate({
8800
- constraint: CONSTRAINT_TYPES.ALAP,
8801
- direction: "lf"
8802
- })
8803
- },
8804
- sf: {
8805
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
8806
- predecessor: getPredecessorDate({
8807
- constraint: CONSTRAINT_TYPES.ALAP,
8808
- direction: "ls"
8809
- })
8810
- }
8811
- };
8812
- }
8813
- if (constraint === CONSTRAINT_TYPES.SNLT) {
8814
- return {
8815
- fs: {
8816
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
8817
- predecessor: getPredecessorDate({ direction: "ef" })
8818
- },
8819
- ss: {
8820
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
8821
- predecessor: getPredecessorDate({ direction: "es" })
8822
- },
8823
- ff: {
8824
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
8825
- predecessor: getPredecessorDate({ direction: "ef" })
8826
- },
8827
- sf: {
8828
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
8829
- predecessor: getPredecessorDate({ direction: "es" })
8830
- }
8831
- };
8832
- }
8833
- if (constraint === CONSTRAINT_TYPES.FNLT) {
8834
- return {
8835
- fs: {
8836
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
8837
- predecessor: getPredecessorDate({ direction: "ef" })
8838
- },
8839
- ss: {
8840
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
8841
- predecessor: getPredecessorDate({ direction: "es" })
8842
- },
8843
- ff: {
8844
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
8845
- predecessor: getPredecessorDate({ direction: "ef" })
8846
- },
8847
- sf: {
8848
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
8849
- predecessor: getPredecessorDate({ direction: "es" })
8850
- }
8851
- };
8852
- }
8853
9222
  }
8854
9223
  calculateMsoMfo(constraint) {
8855
9224
  const CONSTRAINT_DATE2 = this.activity.constraint_date || /* @__PURE__ */ new Date();
8856
- const DATE_TO_CALCULATE = constraint === CONSTRAINT_TYPES.MSO ? this.forwardMap.get(this.activity.id).es : this.forwardMap.get(this.activity.id).ef;
9225
+ const calculation = this.forwardMap.get(this.activity.id);
9226
+ const DATE_TO_CALCULATE = constraint === CONSTRAINT_TYPES.MSO ? calculation?.es : calculation?.ef;
8857
9227
  const freeSlack = this.calculationWithCalendar(
8858
9228
  CONSTRAINT_DATE2,
8859
9229
  DATE_TO_CALCULATE
@@ -8867,6 +9237,9 @@ var FreeFloat = class {
8867
9237
  return filterValues.length ? Math.min(...filterValues) : 0;
8868
9238
  }
8869
9239
  calculationWithCalendar(dateOne, dateTwo) {
9240
+ if (!(dateOne instanceof Date) || !(dateTwo instanceof Date) || !Number.isFinite(dateOne.getTime()) || !Number.isFinite(dateTwo.getTime())) {
9241
+ return Number.NaN;
9242
+ }
8870
9243
  const key = `${dateTwo.getTime()}_${dateOne.getTime()}`;
8871
9244
  if (this.durationCache.has(key)) {
8872
9245
  return this.durationCache.get(key);
@@ -8880,7 +9253,7 @@ var FreeFloat = class {
8880
9253
  return result;
8881
9254
  }
8882
9255
  parseHoursToDays(date2) {
8883
- if (!date2) {
9256
+ if (date2 === void 0 || date2 === null) {
8884
9257
  return 0;
8885
9258
  }
8886
9259
  return date2 / this.gantt.config.hoursPerDay;
@@ -8946,6 +9319,7 @@ var CriticalPath = class {
8946
9319
  this.backwardPathCalculations = null;
8947
9320
  this.totalFloat = /* @__PURE__ */ new Map();
8948
9321
  this.freeFloat = /* @__PURE__ */ new Map();
9322
+ this.linkResidualHours = /* @__PURE__ */ new Map();
8949
9323
  this.currentCalculationId = 0;
8950
9324
  }
8951
9325
  getCurrentCalculationId = () => this.currentCalculationId;
@@ -8976,6 +9350,13 @@ var CriticalPath = class {
8976
9350
  if (!isCurrent()) return false;
8977
9351
  this.forwardPathCalculations = forwardPathCalculations;
8978
9352
  this.backwardPathCalculations = backwardPath.calculations;
9353
+ const linkResidualHours = await this.calculateLinkResidualHours(
9354
+ forwardPath.linkCandidates,
9355
+ forwardPathCalculations,
9356
+ isCurrent
9357
+ );
9358
+ if (!linkResidualHours || !isCurrent()) return false;
9359
+ this.linkResidualHours = linkResidualHours;
8979
9360
  const { totalFloatMap, freeFloatMap } = await this.calculateFloatsAsync(
8980
9361
  forwardPath,
8981
9362
  backwardPath,
@@ -8995,6 +9376,38 @@ var CriticalPath = class {
8995
9376
  throw error;
8996
9377
  }
8997
9378
  }
9379
+ async calculateLinkResidualHours(linkCandidates, forwardCalculations, isCurrent = () => true) {
9380
+ const residuals = /* @__PURE__ */ new Map();
9381
+ let sliceStartTime = performance.now();
9382
+ for (const [linkId, candidate] of linkCandidates) {
9383
+ sliceStartTime = await yieldIfNeeded2(sliceStartTime);
9384
+ if (!isCurrent()) return null;
9385
+ const targetId = Number(candidate.targetId);
9386
+ const target = this.gantt.getTask(targetId);
9387
+ const settled = forwardCalculations.get(targetId);
9388
+ const candidateEf = candidate.ef;
9389
+ const settledEf = settled?.ef;
9390
+ const calendar = target && this.gantt.getCalendar(target.calendar_id);
9391
+ if (!calendar || !(candidateEf instanceof Date) || !(settledEf instanceof Date) || !Number.isFinite(candidateEf.getTime()) || !Number.isFinite(settledEf.getTime())) {
9392
+ residuals.set(linkId, null);
9393
+ continue;
9394
+ }
9395
+ const candidateTime = candidateEf.getTime();
9396
+ const settledTime = settledEf.getTime();
9397
+ const duration = settledTime >= candidateTime ? calendar.calculateDuration({
9398
+ start_date: candidateEf,
9399
+ end_date: settledEf
9400
+ }) : -calendar.calculateDuration({
9401
+ start_date: settledEf,
9402
+ end_date: candidateEf
9403
+ });
9404
+ residuals.set(
9405
+ linkId,
9406
+ Number.isFinite(duration) ? Object.is(duration, -0) ? 0 : duration : null
9407
+ );
9408
+ }
9409
+ return residuals;
9410
+ }
8998
9411
  async calculateFloatsAsync(forward, backward, isCurrent) {
8999
9412
  try {
9000
9413
  const projectDates = this.gantt.getSubtaskDates();
@@ -9098,14 +9511,52 @@ function computeLevels(state) {
9098
9511
  for (const id of activitiesById.keys()) resolveLevel(id, /* @__PURE__ */ new Set());
9099
9512
  return levelById;
9100
9513
  }
9101
- function toLegacyTasks(state) {
9514
+ function createLegacyIds(state) {
9515
+ const canonicalIds = /* @__PURE__ */ new Set();
9516
+ state.forEachActivity((activity, id) => {
9517
+ canonicalIds.add(String(id));
9518
+ if (activity.parentId !== null) canonicalIds.add(String(activity.parentId));
9519
+ });
9520
+ const numericCounts = /* @__PURE__ */ new Map();
9521
+ for (const id of canonicalIds) {
9522
+ const numeric = Number(id);
9523
+ if (Number.isSafeInteger(numeric) && numeric !== 0) {
9524
+ numericCounts.set(numeric, (numericCounts.get(numeric) ?? 0) + 1);
9525
+ }
9526
+ }
9527
+ const canonicalToLegacy = /* @__PURE__ */ new Map();
9528
+ const legacyToCanonical = /* @__PURE__ */ new Map();
9529
+ for (const id of canonicalIds) {
9530
+ const numeric = Number(id);
9531
+ if (Number.isSafeInteger(numeric) && numeric !== 0 && numericCounts.get(numeric) === 1) {
9532
+ canonicalToLegacy.set(id, numeric);
9533
+ legacyToCanonical.set(numeric, id);
9534
+ }
9535
+ }
9536
+ let next = 1;
9537
+ const toLegacy = (rawId) => {
9538
+ const id = String(rawId);
9539
+ const existing = canonicalToLegacy.get(id);
9540
+ if (existing !== void 0) return existing;
9541
+ while (legacyToCanonical.has(next)) next += 1;
9542
+ canonicalToLegacy.set(id, next);
9543
+ legacyToCanonical.set(next, id);
9544
+ return next++;
9545
+ };
9546
+ for (const id of canonicalIds) toLegacy(id);
9547
+ return {
9548
+ toLegacy,
9549
+ toCanonical: (id) => typeof id === "number" ? legacyToCanonical.get(id) ?? null : canonicalToLegacy.has(String(id)) ? String(id) : null
9550
+ };
9551
+ }
9552
+ function toLegacyTasks(state, ids) {
9102
9553
  const levelById = computeLevels(state);
9103
9554
  const leveled = /* @__PURE__ */ new Map();
9104
9555
  state.forEachActivity((activity, id) => {
9105
9556
  const key = String(id);
9106
9557
  leveled.set(key, {
9107
- id: Number(id),
9108
- parent: activity.parentId === null ? 0 : Number(activity.parentId),
9558
+ id: ids.toLegacy(key),
9559
+ parent: activity.parentId === null ? 0 : ids.toLegacy(activity.parentId),
9109
9560
  $level: levelById.get(key) ?? 0,
9110
9561
  $source: state.getOutgoingLinkIds(key),
9111
9562
  $target: state.getIncomingLinkIds(key),
@@ -9179,7 +9630,8 @@ async function createGanttAdapter(state, hoursPerDay) {
9179
9630
  };
9180
9631
  const bounds = projectBounds(state);
9181
9632
  await yieldToBrowser();
9182
- const leveled = toLegacyTasks(state);
9633
+ const ids = createLegacyIds(state);
9634
+ const leveled = toLegacyTasks(state, ids);
9183
9635
  await yieldToBrowser();
9184
9636
  const byTime = [...leveled.values()].sort(
9185
9637
  (a, b) => (a.start_date?.getTime() ?? 0) - (b.start_date?.getTime() ?? 0)
@@ -9189,24 +9641,31 @@ async function createGanttAdapter(state, hoursPerDay) {
9189
9641
  return {
9190
9642
  fullyParsed: true,
9191
9643
  config: { hoursPerDay },
9192
- getTask: (id) => leveled.get(String(id)) ?? null,
9644
+ getTask: (id) => {
9645
+ const canonicalId = ids.toCanonical(id);
9646
+ return canonicalId === null ? null : leveled.get(canonicalId) ?? null;
9647
+ },
9193
9648
  getLink: (id) => {
9194
9649
  const link = state.getLink(id);
9195
9650
  if (!link) return null;
9196
9651
  return {
9197
9652
  ...link,
9198
- source: Number(link.source),
9199
- target: Number(link.target),
9653
+ source: ids.toLegacy(link.source),
9654
+ target: ids.toLegacy(link.target),
9200
9655
  lag: link.lag
9201
9656
  };
9202
9657
  },
9203
9658
  getCalendar: resolveCalendar,
9204
- getChildren: (parentId) => state.getChildren(parentId).map((childId) => Number(childId)),
9659
+ getChildren: (parentId) => {
9660
+ const canonicalId = ids.toCanonical(parentId);
9661
+ return canonicalId === null ? [] : state.getChildren(canonicalId).map(ids.toLegacy);
9662
+ },
9205
9663
  eachTask: (cb) => {
9206
9664
  for (const task of treeOrdered) cb(task);
9207
9665
  },
9208
9666
  getTaskByTime: () => byTime,
9209
9667
  getSubtaskDates: () => bounds,
9668
+ getCanonicalActivityId: ids.toCanonical,
9210
9669
  refreshData: () => {
9211
9670
  }
9212
9671
  };
@@ -9214,21 +9673,92 @@ async function createGanttAdapter(state, hoursPerDay) {
9214
9673
 
9215
9674
  // src/critical-path/run-critical-path.ts
9216
9675
  var CP_WRITE_YIELD_EVERY = 1e3;
9217
- function fieldsForActivity(activityId, progress, maps) {
9218
- const toDate = (value) => value == null ? null : new Date(value);
9219
- const forward = maps.forward?.get(activityId);
9220
- const backward = maps.backward?.get(activityId);
9221
- const totalSlack = maps.totalFloat.get(activityId) ?? null;
9676
+ function fieldsForLink(rawResidual, sourceIsCritical, targetIsCritical, targetProgress) {
9677
+ const freeSlackHours = typeof rawResidual === "number" && Number.isFinite(rawResidual) ? Object.is(rawResidual, -0) ? 0 : rawResidual : null;
9222
9678
  return {
9223
- criticalPath: {
9224
- earlyStart: toDate(forward?.es),
9679
+ freeSlackHours,
9680
+ isCritical: freeSlackHours !== null && freeSlackHours <= 0 && sourceIsCritical && targetIsCritical && targetProgress === 0
9681
+ };
9682
+ }
9683
+ async function collectActivityFields(state, maps, hoursPerDay, toCanonicalActivityId, isCurrent) {
9684
+ const fields = /* @__PURE__ */ new Map();
9685
+ let sinceYield = 0;
9686
+ for (const calculationId of maps.forward?.keys() ?? []) {
9687
+ if (++sinceYield >= CP_WRITE_YIELD_EVERY) {
9688
+ sinceYield = 0;
9689
+ await yieldToBrowser();
9690
+ if (!isCurrent()) return null;
9691
+ }
9692
+ const activityId = toCanonicalActivityId(calculationId);
9693
+ if (activityId === null) continue;
9694
+ const activity = state.getActivity(activityId);
9695
+ if (!activity) continue;
9696
+ fields.set(
9697
+ String(activityId),
9698
+ fieldsForActivity(
9699
+ calculationId,
9700
+ Number(activity.progress ?? 0),
9701
+ maps,
9702
+ hoursPerDay
9703
+ )
9704
+ );
9705
+ }
9706
+ return isCurrent() ? fields : null;
9707
+ }
9708
+ async function collectLinkFields(state, linkResidualHours, activityFields, isCurrent) {
9709
+ const fields = /* @__PURE__ */ new Map();
9710
+ let sinceYield = 0;
9711
+ for (const link of state.getAllLinks()) {
9712
+ if (++sinceYield >= CP_WRITE_YIELD_EVERY) {
9713
+ sinceYield = 0;
9714
+ await yieldToBrowser();
9715
+ if (!isCurrent()) return null;
9716
+ }
9717
+ fields.set(
9718
+ String(link.id),
9719
+ fieldsForLink(
9720
+ linkResidualHours.get(String(link.id)),
9721
+ activityFields.get(link.source)?.isCritical === true,
9722
+ activityFields.get(link.target)?.isCritical === true,
9723
+ state.getActivity(link.target)?.progress
9724
+ )
9725
+ );
9726
+ }
9727
+ return isCurrent() ? fields : null;
9728
+ }
9729
+ var slackDaysToHours = (valueDays, hoursPerDay) => {
9730
+ if (typeof valueDays !== "number" || !Number.isFinite(valueDays) || !Number.isFinite(hoursPerDay) || hoursPerDay <= 0) {
9731
+ return null;
9732
+ }
9733
+ const hours = valueDays * hoursPerDay;
9734
+ if (!Number.isFinite(hours)) return null;
9735
+ return Object.is(hours, -0) ? 0 : hours;
9736
+ };
9737
+ function fieldsForActivity(activityId, progress, maps, hoursPerDay) {
9738
+ const toDate = (value) => {
9739
+ if (value == null) return null;
9740
+ const date2 = new Date(value);
9741
+ return Number.isFinite(date2.getTime()) ? date2 : null;
9742
+ };
9743
+ const forward = maps.forward?.get(activityId);
9744
+ const backward = maps.backward?.get(activityId);
9745
+ const totalSlackHours = slackDaysToHours(
9746
+ maps.totalFloat.get(activityId),
9747
+ hoursPerDay
9748
+ );
9749
+ return {
9750
+ criticalPath: {
9751
+ earlyStart: toDate(forward?.es),
9225
9752
  earlyFinish: toDate(forward?.ef),
9226
9753
  lateStart: toDate(backward?.ls),
9227
9754
  lateFinish: toDate(backward?.lf),
9228
- totalSlackHours: totalSlack
9755
+ totalSlackHours
9229
9756
  },
9230
- isCritical: progress < 100 && totalSlack != null && totalSlack <= 0,
9231
- freeSlackHours: maps.freeFloat.get(activityId)?.activityFreeFloat ?? null
9757
+ isCritical: progress < 100 && totalSlackHours !== null && totalSlackHours <= 0,
9758
+ freeSlackHours: slackDaysToHours(
9759
+ maps.freeFloat.get(activityId)?.activityFreeFloat,
9760
+ hoursPerDay
9761
+ )
9232
9762
  };
9233
9763
  }
9234
9764
  var datesEqual = (left, right) => left === right || left !== null && right !== null && left.getTime() === right.getTime();
@@ -9241,6 +9771,14 @@ function setCriticalPathFieldsIfChanged(state, activityId, fields) {
9241
9771
  }
9242
9772
  state.setActivityFields(activityId, fields);
9243
9773
  }
9774
+ function setCriticalPathLinkFieldsIfChanged(state, linkId, fields) {
9775
+ const link = state.getLink(linkId);
9776
+ if (!link || Object.is(link.freeSlackHours, fields.freeSlackHours) && Object.is(link.isCritical, fields.isCritical)) {
9777
+ return;
9778
+ }
9779
+ state.setLinkField(linkId, "freeSlackHours", fields.freeSlackHours);
9780
+ state.setLinkField(linkId, "isCritical", fields.isCritical);
9781
+ }
9244
9782
  async function computeCriticalPathFields(state, hoursPerDay, isCurrent = () => true) {
9245
9783
  if (!isCurrent()) return null;
9246
9784
  const gantt = await createGanttAdapter(state, hoursPerDay);
@@ -9258,61 +9796,36 @@ async function computeCriticalPathFields(state, hoursPerDay, isCurrent = () => t
9258
9796
  totalFloat: criticalPath.totalFloat,
9259
9797
  freeFloat: criticalPath.freeFloat
9260
9798
  };
9261
- const activityIds = /* @__PURE__ */ new Set([...maps.forward?.keys() ?? []]);
9262
- const fieldsByActivity = /* @__PURE__ */ new Map();
9263
- let sinceYield = 0;
9264
- for (const activityId of activityIds) {
9265
- if (++sinceYield >= CP_WRITE_YIELD_EVERY) {
9266
- sinceYield = 0;
9267
- await yieldToBrowser();
9268
- if (!isCurrent()) return null;
9269
- }
9270
- const activity = state.getActivity(activityId);
9271
- if (!activity) continue;
9272
- const progress = Number(activity.progress ?? 0);
9273
- fieldsByActivity.set(
9274
- activityId,
9275
- fieldsForActivity(activityId, progress, maps)
9276
- );
9277
- }
9278
- return isCurrent() ? fieldsByActivity : null;
9279
- }
9280
- async function applyCriticalPath(state, hoursPerDay, isCurrent = () => true) {
9281
- const fieldsByActivity = await computeCriticalPathFields(
9799
+ const activities = await collectActivityFields(
9282
9800
  state,
9801
+ maps,
9283
9802
  hoursPerDay,
9803
+ gantt.getCanonicalActivityId,
9804
+ isCurrent
9805
+ );
9806
+ if (!activities) return null;
9807
+ const links = await collectLinkFields(
9808
+ state,
9809
+ criticalPath.linkResidualHours,
9810
+ activities,
9284
9811
  isCurrent
9285
9812
  );
9286
- if (!fieldsByActivity || !isCurrent()) return false;
9287
- for (const [activityId, fields] of fieldsByActivity) {
9813
+ return links ? { activities, links } : null;
9814
+ }
9815
+ async function applyCriticalPath(state, hoursPerDay, isCurrent = () => true) {
9816
+ const fields = await computeCriticalPathFields(state, hoursPerDay, isCurrent);
9817
+ if (!fields || !isCurrent()) return false;
9818
+ for (const [activityId, activityFields] of fields.activities) {
9288
9819
  if (!isCurrent()) return false;
9289
- setCriticalPathFieldsIfChanged(state, activityId, fields);
9820
+ setCriticalPathFieldsIfChanged(state, activityId, activityFields);
9821
+ }
9822
+ for (const [linkId, linkFields] of fields.links) {
9823
+ if (!isCurrent()) return false;
9824
+ setCriticalPathLinkFieldsIfChanged(state, linkId, linkFields);
9290
9825
  }
9291
9826
  return true;
9292
9827
  }
9293
9828
 
9294
- // src/constants/activity-properties.ts
9295
- var ACTIVITY_PROPERTY = {
9296
- PARENT: "parentId",
9297
- TYPE: "type",
9298
- START_DATE: "startDate",
9299
- END_DATE: "endDate",
9300
- CONSTRAINT_TYPE: "constraintType",
9301
- CONSTRAINT_DATE: "constraintDate",
9302
- PROGRESS: "progress",
9303
- COST: "cost",
9304
- USED_COST: "usedCost",
9305
- REAL_COST: "realCost",
9306
- WORK_HOURS: "workHours",
9307
- REAL_WORK_HOURS: "realWorkHours",
9308
- CORRELATIVE_ID: "correlativeId",
9309
- CUSTOM_ID: "customId",
9310
- NEW_ACTIVITIES_ARRAY: "newActivityIds",
9311
- HAS_NEW_ACTIVITIES: "hasNewActivities"};
9312
- var LINK_PROPERTY = {
9313
- TYPE: "type",
9314
- LAG: "lag"};
9315
-
9316
9829
  // src/propagations/upward/real-cost.ts
9317
9830
  function emitRealCost(state) {
9318
9831
  const rootIds = [];
@@ -9365,6 +9878,8 @@ var NON_SCHEDULING_INLINE_COLUMNS = /* @__PURE__ */ new Set([
9365
9878
  COLUMN.TAGS
9366
9879
  ]);
9367
9880
  var NON_SCHEDULING_KINDS = /* @__PURE__ */ new Set([
9881
+ "activity-clipboard-clear",
9882
+ "activity-copy",
9368
9883
  "selection-toggle",
9369
9884
  "selection-replace",
9370
9885
  "visibility-set",
@@ -9378,6 +9893,8 @@ var NON_SCHEDULING_KINDS = /* @__PURE__ */ new Set([
9378
9893
  "status-criteria-set"
9379
9894
  ]);
9380
9895
  var PURE_VIEW_STATE_KINDS = /* @__PURE__ */ new Set([
9896
+ "activity-clipboard-clear",
9897
+ "activity-copy",
9381
9898
  "selection-toggle",
9382
9899
  "selection-replace",
9383
9900
  "visibility-set",
@@ -9406,9 +9923,15 @@ function isSchedulingDispatch(action, structuralStaleKinds) {
9406
9923
  return true;
9407
9924
  }
9408
9925
  function dispatchChangesSchedulingState(action) {
9926
+ if (action.kind === "activity-lookahead-sync") {
9927
+ return action.persistedProgress !== void 0;
9928
+ }
9409
9929
  return isSchedulingDispatch(action, NO_STRUCTURAL_EXEMPTIONS);
9410
9930
  }
9411
9931
  function resolveRunCriticalPath(action) {
9932
+ if (action.kind === "activity-lookahead-sync") {
9933
+ return action.persistedProgress !== void 0;
9934
+ }
9412
9935
  return isSchedulingDispatch(
9413
9936
  action,
9414
9937
  STRUCTURAL_KINDS_LEAVING_CRITICAL_PATH_STALE
@@ -9694,6 +10217,23 @@ function detectSirAutoReject(beforeSnap, adapter, touchedIds) {
9694
10217
  return effects;
9695
10218
  }
9696
10219
 
10220
+ // src/dispatch/conversion.ts
10221
+ function snapshotToLink(l) {
10222
+ const view = {
10223
+ id: String(l.id),
10224
+ source: String(l.source),
10225
+ target: String(l.target),
10226
+ type: l.type,
10227
+ lag: Number(l.lag),
10228
+ freeSlackHours: typeof l.freeSlackHours === "number" && Number.isFinite(l.freeSlackHours) ? l.freeSlackHours : null,
10229
+ isCritical: l.isCritical === true
10230
+ };
10231
+ if (l.proplannerId != null) view.proplannerId = Number(l.proplannerId);
10232
+ if (l.ganttId != null) view.ganttId = Number(l.ganttId);
10233
+ if (l.sectorId != null) view.sectorId = Number(l.sectorId);
10234
+ return view;
10235
+ }
10236
+
9697
10237
  // src/dispatch/shared/change-set.ts
9698
10238
  async function assembleChangeSet(adapter, args) {
9699
10239
  const allTouched = collectChangeSetIds(adapter, args);
@@ -9703,7 +10243,8 @@ async function assembleChangeSet(adapter, args) {
9703
10243
  const insertedIds = collectInsertedIds(captured);
9704
10244
  mergeDirtyBeforeImages(adapter, captured, insertedIds, before, allTouched);
9705
10245
  }
9706
- const beforeDiffEffects = args.sirDetection === "before-diff" ? detectSirAutoReject(before, adapter, allTouched) : [];
10246
+ const sirDetection = args.sirDetection;
10247
+ const beforeDiffEffects = sirDetection === "before-diff" ? detectSirAutoReject(before, adapter, allTouched) : [];
9707
10248
  const correlativeBefore = buildCorrelativeBeforeMap(
9708
10249
  args.correlativeShifts,
9709
10250
  allTouched
@@ -9714,18 +10255,91 @@ async function assembleChangeSet(adapter, args) {
9714
10255
  allTouched,
9715
10256
  correlativeBefore
9716
10257
  );
9717
- const effects = args.sirDetection === "after-diff" ? detectSirAutoReject(before, adapter, allTouched) : beforeDiffEffects;
10258
+ const effects = sirDetection === "after-diff" ? detectSirAutoReject(before, adapter, allTouched) : beforeDiffEffects;
9718
10259
  const warnings = args.warnings ?? [];
10260
+ const linkChanges = mergeLinkChanges(
10261
+ args.links ?? [],
10262
+ captured ? buildCapturedCriticalPathLinkChanges(adapter, captured) : []
10263
+ );
9719
10264
  return {
9720
10265
  source: args.source,
9721
10266
  activities: activityChanges,
9722
- links: args.links ?? [],
10267
+ links: linkChanges,
9723
10268
  calendars: [],
9724
10269
  trackingEvents: args.trackingEvents ?? [],
9725
10270
  ...effects.length > 0 ? { effects } : {},
9726
10271
  ...warnings.length > 0 ? { warnings } : {}
9727
10272
  };
9728
10273
  }
10274
+ function buildCapturedCriticalPathLinkChanges(adapter, captured) {
10275
+ const changes = [];
10276
+ for (const operation of captured.ops) {
10277
+ if (operation.kind !== "link-field") continue;
10278
+ const after = adapter.getLink(operation.linkId);
10279
+ if (!after) continue;
10280
+ const fields = {};
10281
+ for (const field of ["freeSlackHours", "isCritical"]) {
10282
+ if (!Object.is(operation.before[field], after[field])) {
10283
+ fields[field] = {
10284
+ before: operation.before[field],
10285
+ after: after[field]
10286
+ };
10287
+ }
10288
+ }
10289
+ if (Object.keys(fields).length === 0) continue;
10290
+ changes.push({
10291
+ id: operation.linkId,
10292
+ kind: "updated",
10293
+ fields,
10294
+ after: snapshotToLink(after)
10295
+ });
10296
+ }
10297
+ return changes;
10298
+ }
10299
+ function mergeLinkChanges(explicit, captured) {
10300
+ const merged = [];
10301
+ const indexById = /* @__PURE__ */ new Map();
10302
+ for (const change of explicit) {
10303
+ const index = indexById.get(change.id);
10304
+ if (index === void 0) {
10305
+ indexById.set(change.id, merged.length);
10306
+ merged.push(change);
10307
+ continue;
10308
+ }
10309
+ const current = merged[index];
10310
+ if ((current.kind === "updated" || current.kind === "created") && change.kind === "updated") {
10311
+ merged[index] = {
10312
+ ...current,
10313
+ fields: { ...current.fields, ...change.fields },
10314
+ after: change.after
10315
+ };
10316
+ continue;
10317
+ }
10318
+ merged[index] = change;
10319
+ }
10320
+ for (const change of captured) {
10321
+ const index = indexById.get(change.id);
10322
+ if (index === void 0) {
10323
+ indexById.set(change.id, merged.length);
10324
+ merged.push(change);
10325
+ continue;
10326
+ }
10327
+ const current = merged[index];
10328
+ if (current.kind === "deleted") continue;
10329
+ const capturedFields = current.kind === "created" ? Object.fromEntries(
10330
+ Object.entries(change.fields ?? {}).map(([field, value]) => [
10331
+ field,
10332
+ { before: void 0, after: value.after }
10333
+ ])
10334
+ ) : change.fields;
10335
+ merged[index] = {
10336
+ ...current,
10337
+ fields: { ...current.fields, ...capturedFields },
10338
+ after: change.after
10339
+ };
10340
+ }
10341
+ return merged;
10342
+ }
9729
10343
  function buildCorrelativeBeforeMap(shifts, touched) {
9730
10344
  if (!shifts || shifts.length === 0) return void 0;
9731
10345
  const correlativeBefore = /* @__PURE__ */ new Map();
@@ -9761,21 +10375,6 @@ function collectInsertedIds(captured) {
9761
10375
  );
9762
10376
  }
9763
10377
 
9764
- // src/dispatch/conversion.ts
9765
- function snapshotToLink(l) {
9766
- const view = {
9767
- id: String(l.id),
9768
- source: String(l.source),
9769
- target: String(l.target),
9770
- type: l.type,
9771
- lag: Number(l.lag)
9772
- };
9773
- if (l.proplannerId != null) view.proplannerId = Number(l.proplannerId);
9774
- if (l.ganttId != null) view.ganttId = Number(l.ganttId);
9775
- if (l.sectorId != null) view.sectorId = Number(l.sectorId);
9776
- return view;
9777
- }
9778
-
9779
10378
  // src/dispatch/shared/helpers.ts
9780
10379
  function buildLinkChangesForBatch(applied, beforeLinks, adapter) {
9781
10380
  const out = [];
@@ -10120,6 +10719,26 @@ function collectDeterministicIds(operations) {
10120
10719
  });
10121
10720
  return ids.size > 0 ? ids : void 0;
10122
10721
  }
10722
+ function resolveDeterministicIds(deterministicIds, deps) {
10723
+ if (deterministicIds === void 0) return void 0;
10724
+ const resolved = /* @__PURE__ */ new Map();
10725
+ const assigned = /* @__PURE__ */ new Set();
10726
+ const nextAvailableId = () => {
10727
+ let candidate = deps.linkIdGen.next();
10728
+ while (deps.adapter.getLink(candidate) !== null || deps.isLinkIdReserved(String(candidate)) || assigned.has(String(candidate))) {
10729
+ candidate = deps.linkIdGen.next();
10730
+ }
10731
+ return candidate;
10732
+ };
10733
+ for (const [operationIndex, explicitId] of deterministicIds) {
10734
+ const isReserved = deps.adapter.getLink(explicitId) !== null || deps.isLinkIdReserved(String(explicitId)) || assigned.has(String(explicitId));
10735
+ const linkId = isReserved ? nextAvailableId() : explicitId;
10736
+ if (!isReserved) deps.linkIdGen.bump(explicitId);
10737
+ assigned.add(String(linkId));
10738
+ resolved.set(operationIndex, linkId);
10739
+ }
10740
+ return resolved;
10741
+ }
10123
10742
  function batchOpToLinkOperation(op, hoursPerDay) {
10124
10743
  if (op.kind === "create") {
10125
10744
  return {
@@ -10152,7 +10771,7 @@ async function dispatchLinkBatch(operations, source, options, deps, deterministi
10152
10771
  operations,
10153
10772
  adapter,
10154
10773
  linkIdGen,
10155
- deterministicIds
10774
+ resolveDeterministicIds(deterministicIds, deps)
10156
10775
  );
10157
10776
  if (operations.length === 1 && applied[0] && applied[0].rejected !== null && source.kind !== "inline-edit") {
10158
10777
  return { ok: false, reason: applied[0].rejected };
@@ -10249,7 +10868,7 @@ async function dispatchInlineEditLinks(action, options, deps) {
10249
10868
  const { specs: specsInDays } = parsePredecessorString(action.newValue);
10250
10869
  const specs = specsInDays.map((spec) => ({
10251
10870
  ...spec,
10252
- lag: lagDaysToHours(spec.lag, sector.hoursPerDay)
10871
+ lag: lagDaysToHours(roundLagDays(spec.lag), sector.hoursPerDay)
10253
10872
  }));
10254
10873
  const byCorrelative = /* @__PURE__ */ new Map();
10255
10874
  adapter.forEachActivity((a) => {
@@ -10334,26 +10953,19 @@ function recomputeHhCascadeForParent(startParentId, adapter) {
10334
10953
  if (!startParentId || startParentId === "0") return;
10335
10954
  let currentParentId = String(startParentId);
10336
10955
  while (currentParentId !== null) {
10337
- const childIds = adapter.getChildren(currentParentId);
10338
- let hhSum = 0;
10339
- let rwSum = 0;
10340
- for (const childId of childIds) {
10341
- const child = adapter.getActivity(childId);
10342
- if (!child) continue;
10343
- const hh = readNumber(child, ACTIVITY_PROPERTY.WORK_HOURS);
10344
- const rw = readChildRealWork(childId, child, hh, adapter);
10345
- hhSum += hh;
10346
- rwSum += rw;
10347
- }
10956
+ const { workHours, realWorkHours } = computeParentHhRollup(
10957
+ currentParentId,
10958
+ adapter
10959
+ );
10348
10960
  adapter.setActivityField(
10349
10961
  currentParentId,
10350
10962
  ACTIVITY_PROPERTY.WORK_HOURS,
10351
- hhSum
10963
+ workHours
10352
10964
  );
10353
10965
  adapter.setActivityField(
10354
10966
  currentParentId,
10355
10967
  ACTIVITY_PROPERTY.REAL_WORK_HOURS,
10356
- rwSum
10968
+ realWorkHours
10357
10969
  );
10358
10970
  const parentSnap = adapter.getActivity(currentParentId);
10359
10971
  if (!parentSnap) break;
@@ -10369,6 +10981,40 @@ function recomputeHhCascadeForActivity(activityId, adapter) {
10369
10981
  if (!parentId || isRootParent(parentId)) return;
10370
10982
  recomputeHhCascadeForParent(parentId, adapter);
10371
10983
  }
10984
+ function recomputeRealWorkFromProgress(progressIds, parentIdsDeepestFirst, adapter) {
10985
+ for (const activityId of progressIds) {
10986
+ if (adapter.hasChildren(activityId)) continue;
10987
+ const activity = adapter.getActivity(activityId);
10988
+ if (!activity) continue;
10989
+ adapter.setActivityField(
10990
+ activityId,
10991
+ ACTIVITY_PROPERTY.REAL_WORK_HOURS,
10992
+ computeLeafRealWork(
10993
+ readNumber(activity, ACTIVITY_PROPERTY.WORK_HOURS),
10994
+ readNumber(activity, ACTIVITY_PROPERTY.PROGRESS)
10995
+ )
10996
+ );
10997
+ }
10998
+ for (const parentId of parentIdsDeepestFirst) {
10999
+ adapter.setActivityField(
11000
+ parentId,
11001
+ ACTIVITY_PROPERTY.REAL_WORK_HOURS,
11002
+ computeParentHhRollup(parentId, adapter).realWorkHours
11003
+ );
11004
+ }
11005
+ }
11006
+ function computeParentHhRollup(parentId, adapter) {
11007
+ let workHours = 0;
11008
+ let realWorkHours = 0;
11009
+ for (const childId of adapter.getChildren(parentId)) {
11010
+ const child = adapter.getActivity(childId);
11011
+ if (!child) continue;
11012
+ const childWorkHours = readNumber(child, ACTIVITY_PROPERTY.WORK_HOURS);
11013
+ workHours += childWorkHours;
11014
+ realWorkHours += readChildRealWork(childId, child, childWorkHours, adapter);
11015
+ }
11016
+ return { workHours, realWorkHours };
11017
+ }
10372
11018
  function readNumber(activity, key) {
10373
11019
  const v = activity[key];
10374
11020
  if (typeof v === "number" && Number.isFinite(v)) return v;
@@ -10586,6 +11232,11 @@ function buildNewActivity(input, adapter) {
10586
11232
  ...base,
10587
11233
  startDate,
10588
11234
  endDate,
11235
+ calendarDuration: computeCalendarDuration({
11236
+ type: base.type,
11237
+ startDate,
11238
+ endDate
11239
+ }),
10589
11240
  newActivityIds: [...base.newActivityIds],
10590
11241
  pendingRequestIds: [...base.pendingRequestIds],
10591
11242
  responsableIds: [...base.responsableIds],
@@ -10736,6 +11387,36 @@ function computeFractionalCidAtIndex(adapter, parentKey, targetIndex, excludeId)
10736
11387
  return (before + after) / 2;
10737
11388
  }
10738
11389
 
11390
+ // src/dispatch/creation-overrides.ts
11391
+ var PUBLIC_CREATION_OVERRIDE_FIELDS = /* @__PURE__ */ new Set([
11392
+ ...[...CREATION_OVERRIDE_FIELDS].filter((field) => field !== "durationHours"),
11393
+ "durationDays"
11394
+ ]);
11395
+ function normalizeActivityCreationOverrides(raw, hoursPerDay) {
11396
+ if (raw === void 0) return { ok: true, value: {} };
11397
+ const prototype = Object.getPrototypeOf(raw);
11398
+ if (prototype !== null && prototype !== Object.prototype) {
11399
+ return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
11400
+ }
11401
+ for (const key of Reflect.ownKeys(raw)) {
11402
+ if (typeof key === "symbol" || !PUBLIC_CREATION_OVERRIDE_FIELDS.has(key)) {
11403
+ return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
11404
+ }
11405
+ }
11406
+ const { durationDays, ...rest } = raw;
11407
+ if (durationDays !== void 0 && (!Number.isFinite(durationDays) || durationDays < 0)) {
11408
+ return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
11409
+ }
11410
+ return {
11411
+ ok: true,
11412
+ value: {
11413
+ ...rest,
11414
+ ...durationDays === 0 && (rest.type === void 0 || rest.type === "task") ? { type: "milestone" } : {},
11415
+ ...durationDays === void 0 ? {} : { durationHours: durationToHours(durationDays, hoursPerDay) }
11416
+ }
11417
+ };
11418
+ }
11419
+
10739
11420
  // src/dispatch/handlers/activity-create.ts
10740
11421
  async function createActivityCore(action, deps, opts = {}) {
10741
11422
  const { adapter, scheduler, sector, calendars, activityIdGen, uidGen } = deps;
@@ -10743,7 +11424,12 @@ async function createActivityCore(action, deps, opts = {}) {
10743
11424
  const target = validateCreateTarget(action, adapter);
10744
11425
  if (!target.ok) return target;
10745
11426
  const { parent, anchorSiblingId, isRoot } = target;
10746
- const newId = resolveNewActivityId(action.activityId, adapter, activityIdGen);
11427
+ const newId = resolveNewActivityId(
11428
+ action.activityId,
11429
+ adapter,
11430
+ activityIdGen,
11431
+ deps.isActivityIdReserved
11432
+ );
10747
11433
  const newUid = uidGen.next();
10748
11434
  const activityInput = {
10749
11435
  id: newId,
@@ -10836,9 +11522,18 @@ function validateAnchorSibling(action, adapter, isRoot, anchorSiblingId) {
10836
11522
  }
10837
11523
  return null;
10838
11524
  }
10839
- function resolveNewActivityId(explicitId, adapter, activityIdGen) {
10840
- if (explicitId === void 0) return activityIdGen.next();
10841
- if (adapter.activityExists(explicitId)) return activityIdGen.next();
11525
+ function resolveNewActivityId(explicitId, adapter, activityIdGen, isActivityIdReserved) {
11526
+ const nextAvailableId = () => {
11527
+ let candidate = activityIdGen.next();
11528
+ while (adapter.activityExists(candidate) || isActivityIdReserved(String(candidate))) {
11529
+ candidate = activityIdGen.next();
11530
+ }
11531
+ return candidate;
11532
+ };
11533
+ if (explicitId === void 0) return nextAvailableId();
11534
+ if (adapter.activityExists(explicitId) || isActivityIdReserved(String(explicitId))) {
11535
+ return nextAvailableId();
11536
+ }
10842
11537
  activityIdGen.bump(explicitId);
10843
11538
  return explicitId;
10844
11539
  }
@@ -10917,7 +11612,15 @@ async function buildCreateChangeSet(adapter, action, coreResult, scheduledIds, h
10917
11612
  }
10918
11613
  async function dispatchActivityCreate(action, options, deps) {
10919
11614
  const { adapter, scheduler, sector } = deps;
10920
- const core = await createActivityCore(action, deps);
11615
+ const normalized2 = normalizeActivityCreationOverrides(
11616
+ action.overrides,
11617
+ sector.hoursPerDay
11618
+ );
11619
+ if (!normalized2.ok) return normalized2;
11620
+ const core = await createActivityCore(
11621
+ { ...action, overrides: normalized2.value },
11622
+ deps
11623
+ );
10921
11624
  if (!core.ok) return core;
10922
11625
  const { newId } = core;
10923
11626
  const createdAutoScheduling = core.newActivity.autoScheduling;
@@ -11129,7 +11832,7 @@ async function dispatchActivityDelete(action, options, deps) {
11129
11832
  for (const linkId of incidentLinkIds) {
11130
11833
  adapter.removeLink(linkId);
11131
11834
  }
11132
- const viewStateChanges = captureDeletedViewState(adapter, toDelete);
11835
+ const viewStateChanges2 = captureDeletedViewState(adapter, toDelete);
11133
11836
  let removed = 0;
11134
11837
  for (const id of toDelete) {
11135
11838
  removed += 1;
@@ -11218,7 +11921,7 @@ async function dispatchActivityDelete(action, options, deps) {
11218
11921
  });
11219
11922
  return {
11220
11923
  ok: true,
11221
- changes: viewStateChanges.length > 0 ? { ...changes, viewState: viewStateChanges } : changes,
11924
+ changes: viewStateChanges2.length > 0 ? { ...changes, viewState: viewStateChanges2 } : changes,
11222
11925
  __beforeSnap: deletedRowsSnap,
11223
11926
  __beforeLinks: beforeLinksByStringId
11224
11927
  };
@@ -11326,7 +12029,7 @@ async function dispatchActivityBatch(action, options, deps) {
11326
12029
  source: link.source,
11327
12030
  target: link.target,
11328
12031
  type: link.type,
11329
- lag: options.inputUnit === "hours" ? link.lag : lagDaysToHours(link.lag, deps.sector.hoursPerDay)
12032
+ lag: lagDaysToHours(link.lag, deps.sector.hoursPerDay)
11330
12033
  },
11331
12034
  linkId: link.linkId
11332
12035
  }));
@@ -11398,7 +12101,7 @@ function validateBatch(action, deps) {
11398
12101
  }
11399
12102
  if (item.activityId !== void 0) {
11400
12103
  const activityId = String(item.activityId);
11401
- if (activityId.length === 0 || activityId === "0" || explicitActivityIds.has(activityId) || action.mode === "append" && deps.adapter.getActivity(activityId) !== null) {
12104
+ if (activityId.length === 0 || activityId === "0" || explicitActivityIds.has(activityId)) {
11402
12105
  return REJECTION_REASON.INVALID;
11403
12106
  }
11404
12107
  explicitActivityIds.add(activityId);
@@ -11420,7 +12123,7 @@ function validateBatch(action, deps) {
11420
12123
  }
11421
12124
  if (link.linkId !== void 0) {
11422
12125
  const linkId = String(link.linkId);
11423
- if (linkId.length === 0 || explicitLinkIds.has(linkId) || action.mode === "append" && deps.adapter.getLink(linkId) !== null) {
12126
+ if (linkId.length === 0 || explicitLinkIds.has(linkId)) {
11424
12127
  return REJECTION_REASON.INVALID;
11425
12128
  }
11426
12129
  explicitLinkIds.add(linkId);
@@ -11456,24 +12159,38 @@ function hasParentCycle(items) {
11456
12159
  return false;
11457
12160
  }
11458
12161
  function buildBatchPlan(action, deps) {
12162
+ const normalizedOverrides = /* @__PURE__ */ new Map();
11459
12163
  for (const item of action.items) {
11460
- if (item.activityId !== void 0) {
11461
- deps.activityIdGen.bump(item.activityId);
11462
- }
12164
+ const normalized2 = normalizeActivityCreationOverrides(
12165
+ item.overrides,
12166
+ deps.sector.hoursPerDay
12167
+ );
12168
+ if (!normalized2.ok) return normalized2;
12169
+ normalizedOverrides.set(item.batchId, normalized2.value);
11463
12170
  }
11464
12171
  const activityIdsByBatchId = /* @__PURE__ */ new Map();
12172
+ const plannedActivityIds = /* @__PURE__ */ new Set();
11465
12173
  for (const item of action.items) {
11466
- activityIdsByBatchId.set(
11467
- item.batchId,
11468
- item.activityId ?? deps.activityIdGen.next()
12174
+ const activityId = resolvePlannedActivityId(
12175
+ item.activityId,
12176
+ action.mode,
12177
+ plannedActivityIds,
12178
+ deps
11469
12179
  );
11470
- }
11471
- for (const link of action.links) {
11472
- if (link.linkId !== void 0) deps.linkIdGen.bump(link.linkId);
11473
- }
11474
- const linkIds = action.links.map(
11475
- (link) => link.linkId ?? deps.linkIdGen.next()
11476
- );
12180
+ plannedActivityIds.add(String(activityId));
12181
+ activityIdsByBatchId.set(item.batchId, activityId);
12182
+ }
12183
+ const plannedLinkIds = /* @__PURE__ */ new Set();
12184
+ const linkIds = action.links.map((link) => {
12185
+ const linkId = resolvePlannedLinkId(
12186
+ link.linkId,
12187
+ action.mode,
12188
+ plannedLinkIds,
12189
+ deps
12190
+ );
12191
+ plannedLinkIds.add(String(linkId));
12192
+ return linkId;
12193
+ });
11477
12194
  const insertionCorrelativeIds = planInsertionCorrelatives(
11478
12195
  action,
11479
12196
  activityIdsByBatchId,
@@ -11496,7 +12213,8 @@ function buildBatchPlan(action, deps) {
11496
12213
  activityId,
11497
12214
  parentId,
11498
12215
  parentBatchId,
11499
- insertionCorrelativeId
12216
+ insertionCorrelativeId,
12217
+ overrides: normalizedOverrides.get(item.batchId) ?? {}
11500
12218
  });
11501
12219
  }
11502
12220
  const links = [];
@@ -11517,6 +12235,30 @@ function buildBatchPlan(action, deps) {
11517
12235
  }
11518
12236
  return { ok: true, plan: { items, links } };
11519
12237
  }
12238
+ function resolvePlannedActivityId(explicitId, mode, plannedIds, deps) {
12239
+ const explicitIsAvailable = explicitId !== void 0 && (mode === "replace" || deps.adapter.getActivity(explicitId) === null && !deps.isActivityIdReserved(String(explicitId))) && !plannedIds.has(String(explicitId));
12240
+ if (explicitIsAvailable) {
12241
+ deps.activityIdGen.bump(explicitId);
12242
+ return explicitId;
12243
+ }
12244
+ let candidate = deps.activityIdGen.next();
12245
+ while (plannedIds.has(String(candidate)) || mode === "append" && (deps.adapter.getActivity(candidate) !== null || deps.isActivityIdReserved(String(candidate)))) {
12246
+ candidate = deps.activityIdGen.next();
12247
+ }
12248
+ return candidate;
12249
+ }
12250
+ function resolvePlannedLinkId(explicitId, mode, plannedIds, deps) {
12251
+ const explicitIsAvailable = explicitId !== void 0 && (mode === "replace" || deps.adapter.getLink(explicitId) === null && !deps.isLinkIdReserved(String(explicitId))) && !plannedIds.has(String(explicitId));
12252
+ if (explicitIsAvailable) {
12253
+ deps.linkIdGen.bump(explicitId);
12254
+ return explicitId;
12255
+ }
12256
+ let candidate = deps.linkIdGen.next();
12257
+ while (plannedIds.has(String(candidate)) || mode === "append" && (deps.adapter.getLink(candidate) !== null || deps.isLinkIdReserved(String(candidate)))) {
12258
+ candidate = deps.linkIdGen.next();
12259
+ }
12260
+ return candidate;
12261
+ }
11520
12262
  function planInsertionCorrelatives(action, activityIdsByBatchId, deps) {
11521
12263
  if (isSimpleRootAppend(action)) {
11522
12264
  const firstCorrelativeId = deps.adapter.activityCount();
@@ -11626,7 +12368,7 @@ async function createPlannedActivities(action, plannedItems, deps) {
11626
12368
  {
11627
12369
  parentId: item.parentId,
11628
12370
  activityId: item.activityId,
11629
- ...item.input.overrides === void 0 ? {} : { overrides: item.input.overrides },
12371
+ overrides: item.overrides,
11630
12372
  ...action.eventSource === void 0 ? {} : { eventSource: action.eventSource }
11631
12373
  },
11632
12374
  deps,
@@ -11668,180 +12410,299 @@ function stringKeyedLinks(links) {
11668
12410
  return new Map(Array.from(links, ([linkId, link]) => [String(linkId), link]));
11669
12411
  }
11670
12412
 
11671
- // src/dispatch/handlers/activity-paste.ts
11672
- function resolvePasteRootDestination(destination, adapter) {
11673
- if ("afterSiblingId" in destination) {
11674
- const sibling = adapter.getActivity(destination.afterSiblingId);
11675
- if (!sibling) {
11676
- return { ok: false, reason: REJECTION_REASON.ANCHOR_SIBLING_NOT_FOUND };
11677
- }
11678
- const parentId2 = sibling.parentId == null ? ROOT_PARENT_ID : String(sibling.parentId);
11679
- return { ok: true, parentId: parentId2, firstAfter: destination.afterSiblingId };
12413
+ // src/dispatch/handlers/sir-sync.ts
12414
+ function dispatchSirSync(action, deps) {
12415
+ const { adapter } = deps;
12416
+ const id = action.activityId;
12417
+ const snapshot = adapter.getActivity(id);
12418
+ if (!snapshot) {
12419
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
11680
12420
  }
11681
- const parentId = destination.parentId;
11682
- const siblings = collectSiblings(adapter, parentId).sort(
11683
- (a, b) => readCorrelativeId(a) - readCorrelativeId(b)
11684
- );
11685
- const before = siblings[destination.index]?.id;
11686
- return { ok: true, parentId, firstBefore: before };
11687
- }
11688
- function applyPasteDomainSemantics(overrides) {
11689
- const { customId: _customId, ...content } = overrides;
12421
+ const before = snapshot.pendingRequestIds ?? [];
12422
+ const after = action.pendingRequests.map((r) => r.id);
12423
+ adapter.setActivityField(action.activityId, "pendingRequestIds", after);
12424
+ const updated = adapter.getActivity(id);
12425
+ if (!updated) {
12426
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
12427
+ }
12428
+ const change = {
12429
+ id,
12430
+ kind: "updated",
12431
+ fields: { pendingRequestIds: { before, after } },
12432
+ // ISSUE-025: `after` must be the COMPLETE post-change entity; a partial
12433
+ // object crashes any consumer that projects the full snapshot.
12434
+ after: structuredCloneActivity(updated)
12435
+ };
11690
12436
  return {
11691
- ...content,
11692
- progress: 0,
11693
- ponderator: 0,
11694
- usedCost: 0,
11695
- workHours: 0,
11696
- isLookahead: false
12437
+ ok: true,
12438
+ changes: {
12439
+ source: action,
12440
+ activities: [change],
12441
+ links: [],
12442
+ calendars: [],
12443
+ trackingEvents: []
12444
+ }
11697
12445
  };
11698
12446
  }
11699
- async function dispatchActivityPaste(action, options, deps) {
11700
- const { adapter, scheduler, sector } = deps;
11701
- const rootDest = resolvePasteRootDestination(action.destination, adapter);
11702
- if (!rootDest.ok) return rootDest;
11703
- const beforeSnap = await snapshotActivities(adapter, /* @__PURE__ */ new Set());
11704
- const touched = /* @__PURE__ */ new Set();
11705
- const originalToNew = /* @__PURE__ */ new Map();
11706
- const createdIds = [];
11707
- let prevRootId;
11708
- for (const input of action.activities) {
11709
- const mappedParent = originalToNew.get(input.originalParentId);
11710
- let parentId;
11711
- let afterSiblingId;
11712
- let beforeSiblingId;
11713
- if (mappedParent !== void 0) {
11714
- parentId = mappedParent;
11715
- } else {
11716
- parentId = rootDest.parentId;
11717
- if (prevRootId === void 0) {
11718
- afterSiblingId = rootDest.firstAfter;
11719
- beforeSiblingId = rootDest.firstBefore;
11720
- } else {
11721
- afterSiblingId = prevRootId;
11722
- }
12447
+
12448
+ // src/propagations/upward/progress-rollup.ts
12449
+ function recomputeAllProgressRollup(adapter) {
12450
+ for (const parentId of parentsDeepestFirst(adapter)) {
12451
+ rollupParent(parentId, adapter);
12452
+ }
12453
+ }
12454
+ function recomputeProgressRollupForParents(parentIdsDeepestFirst, adapter) {
12455
+ for (const parentId of parentIdsDeepestFirst) {
12456
+ rollupParent(parentId, adapter);
12457
+ }
12458
+ }
12459
+ function parentsDeepestFirst(adapter) {
12460
+ const parents = /* @__PURE__ */ new Set();
12461
+ adapter.forEachActivity((_activity, id) => {
12462
+ if (adapter.getChildren(id).length > 0) parents.add(id);
12463
+ });
12464
+ const depthById = /* @__PURE__ */ new Map();
12465
+ for (const id of parents) depthById.set(id, depthOf2(id, adapter));
12466
+ return [...parents].sort(
12467
+ (a, b) => (depthById.get(b) ?? 0) - (depthById.get(a) ?? 0)
12468
+ );
12469
+ }
12470
+ function depthOf2(id, adapter) {
12471
+ let depth = 0;
12472
+ let current = id;
12473
+ while (current && current !== "0") {
12474
+ if (!adapter.getActivity(current)) break;
12475
+ depth += 1;
12476
+ current = adapter.getParentId(current);
12477
+ }
12478
+ return depth;
12479
+ }
12480
+ function rollupParent(parentId, adapter) {
12481
+ const childIds = adapter.getChildren(parentId);
12482
+ if (childIds.length === 0) return;
12483
+ const children = [];
12484
+ for (const childId of childIds) {
12485
+ const child = adapter.getActivity(childId);
12486
+ if (!child) continue;
12487
+ children.push({
12488
+ progress: Number(child.progress ?? 0),
12489
+ ponderator: Number(child.ponderator ?? 0)
12490
+ });
12491
+ }
12492
+ const rollup = computeWeightedProgressRollup(children);
12493
+ if (rollup !== null) {
12494
+ adapter.setActivityField(
12495
+ parentId,
12496
+ COLUMN.PROGRESS,
12497
+ roundProgressPerLevel(rollup)
12498
+ );
12499
+ }
12500
+ }
12501
+
12502
+ // src/dispatch/handlers/persistence-acknowledge.ts
12503
+ function persistedOwnersOf(entities) {
12504
+ const owners = /* @__PURE__ */ new Map();
12505
+ for (const entity of entities) {
12506
+ const proplannerId = entity.proplannerId;
12507
+ if (typeof proplannerId === "number" && Number.isSafeInteger(proplannerId) && proplannerId > 0) {
12508
+ owners.set(proplannerId, String(entity.id));
11723
12509
  }
11724
- const core = await createActivityCore(
11725
- {
11726
- parentId,
11727
- afterSiblingId,
11728
- beforeSiblingId,
11729
- ...input.activityId ? { activityId: input.activityId } : {},
11730
- overrides: applyPasteDomainSemantics(input.overrides),
11731
- eventSource: action.eventSource
11732
- },
11733
- deps,
11734
- {
11735
- skipCorrelativeRecompute: true,
11736
- skipBeforeSnap: true,
11737
- customIdReferenceId: action.referenceActivityId
11738
- }
12510
+ }
12511
+ return owners;
12512
+ }
12513
+ function assertAllNewEntitiesAcknowledged(entities, assignments, entityName) {
12514
+ const acknowledgedIds = new Set(assignments.map(({ id }) => String(id)));
12515
+ for (const entity of entities) {
12516
+ if ((entity.proplannerId == null || entity.proplannerId === 0) && !acknowledgedIds.has(String(entity.id))) {
12517
+ throw new Error(
12518
+ `persistence-acknowledge: missing ${entityName} proplannerId for "${String(entity.id)}"`
12519
+ );
12520
+ }
12521
+ }
12522
+ }
12523
+ function assertValidAssignment(assignment, entityName, readCurrent, ownerByBackendId, seenIds, seenBackendIds) {
12524
+ const id = String(assignment.id);
12525
+ if (id.length === 0 || seenIds.has(id)) {
12526
+ throw new Error(
12527
+ `persistence-acknowledge: duplicate or empty ${entityName} id "${id}"`
11739
12528
  );
11740
- if (!core.ok) return core;
11741
- if (input.baselineSnapshot !== void 0) {
11742
- const snapshot = input.baselineSnapshot;
11743
- adapter.setActivityField(
11744
- core.newId,
11745
- "baselineSnapshot",
11746
- snapshot ? {
11747
- ...snapshot,
11748
- startDate: snapshot.startDate ? new Date(snapshot.startDate) : null,
11749
- endDate: snapshot.endDate ? new Date(snapshot.endDate) : null
11750
- } : null
12529
+ }
12530
+ if (!Number.isSafeInteger(assignment.proplannerId) || assignment.proplannerId <= 0) {
12531
+ throw new Error(
12532
+ `persistence-acknowledge: invalid ${entityName} proplannerId ${String(assignment.proplannerId)}`
12533
+ );
12534
+ }
12535
+ if (seenBackendIds.has(assignment.proplannerId)) {
12536
+ throw new Error(
12537
+ `persistence-acknowledge: duplicate ${entityName} proplannerId ${String(assignment.proplannerId)}`
12538
+ );
12539
+ }
12540
+ const state = readCurrent(id);
12541
+ if (!state.exists) {
12542
+ throw new Error(
12543
+ `persistence-acknowledge: unknown ${entityName} id "${id}"`
12544
+ );
12545
+ }
12546
+ if (state.current !== null && state.current !== 0 && state.current !== assignment.proplannerId) {
12547
+ throw new Error(
12548
+ `persistence-acknowledge: ${entityName} "${id}" already has proplannerId ${String(state.current)}`
12549
+ );
12550
+ }
12551
+ const existingOwner = ownerByBackendId.get(assignment.proplannerId);
12552
+ if (existingOwner !== void 0 && existingOwner !== id) {
12553
+ throw new Error(
12554
+ `persistence-acknowledge: ${entityName} proplannerId ${String(assignment.proplannerId)} already belongs to "${existingOwner}"`
12555
+ );
12556
+ }
12557
+ seenIds.add(id);
12558
+ seenBackendIds.add(assignment.proplannerId);
12559
+ }
12560
+ function assertValidAssignments(assignments, entityName, readCurrent, ownerByBackendId) {
12561
+ const seenIds = /* @__PURE__ */ new Set();
12562
+ const seenBackendIds = /* @__PURE__ */ new Set();
12563
+ for (const assignment of assignments) {
12564
+ assertValidAssignment(
12565
+ assignment,
12566
+ entityName,
12567
+ readCurrent,
12568
+ ownerByBackendId,
12569
+ seenIds,
12570
+ seenBackendIds
12571
+ );
12572
+ }
12573
+ }
12574
+ function assertValidReconciledProgress(entries, deps) {
12575
+ const seen = /* @__PURE__ */ new Set();
12576
+ for (const entry of entries) {
12577
+ const id = String(entry.activityId);
12578
+ if (id.length === 0 || seen.has(id)) {
12579
+ throw new Error(
12580
+ `persistence-acknowledge: duplicate or empty reconciled activity id "${id}"`
12581
+ );
12582
+ }
12583
+ if (!Number.isFinite(entry.progress) || entry.progress < 0 || entry.progress > 100) {
12584
+ throw new Error(
12585
+ `persistence-acknowledge: invalid reconciled progress ${String(entry.progress)} for "${id}"`
11751
12586
  );
11752
12587
  }
11753
- originalToNew.set(input.originalId, core.newId);
11754
- createdIds.push(core.newId);
11755
- if (mappedParent === void 0) prevRootId = core.newId;
12588
+ if (!deps.adapter.getActivity(id)) {
12589
+ throw new Error(
12590
+ `persistence-acknowledge: unknown reconciled activity id "${id}"`
12591
+ );
12592
+ }
12593
+ seen.add(id);
11756
12594
  }
11757
- const correlativeShifts = recomputeCorrelativeIds(adapter);
11758
- const createdIdSet = new Set(createdIds.map(String));
11759
- foldCorrelativeShifts(
11760
- adapter,
11761
- correlativeShifts,
11762
- createdIdSet,
11763
- beforeSnap,
11764
- touched
12595
+ }
12596
+ async function applyReconciledProgress(entries, deps) {
12597
+ const progressToApply = entries.filter((entry) => {
12598
+ const current = deps.adapter.getActivity(String(entry.activityId));
12599
+ return current !== null && current.progress !== entry.progress;
12600
+ });
12601
+ const progressIds = new Set(
12602
+ progressToApply.map((entry) => String(entry.activityId))
11765
12603
  );
11766
- for (const createdId of createdIds) touched.add(String(createdId));
11767
- const appliedLinks = [];
11768
- for (const link of action.links) {
11769
- const newSource = originalToNew.get(link.source);
11770
- const newTarget = originalToNew.get(link.target);
11771
- if (newSource === void 0 || newTarget === void 0) continue;
11772
- const op = {
11773
- kind: "create",
11774
- source: newSource,
11775
- target: newTarget,
11776
- type: link.type,
11777
- lag: lagDaysToHours(link.lag, sector.hoursPerDay)
11778
- };
11779
- const res = applyLinkOperation(op, {
11780
- port: adapter,
11781
- newLinkId: link.linkId ? () => link.linkId : deps.linkIdGen.next
11782
- });
11783
- appliedLinks.push({
11784
- op,
11785
- finalLinkId: res.applied ? res.linkId : null,
11786
- rejected: res.applied ? null : res.rejected ?? "link_op_rejected"
11787
- });
12604
+ const ancestors = findAncestorsOfDirty(deps.adapter, progressIds);
12605
+ const touched = new Set(progressIds);
12606
+ for (const parentId of ancestors) {
12607
+ touched.add(String(parentId));
11788
12608
  }
11789
- const { scheduledIds } = await runPostMutation(
11790
- {
11791
- adapter,
11792
- scheduler,
11793
- sector: deps.sector,
11794
- defaultBaseCalendarId: deps.defaultBaseCalendarId
11795
- },
11796
- {
11797
- action,
11798
- autoscheduleFrom: createdIds[0] ?? null,
11799
- recomputeParentsFrom: createdIds,
11800
- now: deps.now,
11801
- options
12609
+ const before = await snapshotActivities(deps.adapter, touched);
12610
+ deps.adapter.batchUpdate(() => {
12611
+ for (const entry of progressToApply) {
12612
+ deps.adapter.setActivityField(
12613
+ String(entry.activityId),
12614
+ "progress",
12615
+ entry.progress
12616
+ );
11802
12617
  }
12618
+ recomputeProgressRollupForParents(ancestors, deps.adapter);
12619
+ recomputeRealWorkFromProgress(progressIds, ancestors, deps.adapter);
12620
+ });
12621
+ return [...await buildActivityChanges(deps.adapter, before, touched)];
12622
+ }
12623
+ async function dispatchPersistenceAcknowledge(action, deps) {
12624
+ const activities = action.activities ?? [];
12625
+ const links = action.links ?? [];
12626
+ const reconciledProgress = action.reconciledProgress ?? [];
12627
+ assertValidReconciledProgress(reconciledProgress, deps);
12628
+ const currentActivities = deps.adapter.getAllActivities();
12629
+ const currentLinks = deps.adapter.getAllLinks();
12630
+ const activityOwnerByBackendId = persistedOwnersOf(currentActivities);
12631
+ const linkOwnerByBackendId = persistedOwnersOf(currentLinks);
12632
+ assertValidAssignments(
12633
+ activities,
12634
+ "activity",
12635
+ (id) => {
12636
+ const activity = deps.adapter.getActivity(id);
12637
+ return {
12638
+ exists: activity !== null,
12639
+ current: activity?.proplannerId ?? null
12640
+ };
12641
+ },
12642
+ activityOwnerByBackendId
11803
12643
  );
11804
- const linkChanges = buildLinkChangesForBatch(
11805
- appliedLinks,
11806
- /* @__PURE__ */ new Map(),
11807
- adapter
12644
+ assertValidAssignments(
12645
+ links,
12646
+ "link",
12647
+ (id) => {
12648
+ const link = deps.adapter.getLink(id);
12649
+ return { exists: link !== null, current: link?.proplannerId ?? null };
12650
+ },
12651
+ linkOwnerByBackendId
11808
12652
  );
11809
- return {
11810
- ok: true,
11811
- changes: await assembleChangeSet(adapter, {
11812
- source: action,
11813
- beforeSnap,
11814
- touchedIds: touched,
11815
- scheduledIds,
11816
- links: linkChanges,
11817
- hoursPerDay: sector.hoursPerDay
11818
- })
11819
- };
11820
- }
11821
-
11822
- // src/dispatch/handlers/sir-sync.ts
11823
- function dispatchSirSync(action, deps) {
11824
- const { adapter } = deps;
11825
- const id = action.activityId;
11826
- const snapshot = adapter.getActivity(id);
11827
- if (!snapshot) {
11828
- return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
11829
- }
11830
- const before = snapshot.pendingRequestIds ?? [];
11831
- const after = action.pendingRequests.map((r) => r.id);
11832
- adapter.setActivityField(action.activityId, "pendingRequestIds", after);
11833
- const change = {
11834
- id,
11835
- kind: "updated",
11836
- fields: { pendingRequestIds: { before, after } },
11837
- after: { id, pendingRequestIds: after }
11838
- };
12653
+ assertAllNewEntitiesAcknowledged(currentActivities, activities, "activity");
12654
+ assertAllNewEntitiesAcknowledged(currentLinks, links, "link");
12655
+ const activityChanges = await applyReconciledProgress(
12656
+ reconciledProgress,
12657
+ deps
12658
+ );
12659
+ const linkChanges = [];
12660
+ deps.adapter.batchUpdate(() => {
12661
+ for (const assignment of activities) {
12662
+ const before = deps.adapter.getActivity(assignment.id);
12663
+ if (!before || before.proplannerId === assignment.proplannerId) continue;
12664
+ const previousProplannerId = before.proplannerId;
12665
+ deps.adapter.setActivityField(
12666
+ assignment.id,
12667
+ "proplannerId",
12668
+ assignment.proplannerId
12669
+ );
12670
+ activityChanges.push({
12671
+ id: assignment.id,
12672
+ kind: "updated",
12673
+ fields: {
12674
+ proplannerId: {
12675
+ before: previousProplannerId,
12676
+ after: assignment.proplannerId
12677
+ }
12678
+ },
12679
+ after: { ...before, proplannerId: assignment.proplannerId }
12680
+ });
12681
+ }
12682
+ for (const assignment of links) {
12683
+ const before = deps.adapter.getLink(assignment.id);
12684
+ if (!before || before.proplannerId === assignment.proplannerId) continue;
12685
+ const previousProplannerId = before.proplannerId;
12686
+ deps.adapter.setLinkProplannerId(assignment.id, assignment.proplannerId);
12687
+ linkChanges.push({
12688
+ id: assignment.id,
12689
+ kind: "updated",
12690
+ fields: {
12691
+ proplannerId: {
12692
+ before: previousProplannerId,
12693
+ after: assignment.proplannerId
12694
+ }
12695
+ },
12696
+ after: { ...before, proplannerId: assignment.proplannerId }
12697
+ });
12698
+ }
12699
+ });
11839
12700
  return {
11840
12701
  ok: true,
11841
12702
  changes: {
11842
12703
  source: action,
11843
- activities: [change],
11844
- links: [],
12704
+ activities: activityChanges,
12705
+ links: linkChanges,
11845
12706
  calendars: [],
11846
12707
  trackingEvents: []
11847
12708
  }
@@ -11849,8 +12710,15 @@ function dispatchSirSync(action, deps) {
11849
12710
  }
11850
12711
 
11851
12712
  // src/dispatch/handlers/activity-lookahead-sync.ts
11852
- function dispatchActivityLookaheadSync(action, deps) {
12713
+ async function dispatchActivityLookaheadSync(action, options, deps) {
12714
+ if (hasInvalidPersistedProgress(action)) {
12715
+ return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
12716
+ }
11853
12717
  const ids = [...new Set(action.activityIds.map(String))];
12718
+ const acknowledgesUnknownPersistedActivity = action.acknowledgePersistedLookahead === true && ids.some((activityId) => !deps.isActivityIdReserved(activityId));
12719
+ if (acknowledgesUnknownPersistedActivity) {
12720
+ return { ok: false, reason: REJECTION_REASON.INVALID };
12721
+ }
11854
12722
  const snapshots = ids.map((id) => deps.adapter.getActivity(id));
11855
12723
  if (snapshots.some((snapshot) => !snapshot)) {
11856
12724
  return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
@@ -11890,6 +12758,39 @@ function dispatchActivityLookaheadSync(action, deps) {
11890
12758
  }
11891
12759
  });
11892
12760
  }
12761
+ if (action.persistedProgress !== void 0) {
12762
+ const progressChanges = await applyReconciledProgress(
12763
+ [{ activityId: ids[0], progress: action.persistedProgress }],
12764
+ deps
12765
+ );
12766
+ if (progressChanges.length > 0) {
12767
+ await runPostMutation(
12768
+ {
12769
+ adapter: deps.adapter,
12770
+ scheduler: deps.scheduler,
12771
+ sector: deps.sector,
12772
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
12773
+ },
12774
+ {
12775
+ action,
12776
+ autoscheduleFrom: null,
12777
+ recomputeParentsFrom: [],
12778
+ now: deps.now,
12779
+ options
12780
+ }
12781
+ );
12782
+ return {
12783
+ ok: true,
12784
+ changes: await assembleChangeSet(deps.adapter, {
12785
+ source: action,
12786
+ beforeSnap: /* @__PURE__ */ new Map(),
12787
+ touchedIds: [],
12788
+ scheduledIds: [],
12789
+ hoursPerDay: deps.sector.hoursPerDay
12790
+ })
12791
+ };
12792
+ }
12793
+ }
11893
12794
  return {
11894
12795
  ok: true,
11895
12796
  changes: {
@@ -11901,6 +12802,10 @@ function dispatchActivityLookaheadSync(action, deps) {
11901
12802
  }
11902
12803
  };
11903
12804
  }
12805
+ function hasInvalidPersistedProgress(action) {
12806
+ const progress = action.persistedProgress;
12807
+ return progress !== void 0 && (action.activityIds.length !== 1 || !Number.isFinite(progress) || progress < 0 || progress > 100);
12808
+ }
11904
12809
 
11905
12810
  // src/internal/hierarchy/invalid-links.ts
11906
12811
  function collectInvalidLinkIdsForCycle(movedActivityIds, adapter) {
@@ -11934,6 +12839,37 @@ function isAncestor(descendantId, candidateAncestorId, adapter) {
11934
12839
  return false;
11935
12840
  }
11936
12841
 
12842
+ // src/dispatch/shared/hierarchy-cycle-guard.ts
12843
+ function reparentClosesEffectiveCycle(adapter, reparented) {
12844
+ const links = adapter.getAllLinks();
12845
+ const verdict = validateTopologyCommit({
12846
+ port: adapter,
12847
+ currentLinks: links,
12848
+ proposedLinks: links,
12849
+ proposal: { reparented }
12850
+ });
12851
+ return !verdict.ok;
12852
+ }
12853
+ function collectCyclicReparents(planned, adapter) {
12854
+ const cyclic = /* @__PURE__ */ new Set();
12855
+ if (planned.length === 0) return cyclic;
12856
+ const combined = new Map(
12857
+ planned.map((reparent) => [reparent.activityId, reparent.newParentId])
12858
+ );
12859
+ if (!reparentClosesEffectiveCycle(adapter, combined)) return cyclic;
12860
+ const accepted = /* @__PURE__ */ new Map();
12861
+ for (const reparent of planned) {
12862
+ const trial = new Map(accepted);
12863
+ trial.set(reparent.activityId, reparent.newParentId);
12864
+ if (reparentClosesEffectiveCycle(adapter, trial)) {
12865
+ cyclic.add(reparent.activityId);
12866
+ continue;
12867
+ }
12868
+ accepted.set(reparent.activityId, reparent.newParentId);
12869
+ }
12870
+ return cyclic;
12871
+ }
12872
+
11937
12873
  // src/dispatch/handlers/activity-move.ts
11938
12874
  async function dispatchActivityMove(action, options, deps) {
11939
12875
  const { adapter, scheduler, sector } = deps;
@@ -11975,6 +12911,17 @@ async function dispatchActivityMove(action, options, deps) {
11975
12911
  const oldParentKey = normalizeParentKey(oldParent);
11976
12912
  const newParentKey = String(action.parentId);
11977
12913
  const parentChanged = oldParentKey !== newParentKey;
12914
+ if (parentChanged) {
12915
+ const reparented = /* @__PURE__ */ new Map([
12916
+ [
12917
+ action.activityId,
12918
+ newParentIsRoot ? null : action.parentId
12919
+ ]
12920
+ ]);
12921
+ if (reparentClosesEffectiveCycle(adapter, reparented)) {
12922
+ return { ok: false, reason: REJECTION_REASON.HIERARCHY_CYCLE };
12923
+ }
12924
+ }
11978
12925
  const touched = /* @__PURE__ */ new Set([action.activityId]);
11979
12926
  if (parentChanged) {
11980
12927
  if (oldParentKey !== ROOT_PARENT_ID) touched.add(oldParentKey);
@@ -12132,21 +13079,14 @@ async function dispatchActivityMove(action, options, deps) {
12132
13079
  };
12133
13080
  }
12134
13081
 
12135
- // src/dispatch/handlers/activity-indent.ts
12136
- async function dispatchActivityIndent(action, options, deps) {
12137
- const { adapter, scheduler, sector } = deps;
12138
- scheduler.invalidateAllCaches();
12139
- if (action.activityIds.length === 0) {
12140
- return { ok: false, reason: REJECTION_REASON.ACTIVITY_IDS_EMPTY };
12141
- }
12142
- const selectedSet = new Set(action.activityIds);
12143
- const touched = /* @__PURE__ */ new Set();
13082
+ // src/dispatch/shared/indent-planning.ts
13083
+ function planIndentMoves(activityIds, adapter) {
13084
+ const selectedSet = new Set(activityIds);
12144
13085
  const failed = [];
12145
- const succeededIds = [];
12146
13086
  const moves = [];
12147
13087
  const plannedIds = /* @__PURE__ */ new Set();
12148
13088
  const blockingSet = new Set(selectedSet);
12149
- const sortedIds = [...action.activityIds].sort((a, b) => {
13089
+ const sortedIds = [...activityIds].sort((a, b) => {
12150
13090
  const aSnap = adapter.getActivity(a);
12151
13091
  const bSnap = adapter.getActivity(b);
12152
13092
  const ac = aSnap ? readCorrelativeId(aSnap) : NOT_SET_SORT_VALUE2;
@@ -12181,12 +13121,43 @@ async function dispatchActivityIndent(action, options, deps) {
12181
13121
  const oldParentId = isRootParent(oldParentRaw) ? ROOT_PARENT_ID : String(oldParentRaw);
12182
13122
  moves.push({ activityId: id, newParentId: targetParentId, oldParentId });
12183
13123
  plannedIds.add(id);
12184
- touched.add(id);
12185
- touched.add(targetParentId);
12186
- if (oldParentId !== ROOT_PARENT_ID) touched.add(oldParentId);
12187
13124
  }
13125
+ const cyclicMoveIds = collectCyclicReparents(moves, adapter);
13126
+ for (const cyclicId of cyclicMoveIds) {
13127
+ failed.push({
13128
+ activityId: cyclicId,
13129
+ reason: REJECTION_REASON.HIERARCHY_CYCLE
13130
+ });
13131
+ }
13132
+ const allowedMoves = moves.filter(
13133
+ (move) => !cyclicMoveIds.has(move.activityId)
13134
+ );
13135
+ return { moves, cyclicMoveIds, allowedMoves, failed };
13136
+ }
13137
+
13138
+ // src/dispatch/handlers/activity-indent.ts
13139
+ async function dispatchActivityIndent(action, options, deps) {
13140
+ const { adapter, scheduler, sector } = deps;
13141
+ scheduler.invalidateAllCaches();
13142
+ if (action.activityIds.length === 0) {
13143
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_IDS_EMPTY };
13144
+ }
13145
+ const touched = /* @__PURE__ */ new Set();
13146
+ const succeededIds = [];
13147
+ const plan = planIndentMoves(action.activityIds, adapter);
13148
+ const failed = [
13149
+ ...plan.failed
13150
+ ];
13151
+ for (const move of plan.moves) {
13152
+ touched.add(String(move.activityId));
13153
+ touched.add(String(move.newParentId));
13154
+ if (move.oldParentId !== ROOT_PARENT_ID) {
13155
+ touched.add(String(move.oldParentId));
13156
+ }
13157
+ }
13158
+ const allowedMoves = plan.allowedMoves;
12188
13159
  const beforeSnap = await snapshotActivities(adapter, touched);
12189
- for (const { activityId, newParentId } of moves) {
13160
+ for (const { activityId, newParentId } of allowedMoves) {
12190
13161
  adapter.setActivityField(activityId, ACTIVITY_PROPERTY.PARENT, newParentId);
12191
13162
  adapter.setActivityField(
12192
13163
  activityId,
@@ -12196,7 +13167,7 @@ async function dispatchActivityIndent(action, options, deps) {
12196
13167
  succeededIds.push(activityId);
12197
13168
  }
12198
13169
  const promotedParents = /* @__PURE__ */ new Set();
12199
- for (const { activityId, newParentId } of moves) {
13170
+ for (const { activityId, newParentId } of allowedMoves) {
12200
13171
  if (promotedParents.has(newParentId)) continue;
12201
13172
  promotedParents.add(newParentId);
12202
13173
  const newParent = adapter.getActivity(newParentId);
@@ -12248,7 +13219,7 @@ async function dispatchActivityIndent(action, options, deps) {
12248
13219
  releaseClearedCustomId(oldNewParentCustomId, deps.customIdTracker);
12249
13220
  }
12250
13221
  const indentAffectedParents = /* @__PURE__ */ new Set();
12251
- for (const m of moves) {
13222
+ for (const m of allowedMoves) {
12252
13223
  if (m.oldParentId !== ROOT_PARENT_ID)
12253
13224
  indentAffectedParents.add(m.oldParentId);
12254
13225
  indentAffectedParents.add(m.newParentId);
@@ -12409,8 +13380,24 @@ async function dispatchActivityOutdent(action, options, deps) {
12409
13380
  if (grandparentKey !== ROOT_PARENT_ID) touched.add(String(grandparentKey));
12410
13381
  oldParents.add(oldParentKey);
12411
13382
  }
13383
+ const cyclicPlanIds = collectCyclicReparents(
13384
+ planned.map((plan) => ({
13385
+ activityId: plan.activityId,
13386
+ newParentId: plan.grandparentKey
13387
+ })),
13388
+ adapter
13389
+ );
13390
+ for (const cyclicId of cyclicPlanIds) {
13391
+ failed.push({
13392
+ activityId: cyclicId,
13393
+ reason: REJECTION_REASON.HIERARCHY_CYCLE
13394
+ });
13395
+ }
13396
+ const allowedPlans = planned.filter(
13397
+ (plan) => !cyclicPlanIds.has(plan.activityId)
13398
+ );
12412
13399
  const beforeSnap = await snapshotActivities(adapter, touched);
12413
- for (const plan of planned) {
13400
+ for (const plan of allowedPlans) {
12414
13401
  adapter.setActivityField(
12415
13402
  plan.activityId,
12416
13403
  ACTIVITY_PROPERTY.PARENT,
@@ -12444,7 +13431,7 @@ async function dispatchActivityOutdent(action, options, deps) {
12444
13431
  task: parentActivity
12445
13432
  }),
12446
13433
  idsRemoved: new Set(
12447
- planned.filter((plan) => plan.oldParentKey === oldParentKey).map((plan) => plan.activityId)
13434
+ allowedPlans.filter((plan) => plan.oldParentKey === oldParentKey).map((plan) => plan.activityId)
12448
13435
  )
12449
13436
  });
12450
13437
  if (!demotion) {
@@ -12470,7 +13457,7 @@ async function dispatchActivityOutdent(action, options, deps) {
12470
13457
  for (const oldParentKey of oldParents) {
12471
13458
  outdentAffectedParents.add(String(oldParentKey));
12472
13459
  }
12473
- for (const plan of planned) {
13460
+ for (const plan of allowedPlans) {
12474
13461
  if (plan.grandparentKey !== ROOT_PARENT_ID) {
12475
13462
  outdentAffectedParents.add(String(plan.grandparentKey));
12476
13463
  }
@@ -12558,6 +13545,17 @@ async function dispatchActivitySetProgress(action, options, deps) {
12558
13545
  "[ScheduleCore] progressPipeline not registered \u2014 cannot dispatch activity-set-progress"
12559
13546
  );
12560
13547
  }
13548
+ const editGate = pipeline.canEdit(activitySnap, ctx.hierarchy);
13549
+ if (!editGate.allowed) {
13550
+ const declaredButtonOverride = action.origin === "action-button" && (editGate.reason === "has_lookahead_tasks" || editGate.reason === "summary");
13551
+ if (!declaredButtonOverride) {
13552
+ return {
13553
+ ok: false,
13554
+ reason: editGate.reason,
13555
+ ...editGate.alertKey ? { alertKey: editGate.alertKey } : {}
13556
+ };
13557
+ }
13558
+ }
12561
13559
  const changes = pipeline.transform(
12562
13560
  activitySnap,
12563
13561
  action.newValue,
@@ -12974,171 +13972,29 @@ function syncCustomIdTracker(edit, oldCustomId, deps) {
12974
13972
  const rawValue = liveActivity ? liveActivity.customId : null;
12975
13973
  const newCustomId = typeof rawValue === "string" ? rawValue : null;
12976
13974
  if (oldCustomId !== newCustomId) {
12977
- deps.customIdTracker.trackCustomIdChange(oldCustomId, newCustomId);
12978
- }
12979
- }
12980
- function recordConstraintPrior(priors, edit, beforeSnap) {
12981
- if (!isConstraintEditColumn(edit.column)) return;
12982
- const activityKey = String(edit.activityId);
12983
- if (priors.has(activityKey)) return;
12984
- const preBatchSnapshot = beforeSnap.get(activityKey);
12985
- if (!preBatchSnapshot) return;
12986
- priors.set(activityKey, preBatchSnapshot.constraintDate ?? null);
12987
- }
12988
- function mergeBeforeSnapshots2(beforeSnap, ids, deps) {
12989
- const missing = /* @__PURE__ */ new Set();
12990
- for (const candidateId of ids) {
12991
- if (!beforeSnap.has(candidateId)) missing.add(candidateId);
12992
- }
12993
- if (missing.size === 0) return;
12994
- for (const missingId of missing) {
12995
- const liveActivity = deps.adapter.getActivity(missingId);
12996
- if (liveActivity) {
12997
- beforeSnap.set(missingId, structuredCloneActivity(liveActivity));
12998
- }
12999
- }
13000
- }
13001
-
13002
- // src/dispatch/handlers/persistence-acknowledge.ts
13003
- function assertValidAssignment(assignment, entityName, readCurrent, ownerByBackendId, seenIds, seenBackendIds) {
13004
- const id = String(assignment.id);
13005
- if (id.length === 0 || seenIds.has(id)) {
13006
- throw new Error(
13007
- `persistence-acknowledge: duplicate or empty ${entityName} id "${id}"`
13008
- );
13009
- }
13010
- if (!Number.isSafeInteger(assignment.proplannerId) || assignment.proplannerId <= 0) {
13011
- throw new Error(
13012
- `persistence-acknowledge: invalid ${entityName} proplannerId ${String(assignment.proplannerId)}`
13013
- );
13014
- }
13015
- if (seenBackendIds.has(assignment.proplannerId)) {
13016
- throw new Error(
13017
- `persistence-acknowledge: duplicate ${entityName} proplannerId ${String(assignment.proplannerId)}`
13018
- );
13019
- }
13020
- const state = readCurrent(id);
13021
- if (!state.exists) {
13022
- throw new Error(
13023
- `persistence-acknowledge: unknown ${entityName} id "${id}"`
13024
- );
13025
- }
13026
- if (state.current !== null && state.current !== assignment.proplannerId) {
13027
- throw new Error(
13028
- `persistence-acknowledge: ${entityName} "${id}" already has proplannerId ${String(state.current)}`
13029
- );
13030
- }
13031
- const existingOwner = ownerByBackendId.get(assignment.proplannerId);
13032
- if (existingOwner !== void 0 && existingOwner !== id) {
13033
- throw new Error(
13034
- `persistence-acknowledge: ${entityName} proplannerId ${String(assignment.proplannerId)} already belongs to "${existingOwner}"`
13035
- );
13975
+ deps.customIdTracker.trackCustomIdChange(oldCustomId, newCustomId);
13036
13976
  }
13037
- seenIds.add(id);
13038
- seenBackendIds.add(assignment.proplannerId);
13039
13977
  }
13040
- function assertValidAssignments(assignments, entityName, readCurrent, ownerByBackendId) {
13041
- const seenIds = /* @__PURE__ */ new Set();
13042
- const seenBackendIds = /* @__PURE__ */ new Set();
13043
- for (const assignment of assignments) {
13044
- assertValidAssignment(
13045
- assignment,
13046
- entityName,
13047
- readCurrent,
13048
- ownerByBackendId,
13049
- seenIds,
13050
- seenBackendIds
13051
- );
13052
- }
13978
+ function recordConstraintPrior(priors, edit, beforeSnap) {
13979
+ if (!isConstraintEditColumn(edit.column)) return;
13980
+ const activityKey = String(edit.activityId);
13981
+ if (priors.has(activityKey)) return;
13982
+ const preBatchSnapshot = beforeSnap.get(activityKey);
13983
+ if (!preBatchSnapshot) return;
13984
+ priors.set(activityKey, preBatchSnapshot.constraintDate ?? null);
13053
13985
  }
13054
- function dispatchPersistenceAcknowledge(action, deps) {
13055
- const activities = action.activities ?? [];
13056
- const links = action.links ?? [];
13057
- const activityOwnerByBackendId = /* @__PURE__ */ new Map();
13058
- for (const activity of deps.adapter.getAllActivities()) {
13059
- if (activity.proplannerId != null) {
13060
- activityOwnerByBackendId.set(activity.proplannerId, String(activity.id));
13061
- }
13986
+ function mergeBeforeSnapshots2(beforeSnap, ids, deps) {
13987
+ const missing = /* @__PURE__ */ new Set();
13988
+ for (const candidateId of ids) {
13989
+ if (!beforeSnap.has(candidateId)) missing.add(candidateId);
13062
13990
  }
13063
- const linkOwnerByBackendId = /* @__PURE__ */ new Map();
13064
- for (const link of deps.adapter.getAllLinks()) {
13065
- if (link.proplannerId != null) {
13066
- linkOwnerByBackendId.set(link.proplannerId, String(link.id));
13991
+ if (missing.size === 0) return;
13992
+ for (const missingId of missing) {
13993
+ const liveActivity = deps.adapter.getActivity(missingId);
13994
+ if (liveActivity) {
13995
+ beforeSnap.set(missingId, structuredCloneActivity(liveActivity));
13067
13996
  }
13068
13997
  }
13069
- assertValidAssignments(
13070
- activities,
13071
- "activity",
13072
- (id) => {
13073
- const activity = deps.adapter.getActivity(id);
13074
- return {
13075
- exists: activity !== null,
13076
- current: activity?.proplannerId ?? null
13077
- };
13078
- },
13079
- activityOwnerByBackendId
13080
- );
13081
- assertValidAssignments(
13082
- links,
13083
- "link",
13084
- (id) => {
13085
- const link = deps.adapter.getLink(id);
13086
- return { exists: link !== null, current: link?.proplannerId ?? null };
13087
- },
13088
- linkOwnerByBackendId
13089
- );
13090
- const activityChanges = [];
13091
- const linkChanges = [];
13092
- deps.adapter.batchUpdate(() => {
13093
- for (const assignment of activities) {
13094
- const before = deps.adapter.getActivity(assignment.id);
13095
- if (!before || before.proplannerId === assignment.proplannerId) continue;
13096
- const previousProplannerId = before.proplannerId;
13097
- deps.adapter.setActivityField(
13098
- assignment.id,
13099
- "proplannerId",
13100
- assignment.proplannerId
13101
- );
13102
- activityChanges.push({
13103
- id: assignment.id,
13104
- kind: "updated",
13105
- fields: {
13106
- proplannerId: {
13107
- before: previousProplannerId,
13108
- after: assignment.proplannerId
13109
- }
13110
- },
13111
- after: { ...before, proplannerId: assignment.proplannerId }
13112
- });
13113
- }
13114
- for (const assignment of links) {
13115
- const before = deps.adapter.getLink(assignment.id);
13116
- if (!before || before.proplannerId === assignment.proplannerId) continue;
13117
- const previousProplannerId = before.proplannerId;
13118
- deps.adapter.setLinkProplannerId(assignment.id, assignment.proplannerId);
13119
- linkChanges.push({
13120
- id: assignment.id,
13121
- kind: "updated",
13122
- fields: {
13123
- proplannerId: {
13124
- before: previousProplannerId,
13125
- after: assignment.proplannerId
13126
- }
13127
- },
13128
- after: { ...before, proplannerId: assignment.proplannerId }
13129
- });
13130
- }
13131
- });
13132
- return {
13133
- ok: true,
13134
- changes: {
13135
- source: action,
13136
- activities: activityChanges,
13137
- links: linkChanges,
13138
- calendars: [],
13139
- trackingEvents: []
13140
- }
13141
- };
13142
13998
  }
13143
13999
 
13144
14000
  // src/baselines/normalize-baseline-point.ts
@@ -13336,55 +14192,6 @@ function recomputeAllPonderators(criterion, adapter) {
13336
14192
  });
13337
14193
  }
13338
14194
 
13339
- // src/propagations/upward/progress-rollup.ts
13340
- function recomputeAllProgressRollup(adapter) {
13341
- for (const parentId of parentsDeepestFirst(adapter)) {
13342
- rollupParent(parentId, adapter);
13343
- }
13344
- }
13345
- function parentsDeepestFirst(adapter) {
13346
- const parents = /* @__PURE__ */ new Set();
13347
- adapter.forEachActivity((_activity, id) => {
13348
- if (adapter.getChildren(id).length > 0) parents.add(id);
13349
- });
13350
- const depthById = /* @__PURE__ */ new Map();
13351
- for (const id of parents) depthById.set(id, depthOf2(id, adapter));
13352
- return [...parents].sort(
13353
- (a, b) => (depthById.get(b) ?? 0) - (depthById.get(a) ?? 0)
13354
- );
13355
- }
13356
- function depthOf2(id, adapter) {
13357
- let depth = 0;
13358
- let current = id;
13359
- while (current && current !== "0") {
13360
- if (!adapter.getActivity(current)) break;
13361
- depth += 1;
13362
- current = adapter.getParentId(current);
13363
- }
13364
- return depth;
13365
- }
13366
- function rollupParent(parentId, adapter) {
13367
- const childIds = adapter.getChildren(parentId);
13368
- if (childIds.length === 0) return;
13369
- const children = [];
13370
- for (const childId of childIds) {
13371
- const child = adapter.getActivity(childId);
13372
- if (!child) continue;
13373
- children.push({
13374
- progress: Number(child.progress ?? 0),
13375
- ponderator: Number(child.ponderator ?? 0)
13376
- });
13377
- }
13378
- const rollup = computeWeightedProgressRollup(children);
13379
- if (rollup !== null) {
13380
- adapter.setActivityField(
13381
- parentId,
13382
- COLUMN.PROGRESS,
13383
- roundProgressPerLevel(rollup)
13384
- );
13385
- }
13386
- }
13387
-
13388
14195
  // src/dispatch/shared/baseline-weighted-fields.ts
13389
14196
  function recomputeBaselineWeightedFields(deps, criterion) {
13390
14197
  recomputeAllPonderators(criterion, deps.adapter);
@@ -13488,9 +14295,207 @@ async function dispatchStatusCriteriaSet(action, deps) {
13488
14295
  ok: true,
13489
14296
  changes: await assembleChangeSet(deps.adapter, {
13490
14297
  source: action,
13491
- beforeSnap,
13492
- touchedIds: changedIds,
13493
- scheduledIds: [],
14298
+ beforeSnap,
14299
+ touchedIds: changedIds,
14300
+ scheduledIds: [],
14301
+ hoursPerDay: deps.sector.hoursPerDay
14302
+ })
14303
+ };
14304
+ }
14305
+
14306
+ // src/activity-copy-paste/policy.ts
14307
+ var ACTIVITY_COPY_FIELDS = [
14308
+ "name",
14309
+ "description",
14310
+ "type",
14311
+ "startDate",
14312
+ "durationHours",
14313
+ "constraintType",
14314
+ "constraintDate",
14315
+ "calendarId",
14316
+ "cost",
14317
+ "hasCustomPonderator",
14318
+ "subcontractId",
14319
+ "responsableIds",
14320
+ "tagIds"
14321
+ ];
14322
+
14323
+ // src/activity-copy-paste/copy.ts
14324
+ function copyOverrides(activity) {
14325
+ const overrides = {};
14326
+ for (const field of ACTIVITY_COPY_FIELDS) {
14327
+ Object.assign(overrides, { [field]: cloneDomainValue(activity[field]) });
14328
+ }
14329
+ return overrides;
14330
+ }
14331
+ function selectedInTreeOrder(adapter, selected) {
14332
+ const ordered = [];
14333
+ const visit = (id) => {
14334
+ if (selected.has(String(id))) ordered.push(id);
14335
+ for (const childId of adapter.getChildren(id)) visit(childId);
14336
+ };
14337
+ for (const rootId of adapter.getRootIds()) visit(rootId);
14338
+ return ordered;
14339
+ }
14340
+ function dispatchActivityCopy(action, deps) {
14341
+ const selected = new Set(deps.adapter.checkedIds().map(String));
14342
+ if (selected.size === 0) {
14343
+ return { ok: false, reason: REJECTION_REASON.NO_ACTIVITY_SELECTION };
14344
+ }
14345
+ const orderedIds = selectedInTreeOrder(deps.adapter, selected);
14346
+ deps.clipboard.value = {
14347
+ activities: orderedIds.map((id) => {
14348
+ const activity = deps.adapter.getActivity(id);
14349
+ if (activity === null)
14350
+ throw new Error(`Selected activity ${id} is missing`);
14351
+ return {
14352
+ originalId: id,
14353
+ originalParentId: activity.parentId ?? "0",
14354
+ overrides: copyOverrides(activity)
14355
+ };
14356
+ }),
14357
+ links: deps.adapter.getAllLinks().filter(
14358
+ (link) => selected.has(String(link.source)) && selected.has(String(link.target))
14359
+ ).map((link) => cloneDomainValue(link))
14360
+ };
14361
+ const selection = dispatchSelectionReplace(
14362
+ { kind: "selection-replace", activityIds: [] },
14363
+ { adapter: deps.adapter }
14364
+ );
14365
+ if (!selection.ok) return selection;
14366
+ return { ...selection, changes: { ...selection.changes, source: action } };
14367
+ }
14368
+ function dispatchActivityClipboardClear(action, clipboard) {
14369
+ clipboard.value = null;
14370
+ return {
14371
+ ok: true,
14372
+ changes: {
14373
+ source: action,
14374
+ activities: [],
14375
+ links: [],
14376
+ calendars: [],
14377
+ trackingEvents: []
14378
+ }
14379
+ };
14380
+ }
14381
+
14382
+ // src/activity-copy-paste/paste.ts
14383
+ function resolveDestination(afterSiblingId, adapter) {
14384
+ const sibling = adapter.getActivity(afterSiblingId);
14385
+ if (!sibling) {
14386
+ return { ok: false, reason: REJECTION_REASON.ANCHOR_SIBLING_NOT_FOUND };
14387
+ }
14388
+ return {
14389
+ ok: true,
14390
+ parentId: sibling.parentId == null ? ROOT_PARENT_ID : String(sibling.parentId)
14391
+ };
14392
+ }
14393
+ var invariantFailure = () => ({
14394
+ ok: false,
14395
+ reason: REJECTION_REASON.ACTIVITY_PASTE_INVARIANT_FAILED
14396
+ });
14397
+ async function dispatchActivityPasteCopied(action, options, deps) {
14398
+ const clipboard = deps.activityClipboard.value;
14399
+ if (clipboard === null) {
14400
+ return { ok: false, reason: REJECTION_REASON.CLIPBOARD_EMPTY };
14401
+ }
14402
+ const destination = resolveDestination(
14403
+ action.destination.afterSiblingId,
14404
+ deps.adapter
14405
+ );
14406
+ if (!destination.ok) return destination;
14407
+ const beforeSnapshot = await snapshotActivities(
14408
+ deps.adapter,
14409
+ /* @__PURE__ */ new Set()
14410
+ );
14411
+ const touched = /* @__PURE__ */ new Set();
14412
+ const originalToNew = /* @__PURE__ */ new Map();
14413
+ const createdIds = [];
14414
+ let previousRootId;
14415
+ for (const copied of clipboard.activities) {
14416
+ const mappedParent = originalToNew.get(copied.originalParentId);
14417
+ const created = await createActivityCore(
14418
+ {
14419
+ parentId: mappedParent ?? destination.parentId,
14420
+ ...mappedParent === void 0 ? {
14421
+ afterSiblingId: previousRootId ?? action.destination.afterSiblingId
14422
+ } : {},
14423
+ overrides: cloneDomainValue(copied.overrides),
14424
+ eventSource: action.eventSource
14425
+ },
14426
+ deps,
14427
+ {
14428
+ skipCorrelativeRecompute: true,
14429
+ skipBeforeSnap: true,
14430
+ customIdReferenceId: action.destination.afterSiblingId
14431
+ }
14432
+ );
14433
+ if (!created.ok) return created;
14434
+ originalToNew.set(copied.originalId, created.newId);
14435
+ createdIds.push(created.newId);
14436
+ if (mappedParent === void 0) previousRootId = created.newId;
14437
+ }
14438
+ const correlativeShifts = recomputeCorrelativeIds(deps.adapter);
14439
+ const createdIdSet = new Set(createdIds.map(String));
14440
+ foldCorrelativeShifts(
14441
+ deps.adapter,
14442
+ correlativeShifts,
14443
+ createdIdSet,
14444
+ beforeSnapshot,
14445
+ touched
14446
+ );
14447
+ for (const createdId of createdIds) touched.add(String(createdId));
14448
+ const appliedLinks = [];
14449
+ for (const copiedLink of clipboard.links) {
14450
+ const source = originalToNew.get(copiedLink.source);
14451
+ const target = originalToNew.get(copiedLink.target);
14452
+ if (source === void 0 || target === void 0) return invariantFailure();
14453
+ const operation = {
14454
+ kind: "create",
14455
+ source,
14456
+ target,
14457
+ type: copiedLink.type,
14458
+ lag: copiedLink.lag
14459
+ };
14460
+ const result = applyLinkOperation(operation, {
14461
+ port: deps.adapter,
14462
+ newLinkId: deps.linkIdGen.next
14463
+ });
14464
+ if (!result.applied) return invariantFailure();
14465
+ appliedLinks.push({
14466
+ op: operation,
14467
+ finalLinkId: result.linkId,
14468
+ rejected: null
14469
+ });
14470
+ }
14471
+ const { scheduledIds } = await runPostMutation(
14472
+ {
14473
+ adapter: deps.adapter,
14474
+ scheduler: deps.scheduler,
14475
+ sector: deps.sector,
14476
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
14477
+ },
14478
+ {
14479
+ action,
14480
+ autoscheduleFrom: createdIds[0] ?? null,
14481
+ recomputeParentsFrom: createdIds,
14482
+ now: deps.now,
14483
+ options
14484
+ }
14485
+ );
14486
+ const linkChanges = buildLinkChangesForBatch(
14487
+ appliedLinks,
14488
+ /* @__PURE__ */ new Map(),
14489
+ deps.adapter
14490
+ );
14491
+ return {
14492
+ ok: true,
14493
+ changes: await assembleChangeSet(deps.adapter, {
14494
+ source: action,
14495
+ beforeSnap: beforeSnapshot,
14496
+ touchedIds: touched,
14497
+ scheduledIds,
14498
+ links: linkChanges,
13494
14499
  hoursPerDay: deps.sector.hoursPerDay
13495
14500
  })
13496
14501
  };
@@ -13498,6 +14503,18 @@ async function dispatchStatusCriteriaSet(action, deps) {
13498
14503
 
13499
14504
  // src/dispatch/dispatch.ts
13500
14505
  async function performDispatch(action, options, deps) {
14506
+ if (action.kind === "activity-copy") {
14507
+ return dispatchActivityCopy(action, {
14508
+ adapter: deps.adapter,
14509
+ clipboard: deps.activityClipboard
14510
+ });
14511
+ }
14512
+ if (action.kind === "activity-clipboard-clear") {
14513
+ return dispatchActivityClipboardClear(action, deps.activityClipboard);
14514
+ }
14515
+ if (action.kind === "activity-paste-copied") {
14516
+ return dispatchActivityPasteCopied(action, options, deps);
14517
+ }
13501
14518
  if (action.kind === "activity-batch") {
13502
14519
  return dispatchActivityBatch(action, options, deps);
13503
14520
  }
@@ -13522,9 +14539,6 @@ async function performDispatch(action, options, deps) {
13522
14539
  if (action.kind === "activity-create") {
13523
14540
  return dispatchActivityCreate(action, options, deps);
13524
14541
  }
13525
- if (action.kind === "activity-paste") {
13526
- return dispatchActivityPaste(action, options, deps);
13527
- }
13528
14542
  if (action.kind === "activity-delete") {
13529
14543
  return dispatchActivityDelete(action, options, deps);
13530
14544
  }
@@ -13568,7 +14582,7 @@ async function performDispatch(action, options, deps) {
13568
14582
  return dispatchSirSync(action, deps);
13569
14583
  }
13570
14584
  if (action.kind === "activity-lookahead-sync") {
13571
- return dispatchActivityLookaheadSync(action, deps);
14585
+ return dispatchActivityLookaheadSync(action, options, deps);
13572
14586
  }
13573
14587
  if (action.kind === "inline-edit" && (action.column === COLUMN.CUSTOM_PREDECESSORS || action.column === COLUMN.CUSTOM_SUCESSORS)) {
13574
14588
  return dispatchInlineEditLinks(action, options, deps);
@@ -16718,7 +17732,7 @@ var ViewStateStore = class {
16718
17732
  };
16719
17733
 
16720
17734
  // src/internal/state/schedule-state.ts
16721
- var ScheduleState = class {
17735
+ var ScheduleState = class _ScheduleState {
16722
17736
  _activities = /* @__PURE__ */ new Map();
16723
17737
  _lastStartDate = /* @__PURE__ */ new Map();
16724
17738
  _promotionSnapshot = /* @__PURE__ */ new Map();
@@ -16740,22 +17754,52 @@ var ScheduleState = class {
16740
17754
  _promotionSnapshotBefore = null;
16741
17755
  _calendar;
16742
17756
  _flags;
17757
+ _calendarSnapshot;
16743
17758
  constructor(snapshot, buildOrderComparator = () => null) {
16744
17759
  this._buildOrderComparator = buildOrderComparator;
17760
+ this._calendarSnapshot = cloneDomainValue(snapshot.calendars);
16745
17761
  for (const activity of snapshot.activities) {
16746
17762
  this._activities.set(activity.id, activity);
16747
17763
  }
16748
17764
  for (const link of snapshot.links) {
16749
- this._links.set(String(link.id), link);
16750
- this._indexLink(link);
17765
+ const normalized2 = {
17766
+ ...link,
17767
+ freeSlackHours: link.freeSlackHours ?? null,
17768
+ isCritical: link.isCritical === true
17769
+ };
17770
+ this._links.set(String(link.id), normalized2);
17771
+ this._indexLink(normalized2);
16751
17772
  }
16752
17773
  this._calendar = new CalendarCalculator(
16753
- createCalendarSetup(snapshot.calendars)
17774
+ createCalendarSetup(this._calendarSnapshot)
16754
17775
  );
16755
17776
  this._flags = buildFlags(snapshot);
16756
17777
  this._hierarchy = new HierarchyIndex(this._activities);
16757
17778
  this._hierarchy.rebuild();
16758
17779
  }
17780
+ fork() {
17781
+ const fork = new _ScheduleState(
17782
+ {
17783
+ activities: this.getAllActivities().map(cloneCoreActivity),
17784
+ links: this.getAllLinks().map((link) => ({ ...link })),
17785
+ calendars: cloneDomainValue(this._calendarSnapshot)
17786
+ },
17787
+ this._buildOrderComparator
17788
+ );
17789
+ fork._viewState.restore(cloneDomainValue(this._viewState.snapshot()));
17790
+ fork._lastStartDate = new Map(
17791
+ Array.from(this._lastStartDate, ([id, date2]) => [id, new Date(date2)])
17792
+ );
17793
+ fork._promotionSnapshot = new Map(
17794
+ Array.from(this._promotionSnapshot, ([id, snapshot]) => [
17795
+ id,
17796
+ cloneDomainValue(snapshot)
17797
+ ])
17798
+ );
17799
+ fork._flags = cloneDomainValue(this._flags);
17800
+ fork._filterProjection = cloneDomainValue(this._filterProjection);
17801
+ return fork;
17802
+ }
16759
17803
  getAllActivities() {
16760
17804
  return Array.from(this._activities.values());
16761
17805
  }
@@ -16979,6 +18023,7 @@ var ScheduleState = class {
16979
18023
  if (!activity) return;
16980
18024
  this._writeCapture.note(String(activityId), activity);
16981
18025
  activity[field] = value;
18026
+ refreshCalendarDurationIfDateBearing(activity, field);
16982
18027
  if (field === ACTIVITY_PROPERTY.PARENT) this._hierarchy.markDirty();
16983
18028
  if (fieldAffectsVisualOrder(field)) this._cache.visualOrderIds = null;
16984
18029
  }
@@ -16992,6 +18037,7 @@ var ScheduleState = class {
16992
18037
  }
16993
18038
  this._writeCapture.note(String(activityId), activity);
16994
18039
  Reflect.set(activity, field, coerceActivityFieldValue(field, value));
18040
+ refreshCalendarDurationIfDateBearing(activity, field);
16995
18041
  if (field === ACTIVITY_PROPERTY.PARENT) this._hierarchy.markDirty();
16996
18042
  if (fieldAffectsVisualOrder(field)) this._cache.visualOrderIds = null;
16997
18043
  }
@@ -17002,6 +18048,7 @@ var ScheduleState = class {
17002
18048
  let reparented = false;
17003
18049
  for (const field of Object.keys(fields)) {
17004
18050
  Reflect.set(activity, field, Reflect.get(fields, field));
18051
+ refreshCalendarDurationIfDateBearing(activity, field);
17005
18052
  if (field === ACTIVITY_PROPERTY.PARENT) reparented = true;
17006
18053
  if (fieldAffectsVisualOrder(field)) this._cache.visualOrderIds = null;
17007
18054
  }
@@ -17011,8 +18058,13 @@ var ScheduleState = class {
17011
18058
  this._writeCapture.noteLinkAdd(String(link.id));
17012
18059
  const superseded = this._links.get(String(link.id));
17013
18060
  if (superseded) this._deindexLink(superseded);
17014
- this._links.set(String(link.id), { ...link });
17015
- this._indexLink(link);
18061
+ const normalized2 = {
18062
+ ...link,
18063
+ freeSlackHours: link.freeSlackHours ?? null,
18064
+ isCritical: link.isCritical === true
18065
+ };
18066
+ this._links.set(String(link.id), normalized2);
18067
+ this._indexLink(normalized2);
17016
18068
  }
17017
18069
  _indexLink(link) {
17018
18070
  pushLinkRef(this._outgoing, String(link.source), link.id);
@@ -17165,11 +18217,17 @@ var ScheduleState = class {
17165
18217
  };
17166
18218
  var EMPTY_LINK_IDS = [];
17167
18219
  function isMutableLinkField(field) {
17168
- return field === LINK_PROPERTY.LAG || field === LINK_PROPERTY.TYPE;
18220
+ return field === LINK_PROPERTY.LAG || field === LINK_PROPERTY.TYPE || field === LINK_PROPERTY.FREE_SLACK_HOURS || field === LINK_PROPERTY.IS_CRITICAL;
17169
18221
  }
17170
18222
  function fieldAffectsVisualOrder(field) {
17171
18223
  return field === ACTIVITY_PROPERTY.PARENT || field === ACTIVITY_PROPERTY.CORRELATIVE_ID;
17172
18224
  }
18225
+ function refreshCalendarDurationIfDateBearing(activity, field) {
18226
+ if (field !== ACTIVITY_PROPERTY.START_DATE && field !== ACTIVITY_PROPERTY.END_DATE && field !== ACTIVITY_PROPERTY.TYPE) {
18227
+ return;
18228
+ }
18229
+ activity.calendarDuration = computeCalendarDuration(activity);
18230
+ }
17173
18231
  function detachLinkRef(refs, linkId) {
17174
18232
  return refs.filter((ref) => String(ref) !== String(linkId));
17175
18233
  }
@@ -17217,15 +18275,7 @@ function readChildren(state, parentId) {
17217
18275
  return out;
17218
18276
  }
17219
18277
  function readChildrenIds(state, parentId) {
17220
- const key = parentId;
17221
- if (key === ROOT_PARENT_ID) {
17222
- const roots = [];
17223
- state.forEachActivity((activity, id) => {
17224
- if (isRootParent(activity.parentId)) roots.push(id);
17225
- });
17226
- return roots;
17227
- }
17228
- return getChildrenInVisualOrder(key, state);
18278
+ return getChildrenInVisualOrder(parentId, state);
17229
18279
  }
17230
18280
  function readSelectedActivityIds(state) {
17231
18281
  return state.checkedIds().map(String);
@@ -17288,8 +18338,7 @@ function buildPipelineContext(adapter, options) {
17288
18338
  activityReader: createStateBackedActivityReader(adapter),
17289
18339
  dateFormat: options.dateFormat ?? "YYYY-MM-DD",
17290
18340
  customHours: options.customHours ?? {},
17291
- hoursPerDay: options.hoursPerDay,
17292
- inputUnit: options.inputUnit ?? "days"
18341
+ hoursPerDay: options.hoursPerDay
17293
18342
  };
17294
18343
  }
17295
18344
 
@@ -17299,15 +18348,26 @@ function willRunCriticalPath(action) {
17299
18348
  }
17300
18349
 
17301
18350
  // src/dispatch/undo/undo-entry.ts
17302
- function buildHistoryChangeSet(changes) {
17303
- const activitiesWithoutAfter = changes.activities.map((entry) => ({
18351
+ var DERIVED_LINK_FIELDS = /* @__PURE__ */ new Set(["freeSlackHours", "isCritical"]);
18352
+ function withoutDerivedFields(entry, derivedFields) {
18353
+ const fields = Object.fromEntries(
18354
+ Object.entries(entry.fields ?? {}).filter(
18355
+ ([field]) => !derivedFields.has(field)
18356
+ )
18357
+ );
18358
+ if (entry.kind === "updated" && Object.keys(fields).length === 0) return null;
18359
+ return {
17304
18360
  ...entry,
18361
+ ...entry.fields === void 0 ? {} : { fields },
17305
18362
  after: null
17306
- }));
17307
- const linksWithoutAfter = changes.links.map((entry) => ({
18363
+ };
18364
+ }
18365
+ function buildHistoryChangeSet(changes) {
18366
+ const activitiesWithoutAfter = changes.activities.map((entry) => ({
17308
18367
  ...entry,
17309
18368
  after: null
17310
18369
  }));
18370
+ const linksWithoutAfter = changes.links.map((entry) => withoutDerivedFields(entry, DERIVED_LINK_FIELDS)).filter((entry) => entry !== null);
17311
18371
  const source = changes.source.kind === "activity-batch" ? { ...changes.source, items: [], links: [] } : changes.source;
17312
18372
  return cloneDomainValue({
17313
18373
  ...changes,
@@ -17317,7 +18377,22 @@ function buildHistoryChangeSet(changes) {
17317
18377
  });
17318
18378
  }
17319
18379
  function buildUndoEntry(changeSet, beforeSnap, beforeLinks, afterSnap, afterLinks) {
17320
- return { changeSet, beforeSnap, beforeLinks, afterSnap, afterLinks };
18380
+ return {
18381
+ changeSet,
18382
+ beforeSnap,
18383
+ beforeLinks: resetDerivedLinkSnapshots(beforeLinks),
18384
+ afterSnap,
18385
+ afterLinks: resetDerivedLinkSnapshots(afterLinks)
18386
+ };
18387
+ }
18388
+ function resetDerivedLinkSnapshots(snapshots) {
18389
+ if (!snapshots) return void 0;
18390
+ return new Map(
18391
+ Array.from(snapshots, ([id, link]) => [
18392
+ id,
18393
+ { ...link, freeSlackHours: null, isCritical: false }
18394
+ ])
18395
+ );
17321
18396
  }
17322
18397
  function captureCreatedSnapshots(state, changeSet) {
17323
18398
  const afterSnap = /* @__PURE__ */ new Map();
@@ -17371,7 +18446,7 @@ function mergeCoalesced(top, next) {
17371
18446
  }
17372
18447
  function getDispatchHistoryPolicy(action) {
17373
18448
  if (action.kind === "persistence-acknowledge") return "clear-on-success";
17374
- 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 === "selection-toggle" || action.kind === "selection-replace" || action.kind === "visibility-set" || action.kind === "filter-set" || action.kind === "sort-set") {
18449
+ 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") {
17375
18450
  return "skip";
17376
18451
  }
17377
18452
  return "record";
@@ -17615,7 +18690,7 @@ function applyChangeSetSide(state, entry, side) {
17615
18690
  // src/dispatch/undo/undo-recorder.ts
17616
18691
  var DEFAULT_MAX_DEPTH = 100;
17617
18692
  var COALESCE_WINDOW_MS = 600;
17618
- var UndoRecorder = class {
18693
+ var UndoRecorder = class _UndoRecorder {
17619
18694
  constructor(maxDepth = DEFAULT_MAX_DEPTH) {
17620
18695
  this.maxDepth = maxDepth;
17621
18696
  }
@@ -17624,6 +18699,14 @@ var UndoRecorder = class {
17624
18699
  redoStack = [];
17625
18700
  _lastKey = null;
17626
18701
  _lastTime = 0;
18702
+ fork() {
18703
+ const fork = new _UndoRecorder(this.maxDepth);
18704
+ fork.undoStack.push(...cloneDomainValue(this.undoStack));
18705
+ fork.redoStack.push(...cloneDomainValue(this.redoStack));
18706
+ fork._lastKey = this._lastKey;
18707
+ fork._lastTime = this._lastTime;
18708
+ return fork;
18709
+ }
17627
18710
  record(entry, coalesceKey = null, now = 0) {
17628
18711
  const top = this.undoStack[this.undoStack.length - 1];
17629
18712
  if (coalesceKey != null && coalesceKey === this._lastKey && now - this._lastTime <= COALESCE_WINDOW_MS && top !== void 0 && canCoalesce(top, entry)) {
@@ -17747,40 +18830,56 @@ function buildStateSnapshot(activities, links, calendars, baseCalendars = []) {
17747
18830
  }
17748
18831
 
17749
18832
  // src/boundary/save/activity-changes.ts
17750
- var PERSISTABLE_ACTIVITY_FIELDS = [
17751
- "id",
17752
- "proplannerId",
17753
- "parentId",
17754
- "name",
17755
- "description",
17756
- "type",
17757
- "startDate",
17758
- "endDate",
17759
- "durationHours",
17760
- "constraintType",
17761
- "constraintDate",
17762
- "autoScheduling",
17763
- "calendarId",
17764
- "assignedDefaultCalendar",
17765
- "dateOrigin",
17766
- "progress",
17767
- "cost",
17768
- "usedCost",
17769
- "realCost",
17770
- "workHours",
17771
- "realWorkHours",
17772
- "ponderator",
17773
- "hasCustomPonderator",
17774
- "correlativeId",
17775
- "uniqueCorrelativeId",
17776
- "customId",
17777
- "subcontractId",
17778
- "isLookahead",
17779
- "newActivityIds",
17780
- "hasNewActivities",
17781
- "responsableIds",
17782
- "tagIds"
17783
- ];
18833
+ var ACTIVITY_SAVE_FIELD_POLICY = {
18834
+ id: "ignored",
18835
+ proplannerId: "ignored",
18836
+ parentId: "tracked",
18837
+ name: "tracked",
18838
+ description: "tracked",
18839
+ type: "tracked",
18840
+ startDate: "tracked",
18841
+ endDate: "tracked",
18842
+ durationHours: "tracked",
18843
+ constraintType: "tracked",
18844
+ constraintDate: "tracked",
18845
+ autoScheduling: "tracked",
18846
+ calendarId: "tracked",
18847
+ assignedDefaultCalendar: "ignored",
18848
+ dateOrigin: "tracked",
18849
+ progress: "tracked",
18850
+ cost: "tracked",
18851
+ usedCost: "tracked",
18852
+ realCost: "tracked",
18853
+ workHours: "tracked",
18854
+ realWorkHours: "tracked",
18855
+ ponderator: "tracked",
18856
+ hasCustomPonderator: "tracked",
18857
+ correlativeId: "tracked",
18858
+ uniqueCorrelativeId: "tracked",
18859
+ customId: "tracked",
18860
+ subcontractId: "tracked",
18861
+ isLookahead: "tracked",
18862
+ hasLookaheadTasks: "ignored",
18863
+ newActivityIds: "tracked",
18864
+ hasNewActivities: "tracked",
18865
+ isNewActivity: "ignored",
18866
+ pendingRequestIds: "ignored",
18867
+ responsableIds: "tracked",
18868
+ tagIds: "tracked",
18869
+ baselinePoints: "ignored",
18870
+ baselineSnapshot: "ignored",
18871
+ recursiveDurationHours: "ignored",
18872
+ isCritical: "ignored",
18873
+ freeSlackHours: "ignored",
18874
+ calendarDuration: "ignored",
18875
+ expectedProgress: "ignored",
18876
+ expectedProgressBaseline: "ignored",
18877
+ status: "ignored",
18878
+ criticalPath: "ignored"
18879
+ };
18880
+ var PERSISTABLE_ACTIVITY_FIELDS = Object.keys(ACTIVITY_SAVE_FIELD_POLICY).filter((field) => {
18881
+ return ACTIVITY_SAVE_FIELD_POLICY[field] === "tracked";
18882
+ });
17784
18883
  var DATE_FIELDS2 = /* @__PURE__ */ new Set([
17785
18884
  "startDate",
17786
18885
  "endDate",
@@ -17808,7 +18907,6 @@ function snapshotFieldValue(field, value) {
17808
18907
  return value;
17809
18908
  }
17810
18909
  function activityChangedSince(current, saved) {
17811
- if (current.assignedDefaultCalendar) return true;
17812
18910
  for (const field of PERSISTABLE_ACTIVITY_FIELDS) {
17813
18911
  if (!fieldEquals(field, current[field], saved[field])) return true;
17814
18912
  }
@@ -17826,15 +18924,35 @@ function dateEquals(current, saved) {
17826
18924
  return currentTime === savedTime;
17827
18925
  }
17828
18926
  function idArrayEquals(current, saved) {
17829
- const currentIds = Array.isArray(current) ? [...current].map(String).sort() : [];
17830
- const savedIds = Array.isArray(saved) ? [...saved].map(String).sort() : [];
17831
- if (currentIds.length !== savedIds.length) return false;
17832
- return currentIds.every((value, index) => value === savedIds[index]);
18927
+ const currentIds = new Set(Array.isArray(current) ? current.map(String) : []);
18928
+ const savedIds = new Set(Array.isArray(saved) ? saved.map(String) : []);
18929
+ if (currentIds.size !== savedIds.size) return false;
18930
+ for (const currentId of currentIds) {
18931
+ if (!savedIds.has(currentId)) return false;
18932
+ }
18933
+ return true;
17833
18934
  }
17834
18935
 
17835
18936
  // src/boundary/save/link-changes.ts
18937
+ var LINK_SAVE_FIELD_POLICY = {
18938
+ id: "ignored",
18939
+ source: "tracked",
18940
+ target: "tracked",
18941
+ type: "tracked",
18942
+ lag: "tracked",
18943
+ freeSlackHours: "ignored",
18944
+ isCritical: "ignored",
18945
+ ganttId: "ignored",
18946
+ sectorId: "ignored",
18947
+ proplannerId: "ignored"
18948
+ };
18949
+ var TRACKED_LINK_FIELDS = Object.keys(LINK_SAVE_FIELD_POLICY).filter((field) => {
18950
+ return LINK_SAVE_FIELD_POLICY[field] === "tracked";
18951
+ });
17836
18952
  function linkChangedSince(baseline, current) {
17837
- return baseline.source !== current.source || baseline.target !== current.target || baseline.lag !== current.lag || baseline.type !== current.type;
18953
+ return TRACKED_LINK_FIELDS.some(
18954
+ (field) => baseline[field] !== current[field]
18955
+ );
17838
18956
  }
17839
18957
  function checkNoUpdatedLinks(baseline, current) {
17840
18958
  const baselineById = new Map(
@@ -17848,23 +18966,73 @@ function checkNoUpdatedLinks(baseline, current) {
17848
18966
  }
17849
18967
 
17850
18968
  // src/boundary/save/save-tracker.ts
17851
- var SaveTracker = class {
18969
+ function hasPersistedIdentity(proplannerId) {
18970
+ const isNewIdentity = proplannerId == null || proplannerId === 0;
18971
+ if (isNewIdentity) return false;
18972
+ const isValidPersistedIdentity = typeof proplannerId === "number" && Number.isSafeInteger(proplannerId) && proplannerId > 0;
18973
+ if (!isValidPersistedIdentity) {
18974
+ throw new Error(`invalid proplannerId ${String(proplannerId)}`);
18975
+ }
18976
+ return true;
18977
+ }
18978
+ function assertValidPersistedIdentities(activities, links) {
18979
+ for (const activity of activities) {
18980
+ hasPersistedIdentity(activity.proplannerId);
18981
+ }
18982
+ for (const link of links) {
18983
+ hasPersistedIdentity(link.proplannerId);
18984
+ }
18985
+ }
18986
+ var SaveTracker = class _SaveTracker {
18987
+ trackingEnabled;
17852
18988
  initialized = false;
17853
18989
  dirtyActivityIds = /* @__PURE__ */ new Set();
17854
18990
  dirtyLinkIds = /* @__PURE__ */ new Set();
18991
+ initialCalendarRepairIds = /* @__PURE__ */ new Set();
17855
18992
  linksAtLastSave = /* @__PURE__ */ new Map();
17856
18993
  activitiesAtLastSave = /* @__PURE__ */ new Map();
18994
+ constructor(trackingEnabled = true) {
18995
+ this.trackingEnabled = trackingEnabled;
18996
+ }
18997
+ fork() {
18998
+ const fork = new _SaveTracker(false);
18999
+ fork.initialized = this.initialized;
19000
+ for (const [linkId, link] of this.linksAtLastSave) {
19001
+ fork.linksAtLastSave.set(linkId, cloneDomainValue(link));
19002
+ }
19003
+ for (const [activityId, activity] of this.activitiesAtLastSave) {
19004
+ fork.activitiesAtLastSave.set(activityId, cloneDomainValue(activity));
19005
+ }
19006
+ return fork;
19007
+ }
17857
19008
  init(activities, links) {
19009
+ if (!this.trackingEnabled) return;
19010
+ assertValidPersistedIdentities(activities, links);
17858
19011
  this.snapshotActivities(activities);
17859
19012
  this.snapshotLinks(links);
19013
+ for (const activity of activities) {
19014
+ if (activity.assignedDefaultCalendar) {
19015
+ const activityId = String(activity.id);
19016
+ this.initialCalendarRepairIds.add(activityId);
19017
+ this.dirtyActivityIds.add(activityId);
19018
+ }
19019
+ }
17860
19020
  this.initialized = true;
17861
19021
  }
19022
+ acknowledge(activities, links) {
19023
+ if (!this.trackingEnabled) return;
19024
+ assertValidPersistedIdentities(activities, links);
19025
+ this.snapshotActivities(activities);
19026
+ this.snapshotLinks(links);
19027
+ }
17862
19028
  snapshotActivities(activities) {
19029
+ if (!this.trackingEnabled) return;
17863
19030
  this.activitiesAtLastSave.clear();
17864
19031
  this.dirtyActivityIds.clear();
19032
+ this.initialCalendarRepairIds.clear();
17865
19033
  for (const activity of activities) {
17866
19034
  const activityId = String(activity.id);
17867
- if (activity.proplannerId == null) {
19035
+ if (!hasPersistedIdentity(activity.proplannerId)) {
17868
19036
  this.dirtyActivityIds.add(activityId);
17869
19037
  continue;
17870
19038
  }
@@ -17884,11 +19052,12 @@ var SaveTracker = class {
17884
19052
  }
17885
19053
  }
17886
19054
  snapshotLinks(links) {
19055
+ if (!this.trackingEnabled) return;
17887
19056
  this.linksAtLastSave.clear();
17888
19057
  this.dirtyLinkIds.clear();
17889
19058
  for (const link of links) {
17890
19059
  const linkId = String(link.id);
17891
- if (link.proplannerId == null) {
19060
+ if (!hasPersistedIdentity(link.proplannerId)) {
17892
19061
  this.dirtyLinkIds.add(linkId);
17893
19062
  continue;
17894
19063
  }
@@ -17903,12 +19072,11 @@ var SaveTracker = class {
17903
19072
  }
17904
19073
  }
17905
19074
  recordChanges(activities, links, getActivity, getLink) {
17906
- if (!this.initialized) return;
19075
+ if (!this.trackingEnabled || !this.initialized) return;
17907
19076
  for (const change of activities) {
17908
19077
  const activityId = String(change.id);
17909
19078
  const current = getActivity(activityId);
17910
- const saved = this.activitiesAtLastSave.get(activityId);
17911
- if (current === null ? saved !== void 0 : current.proplannerId == null || saved === void 0 || activityChangedSince(current, saved.persistable)) {
19079
+ if (this.activityIsDirty(activityId, current)) {
17912
19080
  this.dirtyActivityIds.add(activityId);
17913
19081
  } else {
17914
19082
  this.dirtyActivityIds.delete(activityId);
@@ -17917,81 +19085,147 @@ var SaveTracker = class {
17917
19085
  for (const change of links) {
17918
19086
  const linkId = String(change.id);
17919
19087
  const current = getLink(linkId);
17920
- const saved = this.linksAtLastSave.get(linkId);
17921
- if (current === null ? saved !== void 0 : current.proplannerId == null || saved === void 0 || linkChangedSince(saved, current)) {
19088
+ if (this.linkIsDirty(linkId, current)) {
17922
19089
  this.dirtyLinkIds.add(linkId);
17923
19090
  } else {
17924
19091
  this.dirtyLinkIds.delete(linkId);
17925
19092
  }
17926
19093
  }
17927
19094
  }
17928
- acknowledgePersistedLookahead(activityIds, getActivity) {
17929
- if (!this.initialized) return;
17930
- for (const activityId of activityIds) {
19095
+ activityIsDirty(activityId, current) {
19096
+ const saved = this.activitiesAtLastSave.get(activityId);
19097
+ if (current === null) return saved !== void 0;
19098
+ return this.initialCalendarRepairIds.has(activityId) || !hasPersistedIdentity(current.proplannerId) || saved === void 0 || activityChangedSince(current, saved.persistable);
19099
+ }
19100
+ linkIsDirty(linkId, current) {
19101
+ const saved = this.linksAtLastSave.get(linkId);
19102
+ if (current === null) return saved !== void 0;
19103
+ return !hasPersistedIdentity(current.proplannerId) || saved === void 0 || linkChangedSince(saved, current);
19104
+ }
19105
+ acknowledgePersistedLookahead(changes, getActivity) {
19106
+ if (!this.trackingEnabled || !this.initialized) return;
19107
+ const acknowledgements = changes.map((change) => {
19108
+ const activityId = typeof change === "string" ? change : String(change.id);
17931
19109
  const current = getActivity(activityId);
17932
19110
  const saved = this.activitiesAtLastSave.get(activityId);
17933
- if (current === null || saved === void 0) continue;
19111
+ const isUnknownPersistedActivity = current === null || saved === void 0;
19112
+ if (isUnknownPersistedActivity) {
19113
+ throw new Error(`unknown persisted activity "${activityId}"`);
19114
+ }
19115
+ return {
19116
+ activityId,
19117
+ acknowledgesLookahead: typeof change === "string" || Boolean(change.fields?.isLookahead),
19118
+ acknowledgesProgress: typeof change !== "string" && Boolean(change.fields?.progress),
19119
+ current,
19120
+ saved
19121
+ };
19122
+ });
19123
+ for (const {
19124
+ activityId,
19125
+ acknowledgesLookahead,
19126
+ acknowledgesProgress,
19127
+ current,
19128
+ saved
19129
+ } of acknowledgements) {
17934
19130
  const persistable = {
17935
19131
  ...saved.persistable,
17936
- isLookahead: current.isLookahead
19132
+ ...acknowledgesLookahead ? { isLookahead: current.isLookahead } : {},
19133
+ ...acknowledgesProgress ? { progress: current.progress } : {}
17937
19134
  };
17938
19135
  this.activitiesAtLastSave.set(activityId, {
17939
19136
  ...saved,
17940
19137
  persistable
17941
19138
  });
17942
- if (current.proplannerId == null || activityChangedSince(current, persistable)) {
19139
+ if (this.initialCalendarRepairIds.has(activityId) || !hasPersistedIdentity(current.proplannerId) || activityChangedSince(current, persistable)) {
17943
19140
  this.dirtyActivityIds.add(activityId);
17944
19141
  } else {
17945
19142
  this.dirtyActivityIds.delete(activityId);
17946
19143
  }
17947
19144
  }
17948
19145
  }
19146
+ hasCheckpointActivityId(activityId) {
19147
+ return this.activitiesAtLastSave.has(activityId);
19148
+ }
19149
+ hasCheckpointLinkId(linkId) {
19150
+ return this.linksAtLastSave.has(linkId);
19151
+ }
17949
19152
  hasUnsavedChanges() {
17950
- return this.initialized && (this.dirtyActivityIds.size > 0 || this.dirtyLinkIds.size > 0);
19153
+ return this.trackingEnabled && this.initialized && (this.dirtyActivityIds.size > 0 || this.dirtyLinkIds.size > 0);
19154
+ }
19155
+ newActivities(getActivity) {
19156
+ if (!this.trackingEnabled || !this.initialized) return [];
19157
+ const activities = [];
19158
+ for (const activityId of this.dirtyActivityIds) {
19159
+ const activity = getActivity(activityId);
19160
+ const isNewActivity = activity !== null && !hasPersistedIdentity(activity.proplannerId);
19161
+ if (isNewActivity) activities.push(activity);
19162
+ }
19163
+ return activities;
19164
+ }
19165
+ newLinks(getLink) {
19166
+ if (!this.trackingEnabled || !this.initialized) return [];
19167
+ const links = [];
19168
+ for (const linkId of this.dirtyLinkIds) {
19169
+ const link = getLink(linkId);
19170
+ const isNewLink = link !== null && !hasPersistedIdentity(link.proplannerId);
19171
+ if (isNewLink) links.push(link);
19172
+ }
19173
+ return links;
19174
+ }
19175
+ deletedActivities(getActivity) {
19176
+ if (!this.trackingEnabled || !this.initialized) return [];
19177
+ const deleted = [];
19178
+ for (const activityId of this.dirtyActivityIds) {
19179
+ const saved = this.activitiesAtLastSave.get(activityId);
19180
+ if (saved && getActivity(activityId) === null) {
19181
+ deleted.push({ ...saved.deletedSnapshot });
19182
+ }
19183
+ }
19184
+ return deleted;
17951
19185
  }
17952
- deletedActivities(activities) {
17953
- if (!this.initialized) return [];
17954
- const currentIds = new Set(
17955
- activities.map((activity) => String(activity.id))
17956
- );
19186
+ deletedLinks(getLink) {
19187
+ if (!this.trackingEnabled || !this.initialized) return [];
17957
19188
  const deleted = [];
17958
- for (const [activityId, saved] of this.activitiesAtLastSave) {
17959
- if (currentIds.has(activityId)) continue;
17960
- deleted.push({ ...saved.deletedSnapshot });
19189
+ for (const linkId of this.dirtyLinkIds) {
19190
+ const saved = this.linksAtLastSave.get(linkId);
19191
+ if (saved && getLink(linkId) === null) deleted.push({ ...saved });
17961
19192
  }
17962
19193
  return deleted;
17963
19194
  }
17964
- deletedLinks(links) {
17965
- if (!this.initialized) return [];
17966
- const currentIds = new Set(links.map((link) => String(link.id)));
17967
- return Array.from(
17968
- this.linksAtLastSave.entries(),
17969
- ([linkId, saved]) => currentIds.has(linkId) ? null : { ...saved }
17970
- ).filter((link) => link !== null);
17971
- }
17972
- modifiedActivities(activities) {
17973
- if (!this.initialized) return [];
17974
- return activities.filter((activity) => {
17975
- if (activity.proplannerId == null) return false;
17976
- if (activity.assignedDefaultCalendar) return true;
17977
- const saved = this.activitiesAtLastSave.get(String(activity.id));
17978
- if (!saved) return true;
17979
- return activityChangedSince(activity, saved.persistable);
17980
- });
19195
+ modifiedActivities(getActivity) {
19196
+ if (!this.trackingEnabled || !this.initialized) return [];
19197
+ const modified = [];
19198
+ for (const activityId of this.dirtyActivityIds) {
19199
+ const activity = getActivity(activityId);
19200
+ if (activity === null || !hasPersistedIdentity(activity.proplannerId)) {
19201
+ continue;
19202
+ }
19203
+ const saved = this.activitiesAtLastSave.get(activityId);
19204
+ if (saved === void 0 || this.initialCalendarRepairIds.has(activityId) || activityChangedSince(activity, saved.persistable)) {
19205
+ modified.push(activity);
19206
+ }
19207
+ }
19208
+ return modified;
17981
19209
  }
17982
- modifiedLinks(links) {
17983
- if (!this.initialized) return [];
17984
- return links.filter((link) => {
17985
- if (link.proplannerId == null) return false;
17986
- const saved = this.linksAtLastSave.get(String(link.id));
17987
- if (!saved) return true;
17988
- return linkChangedSince(saved, link);
17989
- });
19210
+ modifiedLinks(getLink) {
19211
+ if (!this.trackingEnabled || !this.initialized) return [];
19212
+ const modified = [];
19213
+ for (const linkId of this.dirtyLinkIds) {
19214
+ const link = getLink(linkId);
19215
+ if (link === null) continue;
19216
+ if (!hasPersistedIdentity(link.proplannerId)) continue;
19217
+ const saved = this.linksAtLastSave.get(linkId);
19218
+ if (saved === void 0 || linkChangedSince(saved, link)) {
19219
+ modified.push(link);
19220
+ }
19221
+ }
19222
+ return modified;
17990
19223
  }
17991
19224
  clear() {
17992
19225
  this.initialized = false;
17993
19226
  this.dirtyActivityIds.clear();
17994
19227
  this.dirtyLinkIds.clear();
19228
+ this.initialCalendarRepairIds.clear();
17995
19229
  this.linksAtLastSave.clear();
17996
19230
  this.activitiesAtLastSave.clear();
17997
19231
  }
@@ -18045,14 +19279,14 @@ function isAutoSchedulable(progress) {
18045
19279
  }
18046
19280
  function parseConstraintLabel(label, reporter) {
18047
19281
  if (!label) return null;
18048
- const normalized = CONSTRAINT_LABEL_MAP[label.toUpperCase()];
18049
- if (normalized === void 0) {
19282
+ const normalized2 = CONSTRAINT_LABEL_MAP[label.toUpperCase()];
19283
+ if (normalized2 === void 0) {
18050
19284
  reporter.warn(
18051
19285
  `[parseFromBackend] unknown constraint label "${label}" \u2014 defaulting to "asap"`
18052
19286
  );
18053
19287
  return DEFAULT_CONSTRAINT_TYPE;
18054
19288
  }
18055
- return normalized;
19289
+ return normalized2;
18056
19290
  }
18057
19291
  function resolveConstraint(raw, reporter) {
18058
19292
  const constraintType = parseConstraintLabel(raw.constraint, reporter);
@@ -18093,6 +19327,24 @@ function parseIdList(raw) {
18093
19327
  function parseParentId(rawParentId) {
18094
19328
  return rawParentId === ROOT_PARENT_ID ? null : String(rawParentId);
18095
19329
  }
19330
+ function normalizePersistedActivityId(rawId) {
19331
+ if (rawId == null) return null;
19332
+ const isValidIdentity = typeof rawId === "number" && (rawId === 0 || Number.isSafeInteger(rawId) && rawId > 0);
19333
+ if (!isValidIdentity) {
19334
+ throw new Error(`invalid activity proplannerId ${String(rawId)}`);
19335
+ }
19336
+ return rawId;
19337
+ }
19338
+ function parseCriticality(raw, reporter) {
19339
+ if (typeof raw.is_critical === "boolean") return raw.is_critical;
19340
+ reporter.error(
19341
+ "[parseFromBackend] invalid activity is_critical \u2014 defaulting to false",
19342
+ new TypeError(
19343
+ `activity "${raw.unique_id}" received ${typeof raw.is_critical}`
19344
+ )
19345
+ );
19346
+ return false;
19347
+ }
18096
19348
  function parseBaselinePoints(raw, reporter) {
18097
19349
  if (!Array.isArray(raw)) return [];
18098
19350
  return raw.map((point) => normalizeBaselinePoint(point, reporter)).filter((point) => point !== null);
@@ -18113,9 +19365,10 @@ function parseActivity(raw, context) {
18113
19365
  const startDate = parseBackendDate(raw.start_date);
18114
19366
  const endDate = isMilestone ? startDate : parseBackendDate(raw.end_date);
18115
19367
  const durationDays = isMilestone ? 0 : Number(raw.duration);
19368
+ const isCritical = parseCriticality(raw, context.reporter);
18116
19369
  return {
18117
19370
  id: String(raw.unique_id),
18118
- proplannerId: typeof raw.id === "number" ? raw.id : null,
19371
+ proplannerId: normalizePersistedActivityId(raw.id),
18119
19372
  parentId: parseParentId(raw.parent_id),
18120
19373
  name: raw.name,
18121
19374
  description: raw.description ?? "",
@@ -18155,7 +19408,12 @@ function parseActivity(raw, context) {
18155
19408
  Number(raw.sumOfDurationRecursively ?? 0),
18156
19409
  context.hoursPerDay
18157
19410
  ),
18158
- isCritical: Boolean(raw.is_critical),
19411
+ calendarDuration: computeCalendarDuration({
19412
+ type: raw.type,
19413
+ startDate,
19414
+ endDate
19415
+ }),
19416
+ isCritical,
18159
19417
  freeSlackHours: null,
18160
19418
  expectedProgress: null,
18161
19419
  expectedProgressBaseline: null,
@@ -18412,6 +19670,14 @@ function resolveLinkType(rawType) {
18412
19670
  if (isLinkTypeCode(rawType)) return rawType;
18413
19671
  return null;
18414
19672
  }
19673
+ function normalizePersistedLinkId(rawId) {
19674
+ if (rawId == null) return void 0;
19675
+ const isValidIdentity = typeof rawId === "number" && (rawId === 0 || Number.isSafeInteger(rawId) && rawId > 0);
19676
+ if (!isValidIdentity) {
19677
+ throw new Error(`invalid link proplannerId ${String(rawId)}`);
19678
+ }
19679
+ return rawId;
19680
+ }
18415
19681
  function normalizeLinks(rawLinks, context) {
18416
19682
  const parsedLinks = [];
18417
19683
  for (const rawLink of rawLinks) {
@@ -18450,16 +19716,18 @@ function parseLink(link, activityIds, hoursPerDay) {
18450
19716
  }
18451
19717
  const ganttIdRaw = link.ganttId;
18452
19718
  const sectorIdRaw = link.sectorId;
18453
- const proplannerIdRaw = link.id;
19719
+ const proplannerId = normalizePersistedLinkId(link.id);
18454
19720
  return {
18455
19721
  id,
18456
19722
  source,
18457
19723
  target,
18458
19724
  type,
18459
19725
  lag: lagDaysToHours(Number(link.lag ?? 0), hoursPerDay),
19726
+ freeSlackHours: null,
19727
+ isCritical: false,
18460
19728
  ...typeof ganttIdRaw === "number" ? { ganttId: ganttIdRaw } : {},
18461
19729
  ...typeof sectorIdRaw === "number" ? { sectorId: sectorIdRaw } : {},
18462
- ...typeof proplannerIdRaw === "number" ? { proplannerId: proplannerIdRaw } : {}
19730
+ ...proplannerId === void 0 ? {} : { proplannerId }
18463
19731
  };
18464
19732
  }
18465
19733
  function dedupeLinksBySignature(links) {
@@ -18519,7 +19787,8 @@ function parseSector(sector) {
18519
19787
  ...customIdIncrement !== void 0 ? { customIdIncrement } : {},
18520
19788
  ...isPrimaveraEndDate ? { updateDurationForPrimaveraEndDate: true } : {},
18521
19789
  activityCreter: parseActivityCreter(sector.activity_creter),
18522
- statusCriteria: DEFAULT_STATUS_CRITERIA
19790
+ statusCriteria: DEFAULT_STATUS_CRITERIA,
19791
+ ganttId: null
18523
19792
  };
18524
19793
  }
18525
19794
 
@@ -18585,6 +19854,9 @@ function createActivityIdGenerator(options = {}) {
18585
19854
  }
18586
19855
  }
18587
19856
  return {
19857
+ fork() {
19858
+ return createActivityIdGenerator({ seed: counter });
19859
+ },
18588
19860
  next() {
18589
19861
  counter += 1;
18590
19862
  return String(counter);
@@ -18616,6 +19888,9 @@ function createLinkIdGenerator(options = {}) {
18616
19888
  }
18617
19889
  }
18618
19890
  return {
19891
+ fork() {
19892
+ return createLinkIdGenerator({ seed: counter });
19893
+ },
18619
19894
  next() {
18620
19895
  counter += 1;
18621
19896
  return String(counter);
@@ -18651,6 +19926,9 @@ function createUniqueCorrelativeIdGenerator(options = {}) {
18651
19926
  }
18652
19927
  }
18653
19928
  return {
19929
+ fork() {
19930
+ return createUniqueCorrelativeIdGenerator({ seed: counter });
19931
+ },
18654
19932
  next() {
18655
19933
  counter += 1;
18656
19934
  return String(counter);
@@ -18778,7 +20056,7 @@ function selectGenerationStrategy(customId, useActivityCustomIdAsReference) {
18778
20056
  function cloneStats(stats) {
18779
20057
  return { ...stats, usedSuffixes: new Set(stats.usedSuffixes) };
18780
20058
  }
18781
- var CustomIdTracker = class {
20059
+ var CustomIdTracker = class _CustomIdTracker {
18782
20060
  prefixMap = /* @__PURE__ */ new Map();
18783
20061
  processedIds = /* @__PURE__ */ new Set();
18784
20062
  _journalPrefixBefore = null;
@@ -18839,6 +20117,19 @@ var CustomIdTracker = class {
18839
20117
  this.defaultPrefix = options.defaultPrefix ?? DEFAULT_PREFIX;
18840
20118
  this.suffixIncrement = options.suffixIncrement ?? DEFAULT_SUFFIX_INCREMENT;
18841
20119
  }
20120
+ fork() {
20121
+ const fork = new _CustomIdTracker({
20122
+ defaultPrefix: this.defaultPrefix,
20123
+ suffixIncrement: this.suffixIncrement
20124
+ });
20125
+ for (const [prefix, stats] of this.prefixMap) {
20126
+ fork.prefixMap.set(prefix, cloneStats(stats));
20127
+ }
20128
+ for (const customId of this.processedIds) {
20129
+ fork.processedIds.add(customId);
20130
+ }
20131
+ return fork;
20132
+ }
18842
20133
  extractPrefix(customId) {
18843
20134
  return extractPrefix(customId);
18844
20135
  }
@@ -19311,6 +20602,7 @@ function recomputeSkippedLeafEnds(state) {
19311
20602
  durationHours: activity.durationHours,
19312
20603
  task: activity
19313
20604
  });
20605
+ activity.calendarDuration = computeCalendarDuration(activity);
19314
20606
  });
19315
20607
  }
19316
20608
  function recomputeDurationsFromDates(state) {
@@ -19382,6 +20674,7 @@ function initializeCore(input) {
19382
20674
  );
19383
20675
  const sector = parsed2.sector;
19384
20676
  sector.statusCriteria = resolveStatusCriteria(input.statusCriteria);
20677
+ sector.ganttId = input.ganttId ?? null;
19385
20678
  const calendars = parsed2.calendars;
19386
20679
  const baseCalendars = parsed2.baseCalendars ?? [];
19387
20680
  const snapshot = buildStateSnapshot(
@@ -19443,6 +20736,133 @@ function initializeCore(input) {
19443
20736
  };
19444
20737
  }
19445
20738
 
20739
+ // src/previsualization/canonical-digest.ts
20740
+ function normalized(value) {
20741
+ if (value === void 0) return ["undefined"];
20742
+ if (value === null) return ["null"];
20743
+ if (typeof value === "string") return ["string", value];
20744
+ if (typeof value === "boolean") return ["boolean", value];
20745
+ if (typeof value === "number") {
20746
+ return ["number", Object.is(value, -0) ? "-0" : String(value)];
20747
+ }
20748
+ if (typeof value === "bigint") return ["bigint", String(value)];
20749
+ if (value instanceof Date) return ["date", String(value.getTime())];
20750
+ if (Array.isArray(value)) return ["array", value.map(normalized)];
20751
+ if (value instanceof Map) {
20752
+ return [
20753
+ "map",
20754
+ Array.from(value, ([key, entry]) => [
20755
+ normalized(key),
20756
+ normalized(entry)
20757
+ ]).sort(
20758
+ (left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right))
20759
+ )
20760
+ ];
20761
+ }
20762
+ if (value instanceof Set) {
20763
+ return [
20764
+ "set",
20765
+ Array.from(value, normalized).sort(
20766
+ (left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right))
20767
+ )
20768
+ ];
20769
+ }
20770
+ if (typeof value === "object") {
20771
+ return [
20772
+ "object",
20773
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalized(entry)])
20774
+ ];
20775
+ }
20776
+ throw new TypeError(`Unsupported canonical value: ${typeof value}`);
20777
+ }
20778
+ function serializeCanonicalSchedule(value) {
20779
+ return JSON.stringify(normalized(value));
20780
+ }
20781
+
20782
+ // src/previsualization/reconciliation.ts
20783
+ function entityChanges(target, source) {
20784
+ const targetById = new Map(
20785
+ target.map((entity) => [String(entity.id), entity])
20786
+ );
20787
+ const sourceById = new Map(
20788
+ source.map((entity) => [String(entity.id), entity])
20789
+ );
20790
+ const ids = [.../* @__PURE__ */ new Set([...targetById.keys(), ...sourceById.keys()])].sort();
20791
+ const changes = [];
20792
+ for (const id of ids) {
20793
+ const before = sourceById.get(id);
20794
+ const after = targetById.get(id);
20795
+ if (after === void 0) {
20796
+ changes.push({ id, kind: "deleted", after: null });
20797
+ continue;
20798
+ }
20799
+ if (before === void 0) {
20800
+ changes.push({ id, kind: "created", after: cloneDomainValue(after) });
20801
+ continue;
20802
+ }
20803
+ const fields = changedFields(before, after);
20804
+ if (Object.keys(fields).length > 0) {
20805
+ changes.push({
20806
+ id,
20807
+ kind: "updated",
20808
+ fields,
20809
+ after: cloneDomainValue(after)
20810
+ });
20811
+ }
20812
+ }
20813
+ return changes;
20814
+ }
20815
+ function changedFields(before, after) {
20816
+ const fields = {};
20817
+ const keys = [
20818
+ .../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])
20819
+ ].sort();
20820
+ for (const key of keys) {
20821
+ if (serializeCanonicalSchedule(before[key]) !== serializeCanonicalSchedule(after[key])) {
20822
+ fields[key] = {
20823
+ before: cloneDomainValue(before[key]),
20824
+ after: cloneDomainValue(after[key])
20825
+ };
20826
+ }
20827
+ }
20828
+ return fields;
20829
+ }
20830
+ function viewStateChanges(target, source) {
20831
+ const selectedBefore = new Set(source.selectedIds.map(String));
20832
+ const selectedAfter = new Set(target.selectedIds.map(String));
20833
+ const hiddenBefore = new Set(source.hiddenIds.map(String));
20834
+ const hiddenAfter = new Set(target.hiddenIds.map(String));
20835
+ const targetIds = target.activities.map(({ id }) => String(id)).sort();
20836
+ const changes = [];
20837
+ for (const activityId of targetIds) {
20838
+ const checkedBefore = selectedBefore.has(activityId);
20839
+ const checkedAfter = selectedAfter.has(activityId);
20840
+ const visibleBefore = !hiddenBefore.has(activityId);
20841
+ const visibleAfter = !hiddenAfter.has(activityId);
20842
+ if (checkedBefore === checkedAfter && visibleBefore === visibleAfter)
20843
+ continue;
20844
+ changes.push({
20845
+ activityId,
20846
+ ...checkedBefore === checkedAfter ? {} : { checked: { before: checkedBefore, after: checkedAfter } },
20847
+ ...visibleBefore === visibleAfter ? {} : { visible: { before: visibleBefore, after: visibleAfter } }
20848
+ });
20849
+ }
20850
+ return changes;
20851
+ }
20852
+ function createProjectionChangeSet(target, source) {
20853
+ const orderChanged = serializeCanonicalSchedule(target.order) !== serializeCanonicalSchedule(source.order);
20854
+ return {
20855
+ source: { kind: "init" },
20856
+ activities: entityChanges(target.activities, source.activities),
20857
+ links: entityChanges(target.links, source.links),
20858
+ calendars: entityChanges(target.calendars, source.calendars),
20859
+ trackingEvents: [],
20860
+ effects: [],
20861
+ viewState: viewStateChanges(target, source),
20862
+ ...orderChanged ? { order: cloneDomainValue(target.order) } : {}
20863
+ };
20864
+ }
20865
+
19446
20866
  // src/propagations/upward/date-gesture-preview.ts
19447
20867
  var emptyFormula = () => ({
19448
20868
  constantStart: null,
@@ -19588,6 +21008,10 @@ function evaluateEnd(formula, proposedEdgeTimeMs) {
19588
21008
  }
19589
21009
 
19590
21010
  // src/init/schedule-core.ts
21011
+ var FORK_SOURCE = /* @__PURE__ */ Symbol("ScheduleCore.fork.source");
21012
+ function acknowledgesPersistence(action) {
21013
+ return action.kind === "persistence-acknowledge" || action.kind === "activity-lookahead-sync" && action.acknowledgePersistedLookahead === true;
21014
+ }
19591
21015
  function changesRequireFilterProjectionRebuild(changes) {
19592
21016
  for (const change of changes.activities) {
19593
21017
  if (change.kind === "deleted") return true;
@@ -19611,9 +21035,11 @@ function isCurrentFilterProjection(projection, filter, context) {
19611
21035
  const hasCurrentLocale = projection.context.locale === context.locale;
19612
21036
  return hasCurrentHours && hasCurrentLocale;
19613
21037
  }
19614
- var ScheduleCore = class {
21038
+ var ScheduleCore = class _ScheduleCore {
21039
+ _activityClipboard = { value: null };
19615
21040
  _status = SCHEDULE_CORE_STATUS.READY;
19616
21041
  coreRuntime;
21042
+ _isFork;
19617
21043
  _undo = new UndoRecorder();
19618
21044
  _opQueue = Promise.resolve();
19619
21045
  _scheduleRevision = 0;
@@ -19644,6 +21070,42 @@ var ScheduleCore = class {
19644
21070
  _criticalPathRevision = -1;
19645
21071
  _activeCriticalPath = null;
19646
21072
  constructor(input) {
21073
+ if (FORK_SOURCE in input) {
21074
+ this._isFork = true;
21075
+ const source = input[FORK_SOURCE];
21076
+ const state = source.coreRuntime.state.fork();
21077
+ this.coreRuntime = {
21078
+ state,
21079
+ scheduler: new AutoScheduler(state, source.coreRuntime.reporter),
21080
+ sector: cloneDomainValue(source.coreRuntime.sector),
21081
+ activityIdGenerator: source.coreRuntime.activityIdGenerator.fork(),
21082
+ linkIdGenerator: source.coreRuntime.linkIdGenerator.fork(),
21083
+ uniqueCorrelativeIdGenerator: source.coreRuntime.uniqueCorrelativeIdGenerator.fork(),
21084
+ customIdTracker: source.coreRuntime.customIdTracker.fork(),
21085
+ calendars: new Map(
21086
+ Array.from(source.coreRuntime.calendars, ([id, calendar]) => [
21087
+ id,
21088
+ cloneDomainValue(calendar)
21089
+ ])
21090
+ ),
21091
+ baseCalendars: cloneDomainValue(source.coreRuntime.baseCalendars),
21092
+ clock: source.coreRuntime.clock,
21093
+ reporter: source.coreRuntime.reporter
21094
+ };
21095
+ this._activityClipboard.value = cloneDomainValue(
21096
+ source._activityClipboard.value
21097
+ );
21098
+ this._undo = source._undo.fork();
21099
+ this._saveTracker = source._saveTracker.fork();
21100
+ this._scheduleRevision = source._scheduleRevision;
21101
+ this._pendingScheduleRevision = source._scheduleRevision;
21102
+ this._criticalPathRevision = source._criticalPathRevision;
21103
+ this._initPromise = Promise.resolve();
21104
+ this._criticalPathReady = Promise.resolve(null);
21105
+ this._ready = this._initPromise;
21106
+ return;
21107
+ }
21108
+ this._isFork = false;
19647
21109
  const initialized = initializeCore(input);
19648
21110
  this.coreRuntime = {
19649
21111
  state: initialized.state,
@@ -19680,6 +21142,15 @@ var ScheduleCore = class {
19680
21142
  get ready() {
19681
21143
  return this._ready;
19682
21144
  }
21145
+ fork() {
21146
+ return this._enqueue(async () => {
21147
+ await this._ready;
21148
+ this.assertReady();
21149
+ return new _ScheduleCore({
21150
+ [FORK_SOURCE]: this
21151
+ });
21152
+ });
21153
+ }
19683
21154
  get criticalPathReady() {
19684
21155
  return this._criticalPathReady;
19685
21156
  }
@@ -19707,6 +21178,23 @@ var ScheduleCore = class {
19707
21178
  this.assertReady();
19708
21179
  return readAllActivities(this.coreRuntime.state);
19709
21180
  }
21181
+ /**
21182
+ * Read del plan de indent (ISSUE-080): los padres destino DISTINTOS bajo los
21183
+ * que quedarían las actividades si se indentaran ahora. Es exactamente la
21184
+ * planificación que ejecuta el handler (`planIndentMoves`, autoridad única),
21185
+ * expuesta para que la frontera pueda correr el protocolo de conversión a
21186
+ * madre (chequeo backend + modal) ANTES de despachar. Solo lecturas.
21187
+ */
21188
+ getIndentTargetParentIds(activityIds) {
21189
+ this.assertReady();
21190
+ const plan = planIndentMoves(
21191
+ activityIds.map((id) => String(id)),
21192
+ this.coreRuntime.state
21193
+ );
21194
+ return [
21195
+ ...new Set(plan.allowedMoves.map((move) => String(move.newParentId)))
21196
+ ];
21197
+ }
19710
21198
  getChildrenView(parentId) {
19711
21199
  this.assertReady();
19712
21200
  return readChildren(this.coreRuntime.state, parentId);
@@ -19715,10 +21203,45 @@ var ScheduleCore = class {
19715
21203
  this.assertReady();
19716
21204
  return readLink(this.coreRuntime.state, id);
19717
21205
  }
21206
+ getLinksForActivityView(activityId) {
21207
+ this.assertReady();
21208
+ const state = this.coreRuntime.state;
21209
+ const links = [];
21210
+ const seen = /* @__PURE__ */ new Set();
21211
+ const append = (linkId) => {
21212
+ if (seen.has(linkId)) return;
21213
+ seen.add(linkId);
21214
+ const link = readLink(state, linkId);
21215
+ if (link) links.push(link);
21216
+ };
21217
+ for (const linkId of state.getOutgoingLinkIds(activityId)) append(linkId);
21218
+ for (const linkId of state.getIncomingLinkIds(activityId)) append(linkId);
21219
+ return links;
21220
+ }
19718
21221
  getAllLinksView() {
19719
21222
  this.assertReady();
19720
21223
  return readAllLinks(this.coreRuntime.state);
19721
21224
  }
21225
+ auditEffectiveGraph() {
21226
+ this.assertReady();
21227
+ const state = this.coreRuntime.state;
21228
+ const logicalLinks = state.getAllLinks();
21229
+ const effectiveEdges = projectEffectiveEdges(state, logicalLinks);
21230
+ const components = circularComponents(effectiveEdges);
21231
+ let largestComponentSize = 0;
21232
+ for (const component of components) {
21233
+ if (component.length > largestComponentSize) {
21234
+ largestComponentSize = component.length;
21235
+ }
21236
+ }
21237
+ return {
21238
+ activityCount: state.getAllIds().length,
21239
+ logicalLinkCount: logicalLinks.length,
21240
+ effectiveEdgeCount: effectiveEdges.length,
21241
+ circularComponents: components,
21242
+ largestComponentSize
21243
+ };
21244
+ }
19722
21245
  getProperty(activityId, property) {
19723
21246
  this.assertReady();
19724
21247
  const snapshot = this.coreRuntime.state.getActivity(activityId);
@@ -19747,11 +21270,11 @@ var ScheduleCore = class {
19747
21270
  }
19748
21271
  getActiveFilter() {
19749
21272
  this.assertReady();
19750
- return this.coreRuntime.state.getActiveFilter();
21273
+ return cloneDomainValue(this.coreRuntime.state.getActiveFilter());
19751
21274
  }
19752
21275
  getActiveOrder() {
19753
21276
  this.assertReady();
19754
- return this.coreRuntime.state.getActiveOrder();
21277
+ return cloneDomainValue(this.coreRuntime.state.getActiveOrder());
19755
21278
  }
19756
21279
  getVisualOrderIds() {
19757
21280
  this.assertReady();
@@ -19792,23 +21315,65 @@ var ScheduleCore = class {
19792
21315
  }
19793
21316
  getModifiedLinks() {
19794
21317
  this.assertReady();
19795
- return this._saveTracker.modifiedLinks(this.getAllLinksView());
21318
+ return this._saveTracker.modifiedLinks((linkId) => this.coreRuntime.state.getLink(linkId)).map(cloneDomainValue);
21319
+ }
21320
+ getNewLinks() {
21321
+ this.assertReady();
21322
+ return this._saveTracker.newLinks((linkId) => this.coreRuntime.state.getLink(linkId)).map(cloneDomainValue);
19796
21323
  }
19797
21324
  getModifiedActivities() {
19798
21325
  this.assertReady();
19799
- return this._saveTracker.modifiedActivities(this.coreRuntime.state.getAllActivities()).map(cloneDomainValue);
21326
+ return this._saveTracker.modifiedActivities(
21327
+ (activityId) => this.coreRuntime.state.getActivity(activityId)
21328
+ ).map(cloneDomainValue);
21329
+ }
21330
+ getNewActivities() {
21331
+ this.assertReady();
21332
+ return this._saveTracker.newActivities(
21333
+ (activityId) => this.coreRuntime.state.getActivity(activityId)
21334
+ ).map(cloneDomainValue);
19800
21335
  }
19801
21336
  hasUnsavedChanges() {
19802
21337
  this.assertReady();
19803
21338
  return this._saveTracker.hasUnsavedChanges();
19804
21339
  }
21340
+ /** Read-only revision; moves once per substantive mutation. */
21341
+ getScheduleRevision() {
21342
+ return this._scheduleRevision;
21343
+ }
21344
+ getCanonicalDigest() {
21345
+ this.assertReady();
21346
+ const byId = (left, right) => String(left.id).localeCompare(String(right.id));
21347
+ return serializeCanonicalSchedule({
21348
+ sector: this.getSector(),
21349
+ activities: [...this.getAllActivitiesView()].sort(byId),
21350
+ links: [...this.getAllLinksView()].sort(byId),
21351
+ calendars: [...this.getAllCalendars()].sort(byId),
21352
+ baseCalendars: [...this.getBaseCalendars()].sort(byId)
21353
+ });
21354
+ }
21355
+ createProjectionChangeSetFrom(source) {
21356
+ this.assertReady();
21357
+ source.assertReady();
21358
+ const projectionOf = (core) => ({
21359
+ activities: core.getAllActivitiesView(),
21360
+ links: core.getAllLinksView(),
21361
+ calendars: core.getAllCalendars(),
21362
+ selectedIds: core.getSelectedActivityIds(),
21363
+ hiddenIds: core.getHiddenActivityIds(),
21364
+ order: collectBranchOrder(core.coreRuntime.state)
21365
+ });
21366
+ return createProjectionChangeSet(projectionOf(this), projectionOf(source));
21367
+ }
19805
21368
  getDeletedActivitiesSinceLastSave() {
19806
21369
  this.assertReady();
19807
- return this._saveTracker.deletedActivities(this.coreRuntime.state.getAllActivities()).map(cloneDomainValue);
21370
+ return this._saveTracker.deletedActivities(
21371
+ (activityId) => this.coreRuntime.state.getActivity(activityId)
21372
+ ).map(cloneDomainValue);
19808
21373
  }
19809
21374
  getDeletedLinksSinceLastSave() {
19810
21375
  this.assertReady();
19811
- return this._saveTracker.deletedLinks(this.getAllLinksView()).map(cloneDomainValue);
21376
+ return this._saveTracker.deletedLinks((linkId) => this.coreRuntime.state.getLink(linkId)).map(cloneDomainValue);
19812
21377
  }
19813
21378
  _enqueue(work) {
19814
21379
  const workPromise = this._opQueue.then(work, work);
@@ -19903,10 +21468,18 @@ var ScheduleCore = class {
19903
21468
  }
19904
21469
  async _runDispatch(action, options) {
19905
21470
  this.assertReady();
21471
+ if (this._isFork && acknowledgesPersistence(action)) {
21472
+ return {
21473
+ result: {
21474
+ ok: false,
21475
+ reason: REJECTION_REASON.SANDBOX_PERSISTENCE_FORBIDDEN
21476
+ },
21477
+ changedState: false
21478
+ };
21479
+ }
19906
21480
  const pipelineContext = buildPipelineContext(this.coreRuntime.state, {
19907
21481
  hoursPerDay: this.coreRuntime.sector.hoursPerDay,
19908
- dateFormat: this.coreRuntime.sector.dateFormat,
19909
- inputUnit: options.inputUnit ?? "days"
21482
+ dateFormat: this.coreRuntime.sector.dateFormat
19910
21483
  });
19911
21484
  const historyPolicy = getDispatchHistoryPolicy(action);
19912
21485
  const generatorSnapshot = [
@@ -19926,9 +21499,12 @@ var ScheduleCore = class {
19926
21499
  calendars: this.coreRuntime.calendars,
19927
21500
  activityIdGen: this.coreRuntime.activityIdGenerator,
19928
21501
  linkIdGen: this.coreRuntime.linkIdGenerator,
21502
+ isActivityIdReserved: (activityId) => this._saveTracker.hasCheckpointActivityId(activityId),
21503
+ isLinkIdReserved: (linkId) => this._saveTracker.hasCheckpointLinkId(linkId),
19929
21504
  uidGen: this.coreRuntime.uniqueCorrelativeIdGenerator,
19930
21505
  customIdTracker: this.coreRuntime.customIdTracker,
19931
21506
  reporter: this.coreRuntime.reporter,
21507
+ activityClipboard: this._activityClipboard,
19932
21508
  defaultBaseCalendarId: this.coreRuntime.baseCalendars.find(
19933
21509
  (calendar) => calendar.baseDefault
19934
21510
  )?.id ?? null,
@@ -19961,16 +21537,19 @@ var ScheduleCore = class {
19961
21537
  }
19962
21538
  if (result.ok) {
19963
21539
  if (historyPolicy === "clear-on-success") {
19964
- this._saveTracker.snapshotActivities(
19965
- this.coreRuntime.state.getAllActivities()
21540
+ this._saveTracker.acknowledge(
21541
+ this.coreRuntime.state.getAllActivities(),
21542
+ this.coreRuntime.state.getAllLinks()
19966
21543
  );
19967
- this._saveTracker.snapshotLinks(this.getAllLinksView());
19968
21544
  this._undo.clear();
19969
21545
  } else if (action.kind === "activity-lookahead-sync" && action.acknowledgePersistedLookahead === true) {
19970
21546
  this._saveTracker.acknowledgePersistedLookahead(
19971
- action.activityIds,
21547
+ result.changes.activities,
19972
21548
  (activityId) => this.coreRuntime.state.getActivity(activityId)
19973
21549
  );
21550
+ if (action.persistedProgress !== void 0) {
21551
+ this._undo.clear();
21552
+ }
19974
21553
  } else {
19975
21554
  this._saveTracker.recordChanges(
19976
21555
  result.changes.activities,
@@ -20234,13 +21813,20 @@ var ScheduleCore = class {
20234
21813
  this.coreRuntime.customIdTracker.beginCustomIdTransaction();
20235
21814
  let changes;
20236
21815
  try {
20237
- for (const [activityId, fields] of calculatedFields) {
21816
+ for (const [activityId, fields] of calculatedFields.activities) {
20238
21817
  setCriticalPathFieldsIfChanged(
20239
21818
  this.coreRuntime.state,
20240
21819
  activityId,
20241
21820
  fields
20242
21821
  );
20243
21822
  }
21823
+ for (const [linkId, fields] of calculatedFields.links) {
21824
+ setCriticalPathLinkFieldsIfChanged(
21825
+ this.coreRuntime.state,
21826
+ linkId,
21827
+ fields
21828
+ );
21829
+ }
20244
21830
  changes = await assembleChangeSet(this.coreRuntime.state, {
20245
21831
  source: { kind: "init" },
20246
21832
  beforeSnap: /* @__PURE__ */ new Map(),
@@ -20496,6 +22082,9 @@ var DEFAULT_HOURS_PER_DAY = 8;
20496
22082
 
20497
22083
  // src/dispatch/action-kinds.ts
20498
22084
  var DISPATCH_ACTION_KIND = {
22085
+ ACTIVITY_CLIPBOARD_CLEAR: "activity-clipboard-clear",
22086
+ ACTIVITY_COPY: "activity-copy",
22087
+ ACTIVITY_PASTE_COPIED: "activity-paste-copied",
20499
22088
  PERSISTENCE_ACKNOWLEDGE: "persistence-acknowledge",
20500
22089
  BASELINE_APPLY: "baseline-apply",
20501
22090
  PONDERATOR_CRITERION_SET: "ponderator-criterion-set",
@@ -20513,7 +22102,6 @@ var DISPATCH_ACTION_KIND = {
20513
22102
  ACTIVITY_INDENT: "activity-indent",
20514
22103
  ACTIVITY_OUTDENT: "activity-outdent",
20515
22104
  ACTIVITY_SET_PROGRESS: "activity-set-progress",
20516
- ACTIVITY_PASTE: "activity-paste",
20517
22105
  SELECTION_TOGGLE: "selection-toggle",
20518
22106
  SELECTION_REPLACE: "selection-replace",
20519
22107
  VISIBILITY_SET: "visibility-set",
@@ -20523,6 +22111,9 @@ var DISPATCH_ACTION_KIND = {
20523
22111
  ACTIVITY_LOOKAHEAD_SYNC: "activity-lookahead-sync"
20524
22112
  };
20525
22113
  var KIND_CATALOG_COVERS_UNION = {
22114
+ [DISPATCH_ACTION_KIND.ACTIVITY_CLIPBOARD_CLEAR]: true,
22115
+ [DISPATCH_ACTION_KIND.ACTIVITY_COPY]: true,
22116
+ [DISPATCH_ACTION_KIND.ACTIVITY_PASTE_COPIED]: true,
20526
22117
  [DISPATCH_ACTION_KIND.PERSISTENCE_ACKNOWLEDGE]: true,
20527
22118
  [DISPATCH_ACTION_KIND.BASELINE_APPLY]: true,
20528
22119
  [DISPATCH_ACTION_KIND.PONDERATOR_CRITERION_SET]: true,
@@ -20540,7 +22131,6 @@ var KIND_CATALOG_COVERS_UNION = {
20540
22131
  [DISPATCH_ACTION_KIND.ACTIVITY_INDENT]: true,
20541
22132
  [DISPATCH_ACTION_KIND.ACTIVITY_OUTDENT]: true,
20542
22133
  [DISPATCH_ACTION_KIND.ACTIVITY_SET_PROGRESS]: true,
20543
- [DISPATCH_ACTION_KIND.ACTIVITY_PASTE]: true,
20544
22134
  [DISPATCH_ACTION_KIND.SELECTION_TOGGLE]: true,
20545
22135
  [DISPATCH_ACTION_KIND.SELECTION_REPLACE]: true,
20546
22136
  [DISPATCH_ACTION_KIND.VISIBILITY_SET]: true,