@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.js CHANGED
@@ -132,13 +132,19 @@ var REJECTION_REASON = {
132
132
  ACTIVITY_NOT_FOUND: "activity_not_found",
133
133
  PARENT_NOT_FOUND: "parent_not_found",
134
134
  ACTIVITY_IDS_EMPTY: "activity_ids_empty",
135
+ NO_ACTIVITY_SELECTION: "no_activity_selection",
136
+ CLIPBOARD_EMPTY: "clipboard_empty",
137
+ ACTIVITY_PASTE_INVARIANT_FAILED: "activity_paste_invariant_failed",
135
138
  ANCHOR_SIBLING_CONFLICT: "anchor_sibling_conflict",
136
139
  ANCHOR_SIBLING_NOT_FOUND: "anchor_sibling_not_found",
137
140
  ANCHOR_SIBLING_WRONG_PARENT: "anchor_sibling_wrong_parent",
138
141
  PARENT_FROZEN_BY_SIR: "parent_frozen_by_sir",
142
+ SIR_REQUEST_NOT_PENDING: "sir_request_not_pending",
143
+ PREVIEW_UNSUPPORTED_ACTION: "preview_unsupported_action",
139
144
  CANNOT_MOVE_INTO_OWN_DESCENDANT: "cannot_move_into_own_descendant",
140
145
  INDENT_NO_ELIGIBLE_SIBLING: "indent_no_eligible_sibling",
141
146
  OUTDENT_ROOT_LEVEL_NOT_EDITABLE: "outdent_root_level_not_editable",
147
+ HIERARCHY_CYCLE: "hierarchy_cycle",
142
148
  INVALID_PROGRESS_VALUE: "invalid_progress_value_must_be_0_or_100"
143
149
  };
144
150
 
@@ -267,8 +273,17 @@ function toDays(hours, hoursPerDay) {
267
273
  return hours / hoursPerDay;
268
274
  }
269
275
  function calendarDaySpan(startDate, endDate) {
270
- const elapsed = endDate.getTime() - startDate.getTime();
271
- return Math.floor(elapsed / MILLISECONDS_PER_DAY) + 1;
276
+ const startDay = Date.UTC(
277
+ startDate.getUTCFullYear(),
278
+ startDate.getUTCMonth(),
279
+ startDate.getUTCDate()
280
+ );
281
+ const endDay = Date.UTC(
282
+ endDate.getUTCFullYear(),
283
+ endDate.getUTCMonth(),
284
+ endDate.getUTCDate()
285
+ );
286
+ return Math.floor((endDay - startDay) / MILLISECONDS_PER_DAY) + 1;
272
287
  }
