@outbuild-company/schedule-core 1.6.3 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -134,13 +134,19 @@ var REJECTION_REASON = {
134
134
  ACTIVITY_NOT_FOUND: "activity_not_found",
135
135
  PARENT_NOT_FOUND: "parent_not_found",
136
136
  ACTIVITY_IDS_EMPTY: "activity_ids_empty",
137
+ NO_ACTIVITY_SELECTION: "no_activity_selection",
138
+ CLIPBOARD_EMPTY: "clipboard_empty",
139
+ ACTIVITY_PASTE_INVARIANT_FAILED: "activity_paste_invariant_failed",
137
140
  ANCHOR_SIBLING_CONFLICT: "anchor_sibling_conflict",
138
141
  ANCHOR_SIBLING_NOT_FOUND: "anchor_sibling_not_found",
139
142
  ANCHOR_SIBLING_WRONG_PARENT: "anchor_sibling_wrong_parent",
140
143
  PARENT_FROZEN_BY_SIR: "parent_frozen_by_sir",
144
+ SIR_REQUEST_NOT_PENDING: "sir_request_not_pending",
145
+ PREVIEW_UNSUPPORTED_ACTION: "preview_unsupported_action",
141
146
  CANNOT_MOVE_INTO_OWN_DESCENDANT: "cannot_move_into_own_descendant",
142
147
  INDENT_NO_ELIGIBLE_SIBLING: "indent_no_eligible_sibling",
143
148
  OUTDENT_ROOT_LEVEL_NOT_EDITABLE: "outdent_root_level_not_editable",
149
+ HIERARCHY_CYCLE: "hierarchy_cycle",
144
150
  INVALID_PROGRESS_VALUE: "invalid_progress_value_must_be_0_or_100"
145
151
  };
146
152
 
@@ -269,8 +275,17 @@ function toDays(hours, hoursPerDay) {
269
275
  return hours / hoursPerDay;
270
276
  }
271
277
  function calendarDaySpan(startDate, endDate) {
272
- const elapsed = endDate.getTime() - startDate.getTime();
273
- return Math.floor(elapsed / MILLISECONDS_PER_DAY) + 1;
278
+ const startDay = Date.UTC(
279
+ startDate.getUTCFullYear(),
280
+ startDate.getUTCMonth(),
281
+ startDate.getUTCDate()
282
+ );
283
+ const endDay = Date.UTC(
284
+ endDate.getUTCFullYear(),
285
+ endDate.getUTCMonth(),
286
+ endDate.getUTCDate()
287
+ );
288
+ return Math.floor((endDay - startDay) / MILLISECONDS_PER_DAY) + 1;
274
289
  }
