@outbuild-company/schedule-core 1.6.3 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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
@@ -4450,6 +4843,29 @@ var COLUMN_WRITABLE_FIELDS = new Set(
4450
4843
  COLUMN_WRITABLE_FIELD_LIST
4451
4844
  );
4452
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
+
4453
4869
  // src/internal/state/dynamic-writes.ts
4454
4870
  function setActivityFieldDynamic(state, activityId, field, value) {
4455
4871
  state.setActivityFieldDynamic(activityId, field, value);
@@ -4501,11 +4917,6 @@ function cloneCriticalPath(value) {
4501
4917
  };
4502
4918
  }
4503
4919
 
4504
- // src/shared/clone-domain-value.ts
4505
- function cloneDomainValue(value) {
4506
- return structuredClone(value);
4507
- }
4508
-
4509
4920
  // src/dispatch/shared/snapshots.ts
4510
4921
  var YIELD_EVERY_N_ENTRIES = 200;
4511
4922
  function collectTouchedIds(primary, changes) {
@@ -4617,6 +5028,7 @@ function diffActivity(before, after) {
4617
5028
  const beforeRec = before ?? {};
4618
5029
  const afterRec = after;
4619
5030
  const keys = /* @__PURE__ */ new Set([...Object.keys(beforeRec), ...Object.keys(afterRec)]);
5031
+ keys.delete(ACTIVITY_PROPERTY.CALENDAR_DURATION);
4620
5032
  for (const k of keys) {
4621
5033
  if (Object.hasOwn(beforeRec, k) !== Object.hasOwn(afterRec, k) || !fieldValueEqual(beforeRec[k], afterRec[k])) {
4622
5034
  fields[k] = {
@@ -9291,28 +9703,6 @@ async function applyCriticalPath(state, hoursPerDay, isCurrent = () => true) {
9291
9703
  return true;
9292
9704
  }
9293
9705
 
9294
- // src/constants/activity-properties.ts
9295
- var ACTIVITY_PROPERTY = {
9296
- PARENT: "parentId",
9297
- TYPE: "type",
9298
- START_DATE: "startDate",
9299
- END_DATE: "endDate",
9300
- CONSTRAINT_TYPE: "constraintType",
9301
- CONSTRAINT_DATE: "constraintDate",
9302
- PROGRESS: "progress",
9303
- COST: "cost",
9304
- USED_COST: "usedCost",
9305
- REAL_COST: "realCost",
9306
- WORK_HOURS: "workHours",
9307
- REAL_WORK_HOURS: "realWorkHours",
9308
- CORRELATIVE_ID: "correlativeId",
9309
- CUSTOM_ID: "customId",
9310
- NEW_ACTIVITIES_ARRAY: "newActivityIds",
9311
- HAS_NEW_ACTIVITIES: "hasNewActivities"};
9312
- var LINK_PROPERTY = {
9313
- TYPE: "type",
9314
- LAG: "lag"};
9315
-
9316
9706
  // src/propagations/upward/real-cost.ts
9317
9707
  function emitRealCost(state) {
9318
9708
  const rootIds = [];
@@ -9365,6 +9755,8 @@ var NON_SCHEDULING_INLINE_COLUMNS = /* @__PURE__ */ new Set([
9365
9755
  COLUMN.TAGS
9366
9756
  ]);
9367
9757
  var NON_SCHEDULING_KINDS = /* @__PURE__ */ new Set([
9758
+ "activity-clipboard-clear",
9759
+ "activity-copy",
9368
9760
  "selection-toggle",
9369
9761
  "selection-replace",
9370
9762
  "visibility-set",
@@ -9378,6 +9770,8 @@ var NON_SCHEDULING_KINDS = /* @__PURE__ */ new Set([
9378
9770
  "status-criteria-set"
9379
9771
  ]);
9380
9772
  var PURE_VIEW_STATE_KINDS = /* @__PURE__ */ new Set([
9773
+ "activity-clipboard-clear",
9774
+ "activity-copy",
9381
9775
  "selection-toggle",
9382
9776
  "selection-replace",
9383
9777
  "visibility-set",
@@ -9703,7 +10097,8 @@ async function assembleChangeSet(adapter, args) {
9703
10097
  const insertedIds = collectInsertedIds(captured);
9704
10098
  mergeDirtyBeforeImages(adapter, captured, insertedIds, before, allTouched);
9705
10099
  }
9706
- 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) : [];
9707
10102
  const correlativeBefore = buildCorrelativeBeforeMap(
9708
10103
  args.correlativeShifts,
9709
10104
  allTouched
@@ -9714,7 +10109,7 @@ async function assembleChangeSet(adapter, args) {
9714
10109
  allTouched,
9715
10110
  correlativeBefore
9716
10111
  );
9717
- const effects = args.sirDetection === "after-diff" ? detectSirAutoReject(before, adapter, allTouched) : beforeDiffEffects;
10112
+ const effects = sirDetection === "after-diff" ? detectSirAutoReject(before, adapter, allTouched) : beforeDiffEffects;
9718
10113
  const warnings = args.warnings ?? [];
9719
10114
  return {
9720
10115
  source: args.source,
@@ -10043,6 +10438,7 @@ async function dispatchInlineEdit(action, options, deps) {
10043
10438
  touchedIds,
10044
10439
  scheduledIds,
10045
10440
  sirDetection: "before-diff",
10441
+ preview: options.preview,
10046
10442
  links: linkChanges,
10047
10443
  trackingEvents,
10048
10444
  ...constraintWarning ? { warnings: [constraintWarning] } : {},
@@ -10249,7 +10645,7 @@ async function dispatchInlineEditLinks(action, options, deps) {
10249
10645
  const { specs: specsInDays } = parsePredecessorString(action.newValue);
10250
10646
  const specs = specsInDays.map((spec) => ({
10251
10647
  ...spec,
10252
- lag: lagDaysToHours(spec.lag, sector.hoursPerDay)
10648
+ lag: lagDaysToHours(roundLagDays(spec.lag), sector.hoursPerDay)
10253
10649
  }));
10254
10650
  const byCorrelative = /* @__PURE__ */ new Map();
10255
10651
  adapter.forEachActivity((a) => {
@@ -10586,6 +10982,11 @@ function buildNewActivity(input, adapter) {
10586
10982
  ...base,
10587
10983
  startDate,
10588
10984
  endDate,
10985
+ calendarDuration: computeCalendarDuration({
10986
+ type: base.type,
10987
+ startDate,
10988
+ endDate
10989
+ }),
10589
10990
  newActivityIds: [...base.newActivityIds],
10590
10991
  pendingRequestIds: [...base.pendingRequestIds],
10591
10992
  responsableIds: [...base.responsableIds],
@@ -11211,6 +11612,7 @@ async function dispatchActivityDelete(action, options, deps) {
11211
11612
  touchedIds,
11212
11613
  scheduledIds,
11213
11614
  sirDetection: "after-diff",
11615
+ preview: options.preview,
11214
11616
  links: buildLinkDeletions(beforeLinks),
11215
11617
  trackingEvents: [trackingEvent],
11216
11618
  hoursPerDay: sector.hoursPerDay,
@@ -11371,6 +11773,7 @@ async function dispatchActivityBatch(action, options, deps) {
11371
11773
  links: buildLinkChangesForBatch(appliedLinks, beforeLinks, deps.adapter),
11372
11774
  hoursPerDay: deps.sector.hoursPerDay,
11373
11775
  correlativeShifts: existingCorrelativeShifts,
11776
+ preview: options.preview,
11374
11777
  ...action.mode === "replace" ? { sirDetection: "after-diff" } : {}
11375
11778
  });
11376
11779
  return {
@@ -11668,184 +12071,331 @@ function stringKeyedLinks(links) {
11668
12071
  return new Map(Array.from(links, ([linkId, link]) => [String(linkId), link]));
11669
12072
  }
11670
12073
 
11671
- // src/dispatch/handlers/activity-paste.ts
11672
- function resolvePasteRootDestination(destination, adapter) {
11673
- if ("afterSiblingId" in destination) {
11674
- const sibling = adapter.getActivity(destination.afterSiblingId);
11675
- if (!sibling) {
11676
- return { ok: false, reason: REJECTION_REASON.ANCHOR_SIBLING_NOT_FOUND };
11677
- }
11678
- const parentId2 = sibling.parentId == null ? ROOT_PARENT_ID : String(sibling.parentId);
11679
- return { ok: true, parentId: parentId2, firstAfter: destination.afterSiblingId };
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 };
11680
12081
  }
11681
- const parentId = destination.parentId;
11682
- const siblings = collectSiblings(adapter, parentId).sort(
11683
- (a, b) => readCorrelativeId(a) - readCorrelativeId(b)
11684
- );
11685
- const before = siblings[destination.index]?.id;
11686
- return { ok: true, parentId, firstBefore: before };
11687
- }
11688
- function applyPasteDomainSemantics(overrides) {
11689
- const { customId: _customId, ...content } = overrides;
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
+ };
11690
12097
  return {
11691
- ...content,
11692
- progress: 0,
11693
- ponderator: 0,
11694
- usedCost: 0,
11695
- workHours: 0,
11696
- isLookahead: false
12098
+ ok: true,
12099
+ changes: {
12100
+ source: action,
12101
+ activities: [change],
12102
+ links: [],
12103
+ calendars: [],
12104
+ trackingEvents: []
12105
+ }
11697
12106
  };
11698
12107
  }
11699
- async function dispatchActivityPaste(action, options, deps) {
12108
+
12109
+ // src/dispatch/handlers/bulk-edit.ts
12110
+ var CUSTOM_ID_COLUMN = COLUMN.CUSTOM_ID;
12111
+ async function dispatchBulkEdit(action, options, deps) {
11700
12112
  const { adapter, scheduler, sector } = deps;
11701
- const rootDest = resolvePasteRootDestination(action.destination, adapter);
11702
- if (!rootDest.ok) return rootDest;
11703
- const beforeSnap = await snapshotActivities(adapter, /* @__PURE__ */ new Set());
11704
- const touched = /* @__PURE__ */ new Set();
11705
- const originalToNew = /* @__PURE__ */ new Map();
11706
- const createdIds = [];
11707
- let prevRootId;
11708
- for (const input of action.activities) {
11709
- const mappedParent = originalToNew.get(input.originalParentId);
11710
- let parentId;
11711
- let afterSiblingId;
11712
- let beforeSiblingId;
11713
- if (mappedParent !== void 0) {
11714
- parentId = mappedParent;
11715
- } else {
11716
- parentId = rootDest.parentId;
11717
- if (prevRootId === void 0) {
11718
- afterSiblingId = rootDest.firstAfter;
11719
- beforeSiblingId = rootDest.firstBefore;
11720
- } else {
11721
- afterSiblingId = prevRootId;
11722
- }
11723
- }
11724
- const core = await createActivityCore(
11725
- {
11726
- parentId,
11727
- afterSiblingId,
11728
- beforeSiblingId,
11729
- ...input.activityId ? { activityId: input.activityId } : {},
11730
- overrides: applyPasteDomainSemantics(input.overrides),
11731
- eventSource: action.eventSource
11732
- },
11733
- deps,
11734
- {
11735
- skipCorrelativeRecompute: true,
11736
- skipBeforeSnap: true,
11737
- customIdReferenceId: action.referenceActivityId
11738
- }
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
11739
12135
  );
11740
- if (!core.ok) return core;
11741
- if (input.baselineSnapshot !== void 0) {
11742
- const snapshot = input.baselineSnapshot;
11743
- adapter.setActivityField(
11744
- core.newId,
11745
- "baselineSnapshot",
11746
- snapshot ? {
11747
- ...snapshot,
11748
- startDate: snapshot.startDate ? new Date(snapshot.startDate) : null,
11749
- endDate: snapshot.endDate ? new Date(snapshot.endDate) : null
11750
- } : null
11751
- );
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));
12143
+ }
12144
+ const beforeLinkLags = snapshotIncomingLinkLags(adapter, edit.activityId);
12145
+ const preEditSnapshot = snapshotSingleActivity(
12146
+ adapter,
12147
+ String(edit.activityId)
12148
+ );
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
+ });
11752
12168
  }
11753
- originalToNew.set(input.originalId, core.newId);
11754
- createdIds.push(core.newId);
11755
- if (mappedParent === void 0) prevRootId = core.newId;
11756
- }
11757
- const correlativeShifts = recomputeCorrelativeIds(adapter);
11758
- const createdIdSet = new Set(createdIds.map(String));
11759
- foldCorrelativeShifts(
11760
- adapter,
11761
- correlativeShifts,
11762
- createdIdSet,
11763
- beforeSnap,
11764
- touched
11765
- );
11766
- for (const createdId of createdIds) touched.add(String(createdId));
11767
- const appliedLinks = [];
11768
- for (const link of action.links) {
11769
- const newSource = originalToNew.get(link.source);
11770
- const newTarget = originalToNew.get(link.target);
11771
- if (newSource === void 0 || newTarget === void 0) continue;
11772
- const op = {
11773
- kind: "create",
11774
- source: newSource,
11775
- target: newTarget,
11776
- type: link.type,
11777
- lag: lagDaysToHours(link.lag, sector.hoursPerDay)
11778
- };
11779
- const res = applyLinkOperation(op, {
11780
- port: adapter,
11781
- newLinkId: link.linkId ? () => link.linkId : deps.linkIdGen.next
11782
- });
11783
- appliedLinks.push({
11784
- op,
11785
- finalLinkId: res.applied ? res.linkId : null,
11786
- rejected: res.applied ? null : res.rejected ?? "link_op_rejected"
11787
- });
11788
12169
  }
11789
- 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(
11790
12179
  {
11791
12180
  adapter,
11792
12181
  scheduler,
11793
- sector: deps.sector,
12182
+ sector,
11794
12183
  defaultBaseCalendarId: deps.defaultBaseCalendarId
11795
12184
  },
11796
12185
  {
11797
- action,
11798
- autoscheduleFrom: createdIds[0] ?? null,
11799
- recomputeParentsFrom: createdIds,
11800
- now: deps.now,
11801
- 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
+ )
11802
12199
  }
11803
- );
11804
- const linkChanges = buildLinkChangesForBatch(
11805
- appliedLinks,
11806
- /* @__PURE__ */ new Map(),
11807
- adapter
11808
- );
12200
+ ) : runPostMutation(
12201
+ {
12202
+ adapter,
12203
+ scheduler,
12204
+ sector,
12205
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
12206
+ },
12207
+ postMutationArgs
12208
+ ));
11809
12209
  return {
11810
12210
  ok: true,
12211
+ verdicts,
11811
12212
  changes: await assembleChangeSet(adapter, {
11812
12213
  source: action,
11813
12214
  beforeSnap,
11814
- touchedIds: touched,
12215
+ touchedIds,
11815
12216
  scheduledIds,
12217
+ sirDetection: "before-diff",
12218
+ preview: options.preview,
11816
12219
  links: linkChanges,
12220
+ trackingEvents,
12221
+ warnings,
11817
12222
  hoursPerDay: sector.hoursPerDay
11818
12223
  })
11819
12224
  };
11820
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
+ }
11821
12351
 
11822
- // src/dispatch/handlers/sir-sync.ts
11823
- function dispatchSirSync(action, deps) {
12352
+ // src/dispatch/handlers/sir-approve.ts
12353
+ async function dispatchSirApprove(action, options, deps) {
11824
12354
  const { adapter } = deps;
11825
- const id = action.activityId;
11826
- const snapshot = adapter.getActivity(id);
12355
+ const snapshot = adapter.getActivity(action.activityId);
11827
12356
  if (!snapshot) {
11828
12357
  return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
11829
12358
  }
11830
- const before = snapshot.pendingRequestIds ?? [];
11831
- const after = action.pendingRequests.map((r) => r.id);
11832
- adapter.setActivityField(action.activityId, "pendingRequestIds", after);
11833
- const change = {
11834
- id,
11835
- kind: "updated",
11836
- fields: { pendingRequestIds: { before, after } },
11837
- after: { id, pendingRequestIds: after }
11838
- };
11839
- return {
11840
- ok: true,
11841
- changes: {
11842
- source: action,
11843
- activities: [change],
11844
- links: [],
11845
- calendars: [],
11846
- trackingEvents: []
11847
- }
11848
- };
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 } };
11849
12399
  }
11850
12400
 
11851
12401
  // src/dispatch/handlers/activity-lookahead-sync.ts
@@ -11931,7 +12481,38 @@ function isAncestor(descendantId, candidateAncestorId, adapter) {
11931
12481
  if (parentKey === candidateAncestorId) return true;
11932
12482
  cur = parentKey;
11933
12483
  }
11934
- 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;
11935
12516
  }
11936
12517
 
11937
12518
  // src/dispatch/handlers/activity-move.ts
@@ -11975,6 +12556,17 @@ async function dispatchActivityMove(action, options, deps) {
11975
12556
  const oldParentKey = normalizeParentKey(oldParent);
11976
12557
  const newParentKey = String(action.parentId);
11977
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
+ }
11978
12570
  const touched = /* @__PURE__ */ new Set([action.activityId]);
11979
12571
  if (parentChanged) {
11980
12572
  if (oldParentKey !== ROOT_PARENT_ID) touched.add(oldParentKey);
@@ -12126,27 +12718,21 @@ async function dispatchActivityMove(action, options, deps) {
12126
12718
  touchedIds: touched,
12127
12719
  scheduledIds,
12128
12720
  sirDetection: "before-diff",
12721
+ preview: options.preview,
12129
12722
  trackingEvents: [trackingEvent],
12130
12723
  hoursPerDay: sector.hoursPerDay
12131
12724
  })
12132
12725
  };
12133
12726
  }
12134
12727
 
12135
- // src/dispatch/handlers/activity-indent.ts
12136
- async function dispatchActivityIndent(action, options, deps) {
12137
- const { adapter, scheduler, sector } = deps;
12138
- scheduler.invalidateAllCaches();
12139
- if (action.activityIds.length === 0) {
12140
- return { ok: false, reason: REJECTION_REASON.ACTIVITY_IDS_EMPTY };
12141
- }
12142
- const selectedSet = new Set(action.activityIds);
12143
- const touched = /* @__PURE__ */ new Set();
12728
+ // src/dispatch/shared/indent-planning.ts
12729
+ function planIndentMoves(activityIds, adapter) {
12730
+ const selectedSet = new Set(activityIds);
12144
12731
  const failed = [];
12145
- const succeededIds = [];
12146
12732
  const moves = [];
12147
12733
  const plannedIds = /* @__PURE__ */ new Set();
12148
12734
  const blockingSet = new Set(selectedSet);
12149
- const sortedIds = [...action.activityIds].sort((a, b) => {
12735
+ const sortedIds = [...activityIds].sort((a, b) => {
12150
12736
  const aSnap = adapter.getActivity(a);
12151
12737
  const bSnap = adapter.getActivity(b);
12152
12738
  const ac = aSnap ? readCorrelativeId(aSnap) : NOT_SET_SORT_VALUE2;
@@ -12181,12 +12767,43 @@ async function dispatchActivityIndent(action, options, deps) {
12181
12767
  const oldParentId = isRootParent(oldParentRaw) ? ROOT_PARENT_ID : String(oldParentRaw);
12182
12768
  moves.push({ activityId: id, newParentId: targetParentId, oldParentId });
12183
12769
  plannedIds.add(id);
12184
- touched.add(id);
12185
- touched.add(targetParentId);
12186
- if (oldParentId !== ROOT_PARENT_ID) touched.add(oldParentId);
12187
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;
12188
12805
  const beforeSnap = await snapshotActivities(adapter, touched);
12189
- for (const { activityId, newParentId } of moves) {
12806
+ for (const { activityId, newParentId } of allowedMoves) {
12190
12807
  adapter.setActivityField(activityId, ACTIVITY_PROPERTY.PARENT, newParentId);
12191
12808
  adapter.setActivityField(
12192
12809
  activityId,
@@ -12196,7 +12813,7 @@ async function dispatchActivityIndent(action, options, deps) {
12196
12813
  succeededIds.push(activityId);
12197
12814
  }
12198
12815
  const promotedParents = /* @__PURE__ */ new Set();
12199
- for (const { activityId, newParentId } of moves) {
12816
+ for (const { activityId, newParentId } of allowedMoves) {
12200
12817
  if (promotedParents.has(newParentId)) continue;
12201
12818
  promotedParents.add(newParentId);
12202
12819
  const newParent = adapter.getActivity(newParentId);
@@ -12248,7 +12865,7 @@ async function dispatchActivityIndent(action, options, deps) {
12248
12865
  releaseClearedCustomId(oldNewParentCustomId, deps.customIdTracker);
12249
12866
  }
12250
12867
  const indentAffectedParents = /* @__PURE__ */ new Set();
12251
- for (const m of moves) {
12868
+ for (const m of allowedMoves) {
12252
12869
  if (m.oldParentId !== ROOT_PARENT_ID)
12253
12870
  indentAffectedParents.add(m.oldParentId);
12254
12871
  indentAffectedParents.add(m.newParentId);
@@ -12409,8 +13026,24 @@ async function dispatchActivityOutdent(action, options, deps) {
12409
13026
  if (grandparentKey !== ROOT_PARENT_ID) touched.add(String(grandparentKey));
12410
13027
  oldParents.add(oldParentKey);
12411
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
+ );
12412
13045
  const beforeSnap = await snapshotActivities(adapter, touched);
12413
- for (const plan of planned) {
13046
+ for (const plan of allowedPlans) {
12414
13047
  adapter.setActivityField(
12415
13048
  plan.activityId,
12416
13049
  ACTIVITY_PROPERTY.PARENT,
@@ -12444,7 +13077,7 @@ async function dispatchActivityOutdent(action, options, deps) {
12444
13077
  task: parentActivity
12445
13078
  }),
12446
13079
  idsRemoved: new Set(
12447
- planned.filter((plan) => plan.oldParentKey === oldParentKey).map((plan) => plan.activityId)
13080
+ allowedPlans.filter((plan) => plan.oldParentKey === oldParentKey).map((plan) => plan.activityId)
12448
13081
  )
12449
13082
  });
12450
13083
  if (!demotion) {
@@ -12470,7 +13103,7 @@ async function dispatchActivityOutdent(action, options, deps) {
12470
13103
  for (const oldParentKey of oldParents) {
12471
13104
  outdentAffectedParents.add(String(oldParentKey));
12472
13105
  }
12473
- for (const plan of planned) {
13106
+ for (const plan of allowedPlans) {
12474
13107
  if (plan.grandparentKey !== ROOT_PARENT_ID) {
12475
13108
  outdentAffectedParents.add(String(plan.grandparentKey));
12476
13109
  }
@@ -12558,6 +13191,17 @@ async function dispatchActivitySetProgress(action, options, deps) {
12558
13191
  "[ScheduleCore] progressPipeline not registered \u2014 cannot dispatch activity-set-progress"
12559
13192
  );
12560
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
+ }
12561
13205
  const changes = pipeline.transform(
12562
13206
  activitySnap,
12563
13207
  action.newValue,
@@ -12616,11 +13260,11 @@ async function dispatchDatesBatch(action, options, deps) {
12616
13260
  const warnings = [];
12617
13261
  let needsAutoSchedule = false;
12618
13262
  for (const edit of action.edits) {
12619
- const outcome = applyOneEdit(edit, deps);
13263
+ const outcome = applyOneEdit2(edit, deps);
12620
13264
  verdicts.push(outcome.verdict);
12621
13265
  if (!outcome.verdict.ok || !outcome.changes) continue;
12622
13266
  const editTouched = collectTouchedIds(edit.activityId, outcome.changes);
12623
- mergeBeforeSnapshots(beforeSnap, editTouched, deps);
13267
+ mergeBeforeSnapshots2(beforeSnap, editTouched, deps);
12624
13268
  for (const id of editTouched) touchedIds.add(id);
12625
13269
  const beforeLinkLags = snapshotIncomingLinkLags(adapter, edit.activityId);
12626
13270
  const preEditSnapshot = snapshotSingleActivity(
@@ -12674,6 +13318,7 @@ async function dispatchDatesBatch(action, options, deps) {
12674
13318
  touchedIds,
12675
13319
  scheduledIds,
12676
13320
  sirDetection: "before-diff",
13321
+ preview: options.preview,
12677
13322
  links: linkChanges,
12678
13323
  trackingEvents,
12679
13324
  warnings,
@@ -12681,222 +13326,18 @@ async function dispatchDatesBatch(action, options, deps) {
12681
13326
  })
12682
13327
  };
12683
13328
  }
12684
- function applyOneEdit(edit, deps) {
13329
+ function applyOneEdit2(edit, deps) {
12685
13330
  const { ctx } = deps;
12686
13331
  const activityId = edit.activityId;
12687
13332
  const pipeline = findPipelineForColumn(edit.column);
12688
13333
  if (!pipeline) {
12689
13334
  throw new Error(
12690
13335
  `[ScheduleCore] no pipeline registered for column "${edit.column}"`
12691
- );
12692
- }
12693
- const activitySnap = ctx.activityReader.getActivity(edit.activityId);
12694
- if (!activitySnap) {
12695
- throw new Error(`[ScheduleCore] activity "${edit.activityId}" not found`);
12696
- }
12697
- const editGate = pipeline.canEdit(activitySnap, ctx.hierarchy);
12698
- if (!editGate.allowed) {
12699
- return {
12700
- verdict: {
12701
- activityId,
12702
- ok: false,
12703
- reason: editGate.reason ?? REJECTION_REASON.CANNOT_EDIT,
12704
- ...editGate.alertKey ? { alertKey: editGate.alertKey } : {}
12705
- },
12706
- changes: null
12707
- };
12708
- }
12709
- const parsed2 = pipeline.parseInput(edit.newValue, activitySnap, ctx);
12710
- if (!parsed2.ok) {
12711
- return {
12712
- verdict: {
12713
- activityId,
12714
- ok: false,
12715
- reason: parsed2.reason ?? REJECTION_REASON.PARSE_ERROR
12716
- },
12717
- changes: null
12718
- };
12719
- }
12720
- const oldValue = Reflect.get(activitySnap, pipeline.targetField);
12721
- const validation = pipeline.validate(
12722
- activitySnap,
12723
- oldValue,
12724
- parsed2.value,
12725
- ctx
12726
- );
12727
- if (!validation.valid) {
12728
- return {
12729
- verdict: {
12730
- activityId,
12731
- ok: false,
12732
- reason: validation.reason ?? REJECTION_REASON.INVALID,
12733
- ...validation.notification?.messageKey ? { alertKey: validation.notification.messageKey } : {}
12734
- },
12735
- changes: null
12736
- };
12737
- }
12738
- const changes = pipeline.transform(
12739
- activitySnap,
12740
- parsed2.value,
12741
- ctx,
12742
- parsed2.raw
12743
- );
12744
- return { verdict: { activityId, ok: true }, changes };
12745
- }
12746
- function mergeBeforeSnapshots(beforeSnap, ids, deps) {
12747
- const missing = /* @__PURE__ */ new Set();
12748
- for (const id of ids) {
12749
- if (!beforeSnap.has(id)) missing.add(id);
12750
- }
12751
- if (missing.size === 0) return;
12752
- for (const missingId of missing) {
12753
- const liveActivity = deps.adapter.getActivity(missingId);
12754
- if (liveActivity) {
12755
- beforeSnap.set(missingId, structuredCloneActivity(liveActivity));
12756
- }
12757
- }
12758
- }
12759
-
12760
- // src/dispatch/handlers/bulk-edit.ts
12761
- var CUSTOM_ID_COLUMN = COLUMN.CUSTOM_ID;
12762
- async function dispatchBulkEdit(action, options, deps) {
12763
- const { adapter, scheduler, sector } = deps;
12764
- const editsIncludeDuration = action.edits.some(
12765
- (edit) => edit.column === COLUMN.DURATION
12766
- );
12767
- if (editsIncludeDuration) {
12768
- scheduler.invalidateExpandedLinksCache();
12769
- }
12770
- const verdicts = [];
12771
- const beforeSnap = /* @__PURE__ */ new Map();
12772
- const constraintPriorsByActivity = /* @__PURE__ */ new Map();
12773
- const explicitDateEditActivities = /* @__PURE__ */ new Set();
12774
- const touchedIds = /* @__PURE__ */ new Set();
12775
- const trackingEvents = [];
12776
- const linkChanges = [];
12777
- const warnings = [];
12778
- let needsAutoSchedule = false;
12779
- for (const edit of action.edits) {
12780
- const outcome = applyOneEdit2(edit, deps);
12781
- verdicts.push(outcome.verdict);
12782
- if (!outcome.verdict.ok || !outcome.changes) continue;
12783
- const constraintWarning = toConstraintWarning(
12784
- edit.activityId,
12785
- outcome.changes.constraintWarning
12786
- );
12787
- if (constraintWarning) warnings.push(constraintWarning);
12788
- const editTouched = collectTouchedIds(edit.activityId, outcome.changes);
12789
- mergeBeforeSnapshots2(beforeSnap, editTouched, deps);
12790
- for (const touchedId of editTouched) touchedIds.add(touchedId);
12791
- recordConstraintPrior(constraintPriorsByActivity, edit, beforeSnap);
12792
- if (isExplicitConstraintDateEdit(edit.column)) {
12793
- explicitDateEditActivities.add(String(edit.activityId));
12794
- }
12795
- const beforeLinkLags = snapshotIncomingLinkLags(adapter, edit.activityId);
12796
- const preEditSnapshot = snapshotSingleActivity(
12797
- adapter,
12798
- String(edit.activityId)
12799
- );
12800
- const customIdBeforeApply = readLiveCustomId(edit, deps);
12801
- const changes = outcome.changes;
12802
- applyFieldChanges(adapter, edit.activityId, changes);
12803
- runPostProcessorsOnAdapter(
12804
- edit.activityId,
12805
- outcome.changes.postProcessors ?? [],
12806
- adapter,
12807
- preEditSnapshot
12808
- );
12809
- syncCustomIdTracker(edit, customIdBeforeApply, deps);
12810
- linkChanges.push(
12811
- ...diffIncomingLinkLagChanges(beforeLinkLags, adapter, edit.activityId)
12812
- );
12813
- needsAutoSchedule ||= Boolean(outcome.changes.needsAutoSchedule);
12814
- for (const trackingEvent of outcome.changes.trackingEvents ?? []) {
12815
- trackingEvents.push({
12816
- name: trackingEvent.name,
12817
- properties: trackingEvent.properties ?? {}
12818
- });
12819
- }
12820
- }
12821
- const postMutationArgs = {
12822
- action,
12823
- autoscheduleFrom: needsAutoSchedule ? "roots" : null,
12824
- recomputeParentsFrom: touchedIds,
12825
- now: deps.now,
12826
- options
12827
- };
12828
- const batchHasConstraintEdits = constraintPriorsByActivity.size > 0;
12829
- const { scheduledIds } = await (batchHasConstraintEdits ? settleConstraintEdits(
12830
- {
12831
- adapter,
12832
- scheduler,
12833
- sector,
12834
- defaultBaseCalendarId: deps.defaultBaseCalendarId
12835
- },
12836
- {
12837
- postMutationArgs,
12838
- revertTargets: [...constraintPriorsByActivity].filter(
12839
- ([activityId]) => !explicitDateEditActivities.has(activityId)
12840
- ).map(([activityId, priorConstraintDate]) => ({
12841
- activityId,
12842
- priorConstraintDate
12843
- })),
12844
- revert: (activityId, priorConstraintDate) => revertNoOpConstraintEdit(
12845
- activityId,
12846
- priorConstraintDate,
12847
- adapter,
12848
- deps.ctx.calendars
12849
- )
12850
- }
12851
- ) : runPostMutation(
12852
- {
12853
- adapter,
12854
- scheduler,
12855
- sector,
12856
- defaultBaseCalendarId: deps.defaultBaseCalendarId
12857
- },
12858
- postMutationArgs
12859
- ));
12860
- return {
12861
- ok: true,
12862
- verdicts,
12863
- changes: await assembleChangeSet(adapter, {
12864
- source: action,
12865
- beforeSnap,
12866
- touchedIds,
12867
- scheduledIds,
12868
- sirDetection: "before-diff",
12869
- links: linkChanges,
12870
- trackingEvents,
12871
- warnings,
12872
- hoursPerDay: sector.hoursPerDay
12873
- })
12874
- };
12875
- }
12876
- function applyOneEdit2(edit, deps) {
12877
- const { ctx } = deps;
12878
- const activityId = edit.activityId;
12879
- const pipeline = findPipelineForColumn(edit.column);
12880
- if (!pipeline) {
12881
- return {
12882
- verdict: {
12883
- activityId,
12884
- ok: false,
12885
- reason: REJECTION_REASON.NO_PIPELINE_FOR_COLUMN
12886
- },
12887
- changes: null
12888
- };
13336
+ );
12889
13337
  }
12890
13338
  const activitySnap = ctx.activityReader.getActivity(edit.activityId);
12891
13339
  if (!activitySnap) {
12892
- return {
12893
- verdict: {
12894
- activityId,
12895
- ok: false,
12896
- reason: REJECTION_REASON.ACTIVITY_NOT_FOUND
12897
- },
12898
- changes: null
12899
- };
13340
+ throw new Error(`[ScheduleCore] activity "${edit.activityId}" not found`);
12900
13341
  }
12901
13342
  const editGate = pipeline.canEdit(activitySnap, ctx.hierarchy);
12902
13343
  if (!editGate.allowed) {
@@ -12939,21 +13380,6 @@ function applyOneEdit2(edit, deps) {
12939
13380
  changes: null
12940
13381
  };
12941
13382
  }
12942
- if (edit.column === CUSTOM_ID_COLUMN && parsed2.value !== null) {
12943
- const liveActivity = deps.adapter.getActivity(edit.activityId);
12944
- const oldCustomIdRaw = liveActivity ? liveActivity.customId : null;
12945
- const oldCustomId = typeof oldCustomIdRaw === "string" ? oldCustomIdRaw : null;
12946
- if (deps.customIdTracker.isCustomIdInUse(String(parsed2.value), oldCustomId)) {
12947
- return {
12948
- verdict: {
12949
- activityId,
12950
- ok: false,
12951
- reason: REJECTION_REASON.CUSTOM_ID_DUPLICATE
12952
- },
12953
- changes: null
12954
- };
12955
- }
12956
- }
12957
13383
  const changes = pipeline.transform(
12958
13384
  activitySnap,
12959
13385
  parsed2.value,
@@ -12962,33 +13388,10 @@ function applyOneEdit2(edit, deps) {
12962
13388
  );
12963
13389
  return { verdict: { activityId, ok: true }, changes };
12964
13390
  }
12965
- function readLiveCustomId(edit, deps) {
12966
- if (edit.column !== CUSTOM_ID_COLUMN) return null;
12967
- const liveActivity = deps.adapter.getActivity(edit.activityId);
12968
- const rawValue = liveActivity ? liveActivity.customId : null;
12969
- return typeof rawValue === "string" ? rawValue : null;
12970
- }
12971
- function syncCustomIdTracker(edit, oldCustomId, deps) {
12972
- if (edit.column !== CUSTOM_ID_COLUMN) return;
12973
- const liveActivity = deps.adapter.getActivity(edit.activityId);
12974
- const rawValue = liveActivity ? liveActivity.customId : null;
12975
- const newCustomId = typeof rawValue === "string" ? rawValue : null;
12976
- if (oldCustomId !== newCustomId) {
12977
- deps.customIdTracker.trackCustomIdChange(oldCustomId, newCustomId);
12978
- }
12979
- }
12980
- function recordConstraintPrior(priors, edit, beforeSnap) {
12981
- if (!isConstraintEditColumn(edit.column)) return;
12982
- const activityKey = String(edit.activityId);
12983
- if (priors.has(activityKey)) return;
12984
- const preBatchSnapshot = beforeSnap.get(activityKey);
12985
- if (!preBatchSnapshot) return;
12986
- priors.set(activityKey, preBatchSnapshot.constraintDate ?? null);
12987
- }
12988
13391
  function mergeBeforeSnapshots2(beforeSnap, ids, deps) {
12989
13392
  const missing = /* @__PURE__ */ new Set();
12990
- for (const candidateId of ids) {
12991
- if (!beforeSnap.has(candidateId)) missing.add(candidateId);
13393
+ for (const id of ids) {
13394
+ if (!beforeSnap.has(id)) missing.add(id);
12992
13395
  }
12993
13396
  if (missing.size === 0) return;
12994
13397
  for (const missingId of missing) {
@@ -13051,9 +13454,33 @@ function assertValidAssignments(assignments, entityName, readCurrent, ownerByBac
13051
13454
  );
13052
13455
  }
13053
13456
  }
13054
- 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) {
13055
13480
  const activities = action.activities ?? [];
13056
13481
  const links = action.links ?? [];
13482
+ const reconciledProgress = action.reconciledProgress ?? [];
13483
+ assertValidReconciledProgress(reconciledProgress, deps);
13057
13484
  const activityOwnerByBackendId = /* @__PURE__ */ new Map();
13058
13485
  for (const activity of deps.adapter.getAllActivities()) {
13059
13486
  if (activity.proplannerId != null) {
@@ -13089,7 +13516,49 @@ function dispatchPersistenceAcknowledge(action, deps) {
13089
13516
  );
13090
13517
  const activityChanges = [];
13091
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
+ );
13092
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
+ }
13093
13562
  for (const assignment of activities) {
13094
13563
  const before = deps.adapter.getActivity(assignment.id);
13095
13564
  if (!before || before.proplannerId === assignment.proplannerId) continue;
@@ -13496,8 +13965,218 @@ async function dispatchStatusCriteriaSet(action, deps) {
13496
13965
  };
13497
13966
  }
13498
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
+
13499
14166
  // src/dispatch/dispatch.ts
13500
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
+ }
13501
14180
  if (action.kind === "activity-batch") {
13502
14181
  return dispatchActivityBatch(action, options, deps);
13503
14182
  }
@@ -13522,9 +14201,6 @@ async function performDispatch(action, options, deps) {
13522
14201
  if (action.kind === "activity-create") {
13523
14202
  return dispatchActivityCreate(action, options, deps);
13524
14203
  }
13525
- if (action.kind === "activity-paste") {
13526
- return dispatchActivityPaste(action, options, deps);
13527
- }
13528
14204
  if (action.kind === "activity-delete") {
13529
14205
  return dispatchActivityDelete(action, options, deps);
13530
14206
  }
@@ -13567,6 +14243,9 @@ async function performDispatch(action, options, deps) {
13567
14243
  if (action.kind === "sir-sync") {
13568
14244
  return dispatchSirSync(action, deps);
13569
14245
  }
14246
+ if (action.kind === "sir-approve") {
14247
+ return dispatchSirApprove(action, options, deps);
14248
+ }
13570
14249
  if (action.kind === "activity-lookahead-sync") {
13571
14250
  return dispatchActivityLookaheadSync(action, deps);
13572
14251
  }
@@ -16979,6 +17658,7 @@ var ScheduleState = class {
16979
17658
  if (!activity) return;
16980
17659
  this._writeCapture.note(String(activityId), activity);
16981
17660
  activity[field] = value;
17661
+ refreshCalendarDurationIfDateBearing(activity, field);
16982
17662
  if (field === ACTIVITY_PROPERTY.PARENT) this._hierarchy.markDirty();
16983
17663
  if (fieldAffectsVisualOrder(field)) this._cache.visualOrderIds = null;
16984
17664
  }
@@ -16992,6 +17672,7 @@ var ScheduleState = class {
16992
17672
  }
16993
17673
  this._writeCapture.note(String(activityId), activity);
16994
17674
  Reflect.set(activity, field, coerceActivityFieldValue(field, value));
17675
+ refreshCalendarDurationIfDateBearing(activity, field);
16995
17676
  if (field === ACTIVITY_PROPERTY.PARENT) this._hierarchy.markDirty();
16996
17677
  if (fieldAffectsVisualOrder(field)) this._cache.visualOrderIds = null;
16997
17678
  }
@@ -17002,6 +17683,7 @@ var ScheduleState = class {
17002
17683
  let reparented = false;
17003
17684
  for (const field of Object.keys(fields)) {
17004
17685
  Reflect.set(activity, field, Reflect.get(fields, field));
17686
+ refreshCalendarDurationIfDateBearing(activity, field);
17005
17687
  if (field === ACTIVITY_PROPERTY.PARENT) reparented = true;
17006
17688
  if (fieldAffectsVisualOrder(field)) this._cache.visualOrderIds = null;
17007
17689
  }
@@ -17170,6 +17852,12 @@ function isMutableLinkField(field) {
17170
17852
  function fieldAffectsVisualOrder(field) {
17171
17853
  return field === ACTIVITY_PROPERTY.PARENT || field === ACTIVITY_PROPERTY.CORRELATIVE_ID;
17172
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
+ }
17173
17861
  function detachLinkRef(refs, linkId) {
17174
17862
  return refs.filter((ref) => String(ref) !== String(linkId));
17175
17863
  }
@@ -17217,15 +17905,7 @@ function readChildren(state, parentId) {
17217
17905
  return out;
17218
17906
  }
17219
17907
  function readChildrenIds(state, parentId) {
17220
- const key = parentId;
17221
- if (key === ROOT_PARENT_ID) {
17222
- const roots = [];
17223
- state.forEachActivity((activity, id) => {
17224
- if (isRootParent(activity.parentId)) roots.push(id);
17225
- });
17226
- return roots;
17227
- }
17228
- return getChildrenInVisualOrder(key, state);
17908
+ return getChildrenInVisualOrder(parentId, state);
17229
17909
  }
17230
17910
  function readSelectedActivityIds(state) {
17231
17911
  return state.checkedIds().map(String);
@@ -17289,7 +17969,11 @@ function buildPipelineContext(adapter, options) {
17289
17969
  dateFormat: options.dateFormat ?? "YYYY-MM-DD",
17290
17970
  customHours: options.customHours ?? {},
17291
17971
  hoursPerDay: options.hoursPerDay,
17292
- 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 } : {}
17293
17977
  };
17294
17978
  }
17295
17979
 
@@ -17369,9 +18053,10 @@ function mergeCoalesced(top, next) {
17369
18053
  };
17370
18054
  return buildUndoEntry(changeSet);
17371
18055
  }
17372
- function getDispatchHistoryPolicy(action) {
18056
+ function getDispatchHistoryPolicy(action, options) {
18057
+ if (options?.preview === true) return "skip";
17373
18058
  if (action.kind === "persistence-acknowledge") return "clear-on-success";
17374
- if (action.kind === "sir-sync" || action.kind === "activity-lookahead-sync" || action.kind === "baseline-apply" || action.kind === "ponderator-criterion-set" || action.kind === "status-criteria-set" || action.kind === "selection-toggle" || action.kind === "selection-replace" || action.kind === "visibility-set" || action.kind === "filter-set" || action.kind === "sort-set") {
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") {
17375
18060
  return "skip";
17376
18061
  }
17377
18062
  return "record";
@@ -18155,6 +18840,11 @@ function parseActivity(raw, context) {
18155
18840
  Number(raw.sumOfDurationRecursively ?? 0),
18156
18841
  context.hoursPerDay
18157
18842
  ),
18843
+ calendarDuration: computeCalendarDuration({
18844
+ type: raw.type,
18845
+ startDate,
18846
+ endDate
18847
+ }),
18158
18848
  isCritical: Boolean(raw.is_critical),
18159
18849
  freeSlackHours: null,
18160
18850
  expectedProgress: null,
@@ -18519,7 +19209,8 @@ function parseSector(sector) {
18519
19209
  ...customIdIncrement !== void 0 ? { customIdIncrement } : {},
18520
19210
  ...isPrimaveraEndDate ? { updateDurationForPrimaveraEndDate: true } : {},
18521
19211
  activityCreter: parseActivityCreter(sector.activity_creter),
18522
- statusCriteria: DEFAULT_STATUS_CRITERIA
19212
+ statusCriteria: DEFAULT_STATUS_CRITERIA,
19213
+ ganttId: null
18523
19214
  };
18524
19215
  }
18525
19216
 
@@ -19311,6 +20002,7 @@ function recomputeSkippedLeafEnds(state) {
19311
20002
  durationHours: activity.durationHours,
19312
20003
  task: activity
19313
20004
  });
20005
+ activity.calendarDuration = computeCalendarDuration(activity);
19314
20006
  });
19315
20007
  }
19316
20008
  function recomputeDurationsFromDates(state) {
@@ -19382,6 +20074,7 @@ function initializeCore(input) {
19382
20074
  );
19383
20075
  const sector = parsed2.sector;
19384
20076
  sector.statusCriteria = resolveStatusCriteria(input.statusCriteria);
20077
+ sector.ganttId = input.ganttId ?? null;
19385
20078
  const calendars = parsed2.calendars;
19386
20079
  const baseCalendars = parsed2.baseCalendars ?? [];
19387
20080
  const snapshot = buildStateSnapshot(
@@ -19588,6 +20281,7 @@ function evaluateEnd(formula, proposedEdgeTimeMs) {
19588
20281
  }
19589
20282
 
19590
20283
  // src/init/schedule-core.ts
20284
+ var PREVIEW_DISPATCH_KINDS = /* @__PURE__ */ new Set(["inline-edit", "bulk-edit", "dates-batch"]);
19591
20285
  function changesRequireFilterProjectionRebuild(changes) {
19592
20286
  for (const change of changes.activities) {
19593
20287
  if (change.kind === "deleted") return true;
@@ -19612,6 +20306,7 @@ function isCurrentFilterProjection(projection, filter, context) {
19612
20306
  return hasCurrentHours && hasCurrentLocale;
19613
20307
  }
19614
20308
  var ScheduleCore = class {
20309
+ _activityClipboard = { value: null };
19615
20310
  _status = SCHEDULE_CORE_STATUS.READY;
19616
20311
  coreRuntime;
19617
20312
  _undo = new UndoRecorder();
@@ -19707,6 +20402,23 @@ var ScheduleCore = class {
19707
20402
  this.assertReady();
19708
20403
  return readAllActivities(this.coreRuntime.state);
19709
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
+ }
19710
20422
  getChildrenView(parentId) {
19711
20423
  this.assertReady();
19712
20424
  return readChildren(this.coreRuntime.state, parentId);
@@ -19719,6 +20431,26 @@ var ScheduleCore = class {
19719
20431
  this.assertReady();
19720
20432
  return readAllLinks(this.coreRuntime.state);
19721
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
+ }
19722
20454
  getProperty(activityId, property) {
19723
20455
  this.assertReady();
19724
20456
  const snapshot = this.coreRuntime.state.getActivity(activityId);
@@ -19747,11 +20479,11 @@ var ScheduleCore = class {
19747
20479
  }
19748
20480
  getActiveFilter() {
19749
20481
  this.assertReady();
19750
- return this.coreRuntime.state.getActiveFilter();
20482
+ return cloneDomainValue(this.coreRuntime.state.getActiveFilter());
19751
20483
  }
19752
20484
  getActiveOrder() {
19753
20485
  this.assertReady();
19754
- return this.coreRuntime.state.getActiveOrder();
20486
+ return cloneDomainValue(this.coreRuntime.state.getActiveOrder());
19755
20487
  }
19756
20488
  getVisualOrderIds() {
19757
20489
  this.assertReady();
@@ -19802,6 +20534,74 @@ var ScheduleCore = class {
19802
20534
  this.assertReady();
19803
20535
  return this._saveTracker.hasUnsavedChanges();
19804
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
+ }
19805
20605
  getDeletedActivitiesSinceLastSave() {
19806
20606
  this.assertReady();
19807
20607
  return this._saveTracker.deletedActivities(this.coreRuntime.state.getAllActivities()).map(cloneDomainValue);
@@ -19884,11 +20684,11 @@ var ScheduleCore = class {
19884
20684
  });
19885
20685
  }
19886
20686
  async dispatch(action, options = {}) {
19887
- const admitted = this._beginScheduleMutation(action);
20687
+ const admitted = options.preview === true ? false : this._beginScheduleMutation(action);
19888
20688
  return this._enqueue(() => this._dispatchInner(action, options, admitted));
19889
20689
  }
19890
20690
  async applyActivityBatch(action, options = {}) {
19891
- const admitted = this._beginScheduleMutation(action);
20691
+ const admitted = options.preview === true ? false : this._beginScheduleMutation(action);
19892
20692
  return this._enqueue(() => this._dispatchInner(action, options, admitted));
19893
20693
  }
19894
20694
  async _dispatchInner(action, options = {}, admitted = false) {
@@ -19903,12 +20703,23 @@ var ScheduleCore = class {
19903
20703
  }
19904
20704
  async _runDispatch(action, options) {
19905
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
+ }
19906
20715
  const pipelineContext = buildPipelineContext(this.coreRuntime.state, {
19907
20716
  hoursPerDay: this.coreRuntime.sector.hoursPerDay,
19908
20717
  dateFormat: this.coreRuntime.sector.dateFormat,
19909
- 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 } : {}
19910
20721
  });
19911
- const historyPolicy = getDispatchHistoryPolicy(action);
20722
+ const historyPolicy = getDispatchHistoryPolicy(action, options);
19912
20723
  const generatorSnapshot = [
19913
20724
  this.coreRuntime.activityIdGenerator.snapshot(),
19914
20725
  this.coreRuntime.linkIdGenerator.snapshot(),
@@ -19929,6 +20740,7 @@ var ScheduleCore = class {
19929
20740
  uidGen: this.coreRuntime.uniqueCorrelativeIdGenerator,
19930
20741
  customIdTracker: this.coreRuntime.customIdTracker,
19931
20742
  reporter: this.coreRuntime.reporter,
20743
+ activityClipboard: this._activityClipboard,
19932
20744
  defaultBaseCalendarId: this.coreRuntime.baseCalendars.find(
19933
20745
  (calendar) => calendar.baseDefault
19934
20746
  )?.id ?? null,
@@ -19971,7 +20783,7 @@ var ScheduleCore = class {
19971
20783
  action.activityIds,
19972
20784
  (activityId) => this.coreRuntime.state.getActivity(activityId)
19973
20785
  );
19974
- } else {
20786
+ } else if (options.preview !== true) {
19975
20787
  this._saveTracker.recordChanges(
19976
20788
  result.changes.activities,
19977
20789
  result.changes.links,
@@ -19982,6 +20794,20 @@ var ScheduleCore = class {
19982
20794
  }
19983
20795
  result = this._withReappliedViewState(action, result);
19984
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
+ }
19985
20811
  if (dispatchChangesSchedulingState(action) && changeSetIsSubstantive(result.changes)) {
19986
20812
  this._recordScheduleMutation(
19987
20813
  options.skipAutoSchedule !== true && options.skipCriticalPath !== true && resolveRunCriticalPath(action)
@@ -20496,6 +21322,9 @@ var DEFAULT_HOURS_PER_DAY = 8;
20496
21322
 
20497
21323
  // src/dispatch/action-kinds.ts
20498
21324
  var DISPATCH_ACTION_KIND = {
21325
+ ACTIVITY_CLIPBOARD_CLEAR: "activity-clipboard-clear",
21326
+ ACTIVITY_COPY: "activity-copy",
21327
+ ACTIVITY_PASTE_COPIED: "activity-paste-copied",
20499
21328
  PERSISTENCE_ACKNOWLEDGE: "persistence-acknowledge",
20500
21329
  BASELINE_APPLY: "baseline-apply",
20501
21330
  PONDERATOR_CRITERION_SET: "ponderator-criterion-set",
@@ -20513,16 +21342,19 @@ var DISPATCH_ACTION_KIND = {
20513
21342
  ACTIVITY_INDENT: "activity-indent",
20514
21343
  ACTIVITY_OUTDENT: "activity-outdent",
20515
21344
  ACTIVITY_SET_PROGRESS: "activity-set-progress",
20516
- ACTIVITY_PASTE: "activity-paste",
20517
21345
  SELECTION_TOGGLE: "selection-toggle",
20518
21346
  SELECTION_REPLACE: "selection-replace",
20519
21347
  VISIBILITY_SET: "visibility-set",
20520
21348
  FILTER_SET: "filter-set",
20521
21349
  SORT_SET: "sort-set",
20522
21350
  SIR_SYNC: "sir-sync",
21351
+ SIR_APPROVE: "sir-approve",
20523
21352
  ACTIVITY_LOOKAHEAD_SYNC: "activity-lookahead-sync"
20524
21353
  };
20525
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,
20526
21358
  [DISPATCH_ACTION_KIND.PERSISTENCE_ACKNOWLEDGE]: true,
20527
21359
  [DISPATCH_ACTION_KIND.BASELINE_APPLY]: true,
20528
21360
  [DISPATCH_ACTION_KIND.PONDERATOR_CRITERION_SET]: true,
@@ -20540,13 +21372,13 @@ var KIND_CATALOG_COVERS_UNION = {
20540
21372
  [DISPATCH_ACTION_KIND.ACTIVITY_INDENT]: true,
20541
21373
  [DISPATCH_ACTION_KIND.ACTIVITY_OUTDENT]: true,
20542
21374
  [DISPATCH_ACTION_KIND.ACTIVITY_SET_PROGRESS]: true,
20543
- [DISPATCH_ACTION_KIND.ACTIVITY_PASTE]: true,
20544
21375
  [DISPATCH_ACTION_KIND.SELECTION_TOGGLE]: true,
20545
21376
  [DISPATCH_ACTION_KIND.SELECTION_REPLACE]: true,
20546
21377
  [DISPATCH_ACTION_KIND.VISIBILITY_SET]: true,
20547
21378
  [DISPATCH_ACTION_KIND.FILTER_SET]: true,
20548
21379
  [DISPATCH_ACTION_KIND.SORT_SET]: true,
20549
21380
  [DISPATCH_ACTION_KIND.SIR_SYNC]: true,
21381
+ [DISPATCH_ACTION_KIND.SIR_APPROVE]: true,
20550
21382
  [DISPATCH_ACTION_KIND.ACTIVITY_LOOKAHEAD_SYNC]: true
20551
21383
  };
20552
21384
  var DISPATCH_ACTION_KINDS = Object.keys(KIND_CATALOG_COVERS_UNION);