@outbuild-company/schedule-core 1.6.2 → 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
@@ -4425,29 +4818,6 @@ function toConstraintWarning(activityId, notification) {
4425
4818
  };
4426
4819
  }
4427
4820
 
4428
- // src/constants/activity-properties.ts
4429
- var ACTIVITY_PROPERTY = {
4430
- PARENT: "parentId",
4431
- TYPE: "type",
4432
- START_DATE: "startDate",
4433
- END_DATE: "endDate",
4434
- CONSTRAINT_TYPE: "constraintType",
4435
- CONSTRAINT_DATE: "constraintDate",
4436
- AUTO_SCHEDULING: "autoScheduling",
4437
- PROGRESS: "progress",
4438
- COST: "cost",
4439
- USED_COST: "usedCost",
4440
- REAL_COST: "realCost",
4441
- WORK_HOURS: "workHours",
4442
- REAL_WORK_HOURS: "realWorkHours",
4443
- CORRELATIVE_ID: "correlativeId",
4444
- CUSTOM_ID: "customId",
4445
- NEW_ACTIVITIES_ARRAY: "newActivityIds",
4446
- HAS_NEW_ACTIVITIES: "hasNewActivities"};
4447
- var LINK_PROPERTY = {
4448
- TYPE: "type",
4449
- LAG: "lag"};
4450
-
4451
4821
  // src/columns/types.ts