273
288
  var FIELD_REGISTRY = {
274
289
  name: { valueKind: "string", extract: (activity) => activity.name },
@@ -283,7 +298,10 @@ var FIELD_REGISTRY = {
283
298
  },
284
299
  uniqueCorrelativeId: {
285
300
  valueKind: "number",
286
- extract: (activity) => Number(activity.uniqueCorrelativeId)
301
+ extract: (activity) => {
302
+ const value = Number(activity.uniqueCorrelativeId);
303
+ return Number.isFinite(value) ? value : null;
304
+ }
287
305
  },
288
306
  progress: { valueKind: "number", extract: (activity) => activity.progress },
289
307
  durationDays: {
@@ -292,7 +310,7 @@ var FIELD_REGISTRY = {
292
310
  },
293
311
  calendarDuration: {
294
312
  valueKind: "number",
295
- extract: (activity) => calendarDaySpan(activity.startDate, activity.endDate)
313
+ extract: (activity) => activity.type === "milestone" ? 0 : calendarDaySpan(activity.startDate, activity.endDate)
296
314
  },
297
315
  cost: { valueKind: "number", extract: (activity) => activity.cost },
298
316
  usedCost: { valueKind: "number", extract: (activity) => activity.usedCost },
@@ -658,34 +676,102 @@ var OPERATORS_BY_KIND = {
658
676
  boolean: /* @__PURE__ */ new Set(["someOf", "notSomeOf"]),
659
677
  reference: /* @__PURE__ */ new Set(["someOf", "notSomeOf"])
660
678
  };
661
- function validateCriterion(criterion) {
679
+ function normalizeDateRange(value) {
680
+ if (value === void 0) return { ok: true, dateRange: void 0 };
681
+ if (typeof value !== "object" || value === null) {
682
+ return { ok: false, reason: "invalid_value" };
683
+ }
684
+ const { start, end } = value;
685
+ if (!(start instanceof Date) || !(end instanceof Date)) {
686
+ return { ok: false, reason: "invalid_value" };
687
+ }
688
+ const startTime = start.getTime();
689
+ const endTime = end.getTime();
690
+ if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || startTime > endTime) {
691
+ return { ok: false, reason: "invalid_value" };
692
+ }
693
+ return {
694
+ ok: true,
695
+ dateRange: { start: new Date(startTime), end: new Date(endTime) }
696
+ };
697
+ }
698
+ function normalizeCriterion(criterion) {
662
699
  const descriptor = getFieldDescriptor(criterion.field);
663
- if (descriptor === null) return "unknown_field";
700
+ if (descriptor === null) return { ok: false, reason: "unknown_field" };
664
701
  if (!OPERATORS_BY_KIND[descriptor.valueKind].has(criterion.operator)) {
665
- return "unknown_operator";
702
+ return { ok: false, reason: "unknown_operator" };
703
+ }
704
+ if (descriptor.valueKind === "number") {
705
+ const value = normalizeNumberValue(criterion.value);
706
+ return value === null ? { ok: false, reason: "invalid_value" } : {
707
+ ok: true,
708
+ criterion: { ...criterion, value }
709
+ };
710
+ }
711
+ if (descriptor.valueKind === "date") {
712
+ const value = normalizeDateValue(criterion.value);
713
+ return value === null ? { ok: false, reason: "invalid_value" } : {
714
+ ok: true,
715
+ criterion: { ...criterion, value }
716
+ };
717
+ }
718
+ const rejection = validateValueForKind(criterion, descriptor.valueKind);
719
+ if (rejection !== null) return { ok: false, reason: rejection };
720
+ if (descriptor.valueKind === "boolean") {
721
+ return {
722
+ ok: true,
723
+ criterion: {
724
+ ...criterion,
725
+ value: [...new Set(criterion.value)]
726
+ }
727
+ };
728
+ }
729
+ if (descriptor.valueKind === "id-array" || descriptor.valueKind === "enum" || descriptor.valueKind === "reference") {
730
+ return {
731
+ ok: true,
732
+ criterion: {
733
+ ...criterion,
734
+ value: [
735
+ ...new Set(
736
+ criterion.value.map(String)
737
+ )
738
+ ]
739
+ }
740
+ };
666
741
  }
667
- return validateValueForKind(criterion, descriptor.valueKind);
742
+ return { ok: true, criterion };
668
743
  }
669
744
  function validateValueForKind(criterion, valueKind) {
670
745
  const { value } = criterion;
671
- if (valueKind === "id-array" || valueKind === "enum") {
672
- return Array.isArray(value) ? null : "invalid_value";
673
- }
674
- if (Array.isArray(value)) return "invalid_value";
675
- if (valueKind === "number") {
676
- return Number.isFinite(Number(value)) ? null : "invalid_value";
746
+ if (valueKind === "boolean") {
747
+ return Array.isArray(value) && value.every((item) => typeof item === "boolean") ? null : "invalid_value";
677
748
  }
678
- if (valueKind === "date") {
679
- return isValidDateValue(value) ? null : "invalid_value";
749
+ if (valueKind === "id-array" || valueKind === "enum" || valueKind === "reference") {
750
+ return Array.isArray(value) && value.every(
751
+ (item) => typeof item === "string" || typeof item === "number"
752
+ ) ? null : "invalid_value";
680
753
  }
754
+ if (Array.isArray(value)) return "invalid_value";
681
755
  return null;
682
756
  }
683
- function isValidDateValue(value) {
684
- if (value instanceof Date) return true;
685
- if (typeof value === "string" || typeof value === "number") {
686
- return !Number.isNaN(new Date(value).getTime());
687
- }
688
- return false;
757
+ function normalizeNumberValue(value) {
758
+ if (typeof value === "string" && value.trim() === "") return null;
759
+ if (typeof value !== "string" && typeof value !== "number") return null;
760
+ const parsed2 = typeof value === "number" ? value : Number(value);
761
+ return Number.isFinite(parsed2) ? parsed2 : null;
762
+ }
763
+ function normalizeDateValue(value) {
764
+ let date2;
765
+ if (value instanceof Date) date2 = new Date(value.getTime());
766
+ else if (typeof value === "string") date2 = new Date(value);
767
+ else if (typeof value === "number") date2 = new Date(value);
768
+ else return null;
769
+ return Number.isFinite(date2.getTime()) ? date2 : null;
770
+ }
771
+
772
+ // src/shared/clone-domain-value.ts
773
+ function cloneDomainValue(value) {
774
+ return structuredClone(value);
689
775
  }
690
776
 
691
777
  // src/dispatch/filter.ts
@@ -715,15 +801,21 @@ function resolveFilter(adapter, filter, hoursPerDay) {
715
801
  }
716
802
  function dispatchFilterSet(action, deps) {
717
803
  const { adapter, hoursPerDay } = deps;
804
+ const criteria = [];
718
805
  for (const criterion of action.criteria) {
719
- const rejection = validateCriterion(criterion);
720
- if (rejection !== null) return { ok: false, reason: rejection };
721
- }
722
- const filter = action.dateRange === void 0 ? { criteria: action.criteria, logic: action.logic } : {
723
- criteria: action.criteria,
724
- logic: action.logic,
725
- dateRange: action.dateRange
726
- };
806
+ const result = normalizeCriterion(criterion);
807
+ if (!result.ok) return { ok: false, reason: result.reason };
808
+ criteria.push(result.criterion);
809
+ }
810
+ const rangeResult = normalizeDateRange(action.dateRange);
811
+ if (!rangeResult.ok) return { ok: false, reason: rangeResult.reason };
812
+ const filter = cloneDomainValue(
813
+ rangeResult.dateRange === void 0 ? { criteria, logic: action.logic } : {
814
+ criteria,
815
+ logic: action.logic,
816
+ dateRange: rangeResult.dateRange
817
+ }
818
+ );
727
819
  adapter.setActiveFilter(isEmptyFilter(filter) ? null : filter);
728
820
  const { visibleIds, projection } = resolveFilter(
729
821
  adapter,
@@ -868,7 +960,7 @@ function dispatchSortSet(action, deps) {
868
960
  const rejection = validateRule(rule);
869
961
  if (rejection !== null) return { ok: false, reason: rejection };
870
962
  }
871
- const order = action.rules.length === 0 ? null : { rules: action.rules };
963
+ const order = action.rules.length === 0 ? null : cloneDomainValue({ rules: action.rules });
872
964
  adapter.setActiveOrder(order);
873
965
  const changes = {
874
966
  source: action,
@@ -1436,6 +1528,11 @@ function isDescendantOf(childId, ancestorId, adapter) {
1436
1528
  }
1437
1529
  return false;
1438
1530
  }
1531
+ function isSummaryHierarchyLink(source, target, adapter) {
1532
+ const sourceIsSummary = isSummary2(source, adapter);
1533
+ const targetIsSummary = isSummary2(target, adapter);
1534
+ return targetIsSummary && isDescendantOf(source, target, adapter) || sourceIsSummary && isDescendantOf(target, source, adapter);
1535
+ }
1439
1536
  function getLeafDescendants(parentId, adapter) {
1440
1537
  const leaves = [];
1441
1538
  const stack = [parentId];
@@ -2373,6 +2470,18 @@ var noopReporter = {
2373
2470
  }
2374
2471
  };
2375
2472
 
2473
+ // src/internal/state/calendar-duration.ts
2474
+ var CALENDAR_DAY_MS = 864e5;
2475
+ function computeCalendarDuration(activity) {
2476
+ if (activity.type === "milestone") return 0;
2477
+ const start = activity.startDate?.getTime();
2478
+ const end = activity.endDate?.getTime();
2479
+ if (start == null || end == null || Number.isNaN(start) || Number.isNaN(end)) {
2480
+ return null;
2481
+ }
2482
+ return Math.floor((end - start) / CALENDAR_DAY_MS) + 1;
2483
+ }
2484
+
2376
2485
  // src/autoscheduler/integration/apply-results.ts
2377
2486
  function applyResults(adapter, result) {
2378
2487
  if (result.cancelled || result.updatedIds.length === 0) return;
@@ -2384,6 +2493,7 @@ function applyResults(adapter, result) {
2384
2493
  if (!liveActivity) continue;
2385
2494
  liveActivity.startDate = plan.startDate;
2386
2495
  liveActivity.endDate = plan.endDate;
2496
+ liveActivity.calendarDuration = computeCalendarDuration(liveActivity);
2387
2497
  adapter.updateActivity(id);
2388
2498
  }
2389
2499
  });
@@ -2450,13 +2560,253 @@ function hasPath(adjacency, from, to) {
2450
2560
  return false;
2451
2561
  }
2452
2562
 
2563
+ // src/internal/dependency-graph/strongly-connected-components.ts
2564
+ function circularComponents(edges) {
2565
+ const walk = buildWalkState(edges);
2566
+ for (const node of walk.adjacency.keys()) {
2567
+ if (!walk.records.has(node)) collectFromRoot(node, walk);
2568
+ }
2569
+ return walk.components;
2570
+ }
2571
+ function buildWalkState(edges) {
2572
+ const adjacency = /* @__PURE__ */ new Map();
2573
+ const selfLooped = /* @__PURE__ */ new Set();
2574
+ for (const edge of edges) {
2575
+ const source = String(edge.source);
2576
+ const target = String(edge.target);
2577
+ const sourceNeighbors = neighborsOf(adjacency, source);
2578
+ neighborsOf(adjacency, target);
2579
+ if (source === target) {
2580
+ selfLooped.add(source);
2581
+ continue;
2582
+ }
2583
+ sourceNeighbors.push(target);
2584
+ }
2585
+ return {
2586
+ adjacency,
2587
+ selfLooped,
2588
+ records: /* @__PURE__ */ new Map(),
2589
+ componentStack: [],
2590
+ nextIndex: 0,
2591
+ components: []
2592
+ };
2593
+ }
2594
+ function neighborsOf(adjacency, node) {
2595
+ const existing = adjacency.get(node);
2596
+ if (existing) return existing;
2597
+ const created = [];
2598
+ adjacency.set(node, created);
2599
+ return created;
2600
+ }
2601
+ function collectFromRoot(root, walk) {
2602
+ const frames = [];
2603
+ frames.push({ node: root, record: enterNode(root, walk), neighborCursor: 0 });
2604
+ while (frames.length > 0) {
2605
+ const frame = frames[frames.length - 1];
2606
+ if (frame === void 0) break;
2607
+ const neighbors = walk.adjacency.get(frame.node) ?? [];
2608
+ if (frame.neighborCursor < neighbors.length) {
2609
+ const neighbor = neighbors[frame.neighborCursor];
2610
+ frame.neighborCursor += 1;
2611
+ if (neighbor === void 0) continue;
2612
+ visitNeighbor(frame, neighbor, frames, walk);
2613
+ continue;
2614
+ }
2615
+ frames.pop();
2616
+ const parentFrame = frames[frames.length - 1];
2617
+ if (parentFrame && frame.record.lowLink < parentFrame.record.lowLink) {
2618
+ parentFrame.record.lowLink = frame.record.lowLink;
2619
+ }
2620
+ const isComponentRoot = frame.record.lowLink === frame.record.visitIndex;
2621
+ if (isComponentRoot) emitComponent(frame.node, walk);
2622
+ }
2623
+ }
2624
+ function visitNeighbor(frame, neighbor, frames, walk) {
2625
+ const neighborRecord = walk.records.get(neighbor);
2626
+ if (!neighborRecord) {
2627
+ frames.push({
2628
+ node: neighbor,
2629
+ record: enterNode(neighbor, walk),
2630
+ neighborCursor: 0
2631
+ });
2632
+ return;
2633
+ }
2634
+ const lowersCurrent = neighborRecord.onStack && neighborRecord.visitIndex < frame.record.lowLink;
2635
+ if (lowersCurrent) frame.record.lowLink = neighborRecord.visitIndex;
2636
+ }
2637
+ function enterNode(node, walk) {
2638
+ const record = {
2639
+ visitIndex: walk.nextIndex,
2640
+ lowLink: walk.nextIndex,
2641
+ onStack: true
2642
+ };
2643
+ walk.nextIndex += 1;
2644
+ walk.records.set(node, record);
2645
+ walk.componentStack.push({ node, record });
2646
+ return record;
2647
+ }
2648
+ function emitComponent(rootNode, walk) {
2649
+ const component = [];
2650
+ for (; ; ) {
2651
+ const entry = walk.componentStack.pop();
2652
+ if (entry === void 0) break;
2653
+ entry.record.onStack = false;
2654
+ component.push(entry.node);
2655
+ if (entry.node === rootNode) break;
2656
+ }
2657
+ const firstMember = component[0];
2658
+ const isSingleSelfLoop = component.length === 1 && firstMember !== void 0 && walk.selfLooped.has(firstMember);
2659
+ const isCircular = component.length > 1 || isSingleSelfLoop;
2660
+ if (isCircular) walk.components.push(component);
2661
+ }
2662
+
2663
+ // src/internal/dependency-graph/effective-graph.ts
2664
+ function projectEffectiveEdges(port, links, proposal) {
2665
+ const projectionPort = proposal ? new ReparentedPort(port, proposal) : port;
2666
+ return expandParentLinks([...links], projectionPort);
2667
+ }
2668
+ var ReparentedPort = class {
2669
+ constructor(base, proposal) {
2670
+ this.base = base;
2671
+ for (const [childId, proposedParent] of proposal.reparented) {
2672
+ const childKey = String(childId);
2673
+ const newParent = proposedParent === null || isRootParent(proposedParent) ? null : proposedParent;
2674
+ const oldParent = base.getActivity(childId)?.parentId ?? null;
2675
+ const oldKey = oldParent === null || isRootParent(oldParent) ? null : String(oldParent);
2676
+ const newKey = newParent === null ? null : String(newParent);
2677
+ if (oldKey === newKey) continue;
2678
+ this.reparented.set(childKey, newParent);
2679
+ if (oldKey !== null) {
2680
+ const removed = this.movedOutByParent.get(oldKey) ?? /* @__PURE__ */ new Set();
2681
+ removed.add(childKey);
2682
+ this.movedOutByParent.set(oldKey, removed);
2683
+ }
2684
+ if (newKey !== null) {
2685
+ const gained = this.movedInByParent.get(newKey) ?? [];
2686
+ gained.push(childId);
2687
+ this.movedInByParent.set(newKey, gained);
2688
+ }
2689
+ }
2690
+ }
2691
+ base;
2692
+ reparented = /* @__PURE__ */ new Map();
2693
+ movedOutByParent = /* @__PURE__ */ new Map();
2694
+ movedInByParent = /* @__PURE__ */ new Map();
2695
+ withOverride(activity) {
2696
+ if (!activity) return null;
2697
+ const override = this.reparented.get(String(activity.id));
2698
+ if (override === void 0) return activity;
2699
+ return { ...activity, parentId: override };
2700
+ }
2701
+ getActivity(activityId) {
2702
+ return this.withOverride(this.base.getActivity(activityId));
2703
+ }
2704
+ getLiveActivity(activityId) {
2705
+ return this.withOverride(this.base.getLiveActivity(activityId));
2706
+ }
2707
+ getAllActivities() {
2708
+ return this.base.getAllActivities().map((activity) => this.withOverride(activity) ?? activity);
2709
+ }
2710
+ getChildren(parentId) {
2711
+ const parentKey = String(parentId);
2712
+ const removed = this.movedOutByParent.get(parentKey);
2713
+ const baseChildren = this.base.getChildren(parentId);
2714
+ const kept = removed ? baseChildren.filter((childId) => !removed.has(String(childId))) : [...baseChildren];
2715
+ const gained = this.movedInByParent.get(parentKey);
2716
+ if (gained) kept.push(...gained);
2717
+ return kept;
2718
+ }
2719
+ getParent(activityId) {
2720
+ const override = this.reparented.get(String(activityId));
2721
+ if (override === void 0) return this.base.getParent(activityId);
2722
+ if (override === null) return null;
2723
+ return this.getActivity(override);
2724
+ }
2725
+ isChildOf(childId, parentId) {
2726
+ const targetKey = String(parentId);
2727
+ const visited = /* @__PURE__ */ new Set();
2728
+ let current = this.getActivity(childId);
2729
+ while (current) {
2730
+ const parent = current.parentId;
2731
+ if (parent === null || isRootParent(parent)) return false;
2732
+ const parentKey = String(parent);
2733
+ if (parentKey === targetKey) return true;
2734
+ if (visited.has(parentKey)) return false;
2735
+ visited.add(parentKey);
2736
+ current = this.getActivity(parent);
2737
+ }
2738
+ return false;
2739
+ }
2740
+ getAllLinks() {
2741
+ return this.base.getAllLinks();
2742
+ }
2743
+ getAllIds() {
2744
+ return this.base.getAllIds();
2745
+ }
2746
+ getLink(linkId) {
2747
+ return this.base.getLink(linkId);
2748
+ }
2749
+ activityExists(activityId) {
2750
+ return this.base.activityExists(activityId);
2751
+ }
2752
+ getOutgoingLinkIds(activityId) {
2753
+ return this.base.getOutgoingLinkIds(activityId);
2754
+ }
2755
+ getIncomingLinkIds(activityId) {
2756
+ return this.base.getIncomingLinkIds(activityId);
2757
+ }
2758
+ getClosestWorkTime(params) {
2759
+ return this.base.getClosestWorkTime(params);
2760
+ }
2761
+ calculateEndDate(params) {
2762
+ return this.base.calculateEndDate(params);
2763
+ }
2764
+ calculateDuration(params) {
2765
+ return this.base.calculateDuration(params);
2766
+ }
2767
+ batchUpdate(runBatch) {
2768
+ this.base.batchUpdate(runBatch);
2769
+ }
2770
+ updateActivity(activityId) {
2771
+ this.base.updateActivity(activityId);
2772
+ }
2773
+ getFlags() {
2774
+ return this.base.getFlags();
2775
+ }
2776
+ getProjectEnd() {
2777
+ return this.base.getProjectEnd();
2778
+ }
2779
+ };
2780
+
2781
+ // src/internal/dependency-graph/effective-graph-validation.ts
2782
+ function validateTopologyCommit(input) {
2783
+ const { port, currentLinks, proposedLinks, proposal } = input;
2784
+ const proposedEdges = projectEffectiveEdges(port, proposedLinks, proposal);
2785
+ if (!hasDirectedCycle(proposedEdges)) return { ok: true };
2786
+ const currentEdges = projectEffectiveEdges(port, currentLinks);
2787
+ const corruptBefore = corruptActivityIds(currentEdges);
2788
+ const corruptAfter = corruptActivityIds(proposedEdges);
2789
+ const newlyCyclicActivityIds = [...corruptAfter].filter(
2790
+ (activityId) => !corruptBefore.has(activityId)
2791
+ );
2792
+ if (newlyCyclicActivityIds.length === 0) return { ok: true };
2793
+ return { ok: false, newlyCyclicActivityIds };
2794
+ }
2795
+ function corruptActivityIds(edges) {
2796
+ const corrupt = /* @__PURE__ */ new Set();
2797
+ for (const component of circularComponents(edges)) {
2798
+ for (const activityId of component) corrupt.add(activityId);
2799
+ }
2800
+ return corrupt;
2801
+ }
2802
+
2453
2803
  // src/autoscheduler/links/apply-link-operation.ts
2454
2804
  function applyLinkOperation(op, deps) {
2455
2805
  switch (op.kind) {
2456
2806
  case "create":
2457
2807
  return applyCreate(op, deps);
2458
2808
  case "update":
2459
- return applyUpdate(op, deps);
2809
+ return applyLinkUpdate(op, deps.port);
2460
2810
  case "delete":
2461
2811
  return applyDelete(op, deps);
2462
2812
  }
@@ -2482,6 +2832,10 @@ function applyLinkCreates(links, deps) {
2482
2832
  rejected = { applied: false, rejected: "activity-missing" };
2483
2833
  break;
2484
2834
  }
2835
+ if (isSummaryHierarchyLink(operation.source, operation.target, deps.port)) {
2836
+ rejected = { applied: false, rejected: "hierarchy-link" };
2837
+ break;
2838
+ }
2485
2839
  const key = linkKey(operation);
2486
2840
  const duplicateId = duplicateKeys.get(key);
2487
2841
  if (duplicateId !== void 0) {
@@ -2507,6 +2861,24 @@ function applyLinkCreates(links, deps) {
2507
2861
  return { applied: false, rejected: "cycle" };
2508
2862
  }
2509
2863
  if (rejected) return rejected;
2864
+ const proposedLinks = [
2865
+ ...existingLinks,
2866
+ ...links.map(({ operation, linkId }) => ({
2867
+ id: linkId,
2868
+ source: operation.source,
2869
+ target: operation.target,
2870
+ type: operation.type,
2871
+ lag: operation.lag
2872
+ }))
2873
+ ];
2874
+ const effectiveVerdict = validateTopologyCommit({
2875
+ port: deps.port,
2876
+ currentLinks: existingLinks,
2877
+ proposedLinks
2878
+ });
2879
+ if (!effectiveVerdict.ok) {
2880
+ return { applied: false, rejected: "cycle" };
2881
+ }
2510
2882
  for (const { operation, linkId } of links) {
2511
2883
  const preserved = deps.preserved?.get(linkId);
2512
2884
  deps.port.addLink({
@@ -2535,6 +2907,9 @@ function applyCreate(op, deps) {
2535
2907
  if (!src || !tgt) {
2536
2908
  return { applied: false, rejected: "activity-missing" };
2537
2909
  }
2910
+ if (isSummaryHierarchyLink(op.source, op.target, port)) {
2911
+ return { applied: false, rejected: "hierarchy-link" };
2912
+ }
2538
2913
  const existingLinks = port.getAllLinks();
2539
2914
  for (const l of existingLinks) {
2540
2915
  if (String(l.source) === String(op.source) && String(l.target) === String(op.target) && l.type === op.type) {
@@ -2556,11 +2931,18 @@ function applyCreate(op, deps) {
2556
2931
  type: op.type,
2557
2932
  lag: op.lag
2558
2933
  };
2934
+ const effectiveVerdict = validateTopologyCommit({
2935
+ port,
2936
+ currentLinks: existingLinks,
2937
+ proposedLinks: [...existingLinks, link]
2938
+ });
2939
+ if (!effectiveVerdict.ok) {
2940
+ return { applied: false, rejected: "cycle" };
2941
+ }
2559
2942
  port.addLink(link);
2560
2943
  return { applied: true, linkId: id };
2561
2944
  }
2562
- function applyUpdate(op, deps) {
2563
- const { port } = deps;
2945
+ function applyLinkUpdate(op, port) {
2564
2946
  const link = port.getLink(op.linkId);
2565
2947
  if (!link) return { applied: false, rejected: "link-missing" };
2566
2948
  if (op.lag !== void 0) port.setLinkField(op.linkId, "lag", op.lag);
@@ -2731,6 +3113,10 @@ function diffOutgoingLinks(input) {
2731
3113
  }
2732
3114
 
2733
3115
  // src/autoscheduler/links/lag-units.ts
3116
+ function roundLagDays(lagDays) {
3117
+ const rounded = Math.sign(lagDays) * Math.round(Math.abs(lagDays));
3118
+ return Object.is(rounded, -0) ? 0 : rounded;
3119
+ }
2734
3120
  function lagDaysToHours(lagDays, hoursPerDay) {
2735
3121
  return Math.sign(lagDays) * Math.round(Math.abs(lagDays) * hoursPerDay);
2736
3122
  }
@@ -2993,10 +3379,7 @@ function adjustLinkLagOnTaskMove(targetId, adapter, preEditTarget) {
2993
3379
  calendarApi
2994
3380
  );
2995
3381
  if (newLag === link.lag) continue;
2996
- applyLinkOperation(
2997
- { kind: "update", linkId: link.id, lag: newLag },
2998
- { port: adapter }
2999
- );
3382
+ applyLinkUpdate({ linkId: link.id, lag: newLag }, adapter);
3000
3383
  }
3001
3384
  }
3002
3385
 
@@ -3296,6 +3679,19 @@ function durationToHours(durationDays, hoursPerDay) {
3296
3679
  return Math.round(Math.ceil(durationDays * hoursPerDay * 60) / 60);
3297
3680
  }
3298
3681
 
3682
+ // src/columns/shared/error-codes.ts
3683
+ var PARSE_ERROR = {
3684
+ NOT_A_NUMBER: "not_a_number",
3685
+ // ISSUE-057 (CP6): la unidad de una duración se declara, no se adivina.
3686
+ MISSING_INPUT_UNIT: "missing_input_unit",
3687
+ // D-OWNER-5: una duración en días es entera; una fracción se rechaza sin
3688
+ // redondear (el redondeo silencioso era la puerta del bug de las 320 horas).
3689
+ FRACTIONAL_DAYS: "fractional_days"
3690
+ };
3691
+ var VALIDATION_REASON = {
3692
+ UNCHANGED: "unchanged",
3693
+ NEGATIVE_DURATION: "negative_duration"};
3694
+
3299
3695
  // src/columns/end-date/setter.ts
3300
3696
  var setEndDate = (value) => ({
3301
3697
  endDate: value
@@ -3303,7 +3699,7 @@ var setEndDate = (value) => ({
3303
3699
 
3304
3700
  // src/columns/shared/date-normalize.ts
3305
3701
  function truncateToUtcDay(date2, tz) {
3306
- if (tz) {
3702
+ {
3307
3703
  const parts = new Intl.DateTimeFormat("en-CA", {
3308
3704
  timeZone: tz,
3309
3705
  year: "numeric",
@@ -3313,9 +3709,6 @@ function truncateToUtcDay(date2, tz) {
3313
3709
  const get = (kind) => Number(parts.find((p) => p.type === kind)?.value);
3314
3710
  return new Date(Date.UTC(get("year"), get("month") - 1, get("day")));
3315
3711
  }
3316
- return new Date(
3317
- Date.UTC(date2.getFullYear(), date2.getMonth(), date2.getDate())
3318
- );
3319
3712
  }
3320
3713
  function endOfLocalDay(date2, tz) {
3321
3714
  {
@@ -3325,11 +3718,11 @@ function endOfLocalDay(date2, tz) {
3325
3718
  }
3326
3719
  }
3327
3720
  function nextWorkingDay(calendar, domainDay) {
3328
- let cursor = truncateToUtcDay(domainDay);
3721
+ let cursor = truncateToUtcDay(domainDay, "UTC");
3329
3722
  while (!calendar.isWorkTime(cursor, CALENDAR_UNIT.DAY)) {
3330
3723
  cursor = new Date(cursor);
3331
3724
  cursor.setUTCDate(cursor.getUTCDate() + 1);
3332
- cursor = truncateToUtcDay(cursor);
3725
+ cursor = truncateToUtcDay(cursor, "UTC");
3333
3726
  }
3334
3727
  return cursor;
3335
3728
  }
@@ -3367,11 +3760,12 @@ var DATELESS_CONSTRAINTS = /* @__PURE__ */ new Set([
3367
3760
  ]);
3368
3761
  var IMPLIED_CONSTRAINT_ON_DATE_EDIT = "snet";
3369
3762
  function normalizeConstraintDate(typedDay, calendar, constraintType) {
3763
+ const constraintDay = truncateToUtcDay(typedDay, "UTC");
3370
3764
  if (!START_BASED_CONSTRAINTS2.has(constraintType) && !END_BASED_CONSTRAINTS.has(constraintType)) {
3371
- return typedDay;
3765
+ return constraintDay;
3372
3766
  }
3373
- const reference = START_BASED_CONSTRAINTS2.has(constraintType) ? shiftStart(calendar, typedDay) : shiftEnd(calendar, typedDay);
3374
- const result = new Date(typedDay);
3767
+ const reference = START_BASED_CONSTRAINTS2.has(constraintType) ? shiftStart(calendar, constraintDay) : shiftEnd(calendar, constraintDay);
3768
+ const result = new Date(constraintDay);
3375
3769
  result.setUTCHours(
3376
3770
  reference.getUTCHours(),
3377
3771
  reference.getUTCMinutes(),
@@ -3399,10 +3793,6 @@ var POST_PROCESSOR = {
3399
3793
  RECORD_LAST_START_DATE: "recordLastStartDate"
3400
3794
  };
3401
3795
 
3402
- // src/columns/shared/error-codes.ts
3403
- var VALIDATION_REASON = {
3404
- NEGATIVE_DURATION: "negative_duration"};
3405
-
3406
3796
  // src/columns/shared/constraint-validation.ts
3407
3797
  function checkConstraintViolation2(constraintType, constraintDate, projectedDate) {
3408
3798
  return checkConstraintViolation(constraintType, constraintDate, projectedDate);
@@ -3427,10 +3817,19 @@ var durationPipeline = {
3427
3817
  },
3428
3818
  parseInput(rawValue, _activity, ctx) {
3429
3819
  const numericValue = parseFloat(String(rawValue));
3430
- if (Number.isNaN(numericValue)) return parseError("not_a_number");
3431
- return parsed(
3432
- ctx.inputUnit === "hours" ? numericValue : durationToHours(numericValue, ctx.hoursPerDay)
3433
- );
3820
+ if (!Number.isFinite(numericValue)) {
3821
+ return parseError(PARSE_ERROR.NOT_A_NUMBER);
3822
+ }
3823
+ if (ctx.inputUnit === void 0) {
3824
+ return parseError(PARSE_ERROR.MISSING_INPUT_UNIT);
3825
+ }
3826
+ if (ctx.inputUnit === "hours") {
3827
+ return parsed(numericValue);
3828
+ }
3829
+ if (!Number.isInteger(numericValue)) {
3830
+ return parseError(PARSE_ERROR.FRACTIONAL_DAYS);
3831
+ }
3832
+ return parsed(durationToHours(numericValue, ctx.hoursPerDay));
3434
3833
  },
3435
3834
  validate(_activity, oldValue, newValue) {
3436
3835
  if (isNegativeDuration(newValue)) {
@@ -3879,17 +4278,7 @@ var startDatePipeline = {
3879
4278
  transform(activity, newValue, ctx) {
3880
4279
  const calendar = ctx.calendars.getCalendar(activity.calendarId);
3881
4280
  if (!calendar) {
3882
- return fieldChanges(
3883
- {
3884
- startDate: newValue,
3885
- ...setConstraintTypeImplied(),
3886
- ...setConstraintDate(newValue)
3887
- },
3888
- {
3889
- autoSchedule: true,
3890
- postProcessors: ["adjustLinkLagOnTaskMove"]
3891
- }
3892
- );
4281
+ throw new Error("startDatePipeline: no calendar resolved (integrity)");
3893
4282
  }
3894
4283
  const finalStart = shiftStart(calendar, newValue);
3895
4284
  const milestoneActivity = activity.type === "milestone";
@@ -3919,7 +4308,7 @@ function checkParentStartConstraint(activity, newDate, ctx) {
3919
4308
  activity.id
3920
4309
  );
3921
4310
  if (!parentStartDate) return null;
3922
- const parentDay = truncateToUtcDay(parentStartDate);
4311
+ const parentDay = truncateToUtcDay(parentStartDate, "UTC");
3923
4312
  if (newDate.getTime() < parentDay.getTime()) {
3924
4313
  return invalid("before_parent_start");
3925
4314
  }
@@ -3967,7 +4356,6 @@ var endDatePipeline = {
3967
4356
  const calendar = ctx.calendars.getCalendar(activity.calendarId);
3968
4357
  if (!calendar) return parsed(day);
3969
4358
  if (!calendar.isWorkTime(day, CALENDAR_UNIT.DAY)) {
3970
- if (ctx.dateFormat.split(" ").length > 1) return parsed(day);
3971
4359
  return parsed(
3972
4360
  calendar.getClosestWorkTime({
3973
4361
  direction: "past",
@@ -4150,7 +4538,12 @@ var constraintDatePipeline = {
4150
4538
  transform(activity, newValue, ctx) {
4151
4539
  const finalType = isUnconstrained(activity) ? IMPLIED_CONSTRAINT_ON_DATE_EDIT : effectiveConstraintType(activity);
4152
4540
  const calendar = ctx.calendars.getCalendar(activity.calendarId);
4153
- const storedDate = calendar ? normalizeConstraintDate(newValue, calendar, finalType) : newValue;
4541
+ if (!calendar) {
4542
+ throw new Error(
4543
+ "constraintDatePipeline: no calendar resolved (integrity)"
4544
+ );
4545
+ }
4546
+ const storedDate = normalizeConstraintDate(newValue, calendar, finalType);
4154
4547
  const patch = buildConstraintDateExtraFields(activity, storedDate);
4155
4548
  return fieldChanges(patch, {
4156
4549
  autoSchedule: true
@@ -4423,29 +4816,6 @@ function toConstraintWarning(activityId, notification) {
4423
4816
  };
4424
4817
  }
4425
4818
 
4426
- // src/constants/activity-properties.ts
4427
- var ACTIVITY_PROPERTY = {
4428
- PARENT: "parentId",
4429
- TYPE: "type",
4430
- START_DATE: "startDate",
4431
- END_DATE: "endDate",
4432
- CONSTRAINT_TYPE: "constraintType",
4433
- CONSTRAINT_DATE: "constraintDate",
4434
- AUTO_SCHEDULING: "autoScheduling",
4435
- PROGRESS: "progress",
4436
- COST: "cost",
4437
- USED_COST: "usedCost",
4438
- REAL_COST: "realCost",
4439
- WORK_HOURS: "workHours",
4440
- REAL_WORK_HOURS: "realWorkHours",
4441
- CORRELATIVE_ID: "correlativeId",
4442
- CUSTOM_ID: "customId",
4443
- NEW_ACTIVITIES_ARRAY: "newActivityIds",
4444
- HAS_NEW_ACTIVITIES: "hasNewActivities"};
4445
- var LINK_PROPERTY = {
4446
- TYPE: "type",
4447
- LAG: "lag"};
4448
-
4449
4819
  // src/columns/types.ts
4450
4820
  var COLUMN_WRITABLE_FIELD_LIST = [
4451
4821
  "name",
@@ -4473,6 +4843,29 @@ var COLUMN_WRITABLE_FIELDS = new Set(
4473
4843
  COLUMN_WRITABLE_FIELD_LIST
4474
4844
  );
4475
4845
 
4846
+ // src/constants/activity-properties.ts
4847
+ var ACTIVITY_PROPERTY = {
4848
+ PARENT: "parentId",
4849
+ TYPE: "type",
4850
+ START_DATE: "startDate",
4851
+ END_DATE: "endDate",
4852
+ CONSTRAINT_TYPE: "constraintType",
4853
+ CONSTRAINT_DATE: "constraintDate",
4854
+ PROGRESS: "progress",
4855
+ COST: "cost",
4856
+ USED_COST: "usedCost",
4857
+ REAL_COST: "realCost",
4858
+ WORK_HOURS: "workHours",
4859
+ REAL_WORK_HOURS: "realWorkHours",
4860
+ CORRELATIVE_ID: "correlativeId",
4861
+ CUSTOM_ID: "customId",
4862
+ NEW_ACTIVITIES_ARRAY: "newActivityIds",
4863
+ HAS_NEW_ACTIVITIES: "hasNewActivities",
4864
+ CALENDAR_DURATION: "calendarDuration"};
4865
+ var LINK_PROPERTY = {
4866
+ TYPE: "type",
4867
+ LAG: "lag"};
4868
+
4476
4869
  // src/internal/state/dynamic-writes.ts
4477
4870
  function setActivityFieldDynamic(state, activityId, field, value) {
4478
4871
  state.setActivityFieldDynamic(activityId, field, value);
@@ -4524,11 +4917,6 @@ function cloneCriticalPath(value) {
4524
4917
  };
4525
4918
  }
4526
4919
 
4527
- // src/shared/clone-domain-value.ts
4528
- function cloneDomainValue(value) {
4529
- return structuredClone(value);
4530
- }
4531
-
4532
4920
  // src/dispatch/shared/snapshots.ts
4533
4921
  var YIELD_EVERY_N_ENTRIES = 200;
4534
4922
  function collectTouchedIds(primary, changes) {
@@ -4640,6 +5028,7 @@ function diffActivity(before, after) {
4640
5028
  const beforeRec = before ?? {};
4641
5029
  const afterRec = after;
4642
5030
  const keys = /* @__PURE__ */ new Set([...Object.keys(beforeRec), ...Object.keys(afterRec)]);
5031
+ keys.delete(ACTIVITY_PROPERTY.CALENDAR_DURATION);
4643
5032
  for (const k of keys) {
4644
5033
  if (Object.hasOwn(beforeRec, k) !== Object.hasOwn(afterRec, k) || !fieldValueEqual(beforeRec[k], afterRec[k])) {
4645
5034
  fields[k] = {
@@ -9366,6 +9755,8 @@ var NON_SCHEDULING_INLINE_COLUMNS = /* @__PURE__ */ new Set([
9366
9755
  COLUMN.TAGS
9367
9756
  ]);
9368
9757
  var NON_SCHEDULING_KINDS = /* @__PURE__ */ new Set([
9758
+ "activity-clipboard-clear",
9759
+ "activity-copy",
9369
9760
  "selection-toggle",
9370
9761
  "selection-replace",
9371
9762
  "visibility-set",
@@ -9379,6 +9770,8 @@ var NON_SCHEDULING_KINDS = /* @__PURE__ */ new Set([
9379
9770
  "status-criteria-set"
9380
9771
  ]);
9381
9772
  var PURE_VIEW_STATE_KINDS = /* @__PURE__ */ new Set([
9773
+ "activity-clipboard-clear",
9774
+ "activity-copy",
9382
9775
  "selection-toggle",
9383
9776
  "selection-replace",
9384
9777
  "visibility-set",
@@ -9704,7 +10097,8 @@ async function assembleChangeSet(adapter, args) {
9704
10097
  const insertedIds = collectInsertedIds(captured);
9705
10098
  mergeDirtyBeforeImages(adapter, captured, insertedIds, before, allTouched);
9706
10099
  }
9707
- const beforeDiffEffects = args.sirDetection === "before-diff" ? detectSirAutoReject(before, adapter, allTouched) : [];
10100
+ const sirDetection = args.preview === true ? void 0 : args.sirDetection;
10101
+ const beforeDiffEffects = sirDetection === "before-diff" ? detectSirAutoReject(before, adapter, allTouched) : [];
9708
10102
  const correlativeBefore = buildCorrelativeBeforeMap(
9709
10103
  args.correlativeShifts,
9710
10104
  allTouched
@@ -9715,7 +10109,7 @@ async function assembleChangeSet(adapter, args) {
9715
10109
  allTouched,
9716
10110
  correlativeBefore
9717
10111
  );
9718
- const effects = args.sirDetection === "after-diff" ? detectSirAutoReject(before, adapter, allTouched) : beforeDiffEffects;
10112
+ const effects = sirDetection === "after-diff" ? detectSirAutoReject(before, adapter, allTouched) : beforeDiffEffects;
9719
10113
  const warnings = args.warnings ?? [];
9720
10114
  return {
9721
10115
  source: args.source,
@@ -9913,16 +10307,14 @@ async function dispatchInlineEdit(action, options, deps) {
9913
10307
  const { adapter, ctx, scheduler, sector } = deps;
9914
10308
  const pipeline = findPipelineForColumn(action.column);
9915
10309
  if (!pipeline) {
9916
- throw new Error(
9917
- `[ScheduleCore] no pipeline registered for column "${action.column}"`
9918
- );
10310
+ return {
10311
+ ok: false,
10312
+ reason: REJECTION_REASON.NO_PIPELINE_FOR_COLUMN
10313
+ };
9919
10314
  }
9920
- if (columnInvalidatesExpandedLinks(action.column)) {
10315
+ if (action.column === COLUMN.DURATION) {
9921
10316
  scheduler.invalidateExpandedLinksCache();
9922
10317
  }
9923
- if (action.column === ACTIVITY_PROPERTY.PARENT) {
9924
- scheduler.invalidateAllCaches();
9925
- }
9926
10318
  const activitySnap = ctx.activityReader.getActivity(action.activityId);
9927
10319
  if (!activitySnap) {
9928
10320
  return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
@@ -10046,6 +10438,7 @@ async function dispatchInlineEdit(action, options, deps) {
10046
10438
  touchedIds,
10047
10439
  scheduledIds,
10048
10440
  sirDetection: "before-diff",
10441
+ preview: options.preview,
10049
10442
  links: linkChanges,
10050
10443
  trackingEvents,
10051
10444
  ...constraintWarning ? { warnings: [constraintWarning] } : {},
@@ -10053,9 +10446,6 @@ async function dispatchInlineEdit(action, options, deps) {
10053
10446
  })
10054
10447
  };
10055
10448
  }
10056
- function columnInvalidatesExpandedLinks(column) {
10057
- return column === COLUMN.DURATION || column === ACTIVITY_PROPERTY.AUTO_SCHEDULING;
10058
- }
10059
10449
 
10060
10450
  // src/dispatch/handlers/link.ts
10061
10451
  function collectBatchContext(operations, adapter) {
@@ -10255,7 +10645,7 @@ async function dispatchInlineEditLinks(action, options, deps) {
10255
10645
  const { specs: specsInDays } = parsePredecessorString(action.newValue);
10256
10646
  const specs = specsInDays.map((spec) => ({
10257
10647
  ...spec,
10258
- lag: lagDaysToHours(spec.lag, sector.hoursPerDay)
10648
+ lag: lagDaysToHours(roundLagDays(spec.lag), sector.hoursPerDay)
10259
10649
  }));
10260
10650
  const byCorrelative = /* @__PURE__ */ new Map();
10261
10651
  adapter.forEachActivity((a) => {
@@ -10565,11 +10955,24 @@ function buildNewActivity(input, adapter) {
10565
10955
  startDate: rawStartDate,
10566
10956
  endDate: rawStartDate
10567
10957
  };
10568
- const startDate = overrides.startDate ? new Date(base.startDate) : adapter.getClosestWorkTime({
10958
+ let startDate = overrides.startDate ? new Date(base.startDate) : adapter.getClosestWorkTime({
10569
10959
  date: base.startDate,
10570
10960
  dir: WORK_TIME_DIRECTION.FUTURE,
10571
10961
  task: base
10572
10962
  });
10963
+ if (!overrides.startDate && parent && isMilestoneType(parent.type)) {
10964
+ const inheritedDay = new Date(parent.startDate);
10965
+ inheritedDay.setUTCHours(0, 0, 0, 0);
10966
+ const snappedDay = new Date(startDate);
10967
+ snappedDay.setUTCHours(0, 0, 0, 0);
10968
+ if (snappedDay.getTime() !== inheritedDay.getTime()) {
10969
+ startDate = adapter.getClosestWorkTime({
10970
+ date: inheritedDay,
10971
+ dir: WORK_TIME_DIRECTION.FUTURE,
10972
+ task: base
10973
+ });
10974
+ }
10975
+ }
10573
10976
  const endDate = adapter.calculateEndDate({
10574
10977
  startDate,
10575
10978
  durationHours: base.durationHours,
@@ -10579,6 +10982,11 @@ function buildNewActivity(input, adapter) {
10579
10982
  ...base,
10580
10983
  startDate,
10581
10984
  endDate,
10985
+ calendarDuration: computeCalendarDuration({
10986
+ type: base.type,
10987
+ startDate,
10988
+ endDate
10989
+ }),
10582
10990
  newActivityIds: [...base.newActivityIds],
10583
10991
  pendingRequestIds: [...base.pendingRequestIds],
10584
10992
  responsableIds: [...base.responsableIds],
@@ -11204,6 +11612,7 @@ async function dispatchActivityDelete(action, options, deps) {
11204
11612
  touchedIds,
11205
11613
  scheduledIds,
11206
11614
  sirDetection: "after-diff",
11615
+ preview: options.preview,
11207
11616
  links: buildLinkDeletions(beforeLinks),
11208
11617
  trackingEvents: [trackingEvent],
11209
11618
  hoursPerDay: sector.hoursPerDay,
@@ -11364,6 +11773,7 @@ async function dispatchActivityBatch(action, options, deps) {
11364
11773
  links: buildLinkChangesForBatch(appliedLinks, beforeLinks, deps.adapter),
11365
11774
  hoursPerDay: deps.sector.hoursPerDay,
11366
11775
  correlativeShifts: existingCorrelativeShifts,
11776
+ preview: options.preview,
11367
11777
  ...action.mode === "replace" ? { sirDetection: "after-diff" } : {}
11368
11778
  });
11369
11779
  return {
@@ -11661,184 +12071,331 @@ function stringKeyedLinks(links) {
11661
12071
  return new Map(Array.from(links, ([linkId, link]) => [String(linkId), link]));
11662
12072
  }
11663
12073
 
11664
- // src/dispatch/handlers/activity-paste.ts
11665
- function resolvePasteRootDestination(destination, adapter) {
11666
- if ("afterSiblingId" in destination) {
11667
- const sibling = adapter.getActivity(destination.afterSiblingId);
11668
- if (!sibling) {
11669
- return { ok: false, reason: REJECTION_REASON.ANCHOR_SIBLING_NOT_FOUND };
11670
- }
11671
- const parentId2 = sibling.parentId == null ? ROOT_PARENT_ID : String(sibling.parentId);
11672
- return { ok: true, parentId: parentId2, firstAfter: destination.afterSiblingId };
12074
+ // src/dispatch/handlers/sir-sync.ts
12075
+ function dispatchSirSync(action, deps) {
12076
+ const { adapter } = deps;
12077
+ const id = action.activityId;
12078
+ const snapshot = adapter.getActivity(id);
12079
+ if (!snapshot) {
12080
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
11673
12081
  }
11674
- const parentId = destination.parentId;
11675
- const siblings = collectSiblings(adapter, parentId).sort(
11676
- (a, b) => readCorrelativeId(a) - readCorrelativeId(b)
11677
- );
11678
- const before = siblings[destination.index]?.id;
11679
- return { ok: true, parentId, firstBefore: before };
11680
- }
11681
- function applyPasteDomainSemantics(overrides) {
11682
- const { customId: _customId, ...content } = overrides;
12082
+ const before = snapshot.pendingRequestIds ?? [];
12083
+ const after = action.pendingRequests.map((r) => r.id);
12084
+ adapter.setActivityField(action.activityId, "pendingRequestIds", after);
12085
+ const updated = adapter.getActivity(id);
12086
+ if (!updated) {
12087
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
12088
+ }
12089
+ const change = {
12090
+ id,
12091
+ kind: "updated",
12092
+ fields: { pendingRequestIds: { before, after } },
12093
+ // ISSUE-025: `after` must be the COMPLETE post-change entity; a partial
12094
+ // object crashes any consumer that projects the full snapshot.
12095
+ after: structuredCloneActivity(updated)
12096
+ };
11683
12097
  return {
11684
- ...content,
11685
- progress: 0,
11686
- ponderator: 0,
11687
- usedCost: 0,
11688
- workHours: 0,
11689
- isLookahead: false
12098
+ ok: true,
12099
+ changes: {
12100
+ source: action,
12101
+ activities: [change],
12102
+ links: [],
12103
+ calendars: [],
12104
+ trackingEvents: []
12105
+ }
11690
12106
  };
11691
12107
  }
11692
- async function dispatchActivityPaste(action, options, deps) {
11693
- const { adapter, scheduler, sector } = deps;
11694
- const rootDest = resolvePasteRootDestination(action.destination, adapter);
11695
- if (!rootDest.ok) return rootDest;
11696
- const beforeSnap = await snapshotActivities(adapter, /* @__PURE__ */ new Set());
11697
- const touched = /* @__PURE__ */ new Set();
11698
- const originalToNew = /* @__PURE__ */ new Map();
11699
- const createdIds = [];
11700
- let prevRootId;
11701
- for (const input of action.activities) {
11702
- const mappedParent = originalToNew.get(input.originalParentId);
11703
- let parentId;
11704
- let afterSiblingId;
11705
- let beforeSiblingId;
11706
- if (mappedParent !== void 0) {
11707
- parentId = mappedParent;
11708
- } else {
11709
- parentId = rootDest.parentId;
11710
- if (prevRootId === void 0) {
11711
- afterSiblingId = rootDest.firstAfter;
11712
- beforeSiblingId = rootDest.firstBefore;
11713
- } else {
11714
- afterSiblingId = prevRootId;
11715
- }
12108
+
12109
+ // src/dispatch/handlers/bulk-edit.ts
12110
+ var CUSTOM_ID_COLUMN = COLUMN.CUSTOM_ID;
12111
+ async function dispatchBulkEdit(action, options, deps) {
12112
+ const { adapter, scheduler, sector } = deps;
12113
+ const editsIncludeDuration = action.edits.some(
12114
+ (edit) => edit.column === COLUMN.DURATION
12115
+ );
12116
+ if (editsIncludeDuration) {
12117
+ scheduler.invalidateExpandedLinksCache();
12118
+ }
12119
+ const verdicts = [];
12120
+ const beforeSnap = /* @__PURE__ */ new Map();
12121
+ const constraintPriorsByActivity = /* @__PURE__ */ new Map();
12122
+ const explicitDateEditActivities = /* @__PURE__ */ new Set();
12123
+ const touchedIds = /* @__PURE__ */ new Set();
12124
+ const trackingEvents = [];
12125
+ const linkChanges = [];
12126
+ const warnings = [];
12127
+ let needsAutoSchedule = false;
12128
+ for (const edit of action.edits) {
12129
+ const outcome = applyOneEdit(edit, deps);
12130
+ verdicts.push(outcome.verdict);
12131
+ if (!outcome.verdict.ok || !outcome.changes) continue;
12132
+ const constraintWarning = toConstraintWarning(
12133
+ edit.activityId,
12134
+ outcome.changes.constraintWarning
12135
+ );
12136
+ if (constraintWarning) warnings.push(constraintWarning);
12137
+ const editTouched = collectTouchedIds(edit.activityId, outcome.changes);
12138
+ mergeBeforeSnapshots(beforeSnap, editTouched, deps);
12139
+ for (const touchedId of editTouched) touchedIds.add(touchedId);
12140
+ recordConstraintPrior(constraintPriorsByActivity, edit, beforeSnap);
12141
+ if (isExplicitConstraintDateEdit(edit.column)) {
12142
+ explicitDateEditActivities.add(String(edit.activityId));
11716
12143
  }
11717
- const core = await createActivityCore(
11718
- {
11719
- parentId,
11720
- afterSiblingId,
11721
- beforeSiblingId,
11722
- ...input.activityId ? { activityId: input.activityId } : {},
11723
- overrides: applyPasteDomainSemantics(input.overrides),
11724
- eventSource: action.eventSource
11725
- },
11726
- deps,
11727
- {
11728
- skipCorrelativeRecompute: true,
11729
- skipBeforeSnap: true,
11730
- customIdReferenceId: action.referenceActivityId
11731
- }
12144
+ const beforeLinkLags = snapshotIncomingLinkLags(adapter, edit.activityId);
12145
+ const preEditSnapshot = snapshotSingleActivity(
12146
+ adapter,
12147
+ String(edit.activityId)
11732
12148
  );
11733
- if (!core.ok) return core;
11734
- if (input.baselineSnapshot !== void 0) {
11735
- const snapshot = input.baselineSnapshot;
11736
- adapter.setActivityField(
11737
- core.newId,
11738
- "baselineSnapshot",
11739
- snapshot ? {
11740
- ...snapshot,
11741
- startDate: snapshot.startDate ? new Date(snapshot.startDate) : null,
11742
- endDate: snapshot.endDate ? new Date(snapshot.endDate) : null
11743
- } : null
11744
- );
12149
+ const customIdBeforeApply = readLiveCustomId(edit, deps);
12150
+ const changes = outcome.changes;
12151
+ applyFieldChanges(adapter, edit.activityId, changes);
12152
+ runPostProcessorsOnAdapter(
12153
+ edit.activityId,
12154
+ outcome.changes.postProcessors ?? [],
12155
+ adapter,
12156
+ preEditSnapshot
12157
+ );
12158
+ syncCustomIdTracker(edit, customIdBeforeApply, deps);
12159
+ linkChanges.push(
12160
+ ...diffIncomingLinkLagChanges(beforeLinkLags, adapter, edit.activityId)
12161
+ );
12162
+ needsAutoSchedule ||= Boolean(outcome.changes.needsAutoSchedule);
12163
+ for (const trackingEvent of outcome.changes.trackingEvents ?? []) {
12164
+ trackingEvents.push({
12165
+ name: trackingEvent.name,
12166
+ properties: trackingEvent.properties ?? {}
12167
+ });
11745
12168
  }
11746
- originalToNew.set(input.originalId, core.newId);
11747
- createdIds.push(core.newId);
11748
- if (mappedParent === void 0) prevRootId = core.newId;
11749
- }
11750
- const correlativeShifts = recomputeCorrelativeIds(adapter);
11751
- const createdIdSet = new Set(createdIds.map(String));
11752
- foldCorrelativeShifts(
11753
- adapter,
11754
- correlativeShifts,
11755
- createdIdSet,
11756
- beforeSnap,
11757
- touched
11758
- );
11759
- for (const createdId of createdIds) touched.add(String(createdId));
11760
- const appliedLinks = [];
11761
- for (const link of action.links) {
11762
- const newSource = originalToNew.get(link.source);
11763
- const newTarget = originalToNew.get(link.target);
11764
- if (newSource === void 0 || newTarget === void 0) continue;
11765
- const op = {
11766
- kind: "create",
11767
- source: newSource,
11768
- target: newTarget,
11769
- type: link.type,
11770
- lag: lagDaysToHours(link.lag, sector.hoursPerDay)
11771
- };
11772
- const res = applyLinkOperation(op, {
11773
- port: adapter,
11774
- newLinkId: link.linkId ? () => link.linkId : deps.linkIdGen.next
11775
- });
11776
- appliedLinks.push({
11777
- op,
11778
- finalLinkId: res.applied ? res.linkId : null,
11779
- rejected: res.applied ? null : res.rejected ?? "link_op_rejected"
11780
- });
11781
12169
  }
11782
- const { scheduledIds } = await runPostMutation(
12170
+ const postMutationArgs = {
12171
+ action,
12172
+ autoscheduleFrom: needsAutoSchedule ? "roots" : null,
12173
+ recomputeParentsFrom: touchedIds,
12174
+ now: deps.now,
12175
+ options
12176
+ };
12177
+ const batchHasConstraintEdits = constraintPriorsByActivity.size > 0;
12178
+ const { scheduledIds } = await (batchHasConstraintEdits ? settleConstraintEdits(
11783
12179
  {
11784
12180
  adapter,
11785
12181
  scheduler,
11786
- sector: deps.sector,
12182
+ sector,
11787
12183
  defaultBaseCalendarId: deps.defaultBaseCalendarId
11788
12184
  },
11789
12185
  {
11790
- action,
11791
- autoscheduleFrom: createdIds[0] ?? null,
11792
- recomputeParentsFrom: createdIds,
11793
- now: deps.now,
11794
- options
12186
+ postMutationArgs,
12187
+ revertTargets: [...constraintPriorsByActivity].filter(
12188
+ ([activityId]) => !explicitDateEditActivities.has(activityId)
12189
+ ).map(([activityId, priorConstraintDate]) => ({
12190
+ activityId,
12191
+ priorConstraintDate
12192
+ })),
12193
+ revert: (activityId, priorConstraintDate) => revertNoOpConstraintEdit(
12194
+ activityId,
12195
+ priorConstraintDate,
12196
+ adapter,
12197
+ deps.ctx.calendars
12198
+ )
11795
12199
  }
11796
- );
11797
- const linkChanges = buildLinkChangesForBatch(
11798
- appliedLinks,
11799
- /* @__PURE__ */ new Map(),
11800
- adapter
11801
- );
12200
+ ) : runPostMutation(
12201
+ {
12202
+ adapter,
12203
+ scheduler,
12204
+ sector,
12205
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
12206
+ },
12207
+ postMutationArgs
12208
+ ));
11802
12209
  return {
11803
12210
  ok: true,
12211
+ verdicts,
11804
12212
  changes: await assembleChangeSet(adapter, {
11805
12213
  source: action,
11806
12214
  beforeSnap,
11807
- touchedIds: touched,
12215
+ touchedIds,
11808
12216
  scheduledIds,
12217
+ sirDetection: "before-diff",
12218
+ preview: options.preview,
11809
12219
  links: linkChanges,
12220
+ trackingEvents,
12221
+ warnings,
11810
12222
  hoursPerDay: sector.hoursPerDay
11811
12223
  })
11812
12224
  };
11813
12225
  }
12226
+ function applyOneEdit(edit, deps) {
12227
+ const { ctx } = deps;
12228
+ const activityId = edit.activityId;
12229
+ const pipeline = findPipelineForColumn(edit.column);
12230
+ if (!pipeline) {
12231
+ return {
12232
+ verdict: {
12233
+ activityId,
12234
+ ok: false,
12235
+ reason: REJECTION_REASON.NO_PIPELINE_FOR_COLUMN
12236
+ },
12237
+ changes: null
12238
+ };
12239
+ }
12240
+ const activitySnap = ctx.activityReader.getActivity(edit.activityId);
12241
+ if (!activitySnap) {
12242
+ return {
12243
+ verdict: {
12244
+ activityId,
12245
+ ok: false,
12246
+ reason: REJECTION_REASON.ACTIVITY_NOT_FOUND
12247
+ },
12248
+ changes: null
12249
+ };
12250
+ }
12251
+ const editGate = pipeline.canEdit(activitySnap, ctx.hierarchy);
12252
+ if (!editGate.allowed) {
12253
+ return {
12254
+ verdict: {
12255
+ activityId,
12256
+ ok: false,
12257
+ reason: editGate.reason ?? REJECTION_REASON.CANNOT_EDIT,
12258
+ ...editGate.alertKey ? { alertKey: editGate.alertKey } : {}
12259
+ },
12260
+ changes: null
12261
+ };
12262
+ }
12263
+ const parsed2 = pipeline.parseInput(edit.newValue, activitySnap, ctx);
12264
+ if (!parsed2.ok) {
12265
+ return {
12266
+ verdict: {
12267
+ activityId,
12268
+ ok: false,
12269
+ reason: parsed2.reason ?? REJECTION_REASON.PARSE_ERROR
12270
+ },
12271
+ changes: null
12272
+ };
12273
+ }
12274
+ const oldValue = Reflect.get(activitySnap, pipeline.targetField);
12275
+ const validation = pipeline.validate(
12276
+ activitySnap,
12277
+ oldValue,
12278
+ parsed2.value,
12279
+ ctx
12280
+ );
12281
+ if (!validation.valid) {
12282
+ return {
12283
+ verdict: {
12284
+ activityId,
12285
+ ok: false,
12286
+ reason: validation.reason ?? REJECTION_REASON.INVALID,
12287
+ ...validation.notification?.messageKey ? { alertKey: validation.notification.messageKey } : {}
12288
+ },
12289
+ changes: null
12290
+ };
12291
+ }
12292
+ if (edit.column === CUSTOM_ID_COLUMN && parsed2.value !== null) {
12293
+ const liveActivity = deps.adapter.getActivity(edit.activityId);
12294
+ const oldCustomIdRaw = liveActivity ? liveActivity.customId : null;
12295
+ const oldCustomId = typeof oldCustomIdRaw === "string" ? oldCustomIdRaw : null;
12296
+ if (deps.customIdTracker.isCustomIdInUse(String(parsed2.value), oldCustomId)) {
12297
+ return {
12298
+ verdict: {
12299
+ activityId,
12300
+ ok: false,
12301
+ reason: REJECTION_REASON.CUSTOM_ID_DUPLICATE
12302
+ },
12303
+ changes: null
12304
+ };
12305
+ }
12306
+ }
12307
+ const changes = pipeline.transform(
12308
+ activitySnap,
12309
+ parsed2.value,
12310
+ ctx,
12311
+ parsed2.raw
12312
+ );
12313
+ return { verdict: { activityId, ok: true }, changes };
12314
+ }
12315
+ function readLiveCustomId(edit, deps) {
12316
+ if (edit.column !== CUSTOM_ID_COLUMN) return null;
12317
+ const liveActivity = deps.adapter.getActivity(edit.activityId);
12318
+ const rawValue = liveActivity ? liveActivity.customId : null;
12319
+ return typeof rawValue === "string" ? rawValue : null;
12320
+ }
12321
+ function syncCustomIdTracker(edit, oldCustomId, deps) {
12322
+ if (edit.column !== CUSTOM_ID_COLUMN) return;
12323
+ const liveActivity = deps.adapter.getActivity(edit.activityId);
12324
+ const rawValue = liveActivity ? liveActivity.customId : null;
12325
+ const newCustomId = typeof rawValue === "string" ? rawValue : null;
12326
+ if (oldCustomId !== newCustomId) {
12327
+ deps.customIdTracker.trackCustomIdChange(oldCustomId, newCustomId);
12328
+ }
12329
+ }
12330
+ function recordConstraintPrior(priors, edit, beforeSnap) {
12331
+ if (!isConstraintEditColumn(edit.column)) return;
12332
+ const activityKey = String(edit.activityId);
12333
+ if (priors.has(activityKey)) return;
12334
+ const preBatchSnapshot = beforeSnap.get(activityKey);
12335
+ if (!preBatchSnapshot) return;
12336
+ priors.set(activityKey, preBatchSnapshot.constraintDate ?? null);
12337
+ }
12338
+ function mergeBeforeSnapshots(beforeSnap, ids, deps) {
12339
+ const missing = /* @__PURE__ */ new Set();
12340
+ for (const candidateId of ids) {
12341
+ if (!beforeSnap.has(candidateId)) missing.add(candidateId);
12342
+ }
12343
+ if (missing.size === 0) return;
12344
+ for (const missingId of missing) {
12345
+ const liveActivity = deps.adapter.getActivity(missingId);
12346
+ if (liveActivity) {
12347
+ beforeSnap.set(missingId, structuredCloneActivity(liveActivity));
12348
+ }
12349
+ }
12350
+ }
11814
12351
 
11815
- // src/dispatch/handlers/sir-sync.ts
11816
- function dispatchSirSync(action, deps) {
12352
+ // src/dispatch/handlers/sir-approve.ts
12353
+ async function dispatchSirApprove(action, options, deps) {
11817
12354
  const { adapter } = deps;
11818
- const id = action.activityId;
11819
- const snapshot = adapter.getActivity(id);
12355
+ const snapshot = adapter.getActivity(action.activityId);
11820
12356
  if (!snapshot) {
11821
12357
  return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
11822
12358
  }
11823
- const before = snapshot.pendingRequestIds ?? [];
11824
- const after = action.pendingRequests.map((r) => r.id);
11825
- adapter.setActivityField(action.activityId, "pendingRequestIds", after);
11826
- const change = {
11827
- id,
11828
- kind: "updated",
11829
- fields: { pendingRequestIds: { before, after } },
11830
- after: { id, pendingRequestIds: after }
11831
- };
11832
- return {
11833
- ok: true,
11834
- changes: {
11835
- source: action,
11836
- activities: [change],
11837
- links: [],
11838
- calendars: [],
11839
- trackingEvents: []
11840
- }
11841
- };
12359
+ const pending = snapshot.pendingRequestIds ?? [];
12360
+ if (!pending.includes(action.approvedRequestId)) {
12361
+ return { ok: false, reason: REJECTION_REASON.SIR_REQUEST_NOT_PENDING };
12362
+ }
12363
+ const remaining = pending.filter((id) => id !== action.approvedRequestId);
12364
+ adapter.setActivityField(action.activityId, "pendingRequestIds", remaining);
12365
+ const result = await dispatchBulkEdit(
12366
+ {
12367
+ kind: "bulk-edit",
12368
+ edits: [
12369
+ {
12370
+ activityId: action.activityId,
12371
+ column: COLUMN.START_DATE,
12372
+ newValue: action.startDate
12373
+ },
12374
+ {
12375
+ activityId: action.activityId,
12376
+ column: COLUMN.END_DATE,
12377
+ newValue: action.endDate
12378
+ },
12379
+ // asap ÚLTIMO: gana al SNET implícito (paridad legacy).
12380
+ {
12381
+ activityId: action.activityId,
12382
+ column: COLUMN.CONSTRAINT_TYPE,
12383
+ newValue: "asap"
12384
+ }
12385
+ ],
12386
+ eventSource: action.eventSource
12387
+ },
12388
+ options,
12389
+ deps
12390
+ );
12391
+ if (!result.ok) return result;
12392
+ const hardFailure = "verdicts" in result ? result.verdicts.find(
12393
+ (verdict) => !verdict.ok && verdict.reason !== VALIDATION_REASON.UNCHANGED
12394
+ ) : void 0;
12395
+ if (hardFailure) {
12396
+ return { ok: false, reason: hardFailure.reason };
12397
+ }
12398
+ return { ...result, changes: { ...result.changes, source: action } };
11842
12399
  }
11843
12400
 
11844
12401
  // src/dispatch/handlers/activity-lookahead-sync.ts
@@ -11924,7 +12481,38 @@ function isAncestor(descendantId, candidateAncestorId, adapter) {
11924
12481
  if (parentKey === candidateAncestorId) return true;
11925
12482
  cur = parentKey;
11926
12483
  }
11927
- return false;
12484
+ return false;
12485
+ }
12486
+
12487
+ // src/dispatch/shared/hierarchy-cycle-guard.ts
12488
+ function reparentClosesEffectiveCycle(adapter, reparented) {
12489
+ const links = adapter.getAllLinks();
12490
+ const verdict = validateTopologyCommit({
12491
+ port: adapter,
12492
+ currentLinks: links,
12493
+ proposedLinks: links,
12494
+ proposal: { reparented }
12495
+ });
12496
+ return !verdict.ok;
12497
+ }
12498
+ function collectCyclicReparents(planned, adapter) {
12499
+ const cyclic = /* @__PURE__ */ new Set();
12500
+ if (planned.length === 0) return cyclic;
12501
+ const combined = new Map(
12502
+ planned.map((reparent) => [reparent.activityId, reparent.newParentId])
12503
+ );
12504
+ if (!reparentClosesEffectiveCycle(adapter, combined)) return cyclic;
12505
+ const accepted = /* @__PURE__ */ new Map();
12506
+ for (const reparent of planned) {
12507
+ const trial = new Map(accepted);
12508
+ trial.set(reparent.activityId, reparent.newParentId);
12509
+ if (reparentClosesEffectiveCycle(adapter, trial)) {
12510
+ cyclic.add(reparent.activityId);
12511
+ continue;
12512
+ }
12513
+ accepted.set(reparent.activityId, reparent.newParentId);
12514
+ }
12515
+ return cyclic;
11928
12516
  }
11929
12517
 
11930
12518
  // src/dispatch/handlers/activity-move.ts
@@ -11968,6 +12556,17 @@ async function dispatchActivityMove(action, options, deps) {
11968
12556
  const oldParentKey = normalizeParentKey(oldParent);
11969
12557
  const newParentKey = String(action.parentId);
11970
12558
  const parentChanged = oldParentKey !== newParentKey;
12559
+ if (parentChanged) {
12560
+ const reparented = /* @__PURE__ */ new Map([
12561
+ [
12562
+ action.activityId,
12563
+ newParentIsRoot ? null : action.parentId
12564
+ ]
12565
+ ]);
12566
+ if (reparentClosesEffectiveCycle(adapter, reparented)) {
12567
+ return { ok: false, reason: REJECTION_REASON.HIERARCHY_CYCLE };
12568
+ }
12569
+ }
11971
12570
  const touched = /* @__PURE__ */ new Set([action.activityId]);
11972
12571
  if (parentChanged) {
11973
12572
  if (oldParentKey !== ROOT_PARENT_ID) touched.add(oldParentKey);
@@ -12119,27 +12718,21 @@ async function dispatchActivityMove(action, options, deps) {
12119
12718
  touchedIds: touched,
12120
12719
  scheduledIds,
12121
12720
  sirDetection: "before-diff",
12721
+ preview: options.preview,
12122
12722
  trackingEvents: [trackingEvent],
12123
12723
  hoursPerDay: sector.hoursPerDay
12124
12724
  })
12125
12725
  };
12126
12726
  }
12127
12727
 
12128
- // src/dispatch/handlers/activity-indent.ts
12129
- async function dispatchActivityIndent(action, options, deps) {
12130
- const { adapter, scheduler, sector } = deps;
12131
- scheduler.invalidateAllCaches();
12132
- if (action.activityIds.length === 0) {
12133
- return { ok: false, reason: REJECTION_REASON.ACTIVITY_IDS_EMPTY };
12134
- }
12135
- const selectedSet = new Set(action.activityIds);
12136
- const touched = /* @__PURE__ */ new Set();
12728
+ // src/dispatch/shared/indent-planning.ts
12729
+ function planIndentMoves(activityIds, adapter) {
12730
+ const selectedSet = new Set(activityIds);
12137
12731
  const failed = [];
12138
- const succeededIds = [];
12139
12732
  const moves = [];
12140
12733
  const plannedIds = /* @__PURE__ */ new Set();
12141
12734
  const blockingSet = new Set(selectedSet);
12142
- const sortedIds = [...action.activityIds].sort((a, b) => {
12735
+ const sortedIds = [...activityIds].sort((a, b) => {
12143
12736
  const aSnap = adapter.getActivity(a);
12144
12737
  const bSnap = adapter.getActivity(b);
12145
12738
  const ac = aSnap ? readCorrelativeId(aSnap) : NOT_SET_SORT_VALUE2;
@@ -12174,12 +12767,43 @@ async function dispatchActivityIndent(action, options, deps) {
12174
12767
  const oldParentId = isRootParent(oldParentRaw) ? ROOT_PARENT_ID : String(oldParentRaw);
12175
12768
  moves.push({ activityId: id, newParentId: targetParentId, oldParentId });
12176
12769
  plannedIds.add(id);
12177
- touched.add(id);
12178
- touched.add(targetParentId);
12179
- if (oldParentId !== ROOT_PARENT_ID) touched.add(oldParentId);
12180
12770
  }
12771
+ const cyclicMoveIds = collectCyclicReparents(moves, adapter);
12772
+ for (const cyclicId of cyclicMoveIds) {
12773
+ failed.push({
12774
+ activityId: cyclicId,
12775
+ reason: REJECTION_REASON.HIERARCHY_CYCLE
12776
+ });
12777
+ }
12778
+ const allowedMoves = moves.filter(
12779
+ (move) => !cyclicMoveIds.has(move.activityId)
12780
+ );
12781
+ return { moves, cyclicMoveIds, allowedMoves, failed };
12782
+ }
12783
+
12784
+ // src/dispatch/handlers/activity-indent.ts
12785
+ async function dispatchActivityIndent(action, options, deps) {
12786
+ const { adapter, scheduler, sector } = deps;
12787
+ scheduler.invalidateAllCaches();
12788
+ if (action.activityIds.length === 0) {
12789
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_IDS_EMPTY };
12790
+ }
12791
+ const touched = /* @__PURE__ */ new Set();
12792
+ const succeededIds = [];
12793
+ const plan = planIndentMoves(action.activityIds, adapter);
12794
+ const failed = [
12795
+ ...plan.failed
12796
+ ];
12797
+ for (const move of plan.moves) {
12798
+ touched.add(String(move.activityId));
12799
+ touched.add(String(move.newParentId));
12800
+ if (move.oldParentId !== ROOT_PARENT_ID) {
12801
+ touched.add(String(move.oldParentId));
12802
+ }
12803
+ }
12804
+ const allowedMoves = plan.allowedMoves;
12181
12805
  const beforeSnap = await snapshotActivities(adapter, touched);
12182
- for (const { activityId, newParentId } of moves) {
12806
+ for (const { activityId, newParentId } of allowedMoves) {
12183
12807
  adapter.setActivityField(activityId, ACTIVITY_PROPERTY.PARENT, newParentId);
12184
12808
  adapter.setActivityField(
12185
12809
  activityId,
@@ -12189,7 +12813,7 @@ async function dispatchActivityIndent(action, options, deps) {
12189
12813
  succeededIds.push(activityId);
12190
12814
  }
12191
12815
  const promotedParents = /* @__PURE__ */ new Set();
12192
- for (const { activityId, newParentId } of moves) {
12816
+ for (const { activityId, newParentId } of allowedMoves) {
12193
12817
  if (promotedParents.has(newParentId)) continue;
12194
12818
  promotedParents.add(newParentId);
12195
12819
  const newParent = adapter.getActivity(newParentId);
@@ -12241,7 +12865,7 @@ async function dispatchActivityIndent(action, options, deps) {
12241
12865
  releaseClearedCustomId(oldNewParentCustomId, deps.customIdTracker);
12242
12866
  }
12243
12867
  const indentAffectedParents = /* @__PURE__ */ new Set();
12244
- for (const m of moves) {
12868
+ for (const m of allowedMoves) {
12245
12869
  if (m.oldParentId !== ROOT_PARENT_ID)
12246
12870
  indentAffectedParents.add(m.oldParentId);
12247
12871
  indentAffectedParents.add(m.newParentId);
@@ -12402,8 +13026,24 @@ async function dispatchActivityOutdent(action, options, deps) {
12402
13026
  if (grandparentKey !== ROOT_PARENT_ID) touched.add(String(grandparentKey));
12403
13027
  oldParents.add(oldParentKey);
12404
13028
  }
13029
+ const cyclicPlanIds = collectCyclicReparents(
13030
+ planned.map((plan) => ({
13031
+ activityId: plan.activityId,
13032
+ newParentId: plan.grandparentKey
13033
+ })),
13034
+ adapter
13035
+ );
13036
+ for (const cyclicId of cyclicPlanIds) {
13037
+ failed.push({
13038
+ activityId: cyclicId,
13039
+ reason: REJECTION_REASON.HIERARCHY_CYCLE
13040
+ });
13041
+ }
13042
+ const allowedPlans = planned.filter(
13043
+ (plan) => !cyclicPlanIds.has(plan.activityId)
13044
+ );
12405
13045
  const beforeSnap = await snapshotActivities(adapter, touched);
12406
- for (const plan of planned) {
13046
+ for (const plan of allowedPlans) {
12407
13047
  adapter.setActivityField(
12408
13048
  plan.activityId,
12409
13049
  ACTIVITY_PROPERTY.PARENT,
@@ -12437,7 +13077,7 @@ async function dispatchActivityOutdent(action, options, deps) {
12437
13077
  task: parentActivity
12438
13078
  }),
12439
13079
  idsRemoved: new Set(
12440
- planned.filter((plan) => plan.oldParentKey === oldParentKey).map((plan) => plan.activityId)
13080
+ allowedPlans.filter((plan) => plan.oldParentKey === oldParentKey).map((plan) => plan.activityId)
12441
13081
  )
12442
13082
  });
12443
13083
  if (!demotion) {
@@ -12463,7 +13103,7 @@ async function dispatchActivityOutdent(action, options, deps) {
12463
13103
  for (const oldParentKey of oldParents) {
12464
13104
  outdentAffectedParents.add(String(oldParentKey));
12465
13105
  }
12466
- for (const plan of planned) {
13106
+ for (const plan of allowedPlans) {
12467
13107
  if (plan.grandparentKey !== ROOT_PARENT_ID) {
12468
13108
  outdentAffectedParents.add(String(plan.grandparentKey));
12469
13109
  }
@@ -12551,6 +13191,17 @@ async function dispatchActivitySetProgress(action, options, deps) {
12551
13191
  "[ScheduleCore] progressPipeline not registered \u2014 cannot dispatch activity-set-progress"
12552
13192
  );
12553
13193
  }
13194
+ const editGate = pipeline.canEdit(activitySnap, ctx.hierarchy);
13195
+ if (!editGate.allowed) {
13196
+ const declaredButtonOverride = action.origin === "action-button" && (editGate.reason === "has_lookahead_tasks" || editGate.reason === "summary");
13197
+ if (!declaredButtonOverride) {
13198
+ return {
13199
+ ok: false,
13200
+ reason: editGate.reason,
13201
+ ...editGate.alertKey ? { alertKey: editGate.alertKey } : {}
13202
+ };
13203
+ }
13204
+ }
12554
13205
  const changes = pipeline.transform(
12555
13206
  activitySnap,
12556
13207
  action.newValue,
@@ -12609,11 +13260,11 @@ async function dispatchDatesBatch(action, options, deps) {
12609
13260
  const warnings = [];
12610
13261
  let needsAutoSchedule = false;
12611
13262
  for (const edit of action.edits) {
12612
- const outcome = applyOneEdit(edit, deps);
13263
+ const outcome = applyOneEdit2(edit, deps);
12613
13264
  verdicts.push(outcome.verdict);
12614
13265
  if (!outcome.verdict.ok || !outcome.changes) continue;
12615
13266
  const editTouched = collectTouchedIds(edit.activityId, outcome.changes);
12616
- mergeBeforeSnapshots(beforeSnap, editTouched, deps);
13267
+ mergeBeforeSnapshots2(beforeSnap, editTouched, deps);
12617
13268
  for (const id of editTouched) touchedIds.add(id);
12618
13269
  const beforeLinkLags = snapshotIncomingLinkLags(adapter, edit.activityId);
12619
13270
  const preEditSnapshot = snapshotSingleActivity(
@@ -12667,6 +13318,7 @@ async function dispatchDatesBatch(action, options, deps) {
12667
13318
  touchedIds,
12668
13319
  scheduledIds,
12669
13320
  sirDetection: "before-diff",
13321
+ preview: options.preview,
12670
13322
  links: linkChanges,
12671
13323
  trackingEvents,
12672
13324
  warnings,
@@ -12674,7 +13326,7 @@ async function dispatchDatesBatch(action, options, deps) {
12674
13326
  })
12675
13327
  };
12676
13328
  }
12677
- function applyOneEdit(edit, deps) {
13329
+ function applyOneEdit2(edit, deps) {
12678
13330
  const { ctx } = deps;
12679
13331
  const activityId = edit.activityId;
12680
13332
  const pipeline = findPipelineForColumn(edit.column);
@@ -12685,211 +13337,7 @@ function applyOneEdit(edit, deps) {
12685
13337
  }
12686
13338
  const activitySnap = ctx.activityReader.getActivity(edit.activityId);
12687
13339
  if (!activitySnap) {
12688
- throw new Error(`[ScheduleCore] activity "${edit.activityId}" not found`);
12689
- }
12690
- const editGate = pipeline.canEdit(activitySnap, ctx.hierarchy);
12691
- if (!editGate.allowed) {
12692
- return {
12693
- verdict: {
12694
- activityId,
12695
- ok: false,
12696
- reason: editGate.reason ?? REJECTION_REASON.CANNOT_EDIT,
12697
- ...editGate.alertKey ? { alertKey: editGate.alertKey } : {}
12698
- },
12699
- changes: null
12700
- };
12701
- }
12702
- const parsed2 = pipeline.parseInput(edit.newValue, activitySnap, ctx);
12703
- if (!parsed2.ok) {
12704
- return {
12705
- verdict: {
12706
- activityId,
12707
- ok: false,
12708
- reason: parsed2.reason ?? REJECTION_REASON.PARSE_ERROR
12709
- },
12710
- changes: null
12711
- };
12712
- }
12713
- const oldValue = Reflect.get(activitySnap, pipeline.targetField);
12714
- const validation = pipeline.validate(
12715
- activitySnap,
12716
- oldValue,
12717
- parsed2.value,
12718
- ctx
12719
- );
12720
- if (!validation.valid) {
12721
- return {
12722
- verdict: {
12723
- activityId,
12724
- ok: false,
12725
- reason: validation.reason ?? REJECTION_REASON.INVALID,
12726
- ...validation.notification?.messageKey ? { alertKey: validation.notification.messageKey } : {}
12727
- },
12728
- changes: null
12729
- };
12730
- }
12731
- const changes = pipeline.transform(
12732
- activitySnap,
12733
- parsed2.value,
12734
- ctx,
12735
- parsed2.raw
12736
- );
12737
- return { verdict: { activityId, ok: true }, changes };
12738
- }
12739
- function mergeBeforeSnapshots(beforeSnap, ids, deps) {
12740
- const missing = /* @__PURE__ */ new Set();
12741
- for (const id of ids) {
12742
- if (!beforeSnap.has(id)) missing.add(id);
12743
- }
12744
- if (missing.size === 0) return;
12745
- for (const missingId of missing) {
12746
- const liveActivity = deps.adapter.getActivity(missingId);
12747
- if (liveActivity) {
12748
- beforeSnap.set(missingId, structuredCloneActivity(liveActivity));
12749
- }
12750
- }
12751
- }
12752
-
12753
- // src/dispatch/handlers/bulk-edit.ts
12754
- var CUSTOM_ID_COLUMN = COLUMN.CUSTOM_ID;
12755
- async function dispatchBulkEdit(action, options, deps) {
12756
- const { adapter, scheduler, sector } = deps;
12757
- const editsIncludeDuration = action.edits.some(
12758
- (edit) => edit.column === COLUMN.DURATION
12759
- );
12760
- if (editsIncludeDuration) {
12761
- scheduler.invalidateExpandedLinksCache();
12762
- }
12763
- const verdicts = [];
12764
- const beforeSnap = /* @__PURE__ */ new Map();
12765
- const constraintPriorsByActivity = /* @__PURE__ */ new Map();
12766
- const explicitDateEditActivities = /* @__PURE__ */ new Set();
12767
- const touchedIds = /* @__PURE__ */ new Set();
12768
- const trackingEvents = [];
12769
- const linkChanges = [];
12770
- const warnings = [];
12771
- let needsAutoSchedule = false;
12772
- for (const edit of action.edits) {
12773
- const outcome = applyOneEdit2(edit, deps);
12774
- verdicts.push(outcome.verdict);
12775
- if (!outcome.verdict.ok || !outcome.changes) continue;
12776
- const constraintWarning = toConstraintWarning(
12777
- edit.activityId,
12778
- outcome.changes.constraintWarning
12779
- );
12780
- if (constraintWarning) warnings.push(constraintWarning);
12781
- const editTouched = collectTouchedIds(edit.activityId, outcome.changes);
12782
- mergeBeforeSnapshots2(beforeSnap, editTouched, deps);
12783
- for (const touchedId of editTouched) touchedIds.add(touchedId);
12784
- recordConstraintPrior(constraintPriorsByActivity, edit, beforeSnap);
12785
- if (isExplicitConstraintDateEdit(edit.column)) {
12786
- explicitDateEditActivities.add(String(edit.activityId));
12787
- }
12788
- const beforeLinkLags = snapshotIncomingLinkLags(adapter, edit.activityId);
12789
- const preEditSnapshot = snapshotSingleActivity(
12790
- adapter,
12791
- String(edit.activityId)
12792
- );
12793
- const customIdBeforeApply = readLiveCustomId(edit, deps);
12794
- const changes = outcome.changes;
12795
- applyFieldChanges(adapter, edit.activityId, changes);
12796
- runPostProcessorsOnAdapter(
12797
- edit.activityId,
12798
- outcome.changes.postProcessors ?? [],
12799
- adapter,
12800
- preEditSnapshot
12801
- );
12802
- syncCustomIdTracker(edit, customIdBeforeApply, deps);
12803
- linkChanges.push(
12804
- ...diffIncomingLinkLagChanges(beforeLinkLags, adapter, edit.activityId)
12805
- );
12806
- needsAutoSchedule ||= Boolean(outcome.changes.needsAutoSchedule);
12807
- for (const trackingEvent of outcome.changes.trackingEvents ?? []) {
12808
- trackingEvents.push({
12809
- name: trackingEvent.name,
12810
- properties: trackingEvent.properties ?? {}
12811
- });
12812
- }
12813
- }
12814
- const postMutationArgs = {
12815
- action,
12816
- autoscheduleFrom: needsAutoSchedule ? "roots" : null,
12817
- recomputeParentsFrom: touchedIds,
12818
- now: deps.now,
12819
- options
12820
- };
12821
- const batchHasConstraintEdits = constraintPriorsByActivity.size > 0;
12822
- const { scheduledIds } = await (batchHasConstraintEdits ? settleConstraintEdits(
12823
- {
12824
- adapter,
12825
- scheduler,
12826
- sector,
12827
- defaultBaseCalendarId: deps.defaultBaseCalendarId
12828
- },
12829
- {
12830
- postMutationArgs,
12831
- revertTargets: [...constraintPriorsByActivity].filter(
12832
- ([activityId]) => !explicitDateEditActivities.has(activityId)
12833
- ).map(([activityId, priorConstraintDate]) => ({
12834
- activityId,
12835
- priorConstraintDate
12836
- })),
12837
- revert: (activityId, priorConstraintDate) => revertNoOpConstraintEdit(
12838
- activityId,
12839
- priorConstraintDate,
12840
- adapter,
12841
- deps.ctx.calendars
12842
- )
12843
- }
12844
- ) : runPostMutation(
12845
- {
12846
- adapter,
12847
- scheduler,
12848
- sector,
12849
- defaultBaseCalendarId: deps.defaultBaseCalendarId
12850
- },
12851
- postMutationArgs
12852
- ));
12853
- return {
12854
- ok: true,
12855
- verdicts,
12856
- changes: await assembleChangeSet(adapter, {
12857
- source: action,
12858
- beforeSnap,
12859
- touchedIds,
12860
- scheduledIds,
12861
- sirDetection: "before-diff",
12862
- links: linkChanges,
12863
- trackingEvents,
12864
- warnings,
12865
- hoursPerDay: sector.hoursPerDay
12866
- })
12867
- };
12868
- }
12869
- function applyOneEdit2(edit, deps) {
12870
- const { ctx } = deps;
12871
- const activityId = edit.activityId;
12872
- const pipeline = findPipelineForColumn(edit.column);
12873
- if (!pipeline) {
12874
- return {
12875
- verdict: {
12876
- activityId,
12877
- ok: false,
12878
- reason: REJECTION_REASON.NO_PIPELINE_FOR_COLUMN
12879
- },
12880
- changes: null
12881
- };
12882
- }
12883
- const activitySnap = ctx.activityReader.getActivity(edit.activityId);
12884
- if (!activitySnap) {
12885
- return {
12886
- verdict: {
12887
- activityId,
12888
- ok: false,
12889
- reason: REJECTION_REASON.ACTIVITY_NOT_FOUND
12890
- },
12891
- changes: null
12892
- };
13340
+ throw new Error(`[ScheduleCore] activity "${edit.activityId}" not found`);
12893
13341
  }
12894
13342
  const editGate = pipeline.canEdit(activitySnap, ctx.hierarchy);
12895
13343
  if (!editGate.allowed) {
@@ -12932,21 +13380,6 @@ function applyOneEdit2(edit, deps) {
12932
13380
  changes: null
12933
13381
  };
12934
13382
  }
12935
- if (edit.column === CUSTOM_ID_COLUMN && parsed2.value !== null) {
12936
- const liveActivity = deps.adapter.getActivity(edit.activityId);
12937
- const oldCustomIdRaw = liveActivity ? liveActivity.customId : null;
12938
- const oldCustomId = typeof oldCustomIdRaw === "string" ? oldCustomIdRaw : null;
12939
- if (deps.customIdTracker.isCustomIdInUse(String(parsed2.value), oldCustomId)) {
12940
- return {
12941
- verdict: {
12942
- activityId,
12943
- ok: false,
12944
- reason: REJECTION_REASON.CUSTOM_ID_DUPLICATE
12945
- },
12946
- changes: null
12947
- };
12948
- }
12949
- }
12950
13383
  const changes = pipeline.transform(
12951
13384
  activitySnap,
12952
13385
  parsed2.value,
@@ -12955,33 +13388,10 @@ function applyOneEdit2(edit, deps) {
12955
13388
  );
12956
13389
  return { verdict: { activityId, ok: true }, changes };
12957
13390
  }
12958
- function readLiveCustomId(edit, deps) {
12959
- if (edit.column !== CUSTOM_ID_COLUMN) return null;
12960
- const liveActivity = deps.adapter.getActivity(edit.activityId);
12961
- const rawValue = liveActivity ? liveActivity.customId : null;
12962
- return typeof rawValue === "string" ? rawValue : null;
12963
- }
12964
- function syncCustomIdTracker(edit, oldCustomId, deps) {
12965
- if (edit.column !== CUSTOM_ID_COLUMN) return;
12966
- const liveActivity = deps.adapter.getActivity(edit.activityId);
12967
- const rawValue = liveActivity ? liveActivity.customId : null;
12968
- const newCustomId = typeof rawValue === "string" ? rawValue : null;
12969
- if (oldCustomId !== newCustomId) {
12970
- deps.customIdTracker.trackCustomIdChange(oldCustomId, newCustomId);
12971
- }
12972
- }
12973
- function recordConstraintPrior(priors, edit, beforeSnap) {
12974
- if (!isConstraintEditColumn(edit.column)) return;
12975
- const activityKey = String(edit.activityId);
12976
- if (priors.has(activityKey)) return;
12977
- const preBatchSnapshot = beforeSnap.get(activityKey);
12978
- if (!preBatchSnapshot) return;
12979
- priors.set(activityKey, preBatchSnapshot.constraintDate ?? null);
12980
- }
12981
13391
  function mergeBeforeSnapshots2(beforeSnap, ids, deps) {
12982
13392
  const missing = /* @__PURE__ */ new Set();
12983
- for (const candidateId of ids) {
12984
- if (!beforeSnap.has(candidateId)) missing.add(candidateId);
13393
+ for (const id of ids) {
13394
+ if (!beforeSnap.has(id)) missing.add(id);
12985
13395
  }
12986
13396
  if (missing.size === 0) return;
12987
13397
  for (const missingId of missing) {
@@ -13044,9 +13454,33 @@ function assertValidAssignments(assignments, entityName, readCurrent, ownerByBac
13044
13454
  );
13045
13455
  }
13046
13456
  }
13047
- function dispatchPersistenceAcknowledge(action, deps) {
13457
+ function assertValidReconciledProgress(entries, deps) {
13458
+ const seen = /* @__PURE__ */ new Set();
13459
+ for (const entry of entries) {
13460
+ const id = String(entry.activityId);
13461
+ if (id.length === 0 || seen.has(id)) {
13462
+ throw new Error(
13463
+ `persistence-acknowledge: duplicate or empty reconciled activity id "${id}"`
13464
+ );
13465
+ }
13466
+ if (!Number.isFinite(entry.progress) || entry.progress < 0 || entry.progress > 100) {
13467
+ throw new Error(
13468
+ `persistence-acknowledge: invalid reconciled progress ${String(entry.progress)} for "${id}"`
13469
+ );
13470
+ }
13471
+ if (!deps.adapter.getActivity(id)) {
13472
+ throw new Error(
13473
+ `persistence-acknowledge: unknown reconciled activity id "${id}"`
13474
+ );
13475
+ }
13476
+ seen.add(id);
13477
+ }
13478
+ }
13479
+ async function dispatchPersistenceAcknowledge(action, deps) {
13048
13480
  const activities = action.activities ?? [];
13049
13481
  const links = action.links ?? [];
13482
+ const reconciledProgress = action.reconciledProgress ?? [];
13483
+ assertValidReconciledProgress(reconciledProgress, deps);
13050
13484
  const activityOwnerByBackendId = /* @__PURE__ */ new Map();
13051
13485
  for (const activity of deps.adapter.getAllActivities()) {
13052
13486
  if (activity.proplannerId != null) {
@@ -13082,7 +13516,49 @@ function dispatchPersistenceAcknowledge(action, deps) {
13082
13516
  );
13083
13517
  const activityChanges = [];
13084
13518
  const linkChanges = [];
13519
+ const progressToApply = reconciledProgress.filter((entry) => {
13520
+ const current = deps.adapter.getActivity(String(entry.activityId));
13521
+ return current !== null && current.progress !== entry.progress;
13522
+ });
13523
+ const touchedForProgress = new Set(
13524
+ progressToApply.map((entry) => String(entry.activityId))
13525
+ );
13526
+ const parentsToRollup = /* @__PURE__ */ new Set();
13527
+ for (const entry of progressToApply) {
13528
+ const activity = deps.adapter.getActivity(String(entry.activityId));
13529
+ if (activity && !isRootParent(activity.parentId)) {
13530
+ parentsToRollup.add(String(activity.parentId));
13531
+ touchedForProgress.add(String(activity.parentId));
13532
+ }
13533
+ }
13534
+ const beforeProgressSnap = await snapshotActivities(
13535
+ deps.adapter,
13536
+ touchedForProgress
13537
+ );
13085
13538
  deps.adapter.batchUpdate(() => {
13539
+ for (const entry of progressToApply) {
13540
+ deps.adapter.setActivityField(
13541
+ String(entry.activityId),
13542
+ "progress",
13543
+ entry.progress
13544
+ );
13545
+ }
13546
+ for (const parentId of parentsToRollup) {
13547
+ recomputeRollupCascadesForParent(parentId, deps.adapter);
13548
+ }
13549
+ for (const id of touchedForProgress) {
13550
+ const before = beforeProgressSnap.get(id);
13551
+ const after = deps.adapter.getActivity(id);
13552
+ if (!before || !after || before.progress === after.progress) continue;
13553
+ activityChanges.push({
13554
+ id,
13555
+ kind: "updated",
13556
+ fields: {
13557
+ progress: { before: before.progress, after: after.progress }
13558
+ },
13559
+ after: { ...after }
13560
+ });
13561
+ }
13086
13562
  for (const assignment of activities) {
13087
13563
  const before = deps.adapter.getActivity(assignment.id);
13088
13564
  if (!before || before.proplannerId === assignment.proplannerId) continue;
@@ -13489,8 +13965,218 @@ async function dispatchStatusCriteriaSet(action, deps) {
13489
13965
  };
13490
13966
  }
13491
13967
 
13968
+ // src/activity-copy-paste/policy.ts
13969
+ var ACTIVITY_COPY_FIELDS = [
13970
+ "name",
13971
+ "description",
13972
+ "type",
13973
+ "startDate",
13974
+ "durationHours",
13975
+ "constraintType",
13976
+ "constraintDate",
13977
+ "calendarId",
13978
+ "cost",
13979
+ "hasCustomPonderator",
13980
+ "subcontractId",
13981
+ "responsableIds",
13982
+ "tagIds"
13983
+ ];
13984
+
13985
+ // src/activity-copy-paste/copy.ts
13986
+ function copyOverrides(activity) {
13987
+ const overrides = {};
13988
+ for (const field of ACTIVITY_COPY_FIELDS) {
13989
+ Object.assign(overrides, { [field]: cloneDomainValue(activity[field]) });
13990
+ }
13991
+ return overrides;
13992
+ }
13993
+ function selectedInTreeOrder(adapter, selected) {
13994
+ const ordered = [];
13995
+ const visit = (id) => {
13996
+ if (selected.has(String(id))) ordered.push(id);
13997
+ for (const childId of adapter.getChildren(id)) visit(childId);
13998
+ };
13999
+ for (const rootId of adapter.getRootIds()) visit(rootId);
14000
+ return ordered;
14001
+ }
14002
+ function dispatchActivityCopy(action, deps) {
14003
+ const selected = new Set(deps.adapter.checkedIds().map(String));
14004
+ if (selected.size === 0) {
14005
+ return { ok: false, reason: REJECTION_REASON.NO_ACTIVITY_SELECTION };
14006
+ }
14007
+ const orderedIds = selectedInTreeOrder(deps.adapter, selected);
14008
+ deps.clipboard.value = {
14009
+ activities: orderedIds.map((id) => {
14010
+ const activity = deps.adapter.getActivity(id);
14011
+ if (activity === null)
14012
+ throw new Error(`Selected activity ${id} is missing`);
14013
+ return {
14014
+ originalId: id,
14015
+ originalParentId: activity.parentId ?? "0",
14016
+ overrides: copyOverrides(activity)
14017
+ };
14018
+ }),
14019
+ links: deps.adapter.getAllLinks().filter(
14020
+ (link) => selected.has(String(link.source)) && selected.has(String(link.target))
14021
+ ).map((link) => cloneDomainValue(link))
14022
+ };
14023
+ const selection = dispatchSelectionReplace(
14024
+ { kind: "selection-replace", activityIds: [] },
14025
+ { adapter: deps.adapter }
14026
+ );
14027
+ if (!selection.ok) return selection;
14028
+ return { ...selection, changes: { ...selection.changes, source: action } };
14029
+ }
14030
+ function dispatchActivityClipboardClear(action, clipboard) {
14031
+ clipboard.value = null;
14032
+ return {
14033
+ ok: true,
14034
+ changes: {
14035
+ source: action,
14036
+ activities: [],
14037
+ links: [],
14038
+ calendars: [],
14039
+ trackingEvents: []
14040
+ }
14041
+ };
14042
+ }
14043
+
14044
+ // src/activity-copy-paste/paste.ts
14045
+ function resolveDestination(afterSiblingId, adapter) {
14046
+ const sibling = adapter.getActivity(afterSiblingId);
14047
+ if (!sibling) {
14048
+ return { ok: false, reason: REJECTION_REASON.ANCHOR_SIBLING_NOT_FOUND };
14049
+ }
14050
+ return {
14051
+ ok: true,
14052
+ parentId: sibling.parentId == null ? ROOT_PARENT_ID : String(sibling.parentId)
14053
+ };
14054
+ }
14055
+ var invariantFailure = () => ({
14056
+ ok: false,
14057
+ reason: REJECTION_REASON.ACTIVITY_PASTE_INVARIANT_FAILED
14058
+ });
14059
+ async function dispatchActivityPasteCopied(action, options, deps) {
14060
+ const clipboard = deps.activityClipboard.value;
14061
+ if (clipboard === null) {
14062
+ return { ok: false, reason: REJECTION_REASON.CLIPBOARD_EMPTY };
14063
+ }
14064
+ const destination = resolveDestination(
14065
+ action.destination.afterSiblingId,
14066
+ deps.adapter
14067
+ );
14068
+ if (!destination.ok) return destination;
14069
+ const beforeSnapshot = await snapshotActivities(
14070
+ deps.adapter,
14071
+ /* @__PURE__ */ new Set()
14072
+ );
14073
+ const touched = /* @__PURE__ */ new Set();
14074
+ const originalToNew = /* @__PURE__ */ new Map();
14075
+ const createdIds = [];
14076
+ let previousRootId;
14077
+ for (const copied of clipboard.activities) {
14078
+ const mappedParent = originalToNew.get(copied.originalParentId);
14079
+ const created = await createActivityCore(
14080
+ {
14081
+ parentId: mappedParent ?? destination.parentId,
14082
+ ...mappedParent === void 0 ? {
14083
+ afterSiblingId: previousRootId ?? action.destination.afterSiblingId
14084
+ } : {},
14085
+ overrides: cloneDomainValue(copied.overrides),
14086
+ eventSource: action.eventSource
14087
+ },
14088
+ deps,
14089
+ {
14090
+ skipCorrelativeRecompute: true,
14091
+ skipBeforeSnap: true,
14092
+ customIdReferenceId: action.destination.afterSiblingId
14093
+ }
14094
+ );
14095
+ if (!created.ok) return created;
14096
+ originalToNew.set(copied.originalId, created.newId);
14097
+ createdIds.push(created.newId);
14098
+ if (mappedParent === void 0) previousRootId = created.newId;
14099
+ }
14100
+ const correlativeShifts = recomputeCorrelativeIds(deps.adapter);
14101
+ const createdIdSet = new Set(createdIds.map(String));
14102
+ foldCorrelativeShifts(
14103
+ deps.adapter,
14104
+ correlativeShifts,
14105
+ createdIdSet,
14106
+ beforeSnapshot,
14107
+ touched
14108
+ );
14109
+ for (const createdId of createdIds) touched.add(String(createdId));
14110
+ const appliedLinks = [];
14111
+ for (const copiedLink of clipboard.links) {
14112
+ const source = originalToNew.get(copiedLink.source);
14113
+ const target = originalToNew.get(copiedLink.target);
14114
+ if (source === void 0 || target === void 0) return invariantFailure();
14115
+ const operation = {
14116
+ kind: "create",
14117
+ source,
14118
+ target,
14119
+ type: copiedLink.type,
14120
+ lag: copiedLink.lag
14121
+ };
14122
+ const result = applyLinkOperation(operation, {
14123
+ port: deps.adapter,
14124
+ newLinkId: deps.linkIdGen.next
14125
+ });
14126
+ if (!result.applied) return invariantFailure();
14127
+ appliedLinks.push({
14128
+ op: operation,
14129
+ finalLinkId: result.linkId,
14130
+ rejected: null
14131
+ });
14132
+ }
14133
+ const { scheduledIds } = await runPostMutation(
14134
+ {
14135
+ adapter: deps.adapter,
14136
+ scheduler: deps.scheduler,
14137
+ sector: deps.sector,
14138
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
14139
+ },
14140
+ {
14141
+ action,
14142
+ autoscheduleFrom: createdIds[0] ?? null,
14143
+ recomputeParentsFrom: createdIds,
14144
+ now: deps.now,
14145
+ options
14146
+ }
14147
+ );
14148
+ const linkChanges = buildLinkChangesForBatch(
14149
+ appliedLinks,
14150
+ /* @__PURE__ */ new Map(),
14151
+ deps.adapter
14152
+ );
14153
+ return {
14154
+ ok: true,
14155
+ changes: await assembleChangeSet(deps.adapter, {
14156
+ source: action,
14157
+ beforeSnap: beforeSnapshot,
14158
+ touchedIds: touched,
14159
+ scheduledIds,
14160
+ links: linkChanges,
14161
+ hoursPerDay: deps.sector.hoursPerDay
14162
+ })
14163
+ };
14164
+ }
14165
+
13492
14166
  // src/dispatch/dispatch.ts
13493
14167
  async function performDispatch(action, options, deps) {
14168
+ if (action.kind === "activity-copy") {
14169
+ return dispatchActivityCopy(action, {
14170
+ adapter: deps.adapter,
14171
+ clipboard: deps.activityClipboard
14172
+ });
14173
+ }
14174
+ if (action.kind === "activity-clipboard-clear") {
14175
+ return dispatchActivityClipboardClear(action, deps.activityClipboard);
14176
+ }
14177
+ if (action.kind === "activity-paste-copied") {
14178
+ return dispatchActivityPasteCopied(action, options, deps);
14179
+ }
13494
14180
  if (action.kind === "activity-batch") {
13495
14181
  return dispatchActivityBatch(action, options, deps);
13496
14182
  }
@@ -13515,9 +14201,6 @@ async function performDispatch(action, options, deps) {
13515
14201
  if (action.kind === "activity-create") {
13516
14202
  return dispatchActivityCreate(action, options, deps);
13517
14203
  }
13518
- if (action.kind === "activity-paste") {
13519
- return dispatchActivityPaste(action, options, deps);
13520
- }
13521
14204
  if (action.kind === "activity-delete") {
13522
14205
  return dispatchActivityDelete(action, options, deps);
13523
14206
  }
@@ -13560,6 +14243,9 @@ async function performDispatch(action, options, deps) {
13560
14243
  if (action.kind === "sir-sync") {
13561
14244
  return dispatchSirSync(action, deps);
13562
14245
  }
14246
+ if (action.kind === "sir-approve") {
14247
+ return dispatchSirApprove(action, options, deps);
14248
+ }
13563
14249
  if (action.kind === "activity-lookahead-sync") {
13564
14250
  return dispatchActivityLookaheadSync(action, deps);
13565
14251
  }
@@ -16972,6 +17658,7 @@ var ScheduleState = class {
16972
17658
  if (!activity) return;
16973
17659
  this._writeCapture.note(String(activityId), activity);
16974
17660
  activity[field] = value;
17661
+ refreshCalendarDurationIfDateBearing(activity, field);
16975
17662
  if (field === ACTIVITY_PROPERTY.PARENT) this._hierarchy.markDirty();
16976
17663
  if (fieldAffectsVisualOrder(field)) this._cache.visualOrderIds = null;
16977
17664
  }
@@ -16985,6 +17672,7 @@ var ScheduleState = class {
16985
17672
  }
16986
17673
  this._writeCapture.note(String(activityId), activity);
16987
17674
  Reflect.set(activity, field, coerceActivityFieldValue(field, value));
17675
+ refreshCalendarDurationIfDateBearing(activity, field);
16988
17676
  if (field === ACTIVITY_PROPERTY.PARENT) this._hierarchy.markDirty();
16989
17677
  if (fieldAffectsVisualOrder(field)) this._cache.visualOrderIds = null;
16990
17678
  }
@@ -16995,6 +17683,7 @@ var ScheduleState = class {
16995
17683
  let reparented = false;
16996
17684
  for (const field of Object.keys(fields)) {
16997
17685
  Reflect.set(activity, field, Reflect.get(fields, field));
17686
+ refreshCalendarDurationIfDateBearing(activity, field);
16998
17687
  if (field === ACTIVITY_PROPERTY.PARENT) reparented = true;
16999
17688
  if (fieldAffectsVisualOrder(field)) this._cache.visualOrderIds = null;
17000
17689
  }
@@ -17163,6 +17852,12 @@ function isMutableLinkField(field) {
17163
17852
  function fieldAffectsVisualOrder(field) {
17164
17853
  return field === ACTIVITY_PROPERTY.PARENT || field === ACTIVITY_PROPERTY.CORRELATIVE_ID;
17165
17854
  }
17855
+ function refreshCalendarDurationIfDateBearing(activity, field) {
17856
+ if (field !== ACTIVITY_PROPERTY.START_DATE && field !== ACTIVITY_PROPERTY.END_DATE && field !== ACTIVITY_PROPERTY.TYPE) {
17857
+ return;
17858
+ }
17859
+ activity.calendarDuration = computeCalendarDuration(activity);
17860
+ }
17166
17861
  function detachLinkRef(refs, linkId) {
17167
17862
  return refs.filter((ref) => String(ref) !== String(linkId));
17168
17863
  }
@@ -17210,15 +17905,7 @@ function readChildren(state, parentId) {
17210
17905
  return out;
17211
17906
  }
17212
17907
  function readChildrenIds(state, parentId) {
17213
- const key = parentId;
17214
- if (key === ROOT_PARENT_ID) {
17215
- const roots = [];
17216
- state.forEachActivity((activity, id) => {
17217
- if (isRootParent(activity.parentId)) roots.push(id);
17218
- });
17219
- return roots;
17220
- }
17221
- return getChildrenInVisualOrder(key, state);
17908
+ return getChildrenInVisualOrder(parentId, state);
17222
17909
  }
17223
17910
  function readSelectedActivityIds(state) {
17224
17911
  return state.checkedIds().map(String);
@@ -17282,7 +17969,11 @@ function buildPipelineContext(adapter, options) {
17282
17969
  dateFormat: options.dateFormat ?? "YYYY-MM-DD",
17283
17970
  customHours: options.customHours ?? {},
17284
17971
  hoursPerDay: options.hoursPerDay,
17285
- inputUnit: options.inputUnit ?? "days"
17972
+ // ISSUE-057 (CP6): sin default. Un comando de duración sin inputUnit se
17973
+ // rechaza en el pipeline; el resto de columnas no lo leen. La clave se
17974
+ // OMITE cuando no se declaró (exactOptionalPropertyTypes), no se pone a
17975
+ // undefined.
17976
+ ...options.inputUnit !== void 0 ? { inputUnit: options.inputUnit } : {}
17286
17977
  };
17287
17978
  }
17288
17979
 
@@ -17362,9 +18053,10 @@ function mergeCoalesced(top, next) {
17362
18053
  };
17363
18054
  return buildUndoEntry(changeSet);
17364
18055
  }
17365
- function getDispatchHistoryPolicy(action) {
18056
+ function getDispatchHistoryPolicy(action, options) {
18057
+ if (options?.preview === true) return "skip";
17366
18058
  if (action.kind === "persistence-acknowledge") return "clear-on-success";
17367
- 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") {
18059
+ 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") {
17368
18060
  return "skip";
17369
18061
  }
17370
18062
  return "record";
@@ -18148,8 +18840,13 @@ function parseActivity(raw, context) {
18148
18840
  Number(raw.sumOfDurationRecursively ?? 0),
18149
18841
  context.hoursPerDay
18150
18842
  ),
18843
+ calendarDuration: computeCalendarDuration({
18844
+ type: raw.type,
18845
+ startDate,
18846
+ endDate
18847
+ }),
18151
18848
  isCritical: Boolean(raw.is_critical),
18152
- freeSlackHours: raw.freeSlack,
18849
+ freeSlackHours: null,
18153
18850
  expectedProgress: null,
18154
18851
  expectedProgressBaseline: null,
18155
18852
  status: null,
@@ -18512,7 +19209,8 @@ function parseSector(sector) {
18512
19209
  ...customIdIncrement !== void 0 ? { customIdIncrement } : {},
18513
19210
  ...isPrimaveraEndDate ? { updateDurationForPrimaveraEndDate: true } : {},
18514
19211
  activityCreter: parseActivityCreter(sector.activity_creter),
18515
- statusCriteria: DEFAULT_STATUS_CRITERIA
19212
+ statusCriteria: DEFAULT_STATUS_CRITERIA,
19213
+ ganttId: null
18516
19214
  };
18517
19215
  }
18518
19216
 
@@ -18598,7 +19296,7 @@ function createActivityIdGenerator(options = {}) {
18598
19296
 
18599
19297
  // src/generators/link-id-generator.ts
18600
19298
  function createLinkIdGenerator(options = {}) {
18601
- let counter = options.seed ?? Date.now();
19299
+ let counter = options.seed ?? 0;
18602
19300
  const raise = (value) => {
18603
19301
  if (value > counter) counter = value;
18604
19302
  };
@@ -19304,6 +20002,7 @@ function recomputeSkippedLeafEnds(state) {
19304
20002
  durationHours: activity.durationHours,
19305
20003
  task: activity
19306
20004
  });
20005
+ activity.calendarDuration = computeCalendarDuration(activity);
19307
20006
  });
19308
20007
  }
19309
20008
  function recomputeDurationsFromDates(state) {
@@ -19360,7 +20059,8 @@ var SCHEDULE_CORE_STATUS = {
19360
20059
  };
19361
20060
  function initializeCore(input) {
19362
20061
  const clock = input.clock ?? null;
19363
- const loadNow = clock ? endOfLocalDay(clock()) : null;
20062
+ const initializedAt = clock?.() ?? null;
20063
+ const loadNow = initializedAt ? endOfLocalDay(initializedAt) : null;
19364
20064
  const reporter = input.reporter ?? noopReporter;
19365
20065
  const parsed2 = parseFromBackend(
19366
20066
  {
@@ -19374,6 +20074,7 @@ function initializeCore(input) {
19374
20074
  );
19375
20075
  const sector = parsed2.sector;
19376
20076
  sector.statusCriteria = resolveStatusCriteria(input.statusCriteria);
20077
+ sector.ganttId = input.ganttId ?? null;
19377
20078
  const calendars = parsed2.calendars;
19378
20079
  const baseCalendars = parsed2.baseCalendars ?? [];
19379
20080
  const snapshot = buildStateSnapshot(
@@ -19391,6 +20092,7 @@ function initializeCore(input) {
19391
20092
  knownIds: parsed2.activities.map((a) => a.id)
19392
20093
  });
19393
20094
  const linkIdGen = createLinkIdGenerator({
20095
+ seed: initializedAt?.getTime() ?? 0,
19394
20096
  knownIds: parsed2.links.map((l) => l.id)
19395
20097
  });
19396
20098
  const uidGen = createUniqueCorrelativeIdGenerator({
@@ -19579,6 +20281,7 @@ function evaluateEnd(formula, proposedEdgeTimeMs) {
19579
20281
  }
19580
20282
 
19581
20283
  // src/init/schedule-core.ts
20284
+ var PREVIEW_DISPATCH_KINDS = /* @__PURE__ */ new Set(["inline-edit", "bulk-edit", "dates-batch"]);
19582
20285
  function changesRequireFilterProjectionRebuild(changes) {
19583
20286
  for (const change of changes.activities) {
19584
20287
  if (change.kind === "deleted") return true;
@@ -19603,6 +20306,7 @@ function isCurrentFilterProjection(projection, filter, context) {
19603
20306
  return hasCurrentHours && hasCurrentLocale;
19604
20307
  }
19605
20308
  var ScheduleCore = class {
20309
+ _activityClipboard = { value: null };
19606
20310
  _status = SCHEDULE_CORE_STATUS.READY;
19607
20311
  coreRuntime;
19608
20312
  _undo = new UndoRecorder();
@@ -19698,6 +20402,23 @@ var ScheduleCore = class {
19698
20402
  this.assertReady();
19699
20403
  return readAllActivities(this.coreRuntime.state);
19700
20404
  }
20405
+ /**
20406
+ * Read del plan de indent (ISSUE-080): los padres destino DISTINTOS bajo los
20407
+ * que quedarían las actividades si se indentaran ahora. Es exactamente la
20408
+ * planificación que ejecuta el handler (`planIndentMoves`, autoridad única),
20409
+ * expuesta para que la frontera pueda correr el protocolo de conversión a
20410
+ * madre (chequeo backend + modal) ANTES de despachar. Solo lecturas.
20411
+ */
20412
+ getIndentTargetParentIds(activityIds) {
20413
+ this.assertReady();
20414
+ const plan = planIndentMoves(
20415
+ activityIds.map((id) => String(id)),
20416
+ this.coreRuntime.state
20417
+ );
20418
+ return [
20419
+ ...new Set(plan.allowedMoves.map((move) => String(move.newParentId)))
20420
+ ];
20421
+ }
19701
20422
  getChildrenView(parentId) {
19702
20423
  this.assertReady();
19703
20424
  return readChildren(this.coreRuntime.state, parentId);
@@ -19710,6 +20431,26 @@ var ScheduleCore = class {
19710
20431
  this.assertReady();
19711
20432
  return readAllLinks(this.coreRuntime.state);
19712
20433
  }
20434
+ auditEffectiveGraph() {
20435
+ this.assertReady();
20436
+ const state = this.coreRuntime.state;
20437
+ const logicalLinks = state.getAllLinks();
20438
+ const effectiveEdges = projectEffectiveEdges(state, logicalLinks);
20439
+ const components = circularComponents(effectiveEdges);
20440
+ let largestComponentSize = 0;
20441
+ for (const component of components) {
20442
+ if (component.length > largestComponentSize) {
20443
+ largestComponentSize = component.length;
20444
+ }
20445
+ }
20446
+ return {
20447
+ activityCount: state.getAllIds().length,
20448
+ logicalLinkCount: logicalLinks.length,
20449
+ effectiveEdgeCount: effectiveEdges.length,
20450
+ circularComponents: components,
20451
+ largestComponentSize
20452
+ };
20453
+ }
19713
20454
  getProperty(activityId, property) {
19714
20455
  this.assertReady();
19715
20456
  const snapshot = this.coreRuntime.state.getActivity(activityId);
@@ -19738,11 +20479,11 @@ var ScheduleCore = class {
19738
20479
  }
19739
20480
  getActiveFilter() {
19740
20481
  this.assertReady();
19741
- return this.coreRuntime.state.getActiveFilter();
20482
+ return cloneDomainValue(this.coreRuntime.state.getActiveFilter());
19742
20483
  }
19743
20484
  getActiveOrder() {
19744
20485
  this.assertReady();
19745
- return this.coreRuntime.state.getActiveOrder();
20486
+ return cloneDomainValue(this.coreRuntime.state.getActiveOrder());
19746
20487
  }
19747
20488
  getVisualOrderIds() {
19748
20489
  this.assertReady();
@@ -19793,6 +20534,74 @@ var ScheduleCore = class {
19793
20534
  this.assertReady();
19794
20535
  return this._saveTracker.hasUnsavedChanges();
19795
20536
  }
20537
+ /**
20538
+ * Read-only view of the internal schedule revision. It moves once per
20539
+ * substantive CANONICAL mutation (dispatch, undo, redo). It does NOT move
20540
+ * for a preview dispatch nor for `applyPreviewInverse`: comparing the value
20541
+ * captured in `PreviewDispatchInfo.revision` against the current one
20542
+ * answers "did the schedule change while the preview was open".
20543
+ */
20544
+ getScheduleRevision() {
20545
+ return this._scheduleRevision;
20546
+ }
20547
+ /**
20548
+ * Exit channel for a preview dispatch (`DispatchOptions.preview`): applies
20549
+ * the inverse ChangeSet the preview returned, restoring the canonical
20550
+ * state exactly. Like the preview itself it leaves no session trace: no
20551
+ * save-tracker dirty, no undo entry, no effects, and the schedule revision
20552
+ * does not move (see `getScheduleRevision`).
20553
+ *
20554
+ * Returns the ChangeSet to project to the view layer: the same changes as
20555
+ * `inverse`, but with `after` images re-read from the restored state (the
20556
+ * ones inside `inverse` were captured while the preview edit was applied).
20557
+ */
20558
+ applyPreviewInverse(inverse) {
20559
+ return this._enqueue(async () => {
20560
+ this.assertReady();
20561
+ const state = this.coreRuntime.state;
20562
+ const afterSnap = /* @__PURE__ */ new Map();
20563
+ for (const change of inverse.activities) {
20564
+ if (change.kind !== "deleted" && change.after) {
20565
+ afterSnap.set(
20566
+ String(change.id),
20567
+ structuredCloneActivity(change.after)
20568
+ );
20569
+ }
20570
+ }
20571
+ const afterLinks = /* @__PURE__ */ new Map();
20572
+ for (const change of inverse.links) {
20573
+ if (change.kind !== "deleted" && change.after) {
20574
+ afterLinks.set(String(change.id), { ...change.after });
20575
+ }
20576
+ }
20577
+ const entry = buildUndoEntry(
20578
+ inverse,
20579
+ void 0,
20580
+ void 0,
20581
+ afterSnap,
20582
+ afterLinks
20583
+ );
20584
+ applyRedo(state, entry);
20585
+ if (needsResync(entry)) this._resyncCustomIdTrackerFromModel();
20586
+ const restored = {
20587
+ ...inverse,
20588
+ activities: inverse.activities.map((change) => {
20589
+ if (change.kind === "deleted") return { ...change, after: null };
20590
+ const live = state.getActivity(String(change.id));
20591
+ return {
20592
+ ...change,
20593
+ after: live ? structuredCloneActivity(live) : null
20594
+ };
20595
+ }),
20596
+ links: inverse.links.map((change) => {
20597
+ if (change.kind === "deleted") return { ...change, after: null };
20598
+ const live = state.getLink(String(change.id));
20599
+ return { ...change, after: live ? snapshotToLink(live) : null };
20600
+ })
20601
+ };
20602
+ return this._reapplyViewState(restored) ?? restored;
20603
+ });
20604
+ }
19796
20605
  getDeletedActivitiesSinceLastSave() {
19797
20606
  this.assertReady();
19798
20607
  return this._saveTracker.deletedActivities(this.coreRuntime.state.getAllActivities()).map(cloneDomainValue);
@@ -19875,11 +20684,11 @@ var ScheduleCore = class {
19875
20684
  });
19876
20685
  }
19877
20686
  async dispatch(action, options = {}) {
19878
- const admitted = this._beginScheduleMutation(action);
20687
+ const admitted = options.preview === true ? false : this._beginScheduleMutation(action);
19879
20688
  return this._enqueue(() => this._dispatchInner(action, options, admitted));
19880
20689
  }
19881
20690
  async applyActivityBatch(action, options = {}) {
19882
- const admitted = this._beginScheduleMutation(action);
20691
+ const admitted = options.preview === true ? false : this._beginScheduleMutation(action);
19883
20692
  return this._enqueue(() => this._dispatchInner(action, options, admitted));
19884
20693
  }
19885
20694
  async _dispatchInner(action, options = {}, admitted = false) {
@@ -19894,12 +20703,23 @@ var ScheduleCore = class {
19894
20703
  }
19895
20704
  async _runDispatch(action, options) {
19896
20705
  this.assertReady();
20706
+ if (options.preview === true && !PREVIEW_DISPATCH_KINDS.has(action.kind)) {
20707
+ return {
20708
+ result: {
20709
+ ok: false,
20710
+ reason: REJECTION_REASON.PREVIEW_UNSUPPORTED_ACTION
20711
+ },
20712
+ changedState: false
20713
+ };
20714
+ }
19897
20715
  const pipelineContext = buildPipelineContext(this.coreRuntime.state, {
19898
20716
  hoursPerDay: this.coreRuntime.sector.hoursPerDay,
19899
20717
  dateFormat: this.coreRuntime.sector.dateFormat,
19900
- inputUnit: options.inputUnit ?? "days"
20718
+ // ISSUE-057 (CP6): sin default silencioso 'days'; la clave se omite si
20719
+ // no se declaró (exactOptionalPropertyTypes).
20720
+ ...options.inputUnit !== void 0 ? { inputUnit: options.inputUnit } : {}
19901
20721
  });
19902
- const historyPolicy = getDispatchHistoryPolicy(action);
20722
+ const historyPolicy = getDispatchHistoryPolicy(action, options);
19903
20723
  const generatorSnapshot = [
19904
20724
  this.coreRuntime.activityIdGenerator.snapshot(),
19905
20725
  this.coreRuntime.linkIdGenerator.snapshot(),
@@ -19920,6 +20740,7 @@ var ScheduleCore = class {
19920
20740
  uidGen: this.coreRuntime.uniqueCorrelativeIdGenerator,
19921
20741
  customIdTracker: this.coreRuntime.customIdTracker,
19922
20742
  reporter: this.coreRuntime.reporter,
20743
+ activityClipboard: this._activityClipboard,
19923
20744
  defaultBaseCalendarId: this.coreRuntime.baseCalendars.find(
19924
20745
  (calendar) => calendar.baseDefault
19925
20746
  )?.id ?? null,
@@ -19962,7 +20783,7 @@ var ScheduleCore = class {
19962
20783
  action.activityIds,
19963
20784
  (activityId) => this.coreRuntime.state.getActivity(activityId)
19964
20785
  );
19965
- } else {
20786
+ } else if (options.preview !== true) {
19966
20787
  this._saveTracker.recordChanges(
19967
20788
  result.changes.activities,
19968
20789
  result.changes.links,
@@ -19973,6 +20794,20 @@ var ScheduleCore = class {
19973
20794
  }
19974
20795
  result = this._withReappliedViewState(action, result);
19975
20796
  if (!result.ok) return { result, changedState: false };
20797
+ if (options.preview === true) {
20798
+ const inverse = buildInverseChangeSet(
20799
+ this.coreRuntime.state,
20800
+ buildUndoEntry(buildHistoryChangeSet(result.changes)),
20801
+ "before"
20802
+ );
20803
+ return {
20804
+ result: {
20805
+ ...toPublicDispatchResult(result),
20806
+ preview: { inverse, revision: this._scheduleRevision }
20807
+ },
20808
+ changedState: false
20809
+ };
20810
+ }
19976
20811
  if (dispatchChangesSchedulingState(action) && changeSetIsSubstantive(result.changes)) {
19977
20812
  this._recordScheduleMutation(
19978
20813
  options.skipAutoSchedule !== true && options.skipCriticalPath !== true && resolveRunCriticalPath(action)
@@ -20487,6 +21322,9 @@ var DEFAULT_HOURS_PER_DAY = 8;
20487
21322
 
20488
21323
  // src/dispatch/action-kinds.ts
20489
21324
  var DISPATCH_ACTION_KIND = {
21325
+ ACTIVITY_CLIPBOARD_CLEAR: "activity-clipboard-clear",
21326
+ ACTIVITY_COPY: "activity-copy",
21327
+ ACTIVITY_PASTE_COPIED: "activity-paste-copied",
20490
21328
  PERSISTENCE_ACKNOWLEDGE: "persistence-acknowledge",
20491
21329
  BASELINE_APPLY: "baseline-apply",
20492
21330
  PONDERATOR_CRITERION_SET: "ponderator-criterion-set",
@@ -20504,16 +21342,19 @@ var DISPATCH_ACTION_KIND = {
20504
21342
  ACTIVITY_INDENT: "activity-indent",
20505
21343
  ACTIVITY_OUTDENT: "activity-outdent",
20506
21344
  ACTIVITY_SET_PROGRESS: "activity-set-progress",
20507
- ACTIVITY_PASTE: "activity-paste",
20508
21345
  SELECTION_TOGGLE: "selection-toggle",
20509
21346
  SELECTION_REPLACE: "selection-replace",
20510
21347
  VISIBILITY_SET: "visibility-set",
20511
21348
  FILTER_SET: "filter-set",
20512
21349
  SORT_SET: "sort-set",
20513
21350
  SIR_SYNC: "sir-sync",
21351
+ SIR_APPROVE: "sir-approve",
20514
21352
  ACTIVITY_LOOKAHEAD_SYNC: "activity-lookahead-sync"
20515
21353
  };
20516
21354
  var KIND_CATALOG_COVERS_UNION = {
21355
+ [DISPATCH_ACTION_KIND.ACTIVITY_CLIPBOARD_CLEAR]: true,
21356
+ [DISPATCH_ACTION_KIND.ACTIVITY_COPY]: true,
21357
+ [DISPATCH_ACTION_KIND.ACTIVITY_PASTE_COPIED]: true,
20517
21358
  [DISPATCH_ACTION_KIND.PERSISTENCE_ACKNOWLEDGE]: true,
20518
21359
  [DISPATCH_ACTION_KIND.BASELINE_APPLY]: true,
20519
21360
  [DISPATCH_ACTION_KIND.PONDERATOR_CRITERION_SET]: true,
@@ -20531,13 +21372,13 @@ var KIND_CATALOG_COVERS_UNION = {
20531
21372
  [DISPATCH_ACTION_KIND.ACTIVITY_INDENT]: true,
20532
21373
  [DISPATCH_ACTION_KIND.ACTIVITY_OUTDENT]: true,
20533
21374
  [DISPATCH_ACTION_KIND.ACTIVITY_SET_PROGRESS]: true,
20534
- [DISPATCH_ACTION_KIND.ACTIVITY_PASTE]: true,
20535
21375
  [DISPATCH_ACTION_KIND.SELECTION_TOGGLE]: true,
20536
21376
  [DISPATCH_ACTION_KIND.SELECTION_REPLACE]: true,
20537
21377
  [DISPATCH_ACTION_KIND.VISIBILITY_SET]: true,
20538
21378
  [DISPATCH_ACTION_KIND.FILTER_SET]: true,
20539
21379
  [DISPATCH_ACTION_KIND.SORT_SET]: true,
20540
21380
  [DISPATCH_ACTION_KIND.SIR_SYNC]: true,
21381
+ [DISPATCH_ACTION_KIND.SIR_APPROVE]: true,
20541
21382
  [DISPATCH_ACTION_KIND.ACTIVITY_LOOKAHEAD_SYNC]: true
20542
21383
  };
20543
21384
  var DISPATCH_ACTION_KINDS = Object.keys(KIND_CATALOG_COVERS_UNION);
@@ -20550,6 +21391,6 @@ var CREATION_KIND = {
20550
21391
  ROOT: "root"
20551
21392
  };
20552
21393
 
20553
- export { ACTIVITY_TYPE, CALENDAR_UNIT, COLUMN, CONSTRAINT_TYPE2 as CONSTRAINT_TYPE, CONSTRAINT_TYPE_TO_LABEL, CREATION_KIND, DEFAULT_HOURS_PER_DAY, DISPATCH_ACTION_KIND, DISPATCH_ACTION_KINDS, DISPATCH_TRACK_EVENT, LINK_CODE_TO_TYPE, LINK_TYPE, LINK_TYPE_CODE, NEW_ACTIVITY_DEFAULTS, REJECTION_REASON, ROOT_PARENT_ID, ScheduleCore, WORK_TIME_DIRECTION, checkNoUpdatedLinks, computeExpectedProgress, expectedProgressFromBaseline, getActiveBaseline, getUnsavedActivities, isRootParent, normalizeParentKey, parseFromBackend, willRunCriticalPath, yieldToBrowser };
21394
+ export { ACTIVITY_TYPE, CALENDAR_UNIT, COLUMN, CONSTRAINT_TYPE2 as CONSTRAINT_TYPE, CONSTRAINT_TYPE_TO_LABEL, CREATION_KIND, DEFAULT_HOURS_PER_DAY, DISPATCH_ACTION_KIND, DISPATCH_ACTION_KINDS, DISPATCH_TRACK_EVENT, LINK_CODE_TO_TYPE, LINK_TYPE, LINK_TYPE_CODE, NEW_ACTIVITY_DEFAULTS, REJECTION_REASON, ROOT_PARENT_ID, SCHEDULE_CORE_STATUS, STATUS, ScheduleCore, WORK_TIME_DIRECTION, checkNoUpdatedLinks, computeExpectedProgress, expectedProgressFromBaseline, getActiveBaseline, getUnsavedActivities, isRootParent, normalizeParentKey, parseFromBackend, willRunCriticalPath, yieldToBrowser };
20554
21395
  //# sourceMappingURL=index.js.map
20555
21396
  //# sourceMappingURL=index.js.map