275
290
  var FIELD_REGISTRY = {
276
291
  name: { valueKind: "string", extract: (activity) => activity.name },
@@ -285,7 +300,10 @@ var FIELD_REGISTRY = {
285
300
  },
286
301
  uniqueCorrelativeId: {
287
302
  valueKind: "number",
288
- extract: (activity) => Number(activity.uniqueCorrelativeId)
303
+ extract: (activity) => {
304
+ const value = Number(activity.uniqueCorrelativeId);
305
+ return Number.isFinite(value) ? value : null;
306
+ }
289
307
  },
290
308
  progress: { valueKind: "number", extract: (activity) => activity.progress },
291
309
  durationDays: {
@@ -294,7 +312,7 @@ var FIELD_REGISTRY = {
294
312
  },
295
313
  calendarDuration: {
296
314
  valueKind: "number",
297
- extract: (activity) => calendarDaySpan(activity.startDate, activity.endDate)
315
+ extract: (activity) => activity.type === "milestone" ? 0 : calendarDaySpan(activity.startDate, activity.endDate)
298
316
  },
299
317
  cost: { valueKind: "number", extract: (activity) => activity.cost },
300
318
  usedCost: { valueKind: "number", extract: (activity) => activity.usedCost },
@@ -660,34 +678,102 @@ var OPERATORS_BY_KIND = {
660
678
  boolean: /* @__PURE__ */ new Set(["someOf", "notSomeOf"]),
661
679
  reference: /* @__PURE__ */ new Set(["someOf", "notSomeOf"])
662
680
  };
663
- function validateCriterion(criterion) {
681
+ function normalizeDateRange(value) {
682
+ if (value === void 0) return { ok: true, dateRange: void 0 };
683
+ if (typeof value !== "object" || value === null) {
684
+ return { ok: false, reason: "invalid_value" };
685
+ }
686
+ const { start, end } = value;
687
+ if (!(start instanceof Date) || !(end instanceof Date)) {
688
+ return { ok: false, reason: "invalid_value" };
689
+ }
690
+ const startTime = start.getTime();
691
+ const endTime = end.getTime();
692
+ if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || startTime > endTime) {
693
+ return { ok: false, reason: "invalid_value" };
694
+ }
695
+ return {
696
+ ok: true,
697
+ dateRange: { start: new Date(startTime), end: new Date(endTime) }
698
+ };
699
+ }
700
+ function normalizeCriterion(criterion) {
664
701
  const descriptor = getFieldDescriptor(criterion.field);
665
- if (descriptor === null) return "unknown_field";
702
+ if (descriptor === null) return { ok: false, reason: "unknown_field" };
666
703
  if (!OPERATORS_BY_KIND[descriptor.valueKind].has(criterion.operator)) {
667
- return "unknown_operator";
704
+ return { ok: false, reason: "unknown_operator" };
705
+ }
706
+ if (descriptor.valueKind === "number") {
707
+ const value = normalizeNumberValue(criterion.value);
708
+ return value === null ? { ok: false, reason: "invalid_value" } : {
709
+ ok: true,
710
+ criterion: { ...criterion, value }
711
+ };
712
+ }
713
+ if (descriptor.valueKind === "date") {
714
+ const value = normalizeDateValue(criterion.value);
715
+ return value === null ? { ok: false, reason: "invalid_value" } : {
716
+ ok: true,
717
+ criterion: { ...criterion, value }
718
+ };
719
+ }
720
+ const rejection = validateValueForKind(criterion, descriptor.valueKind);
721
+ if (rejection !== null) return { ok: false, reason: rejection };
722
+ if (descriptor.valueKind === "boolean") {
723
+ return {
724
+ ok: true,
725
+ criterion: {
726
+ ...criterion,
727
+ value: [...new Set(criterion.value)]
728
+ }
729
+ };
730
+ }
731
+ if (descriptor.valueKind === "id-array" || descriptor.valueKind === "enum" || descriptor.valueKind === "reference") {
732
+ return {
733
+ ok: true,
734
+ criterion: {
735
+ ...criterion,
736
+ value: [
737
+ ...new Set(
738
+ criterion.value.map(String)
739
+ )
740
+ ]
741
+ }
742
+ };
668
743
  }
669
- return validateValueForKind(criterion, descriptor.valueKind);
744
+ return { ok: true, criterion };
670
745
  }
671
746
  function validateValueForKind(criterion, valueKind) {
672
747
  const { value } = criterion;
673
- if (valueKind === "id-array" || valueKind === "enum") {
674
- return Array.isArray(value) ? null : "invalid_value";
675
- }
676
- if (Array.isArray(value)) return "invalid_value";
677
- if (valueKind === "number") {
678
- return Number.isFinite(Number(value)) ? null : "invalid_value";
748
+ if (valueKind === "boolean") {
749
+ return Array.isArray(value) && value.every((item) => typeof item === "boolean") ? null : "invalid_value";
679
750
  }
680
- if (valueKind === "date") {
681
- return isValidDateValue(value) ? null : "invalid_value";
751
+ if (valueKind === "id-array" || valueKind === "enum" || valueKind === "reference") {
752
+ return Array.isArray(value) && value.every(
753
+ (item) => typeof item === "string" || typeof item === "number"
754
+ ) ? null : "invalid_value";
682
755
  }
756
+ if (Array.isArray(value)) return "invalid_value";
683
757
  return null;
684
758
  }
685
- function isValidDateValue(value) {
686
- if (value instanceof Date) return true;
687
- if (typeof value === "string" || typeof value === "number") {
688
- return !Number.isNaN(new Date(value).getTime());
689
- }
690
- return false;
759
+ function normalizeNumberValue(value) {
760
+ if (typeof value === "string" && value.trim() === "") return null;
761
+ if (typeof value !== "string" && typeof value !== "number") return null;
762
+ const parsed2 = typeof value === "number" ? value : Number(value);
763
+ return Number.isFinite(parsed2) ? parsed2 : null;
764
+ }
765
+ function normalizeDateValue(value) {
766
+ let date2;
767
+ if (value instanceof Date) date2 = new Date(value.getTime());
768
+ else if (typeof value === "string") date2 = new Date(value);
769
+ else if (typeof value === "number") date2 = new Date(value);
770
+ else return null;
771
+ return Number.isFinite(date2.getTime()) ? date2 : null;
772
+ }
773
+
774
+ // src/shared/clone-domain-value.ts
775
+ function cloneDomainValue(value) {
776
+ return structuredClone(value);
691
777
  }
692
778
 
693
779
  // src/dispatch/filter.ts
@@ -717,15 +803,21 @@ function resolveFilter(adapter, filter, hoursPerDay) {
717
803
  }
718
804
  function dispatchFilterSet(action, deps) {
719
805
  const { adapter, hoursPerDay } = deps;
806
+ const criteria = [];
720
807
  for (const criterion of action.criteria) {
721
- const rejection = validateCriterion(criterion);
722
- if (rejection !== null) return { ok: false, reason: rejection };
723
- }
724
- const filter = action.dateRange === void 0 ? { criteria: action.criteria, logic: action.logic } : {
725
- criteria: action.criteria,
726
- logic: action.logic,
727
- dateRange: action.dateRange
728
- };
808
+ const result = normalizeCriterion(criterion);
809
+ if (!result.ok) return { ok: false, reason: result.reason };
810
+ criteria.push(result.criterion);
811
+ }
812
+ const rangeResult = normalizeDateRange(action.dateRange);
813
+ if (!rangeResult.ok) return { ok: false, reason: rangeResult.reason };
814
+ const filter = cloneDomainValue(
815
+ rangeResult.dateRange === void 0 ? { criteria, logic: action.logic } : {
816
+ criteria,
817
+ logic: action.logic,
818
+ dateRange: rangeResult.dateRange
819
+ }
820
+ );
729
821
  adapter.setActiveFilter(isEmptyFilter(filter) ? null : filter);
730
822
  const { visibleIds, projection } = resolveFilter(
731
823
  adapter,
@@ -870,7 +962,7 @@ function dispatchSortSet(action, deps) {
870
962
  const rejection = validateRule(rule);
871
963
  if (rejection !== null) return { ok: false, reason: rejection };
872
964
  }
873
- const order = action.rules.length === 0 ? null : { rules: action.rules };
965
+ const order = action.rules.length === 0 ? null : cloneDomainValue({ rules: action.rules });
874
966
  adapter.setActiveOrder(order);
875
967
  const changes = {
876
968
  source: action,
@@ -1438,6 +1530,11 @@ function isDescendantOf(childId, ancestorId, adapter) {
1438
1530
  }
1439
1531
  return false;
1440
1532
  }
1533
+ function isSummaryHierarchyLink(source, target, adapter) {
1534
+ const sourceIsSummary = isSummary2(source, adapter);
1535
+ const targetIsSummary = isSummary2(target, adapter);
1536
+ return targetIsSummary && isDescendantOf(source, target, adapter) || sourceIsSummary && isDescendantOf(target, source, adapter);
1537
+ }
1441
1538
  function getLeafDescendants(parentId, adapter) {
1442
1539
  const leaves = [];
1443
1540
  const stack = [parentId];
@@ -2375,6 +2472,18 @@ var noopReporter = {
2375
2472
  }
2376
2473
  };
2377
2474
 
2475
+ // src/internal/state/calendar-duration.ts
2476
+ var CALENDAR_DAY_MS = 864e5;
2477
+ function computeCalendarDuration(activity) {
2478
+ if (activity.type === "milestone") return 0;
2479
+ const start = activity.startDate?.getTime();
2480
+ const end = activity.endDate?.getTime();
2481
+ if (start == null || end == null || Number.isNaN(start) || Number.isNaN(end)) {
2482
+ return null;
2483
+ }
2484
+ return Math.floor((end - start) / CALENDAR_DAY_MS) + 1;
2485
+ }
2486
+
2378
2487
  // src/autoscheduler/integration/apply-results.ts
2379
2488
  function applyResults(adapter, result) {
2380
2489
  if (result.cancelled || result.updatedIds.length === 0) return;
@@ -2386,6 +2495,7 @@ function applyResults(adapter, result) {
2386
2495
  if (!liveActivity) continue;
2387
2496
  liveActivity.startDate = plan.startDate;
2388
2497
  liveActivity.endDate = plan.endDate;
2498
+ liveActivity.calendarDuration = computeCalendarDuration(liveActivity);
2389
2499
  adapter.updateActivity(id);
2390
2500
  }
2391
2501
  });
@@ -2452,13 +2562,253 @@ function hasPath(adjacency, from, to) {
2452
2562
  return false;
2453
2563
  }
2454
2564
 
2565
+ // src/internal/dependency-graph/strongly-connected-components.ts
2566
+ function circularComponents(edges) {
2567
+ const walk = buildWalkState(edges);
2568
+ for (const node of walk.adjacency.keys()) {
2569
+ if (!walk.records.has(node)) collectFromRoot(node, walk);
2570
+ }
2571
+ return walk.components;
2572
+ }
2573
+ function buildWalkState(edges) {
2574
+ const adjacency = /* @__PURE__ */ new Map();
2575
+ const selfLooped = /* @__PURE__ */ new Set();
2576
+ for (const edge of edges) {
2577
+ const source = String(edge.source);
2578
+ const target = String(edge.target);
2579
+ const sourceNeighbors = neighborsOf(adjacency, source);
2580
+ neighborsOf(adjacency, target);
2581
+ if (source === target) {
2582
+ selfLooped.add(source);
2583
+ continue;
2584
+ }
2585
+ sourceNeighbors.push(target);
2586
+ }
2587
+ return {
2588
+ adjacency,
2589
+ selfLooped,
2590
+ records: /* @__PURE__ */ new Map(),
2591
+ componentStack: [],
2592
+ nextIndex: 0,
2593
+ components: []
2594
+ };
2595
+ }
2596
+ function neighborsOf(adjacency, node) {
2597
+ const existing = adjacency.get(node);
2598
+ if (existing) return existing;
2599
+ const created = [];
2600
+ adjacency.set(node, created);
2601
+ return created;
2602
+ }
2603
+ function collectFromRoot(root, walk) {
2604
+ const frames = [];
2605
+ frames.push({ node: root, record: enterNode(root, walk), neighborCursor: 0 });
2606
+ while (frames.length > 0) {
2607
+ const frame = frames[frames.length - 1];
2608
+ if (frame === void 0) break;
2609
+ const neighbors = walk.adjacency.get(frame.node) ?? [];
2610
+ if (frame.neighborCursor < neighbors.length) {
2611
+ const neighbor = neighbors[frame.neighborCursor];
2612
+ frame.neighborCursor += 1;
2613
+ if (neighbor === void 0) continue;
2614
+ visitNeighbor(frame, neighbor, frames, walk);
2615
+ continue;
2616
+ }
2617
+ frames.pop();
2618
+ const parentFrame = frames[frames.length - 1];
2619
+ if (parentFrame && frame.record.lowLink < parentFrame.record.lowLink) {
2620
+ parentFrame.record.lowLink = frame.record.lowLink;
2621
+ }
2622
+ const isComponentRoot = frame.record.lowLink === frame.record.visitIndex;
2623
+ if (isComponentRoot) emitComponent(frame.node, walk);
2624
+ }
2625
+ }
2626
+ function visitNeighbor(frame, neighbor, frames, walk) {
2627
+ const neighborRecord = walk.records.get(neighbor);
2628
+ if (!neighborRecord) {
2629
+ frames.push({
2630
+ node: neighbor,
2631
+ record: enterNode(neighbor, walk),
2632
+ neighborCursor: 0
2633
+ });
2634
+ return;
2635
+ }
2636
+ const lowersCurrent = neighborRecord.onStack && neighborRecord.visitIndex < frame.record.lowLink;
2637
+ if (lowersCurrent) frame.record.lowLink = neighborRecord.visitIndex;
2638
+ }
2639
+ function enterNode(node, walk) {
2640
+ const record = {
2641
+ visitIndex: walk.nextIndex,
2642
+ lowLink: walk.nextIndex,
2643
+ onStack: true
2644
+ };
2645
+ walk.nextIndex += 1;
2646
+ walk.records.set(node, record);
2647
+ walk.componentStack.push({ node, record });
2648
+ return record;
2649
+ }
2650
+ function emitComponent(rootNode, walk) {
2651
+ const component = [];
2652
+ for (; ; ) {
2653
+ const entry = walk.componentStack.pop();
2654
+ if (entry === void 0) break;
2655
+ entry.record.onStack = false;
2656
+ component.push(entry.node);
2657
+ if (entry.node === rootNode) break;
2658
+ }
2659
+ const firstMember = component[0];
2660
+ const isSingleSelfLoop = component.length === 1 && firstMember !== void 0 && walk.selfLooped.has(firstMember);
2661
+ const isCircular = component.length > 1 || isSingleSelfLoop;
2662
+ if (isCircular) walk.components.push(component);
2663
+ }
2664
+
2665
+ // src/internal/dependency-graph/effective-graph.ts
2666
+ function projectEffectiveEdges(port, links, proposal) {
2667
+ const projectionPort = proposal ? new ReparentedPort(port, proposal) : port;
2668
+ return expandParentLinks([...links], projectionPort);
2669
+ }
2670
+ var ReparentedPort = class {
2671
+ constructor(base, proposal) {
2672
+ this.base = base;
2673
+ for (const [childId, proposedParent] of proposal.reparented) {
2674
+ const childKey = String(childId);
2675
+ const newParent = proposedParent === null || isRootParent(proposedParent) ? null : proposedParent;
2676
+ const oldParent = base.getActivity(childId)?.parentId ?? null;
2677
+ const oldKey = oldParent === null || isRootParent(oldParent) ? null : String(oldParent);
2678
+ const newKey = newParent === null ? null : String(newParent);
2679
+ if (oldKey === newKey) continue;
2680
+ this.reparented.set(childKey, newParent);
2681
+ if (oldKey !== null) {
2682
+ const removed = this.movedOutByParent.get(oldKey) ?? /* @__PURE__ */ new Set();
2683
+ removed.add(childKey);
2684
+ this.movedOutByParent.set(oldKey, removed);
2685
+ }
2686
+ if (newKey !== null) {
2687
+ const gained = this.movedInByParent.get(newKey) ?? [];
2688
+ gained.push(childId);
2689
+ this.movedInByParent.set(newKey, gained);
2690
+ }
2691
+ }
2692
+ }
2693
+ base;
2694
+ reparented = /* @__PURE__ */ new Map();
2695
+ movedOutByParent = /* @__PURE__ */ new Map();
2696
+ movedInByParent = /* @__PURE__ */ new Map();
2697
+ withOverride(activity) {
2698
+ if (!activity) return null;
2699
+ const override = this.reparented.get(String(activity.id));
2700
+ if (override === void 0) return activity;
2701
+ return { ...activity, parentId: override };
2702
+ }
2703
+ getActivity(activityId) {
2704
+ return this.withOverride(this.base.getActivity(activityId));
2705
+ }
2706
+ getLiveActivity(activityId) {
2707
+ return this.withOverride(this.base.getLiveActivity(activityId));
2708
+ }
2709
+ getAllActivities() {
2710
+ return this.base.getAllActivities().map((activity) => this.withOverride(activity) ?? activity);
2711
+ }
2712
+ getChildren(parentId) {
2713
+ const parentKey = String(parentId);
2714
+ const removed = this.movedOutByParent.get(parentKey);
2715
+ const baseChildren = this.base.getChildren(parentId);
2716
+ const kept = removed ? baseChildren.filter((childId) => !removed.has(String(childId))) : [...baseChildren];
2717
+ const gained = this.movedInByParent.get(parentKey);
2718
+ if (gained) kept.push(...gained);
2719
+ return kept;
2720
+ }
2721
+ getParent(activityId) {
2722
+ const override = this.reparented.get(String(activityId));
2723
+ if (override === void 0) return this.base.getParent(activityId);
2724
+ if (override === null) return null;
2725
+ return this.getActivity(override);
2726
+ }
2727
+ isChildOf(childId, parentId) {
2728
+ const targetKey = String(parentId);
2729
+ const visited = /* @__PURE__ */ new Set();
2730
+ let current = this.getActivity(childId);
2731
+ while (current) {
2732
+ const parent = current.parentId;
2733
+ if (parent === null || isRootParent(parent)) return false;
2734
+ const parentKey = String(parent);
2735
+ if (parentKey === targetKey) return true;
2736
+ if (visited.has(parentKey)) return false;
2737
+ visited.add(parentKey);
2738
+ current = this.getActivity(parent);
2739
+ }
2740
+ return false;
2741
+ }
2742
+ getAllLinks() {
2743
+ return this.base.getAllLinks();
2744
+ }
2745
+ getAllIds() {
2746
+ return this.base.getAllIds();
2747
+ }
2748
+ getLink(linkId) {
2749
+ return this.base.getLink(linkId);
2750
+ }
2751
+ activityExists(activityId) {
2752
+ return this.base.activityExists(activityId);
2753
+ }
2754
+ getOutgoingLinkIds(activityId) {
2755
+ return this.base.getOutgoingLinkIds(activityId);
2756
+ }
2757
+ getIncomingLinkIds(activityId) {
2758
+ return this.base.getIncomingLinkIds(activityId);
2759
+ }
2760
+ getClosestWorkTime(params) {
2761
+ return this.base.getClosestWorkTime(params);
2762
+ }
2763
+ calculateEndDate(params) {
2764
+ return this.base.calculateEndDate(params);
2765
+ }
2766
+ calculateDuration(params) {
2767
+ return this.base.calculateDuration(params);
2768
+ }
2769
+ batchUpdate(runBatch) {
2770
+ this.base.batchUpdate(runBatch);
2771
+ }
2772
+ updateActivity(activityId) {
2773
+ this.base.updateActivity(activityId);
2774
+ }
2775
+ getFlags() {
2776
+ return this.base.getFlags();
2777
+ }
2778
+ getProjectEnd() {
2779
+ return this.base.getProjectEnd();
2780
+ }
2781
+ };
2782
+
2783
+ // src/internal/dependency-graph/effective-graph-validation.ts
2784
+ function validateTopologyCommit(input) {
2785
+ const { port, currentLinks, proposedLinks, proposal } = input;
2786
+ const proposedEdges = projectEffectiveEdges(port, proposedLinks, proposal);
2787
+ if (!hasDirectedCycle(proposedEdges)) return { ok: true };
2788
+ const currentEdges = projectEffectiveEdges(port, currentLinks);
2789
+ const corruptBefore = corruptActivityIds(currentEdges);
2790
+ const corruptAfter = corruptActivityIds(proposedEdges);
2791
+ const newlyCyclicActivityIds = [...corruptAfter].filter(
2792
+ (activityId) => !corruptBefore.has(activityId)
2793
+ );
2794
+ if (newlyCyclicActivityIds.length === 0) return { ok: true };
2795
+ return { ok: false, newlyCyclicActivityIds };
2796
+ }
2797
+ function corruptActivityIds(edges) {
2798
+ const corrupt = /* @__PURE__ */ new Set();
2799
+ for (const component of circularComponents(edges)) {
2800
+ for (const activityId of component) corrupt.add(activityId);
2801
+ }
2802
+ return corrupt;
2803
+ }
2804
+
2455
2805
  // src/autoscheduler/links/apply-link-operation.ts
2456
2806
  function applyLinkOperation(op, deps) {
2457
2807
  switch (op.kind) {
2458
2808
  case "create":
2459
2809
  return applyCreate(op, deps);
2460
2810
  case "update":
2461
- return applyUpdate(op, deps);
2811
+ return applyLinkUpdate(op, deps.port);
2462
2812
  case "delete":
2463
2813
  return applyDelete(op, deps);
2464
2814
  }
@@ -2484,6 +2834,10 @@ function applyLinkCreates(links, deps) {
2484
2834
  rejected = { applied: false, rejected: "activity-missing" };
2485
2835
  break;
2486
2836
  }
2837
+ if (isSummaryHierarchyLink(operation.source, operation.target, deps.port)) {
2838
+ rejected = { applied: false, rejected: "hierarchy-link" };
2839
+ break;
2840
+ }
2487
2841
  const key = linkKey(operation);
2488
2842
  const duplicateId = duplicateKeys.get(key);
2489
2843
  if (duplicateId !== void 0) {
@@ -2509,6 +2863,24 @@ function applyLinkCreates(links, deps) {
2509
2863
  return { applied: false, rejected: "cycle" };
2510
2864
  }
2511
2865
  if (rejected) return rejected;
2866
+ const proposedLinks = [
2867
+ ...existingLinks,
2868
+ ...links.map(({ operation, linkId }) => ({
2869
+ id: linkId,
2870
+ source: operation.source,
2871
+ target: operation.target,
2872
+ type: operation.type,
2873
+ lag: operation.lag
2874
+ }))
2875
+ ];
2876
+ const effectiveVerdict = validateTopologyCommit({
2877
+ port: deps.port,
2878
+ currentLinks: existingLinks,
2879
+ proposedLinks
2880
+ });
2881
+ if (!effectiveVerdict.ok) {
2882
+ return { applied: false, rejected: "cycle" };
2883
+ }
2512
2884
  for (const { operation, linkId } of links) {
2513
2885
  const preserved = deps.preserved?.get(linkId);
2514
2886
  deps.port.addLink({
@@ -2537,6 +2909,9 @@ function applyCreate(op, deps) {
2537
2909
  if (!src || !tgt) {
2538
2910
  return { applied: false, rejected: "activity-missing" };
2539
2911
  }
2912
+ if (isSummaryHierarchyLink(op.source, op.target, port)) {
2913
+ return { applied: false, rejected: "hierarchy-link" };
2914
+ }
2540
2915
  const existingLinks = port.getAllLinks();
2541
2916
  for (const l of existingLinks) {
2542
2917
  if (String(l.source) === String(op.source) && String(l.target) === String(op.target) && l.type === op.type) {
@@ -2558,11 +2933,18 @@ function applyCreate(op, deps) {
2558
2933
  type: op.type,
2559
2934
  lag: op.lag
2560
2935
  };
2936
+ const effectiveVerdict = validateTopologyCommit({
2937
+ port,
2938
+ currentLinks: existingLinks,
2939
+ proposedLinks: [...existingLinks, link]
2940
+ });
2941
+ if (!effectiveVerdict.ok) {
2942
+ return { applied: false, rejected: "cycle" };
2943
+ }
2561
2944
  port.addLink(link);
2562
2945
  return { applied: true, linkId: id };
2563
2946
  }
2564
- function applyUpdate(op, deps) {
2565
- const { port } = deps;
2947
+ function applyLinkUpdate(op, port) {
2566
2948
  const link = port.getLink(op.linkId);
2567
2949
  if (!link) return { applied: false, rejected: "link-missing" };
2568
2950
  if (op.lag !== void 0) port.setLinkField(op.linkId, "lag", op.lag);
@@ -2733,6 +3115,10 @@ function diffOutgoingLinks(input) {
2733
3115
  }
2734
3116
 
2735
3117
  // src/autoscheduler/links/lag-units.ts
3118
+ function roundLagDays(lagDays) {
3119
+ const rounded = Math.sign(lagDays) * Math.round(Math.abs(lagDays));
3120
+ return Object.is(rounded, -0) ? 0 : rounded;
3121
+ }
2736
3122
  function lagDaysToHours(lagDays, hoursPerDay) {
2737
3123
  return Math.sign(lagDays) * Math.round(Math.abs(lagDays) * hoursPerDay);
2738
3124
  }
@@ -2995,10 +3381,7 @@ function adjustLinkLagOnTaskMove(targetId, adapter, preEditTarget) {
2995
3381
  calendarApi
2996
3382
  );
2997
3383
  if (newLag === link.lag) continue;
2998
- applyLinkOperation(
2999
- { kind: "update", linkId: link.id, lag: newLag },
3000
- { port: adapter }
3001
- );
3384
+ applyLinkUpdate({ linkId: link.id, lag: newLag }, adapter);
3002
3385
  }
3003
3386
  }
3004
3387
 
@@ -3298,6 +3681,19 @@ function durationToHours(durationDays, hoursPerDay) {
3298
3681
  return Math.round(Math.ceil(durationDays * hoursPerDay * 60) / 60);
3299
3682
  }
3300
3683
 
3684
+ // src/columns/shared/error-codes.ts
3685
+ var PARSE_ERROR = {
3686
+ NOT_A_NUMBER: "not_a_number",
3687
+ // ISSUE-057 (CP6): la unidad de una duración se declara, no se adivina.
3688
+ MISSING_INPUT_UNIT: "missing_input_unit",
3689
+ // D-OWNER-5: una duración en días es entera; una fracción se rechaza sin
3690
+ // redondear (el redondeo silencioso era la puerta del bug de las 320 horas).
3691
+ FRACTIONAL_DAYS: "fractional_days"
3692
+ };
3693
+ var VALIDATION_REASON = {
3694
+ UNCHANGED: "unchanged",
3695
+ NEGATIVE_DURATION: "negative_duration"};
3696
+
3301
3697
  // src/columns/end-date/setter.ts
3302
3698
  var setEndDate = (value) => ({
3303
3699
  endDate: value
@@ -3305,7 +3701,7 @@ var setEndDate = (value) => ({
3305
3701
 
3306
3702
  // src/columns/shared/date-normalize.ts
3307
3703
  function truncateToUtcDay(date2, tz) {
3308
- if (tz) {
3704
+ {
3309
3705
  const parts = new Intl.DateTimeFormat("en-CA", {
3310
3706
  timeZone: tz,
3311
3707
  year: "numeric",
@@ -3315,9 +3711,6 @@ function truncateToUtcDay(date2, tz) {
3315
3711
  const get = (kind) => Number(parts.find((p) => p.type === kind)?.value);
3316
3712
  return new Date(Date.UTC(get("year"), get("month") - 1, get("day")));
3317
3713
  }
3318
- return new Date(
3319
- Date.UTC(date2.getFullYear(), date2.getMonth(), date2.getDate())
3320
- );
3321
3714
  }
3322
3715
  function endOfLocalDay(date2, tz) {
3323
3716
  {
@@ -3327,11 +3720,11 @@ function endOfLocalDay(date2, tz) {
3327
3720
  }
3328
3721
  }
3329
3722
  function nextWorkingDay(calendar, domainDay) {
3330
- let cursor = truncateToUtcDay(domainDay);
3723
+ let cursor = truncateToUtcDay(domainDay, "UTC");
3331
3724
  while (!calendar.isWorkTime(cursor, CALENDAR_UNIT.DAY)) {
3332
3725
  cursor = new Date(cursor);
3333
3726
  cursor.setUTCDate(cursor.getUTCDate() + 1);
3334
- cursor = truncateToUtcDay(cursor);
3727
+ cursor = truncateToUtcDay(cursor, "UTC");
3335
3728
  }
3336
3729
  return cursor;
3337
3730
  }
@@ -3369,11 +3762,12 @@ var DATELESS_CONSTRAINTS = /* @__PURE__ */ new Set([
3369
3762
  ]);
3370
3763
  var IMPLIED_CONSTRAINT_ON_DATE_EDIT = "snet";
3371
3764
  function normalizeConstraintDate(typedDay, calendar, constraintType) {
3765
+ const constraintDay = truncateToUtcDay(typedDay, "UTC");
3372
3766
  if (!START_BASED_CONSTRAINTS2.has(constraintType) && !END_BASED_CONSTRAINTS.has(constraintType)) {
3373
- return typedDay;
3767
+ return constraintDay;
3374
3768
  }
3375
- const reference = START_BASED_CONSTRAINTS2.has(constraintType) ? shiftStart(calendar, typedDay) : shiftEnd(calendar, typedDay);
3376
- const result = new Date(typedDay);
3769
+ const reference = START_BASED_CONSTRAINTS2.has(constraintType) ? shiftStart(calendar, constraintDay) : shiftEnd(calendar, constraintDay);
3770
+ const result = new Date(constraintDay);
3377
3771
  result.setUTCHours(
3378
3772
  reference.getUTCHours(),
3379
3773
  reference.getUTCMinutes(),
@@ -3401,10 +3795,6 @@ var POST_PROCESSOR = {
3401
3795
  RECORD_LAST_START_DATE: "recordLastStartDate"
3402
3796
  };
3403
3797
 
3404
- // src/columns/shared/error-codes.ts
3405
- var VALIDATION_REASON = {
3406
- NEGATIVE_DURATION: "negative_duration"};
3407
-
3408
3798
  // src/columns/shared/constraint-validation.ts
3409
3799
  function checkConstraintViolation2(constraintType, constraintDate, projectedDate) {
3410
3800
  return checkConstraintViolation(constraintType, constraintDate, projectedDate);
@@ -3429,10 +3819,19 @@ var durationPipeline = {
3429
3819
  },
3430
3820
  parseInput(rawValue, _activity, ctx) {
3431
3821
  const numericValue = parseFloat(String(rawValue));
3432
- if (Number.isNaN(numericValue)) return parseError("not_a_number");
3433
- return parsed(
3434
- ctx.inputUnit === "hours" ? numericValue : durationToHours(numericValue, ctx.hoursPerDay)
3435
- );
3822
+ if (!Number.isFinite(numericValue)) {
3823
+ return parseError(PARSE_ERROR.NOT_A_NUMBER);
3824
+ }
3825
+ if (ctx.inputUnit === void 0) {
3826
+ return parseError(PARSE_ERROR.MISSING_INPUT_UNIT);
3827
+ }
3828
+ if (ctx.inputUnit === "hours") {
3829
+ return parsed(numericValue);
3830
+ }
3831
+ if (!Number.isInteger(numericValue)) {
3832
+ return parseError(PARSE_ERROR.FRACTIONAL_DAYS);
3833
+ }
3834
+ return parsed(durationToHours(numericValue, ctx.hoursPerDay));
3436
3835
  },
3437
3836
  validate(_activity, oldValue, newValue) {
3438
3837
  if (isNegativeDuration(newValue)) {
@@ -3881,17 +4280,7 @@ var startDatePipeline = {
3881
4280
  transform(activity, newValue, ctx) {
3882
4281
  const calendar = ctx.calendars.getCalendar(activity.calendarId);
3883
4282
  if (!calendar) {
3884
- return fieldChanges(
3885
- {
3886
- startDate: newValue,
3887
- ...setConstraintTypeImplied(),
3888
- ...setConstraintDate(newValue)
3889
- },
3890
- {
3891
- autoSchedule: true,
3892
- postProcessors: ["adjustLinkLagOnTaskMove"]
3893
- }
3894
- );
4283
+ throw new Error("startDatePipeline: no calendar resolved (integrity)");
3895
4284
  }
3896
4285
  const finalStart = shiftStart(calendar, newValue);
3897
4286
  const milestoneActivity = activity.type === "milestone";
@@ -3921,7 +4310,7 @@ function checkParentStartConstraint(activity, newDate, ctx) {
3921
4310
  activity.id
3922
4311
  );
3923
4312
  if (!parentStartDate) return null;
3924
- const parentDay = truncateToUtcDay(parentStartDate);
4313
+ const parentDay = truncateToUtcDay(parentStartDate, "UTC");
3925
4314
  if (newDate.getTime() < parentDay.getTime()) {
3926
4315
  return invalid("before_parent_start");
3927
4316
  }
@@ -3969,7 +4358,6 @@ var endDatePipeline = {
3969
4358
  const calendar = ctx.calendars.getCalendar(activity.calendarId);
3970
4359
  if (!calendar) return parsed(day);
3971
4360
  if (!calendar.isWorkTime(day, CALENDAR_UNIT.DAY)) {
3972
- if (ctx.dateFormat.split(" ").length > 1) return parsed(day);
3973
4361
  return parsed(
3974
4362
  calendar.getClosestWorkTime({
3975
4363
  direction: "past",
@@ -4152,7 +4540,12 @@ var constraintDatePipeline = {
4152
4540
  transform(activity, newValue, ctx) {
4153
4541
  const finalType = isUnconstrained(activity) ? IMPLIED_CONSTRAINT_ON_DATE_EDIT : effectiveConstraintType(activity);
4154
4542
  const calendar = ctx.calendars.getCalendar(activity.calendarId);
4155
- const storedDate = calendar ? normalizeConstraintDate(newValue, calendar, finalType) : newValue;
4543
+ if (!calendar) {
4544
+ throw new Error(
4545
+ "constraintDatePipeline: no calendar resolved (integrity)"
4546
+ );
4547
+ }
4548
+ const storedDate = normalizeConstraintDate(newValue, calendar, finalType);
4156
4549
  const patch = buildConstraintDateExtraFields(activity, storedDate);
4157
4550
  return fieldChanges(patch, {
4158
4551
  autoSchedule: true
@@ -4452,6 +4845,29 @@ var COLUMN_WRITABLE_FIELDS = new Set(
4452
4845
  COLUMN_WRITABLE_FIELD_LIST
4453
4846
  );
4454
4847
 
4848
+ // src/constants/activity-properties.ts
4849
+ var ACTIVITY_PROPERTY = {
4850
+ PARENT: "parentId",
4851
+ TYPE: "type",
4852
+ START_DATE: "startDate",
4853
+ END_DATE: "endDate",
4854
+ CONSTRAINT_TYPE: "constraintType",
4855
+ CONSTRAINT_DATE: "constraintDate",
4856
+ PROGRESS: "progress",
4857
+ COST: "cost",
4858
+ USED_COST: "usedCost",
4859
+ REAL_COST: "realCost",
4860
+ WORK_HOURS: "workHours",
4861
+ REAL_WORK_HOURS: "realWorkHours",
4862
+ CORRELATIVE_ID: "correlativeId",
4863
+ CUSTOM_ID: "customId",
4864
+ NEW_ACTIVITIES_ARRAY: "newActivityIds",
4865
+ HAS_NEW_ACTIVITIES: "hasNewActivities",
4866
+ CALENDAR_DURATION: "calendarDuration"};
4867
+ var LINK_PROPERTY = {
4868
+ TYPE: "type",
4869
+ LAG: "lag"};
4870
+
4455
4871
  // src/internal/state/dynamic-writes.ts
4456
4872
  function setActivityFieldDynamic(state, activityId, field, value) {
4457
4873
  state.setActivityFieldDynamic(activityId, field, value);
@@ -4503,11 +4919,6 @@ function cloneCriticalPath(value) {
4503
4919
  };
4504
4920
  }
4505
4921
 
4506
- // src/shared/clone-domain-value.ts
4507
- function cloneDomainValue(value) {
4508
- return structuredClone(value);
4509
- }
4510
-
4511
4922
  // src/dispatch/shared/snapshots.ts
4512
4923
  var YIELD_EVERY_N_ENTRIES = 200;
4513
4924
  function collectTouchedIds(primary, changes) {
@@ -4619,6 +5030,7 @@ function diffActivity(before, after) {
4619
5030
  const beforeRec = before ?? {};
4620
5031
  const afterRec = after;
4621
5032
  const keys = /* @__PURE__ */ new Set([...Object.keys(beforeRec), ...Object.keys(afterRec)]);
5033
+ keys.delete(ACTIVITY_PROPERTY.CALENDAR_DURATION);
4622
5034
  for (const k of keys) {
4623
5035
  if (Object.hasOwn(beforeRec, k) !== Object.hasOwn(afterRec, k) || !fieldValueEqual(beforeRec[k], afterRec[k])) {
4624
5036
  fields[k] = {
@@ -9293,28 +9705,6 @@ async function applyCriticalPath(state, hoursPerDay, isCurrent = () => true) {
9293
9705
  return true;
9294
9706
  }
9295
9707
 
9296
- // src/constants/activity-properties.ts
9297
- var ACTIVITY_PROPERTY = {
9298
- PARENT: "parentId",
9299
- TYPE: "type",
9300
- START_DATE: "startDate",
9301
- END_DATE: "endDate",
9302
- CONSTRAINT_TYPE: "constraintType",
9303
- CONSTRAINT_DATE: "constraintDate",
9304
- PROGRESS: "progress",
9305
- COST: "cost",
9306
- USED_COST: "usedCost",
9307
- REAL_COST: "realCost",
9308
- WORK_HOURS: "workHours",
9309
- REAL_WORK_HOURS: "realWorkHours",
9310
- CORRELATIVE_ID: "correlativeId",
9311
- CUSTOM_ID: "customId",
9312
- NEW_ACTIVITIES_ARRAY: "newActivityIds",
9313
- HAS_NEW_ACTIVITIES: "hasNewActivities"};
9314
- var LINK_PROPERTY = {
9315
- TYPE: "type",
9316
- LAG: "lag"};
9317
-
9318
9708
  // src/propagations/upward/real-cost.ts
9319
9709
  function emitRealCost(state) {
9320
9710
  const rootIds = [];
@@ -9367,6 +9757,8 @@ var NON_SCHEDULING_INLINE_COLUMNS = /* @__PURE__ */ new Set([
9367
9757
  COLUMN.TAGS
9368
9758
  ]);
9369
9759
  var NON_SCHEDULING_KINDS = /* @__PURE__ */ new Set([
9760
+ "activity-clipboard-clear",
9761
+ "activity-copy",
9370
9762
  "selection-toggle",
9371
9763
  "selection-replace",
9372
9764
  "visibility-set",
@@ -9380,6 +9772,8 @@ var NON_SCHEDULING_KINDS = /* @__PURE__ */ new Set([
9380
9772
  "status-criteria-set"
9381
9773
  ]);
9382
9774
  var PURE_VIEW_STATE_KINDS = /* @__PURE__ */ new Set([
9775
+ "activity-clipboard-clear",
9776
+ "activity-copy",
9383
9777
  "selection-toggle",
9384
9778
  "selection-replace",
9385
9779
  "visibility-set",
@@ -9705,7 +10099,8 @@ async function assembleChangeSet(adapter, args) {
9705
10099
  const insertedIds = collectInsertedIds(captured);
9706
10100
  mergeDirtyBeforeImages(adapter, captured, insertedIds, before, allTouched);
9707
10101
  }
9708
- const beforeDiffEffects = args.sirDetection === "before-diff" ? detectSirAutoReject(before, adapter, allTouched) : [];
10102
+ const sirDetection = args.preview === true ? void 0 : args.sirDetection;
10103
+ const beforeDiffEffects = sirDetection === "before-diff" ? detectSirAutoReject(before, adapter, allTouched) : [];
9709
10104
  const correlativeBefore = buildCorrelativeBeforeMap(
9710
10105
  args.correlativeShifts,
9711
10106
  allTouched
@@ -9716,7 +10111,7 @@ async function assembleChangeSet(adapter, args) {
9716
10111
  allTouched,
9717
10112
  correlativeBefore
9718
10113
  );
9719
- const effects = args.sirDetection === "after-diff" ? detectSirAutoReject(before, adapter, allTouched) : beforeDiffEffects;
10114
+ const effects = sirDetection === "after-diff" ? detectSirAutoReject(before, adapter, allTouched) : beforeDiffEffects;
9720
10115
  const warnings = args.warnings ?? [];
9721
10116
  return {
9722
10117
  source: args.source,
@@ -10045,6 +10440,7 @@ async function dispatchInlineEdit(action, options, deps) {
10045
10440
  touchedIds,
10046
10441
  scheduledIds,
10047
10442
  sirDetection: "before-diff",
10443
+ preview: options.preview,
10048
10444
  links: linkChanges,
10049
10445
  trackingEvents,
10050
10446
  ...constraintWarning ? { warnings: [constraintWarning] } : {},
@@ -10251,7 +10647,7 @@ async function dispatchInlineEditLinks(action, options, deps) {
10251
10647
  const { specs: specsInDays } = parsePredecessorString(action.newValue);
10252
10648
  const specs = specsInDays.map((spec) => ({
10253
10649
  ...spec,
10254
- lag: lagDaysToHours(spec.lag, sector.hoursPerDay)
10650
+ lag: lagDaysToHours(roundLagDays(spec.lag), sector.hoursPerDay)
10255
10651
  }));
10256
10652
  const byCorrelative = /* @__PURE__ */ new Map();
10257
10653
  adapter.forEachActivity((a) => {
@@ -10588,6 +10984,11 @@ function buildNewActivity(input, adapter) {
10588
10984
  ...base,
10589
10985
  startDate,
10590
10986
  endDate,
10987
+ calendarDuration: computeCalendarDuration({
10988
+ type: base.type,
10989
+ startDate,
10990
+ endDate
10991
+ }),
10591
10992
  newActivityIds: [...base.newActivityIds],
10592
10993
  pendingRequestIds: [...base.pendingRequestIds],
10593
10994
  responsableIds: [...base.responsableIds],
@@ -11213,6 +11614,7 @@ async function dispatchActivityDelete(action, options, deps) {
11213
11614
  touchedIds,
11214
11615
  scheduledIds,
11215
11616
  sirDetection: "after-diff",
11617
+ preview: options.preview,
11216
11618
  links: buildLinkDeletions(beforeLinks),
11217
11619
  trackingEvents: [trackingEvent],
11218
11620
  hoursPerDay: sector.hoursPerDay,
@@ -11373,6 +11775,7 @@ async function dispatchActivityBatch(action, options, deps) {
11373
11775
  links: buildLinkChangesForBatch(appliedLinks, beforeLinks, deps.adapter),
11374
11776
  hoursPerDay: deps.sector.hoursPerDay,
11375
11777
  correlativeShifts: existingCorrelativeShifts,
11778
+ preview: options.preview,
11376
11779
  ...action.mode === "replace" ? { sirDetection: "after-diff" } : {}
11377
11780
  });
11378
11781
  return {
@@ -11670,184 +12073,331 @@ function stringKeyedLinks(links) {
11670
12073
  return new Map(Array.from(links, ([linkId, link]) => [String(linkId), link]));
11671
12074
  }
11672
12075
 
11673
- // src/dispatch/handlers/activity-paste.ts
11674
- function resolvePasteRootDestination(destination, adapter) {
11675
- if ("afterSiblingId" in destination) {
11676
- const sibling = adapter.getActivity(destination.afterSiblingId);
11677
- if (!sibling) {
11678
- return { ok: false, reason: REJECTION_REASON.ANCHOR_SIBLING_NOT_FOUND };
11679
- }
11680
- const parentId2 = sibling.parentId == null ? ROOT_PARENT_ID : String(sibling.parentId);
11681
- return { ok: true, parentId: parentId2, firstAfter: destination.afterSiblingId };
12076
+ // src/dispatch/handlers/sir-sync.ts
12077
+ function dispatchSirSync(action, deps) {
12078
+ const { adapter } = deps;
12079
+ const id = action.activityId;
12080
+ const snapshot = adapter.getActivity(id);
12081
+ if (!snapshot) {
12082
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
11682
12083
  }
11683
- const parentId = destination.parentId;
11684
- const siblings = collectSiblings(adapter, parentId).sort(
11685
- (a, b) => readCorrelativeId(a) - readCorrelativeId(b)
11686
- );
11687
- const before = siblings[destination.index]?.id;
11688
- return { ok: true, parentId, firstBefore: before };
11689
- }
11690
- function applyPasteDomainSemantics(overrides) {
11691
- const { customId: _customId, ...content } = overrides;
12084
+ const before = snapshot.pendingRequestIds ?? [];
12085
+ const after = action.pendingRequests.map((r) => r.id);
12086
+ adapter.setActivityField(action.activityId, "pendingRequestIds", after);
12087
+ const updated = adapter.getActivity(id);
12088
+ if (!updated) {
12089
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
12090
+ }
12091
+ const change = {
12092
+ id,
12093
+ kind: "updated",
12094
+ fields: { pendingRequestIds: { before, after } },
12095
+ // ISSUE-025: `after` must be the COMPLETE post-change entity; a partial
12096
+ // object crashes any consumer that projects the full snapshot.
12097
+ after: structuredCloneActivity(updated)
12098
+ };
11692
12099
  return {
11693
- ...content,
11694
- progress: 0,
11695
- ponderator: 0,
11696
- usedCost: 0,
11697
- workHours: 0,
11698
- isLookahead: false
12100
+ ok: true,
12101
+ changes: {
12102
+ source: action,
12103
+ activities: [change],
12104
+ links: [],
12105
+ calendars: [],
12106
+ trackingEvents: []
12107
+ }
11699
12108
  };
11700
12109
  }
11701
- async function dispatchActivityPaste(action, options, deps) {
12110
+
12111
+ // src/dispatch/handlers/bulk-edit.ts
12112
+ var CUSTOM_ID_COLUMN = COLUMN.CUSTOM_ID;
12113
+ async function dispatchBulkEdit(action, options, deps) {
11702
12114
  const { adapter, scheduler, sector } = deps;
11703
- const rootDest = resolvePasteRootDestination(action.destination, adapter);
11704
- if (!rootDest.ok) return rootDest;
11705
- const beforeSnap = await snapshotActivities(adapter, /* @__PURE__ */ new Set());
11706
- const touched = /* @__PURE__ */ new Set();
11707
- const originalToNew = /* @__PURE__ */ new Map();
11708
- const createdIds = [];
11709
- let prevRootId;
11710
- for (const input of action.activities) {
11711
- const mappedParent = originalToNew.get(input.originalParentId);
11712
- let parentId;
11713
- let afterSiblingId;
11714
- let beforeSiblingId;
11715
- if (mappedParent !== void 0) {
11716
- parentId = mappedParent;
11717
- } else {
11718
- parentId = rootDest.parentId;
11719
- if (prevRootId === void 0) {
11720
- afterSiblingId = rootDest.firstAfter;
11721
- beforeSiblingId = rootDest.firstBefore;
11722
- } else {
11723
- afterSiblingId = prevRootId;
11724
- }
11725
- }
11726
- const core = await createActivityCore(
11727
- {
11728
- parentId,
11729
- afterSiblingId,
11730
- beforeSiblingId,
11731
- ...input.activityId ? { activityId: input.activityId } : {},
11732
- overrides: applyPasteDomainSemantics(input.overrides),
11733
- eventSource: action.eventSource
11734
- },
11735
- deps,
11736
- {
11737
- skipCorrelativeRecompute: true,
11738
- skipBeforeSnap: true,
11739
- customIdReferenceId: action.referenceActivityId
11740
- }
12115
+ const editsIncludeDuration = action.edits.some(
12116
+ (edit) => edit.column === COLUMN.DURATION
12117
+ );
12118
+ if (editsIncludeDuration) {
12119
+ scheduler.invalidateExpandedLinksCache();
12120
+ }
12121
+ const verdicts = [];
12122
+ const beforeSnap = /* @__PURE__ */ new Map();
12123
+ const constraintPriorsByActivity = /* @__PURE__ */ new Map();
12124
+ const explicitDateEditActivities = /* @__PURE__ */ new Set();
12125
+ const touchedIds = /* @__PURE__ */ new Set();
12126
+ const trackingEvents = [];
12127
+ const linkChanges = [];
12128
+ const warnings = [];
12129
+ let needsAutoSchedule = false;
12130
+ for (const edit of action.edits) {
12131
+ const outcome = applyOneEdit(edit, deps);
12132
+ verdicts.push(outcome.verdict);
12133
+ if (!outcome.verdict.ok || !outcome.changes) continue;
12134
+ const constraintWarning = toConstraintWarning(
12135
+ edit.activityId,
12136
+ outcome.changes.constraintWarning
11741
12137
  );
11742
- if (!core.ok) return core;
11743
- if (input.baselineSnapshot !== void 0) {
11744
- const snapshot = input.baselineSnapshot;
11745
- adapter.setActivityField(
11746
- core.newId,
11747
- "baselineSnapshot",
11748
- snapshot ? {
11749
- ...snapshot,
11750
- startDate: snapshot.startDate ? new Date(snapshot.startDate) : null,
11751
- endDate: snapshot.endDate ? new Date(snapshot.endDate) : null
11752
- } : null
11753
- );
12138
+ if (constraintWarning) warnings.push(constraintWarning);
12139
+ const editTouched = collectTouchedIds(edit.activityId, outcome.changes);
12140
+ mergeBeforeSnapshots(beforeSnap, editTouched, deps);
12141
+ for (const touchedId of editTouched) touchedIds.add(touchedId);
12142
+ recordConstraintPrior(constraintPriorsByActivity, edit, beforeSnap);
12143
+ if (isExplicitConstraintDateEdit(edit.column)) {
12144
+ explicitDateEditActivities.add(String(edit.activityId));
12145
+ }
12146
+ const beforeLinkLags = snapshotIncomingLinkLags(adapter, edit.activityId);
12147
+ const preEditSnapshot = snapshotSingleActivity(
12148
+ adapter,
12149
+ String(edit.activityId)
12150
+ );
12151
+ const customIdBeforeApply = readLiveCustomId(edit, deps);
12152
+ const changes = outcome.changes;
12153
+ applyFieldChanges(adapter, edit.activityId, changes);
12154
+ runPostProcessorsOnAdapter(
12155
+ edit.activityId,
12156
+ outcome.changes.postProcessors ?? [],
12157
+ adapter,
12158
+ preEditSnapshot
12159
+ );
12160
+ syncCustomIdTracker(edit, customIdBeforeApply, deps);
12161
+ linkChanges.push(
12162
+ ...diffIncomingLinkLagChanges(beforeLinkLags, adapter, edit.activityId)
12163
+ );
12164
+ needsAutoSchedule ||= Boolean(outcome.changes.needsAutoSchedule);
12165
+ for (const trackingEvent of outcome.changes.trackingEvents ?? []) {
12166
+ trackingEvents.push({
12167
+ name: trackingEvent.name,
12168
+ properties: trackingEvent.properties ?? {}
12169
+ });
11754
12170
  }
11755
- originalToNew.set(input.originalId, core.newId);
11756
- createdIds.push(core.newId);
11757
- if (mappedParent === void 0) prevRootId = core.newId;
11758
- }
11759
- const correlativeShifts = recomputeCorrelativeIds(adapter);
11760
- const createdIdSet = new Set(createdIds.map(String));
11761
- foldCorrelativeShifts(
11762
- adapter,
11763
- correlativeShifts,
11764
- createdIdSet,
11765
- beforeSnap,
11766
- touched
11767
- );
11768
- for (const createdId of createdIds) touched.add(String(createdId));
11769
- const appliedLinks = [];
11770
- for (const link of action.links) {
11771
- const newSource = originalToNew.get(link.source);
11772
- const newTarget = originalToNew.get(link.target);
11773
- if (newSource === void 0 || newTarget === void 0) continue;
11774
- const op = {
11775
- kind: "create",
11776
- source: newSource,
11777
- target: newTarget,
11778
- type: link.type,
11779
- lag: lagDaysToHours(link.lag, sector.hoursPerDay)
11780
- };
11781
- const res = applyLinkOperation(op, {
11782
- port: adapter,
11783
- newLinkId: link.linkId ? () => link.linkId : deps.linkIdGen.next
11784
- });
11785
- appliedLinks.push({
11786
- op,
11787
- finalLinkId: res.applied ? res.linkId : null,
11788
- rejected: res.applied ? null : res.rejected ?? "link_op_rejected"
11789
- });
11790
12171
  }
11791
- const { scheduledIds } = await runPostMutation(
12172
+ const postMutationArgs = {
12173
+ action,
12174
+ autoscheduleFrom: needsAutoSchedule ? "roots" : null,
12175
+ recomputeParentsFrom: touchedIds,
12176
+ now: deps.now,
12177
+ options
12178
+ };
12179
+ const batchHasConstraintEdits = constraintPriorsByActivity.size > 0;
12180
+ const { scheduledIds } = await (batchHasConstraintEdits ? settleConstraintEdits(
11792
12181
  {
11793
12182
  adapter,
11794
12183
  scheduler,
11795
- sector: deps.sector,
12184
+ sector,
11796
12185
  defaultBaseCalendarId: deps.defaultBaseCalendarId
11797
12186
  },
11798
12187
  {
11799
- action,
11800
- autoscheduleFrom: createdIds[0] ?? null,
11801
- recomputeParentsFrom: createdIds,
11802
- now: deps.now,
11803
- options
12188
+ postMutationArgs,
12189
+ revertTargets: [...constraintPriorsByActivity].filter(
12190
+ ([activityId]) => !explicitDateEditActivities.has(activityId)
12191
+ ).map(([activityId, priorConstraintDate]) => ({
12192
+ activityId,
12193
+ priorConstraintDate
12194
+ })),
12195
+ revert: (activityId, priorConstraintDate) => revertNoOpConstraintEdit(
12196
+ activityId,
12197
+ priorConstraintDate,
12198
+ adapter,
12199
+ deps.ctx.calendars
12200
+ )
11804
12201
  }
11805
- );
11806
- const linkChanges = buildLinkChangesForBatch(
11807
- appliedLinks,
11808
- /* @__PURE__ */ new Map(),
11809
- adapter
11810
- );
12202
+ ) : runPostMutation(
12203
+ {
12204
+ adapter,
12205
+ scheduler,
12206
+ sector,
12207
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
12208
+ },
12209
+ postMutationArgs
12210
+ ));
11811
12211
  return {
11812
12212
  ok: true,
12213
+ verdicts,
11813
12214
  changes: await assembleChangeSet(adapter, {
11814
12215
  source: action,
11815
12216
  beforeSnap,
11816
- touchedIds: touched,
12217
+ touchedIds,
11817
12218
  scheduledIds,
12219
+ sirDetection: "before-diff",
12220
+ preview: options.preview,
11818
12221
  links: linkChanges,
12222
+ trackingEvents,
12223
+ warnings,
11819
12224
  hoursPerDay: sector.hoursPerDay
11820
12225
  })
11821
12226
  };
11822
12227
  }
12228
+ function applyOneEdit(edit, deps) {
12229
+ const { ctx } = deps;
12230
+ const activityId = edit.activityId;
12231
+ const pipeline = findPipelineForColumn(edit.column);
12232
+ if (!pipeline) {
12233
+ return {
12234
+ verdict: {
12235
+ activityId,
12236
+ ok: false,
12237
+ reason: REJECTION_REASON.NO_PIPELINE_FOR_COLUMN
12238
+ },
12239
+ changes: null
12240
+ };
12241
+ }
12242
+ const activitySnap = ctx.activityReader.getActivity(edit.activityId);
12243
+ if (!activitySnap) {
12244
+ return {
12245
+ verdict: {
12246
+ activityId,
12247
+ ok: false,
12248
+ reason: REJECTION_REASON.ACTIVITY_NOT_FOUND
12249
+ },
12250
+ changes: null
12251
+ };
12252
+ }
12253
+ const editGate = pipeline.canEdit(activitySnap, ctx.hierarchy);
12254
+ if (!editGate.allowed) {
12255
+ return {
12256
+ verdict: {
12257
+ activityId,
12258
+ ok: false,
12259
+ reason: editGate.reason ?? REJECTION_REASON.CANNOT_EDIT,
12260
+ ...editGate.alertKey ? { alertKey: editGate.alertKey } : {}
12261
+ },
12262
+ changes: null
12263
+ };
12264
+ }
12265
+ const parsed2 = pipeline.parseInput(edit.newValue, activitySnap, ctx);
12266
+ if (!parsed2.ok) {
12267
+ return {
12268
+ verdict: {
12269
+ activityId,
12270
+ ok: false,
12271
+ reason: parsed2.reason ?? REJECTION_REASON.PARSE_ERROR
12272
+ },
12273
+ changes: null
12274
+ };
12275
+ }
12276
+ const oldValue = Reflect.get(activitySnap, pipeline.targetField);
12277
+ const validation = pipeline.validate(
12278
+ activitySnap,
12279
+ oldValue,
12280
+ parsed2.value,
12281
+ ctx
12282
+ );
12283
+ if (!validation.valid) {
12284
+ return {
12285
+ verdict: {
12286
+ activityId,
12287
+ ok: false,
12288
+ reason: validation.reason ?? REJECTION_REASON.INVALID,
12289
+ ...validation.notification?.messageKey ? { alertKey: validation.notification.messageKey } : {}
12290
+ },
12291
+ changes: null
12292
+ };
12293
+ }
12294
+ if (edit.column === CUSTOM_ID_COLUMN && parsed2.value !== null) {
12295
+ const liveActivity = deps.adapter.getActivity(edit.activityId);
12296
+ const oldCustomIdRaw = liveActivity ? liveActivity.customId : null;
12297
+ const oldCustomId = typeof oldCustomIdRaw === "string" ? oldCustomIdRaw : null;
12298
+ if (deps.customIdTracker.isCustomIdInUse(String(parsed2.value), oldCustomId)) {
12299
+ return {
12300
+ verdict: {
12301
+ activityId,
12302
+ ok: false,
12303
+ reason: REJECTION_REASON.CUSTOM_ID_DUPLICATE
12304
+ },
12305
+ changes: null
12306
+ };
12307
+ }
12308
+ }
12309
+ const changes = pipeline.transform(
12310
+ activitySnap,
12311
+ parsed2.value,
12312
+ ctx,
12313
+ parsed2.raw
12314
+ );
12315
+ return { verdict: { activityId, ok: true }, changes };
12316
+ }
12317
+ function readLiveCustomId(edit, deps) {
12318
+ if (edit.column !== CUSTOM_ID_COLUMN) return null;
12319
+ const liveActivity = deps.adapter.getActivity(edit.activityId);
12320
+ const rawValue = liveActivity ? liveActivity.customId : null;
12321
+ return typeof rawValue === "string" ? rawValue : null;
12322
+ }
12323
+ function syncCustomIdTracker(edit, oldCustomId, deps) {
12324
+ if (edit.column !== CUSTOM_ID_COLUMN) return;
12325
+ const liveActivity = deps.adapter.getActivity(edit.activityId);
12326
+ const rawValue = liveActivity ? liveActivity.customId : null;
12327
+ const newCustomId = typeof rawValue === "string" ? rawValue : null;
12328
+ if (oldCustomId !== newCustomId) {
12329
+ deps.customIdTracker.trackCustomIdChange(oldCustomId, newCustomId);
12330
+ }
12331
+ }
12332
+ function recordConstraintPrior(priors, edit, beforeSnap) {
12333
+ if (!isConstraintEditColumn(edit.column)) return;
12334
+ const activityKey = String(edit.activityId);
12335
+ if (priors.has(activityKey)) return;
12336
+ const preBatchSnapshot = beforeSnap.get(activityKey);
12337
+ if (!preBatchSnapshot) return;
12338
+ priors.set(activityKey, preBatchSnapshot.constraintDate ?? null);
12339
+ }
12340
+ function mergeBeforeSnapshots(beforeSnap, ids, deps) {
12341
+ const missing = /* @__PURE__ */ new Set();
12342
+ for (const candidateId of ids) {
12343
+ if (!beforeSnap.has(candidateId)) missing.add(candidateId);
12344
+ }
12345
+ if (missing.size === 0) return;
12346
+ for (const missingId of missing) {
12347
+ const liveActivity = deps.adapter.getActivity(missingId);
12348
+ if (liveActivity) {
12349
+ beforeSnap.set(missingId, structuredCloneActivity(liveActivity));
12350
+ }
12351
+ }
12352
+ }
11823
12353
 
11824
- // src/dispatch/handlers/sir-sync.ts
11825
- function dispatchSirSync(action, deps) {
12354
+ // src/dispatch/handlers/sir-approve.ts
12355
+ async function dispatchSirApprove(action, options, deps) {
11826
12356
  const { adapter } = deps;
11827
- const id = action.activityId;
11828
- const snapshot = adapter.getActivity(id);
12357
+ const snapshot = adapter.getActivity(action.activityId);
11829
12358
  if (!snapshot) {
11830
12359
  return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
11831
12360
  }
11832
- const before = snapshot.pendingRequestIds ?? [];
11833
- const after = action.pendingRequests.map((r) => r.id);
11834
- adapter.setActivityField(action.activityId, "pendingRequestIds", after);
11835
- const change = {
11836
- id,
11837
- kind: "updated",
11838
- fields: { pendingRequestIds: { before, after } },
11839
- after: { id, pendingRequestIds: after }
11840
- };
11841
- return {
11842
- ok: true,
11843
- changes: {
11844
- source: action,
11845
- activities: [change],
11846
- links: [],
11847
- calendars: [],
11848
- trackingEvents: []
11849
- }
11850
- };
12361
+ const pending = snapshot.pendingRequestIds ?? [];
12362
+ if (!pending.includes(action.approvedRequestId)) {
12363
+ return { ok: false, reason: REJECTION_REASON.SIR_REQUEST_NOT_PENDING };
12364
+ }
12365
+ const remaining = pending.filter((id) => id !== action.approvedRequestId);
12366
+ adapter.setActivityField(action.activityId, "pendingRequestIds", remaining);
12367
+ const result = await dispatchBulkEdit(
12368
+ {
12369
+ kind: "bulk-edit",
12370
+ edits: [
12371
+ {
12372
+ activityId: action.activityId,
12373
+ column: COLUMN.START_DATE,
12374
+ newValue: action.startDate
12375
+ },
12376
+ {
12377
+ activityId: action.activityId,
12378
+ column: COLUMN.END_DATE,
12379
+ newValue: action.endDate
12380
+ },
12381
+ // asap ÚLTIMO: gana al SNET implícito (paridad legacy).
12382
+ {
12383
+ activityId: action.activityId,
12384
+ column: COLUMN.CONSTRAINT_TYPE,
12385
+ newValue: "asap"
12386
+ }
12387
+ ],
12388
+ eventSource: action.eventSource
12389
+ },
12390
+ options,
12391
+ deps
12392
+ );
12393
+ if (!result.ok) return result;
12394
+ const hardFailure = "verdicts" in result ? result.verdicts.find(
12395
+ (verdict) => !verdict.ok && verdict.reason !== VALIDATION_REASON.UNCHANGED
12396
+ ) : void 0;
12397
+ if (hardFailure) {
12398
+ return { ok: false, reason: hardFailure.reason };
12399
+ }
12400
+ return { ...result, changes: { ...result.changes, source: action } };
11851
12401
  }
11852
12402
 
11853
12403
  // src/dispatch/handlers/activity-lookahead-sync.ts
@@ -11933,7 +12483,38 @@ function isAncestor(descendantId, candidateAncestorId, adapter) {
11933
12483
  if (parentKey === candidateAncestorId) return true;
11934
12484
  cur = parentKey;
11935
12485
  }
11936
- return false;
12486
+ return false;
12487
+ }
12488
+
12489
+ // src/dispatch/shared/hierarchy-cycle-guard.ts
12490
+ function reparentClosesEffectiveCycle(adapter, reparented) {
12491
+ const links = adapter.getAllLinks();
12492
+ const verdict = validateTopologyCommit({
12493
+ port: adapter,
12494
+ currentLinks: links,
12495
+ proposedLinks: links,
12496
+ proposal: { reparented }
12497
+ });
12498
+ return !verdict.ok;
12499
+ }
12500
+ function collectCyclicReparents(planned, adapter) {
12501
+ const cyclic = /* @__PURE__ */ new Set();
12502
+ if (planned.length === 0) return cyclic;
12503
+ const combined = new Map(
12504
+ planned.map((reparent) => [reparent.activityId, reparent.newParentId])
12505
+ );
12506
+ if (!reparentClosesEffectiveCycle(adapter, combined)) return cyclic;
12507
+ const accepted = /* @__PURE__ */ new Map();
12508
+ for (const reparent of planned) {
12509
+ const trial = new Map(accepted);
12510
+ trial.set(reparent.activityId, reparent.newParentId);
12511
+ if (reparentClosesEffectiveCycle(adapter, trial)) {
12512
+ cyclic.add(reparent.activityId);
12513
+ continue;
12514
+ }
12515
+ accepted.set(reparent.activityId, reparent.newParentId);
12516
+ }
12517
+ return cyclic;
11937
12518
  }
11938
12519
 
11939
12520
  // src/dispatch/handlers/activity-move.ts
@@ -11977,6 +12558,17 @@ async function dispatchActivityMove(action, options, deps) {
11977
12558
  const oldParentKey = normalizeParentKey(oldParent);
11978
12559
  const newParentKey = String(action.parentId);
11979
12560
  const parentChanged = oldParentKey !== newParentKey;
12561
+ if (parentChanged) {
12562
+ const reparented = /* @__PURE__ */ new Map([
12563
+ [
12564
+ action.activityId,
12565
+ newParentIsRoot ? null : action.parentId
12566
+ ]
12567
+ ]);
12568
+ if (reparentClosesEffectiveCycle(adapter, reparented)) {
12569
+ return { ok: false, reason: REJECTION_REASON.HIERARCHY_CYCLE };
12570
+ }
12571
+ }
11980
12572
  const touched = /* @__PURE__ */ new Set([action.activityId]);
11981
12573
  if (parentChanged) {
11982
12574
  if (oldParentKey !== ROOT_PARENT_ID) touched.add(oldParentKey);
@@ -12128,27 +12720,21 @@ async function dispatchActivityMove(action, options, deps) {
12128
12720
  touchedIds: touched,
12129
12721
  scheduledIds,
12130
12722
  sirDetection: "before-diff",
12723
+ preview: options.preview,
12131
12724
  trackingEvents: [trackingEvent],
12132
12725
  hoursPerDay: sector.hoursPerDay
12133
12726
  })
12134
12727
  };
12135
12728
  }
12136
12729
 
12137
- // src/dispatch/handlers/activity-indent.ts
12138
- async function dispatchActivityIndent(action, options, deps) {
12139
- const { adapter, scheduler, sector } = deps;
12140
- scheduler.invalidateAllCaches();
12141
- if (action.activityIds.length === 0) {
12142
- return { ok: false, reason: REJECTION_REASON.ACTIVITY_IDS_EMPTY };
12143
- }
12144
- const selectedSet = new Set(action.activityIds);
12145
- const touched = /* @__PURE__ */ new Set();
12730
+ // src/dispatch/shared/indent-planning.ts
12731
+ function planIndentMoves(activityIds, adapter) {
12732
+ const selectedSet = new Set(activityIds);
12146
12733
  const failed = [];
12147
- const succeededIds = [];
12148
12734
  const moves = [];
12149
12735
  const plannedIds = /* @__PURE__ */ new Set();
12150
12736
  const blockingSet = new Set(selectedSet);
12151
- const sortedIds = [...action.activityIds].sort((a, b) => {
12737
+ const sortedIds = [...activityIds].sort((a, b) => {
12152
12738
  const aSnap = adapter.getActivity(a);
12153
12739
  const bSnap = adapter.getActivity(b);
12154
12740
  const ac = aSnap ? readCorrelativeId(aSnap) : NOT_SET_SORT_VALUE2;
@@ -12183,12 +12769,43 @@ async function dispatchActivityIndent(action, options, deps) {
12183
12769
  const oldParentId = isRootParent(oldParentRaw) ? ROOT_PARENT_ID : String(oldParentRaw);
12184
12770
  moves.push({ activityId: id, newParentId: targetParentId, oldParentId });
12185
12771
  plannedIds.add(id);
12186
- touched.add(id);
12187
- touched.add(targetParentId);
12188
- if (oldParentId !== ROOT_PARENT_ID) touched.add(oldParentId);
12189
12772
  }
12773
+ const cyclicMoveIds = collectCyclicReparents(moves, adapter);
12774
+ for (const cyclicId of cyclicMoveIds) {
12775
+ failed.push({
12776
+ activityId: cyclicId,
12777
+ reason: REJECTION_REASON.HIERARCHY_CYCLE
12778
+ });
12779
+ }
12780
+ const allowedMoves = moves.filter(
12781
+ (move) => !cyclicMoveIds.has(move.activityId)
12782
+ );
12783
+ return { moves, cyclicMoveIds, allowedMoves, failed };
12784
+ }
12785
+
12786
+ // src/dispatch/handlers/activity-indent.ts
12787
+ async function dispatchActivityIndent(action, options, deps) {
12788
+ const { adapter, scheduler, sector } = deps;
12789
+ scheduler.invalidateAllCaches();
12790
+ if (action.activityIds.length === 0) {
12791
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_IDS_EMPTY };
12792
+ }
12793
+ const touched = /* @__PURE__ */ new Set();
12794
+ const succeededIds = [];
12795
+ const plan = planIndentMoves(action.activityIds, adapter);
12796
+ const failed = [
12797
+ ...plan.failed
12798
+ ];
12799
+ for (const move of plan.moves) {
12800
+ touched.add(String(move.activityId));
12801
+ touched.add(String(move.newParentId));
12802
+ if (move.oldParentId !== ROOT_PARENT_ID) {
12803
+ touched.add(String(move.oldParentId));
12804
+ }
12805
+ }
12806
+ const allowedMoves = plan.allowedMoves;
12190
12807
  const beforeSnap = await snapshotActivities(adapter, touched);
12191
- for (const { activityId, newParentId } of moves) {
12808
+ for (const { activityId, newParentId } of allowedMoves) {
12192
12809
  adapter.setActivityField(activityId, ACTIVITY_PROPERTY.PARENT, newParentId);
12193
12810
  adapter.setActivityField(
12194
12811
  activityId,
@@ -12198,7 +12815,7 @@ async function dispatchActivityIndent(action, options, deps) {
12198
12815
  succeededIds.push(activityId);
12199
12816
  }
12200
12817
  const promotedParents = /* @__PURE__ */ new Set();
12201
- for (const { activityId, newParentId } of moves) {
12818
+ for (const { activityId, newParentId } of allowedMoves) {
12202
12819
  if (promotedParents.has(newParentId)) continue;
12203
12820
  promotedParents.add(newParentId);
12204
12821
  const newParent = adapter.getActivity(newParentId);
@@ -12250,7 +12867,7 @@ async function dispatchActivityIndent(action, options, deps) {
12250
12867
  releaseClearedCustomId(oldNewParentCustomId, deps.customIdTracker);
12251
12868
  }
12252
12869
  const indentAffectedParents = /* @__PURE__ */ new Set();
12253
- for (const m of moves) {
12870
+ for (const m of allowedMoves) {
12254
12871
  if (m.oldParentId !== ROOT_PARENT_ID)
12255
12872
  indentAffectedParents.add(m.oldParentId);
12256
12873
  indentAffectedParents.add(m.newParentId);
@@ -12411,8 +13028,24 @@ async function dispatchActivityOutdent(action, options, deps) {
12411
13028
  if (grandparentKey !== ROOT_PARENT_ID) touched.add(String(grandparentKey));
12412
13029
  oldParents.add(oldParentKey);
12413
13030
  }
13031
+ const cyclicPlanIds = collectCyclicReparents(
13032
+ planned.map((plan) => ({
13033
+ activityId: plan.activityId,
13034
+ newParentId: plan.grandparentKey
13035
+ })),
13036
+ adapter
13037
+ );
13038
+ for (const cyclicId of cyclicPlanIds) {
13039
+ failed.push({
13040
+ activityId: cyclicId,
13041
+ reason: REJECTION_REASON.HIERARCHY_CYCLE
13042
+ });
13043
+ }
13044
+ const allowedPlans = planned.filter(
13045
+ (plan) => !cyclicPlanIds.has(plan.activityId)
13046
+ );
12414
13047
  const beforeSnap = await snapshotActivities(adapter, touched);
12415
- for (const plan of planned) {
13048
+ for (const plan of allowedPlans) {
12416
13049
  adapter.setActivityField(
12417
13050
  plan.activityId,
12418
13051
  ACTIVITY_PROPERTY.PARENT,
@@ -12446,7 +13079,7 @@ async function dispatchActivityOutdent(action, options, deps) {
12446
13079
  task: parentActivity
12447
13080
  }),
12448
13081
  idsRemoved: new Set(
12449
- planned.filter((plan) => plan.oldParentKey === oldParentKey).map((plan) => plan.activityId)
13082
+ allowedPlans.filter((plan) => plan.oldParentKey === oldParentKey).map((plan) => plan.activityId)
12450
13083
  )
12451
13084
  });
12452
13085
  if (!demotion) {
@@ -12472,7 +13105,7 @@ async function dispatchActivityOutdent(action, options, deps) {
12472
13105
  for (const oldParentKey of oldParents) {
12473
13106
  outdentAffectedParents.add(String(oldParentKey));
12474
13107
  }
12475
- for (const plan of planned) {
13108
+ for (const plan of allowedPlans) {
12476
13109
  if (plan.grandparentKey !== ROOT_PARENT_ID) {
12477
13110
  outdentAffectedParents.add(String(plan.grandparentKey));
12478
13111
  }
@@ -12560,6 +13193,17 @@ async function dispatchActivitySetProgress(action, options, deps) {
12560
13193
  "[ScheduleCore] progressPipeline not registered \u2014 cannot dispatch activity-set-progress"
12561
13194
  );
12562
13195
  }
13196
+ const editGate = pipeline.canEdit(activitySnap, ctx.hierarchy);
13197
+ if (!editGate.allowed) {
13198
+ const declaredButtonOverride = action.origin === "action-button" && (editGate.reason === "has_lookahead_tasks" || editGate.reason === "summary");
13199
+ if (!declaredButtonOverride) {
13200
+ return {
13201
+ ok: false,
13202
+ reason: editGate.reason,
13203
+ ...editGate.alertKey ? { alertKey: editGate.alertKey } : {}
13204
+ };
13205
+ }
13206
+ }
12563
13207
  const changes = pipeline.transform(
12564
13208
  activitySnap,
12565
13209
  action.newValue,
@@ -12618,11 +13262,11 @@ async function dispatchDatesBatch(action, options, deps) {
12618
13262
  const warnings = [];
12619
13263
  let needsAutoSchedule = false;
12620
13264
  for (const edit of action.edits) {
12621
- const outcome = applyOneEdit(edit, deps);
13265
+ const outcome = applyOneEdit2(edit, deps);
12622
13266
  verdicts.push(outcome.verdict);
12623
13267
  if (!outcome.verdict.ok || !outcome.changes) continue;
12624
13268
  const editTouched = collectTouchedIds(edit.activityId, outcome.changes);
12625
- mergeBeforeSnapshots(beforeSnap, editTouched, deps);
13269
+ mergeBeforeSnapshots2(beforeSnap, editTouched, deps);
12626
13270
  for (const id of editTouched) touchedIds.add(id);
12627
13271
  const beforeLinkLags = snapshotIncomingLinkLags(adapter, edit.activityId);
12628
13272
  const preEditSnapshot = snapshotSingleActivity(
@@ -12676,6 +13320,7 @@ async function dispatchDatesBatch(action, options, deps) {
12676
13320
  touchedIds,
12677
13321
  scheduledIds,
12678
13322
  sirDetection: "before-diff",
13323
+ preview: options.preview,
12679
13324
  links: linkChanges,
12680
13325
  trackingEvents,
12681
13326
  warnings,
@@ -12683,222 +13328,18 @@ async function dispatchDatesBatch(action, options, deps) {
12683
13328
  })
12684
13329
  };
12685
13330
  }
12686
- function applyOneEdit(edit, deps) {
13331
+ function applyOneEdit2(edit, deps) {
12687
13332
  const { ctx } = deps;
12688
13333
  const activityId = edit.activityId;
12689
13334
  const pipeline = findPipelineForColumn(edit.column);
12690
13335
  if (!pipeline) {
12691
13336
  throw new Error(
12692
13337
  `[ScheduleCore] no pipeline registered for column "${edit.column}"`
12693
- );
12694
- }
12695
- const activitySnap = ctx.activityReader.getActivity(edit.activityId);
12696
- if (!activitySnap) {
12697
- throw new Error(`[ScheduleCore] activity "${edit.activityId}" not found`);
12698
- }
12699
- const editGate = pipeline.canEdit(activitySnap, ctx.hierarchy);
12700
- if (!editGate.allowed) {
12701
- return {
12702
- verdict: {
12703
- activityId,
12704
- ok: false,
12705
- reason: editGate.reason ?? REJECTION_REASON.CANNOT_EDIT,
12706
- ...editGate.alertKey ? { alertKey: editGate.alertKey } : {}
12707
- },
12708
- changes: null
12709
- };
12710
- }
12711
- const parsed2 = pipeline.parseInput(edit.newValue, activitySnap, ctx);
12712
- if (!parsed2.ok) {
12713
- return {
12714
- verdict: {
12715
- activityId,
12716
- ok: false,
12717
- reason: parsed2.reason ?? REJECTION_REASON.PARSE_ERROR
12718
- },
12719
- changes: null
12720
- };
12721
- }
12722
- const oldValue = Reflect.get(activitySnap, pipeline.targetField);
12723
- const validation = pipeline.validate(
12724
- activitySnap,
12725
- oldValue,
12726
- parsed2.value,
12727
- ctx
12728
- );
12729
- if (!validation.valid) {
12730
- return {
12731
- verdict: {
12732
- activityId,
12733
- ok: false,
12734
- reason: validation.reason ?? REJECTION_REASON.INVALID,
12735
- ...validation.notification?.messageKey ? { alertKey: validation.notification.messageKey } : {}
12736
- },
12737
- changes: null
12738
- };
12739
- }
12740
- const changes = pipeline.transform(
12741
- activitySnap,
12742
- parsed2.value,
12743
- ctx,
12744
- parsed2.raw
12745
- );
12746
- return { verdict: { activityId, ok: true }, changes };
12747
- }
12748
- function mergeBeforeSnapshots(beforeSnap, ids, deps) {
12749
- const missing = /* @__PURE__ */ new Set();
12750
- for (const id of ids) {
12751
- if (!beforeSnap.has(id)) missing.add(id);
12752
- }
12753
- if (missing.size === 0) return;
12754
- for (const missingId of missing) {
12755
- const liveActivity = deps.adapter.getActivity(missingId);
12756
- if (liveActivity) {
12757
- beforeSnap.set(missingId, structuredCloneActivity(liveActivity));
12758
- }
12759
- }
12760
- }
12761
-
12762
- // src/dispatch/handlers/bulk-edit.ts
12763
- var CUSTOM_ID_COLUMN = COLUMN.CUSTOM_ID;
12764
- async function dispatchBulkEdit(action, options, deps) {
12765
- const { adapter, scheduler, sector } = deps;
12766
- const editsIncludeDuration = action.edits.some(
12767
- (edit) => edit.column === COLUMN.DURATION
12768
- );
12769
- if (editsIncludeDuration) {
12770
- scheduler.invalidateExpandedLinksCache();
12771
- }
12772
- const verdicts = [];
12773
- const beforeSnap = /* @__PURE__ */ new Map();
12774
- const constraintPriorsByActivity = /* @__PURE__ */ new Map();
12775
- const explicitDateEditActivities = /* @__PURE__ */ new Set();
12776
- const touchedIds = /* @__PURE__ */ new Set();
12777
- const trackingEvents = [];
12778
- const linkChanges = [];
12779
- const warnings = [];
12780
- let needsAutoSchedule = false;
12781
- for (const edit of action.edits) {
12782
- const outcome = applyOneEdit2(edit, deps);
12783
- verdicts.push(outcome.verdict);
12784
- if (!outcome.verdict.ok || !outcome.changes) continue;
12785
- const constraintWarning = toConstraintWarning(
12786
- edit.activityId,
12787
- outcome.changes.constraintWarning
12788
- );
12789
- if (constraintWarning) warnings.push(constraintWarning);
12790
- const editTouched = collectTouchedIds(edit.activityId, outcome.changes);
12791
- mergeBeforeSnapshots2(beforeSnap, editTouched, deps);
12792
- for (const touchedId of editTouched) touchedIds.add(touchedId);
12793
- recordConstraintPrior(constraintPriorsByActivity, edit, beforeSnap);
12794
- if (isExplicitConstraintDateEdit(edit.column)) {
12795
- explicitDateEditActivities.add(String(edit.activityId));
12796
- }
12797
- const beforeLinkLags = snapshotIncomingLinkLags(adapter, edit.activityId);
12798
- const preEditSnapshot = snapshotSingleActivity(
12799
- adapter,
12800
- String(edit.activityId)
12801
- );
12802
- const customIdBeforeApply = readLiveCustomId(edit, deps);
12803
- const changes = outcome.changes;
12804
- applyFieldChanges(adapter, edit.activityId, changes);
12805
- runPostProcessorsOnAdapter(
12806
- edit.activityId,
12807
- outcome.changes.postProcessors ?? [],
12808
- adapter,
12809
- preEditSnapshot
12810
- );
12811
- syncCustomIdTracker(edit, customIdBeforeApply, deps);
12812
- linkChanges.push(
12813
- ...diffIncomingLinkLagChanges(beforeLinkLags, adapter, edit.activityId)
12814
- );
12815
- needsAutoSchedule ||= Boolean(outcome.changes.needsAutoSchedule);
12816
- for (const trackingEvent of outcome.changes.trackingEvents ?? []) {
12817
- trackingEvents.push({
12818
- name: trackingEvent.name,
12819
- properties: trackingEvent.properties ?? {}
12820
- });
12821
- }
12822
- }
12823
- const postMutationArgs = {
12824
- action,
12825
- autoscheduleFrom: needsAutoSchedule ? "roots" : null,
12826
- recomputeParentsFrom: touchedIds,
12827
- now: deps.now,
12828
- options
12829
- };
12830
- const batchHasConstraintEdits = constraintPriorsByActivity.size > 0;
12831
- const { scheduledIds } = await (batchHasConstraintEdits ? settleConstraintEdits(
12832
- {
12833
- adapter,
12834
- scheduler,
12835
- sector,
12836
- defaultBaseCalendarId: deps.defaultBaseCalendarId
12837
- },
12838
- {
12839
- postMutationArgs,
12840
- revertTargets: [...constraintPriorsByActivity].filter(
12841
- ([activityId]) => !explicitDateEditActivities.has(activityId)
12842
- ).map(([activityId, priorConstraintDate]) => ({
12843
- activityId,
12844
- priorConstraintDate
12845
- })),
12846
- revert: (activityId, priorConstraintDate) => revertNoOpConstraintEdit(
12847
- activityId,
12848
- priorConstraintDate,
12849
- adapter,
12850
- deps.ctx.calendars
12851
- )
12852
- }
12853
- ) : runPostMutation(
12854
- {
12855
- adapter,
12856
- scheduler,
12857
- sector,
12858
- defaultBaseCalendarId: deps.defaultBaseCalendarId
12859
- },
12860
- postMutationArgs
12861
- ));
12862
- return {
12863
- ok: true,
12864
- verdicts,
12865
- changes: await assembleChangeSet(adapter, {
12866
- source: action,
12867
- beforeSnap,
12868
- touchedIds,
12869
- scheduledIds,
12870
- sirDetection: "before-diff",
12871
- links: linkChanges,
12872
- trackingEvents,
12873
- warnings,
12874
- hoursPerDay: sector.hoursPerDay
12875
- })
12876
- };
12877
- }
12878
- function applyOneEdit2(edit, deps) {
12879
- const { ctx } = deps;
12880
- const activityId = edit.activityId;
12881
- const pipeline = findPipelineForColumn(edit.column);
12882
- if (!pipeline) {
12883
- return {
12884
- verdict: {
12885
- activityId,
12886
- ok: false,
12887
- reason: REJECTION_REASON.NO_PIPELINE_FOR_COLUMN
12888
- },
12889
- changes: null
12890
- };
13338
+ );
12891
13339
  }
12892
13340
  const activitySnap = ctx.activityReader.getActivity(edit.activityId);
12893
13341
  if (!activitySnap) {
12894
- return {
12895
- verdict: {
12896
- activityId,
12897
- ok: false,
12898
- reason: REJECTION_REASON.ACTIVITY_NOT_FOUND
12899
- },
12900
- changes: null
12901
- };
13342
+ throw new Error(`[ScheduleCore] activity "${edit.activityId}" not found`);
12902
13343
  }
12903
13344
  const editGate = pipeline.canEdit(activitySnap, ctx.hierarchy);
12904
13345
  if (!editGate.allowed) {
@@ -12941,21 +13382,6 @@ function applyOneEdit2(edit, deps) {
12941
13382
  changes: null
12942
13383
  };
12943
13384
  }
12944
- if (edit.column === CUSTOM_ID_COLUMN && parsed2.value !== null) {
12945
- const liveActivity = deps.adapter.getActivity(edit.activityId);
12946
- const oldCustomIdRaw = liveActivity ? liveActivity.customId : null;
12947
- const oldCustomId = typeof oldCustomIdRaw === "string" ? oldCustomIdRaw : null;
12948
- if (deps.customIdTracker.isCustomIdInUse(String(parsed2.value), oldCustomId)) {
12949
- return {
12950
- verdict: {
12951
- activityId,
12952
- ok: false,
12953
- reason: REJECTION_REASON.CUSTOM_ID_DUPLICATE
12954
- },
12955
- changes: null
12956
- };
12957
- }
12958
- }
12959
13385
  const changes = pipeline.transform(
12960
13386
  activitySnap,
12961
13387
  parsed2.value,
@@ -12964,33 +13390,10 @@ function applyOneEdit2(edit, deps) {
12964
13390
  );
12965
13391
  return { verdict: { activityId, ok: true }, changes };
12966
13392
  }
12967
- function readLiveCustomId(edit, deps) {
12968
- if (edit.column !== CUSTOM_ID_COLUMN) return null;
12969
- const liveActivity = deps.adapter.getActivity(edit.activityId);
12970
- const rawValue = liveActivity ? liveActivity.customId : null;
12971
- return typeof rawValue === "string" ? rawValue : null;
12972
- }
12973
- function syncCustomIdTracker(edit, oldCustomId, deps) {
12974
- if (edit.column !== CUSTOM_ID_COLUMN) return;
12975
- const liveActivity = deps.adapter.getActivity(edit.activityId);
12976
- const rawValue = liveActivity ? liveActivity.customId : null;
12977
- const newCustomId = typeof rawValue === "string" ? rawValue : null;
12978
- if (oldCustomId !== newCustomId) {
12979
- deps.customIdTracker.trackCustomIdChange(oldCustomId, newCustomId);
12980
- }
12981
- }
12982
- function recordConstraintPrior(priors, edit, beforeSnap) {
12983
- if (!isConstraintEditColumn(edit.column)) return;
12984
- const activityKey = String(edit.activityId);
12985
- if (priors.has(activityKey)) return;
12986
- const preBatchSnapshot = beforeSnap.get(activityKey);
12987
- if (!preBatchSnapshot) return;
12988
- priors.set(activityKey, preBatchSnapshot.constraintDate ?? null);
12989
- }
12990
13393
  function mergeBeforeSnapshots2(beforeSnap, ids, deps) {
12991
13394
  const missing = /* @__PURE__ */ new Set();
12992
- for (const candidateId of ids) {
12993
- if (!beforeSnap.has(candidateId)) missing.add(candidateId);
13395
+ for (const id of ids) {
13396
+ if (!beforeSnap.has(id)) missing.add(id);
12994
13397
  }
12995
13398
  if (missing.size === 0) return;
12996
13399
  for (const missingId of missing) {
@@ -13053,9 +13456,33 @@ function assertValidAssignments(assignments, entityName, readCurrent, ownerByBac
13053
13456
  );
13054
13457
  }
13055
13458
  }
13056
- function dispatchPersistenceAcknowledge(action, deps) {
13459
+ function assertValidReconciledProgress(entries, deps) {
13460
+ const seen = /* @__PURE__ */ new Set();
13461
+ for (const entry of entries) {
13462
+ const id = String(entry.activityId);
13463
+ if (id.length === 0 || seen.has(id)) {
13464
+ throw new Error(
13465
+ `persistence-acknowledge: duplicate or empty reconciled activity id "${id}"`
13466
+ );
13467
+ }
13468
+ if (!Number.isFinite(entry.progress) || entry.progress < 0 || entry.progress > 100) {
13469
+ throw new Error(
13470
+ `persistence-acknowledge: invalid reconciled progress ${String(entry.progress)} for "${id}"`
13471
+ );
13472
+ }
13473
+ if (!deps.adapter.getActivity(id)) {
13474
+ throw new Error(
13475
+ `persistence-acknowledge: unknown reconciled activity id "${id}"`
13476
+ );
13477
+ }
13478
+ seen.add(id);
13479
+ }
13480
+ }
13481
+ async function dispatchPersistenceAcknowledge(action, deps) {
13057
13482
  const activities = action.activities ?? [];
13058
13483
  const links = action.links ?? [];
13484
+ const reconciledProgress = action.reconciledProgress ?? [];
13485
+ assertValidReconciledProgress(reconciledProgress, deps);
13059
13486
  const activityOwnerByBackendId = /* @__PURE__ */ new Map();
13060
13487
  for (const activity of deps.adapter.getAllActivities()) {
13061
13488
  if (activity.proplannerId != null) {
@@ -13091,7 +13518,49 @@ function dispatchPersistenceAcknowledge(action, deps) {
13091
13518
  );
13092
13519
  const activityChanges = [];
13093
13520
  const linkChanges = [];
13521
+ const progressToApply = reconciledProgress.filter((entry) => {
13522
+ const current = deps.adapter.getActivity(String(entry.activityId));
13523
+ return current !== null && current.progress !== entry.progress;
13524
+ });
13525
+ const touchedForProgress = new Set(
13526
+ progressToApply.map((entry) => String(entry.activityId))
13527
+ );
13528
+ const parentsToRollup = /* @__PURE__ */ new Set();
13529
+ for (const entry of progressToApply) {
13530
+ const activity = deps.adapter.getActivity(String(entry.activityId));
13531
+ if (activity && !isRootParent(activity.parentId)) {
13532
+ parentsToRollup.add(String(activity.parentId));
13533
+ touchedForProgress.add(String(activity.parentId));
13534
+ }
13535
+ }
13536
+ const beforeProgressSnap = await snapshotActivities(
13537
+ deps.adapter,
13538
+ touchedForProgress
13539
+ );
13094
13540
  deps.adapter.batchUpdate(() => {
13541
+ for (const entry of progressToApply) {
13542
+ deps.adapter.setActivityField(
13543
+ String(entry.activityId),
13544
+ "progress",
13545
+ entry.progress
13546
+ );
13547
+ }
13548
+ for (const parentId of parentsToRollup) {
13549
+ recomputeRollupCascadesForParent(parentId, deps.adapter);
13550
+ }
13551
+ for (const id of touchedForProgress) {
13552
+ const before = beforeProgressSnap.get(id);
13553
+ const after = deps.adapter.getActivity(id);
13554
+ if (!before || !after || before.progress === after.progress) continue;
13555
+ activityChanges.push({
13556
+ id,
13557
+ kind: "updated",
13558
+ fields: {
13559
+ progress: { before: before.progress, after: after.progress }
13560
+ },
13561
+ after: { ...after }
13562
+ });
13563
+ }
13095
13564
  for (const assignment of activities) {
13096
13565
  const before = deps.adapter.getActivity(assignment.id);
13097
13566
  if (!before || before.proplannerId === assignment.proplannerId) continue;
@@ -13498,8 +13967,218 @@ async function dispatchStatusCriteriaSet(action, deps) {
13498
13967
  };
13499
13968
  }
13500
13969
 
13970
+ // src/activity-copy-paste/policy.ts
13971
+ var ACTIVITY_COPY_FIELDS = [
13972
+ "name",
13973
+ "description",
13974
+ "type",
13975
+ "startDate",
13976
+ "durationHours",
13977
+ "constraintType",
13978
+ "constraintDate",
13979
+ "calendarId",
13980
+ "cost",
13981
+ "hasCustomPonderator",
13982
+ "subcontractId",
13983
+ "responsableIds",
13984
+ "tagIds"
13985
+ ];
13986
+
13987
+ // src/activity-copy-paste/copy.ts
13988
+ function copyOverrides(activity) {
13989
+ const overrides = {};
13990
+ for (const field of ACTIVITY_COPY_FIELDS) {
13991
+ Object.assign(overrides, { [field]: cloneDomainValue(activity[field]) });
13992
+ }
13993
+ return overrides;
13994
+ }
13995
+ function selectedInTreeOrder(adapter, selected) {
13996
+ const ordered = [];
13997
+ const visit = (id) => {
13998
+ if (selected.has(String(id))) ordered.push(id);
13999
+ for (const childId of adapter.getChildren(id)) visit(childId);
14000
+ };
14001
+ for (const rootId of adapter.getRootIds()) visit(rootId);
14002
+ return ordered;
14003
+ }
14004
+ function dispatchActivityCopy(action, deps) {
14005
+ const selected = new Set(deps.adapter.checkedIds().map(String));
14006
+ if (selected.size === 0) {
14007
+ return { ok: false, reason: REJECTION_REASON.NO_ACTIVITY_SELECTION };
14008
+ }
14009
+ const orderedIds = selectedInTreeOrder(deps.adapter, selected);
14010
+ deps.clipboard.value = {
14011
+ activities: orderedIds.map((id) => {
14012
+ const activity = deps.adapter.getActivity(id);
14013
+ if (activity === null)
14014
+ throw new Error(`Selected activity ${id} is missing`);
14015
+ return {
14016
+ originalId: id,
14017
+ originalParentId: activity.parentId ?? "0",
14018
+ overrides: copyOverrides(activity)
14019
+ };
14020
+ }),
14021
+ links: deps.adapter.getAllLinks().filter(
14022
+ (link) => selected.has(String(link.source)) && selected.has(String(link.target))
14023
+ ).map((link) => cloneDomainValue(link))
14024
+ };
14025
+ const selection = dispatchSelectionReplace(
14026
+ { kind: "selection-replace", activityIds: [] },
14027
+ { adapter: deps.adapter }
14028
+ );
14029
+ if (!selection.ok) return selection;
14030
+ return { ...selection, changes: { ...selection.changes, source: action } };
14031
+ }
14032
+ function dispatchActivityClipboardClear(action, clipboard) {
14033
+ clipboard.value = null;
14034
+ return {
14035
+ ok: true,
14036
+ changes: {
14037
+ source: action,
14038
+ activities: [],
14039
+ links: [],
14040
+ calendars: [],
14041
+ trackingEvents: []
14042
+ }
14043
+ };
14044
+ }
14045
+
14046
+ // src/activity-copy-paste/paste.ts
14047
+ function resolveDestination(afterSiblingId, adapter) {
14048
+ const sibling = adapter.getActivity(afterSiblingId);
14049
+ if (!sibling) {
14050
+ return { ok: false, reason: REJECTION_REASON.ANCHOR_SIBLING_NOT_FOUND };
14051
+ }
14052
+ return {
14053
+ ok: true,
14054
+ parentId: sibling.parentId == null ? ROOT_PARENT_ID : String(sibling.parentId)
14055
+ };
14056
+ }
14057
+ var invariantFailure = () => ({
14058
+ ok: false,
14059
+ reason: REJECTION_REASON.ACTIVITY_PASTE_INVARIANT_FAILED
14060
+ });
14061
+ async function dispatchActivityPasteCopied(action, options, deps) {
14062
+ const clipboard = deps.activityClipboard.value;
14063
+ if (clipboard === null) {
14064
+ return { ok: false, reason: REJECTION_REASON.CLIPBOARD_EMPTY };
14065
+ }
14066
+ const destination = resolveDestination(
14067
+ action.destination.afterSiblingId,
14068
+ deps.adapter
14069
+ );
14070
+ if (!destination.ok) return destination;
14071
+ const beforeSnapshot = await snapshotActivities(
14072
+ deps.adapter,
14073
+ /* @__PURE__ */ new Set()
14074
+ );
14075
+ const touched = /* @__PURE__ */ new Set();
14076
+ const originalToNew = /* @__PURE__ */ new Map();
14077
+ const createdIds = [];
14078
+ let previousRootId;
14079
+ for (const copied of clipboard.activities) {
14080
+ const mappedParent = originalToNew.get(copied.originalParentId);
14081
+ const created = await createActivityCore(
14082
+ {
14083
+ parentId: mappedParent ?? destination.parentId,
14084
+ ...mappedParent === void 0 ? {
14085
+ afterSiblingId: previousRootId ?? action.destination.afterSiblingId
14086
+ } : {},
14087
+ overrides: cloneDomainValue(copied.overrides),
14088
+ eventSource: action.eventSource
14089
+ },
14090
+ deps,
14091
+ {
14092
+ skipCorrelativeRecompute: true,
14093
+ skipBeforeSnap: true,
14094
+ customIdReferenceId: action.destination.afterSiblingId
14095
+ }
14096
+ );
14097
+ if (!created.ok) return created;
14098
+ originalToNew.set(copied.originalId, created.newId);
14099
+ createdIds.push(created.newId);
14100
+ if (mappedParent === void 0) previousRootId = created.newId;
14101
+ }
14102
+ const correlativeShifts = recomputeCorrelativeIds(deps.adapter);
14103
+ const createdIdSet = new Set(createdIds.map(String));
14104
+ foldCorrelativeShifts(
14105
+ deps.adapter,
14106
+ correlativeShifts,
14107
+ createdIdSet,
14108
+ beforeSnapshot,
14109
+ touched
14110
+ );
14111
+ for (const createdId of createdIds) touched.add(String(createdId));
14112
+ const appliedLinks = [];
14113
+ for (const copiedLink of clipboard.links) {
14114
+ const source = originalToNew.get(copiedLink.source);
14115
+ const target = originalToNew.get(copiedLink.target);
14116
+ if (source === void 0 || target === void 0) return invariantFailure();
14117
+ const operation = {
14118
+ kind: "create",
14119
+ source,
14120
+ target,
14121
+ type: copiedLink.type,
14122
+ lag: copiedLink.lag
14123
+ };
14124
+ const result = applyLinkOperation(operation, {
14125
+ port: deps.adapter,
14126
+ newLinkId: deps.linkIdGen.next
14127
+ });
14128
+ if (!result.applied) return invariantFailure();
14129
+ appliedLinks.push({
14130
+ op: operation,
14131
+ finalLinkId: result.linkId,
14132
+ rejected: null
14133
+ });
14134
+ }
14135
+ const { scheduledIds } = await runPostMutation(
14136
+ {
14137
+ adapter: deps.adapter,
14138
+ scheduler: deps.scheduler,
14139
+ sector: deps.sector,
14140
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
14141
+ },
14142
+ {
14143
+ action,
14144
+ autoscheduleFrom: createdIds[0] ?? null,
14145
+ recomputeParentsFrom: createdIds,
14146
+ now: deps.now,
14147
+ options
14148
+ }
14149
+ );
14150
+ const linkChanges = buildLinkChangesForBatch(
14151
+ appliedLinks,
14152
+ /* @__PURE__ */ new Map(),
14153
+ deps.adapter
14154
+ );
14155
+ return {
14156
+ ok: true,
14157
+ changes: await assembleChangeSet(deps.adapter, {
14158
+ source: action,
14159
+ beforeSnap: beforeSnapshot,
14160
+ touchedIds: touched,
14161
+ scheduledIds,
14162
+ links: linkChanges,
14163
+ hoursPerDay: deps.sector.hoursPerDay
14164
+ })
14165
+ };
14166
+ }
14167
+
13501
14168
  // src/dispatch/dispatch.ts
13502
14169
  async function performDispatch(action, options, deps) {
14170
+ if (action.kind === "activity-copy") {
14171
+ return dispatchActivityCopy(action, {
14172
+ adapter: deps.adapter,
14173
+ clipboard: deps.activityClipboard
14174
+ });
14175
+ }
14176
+ if (action.kind === "activity-clipboard-clear") {
14177
+ return dispatchActivityClipboardClear(action, deps.activityClipboard);
14178
+ }
14179
+ if (action.kind === "activity-paste-copied") {
14180
+ return dispatchActivityPasteCopied(action, options, deps);
14181
+ }
13503
14182
  if (action.kind === "activity-batch") {
13504
14183
  return dispatchActivityBatch(action, options, deps);
13505
14184
  }
@@ -13524,9 +14203,6 @@ async function performDispatch(action, options, deps) {
13524
14203
  if (action.kind === "activity-create") {
13525
14204
  return dispatchActivityCreate(action, options, deps);
13526
14205
  }
13527
- if (action.kind === "activity-paste") {
13528
- return dispatchActivityPaste(action, options, deps);
13529
- }
13530
14206
  if (action.kind === "activity-delete") {
13531
14207
  return dispatchActivityDelete(action, options, deps);
13532
14208
  }
@@ -13569,6 +14245,9 @@ async function performDispatch(action, options, deps) {
13569
14245
  if (action.kind === "sir-sync") {
13570
14246
  return dispatchSirSync(action, deps);
13571
14247
  }
14248
+ if (action.kind === "sir-approve") {
14249
+ return dispatchSirApprove(action, options, deps);
14250
+ }
13572
14251
  if (action.kind === "activity-lookahead-sync") {
13573
14252
  return dispatchActivityLookaheadSync(action, deps);
13574
14253
  }
@@ -16981,6 +17660,7 @@ var ScheduleState = class {
16981
17660
  if (!activity) return;
16982
17661
  this._writeCapture.note(String(activityId), activity);
16983
17662
  activity[field] = value;
17663
+ refreshCalendarDurationIfDateBearing(activity, field);
16984
17664
  if (field === ACTIVITY_PROPERTY.PARENT) this._hierarchy.markDirty();
16985
17665
  if (fieldAffectsVisualOrder(field)) this._cache.visualOrderIds = null;
16986
17666
  }
@@ -16994,6 +17674,7 @@ var ScheduleState = class {
16994
17674
  }
16995
17675
  this._writeCapture.note(String(activityId), activity);
16996
17676
  Reflect.set(activity, field, coerceActivityFieldValue(field, value));
17677
+ refreshCalendarDurationIfDateBearing(activity, field);
16997
17678
  if (field === ACTIVITY_PROPERTY.PARENT) this._hierarchy.markDirty();
16998
17679
  if (fieldAffectsVisualOrder(field)) this._cache.visualOrderIds = null;
16999
17680
  }
@@ -17004,6 +17685,7 @@ var ScheduleState = class {
17004
17685
  let reparented = false;
17005
17686
  for (const field of Object.keys(fields)) {
17006
17687
  Reflect.set(activity, field, Reflect.get(fields, field));
17688
+ refreshCalendarDurationIfDateBearing(activity, field);
17007
17689
  if (field === ACTIVITY_PROPERTY.PARENT) reparented = true;
17008
17690
  if (fieldAffectsVisualOrder(field)) this._cache.visualOrderIds = null;
17009
17691
  }
@@ -17172,6 +17854,12 @@ function isMutableLinkField(field) {
17172
17854
  function fieldAffectsVisualOrder(field) {
17173
17855
  return field === ACTIVITY_PROPERTY.PARENT || field === ACTIVITY_PROPERTY.CORRELATIVE_ID;
17174
17856
  }
17857
+ function refreshCalendarDurationIfDateBearing(activity, field) {
17858
+ if (field !== ACTIVITY_PROPERTY.START_DATE && field !== ACTIVITY_PROPERTY.END_DATE && field !== ACTIVITY_PROPERTY.TYPE) {
17859
+ return;
17860
+ }
17861
+ activity.calendarDuration = computeCalendarDuration(activity);
17862
+ }
17175
17863
  function detachLinkRef(refs, linkId) {
17176
17864
  return refs.filter((ref) => String(ref) !== String(linkId));
17177
17865
  }
@@ -17219,15 +17907,7 @@ function readChildren(state, parentId) {
17219
17907
  return out;
17220
17908
  }
17221
17909
  function readChildrenIds(state, parentId) {
17222
- const key = parentId;
17223
- if (key === ROOT_PARENT_ID) {
17224
- const roots = [];
17225
- state.forEachActivity((activity, id) => {
17226
- if (isRootParent(activity.parentId)) roots.push(id);
17227
- });
17228
- return roots;
17229
- }
17230
- return getChildrenInVisualOrder(key, state);
17910
+ return getChildrenInVisualOrder(parentId, state);
17231
17911
  }
17232
17912
  function readSelectedActivityIds(state) {
17233
17913
  return state.checkedIds().map(String);
@@ -17291,7 +17971,11 @@ function buildPipelineContext(adapter, options) {
17291
17971
  dateFormat: options.dateFormat ?? "YYYY-MM-DD",
17292
17972
  customHours: options.customHours ?? {},
17293
17973
  hoursPerDay: options.hoursPerDay,
17294
- inputUnit: options.inputUnit ?? "days"
17974
+ // ISSUE-057 (CP6): sin default. Un comando de duración sin inputUnit se
17975
+ // rechaza en el pipeline; el resto de columnas no lo leen. La clave se
17976
+ // OMITE cuando no se declaró (exactOptionalPropertyTypes), no se pone a
17977
+ // undefined.
17978
+ ...options.inputUnit !== void 0 ? { inputUnit: options.inputUnit } : {}
17295
17979
  };
17296
17980
  }
17297
17981
 
@@ -17371,9 +18055,10 @@ function mergeCoalesced(top, next) {
17371
18055
  };
17372
18056
  return buildUndoEntry(changeSet);
17373
18057
  }
17374
- function getDispatchHistoryPolicy(action) {
18058
+ function getDispatchHistoryPolicy(action, options) {
18059
+ if (options?.preview === true) return "skip";
17375
18060
  if (action.kind === "persistence-acknowledge") return "clear-on-success";
17376
- 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") {
18061
+ 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") {
17377
18062
  return "skip";
17378
18063
  }
17379
18064
  return "record";
@@ -18157,6 +18842,11 @@ function parseActivity(raw, context) {
18157
18842
  Number(raw.sumOfDurationRecursively ?? 0),
18158
18843
  context.hoursPerDay
18159
18844
  ),
18845
+ calendarDuration: computeCalendarDuration({
18846
+ type: raw.type,
18847
+ startDate,
18848
+ endDate
18849
+ }),
18160
18850
  isCritical: Boolean(raw.is_critical),
18161
18851
  freeSlackHours: null,
18162
18852
  expectedProgress: null,
@@ -18521,7 +19211,8 @@ function parseSector(sector) {
18521
19211
  ...customIdIncrement !== void 0 ? { customIdIncrement } : {},
18522
19212
  ...isPrimaveraEndDate ? { updateDurationForPrimaveraEndDate: true } : {},
18523
19213
  activityCreter: parseActivityCreter(sector.activity_creter),
18524
- statusCriteria: DEFAULT_STATUS_CRITERIA
19214
+ statusCriteria: DEFAULT_STATUS_CRITERIA,
19215
+ ganttId: null
18525
19216
  };
18526
19217
  }
18527
19218
 
@@ -19313,6 +20004,7 @@ function recomputeSkippedLeafEnds(state) {
19313
20004
  durationHours: activity.durationHours,
19314
20005
  task: activity
19315
20006
  });
20007
+ activity.calendarDuration = computeCalendarDuration(activity);
19316
20008
  });
19317
20009
  }
19318
20010
  function recomputeDurationsFromDates(state) {
@@ -19384,6 +20076,7 @@ function initializeCore(input) {
19384
20076
  );
19385
20077
  const sector = parsed2.sector;
19386
20078
  sector.statusCriteria = resolveStatusCriteria(input.statusCriteria);
20079
+ sector.ganttId = input.ganttId ?? null;
19387
20080
  const calendars = parsed2.calendars;
19388
20081
  const baseCalendars = parsed2.baseCalendars ?? [];
19389
20082
  const snapshot = buildStateSnapshot(
@@ -19590,6 +20283,7 @@ function evaluateEnd(formula, proposedEdgeTimeMs) {
19590
20283
  }
19591
20284
 
19592
20285
  // src/init/schedule-core.ts
20286
+ var PREVIEW_DISPATCH_KINDS = /* @__PURE__ */ new Set(["inline-edit", "bulk-edit", "dates-batch"]);
19593
20287
  function changesRequireFilterProjectionRebuild(changes) {
19594
20288
  for (const change of changes.activities) {
19595
20289
  if (change.kind === "deleted") return true;
@@ -19614,6 +20308,7 @@ function isCurrentFilterProjection(projection, filter, context) {
19614
20308
  return hasCurrentHours && hasCurrentLocale;
19615
20309
  }
19616
20310
  var ScheduleCore = class {
20311
+ _activityClipboard = { value: null };
19617
20312
  _status = SCHEDULE_CORE_STATUS.READY;
19618
20313
  coreRuntime;
19619
20314
  _undo = new UndoRecorder();
@@ -19709,6 +20404,23 @@ var ScheduleCore = class {
19709
20404
  this.assertReady();
19710
20405
  return readAllActivities(this.coreRuntime.state);
19711
20406
  }
20407
+ /**
20408
+ * Read del plan de indent (ISSUE-080): los padres destino DISTINTOS bajo los
20409
+ * que quedarían las actividades si se indentaran ahora. Es exactamente la
20410
+ * planificación que ejecuta el handler (`planIndentMoves`, autoridad única),
20411
+ * expuesta para que la frontera pueda correr el protocolo de conversión a
20412
+ * madre (chequeo backend + modal) ANTES de despachar. Solo lecturas.
20413
+ */
20414
+ getIndentTargetParentIds(activityIds) {
20415
+ this.assertReady();
20416
+ const plan = planIndentMoves(
20417
+ activityIds.map((id) => String(id)),
20418
+ this.coreRuntime.state
20419
+ );
20420
+ return [
20421
+ ...new Set(plan.allowedMoves.map((move) => String(move.newParentId)))
20422
+ ];
20423
+ }
19712
20424
  getChildrenView(parentId) {
19713
20425
  this.assertReady();
19714
20426
  return readChildren(this.coreRuntime.state, parentId);
@@ -19721,6 +20433,26 @@ var ScheduleCore = class {
19721
20433
  this.assertReady();
19722
20434
  return readAllLinks(this.coreRuntime.state);
19723
20435
  }
20436
+ auditEffectiveGraph() {
20437
+ this.assertReady();
20438
+ const state = this.coreRuntime.state;
20439
+ const logicalLinks = state.getAllLinks();
20440
+ const effectiveEdges = projectEffectiveEdges(state, logicalLinks);
20441
+ const components = circularComponents(effectiveEdges);
20442
+ let largestComponentSize = 0;
20443
+ for (const component of components) {
20444
+ if (component.length > largestComponentSize) {
20445
+ largestComponentSize = component.length;
20446
+ }
20447
+ }
20448
+ return {
20449
+ activityCount: state.getAllIds().length,
20450
+ logicalLinkCount: logicalLinks.length,
20451
+ effectiveEdgeCount: effectiveEdges.length,
20452
+ circularComponents: components,
20453
+ largestComponentSize
20454
+ };
20455
+ }
19724
20456
  getProperty(activityId, property) {
19725
20457
  this.assertReady();
19726
20458
  const snapshot = this.coreRuntime.state.getActivity(activityId);
@@ -19749,11 +20481,11 @@ var ScheduleCore = class {
19749
20481
  }
19750
20482
  getActiveFilter() {
19751
20483
  this.assertReady();
19752
- return this.coreRuntime.state.getActiveFilter();
20484
+ return cloneDomainValue(this.coreRuntime.state.getActiveFilter());
19753
20485
  }
19754
20486
  getActiveOrder() {
19755
20487
  this.assertReady();
19756
- return this.coreRuntime.state.getActiveOrder();
20488
+ return cloneDomainValue(this.coreRuntime.state.getActiveOrder());
19757
20489
  }
19758
20490
  getVisualOrderIds() {
19759
20491
  this.assertReady();
@@ -19804,6 +20536,74 @@ var ScheduleCore = class {
19804
20536
  this.assertReady();
19805
20537
  return this._saveTracker.hasUnsavedChanges();
19806
20538
  }
20539
+ /**
20540
+ * Read-only view of the internal schedule revision. It moves once per
20541
+ * substantive CANONICAL mutation (dispatch, undo, redo). It does NOT move
20542
+ * for a preview dispatch nor for `applyPreviewInverse`: comparing the value
20543
+ * captured in `PreviewDispatchInfo.revision` against the current one
20544
+ * answers "did the schedule change while the preview was open".
20545
+ */
20546
+ getScheduleRevision() {
20547
+ return this._scheduleRevision;
20548
+ }
20549
+ /**
20550
+ * Exit channel for a preview dispatch (`DispatchOptions.preview`): applies
20551
+ * the inverse ChangeSet the preview returned, restoring the canonical
20552
+ * state exactly. Like the preview itself it leaves no session trace: no
20553
+ * save-tracker dirty, no undo entry, no effects, and the schedule revision
20554
+ * does not move (see `getScheduleRevision`).
20555
+ *
20556
+ * Returns the ChangeSet to project to the view layer: the same changes as
20557
+ * `inverse`, but with `after` images re-read from the restored state (the
20558
+ * ones inside `inverse` were captured while the preview edit was applied).
20559
+ */
20560
+ applyPreviewInverse(inverse) {
20561
+ return this._enqueue(async () => {
20562
+ this.assertReady();
20563
+ const state = this.coreRuntime.state;
20564
+ const afterSnap = /* @__PURE__ */ new Map();
20565
+ for (const change of inverse.activities) {
20566
+ if (change.kind !== "deleted" && change.after) {
20567
+ afterSnap.set(
20568
+ String(change.id),
20569
+ structuredCloneActivity(change.after)
20570
+ );
20571
+ }
20572
+ }
20573
+ const afterLinks = /* @__PURE__ */ new Map();
20574
+ for (const change of inverse.links) {
20575
+ if (change.kind !== "deleted" && change.after) {
20576
+ afterLinks.set(String(change.id), { ...change.after });
20577
+ }
20578
+ }
20579
+ const entry = buildUndoEntry(
20580
+ inverse,
20581
+ void 0,
20582
+ void 0,
20583
+ afterSnap,
20584
+ afterLinks
20585
+ );
20586
+ applyRedo(state, entry);
20587
+ if (needsResync(entry)) this._resyncCustomIdTrackerFromModel();
20588
+ const restored = {
20589
+ ...inverse,
20590
+ activities: inverse.activities.map((change) => {
20591
+ if (change.kind === "deleted") return { ...change, after: null };
20592
+ const live = state.getActivity(String(change.id));
20593
+ return {
20594
+ ...change,
20595
+ after: live ? structuredCloneActivity(live) : null
20596
+ };
20597
+ }),
20598
+ links: inverse.links.map((change) => {
20599
+ if (change.kind === "deleted") return { ...change, after: null };
20600
+ const live = state.getLink(String(change.id));
20601
+ return { ...change, after: live ? snapshotToLink(live) : null };
20602
+ })
20603
+ };
20604
+ return this._reapplyViewState(restored) ?? restored;
20605
+ });
20606
+ }
19807
20607
  getDeletedActivitiesSinceLastSave() {
19808
20608
  this.assertReady();
19809
20609
  return this._saveTracker.deletedActivities(this.coreRuntime.state.getAllActivities()).map(cloneDomainValue);
@@ -19886,11 +20686,11 @@ var ScheduleCore = class {
19886
20686
  });
19887
20687
  }
19888
20688
  async dispatch(action, options = {}) {
19889
- const admitted = this._beginScheduleMutation(action);
20689
+ const admitted = options.preview === true ? false : this._beginScheduleMutation(action);
19890
20690
  return this._enqueue(() => this._dispatchInner(action, options, admitted));
19891
20691
  }
19892
20692
  async applyActivityBatch(action, options = {}) {
19893
- const admitted = this._beginScheduleMutation(action);
20693
+ const admitted = options.preview === true ? false : this._beginScheduleMutation(action);
19894
20694
  return this._enqueue(() => this._dispatchInner(action, options, admitted));
19895
20695
  }
19896
20696
  async _dispatchInner(action, options = {}, admitted = false) {
@@ -19905,12 +20705,23 @@ var ScheduleCore = class {
19905
20705
  }
19906
20706
  async _runDispatch(action, options) {
19907
20707
  this.assertReady();
20708
+ if (options.preview === true && !PREVIEW_DISPATCH_KINDS.has(action.kind)) {
20709
+ return {
20710
+ result: {
20711
+ ok: false,
20712
+ reason: REJECTION_REASON.PREVIEW_UNSUPPORTED_ACTION
20713
+ },
20714
+ changedState: false
20715
+ };
20716
+ }
19908
20717
  const pipelineContext = buildPipelineContext(this.coreRuntime.state, {
19909
20718
  hoursPerDay: this.coreRuntime.sector.hoursPerDay,
19910
20719
  dateFormat: this.coreRuntime.sector.dateFormat,
19911
- inputUnit: options.inputUnit ?? "days"
20720
+ // ISSUE-057 (CP6): sin default silencioso 'days'; la clave se omite si
20721
+ // no se declaró (exactOptionalPropertyTypes).
20722
+ ...options.inputUnit !== void 0 ? { inputUnit: options.inputUnit } : {}
19912
20723
  });
19913
- const historyPolicy = getDispatchHistoryPolicy(action);
20724
+ const historyPolicy = getDispatchHistoryPolicy(action, options);
19914
20725
  const generatorSnapshot = [
19915
20726
  this.coreRuntime.activityIdGenerator.snapshot(),
19916
20727
  this.coreRuntime.linkIdGenerator.snapshot(),
@@ -19931,6 +20742,7 @@ var ScheduleCore = class {
19931
20742
  uidGen: this.coreRuntime.uniqueCorrelativeIdGenerator,
19932
20743
  customIdTracker: this.coreRuntime.customIdTracker,
19933
20744
  reporter: this.coreRuntime.reporter,
20745
+ activityClipboard: this._activityClipboard,
19934
20746
  defaultBaseCalendarId: this.coreRuntime.baseCalendars.find(
19935
20747
  (calendar) => calendar.baseDefault
19936
20748
  )?.id ?? null,
@@ -19973,7 +20785,7 @@ var ScheduleCore = class {
19973
20785
  action.activityIds,
19974
20786
  (activityId) => this.coreRuntime.state.getActivity(activityId)
19975
20787
  );
19976
- } else {
20788
+ } else if (options.preview !== true) {
19977
20789
  this._saveTracker.recordChanges(
19978
20790
  result.changes.activities,
19979
20791
  result.changes.links,
@@ -19984,6 +20796,20 @@ var ScheduleCore = class {
19984
20796
  }
19985
20797
  result = this._withReappliedViewState(action, result);
19986
20798
  if (!result.ok) return { result, changedState: false };
20799
+ if (options.preview === true) {
20800
+ const inverse = buildInverseChangeSet(
20801
+ this.coreRuntime.state,
20802
+ buildUndoEntry(buildHistoryChangeSet(result.changes)),
20803
+ "before"
20804
+ );
20805
+ return {
20806
+ result: {
20807
+ ...toPublicDispatchResult(result),
20808
+ preview: { inverse, revision: this._scheduleRevision }
20809
+ },
20810
+ changedState: false
20811
+ };
20812
+ }
19987
20813
  if (dispatchChangesSchedulingState(action) && changeSetIsSubstantive(result.changes)) {
19988
20814
  this._recordScheduleMutation(
19989
20815
  options.skipAutoSchedule !== true && options.skipCriticalPath !== true && resolveRunCriticalPath(action)
@@ -20498,6 +21324,9 @@ var DEFAULT_HOURS_PER_DAY = 8;
20498
21324
 
20499
21325
  // src/dispatch/action-kinds.ts
20500
21326
  var DISPATCH_ACTION_KIND = {
21327
+ ACTIVITY_CLIPBOARD_CLEAR: "activity-clipboard-clear",
21328
+ ACTIVITY_COPY: "activity-copy",
21329
+ ACTIVITY_PASTE_COPIED: "activity-paste-copied",
20501
21330
  PERSISTENCE_ACKNOWLEDGE: "persistence-acknowledge",
20502
21331
  BASELINE_APPLY: "baseline-apply",
20503
21332
  PONDERATOR_CRITERION_SET: "ponderator-criterion-set",
@@ -20515,16 +21344,19 @@ var DISPATCH_ACTION_KIND = {
20515
21344
  ACTIVITY_INDENT: "activity-indent",
20516
21345
  ACTIVITY_OUTDENT: "activity-outdent",
20517
21346
  ACTIVITY_SET_PROGRESS: "activity-set-progress",
20518
- ACTIVITY_PASTE: "activity-paste",
20519
21347
  SELECTION_TOGGLE: "selection-toggle",
20520
21348
  SELECTION_REPLACE: "selection-replace",
20521
21349
  VISIBILITY_SET: "visibility-set",
20522
21350
  FILTER_SET: "filter-set",
20523
21351
  SORT_SET: "sort-set",
20524
21352
  SIR_SYNC: "sir-sync",
21353
+ SIR_APPROVE: "sir-approve",
20525
21354
  ACTIVITY_LOOKAHEAD_SYNC: "activity-lookahead-sync"
20526
21355
  };
20527
21356
  var KIND_CATALOG_COVERS_UNION = {
21357
+ [DISPATCH_ACTION_KIND.ACTIVITY_CLIPBOARD_CLEAR]: true,
21358
+ [DISPATCH_ACTION_KIND.ACTIVITY_COPY]: true,
21359
+ [DISPATCH_ACTION_KIND.ACTIVITY_PASTE_COPIED]: true,
20528
21360
  [DISPATCH_ACTION_KIND.PERSISTENCE_ACKNOWLEDGE]: true,
20529
21361
  [DISPATCH_ACTION_KIND.BASELINE_APPLY]: true,
20530
21362
  [DISPATCH_ACTION_KIND.PONDERATOR_CRITERION_SET]: true,
@@ -20542,13 +21374,13 @@ var KIND_CATALOG_COVERS_UNION = {
20542
21374
  [DISPATCH_ACTION_KIND.ACTIVITY_INDENT]: true,
20543
21375
  [DISPATCH_ACTION_KIND.ACTIVITY_OUTDENT]: true,
20544
21376
  [DISPATCH_ACTION_KIND.ACTIVITY_SET_PROGRESS]: true,
20545
- [DISPATCH_ACTION_KIND.ACTIVITY_PASTE]: true,
20546
21377
  [DISPATCH_ACTION_KIND.SELECTION_TOGGLE]: true,
20547
21378
  [DISPATCH_ACTION_KIND.SELECTION_REPLACE]: true,
20548
21379
  [DISPATCH_ACTION_KIND.VISIBILITY_SET]: true,
20549
21380
  [DISPATCH_ACTION_KIND.FILTER_SET]: true,
20550
21381
  [DISPATCH_ACTION_KIND.SORT_SET]: true,
20551
21382
  [DISPATCH_ACTION_KIND.SIR_SYNC]: true,
21383
+ [DISPATCH_ACTION_KIND.SIR_APPROVE]: true,
20552
21384
  [DISPATCH_ACTION_KIND.ACTIVITY_LOOKAHEAD_SYNC]: true
20553
21385
  };
20554
21386
  var DISPATCH_ACTION_KINDS = Object.keys(KIND_CATALOG_COVERS_UNION);