4452
4822
  var COLUMN_WRITABLE_FIELD_LIST = [
4453
4823
  "name",
@@ -4475,6 +4845,29 @@ var COLUMN_WRITABLE_FIELDS = new Set(
4475
4845
  COLUMN_WRITABLE_FIELD_LIST
4476
4846
  );
4477
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
+
4478
4871
  // src/internal/state/dynamic-writes.ts
4479
4872
  function setActivityFieldDynamic(state, activityId, field, value) {
4480
4873
  state.setActivityFieldDynamic(activityId, field, value);
@@ -4526,11 +4919,6 @@ function cloneCriticalPath(value) {
4526
4919
  };
4527
4920
  }
4528
4921
 
4529
- // src/shared/clone-domain-value.ts
4530
- function cloneDomainValue(value) {
4531
- return structuredClone(value);
4532
- }
4533
-
4534
4922
  // src/dispatch/shared/snapshots.ts
4535
4923
  var YIELD_EVERY_N_ENTRIES = 200;
4536
4924
  function collectTouchedIds(primary, changes) {
@@ -4642,6 +5030,7 @@ function diffActivity(before, after) {
4642
5030
  const beforeRec = before ?? {};
4643
5031
  const afterRec = after;
4644
5032
  const keys = /* @__PURE__ */ new Set([...Object.keys(beforeRec), ...Object.keys(afterRec)]);
5033
+ keys.delete(ACTIVITY_PROPERTY.CALENDAR_DURATION);
4645
5034
  for (const k of keys) {
4646
5035
  if (Object.hasOwn(beforeRec, k) !== Object.hasOwn(afterRec, k) || !fieldValueEqual(beforeRec[k], afterRec[k])) {
4647
5036
  fields[k] = {
@@ -9368,6 +9757,8 @@ var NON_SCHEDULING_INLINE_COLUMNS = /* @__PURE__ */ new Set([
9368
9757
  COLUMN.TAGS
9369
9758
  ]);
9370
9759
  var NON_SCHEDULING_KINDS = /* @__PURE__ */ new Set([
9760
+ "activity-clipboard-clear",
9761
+ "activity-copy",
9371
9762
  "selection-toggle",
9372
9763
  "selection-replace",
9373
9764
  "visibility-set",
@@ -9381,6 +9772,8 @@ var NON_SCHEDULING_KINDS = /* @__PURE__ */ new Set([
9381
9772
  "status-criteria-set"
9382
9773
  ]);
9383
9774
  var PURE_VIEW_STATE_KINDS = /* @__PURE__ */ new Set([
9775
+ "activity-clipboard-clear",
9776
+ "activity-copy",
9384
9777
  "selection-toggle",
9385
9778
  "selection-replace",
9386
9779
  "visibility-set",
@@ -9706,7 +10099,8 @@ async function assembleChangeSet(adapter, args) {
9706
10099
  const insertedIds = collectInsertedIds(captured);
9707
10100
  mergeDirtyBeforeImages(adapter, captured, insertedIds, before, allTouched);
9708
10101
  }
9709
- 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) : [];
9710
10104
  const correlativeBefore = buildCorrelativeBeforeMap(
9711
10105
  args.correlativeShifts,
9712
10106
  allTouched
@@ -9717,7 +10111,7 @@ async function assembleChangeSet(adapter, args) {
9717
10111
  allTouched,
9718
10112
  correlativeBefore
9719
10113
  );
9720
- const effects = args.sirDetection === "after-diff" ? detectSirAutoReject(before, adapter, allTouched) : beforeDiffEffects;
10114
+ const effects = sirDetection === "after-diff" ? detectSirAutoReject(before, adapter, allTouched) : beforeDiffEffects;
9721
10115
  const warnings = args.warnings ?? [];
9722
10116
  return {
9723
10117
  source: args.source,
@@ -9915,16 +10309,14 @@ async function dispatchInlineEdit(action, options, deps) {
9915
10309
  const { adapter, ctx, scheduler, sector } = deps;
9916
10310
  const pipeline = findPipelineForColumn(action.column);
9917
10311
  if (!pipeline) {
9918
- throw new Error(
9919
- `[ScheduleCore] no pipeline registered for column "${action.column}"`
9920
- );
10312
+ return {
10313
+ ok: false,
10314
+ reason: REJECTION_REASON.NO_PIPELINE_FOR_COLUMN
10315
+ };
9921
10316
  }
9922
- if (columnInvalidatesExpandedLinks(action.column)) {
10317
+ if (action.column === COLUMN.DURATION) {
9923
10318
  scheduler.invalidateExpandedLinksCache();
9924
10319
  }
9925
- if (action.column === ACTIVITY_PROPERTY.PARENT) {
9926
- scheduler.invalidateAllCaches();
9927
- }
9928
10320
  const activitySnap = ctx.activityReader.getActivity(action.activityId);
9929
10321
  if (!activitySnap) {
9930
10322
  return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
@@ -10048,6 +10440,7 @@ async function dispatchInlineEdit(action, options, deps) {
10048
10440
  touchedIds,
10049
10441
  scheduledIds,
10050
10442
  sirDetection: "before-diff",
10443
+ preview: options.preview,
10051
10444
  links: linkChanges,
10052
10445
  trackingEvents,
10053
10446
  ...constraintWarning ? { warnings: [constraintWarning] } : {},
@@ -10055,9 +10448,6 @@ async function dispatchInlineEdit(action, options, deps) {
10055
10448
  })
10056
10449
  };
10057
10450
  }
10058
- function columnInvalidatesExpandedLinks(column) {
10059
- return column === COLUMN.DURATION || column === ACTIVITY_PROPERTY.AUTO_SCHEDULING;
10060
- }
10061
10451
 
10062
10452
  // src/dispatch/handlers/link.ts
10063
10453
  function collectBatchContext(operations, adapter) {
@@ -10257,7 +10647,7 @@ async function dispatchInlineEditLinks(action, options, deps) {
10257
10647
  const { specs: specsInDays } = parsePredecessorString(action.newValue);
10258
10648
  const specs = specsInDays.map((spec) => ({
10259
10649
  ...spec,
10260
- lag: lagDaysToHours(spec.lag, sector.hoursPerDay)
10650
+ lag: lagDaysToHours(roundLagDays(spec.lag), sector.hoursPerDay)
10261
10651
  }));
10262
10652
  const byCorrelative = /* @__PURE__ */ new Map();
10263
10653
  adapter.forEachActivity((a) => {
@@ -10567,11 +10957,24 @@ function buildNewActivity(input, adapter) {
10567
10957
  startDate: rawStartDate,
10568
10958
  endDate: rawStartDate
10569
10959
  };
10570
- const startDate = overrides.startDate ? new Date(base.startDate) : adapter.getClosestWorkTime({
10960
+ let startDate = overrides.startDate ? new Date(base.startDate) : adapter.getClosestWorkTime({
10571
10961
  date: base.startDate,
10572
10962
  dir: WORK_TIME_DIRECTION.FUTURE,
10573
10963
  task: base
10574
10964
  });
10965
+ if (!overrides.startDate && parent && isMilestoneType(parent.type)) {
10966
+ const inheritedDay = new Date(parent.startDate);
10967
+ inheritedDay.setUTCHours(0, 0, 0, 0);
10968
+ const snappedDay = new Date(startDate);
10969
+ snappedDay.setUTCHours(0, 0, 0, 0);
10970
+ if (snappedDay.getTime() !== inheritedDay.getTime()) {
10971
+ startDate = adapter.getClosestWorkTime({
10972
+ date: inheritedDay,
10973
+ dir: WORK_TIME_DIRECTION.FUTURE,
10974
+ task: base
10975
+ });
10976
+ }
10977
+ }
10575
10978
  const endDate = adapter.calculateEndDate({
10576
10979
  startDate,
10577
10980
  durationHours: base.durationHours,
@@ -10581,6 +10984,11 @@ function buildNewActivity(input, adapter) {
10581
10984
  ...base,
10582
10985
  startDate,
10583
10986
  endDate,
10987
+ calendarDuration: computeCalendarDuration({
10988
+ type: base.type,
10989
+ startDate,
10990
+ endDate
10991
+ }),
10584
10992
  newActivityIds: [...base.newActivityIds],
10585
10993
  pendingRequestIds: [...base.pendingRequestIds],
10586
10994
  responsableIds: [...base.responsableIds],
@@ -11206,6 +11614,7 @@ async function dispatchActivityDelete(action, options, deps) {
11206
11614
  touchedIds,
11207
11615
  scheduledIds,
11208
11616
  sirDetection: "after-diff",
11617
+ preview: options.preview,
11209
11618
  links: buildLinkDeletions(beforeLinks),
11210
11619
  trackingEvents: [trackingEvent],
11211
11620
  hoursPerDay: sector.hoursPerDay,
@@ -11366,6 +11775,7 @@ async function dispatchActivityBatch(action, options, deps) {
11366
11775
  links: buildLinkChangesForBatch(appliedLinks, beforeLinks, deps.adapter),
11367
11776
  hoursPerDay: deps.sector.hoursPerDay,
11368
11777
  correlativeShifts: existingCorrelativeShifts,
11778
+ preview: options.preview,
11369
11779
  ...action.mode === "replace" ? { sirDetection: "after-diff" } : {}
11370
11780
  });
11371
11781
  return {
@@ -11663,184 +12073,331 @@ function stringKeyedLinks(links) {
11663
12073
  return new Map(Array.from(links, ([linkId, link]) => [String(linkId), link]));
11664
12074
  }
11665
12075
 
11666
- // src/dispatch/handlers/activity-paste.ts
11667
- function resolvePasteRootDestination(destination, adapter) {
11668
- if ("afterSiblingId" in destination) {
11669
- const sibling = adapter.getActivity(destination.afterSiblingId);
11670
- if (!sibling) {
11671
- return { ok: false, reason: REJECTION_REASON.ANCHOR_SIBLING_NOT_FOUND };
11672
- }
11673
- const parentId2 = sibling.parentId == null ? ROOT_PARENT_ID : String(sibling.parentId);
11674
- 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 };
11675
12083
  }
11676
- const parentId = destination.parentId;
11677
- const siblings = collectSiblings(adapter, parentId).sort(
11678
- (a, b) => readCorrelativeId(a) - readCorrelativeId(b)
11679
- );
11680
- const before = siblings[destination.index]?.id;
11681
- return { ok: true, parentId, firstBefore: before };
11682
- }
11683
- function applyPasteDomainSemantics(overrides) {
11684
- 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
+ };
11685
12099
  return {
11686
- ...content,
11687
- progress: 0,
11688
- ponderator: 0,
11689
- usedCost: 0,
11690
- workHours: 0,
11691
- isLookahead: false
12100
+ ok: true,
12101
+ changes: {
12102
+ source: action,
12103
+ activities: [change],
12104
+ links: [],
12105
+ calendars: [],
12106
+ trackingEvents: []
12107
+ }
11692
12108
  };
11693
12109
  }
11694
- async function dispatchActivityPaste(action, options, deps) {
11695
- const { adapter, scheduler, sector } = deps;
11696
- const rootDest = resolvePasteRootDestination(action.destination, adapter);
11697
- if (!rootDest.ok) return rootDest;
11698
- const beforeSnap = await snapshotActivities(adapter, /* @__PURE__ */ new Set());
11699
- const touched = /* @__PURE__ */ new Set();
11700
- const originalToNew = /* @__PURE__ */ new Map();
11701
- const createdIds = [];
11702
- let prevRootId;
11703
- for (const input of action.activities) {
11704
- const mappedParent = originalToNew.get(input.originalParentId);
11705
- let parentId;
11706
- let afterSiblingId;
11707
- let beforeSiblingId;
11708
- if (mappedParent !== void 0) {
11709
- parentId = mappedParent;
11710
- } else {
11711
- parentId = rootDest.parentId;
11712
- if (prevRootId === void 0) {
11713
- afterSiblingId = rootDest.firstAfter;
11714
- beforeSiblingId = rootDest.firstBefore;
11715
- } else {
11716
- afterSiblingId = prevRootId;
11717
- }
12110
+
12111
+ // src/dispatch/handlers/bulk-edit.ts
12112
+ var CUSTOM_ID_COLUMN = COLUMN.CUSTOM_ID;
12113
+ async function dispatchBulkEdit(action, options, deps) {
12114
+ const { adapter, scheduler, sector } = deps;
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
12137
+ );
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));
11718
12145
  }
11719
- const core = await createActivityCore(
11720
- {
11721
- parentId,
11722
- afterSiblingId,
11723
- beforeSiblingId,
11724
- ...input.activityId ? { activityId: input.activityId } : {},
11725
- overrides: applyPasteDomainSemantics(input.overrides),
11726
- eventSource: action.eventSource
11727
- },
11728
- deps,
11729
- {
11730
- skipCorrelativeRecompute: true,
11731
- skipBeforeSnap: true,
11732
- customIdReferenceId: action.referenceActivityId
11733
- }
12146
+ const beforeLinkLags = snapshotIncomingLinkLags(adapter, edit.activityId);
12147
+ const preEditSnapshot = snapshotSingleActivity(
12148
+ adapter,
12149
+ String(edit.activityId)
11734
12150
  );
11735
- if (!core.ok) return core;
11736
- if (input.baselineSnapshot !== void 0) {
11737
- const snapshot = input.baselineSnapshot;
11738
- adapter.setActivityField(
11739
- core.newId,
11740
- "baselineSnapshot",
11741
- snapshot ? {
11742
- ...snapshot,
11743
- startDate: snapshot.startDate ? new Date(snapshot.startDate) : null,
11744
- endDate: snapshot.endDate ? new Date(snapshot.endDate) : null
11745
- } : null
11746
- );
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
+ });
11747
12170
  }
11748
- originalToNew.set(input.originalId, core.newId);
11749
- createdIds.push(core.newId);
11750
- if (mappedParent === void 0) prevRootId = core.newId;
11751
- }
11752
- const correlativeShifts = recomputeCorrelativeIds(adapter);
11753
- const createdIdSet = new Set(createdIds.map(String));
11754
- foldCorrelativeShifts(
11755
- adapter,
11756
- correlativeShifts,
11757
- createdIdSet,
11758
- beforeSnap,
11759
- touched
11760
- );
11761
- for (const createdId of createdIds) touched.add(String(createdId));
11762
- const appliedLinks = [];
11763
- for (const link of action.links) {
11764
- const newSource = originalToNew.get(link.source);
11765
- const newTarget = originalToNew.get(link.target);
11766
- if (newSource === void 0 || newTarget === void 0) continue;
11767
- const op = {
11768
- kind: "create",
11769
- source: newSource,
11770
- target: newTarget,
11771
- type: link.type,
11772
- lag: lagDaysToHours(link.lag, sector.hoursPerDay)
11773
- };
11774
- const res = applyLinkOperation(op, {
11775
- port: adapter,
11776
- newLinkId: link.linkId ? () => link.linkId : deps.linkIdGen.next
11777
- });
11778
- appliedLinks.push({
11779
- op,
11780
- finalLinkId: res.applied ? res.linkId : null,
11781
- rejected: res.applied ? null : res.rejected ?? "link_op_rejected"
11782
- });
11783
12171
  }
11784
- 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(
11785
12181
  {
11786
12182
  adapter,
11787
12183
  scheduler,
11788
- sector: deps.sector,
12184
+ sector,
11789
12185
  defaultBaseCalendarId: deps.defaultBaseCalendarId
11790
12186
  },
11791
12187
  {
11792
- action,
11793
- autoscheduleFrom: createdIds[0] ?? null,
11794
- recomputeParentsFrom: createdIds,
11795
- now: deps.now,
11796
- 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
+ )
11797
12201
  }
11798
- );
11799
- const linkChanges = buildLinkChangesForBatch(
11800
- appliedLinks,
11801
- /* @__PURE__ */ new Map(),
11802
- adapter
11803
- );
12202
+ ) : runPostMutation(
12203
+ {
12204
+ adapter,
12205
+ scheduler,
12206
+ sector,
12207
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
12208
+ },
12209
+ postMutationArgs
12210
+ ));
11804
12211
  return {
11805
12212
  ok: true,
12213
+ verdicts,
11806
12214
  changes: await assembleChangeSet(adapter, {
11807
12215
  source: action,
11808
12216
  beforeSnap,
11809
- touchedIds: touched,
12217
+ touchedIds,
11810
12218
  scheduledIds,
12219
+ sirDetection: "before-diff",
12220
+ preview: options.preview,
11811
12221
  links: linkChanges,
12222
+ trackingEvents,
12223
+ warnings,
11812
12224
  hoursPerDay: sector.hoursPerDay
11813
12225
  })
11814
12226
  };
11815
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
+ }
11816
12353
 
11817
- // src/dispatch/handlers/sir-sync.ts
11818
- function dispatchSirSync(action, deps) {
12354
+ // src/dispatch/handlers/sir-approve.ts
12355
+ async function dispatchSirApprove(action, options, deps) {
11819
12356
  const { adapter } = deps;
11820
- const id = action.activityId;
11821
- const snapshot = adapter.getActivity(id);
12357
+ const snapshot = adapter.getActivity(action.activityId);
11822
12358
  if (!snapshot) {
11823
12359
  return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
11824
12360
  }
11825
- const before = snapshot.pendingRequestIds ?? [];
11826
- const after = action.pendingRequests.map((r) => r.id);
11827
- adapter.setActivityField(action.activityId, "pendingRequestIds", after);
11828
- const change = {
11829
- id,
11830
- kind: "updated",
11831
- fields: { pendingRequestIds: { before, after } },
11832
- after: { id, pendingRequestIds: after }
11833
- };
11834
- return {
11835
- ok: true,
11836
- changes: {
11837
- source: action,
11838
- activities: [change],
11839
- links: [],
11840
- calendars: [],
11841
- trackingEvents: []
11842
- }
11843
- };
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 } };
11844
12401
  }
11845
12402
 
11846
12403
  // src/dispatch/handlers/activity-lookahead-sync.ts
@@ -11926,7 +12483,38 @@ function isAncestor(descendantId, candidateAncestorId, adapter) {
11926
12483
  if (parentKey === candidateAncestorId) return true;
11927
12484
  cur = parentKey;
11928
12485
  }
11929
- 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;
11930
12518
  }
11931
12519
 
11932
12520
  // src/dispatch/handlers/activity-move.ts
@@ -11970,6 +12558,17 @@ async function dispatchActivityMove(action, options, deps) {
11970
12558
  const oldParentKey = normalizeParentKey(oldParent);
11971
12559
  const newParentKey = String(action.parentId);
11972
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
+ }
11973
12572
  const touched = /* @__PURE__ */ new Set([action.activityId]);
11974
12573
  if (parentChanged) {
11975
12574
  if (oldParentKey !== ROOT_PARENT_ID) touched.add(oldParentKey);
@@ -12121,27 +12720,21 @@ async function dispatchActivityMove(action, options, deps) {
12121
12720
  touchedIds: touched,
12122
12721
  scheduledIds,
12123
12722
  sirDetection: "before-diff",
12723
+ preview: options.preview,
12124
12724
  trackingEvents: [trackingEvent],
12125
12725
  hoursPerDay: sector.hoursPerDay
12126
12726
  })
12127
12727
  };
12128
12728
  }
12129
12729
 
12130
- // src/dispatch/handlers/activity-indent.ts
12131
- async function dispatchActivityIndent(action, options, deps) {
12132
- const { adapter, scheduler, sector } = deps;
12133
- scheduler.invalidateAllCaches();
12134
- if (action.activityIds.length === 0) {
12135
- return { ok: false, reason: REJECTION_REASON.ACTIVITY_IDS_EMPTY };
12136
- }
12137
- const selectedSet = new Set(action.activityIds);
12138
- const touched = /* @__PURE__ */ new Set();
12730
+ // src/dispatch/shared/indent-planning.ts
12731
+ function planIndentMoves(activityIds, adapter) {
12732
+ const selectedSet = new Set(activityIds);
12139
12733
  const failed = [];
12140
- const succeededIds = [];
12141
12734
  const moves = [];
12142
12735
  const plannedIds = /* @__PURE__ */ new Set();
12143
12736
  const blockingSet = new Set(selectedSet);
12144
- const sortedIds = [...action.activityIds].sort((a, b) => {
12737
+ const sortedIds = [...activityIds].sort((a, b) => {
12145
12738
  const aSnap = adapter.getActivity(a);
12146
12739
  const bSnap = adapter.getActivity(b);
12147
12740
  const ac = aSnap ? readCorrelativeId(aSnap) : NOT_SET_SORT_VALUE2;
@@ -12176,12 +12769,43 @@ async function dispatchActivityIndent(action, options, deps) {
12176
12769
  const oldParentId = isRootParent(oldParentRaw) ? ROOT_PARENT_ID : String(oldParentRaw);
12177
12770
  moves.push({ activityId: id, newParentId: targetParentId, oldParentId });
12178
12771
  plannedIds.add(id);
12179
- touched.add(id);
12180
- touched.add(targetParentId);
12181
- if (oldParentId !== ROOT_PARENT_ID) touched.add(oldParentId);
12182
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;
12183
12807
  const beforeSnap = await snapshotActivities(adapter, touched);
12184
- for (const { activityId, newParentId } of moves) {
12808
+ for (const { activityId, newParentId } of allowedMoves) {
12185
12809
  adapter.setActivityField(activityId, ACTIVITY_PROPERTY.PARENT, newParentId);
12186
12810
  adapter.setActivityField(
12187
12811
  activityId,
@@ -12191,7 +12815,7 @@ async function dispatchActivityIndent(action, options, deps) {
12191
12815
  succeededIds.push(activityId);
12192
12816
  }
12193
12817
  const promotedParents = /* @__PURE__ */ new Set();
12194
- for (const { activityId, newParentId } of moves) {
12818
+ for (const { activityId, newParentId } of allowedMoves) {
12195
12819
  if (promotedParents.has(newParentId)) continue;
12196
12820
  promotedParents.add(newParentId);
12197
12821
  const newParent = adapter.getActivity(newParentId);
@@ -12243,7 +12867,7 @@ async function dispatchActivityIndent(action, options, deps) {
12243
12867
  releaseClearedCustomId(oldNewParentCustomId, deps.customIdTracker);
12244
12868
  }
12245
12869
  const indentAffectedParents = /* @__PURE__ */ new Set();
12246
- for (const m of moves) {
12870
+ for (const m of allowedMoves) {
12247
12871
  if (m.oldParentId !== ROOT_PARENT_ID)
12248
12872
  indentAffectedParents.add(m.oldParentId);
12249
12873
  indentAffectedParents.add(m.newParentId);
@@ -12404,8 +13028,24 @@ async function dispatchActivityOutdent(action, options, deps) {
12404
13028
  if (grandparentKey !== ROOT_PARENT_ID) touched.add(String(grandparentKey));
12405
13029
  oldParents.add(oldParentKey);
12406
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
+ );
12407
13047
  const beforeSnap = await snapshotActivities(adapter, touched);
12408
- for (const plan of planned) {
13048
+ for (const plan of allowedPlans) {
12409
13049
  adapter.setActivityField(
12410
13050
  plan.activityId,
12411
13051
  ACTIVITY_PROPERTY.PARENT,
@@ -12439,7 +13079,7 @@ async function dispatchActivityOutdent(action, options, deps) {
12439
13079
  task: parentActivity
12440
13080
  }),
12441
13081
  idsRemoved: new Set(
12442
- planned.filter((plan) => plan.oldParentKey === oldParentKey).map((plan) => plan.activityId)
13082
+ allowedPlans.filter((plan) => plan.oldParentKey === oldParentKey).map((plan) => plan.activityId)
12443
13083
  )
12444
13084
  });
12445
13085
  if (!demotion) {
@@ -12465,7 +13105,7 @@ async function dispatchActivityOutdent(action, options, deps) {
12465
13105
  for (const oldParentKey of oldParents) {
12466
13106
  outdentAffectedParents.add(String(oldParentKey));
12467
13107
  }
12468
- for (const plan of planned) {
13108
+ for (const plan of allowedPlans) {
12469
13109
  if (plan.grandparentKey !== ROOT_PARENT_ID) {
12470
13110
  outdentAffectedParents.add(String(plan.grandparentKey));
12471
13111
  }
@@ -12553,6 +13193,17 @@ async function dispatchActivitySetProgress(action, options, deps) {
12553
13193
  "[ScheduleCore] progressPipeline not registered \u2014 cannot dispatch activity-set-progress"
12554
13194
  );
12555
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
+ }
12556
13207
  const changes = pipeline.transform(
12557
13208
  activitySnap,
12558
13209
  action.newValue,
@@ -12611,11 +13262,11 @@ async function dispatchDatesBatch(action, options, deps) {
12611
13262
  const warnings = [];
12612
13263
  let needsAutoSchedule = false;
12613
13264
  for (const edit of action.edits) {
12614
- const outcome = applyOneEdit(edit, deps);
13265
+ const outcome = applyOneEdit2(edit, deps);
12615
13266
  verdicts.push(outcome.verdict);
12616
13267
  if (!outcome.verdict.ok || !outcome.changes) continue;
12617
13268
  const editTouched = collectTouchedIds(edit.activityId, outcome.changes);
12618
- mergeBeforeSnapshots(beforeSnap, editTouched, deps);
13269
+ mergeBeforeSnapshots2(beforeSnap, editTouched, deps);
12619
13270
  for (const id of editTouched) touchedIds.add(id);
12620
13271
  const beforeLinkLags = snapshotIncomingLinkLags(adapter, edit.activityId);
12621
13272
  const preEditSnapshot = snapshotSingleActivity(
@@ -12669,6 +13320,7 @@ async function dispatchDatesBatch(action, options, deps) {
12669
13320
  touchedIds,
12670
13321
  scheduledIds,
12671
13322
  sirDetection: "before-diff",
13323
+ preview: options.preview,
12672
13324
  links: linkChanges,
12673
13325
  trackingEvents,
12674
13326
  warnings,
@@ -12676,7 +13328,7 @@ async function dispatchDatesBatch(action, options, deps) {
12676
13328
  })
12677
13329
  };
12678
13330
  }
12679
- function applyOneEdit(edit, deps) {
13331
+ function applyOneEdit2(edit, deps) {
12680
13332
  const { ctx } = deps;
12681
13333
  const activityId = edit.activityId;
12682
13334
  const pipeline = findPipelineForColumn(edit.column);
@@ -12687,211 +13339,7 @@ function applyOneEdit(edit, deps) {
12687
13339
  }
12688
13340
  const activitySnap = ctx.activityReader.getActivity(edit.activityId);
12689
13341
  if (!activitySnap) {
12690
- throw new Error(`[ScheduleCore] activity "${edit.activityId}" not found`);
12691
- }
12692
- const editGate = pipeline.canEdit(activitySnap, ctx.hierarchy);
12693
- if (!editGate.allowed) {
12694
- return {
12695
- verdict: {
12696
- activityId,
12697
- ok: false,
12698
- reason: editGate.reason ?? REJECTION_REASON.CANNOT_EDIT,
12699
- ...editGate.alertKey ? { alertKey: editGate.alertKey } : {}
12700
- },
12701
- changes: null
12702
- };
12703
- }
12704
- const parsed2 = pipeline.parseInput(edit.newValue, activitySnap, ctx);
12705
- if (!parsed2.ok) {
12706
- return {
12707
- verdict: {
12708
- activityId,
12709
- ok: false,
12710
- reason: parsed2.reason ?? REJECTION_REASON.PARSE_ERROR
12711
- },
12712
- changes: null
12713
- };
12714
- }
12715
- const oldValue = Reflect.get(activitySnap, pipeline.targetField);
12716
- const validation = pipeline.validate(
12717
- activitySnap,
12718
- oldValue,
12719
- parsed2.value,
12720
- ctx
12721
- );
12722
- if (!validation.valid) {
12723
- return {
12724
- verdict: {
12725
- activityId,
12726
- ok: false,
12727
- reason: validation.reason ?? REJECTION_REASON.INVALID,
12728
- ...validation.notification?.messageKey ? { alertKey: validation.notification.messageKey } : {}
12729
- },
12730
- changes: null
12731
- };
12732
- }
12733
- const changes = pipeline.transform(
12734
- activitySnap,
12735
- parsed2.value,
12736
- ctx,
12737
- parsed2.raw
12738
- );
12739
- return { verdict: { activityId, ok: true }, changes };
12740
- }
12741
- function mergeBeforeSnapshots(beforeSnap, ids, deps) {
12742
- const missing = /* @__PURE__ */ new Set();
12743
- for (const id of ids) {
12744
- if (!beforeSnap.has(id)) missing.add(id);
12745
- }
12746
- if (missing.size === 0) return;
12747
- for (const missingId of missing) {
12748
- const liveActivity = deps.adapter.getActivity(missingId);
12749
- if (liveActivity) {
12750
- beforeSnap.set(missingId, structuredCloneActivity(liveActivity));
12751
- }
12752
- }
12753
- }
12754
-
12755
- // src/dispatch/handlers/bulk-edit.ts
12756
- var CUSTOM_ID_COLUMN = COLUMN.CUSTOM_ID;
12757
- async function dispatchBulkEdit(action, options, deps) {
12758
- const { adapter, scheduler, sector } = deps;
12759
- const editsIncludeDuration = action.edits.some(
12760
- (edit) => edit.column === COLUMN.DURATION
12761
- );
12762
- if (editsIncludeDuration) {
12763
- scheduler.invalidateExpandedLinksCache();
12764
- }
12765
- const verdicts = [];
12766
- const beforeSnap = /* @__PURE__ */ new Map();
12767
- const constraintPriorsByActivity = /* @__PURE__ */ new Map();
12768
- const explicitDateEditActivities = /* @__PURE__ */ new Set();
12769
- const touchedIds = /* @__PURE__ */ new Set();
12770
- const trackingEvents = [];
12771
- const linkChanges = [];
12772
- const warnings = [];
12773
- let needsAutoSchedule = false;
12774
- for (const edit of action.edits) {
12775
- const outcome = applyOneEdit2(edit, deps);
12776
- verdicts.push(outcome.verdict);
12777
- if (!outcome.verdict.ok || !outcome.changes) continue;
12778
- const constraintWarning = toConstraintWarning(
12779
- edit.activityId,
12780
- outcome.changes.constraintWarning
12781
- );
12782
- if (constraintWarning) warnings.push(constraintWarning);
12783
- const editTouched = collectTouchedIds(edit.activityId, outcome.changes);
12784
- mergeBeforeSnapshots2(beforeSnap, editTouched, deps);
12785
- for (const touchedId of editTouched) touchedIds.add(touchedId);
12786
- recordConstraintPrior(constraintPriorsByActivity, edit, beforeSnap);
12787
- if (isExplicitConstraintDateEdit(edit.column)) {
12788
- explicitDateEditActivities.add(String(edit.activityId));
12789
- }
12790
- const beforeLinkLags = snapshotIncomingLinkLags(adapter, edit.activityId);
12791
- const preEditSnapshot = snapshotSingleActivity(
12792
- adapter,
12793
- String(edit.activityId)
12794
- );
12795
- const customIdBeforeApply = readLiveCustomId(edit, deps);
12796
- const changes = outcome.changes;
12797
- applyFieldChanges(adapter, edit.activityId, changes);
12798
- runPostProcessorsOnAdapter(
12799
- edit.activityId,
12800
- outcome.changes.postProcessors ?? [],
12801
- adapter,
12802
- preEditSnapshot
12803
- );
12804
- syncCustomIdTracker(edit, customIdBeforeApply, deps);
12805
- linkChanges.push(
12806
- ...diffIncomingLinkLagChanges(beforeLinkLags, adapter, edit.activityId)
12807
- );
12808
- needsAutoSchedule ||= Boolean(outcome.changes.needsAutoSchedule);
12809
- for (const trackingEvent of outcome.changes.trackingEvents ?? []) {
12810
- trackingEvents.push({
12811
- name: trackingEvent.name,
12812
- properties: trackingEvent.properties ?? {}
12813
- });
12814
- }
12815
- }
12816
- const postMutationArgs = {
12817
- action,
12818
- autoscheduleFrom: needsAutoSchedule ? "roots" : null,
12819
- recomputeParentsFrom: touchedIds,
12820
- now: deps.now,
12821
- options
12822
- };
12823
- const batchHasConstraintEdits = constraintPriorsByActivity.size > 0;
12824
- const { scheduledIds } = await (batchHasConstraintEdits ? settleConstraintEdits(
12825
- {
12826
- adapter,
12827
- scheduler,
12828
- sector,
12829
- defaultBaseCalendarId: deps.defaultBaseCalendarId
12830
- },
12831
- {
12832
- postMutationArgs,
12833
- revertTargets: [...constraintPriorsByActivity].filter(
12834
- ([activityId]) => !explicitDateEditActivities.has(activityId)
12835
- ).map(([activityId, priorConstraintDate]) => ({
12836
- activityId,
12837
- priorConstraintDate
12838
- })),
12839
- revert: (activityId, priorConstraintDate) => revertNoOpConstraintEdit(
12840
- activityId,
12841
- priorConstraintDate,
12842
- adapter,
12843
- deps.ctx.calendars
12844
- )
12845
- }
12846
- ) : runPostMutation(
12847
- {
12848
- adapter,
12849
- scheduler,
12850
- sector,
12851
- defaultBaseCalendarId: deps.defaultBaseCalendarId
12852
- },
12853
- postMutationArgs
12854
- ));
12855
- return {
12856
- ok: true,
12857
- verdicts,
12858
- changes: await assembleChangeSet(adapter, {
12859
- source: action,
12860
- beforeSnap,
12861
- touchedIds,
12862
- scheduledIds,
12863
- sirDetection: "before-diff",
12864
- links: linkChanges,
12865
- trackingEvents,
12866
- warnings,
12867
- hoursPerDay: sector.hoursPerDay
12868
- })
12869
- };
12870
- }
12871
- function applyOneEdit2(edit, deps) {
12872
- const { ctx } = deps;
12873
- const activityId = edit.activityId;
12874
- const pipeline = findPipelineForColumn(edit.column);
12875
- if (!pipeline) {
12876
- return {
12877
- verdict: {
12878
- activityId,
12879
- ok: false,
12880
- reason: REJECTION_REASON.NO_PIPELINE_FOR_COLUMN
12881
- },
12882
- changes: null
12883
- };
12884
- }
12885
- const activitySnap = ctx.activityReader.getActivity(edit.activityId);
12886
- if (!activitySnap) {
12887
- return {
12888
- verdict: {
12889
- activityId,
12890
- ok: false,
12891
- reason: REJECTION_REASON.ACTIVITY_NOT_FOUND
12892
- },
12893
- changes: null
12894
- };
13342
+ throw new Error(`[ScheduleCore] activity "${edit.activityId}" not found`);
12895
13343
  }
12896
13344
  const editGate = pipeline.canEdit(activitySnap, ctx.hierarchy);
12897
13345
  if (!editGate.allowed) {
@@ -12934,21 +13382,6 @@ function applyOneEdit2(edit, deps) {
12934
13382
  changes: null
12935
13383
  };
12936
13384
  }
12937
- if (edit.column === CUSTOM_ID_COLUMN && parsed2.value !== null) {
12938
- const liveActivity = deps.adapter.getActivity(edit.activityId);
12939
- const oldCustomIdRaw = liveActivity ? liveActivity.customId : null;
12940
- const oldCustomId = typeof oldCustomIdRaw === "string" ? oldCustomIdRaw : null;
12941
- if (deps.customIdTracker.isCustomIdInUse(String(parsed2.value), oldCustomId)) {
12942
- return {
12943
- verdict: {
12944
- activityId,
12945
- ok: false,
12946
- reason: REJECTION_REASON.CUSTOM_ID_DUPLICATE
12947
- },
12948
- changes: null
12949
- };
12950
- }
12951
- }
12952
13385
  const changes = pipeline.transform(
12953
13386
  activitySnap,
12954
13387
  parsed2.value,
@@ -12957,33 +13390,10 @@ function applyOneEdit2(edit, deps) {
12957
13390
  );
12958
13391
  return { verdict: { activityId, ok: true }, changes };
12959
13392
  }
12960
- function readLiveCustomId(edit, deps) {
12961
- if (edit.column !== CUSTOM_ID_COLUMN) return null;
12962
- const liveActivity = deps.adapter.getActivity(edit.activityId);
12963
- const rawValue = liveActivity ? liveActivity.customId : null;
12964
- return typeof rawValue === "string" ? rawValue : null;
12965
- }
12966
- function syncCustomIdTracker(edit, oldCustomId, deps) {
12967
- if (edit.column !== CUSTOM_ID_COLUMN) return;
12968
- const liveActivity = deps.adapter.getActivity(edit.activityId);
12969
- const rawValue = liveActivity ? liveActivity.customId : null;
12970
- const newCustomId = typeof rawValue === "string" ? rawValue : null;
12971
- if (oldCustomId !== newCustomId) {
12972
- deps.customIdTracker.trackCustomIdChange(oldCustomId, newCustomId);
12973
- }
12974
- }
12975
- function recordConstraintPrior(priors, edit, beforeSnap) {
12976
- if (!isConstraintEditColumn(edit.column)) return;
12977
- const activityKey = String(edit.activityId);
12978
- if (priors.has(activityKey)) return;
12979
- const preBatchSnapshot = beforeSnap.get(activityKey);
12980
- if (!preBatchSnapshot) return;
12981
- priors.set(activityKey, preBatchSnapshot.constraintDate ?? null);
12982
- }
12983
13393
  function mergeBeforeSnapshots2(beforeSnap, ids, deps) {
12984
13394
  const missing = /* @__PURE__ */ new Set();
12985
- for (const candidateId of ids) {
12986
- if (!beforeSnap.has(candidateId)) missing.add(candidateId);
13395
+ for (const id of ids) {
13396
+ if (!beforeSnap.has(id)) missing.add(id);
12987
13397
  }
12988
13398
  if (missing.size === 0) return;
12989
13399
  for (const missingId of missing) {
@@ -13046,9 +13456,33 @@ function assertValidAssignments(assignments, entityName, readCurrent, ownerByBac
13046
13456
  );
13047
13457
  }
13048
13458
  }
13049
- 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) {
13050
13482
  const activities = action.activities ?? [];
13051
13483
  const links = action.links ?? [];
13484
+ const reconciledProgress = action.reconciledProgress ?? [];
13485
+ assertValidReconciledProgress(reconciledProgress, deps);
13052
13486
  const activityOwnerByBackendId = /* @__PURE__ */ new Map();
13053
13487
  for (const activity of deps.adapter.getAllActivities()) {
13054
13488
  if (activity.proplannerId != null) {
@@ -13084,7 +13518,49 @@ function dispatchPersistenceAcknowledge(action, deps) {
13084
13518
  );
13085
13519
  const activityChanges = [];
13086
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
+ );
13087
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
+ }
13088
13564
  for (const assignment of activities) {
13089
13565
  const before = deps.adapter.getActivity(assignment.id);
13090
13566
  if (!before || before.proplannerId === assignment.proplannerId) continue;
@@ -13491,8 +13967,218 @@ async function dispatchStatusCriteriaSet(action, deps) {
13491
13967
  };
13492
13968
  }
13493
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
+
13494
14168
  // src/dispatch/dispatch.ts
13495
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
+ }
13496
14182
  if (action.kind === "activity-batch") {
13497
14183
  return dispatchActivityBatch(action, options, deps);
13498
14184
  }
@@ -13517,9 +14203,6 @@ async function performDispatch(action, options, deps) {
13517
14203
  if (action.kind === "activity-create") {
13518
14204
  return dispatchActivityCreate(action, options, deps);
13519
14205
  }
13520
- if (action.kind === "activity-paste") {
13521
- return dispatchActivityPaste(action, options, deps);
13522
- }
13523
14206
  if (action.kind === "activity-delete") {
13524
14207
  return dispatchActivityDelete(action, options, deps);
13525
14208
  }
@@ -13562,6 +14245,9 @@ async function performDispatch(action, options, deps) {
13562
14245
  if (action.kind === "sir-sync") {
13563
14246
  return dispatchSirSync(action, deps);
13564
14247
  }
14248
+ if (action.kind === "sir-approve") {
14249
+ return dispatchSirApprove(action, options, deps);
14250
+ }
13565
14251
  if (action.kind === "activity-lookahead-sync") {
13566
14252
  return dispatchActivityLookaheadSync(action, deps);
13567
14253
  }
@@ -16974,6 +17660,7 @@ var ScheduleState = class {
16974
17660
  if (!activity) return;
16975
17661
  this._writeCapture.note(String(activityId), activity);
16976
17662
  activity[field] = value;
17663
+ refreshCalendarDurationIfDateBearing(activity, field);
16977
17664
  if (field === ACTIVITY_PROPERTY.PARENT) this._hierarchy.markDirty();
16978
17665
  if (fieldAffectsVisualOrder(field)) this._cache.visualOrderIds = null;
16979
17666
  }
@@ -16987,6 +17674,7 @@ var ScheduleState = class {
16987
17674
  }
16988
17675
  this._writeCapture.note(String(activityId), activity);
16989
17676
  Reflect.set(activity, field, coerceActivityFieldValue(field, value));
17677
+ refreshCalendarDurationIfDateBearing(activity, field);
16990
17678
  if (field === ACTIVITY_PROPERTY.PARENT) this._hierarchy.markDirty();
16991
17679
  if (fieldAffectsVisualOrder(field)) this._cache.visualOrderIds = null;
16992
17680
  }
@@ -16997,6 +17685,7 @@ var ScheduleState = class {
16997
17685
  let reparented = false;
16998
17686
  for (const field of Object.keys(fields)) {
16999
17687
  Reflect.set(activity, field, Reflect.get(fields, field));
17688
+ refreshCalendarDurationIfDateBearing(activity, field);
17000
17689
  if (field === ACTIVITY_PROPERTY.PARENT) reparented = true;
17001
17690
  if (fieldAffectsVisualOrder(field)) this._cache.visualOrderIds = null;
17002
17691
  }
@@ -17165,6 +17854,12 @@ function isMutableLinkField(field) {
17165
17854
  function fieldAffectsVisualOrder(field) {
17166
17855
  return field === ACTIVITY_PROPERTY.PARENT || field === ACTIVITY_PROPERTY.CORRELATIVE_ID;
17167
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
+ }
17168
17863
  function detachLinkRef(refs, linkId) {
17169
17864
  return refs.filter((ref) => String(ref) !== String(linkId));
17170
17865
  }
@@ -17212,15 +17907,7 @@ function readChildren(state, parentId) {
17212
17907
  return out;
17213
17908
  }
17214
17909
  function readChildrenIds(state, parentId) {
17215
- const key = parentId;
17216
- if (key === ROOT_PARENT_ID) {
17217
- const roots = [];
17218
- state.forEachActivity((activity, id) => {
17219
- if (isRootParent(activity.parentId)) roots.push(id);
17220
- });
17221
- return roots;
17222
- }
17223
- return getChildrenInVisualOrder(key, state);
17910
+ return getChildrenInVisualOrder(parentId, state);
17224
17911
  }
17225
17912
  function readSelectedActivityIds(state) {
17226
17913
  return state.checkedIds().map(String);
@@ -17284,7 +17971,11 @@ function buildPipelineContext(adapter, options) {
17284
17971
  dateFormat: options.dateFormat ?? "YYYY-MM-DD",
17285
17972
  customHours: options.customHours ?? {},
17286
17973
  hoursPerDay: options.hoursPerDay,
17287
- 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 } : {}
17288
17979
  };
17289
17980
  }
17290
17981
 
@@ -17364,9 +18055,10 @@ function mergeCoalesced(top, next) {
17364
18055
  };
17365
18056
  return buildUndoEntry(changeSet);
17366
18057
  }
17367
- function getDispatchHistoryPolicy(action) {
18058
+ function getDispatchHistoryPolicy(action, options) {
18059
+ if (options?.preview === true) return "skip";
17368
18060
  if (action.kind === "persistence-acknowledge") return "clear-on-success";
17369
- 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") {
17370
18062
  return "skip";
17371
18063
  }
17372
18064
  return "record";
@@ -18150,8 +18842,13 @@ function parseActivity(raw, context) {
18150
18842
  Number(raw.sumOfDurationRecursively ?? 0),
18151
18843
  context.hoursPerDay
18152
18844
  ),
18845
+ calendarDuration: computeCalendarDuration({
18846
+ type: raw.type,
18847
+ startDate,
18848
+ endDate
18849
+ }),
18153
18850
  isCritical: Boolean(raw.is_critical),
18154
- freeSlackHours: raw.freeSlack,
18851
+ freeSlackHours: null,
18155
18852
  expectedProgress: null,
18156
18853
  expectedProgressBaseline: null,
18157
18854
  status: null,
@@ -18514,7 +19211,8 @@ function parseSector(sector) {
18514
19211
  ...customIdIncrement !== void 0 ? { customIdIncrement } : {},
18515
19212
  ...isPrimaveraEndDate ? { updateDurationForPrimaveraEndDate: true } : {},
18516
19213
  activityCreter: parseActivityCreter(sector.activity_creter),
18517
- statusCriteria: DEFAULT_STATUS_CRITERIA
19214
+ statusCriteria: DEFAULT_STATUS_CRITERIA,
19215
+ ganttId: null
18518
19216
  };
18519
19217
  }
18520
19218
 
@@ -18600,7 +19298,7 @@ function createActivityIdGenerator(options = {}) {
18600
19298
 
18601
19299
  // src/generators/link-id-generator.ts
18602
19300
  function createLinkIdGenerator(options = {}) {
18603
- let counter = options.seed ?? Date.now();
19301
+ let counter = options.seed ?? 0;
18604
19302
  const raise = (value) => {
18605
19303
  if (value > counter) counter = value;
18606
19304
  };
@@ -19306,6 +20004,7 @@ function recomputeSkippedLeafEnds(state) {
19306
20004
  durationHours: activity.durationHours,
19307
20005
  task: activity
19308
20006
  });
20007
+ activity.calendarDuration = computeCalendarDuration(activity);
19309
20008
  });
19310
20009
  }
19311
20010
  function recomputeDurationsFromDates(state) {
@@ -19362,7 +20061,8 @@ var SCHEDULE_CORE_STATUS = {
19362
20061
  };
19363
20062
  function initializeCore(input) {
19364
20063
  const clock = input.clock ?? null;
19365
- const loadNow = clock ? endOfLocalDay(clock()) : null;
20064
+ const initializedAt = clock?.() ?? null;
20065
+ const loadNow = initializedAt ? endOfLocalDay(initializedAt) : null;
19366
20066
  const reporter = input.reporter ?? noopReporter;
19367
20067
  const parsed2 = parseFromBackend(
19368
20068
  {
@@ -19376,6 +20076,7 @@ function initializeCore(input) {
19376
20076
  );
19377
20077
  const sector = parsed2.sector;
19378
20078
  sector.statusCriteria = resolveStatusCriteria(input.statusCriteria);
20079
+ sector.ganttId = input.ganttId ?? null;
19379
20080
  const calendars = parsed2.calendars;
19380
20081
  const baseCalendars = parsed2.baseCalendars ?? [];
19381
20082
  const snapshot = buildStateSnapshot(
@@ -19393,6 +20094,7 @@ function initializeCore(input) {
19393
20094
  knownIds: parsed2.activities.map((a) => a.id)
19394
20095
  });
19395
20096
  const linkIdGen = createLinkIdGenerator({
20097
+ seed: initializedAt?.getTime() ?? 0,
19396
20098
  knownIds: parsed2.links.map((l) => l.id)
19397
20099
  });
19398
20100
  const uidGen = createUniqueCorrelativeIdGenerator({
@@ -19581,6 +20283,7 @@ function evaluateEnd(formula, proposedEdgeTimeMs) {
19581
20283
  }
19582
20284
 
19583
20285
  // src/init/schedule-core.ts
20286
+ var PREVIEW_DISPATCH_KINDS = /* @__PURE__ */ new Set(["inline-edit", "bulk-edit", "dates-batch"]);
19584
20287
  function changesRequireFilterProjectionRebuild(changes) {
19585
20288
  for (const change of changes.activities) {
19586
20289
  if (change.kind === "deleted") return true;
@@ -19605,6 +20308,7 @@ function isCurrentFilterProjection(projection, filter, context) {
19605
20308
  return hasCurrentHours && hasCurrentLocale;
19606
20309
  }
19607
20310
  var ScheduleCore = class {
20311
+ _activityClipboard = { value: null };
19608
20312
  _status = SCHEDULE_CORE_STATUS.READY;
19609
20313
  coreRuntime;
19610
20314
  _undo = new UndoRecorder();
@@ -19700,6 +20404,23 @@ var ScheduleCore = class {
19700
20404
  this.assertReady();
19701
20405
  return readAllActivities(this.coreRuntime.state);
19702
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
+ }
19703
20424
  getChildrenView(parentId) {
19704
20425
  this.assertReady();
19705
20426
  return readChildren(this.coreRuntime.state, parentId);
@@ -19712,6 +20433,26 @@ var ScheduleCore = class {
19712
20433
  this.assertReady();
19713
20434
  return readAllLinks(this.coreRuntime.state);
19714
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
+ }
19715
20456
  getProperty(activityId, property) {
19716
20457
  this.assertReady();
19717
20458
  const snapshot = this.coreRuntime.state.getActivity(activityId);
@@ -19740,11 +20481,11 @@ var ScheduleCore = class {
19740
20481
  }
19741
20482
  getActiveFilter() {
19742
20483
  this.assertReady();
19743
- return this.coreRuntime.state.getActiveFilter();
20484
+ return cloneDomainValue(this.coreRuntime.state.getActiveFilter());
19744
20485
  }
19745
20486
  getActiveOrder() {
19746
20487
  this.assertReady();
19747
- return this.coreRuntime.state.getActiveOrder();
20488
+ return cloneDomainValue(this.coreRuntime.state.getActiveOrder());
19748
20489
  }
19749
20490
  getVisualOrderIds() {
19750
20491
  this.assertReady();
@@ -19795,6 +20536,74 @@ var ScheduleCore = class {
19795
20536
  this.assertReady();
19796
20537
  return this._saveTracker.hasUnsavedChanges();
19797
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
+ }
19798
20607
  getDeletedActivitiesSinceLastSave() {
19799
20608
  this.assertReady();
19800
20609
  return this._saveTracker.deletedActivities(this.coreRuntime.state.getAllActivities()).map(cloneDomainValue);
@@ -19877,11 +20686,11 @@ var ScheduleCore = class {
19877
20686
  });
19878
20687
  }
19879
20688
  async dispatch(action, options = {}) {
19880
- const admitted = this._beginScheduleMutation(action);
20689
+ const admitted = options.preview === true ? false : this._beginScheduleMutation(action);
19881
20690
  return this._enqueue(() => this._dispatchInner(action, options, admitted));
19882
20691
  }
19883
20692
  async applyActivityBatch(action, options = {}) {
19884
- const admitted = this._beginScheduleMutation(action);
20693
+ const admitted = options.preview === true ? false : this._beginScheduleMutation(action);
19885
20694
  return this._enqueue(() => this._dispatchInner(action, options, admitted));
19886
20695
  }
19887
20696
  async _dispatchInner(action, options = {}, admitted = false) {
@@ -19896,12 +20705,23 @@ var ScheduleCore = class {
19896
20705
  }
19897
20706
  async _runDispatch(action, options) {
19898
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
+ }
19899
20717
  const pipelineContext = buildPipelineContext(this.coreRuntime.state, {
19900
20718
  hoursPerDay: this.coreRuntime.sector.hoursPerDay,
19901
20719
  dateFormat: this.coreRuntime.sector.dateFormat,
19902
- 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 } : {}
19903
20723
  });
19904
- const historyPolicy = getDispatchHistoryPolicy(action);
20724
+ const historyPolicy = getDispatchHistoryPolicy(action, options);
19905
20725
  const generatorSnapshot = [
19906
20726
  this.coreRuntime.activityIdGenerator.snapshot(),
19907
20727
  this.coreRuntime.linkIdGenerator.snapshot(),
@@ -19922,6 +20742,7 @@ var ScheduleCore = class {
19922
20742
  uidGen: this.coreRuntime.uniqueCorrelativeIdGenerator,
19923
20743
  customIdTracker: this.coreRuntime.customIdTracker,
19924
20744
  reporter: this.coreRuntime.reporter,
20745
+ activityClipboard: this._activityClipboard,
19925
20746
  defaultBaseCalendarId: this.coreRuntime.baseCalendars.find(
19926
20747
  (calendar) => calendar.baseDefault
19927
20748
  )?.id ?? null,
@@ -19964,7 +20785,7 @@ var ScheduleCore = class {
19964
20785
  action.activityIds,
19965
20786
  (activityId) => this.coreRuntime.state.getActivity(activityId)
19966
20787
  );
19967
- } else {
20788
+ } else if (options.preview !== true) {
19968
20789
  this._saveTracker.recordChanges(
19969
20790
  result.changes.activities,
19970
20791
  result.changes.links,
@@ -19975,6 +20796,20 @@ var ScheduleCore = class {
19975
20796
  }
19976
20797
  result = this._withReappliedViewState(action, result);
19977
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
+ }
19978
20813
  if (dispatchChangesSchedulingState(action) && changeSetIsSubstantive(result.changes)) {
19979
20814
  this._recordScheduleMutation(
19980
20815
  options.skipAutoSchedule !== true && options.skipCriticalPath !== true && resolveRunCriticalPath(action)
@@ -20489,6 +21324,9 @@ var DEFAULT_HOURS_PER_DAY = 8;
20489
21324
 
20490
21325
  // src/dispatch/action-kinds.ts
20491
21326
  var DISPATCH_ACTION_KIND = {
21327
+ ACTIVITY_CLIPBOARD_CLEAR: "activity-clipboard-clear",
21328
+ ACTIVITY_COPY: "activity-copy",
21329
+ ACTIVITY_PASTE_COPIED: "activity-paste-copied",
20492
21330
  PERSISTENCE_ACKNOWLEDGE: "persistence-acknowledge",
20493
21331
  BASELINE_APPLY: "baseline-apply",
20494
21332
  PONDERATOR_CRITERION_SET: "ponderator-criterion-set",
@@ -20506,16 +21344,19 @@ var DISPATCH_ACTION_KIND = {
20506
21344
  ACTIVITY_INDENT: "activity-indent",
20507
21345
  ACTIVITY_OUTDENT: "activity-outdent",
20508
21346
  ACTIVITY_SET_PROGRESS: "activity-set-progress",
20509
- ACTIVITY_PASTE: "activity-paste",
20510
21347
  SELECTION_TOGGLE: "selection-toggle",
20511
21348
  SELECTION_REPLACE: "selection-replace",
20512
21349
  VISIBILITY_SET: "visibility-set",
20513
21350
  FILTER_SET: "filter-set",
20514
21351
  SORT_SET: "sort-set",
20515
21352
  SIR_SYNC: "sir-sync",
21353
+ SIR_APPROVE: "sir-approve",
20516
21354
  ACTIVITY_LOOKAHEAD_SYNC: "activity-lookahead-sync"
20517
21355
  };
20518
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,
20519
21360
  [DISPATCH_ACTION_KIND.PERSISTENCE_ACKNOWLEDGE]: true,
20520
21361
  [DISPATCH_ACTION_KIND.BASELINE_APPLY]: true,
20521
21362
  [DISPATCH_ACTION_KIND.PONDERATOR_CRITERION_SET]: true,
@@ -20533,13 +21374,13 @@ var KIND_CATALOG_COVERS_UNION = {
20533
21374
  [DISPATCH_ACTION_KIND.ACTIVITY_INDENT]: true,
20534
21375
  [DISPATCH_ACTION_KIND.ACTIVITY_OUTDENT]: true,
20535
21376
  [DISPATCH_ACTION_KIND.ACTIVITY_SET_PROGRESS]: true,
20536
- [DISPATCH_ACTION_KIND.ACTIVITY_PASTE]: true,
20537
21377
  [DISPATCH_ACTION_KIND.SELECTION_TOGGLE]: true,
20538
21378
  [DISPATCH_ACTION_KIND.SELECTION_REPLACE]: true,
20539
21379
  [DISPATCH_ACTION_KIND.VISIBILITY_SET]: true,
20540
21380
  [DISPATCH_ACTION_KIND.FILTER_SET]: true,
20541
21381
  [DISPATCH_ACTION_KIND.SORT_SET]: true,
20542
21382
  [DISPATCH_ACTION_KIND.SIR_SYNC]: true,
21383
+ [DISPATCH_ACTION_KIND.SIR_APPROVE]: true,
20543
21384
  [DISPATCH_ACTION_KIND.ACTIVITY_LOOKAHEAD_SYNC]: true
20544
21385
  };
20545
21386
  var DISPATCH_ACTION_KINDS = Object.keys(KIND_CATALOG_COVERS_UNION);
@@ -20568,6 +21409,8 @@ exports.LINK_TYPE_CODE = LINK_TYPE_CODE;
20568
21409
  exports.NEW_ACTIVITY_DEFAULTS = NEW_ACTIVITY_DEFAULTS;
20569
21410
  exports.REJECTION_REASON = REJECTION_REASON;
20570
21411
  exports.ROOT_PARENT_ID = ROOT_PARENT_ID;
21412
+ exports.SCHEDULE_CORE_STATUS = SCHEDULE_CORE_STATUS;
21413
+ exports.STATUS = STATUS;
20571
21414
  exports.ScheduleCore = ScheduleCore;
20572
21415
  exports.WORK_TIME_DIRECTION = WORK_TIME_DIRECTION;
20573
21416
  exports.checkNoUpdatedLinks = checkNoUpdatedLinks;