@outbuild-company/schedule-core 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -141,12 +141,11 @@ var REJECTION_REASON = {
141
141
  ANCHOR_SIBLING_NOT_FOUND: "anchor_sibling_not_found",
142
142
  ANCHOR_SIBLING_WRONG_PARENT: "anchor_sibling_wrong_parent",
143
143
  PARENT_FROZEN_BY_SIR: "parent_frozen_by_sir",
144
- SIR_REQUEST_NOT_PENDING: "sir_request_not_pending",
145
- PREVIEW_UNSUPPORTED_ACTION: "preview_unsupported_action",
146
144
  CANNOT_MOVE_INTO_OWN_DESCENDANT: "cannot_move_into_own_descendant",
147
145
  INDENT_NO_ELIGIBLE_SIBLING: "indent_no_eligible_sibling",
148
146
  OUTDENT_ROOT_LEVEL_NOT_EDITABLE: "outdent_root_level_not_editable",
149
147
  HIERARCHY_CYCLE: "hierarchy_cycle",
148
+ SANDBOX_PERSISTENCE_FORBIDDEN: "sandbox_persistence_forbidden",
150
149
  INVALID_PROGRESS_VALUE: "invalid_progress_value_must_be_0_or_100"
151
150
  };
152
151
 
@@ -1793,6 +1792,8 @@ function buildVirtualLink(originalLink, sourceId, targetId, counter, sourceLag,
1793
1792
  target: targetId,
1794
1793
  type: originalLink.type,
1795
1794
  lag: originalLink.lag,
1795
+ freeSlackHours: null,
1796
+ isCritical: false,
1796
1797
  _sourceLag: sourceLag,
1797
1798
  _targetLag: targetLag,
1798
1799
  _trueLag: trueLag,
@@ -2870,7 +2871,9 @@ function applyLinkCreates(links, deps) {
2870
2871
  source: operation.source,
2871
2872
  target: operation.target,
2872
2873
  type: operation.type,
2873
- lag: operation.lag
2874
+ lag: operation.lag,
2875
+ freeSlackHours: null,
2876
+ isCritical: false
2874
2877
  }))
2875
2878
  ];
2876
2879
  const effectiveVerdict = validateTopologyCommit({
@@ -2889,6 +2892,8 @@ function applyLinkCreates(links, deps) {
2889
2892
  target: operation.target,
2890
2893
  type: operation.type,
2891
2894
  lag: operation.lag,
2895
+ freeSlackHours: null,
2896
+ isCritical: false,
2892
2897
  ...preserved?.ganttId === void 0 ? {} : { ganttId: preserved.ganttId },
2893
2898
  ...preserved?.sectorId === void 0 ? {} : { sectorId: preserved.sectorId },
2894
2899
  ...preserved?.proplannerId === void 0 ? {} : { proplannerId: preserved.proplannerId }
@@ -2931,7 +2936,9 @@ function applyCreate(op, deps) {
2931
2936
  source: op.source,
2932
2937
  target: op.target,
2933
2938
  type: op.type,
2934
- lag: op.lag
2939
+ lag: op.lag,
2940
+ freeSlackHours: null,
2941
+ isCritical: false
2935
2942
  };
2936
2943
  const effectiveVerdict = validateTopologyCommit({
2937
2944
  port,
@@ -3445,19 +3452,44 @@ async function recomputeParentsCooperatively(parents, adapter, progressMode, opt
3445
3452
  }
3446
3453
  function findAncestorsOfDirty(adapter, dirtyIds) {
3447
3454
  const ancestors = /* @__PURE__ */ new Set();
3448
- for (const id of dirtyIds) walkAncestors(id, adapter, ancestors);
3455
+ const parentById = /* @__PURE__ */ new Map();
3456
+ for (const id of dirtyIds) {
3457
+ walkAncestors(id, adapter, ancestors, parentById);
3458
+ }
3449
3459
  if (ancestors.size === 0) return [];
3450
3460
  const ids = Array.from(ancestors);
3451
- const depths = computeDepths(ids, adapter);
3461
+ const depths = computeKnownDepths(ids, parentById);
3452
3462
  return sortDeepestFirst(ids, depths);
3453
3463
  }
3454
- function walkAncestors(id, adapter, collector) {
3464
+ function walkAncestors(id, adapter, collector, parentById) {
3455
3465
  let current = adapter.getParentId(id);
3456
3466
  while (current && !isRootParent(current)) {
3457
3467
  if (collector.has(current)) return;
3458
3468
  collector.add(current);
3459
- current = adapter.getParentId(current);
3469
+ const parent = adapter.getParentId(current);
3470
+ parentById.set(current, parent);
3471
+ current = parent;
3472
+ }
3473
+ }
3474
+ function computeKnownDepths(ids, parentById) {
3475
+ const depths = /* @__PURE__ */ new Map();
3476
+ for (const id of ids) {
3477
+ if (depths.has(id)) continue;
3478
+ const path = [];
3479
+ const inPath = /* @__PURE__ */ new Set();
3480
+ let current = id;
3481
+ while (current && !isRootParent(current) && !depths.has(current) && !inPath.has(current)) {
3482
+ path.push(current);
3483
+ inPath.add(current);
3484
+ current = parentById.get(current) ?? null;
3485
+ }
3486
+ let depth = current ? depths.get(current) ?? 0 : 0;
3487
+ for (let index = path.length - 1; index >= 0; index -= 1) {
3488
+ depth += 1;
3489
+ depths.set(path[index], depth);
3490
+ }
3460
3491
  }
3492
+ return depths;
3461
3493
  }
3462
3494
  function aggregateChildren(childrenIds, adapter, includeRealWork) {
3463
3495
  let minStart = null;
@@ -3681,19 +3713,6 @@ function durationToHours(durationDays, hoursPerDay) {
3681
3713
  return Math.round(Math.ceil(durationDays * hoursPerDay * 60) / 60);
3682
3714
  }
3683
3715
 
3684
- // src/columns/shared/error-codes.ts
3685
- var PARSE_ERROR = {
3686
- NOT_A_NUMBER: "not_a_number",
3687
- // ISSUE-057 (CP6): la unidad de una duración se declara, no se adivina.
3688
- MISSING_INPUT_UNIT: "missing_input_unit",
3689
- // D-OWNER-5: una duración en días es entera; una fracción se rechaza sin
3690
- // redondear (el redondeo silencioso era la puerta del bug de las 320 horas).
3691
- FRACTIONAL_DAYS: "fractional_days"
3692
- };
3693
- var VALIDATION_REASON = {
3694
- UNCHANGED: "unchanged",
3695
- NEGATIVE_DURATION: "negative_duration"};
3696
-
3697
3716
  // src/columns/end-date/setter.ts
3698
3717
  var setEndDate = (value) => ({
3699
3718
  endDate: value
@@ -3795,6 +3814,10 @@ var POST_PROCESSOR = {
3795
3814
  RECORD_LAST_START_DATE: "recordLastStartDate"
3796
3815
  };
3797
3816
 
3817
+ // src/columns/shared/error-codes.ts
3818
+ var VALIDATION_REASON = {
3819
+ NEGATIVE_DURATION: "negative_duration"};
3820
+
3798
3821
  // src/columns/shared/constraint-validation.ts
3799
3822
  function checkConstraintViolation2(constraintType, constraintDate, projectedDate) {
3800
3823
  return checkConstraintViolation(constraintType, constraintDate, projectedDate);
@@ -3819,18 +3842,7 @@ var durationPipeline = {
3819
3842
  },
3820
3843
  parseInput(rawValue, _activity, ctx) {
3821
3844
  const numericValue = parseFloat(String(rawValue));
3822
- if (!Number.isFinite(numericValue)) {
3823
- return parseError(PARSE_ERROR.NOT_A_NUMBER);
3824
- }
3825
- if (ctx.inputUnit === void 0) {
3826
- return parseError(PARSE_ERROR.MISSING_INPUT_UNIT);
3827
- }
3828
- if (ctx.inputUnit === "hours") {
3829
- return parsed(numericValue);
3830
- }
3831
- if (!Number.isInteger(numericValue)) {
3832
- return parseError(PARSE_ERROR.FRACTIONAL_DAYS);
3833
- }
3845
+ if (!Number.isFinite(numericValue)) return parseError("not_a_number");
3834
3846
  return parsed(durationToHours(numericValue, ctx.hoursPerDay));
3835
3847
  },
3836
3848
  validate(_activity, oldValue, newValue) {
@@ -4866,7 +4878,9 @@ var ACTIVITY_PROPERTY = {
4866
4878
  CALENDAR_DURATION: "calendarDuration"};
4867
4879
  var LINK_PROPERTY = {
4868
4880
  TYPE: "type",
4869
- LAG: "lag"};
4881
+ LAG: "lag",
4882
+ FREE_SLACK_HOURS: "freeSlackHours",
4883
+ IS_CRITICAL: "isCritical"};
4870
4884
 
4871
4885
  // src/internal/state/dynamic-writes.ts
4872
4886
  function setActivityFieldDynamic(state, activityId, field, value) {
@@ -5722,10 +5736,11 @@ function mutateTheDateToFutureOrPastInBaseRestriction(dateBaseToCalculate, restr
5722
5736
 
5723
5737
  // src/critical-path/legacy/base/parents-calculations/forward.js
5724
5738
  var CalculateForwardParentsWithLinks = class {
5725
- constructor(linkedActivitiesData, activity, gantt) {
5739
+ constructor(linkedActivitiesData, activity, gantt, linkCandidates) {
5726
5740
  this.linkedActivitiesData = linkedActivitiesData;
5727
5741
  this.activity = activity;
5728
5742
  this.gantt = gantt;
5743
+ this.linkCandidates = linkCandidates;
5729
5744
  this.activityCalendar = this.gantt.getCalendar(this.activity.calendar_id);
5730
5745
  if (!this.activityCalendar) {
5731
5746
  throw new Error(
@@ -5770,6 +5785,10 @@ var CalculateForwardParentsWithLinks = class {
5770
5785
  continue;
5771
5786
  }
5772
5787
  if (calculation) {
5788
+ this.linkCandidates?.set(linkedActivity.linkId, {
5789
+ targetId: String(this.activity.id),
5790
+ ...calculation
5791
+ });
5773
5792
  calculations.push(calculation);
5774
5793
  }
5775
5794
  }
@@ -6217,7 +6236,8 @@ function parentWithLinks(params) {
6217
6236
  linkDirection,
6218
6237
  calculations,
6219
6238
  gantt,
6220
- direction
6239
+ direction,
6240
+ linkCandidates
6221
6241
  } = params;
6222
6242
  const linksArray = parent[linkProperty];
6223
6243
  const parentData = extractParentData(parent);
@@ -6232,7 +6252,8 @@ function parentWithLinks(params) {
6232
6252
  calculation = new forward_default(
6233
6253
  linksWithActivityData,
6234
6254
  parent,
6235
- gantt
6255
+ gantt,
6256
+ linkCandidates
6236
6257
  );
6237
6258
  calculation = calculation.calculate();
6238
6259
  } else {
@@ -6277,6 +6298,7 @@ function getLinksWithActivityDataForward(linksArray, linkDirection, calculations
6277
6298
  return null;
6278
6299
  }
6279
6300
  return {
6301
+ linkId: String(linkData.id),
6280
6302
  es: calculationsOfLinkedActivity.es,
6281
6303
  ef: calculationsOfLinkedActivity.ef,
6282
6304
  linkLag: linkData.lag,
@@ -6377,6 +6399,7 @@ var CalculationOfParent = class {
6377
6399
  this.calculationOfParentsFromLinks = params.calculationOfParentsFromLinks;
6378
6400
  this.singleParents = params.singleParents;
6379
6401
  this.parentCalculationFromChildren = params.parentCalculationFromChildren;
6402
+ this.linkCandidates = params.linkCandidates;
6380
6403
  this.gantt = params.gantt;
6381
6404
  }
6382
6405
  handleParentWithLinks() {
@@ -6424,7 +6447,8 @@ var CalculationOfParent = class {
6424
6447
  linkDirection: this.linkDirection,
6425
6448
  gantt: this.gantt,
6426
6449
  calculations: this.calculations,
6427
- direction: this.direction
6450
+ direction: this.direction,
6451
+ linkCandidates: this.linkCandidates
6428
6452
  });
6429
6453
  this.calculationsOfParentThatImpactChildrens.set(activity.id, calculation);
6430
6454
  this.calculationOfParentsFromLinks.add(activity.id);
@@ -7073,6 +7097,7 @@ var CriticalPathHelpers = class {
7073
7097
  calculationOfParentsFromLinks: this.calculationOfParentsFromLinks,
7074
7098
  singleParents: this.singleParents,
7075
7099
  parentCalculationFromChildren: this.parentCalculationFromChildren,
7100
+ linkCandidates: this.linkCandidates,
7076
7101
  gantt: this.gantt
7077
7102
  });
7078
7103
  if (this.pendingParentsWithLinks.has(activity.id)) {
@@ -7140,6 +7165,7 @@ var LinksRulesCalculationsMethods = class {
7140
7165
  this.parentsWithLinks = calculationObject.parentsWithLinks;
7141
7166
  this.parentsCalculations = calculationObject.parentsCalculations;
7142
7167
  this.gantt = calculationObject.ganttInstance;
7168
+ this.linkCandidates = calculationObject.linkCandidates;
7143
7169
  }
7144
7170
  getEsAndEfFromPreviousCalculatedActivities(activity) {
7145
7171
  const isAlap = this.alapMap.has(activity);
@@ -7311,7 +7337,12 @@ var LinksConstraintCalculator = class extends LinksRulesCalculationsMethods_defa
7311
7337
  if (!linkData) {
7312
7338
  throw new Error("Link data not found", { cause: { linkId: link } });
7313
7339
  }
7314
- return this.calculateLink(linkData);
7340
+ const calculation = this.calculateLink(linkData);
7341
+ this.linkCandidates?.set(String(linkData.id), {
7342
+ targetId: String(this.referenceActivity.id),
7343
+ ...calculation
7344
+ });
7345
+ return calculation;
7315
7346
  });
7316
7347
  const parent = Number(this.referenceActivity.parent);
7317
7348
  if (this.parentsWithLinks.has(parent)) {
@@ -7577,6 +7608,7 @@ var ForwardPath = class extends generic_calculations_default {
7577
7608
  this.totalFloatSnltFnlt = /* @__PURE__ */ new Map();
7578
7609
  this.totalFloatForMsoMfo = /* @__PURE__ */ new Map();
7579
7610
  this.totalFloatForAsap = /* @__PURE__ */ new Map();
7611
+ this.linkCandidates = /* @__PURE__ */ new Map();
7580
7612
  this.calculationOfParentsFromLinks = /* @__PURE__ */ new Set();
7581
7613
  this.parentCalculationFromChildren = /* @__PURE__ */ new Set();
7582
7614
  this.forbiddenActivitiesLinkedToParentsWithLinks = /* @__PURE__ */ new Set();
@@ -7675,7 +7707,8 @@ var ForwardPath = class extends generic_calculations_default {
7675
7707
  constraint: constraintType,
7676
7708
  parentsWithLinks: this.pendingParentsWithLinks,
7677
7709
  parentsCalculations: this.calculationsOfParentThatImpactChildrens,
7678
- ganttInstance: this.gantt
7710
+ ganttInstance: this.gantt,
7711
+ linkCandidates: this.linkCandidates
7679
7712
  }).calculate();
7680
7713
  if (constraintType === CONSTRAINT_TYPES.ASAP) {
7681
7714
  this.totalFloatForAsap.set(activity, {
@@ -7693,7 +7726,8 @@ var ForwardPath = class extends generic_calculations_default {
7693
7726
  constraint: constraintType,
7694
7727
  parentsWithLinks: this.pendingParentsWithLinks,
7695
7728
  parentsCalculations: this.calculationsOfParentThatImpactChildrens,
7696
- ganttInstance: this.gantt
7729
+ ganttInstance: this.gantt,
7730
+ linkCandidates: this.linkCandidates
7697
7731
  }).calculate());
7698
7732
  }
7699
7733
  if (constraintType === CONSTRAINT_TYPES.ALAP) {
@@ -8730,7 +8764,6 @@ var BackwardPath = class extends generic_calculations_default {
8730
8764
  }
8731
8765
  }
8732
8766
  if ([CONSTRAINT_TYPES.SNLT].includes(activity.constraint_type)) {
8733
- const constraintDate = activity.constraint_date;
8734
8767
  const parentLinkCalcualation2 = this.calculationsOfParentThatImpactChildrens.get(parent);
8735
8768
  const lsToComparison = addDurationToDate(
8736
8769
  this.gantt.getCalendar(activity.calendar_id),
@@ -8738,6 +8771,7 @@ var BackwardPath = class extends generic_calculations_default {
8738
8771
  -activity.duration,
8739
8772
  activity
8740
8773
  );
8774
+ const constraintDate = activity.constraint_date ?? lsToComparison;
8741
8775
  ls = new Date(
8742
8776
  Math.min(constraintDate.getTime(), lsToComparison.getTime())
8743
8777
  );
@@ -8749,8 +8783,8 @@ var BackwardPath = class extends generic_calculations_default {
8749
8783
  );
8750
8784
  }
8751
8785
  if ([CONSTRAINT_TYPES.FNLT].includes(activity.constraint_type)) {
8752
- const constraintDate = activity.constraint_date;
8753
8786
  const parentLinkCalcualation2 = this.calculationsOfParentThatImpactChildrens.get(parent);
8787
+ const constraintDate = activity.constraint_date ?? parentLinkCalcualation2.lf;
8754
8788
  ls = new Date(
8755
8789
  Math.min(constraintDate.getTime(), parentLinkCalcualation2.lf.getTime())
8756
8790
  );
@@ -9134,18 +9168,20 @@ var FreeFloat = class {
9134
9168
  );
9135
9169
  }
9136
9170
  calculateFreeFloatForEndOfProject(constraint) {
9171
+ const calculation = constraint === CONSTRAINT_TYPES.ALAP ? this.backwardMap.get(this.activity.id) : this.forwardMap.get(this.activity.id);
9137
9172
  return this.parseHoursToDays(
9138
9173
  this.calculationWithCalendar(
9139
9174
  this.gantt.getSubtaskDates().end_date,
9140
- constraint === CONSTRAINT_TYPES.ALAP ? this.backwardMap.get(this.activity.id).lf : this.forwardMap.get(this.activity.id).ef
9175
+ constraint === CONSTRAINT_TYPES.ALAP ? calculation?.lf : calculation?.ef
9141
9176
  )
9142
9177
  );
9143
9178
  }
9144
9179
  calculateFreeFloatOfRestriction(constraint) {
9180
+ const calculation = this.forwardMap.get(this.activity.id);
9145
9181
  return this.parseHoursToDays(
9146
9182
  this.calculationWithCalendar(
9147
9183
  this.activity.constraint_date,
9148
- constraint === CONSTRAINT_TYPES.FNLT ? this.forwardMap.get(this.activity.id).ef : this.forwardMap.get(this.activity.id).es
9184
+ constraint === CONSTRAINT_TYPES.FNLT ? calculation?.ef : calculation?.es
9149
9185
  )
9150
9186
  );
9151
9187
  }
@@ -9163,111 +9199,33 @@ var FreeFloat = class {
9163
9199
  sucessorId,
9164
9200
  predecessorId
9165
9201
  }) {
9166
- const getPredecessorDate = ({
9167
- direction,
9168
- constraint: constraint2 = CONSTRAINT_TYPES.ASAP
9169
- }) => {
9170
- if (constraint2 === CONSTRAINT_TYPES.ALAP) {
9171
- return this.backwardMap.get(predecessorId)[direction];
9202
+ const successor = (isSuscessorAlap ? this.backwardMap : this.forwardMap).get(sucessorId);
9203
+ const predecessor = (constraint === CONSTRAINT_TYPES.ALAP ? this.backwardMap : this.forwardMap).get(predecessorId);
9204
+ const predecessorStart = constraint === CONSTRAINT_TYPES.ALAP ? predecessor?.ls : predecessor?.es;
9205
+ const predecessorFinish = constraint === CONSTRAINT_TYPES.ALAP ? predecessor?.lf : predecessor?.ef;
9206
+ return {
9207
+ fs: {
9208
+ sucessor: successor?.ls ?? successor?.es,
9209
+ predecessor: predecessorFinish
9210
+ },
9211
+ ss: {
9212
+ sucessor: successor?.ls ?? successor?.es,
9213
+ predecessor: predecessorStart
9214
+ },
9215
+ ff: {
9216
+ sucessor: successor?.lf ?? successor?.ef,
9217
+ predecessor: predecessorFinish
9218
+ },
9219
+ sf: {
9220
+ sucessor: successor?.lf ?? successor?.ef,
9221
+ predecessor: predecessorStart
9172
9222
  }
9173
- return this.forwardMap.get(predecessorId)[direction];
9174
9223
  };
9175
- if (constraint === CONSTRAINT_TYPES.ASAP || constraint === CONSTRAINT_TYPES.SNET || constraint === CONSTRAINT_TYPES.FNET || constraint === CONSTRAINT_TYPES.MSO || constraint === CONSTRAINT_TYPES.MFO) {
9176
- return {
9177
- fs: {
9178
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
9179
- predecessor: getPredecessorDate({ direction: "ef" })
9180
- },
9181
- ss: {
9182
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
9183
- predecessor: getPredecessorDate({ direction: "es" })
9184
- },
9185
- ff: {
9186
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
9187
- predecessor: getPredecessorDate({ direction: "ef" })
9188
- },
9189
- sf: {
9190
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
9191
- predecessor: getPredecessorDate({ direction: "es" })
9192
- }
9193
- };
9194
- }
9195
- if (constraint === CONSTRAINT_TYPES.ALAP) {
9196
- return {
9197
- fs: {
9198
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
9199
- predecessor: getPredecessorDate({
9200
- constraint: CONSTRAINT_TYPES.ALAP,
9201
- direction: "lf"
9202
- })
9203
- },
9204
- ss: {
9205
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
9206
- predecessor: getPredecessorDate({
9207
- constraint: CONSTRAINT_TYPES.ALAP,
9208
- direction: "ls"
9209
- })
9210
- },
9211
- ff: {
9212
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
9213
- predecessor: getPredecessorDate({
9214
- constraint: CONSTRAINT_TYPES.ALAP,
9215
- direction: "lf"
9216
- })
9217
- },
9218
- sf: {
9219
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
9220
- predecessor: getPredecessorDate({
9221
- constraint: CONSTRAINT_TYPES.ALAP,
9222
- direction: "ls"
9223
- })
9224
- }
9225
- };
9226
- }
9227
- if (constraint === CONSTRAINT_TYPES.SNLT) {
9228
- return {
9229
- fs: {
9230
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
9231
- predecessor: getPredecessorDate({ direction: "ef" })
9232
- },
9233
- ss: {
9234
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
9235
- predecessor: getPredecessorDate({ direction: "es" })
9236
- },
9237
- ff: {
9238
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
9239
- predecessor: getPredecessorDate({ direction: "ef" })
9240
- },
9241
- sf: {
9242
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
9243
- predecessor: getPredecessorDate({ direction: "es" })
9244
- }
9245
- };
9246
- }
9247
- if (constraint === CONSTRAINT_TYPES.FNLT) {
9248
- return {
9249
- fs: {
9250
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
9251
- predecessor: getPredecessorDate({ direction: "ef" })
9252
- },
9253
- ss: {
9254
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
9255
- predecessor: getPredecessorDate({ direction: "es" })
9256
- },
9257
- ff: {
9258
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
9259
- predecessor: getPredecessorDate({ direction: "ef" })
9260
- },
9261
- sf: {
9262
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
9263
- predecessor: getPredecessorDate({ direction: "es" })
9264
- }
9265
- };
9266
- }
9267
9224
  }
9268
9225
  calculateMsoMfo(constraint) {
9269
9226
  const CONSTRAINT_DATE2 = this.activity.constraint_date || /* @__PURE__ */ new Date();
9270
- const DATE_TO_CALCULATE = constraint === CONSTRAINT_TYPES.MSO ? this.forwardMap.get(this.activity.id).es : this.forwardMap.get(this.activity.id).ef;
9227
+ const calculation = this.forwardMap.get(this.activity.id);
9228
+ const DATE_TO_CALCULATE = constraint === CONSTRAINT_TYPES.MSO ? calculation?.es : calculation?.ef;
9271
9229
  const freeSlack = this.calculationWithCalendar(
9272
9230
  CONSTRAINT_DATE2,
9273
9231
  DATE_TO_CALCULATE
@@ -9281,6 +9239,9 @@ var FreeFloat = class {
9281
9239
  return filterValues.length ? Math.min(...filterValues) : 0;
9282
9240
  }
9283
9241
  calculationWithCalendar(dateOne, dateTwo) {
9242
+ if (!(dateOne instanceof Date) || !(dateTwo instanceof Date) || !Number.isFinite(dateOne.getTime()) || !Number.isFinite(dateTwo.getTime())) {
9243
+ return Number.NaN;
9244
+ }
9284
9245
  const key = `${dateTwo.getTime()}_${dateOne.getTime()}`;
9285
9246
  if (this.durationCache.has(key)) {
9286
9247
  return this.durationCache.get(key);
@@ -9294,7 +9255,7 @@ var FreeFloat = class {
9294
9255
  return result;
9295
9256
  }
9296
9257
  parseHoursToDays(date2) {
9297
- if (!date2) {
9258
+ if (date2 === void 0 || date2 === null) {
9298
9259
  return 0;
9299
9260
  }
9300
9261
  return date2 / this.gantt.config.hoursPerDay;
@@ -9360,6 +9321,7 @@ var CriticalPath = class {
9360
9321
  this.backwardPathCalculations = null;
9361
9322
  this.totalFloat = /* @__PURE__ */ new Map();
9362
9323
  this.freeFloat = /* @__PURE__ */ new Map();
9324
+ this.linkResidualHours = /* @__PURE__ */ new Map();
9363
9325
  this.currentCalculationId = 0;
9364
9326
  }
9365
9327
  getCurrentCalculationId = () => this.currentCalculationId;
@@ -9390,6 +9352,13 @@ var CriticalPath = class {
9390
9352
  if (!isCurrent()) return false;
9391
9353
  this.forwardPathCalculations = forwardPathCalculations;
9392
9354
  this.backwardPathCalculations = backwardPath.calculations;
9355
+ const linkResidualHours = await this.calculateLinkResidualHours(
9356
+ forwardPath.linkCandidates,
9357
+ forwardPathCalculations,
9358
+ isCurrent
9359
+ );
9360
+ if (!linkResidualHours || !isCurrent()) return false;
9361
+ this.linkResidualHours = linkResidualHours;
9393
9362
  const { totalFloatMap, freeFloatMap } = await this.calculateFloatsAsync(
9394
9363
  forwardPath,
9395
9364
  backwardPath,
@@ -9409,6 +9378,38 @@ var CriticalPath = class {
9409
9378
  throw error;
9410
9379
  }
9411
9380
  }
9381
+ async calculateLinkResidualHours(linkCandidates, forwardCalculations, isCurrent = () => true) {
9382
+ const residuals = /* @__PURE__ */ new Map();
9383
+ let sliceStartTime = performance.now();
9384
+ for (const [linkId, candidate] of linkCandidates) {
9385
+ sliceStartTime = await yieldIfNeeded2(sliceStartTime);
9386
+ if (!isCurrent()) return null;
9387
+ const targetId = Number(candidate.targetId);
9388
+ const target = this.gantt.getTask(targetId);
9389
+ const settled = forwardCalculations.get(targetId);
9390
+ const candidateEf = candidate.ef;
9391
+ const settledEf = settled?.ef;
9392
+ const calendar = target && this.gantt.getCalendar(target.calendar_id);
9393
+ if (!calendar || !(candidateEf instanceof Date) || !(settledEf instanceof Date) || !Number.isFinite(candidateEf.getTime()) || !Number.isFinite(settledEf.getTime())) {
9394
+ residuals.set(linkId, null);
9395
+ continue;
9396
+ }
9397
+ const candidateTime = candidateEf.getTime();
9398
+ const settledTime = settledEf.getTime();
9399
+ const duration = settledTime >= candidateTime ? calendar.calculateDuration({
9400
+ start_date: candidateEf,
9401
+ end_date: settledEf
9402
+ }) : -calendar.calculateDuration({
9403
+ start_date: settledEf,
9404
+ end_date: candidateEf
9405
+ });
9406
+ residuals.set(
9407
+ linkId,
9408
+ Number.isFinite(duration) ? Object.is(duration, -0) ? 0 : duration : null
9409
+ );
9410
+ }
9411
+ return residuals;
9412
+ }
9412
9413
  async calculateFloatsAsync(forward, backward, isCurrent) {
9413
9414
  try {
9414
9415
  const projectDates = this.gantt.getSubtaskDates();
@@ -9512,14 +9513,52 @@ function computeLevels(state) {
9512
9513
  for (const id of activitiesById.keys()) resolveLevel(id, /* @__PURE__ */ new Set());
9513
9514
  return levelById;
9514
9515
  }
9515
- function toLegacyTasks(state) {
9516
+ function createLegacyIds(state) {
9517
+ const canonicalIds = /* @__PURE__ */ new Set();
9518
+ state.forEachActivity((activity, id) => {
9519
+ canonicalIds.add(String(id));
9520
+ if (activity.parentId !== null) canonicalIds.add(String(activity.parentId));
9521
+ });
9522
+ const numericCounts = /* @__PURE__ */ new Map();
9523
+ for (const id of canonicalIds) {
9524
+ const numeric = Number(id);
9525
+ if (Number.isSafeInteger(numeric) && numeric !== 0) {
9526
+ numericCounts.set(numeric, (numericCounts.get(numeric) ?? 0) + 1);
9527
+ }
9528
+ }
9529
+ const canonicalToLegacy = /* @__PURE__ */ new Map();
9530
+ const legacyToCanonical = /* @__PURE__ */ new Map();
9531
+ for (const id of canonicalIds) {
9532
+ const numeric = Number(id);
9533
+ if (Number.isSafeInteger(numeric) && numeric !== 0 && numericCounts.get(numeric) === 1) {
9534
+ canonicalToLegacy.set(id, numeric);
9535
+ legacyToCanonical.set(numeric, id);
9536
+ }
9537
+ }
9538
+ let next = 1;
9539
+ const toLegacy = (rawId) => {
9540
+ const id = String(rawId);
9541
+ const existing = canonicalToLegacy.get(id);
9542
+ if (existing !== void 0) return existing;
9543
+ while (legacyToCanonical.has(next)) next += 1;
9544
+ canonicalToLegacy.set(id, next);
9545
+ legacyToCanonical.set(next, id);
9546
+ return next++;
9547
+ };
9548
+ for (const id of canonicalIds) toLegacy(id);
9549
+ return {
9550
+ toLegacy,
9551
+ toCanonical: (id) => typeof id === "number" ? legacyToCanonical.get(id) ?? null : canonicalToLegacy.has(String(id)) ? String(id) : null
9552
+ };
9553
+ }
9554
+ function toLegacyTasks(state, ids) {
9516
9555
  const levelById = computeLevels(state);
9517
9556
  const leveled = /* @__PURE__ */ new Map();
9518
9557
  state.forEachActivity((activity, id) => {
9519
9558
  const key = String(id);
9520
9559
  leveled.set(key, {
9521
- id: Number(id),
9522
- parent: activity.parentId === null ? 0 : Number(activity.parentId),
9560
+ id: ids.toLegacy(key),
9561
+ parent: activity.parentId === null ? 0 : ids.toLegacy(activity.parentId),
9523
9562
  $level: levelById.get(key) ?? 0,
9524
9563
  $source: state.getOutgoingLinkIds(key),
9525
9564
  $target: state.getIncomingLinkIds(key),
@@ -9593,7 +9632,8 @@ async function createGanttAdapter(state, hoursPerDay) {
9593
9632
  };
9594
9633
  const bounds = projectBounds(state);
9595
9634
  await yieldToBrowser();
9596
- const leveled = toLegacyTasks(state);
9635
+ const ids = createLegacyIds(state);
9636
+ const leveled = toLegacyTasks(state, ids);
9597
9637
  await yieldToBrowser();
9598
9638
  const byTime = [...leveled.values()].sort(
9599
9639
  (a, b) => (a.start_date?.getTime() ?? 0) - (b.start_date?.getTime() ?? 0)
@@ -9603,24 +9643,31 @@ async function createGanttAdapter(state, hoursPerDay) {
9603
9643
  return {
9604
9644
  fullyParsed: true,
9605
9645
  config: { hoursPerDay },
9606
- getTask: (id) => leveled.get(String(id)) ?? null,
9646
+ getTask: (id) => {
9647
+ const canonicalId = ids.toCanonical(id);
9648
+ return canonicalId === null ? null : leveled.get(canonicalId) ?? null;
9649
+ },
9607
9650
  getLink: (id) => {
9608
9651
  const link = state.getLink(id);
9609
9652
  if (!link) return null;
9610
9653
  return {
9611
9654
  ...link,
9612
- source: Number(link.source),
9613
- target: Number(link.target),
9655
+ source: ids.toLegacy(link.source),
9656
+ target: ids.toLegacy(link.target),
9614
9657
  lag: link.lag
9615
9658
  };
9616
9659
  },
9617
9660
  getCalendar: resolveCalendar,
9618
- getChildren: (parentId) => state.getChildren(parentId).map((childId) => Number(childId)),
9661
+ getChildren: (parentId) => {
9662
+ const canonicalId = ids.toCanonical(parentId);
9663
+ return canonicalId === null ? [] : state.getChildren(canonicalId).map(ids.toLegacy);
9664
+ },
9619
9665
  eachTask: (cb) => {
9620
9666
  for (const task of treeOrdered) cb(task);
9621
9667
  },
9622
9668
  getTaskByTime: () => byTime,
9623
9669
  getSubtaskDates: () => bounds,
9670
+ getCanonicalActivityId: ids.toCanonical,
9624
9671
  refreshData: () => {
9625
9672
  }
9626
9673
  };
@@ -9628,21 +9675,92 @@ async function createGanttAdapter(state, hoursPerDay) {
9628
9675
 
9629
9676
  // src/critical-path/run-critical-path.ts
9630
9677
  var CP_WRITE_YIELD_EVERY = 1e3;
9631
- function fieldsForActivity(activityId, progress, maps) {
9632
- const toDate = (value) => value == null ? null : new Date(value);
9678
+ function fieldsForLink(rawResidual, sourceIsCritical, targetIsCritical, targetProgress) {
9679
+ const freeSlackHours = typeof rawResidual === "number" && Number.isFinite(rawResidual) ? Object.is(rawResidual, -0) ? 0 : rawResidual : null;
9680
+ return {
9681
+ freeSlackHours,
9682
+ isCritical: freeSlackHours !== null && freeSlackHours <= 0 && sourceIsCritical && targetIsCritical && targetProgress === 0
9683
+ };
9684
+ }
9685
+ async function collectActivityFields(state, maps, hoursPerDay, toCanonicalActivityId, isCurrent) {
9686
+ const fields = /* @__PURE__ */ new Map();
9687
+ let sinceYield = 0;
9688
+ for (const calculationId of maps.forward?.keys() ?? []) {
9689
+ if (++sinceYield >= CP_WRITE_YIELD_EVERY) {
9690
+ sinceYield = 0;
9691
+ await yieldToBrowser();
9692
+ if (!isCurrent()) return null;
9693
+ }
9694
+ const activityId = toCanonicalActivityId(calculationId);
9695
+ if (activityId === null) continue;
9696
+ const activity = state.getActivity(activityId);
9697
+ if (!activity) continue;
9698
+ fields.set(
9699
+ String(activityId),
9700
+ fieldsForActivity(
9701
+ calculationId,
9702
+ Number(activity.progress ?? 0),
9703
+ maps,
9704
+ hoursPerDay
9705
+ )
9706
+ );
9707
+ }
9708
+ return isCurrent() ? fields : null;
9709
+ }
9710
+ async function collectLinkFields(state, linkResidualHours, activityFields, isCurrent) {
9711
+ const fields = /* @__PURE__ */ new Map();
9712
+ let sinceYield = 0;
9713
+ for (const link of state.getAllLinks()) {
9714
+ if (++sinceYield >= CP_WRITE_YIELD_EVERY) {
9715
+ sinceYield = 0;
9716
+ await yieldToBrowser();
9717
+ if (!isCurrent()) return null;
9718
+ }
9719
+ fields.set(
9720
+ String(link.id),
9721
+ fieldsForLink(
9722
+ linkResidualHours.get(String(link.id)),
9723
+ activityFields.get(link.source)?.isCritical === true,
9724
+ activityFields.get(link.target)?.isCritical === true,
9725
+ state.getActivity(link.target)?.progress
9726
+ )
9727
+ );
9728
+ }
9729
+ return isCurrent() ? fields : null;
9730
+ }
9731
+ var slackDaysToHours = (valueDays, hoursPerDay) => {
9732
+ if (typeof valueDays !== "number" || !Number.isFinite(valueDays) || !Number.isFinite(hoursPerDay) || hoursPerDay <= 0) {
9733
+ return null;
9734
+ }
9735
+ const hours = valueDays * hoursPerDay;
9736
+ if (!Number.isFinite(hours)) return null;
9737
+ return Object.is(hours, -0) ? 0 : hours;
9738
+ };
9739
+ function fieldsForActivity(activityId, progress, maps, hoursPerDay) {
9740
+ const toDate = (value) => {
9741
+ if (value == null) return null;
9742
+ const date2 = new Date(value);
9743
+ return Number.isFinite(date2.getTime()) ? date2 : null;
9744
+ };
9633
9745
  const forward = maps.forward?.get(activityId);
9634
9746
  const backward = maps.backward?.get(activityId);
9635
- const totalSlack = maps.totalFloat.get(activityId) ?? null;
9747
+ const totalSlackHours = slackDaysToHours(
9748
+ maps.totalFloat.get(activityId),
9749
+ hoursPerDay
9750
+ );
9636
9751
  return {
9637
9752
  criticalPath: {
9638
9753
  earlyStart: toDate(forward?.es),
9639
9754
  earlyFinish: toDate(forward?.ef),
9640
9755
  lateStart: toDate(backward?.ls),
9641
9756
  lateFinish: toDate(backward?.lf),
9642
- totalSlackHours: totalSlack
9757
+ totalSlackHours
9643
9758
  },
9644
- isCritical: progress < 100 && totalSlack != null && totalSlack <= 0,
9645
- freeSlackHours: maps.freeFloat.get(activityId)?.activityFreeFloat ?? null
9759
+ isCritical: progress < 100 && totalSlackHours !== null && totalSlackHours <= 0,
9760
+ freeSlackHours: slackDaysToHours(
9761
+ maps.freeFloat.get(activityId)?.activityFreeFloat,
9762
+ hoursPerDay
9763
+ )
9646
9764
  };
9647
9765
  }
9648
9766
  var datesEqual = (left, right) => left === right || left !== null && right !== null && left.getTime() === right.getTime();
@@ -9655,6 +9773,14 @@ function setCriticalPathFieldsIfChanged(state, activityId, fields) {
9655
9773
  }
9656
9774
  state.setActivityFields(activityId, fields);
9657
9775
  }
9776
+ function setCriticalPathLinkFieldsIfChanged(state, linkId, fields) {
9777
+ const link = state.getLink(linkId);
9778
+ if (!link || Object.is(link.freeSlackHours, fields.freeSlackHours) && Object.is(link.isCritical, fields.isCritical)) {
9779
+ return;
9780
+ }
9781
+ state.setLinkField(linkId, "freeSlackHours", fields.freeSlackHours);
9782
+ state.setLinkField(linkId, "isCritical", fields.isCritical);
9783
+ }
9658
9784
  async function computeCriticalPathFields(state, hoursPerDay, isCurrent = () => true) {
9659
9785
  if (!isCurrent()) return null;
9660
9786
  const gantt = await createGanttAdapter(state, hoursPerDay);
@@ -9672,35 +9798,32 @@ async function computeCriticalPathFields(state, hoursPerDay, isCurrent = () => t
9672
9798
  totalFloat: criticalPath.totalFloat,
9673
9799
  freeFloat: criticalPath.freeFloat
9674
9800
  };
9675
- const activityIds = /* @__PURE__ */ new Set([...maps.forward?.keys() ?? []]);
9676
- const fieldsByActivity = /* @__PURE__ */ new Map();
9677
- let sinceYield = 0;
9678
- for (const activityId of activityIds) {
9679
- if (++sinceYield >= CP_WRITE_YIELD_EVERY) {
9680
- sinceYield = 0;
9681
- await yieldToBrowser();
9682
- if (!isCurrent()) return null;
9683
- }
9684
- const activity = state.getActivity(activityId);
9685
- if (!activity) continue;
9686
- const progress = Number(activity.progress ?? 0);
9687
- fieldsByActivity.set(
9688
- activityId,
9689
- fieldsForActivity(activityId, progress, maps)
9690
- );
9691
- }
9692
- return isCurrent() ? fieldsByActivity : null;
9693
- }
9694
- async function applyCriticalPath(state, hoursPerDay, isCurrent = () => true) {
9695
- const fieldsByActivity = await computeCriticalPathFields(
9801
+ const activities = await collectActivityFields(
9696
9802
  state,
9803
+ maps,
9697
9804
  hoursPerDay,
9805
+ gantt.getCanonicalActivityId,
9806
+ isCurrent
9807
+ );
9808
+ if (!activities) return null;
9809
+ const links = await collectLinkFields(
9810
+ state,
9811
+ criticalPath.linkResidualHours,
9812
+ activities,
9698
9813
  isCurrent
9699
9814
  );
9700
- if (!fieldsByActivity || !isCurrent()) return false;
9701
- for (const [activityId, fields] of fieldsByActivity) {
9815
+ return links ? { activities, links } : null;
9816
+ }
9817
+ async function applyCriticalPath(state, hoursPerDay, isCurrent = () => true) {
9818
+ const fields = await computeCriticalPathFields(state, hoursPerDay, isCurrent);
9819
+ if (!fields || !isCurrent()) return false;
9820
+ for (const [activityId, activityFields] of fields.activities) {
9702
9821
  if (!isCurrent()) return false;
9703
- setCriticalPathFieldsIfChanged(state, activityId, fields);
9822
+ setCriticalPathFieldsIfChanged(state, activityId, activityFields);
9823
+ }
9824
+ for (const [linkId, linkFields] of fields.links) {
9825
+ if (!isCurrent()) return false;
9826
+ setCriticalPathLinkFieldsIfChanged(state, linkId, linkFields);
9704
9827
  }
9705
9828
  return true;
9706
9829
  }
@@ -9802,9 +9925,15 @@ function isSchedulingDispatch(action, structuralStaleKinds) {
9802
9925
  return true;
9803
9926
  }
9804
9927
  function dispatchChangesSchedulingState(action) {
9928
+ if (action.kind === "activity-lookahead-sync") {
9929
+ return action.persistedProgress !== void 0;
9930
+ }
9805
9931
  return isSchedulingDispatch(action, NO_STRUCTURAL_EXEMPTIONS);
9806
9932
  }
9807
9933
  function resolveRunCriticalPath(action) {
9934
+ if (action.kind === "activity-lookahead-sync") {
9935
+ return action.persistedProgress !== void 0;
9936
+ }
9808
9937
  return isSchedulingDispatch(
9809
9938
  action,
9810
9939
  STRUCTURAL_KINDS_LEAVING_CRITICAL_PATH_STALE
@@ -10090,6 +10219,23 @@ function detectSirAutoReject(beforeSnap, adapter, touchedIds) {
10090
10219
  return effects;
10091
10220
  }
10092
10221
 
10222
+ // src/dispatch/conversion.ts
10223
+ function snapshotToLink(l) {
10224
+ const view = {
10225
+ id: String(l.id),
10226
+ source: String(l.source),
10227
+ target: String(l.target),
10228
+ type: l.type,
10229
+ lag: Number(l.lag),
10230
+ freeSlackHours: typeof l.freeSlackHours === "number" && Number.isFinite(l.freeSlackHours) ? l.freeSlackHours : null,
10231
+ isCritical: l.isCritical === true
10232
+ };
10233
+ if (l.proplannerId != null) view.proplannerId = Number(l.proplannerId);
10234
+ if (l.ganttId != null) view.ganttId = Number(l.ganttId);
10235
+ if (l.sectorId != null) view.sectorId = Number(l.sectorId);
10236
+ return view;
10237
+ }
10238
+
10093
10239
  // src/dispatch/shared/change-set.ts
10094
10240
  async function assembleChangeSet(adapter, args) {
10095
10241
  const allTouched = collectChangeSetIds(adapter, args);
@@ -10099,7 +10245,7 @@ async function assembleChangeSet(adapter, args) {
10099
10245
  const insertedIds = collectInsertedIds(captured);
10100
10246
  mergeDirtyBeforeImages(adapter, captured, insertedIds, before, allTouched);
10101
10247
  }
10102
- const sirDetection = args.preview === true ? void 0 : args.sirDetection;
10248
+ const sirDetection = args.sirDetection;
10103
10249
  const beforeDiffEffects = sirDetection === "before-diff" ? detectSirAutoReject(before, adapter, allTouched) : [];
10104
10250
  const correlativeBefore = buildCorrelativeBeforeMap(
10105
10251
  args.correlativeShifts,
@@ -10113,16 +10259,89 @@ async function assembleChangeSet(adapter, args) {
10113
10259
  );
10114
10260
  const effects = sirDetection === "after-diff" ? detectSirAutoReject(before, adapter, allTouched) : beforeDiffEffects;
10115
10261
  const warnings = args.warnings ?? [];
10262
+ const linkChanges = mergeLinkChanges(
10263
+ args.links ?? [],
10264
+ captured ? buildCapturedCriticalPathLinkChanges(adapter, captured) : []
10265
+ );
10116
10266
  return {
10117
10267
  source: args.source,
10118
10268
  activities: activityChanges,
10119
- links: args.links ?? [],
10269
+ links: linkChanges,
10120
10270
  calendars: [],
10121
10271
  trackingEvents: args.trackingEvents ?? [],
10122
10272
  ...effects.length > 0 ? { effects } : {},
10123
10273
  ...warnings.length > 0 ? { warnings } : {}
10124
10274
  };
10125
10275
  }
10276
+ function buildCapturedCriticalPathLinkChanges(adapter, captured) {
10277
+ const changes = [];
10278
+ for (const operation of captured.ops) {
10279
+ if (operation.kind !== "link-field") continue;
10280
+ const after = adapter.getLink(operation.linkId);
10281
+ if (!after) continue;
10282
+ const fields = {};
10283
+ for (const field of ["freeSlackHours", "isCritical"]) {
10284
+ if (!Object.is(operation.before[field], after[field])) {
10285
+ fields[field] = {
10286
+ before: operation.before[field],
10287
+ after: after[field]
10288
+ };
10289
+ }
10290
+ }
10291
+ if (Object.keys(fields).length === 0) continue;
10292
+ changes.push({
10293
+ id: operation.linkId,
10294
+ kind: "updated",
10295
+ fields,
10296
+ after: snapshotToLink(after)
10297
+ });
10298
+ }
10299
+ return changes;
10300
+ }
10301
+ function mergeLinkChanges(explicit, captured) {
10302
+ const merged = [];
10303
+ const indexById = /* @__PURE__ */ new Map();
10304
+ for (const change of explicit) {
10305
+ const index = indexById.get(change.id);
10306
+ if (index === void 0) {
10307
+ indexById.set(change.id, merged.length);
10308
+ merged.push(change);
10309
+ continue;
10310
+ }
10311
+ const current = merged[index];
10312
+ if ((current.kind === "updated" || current.kind === "created") && change.kind === "updated") {
10313
+ merged[index] = {
10314
+ ...current,
10315
+ fields: { ...current.fields, ...change.fields },
10316
+ after: change.after
10317
+ };
10318
+ continue;
10319
+ }
10320
+ merged[index] = change;
10321
+ }
10322
+ for (const change of captured) {
10323
+ const index = indexById.get(change.id);
10324
+ if (index === void 0) {
10325
+ indexById.set(change.id, merged.length);
10326
+ merged.push(change);
10327
+ continue;
10328
+ }
10329
+ const current = merged[index];
10330
+ if (current.kind === "deleted") continue;
10331
+ const capturedFields = current.kind === "created" ? Object.fromEntries(
10332
+ Object.entries(change.fields ?? {}).map(([field, value]) => [
10333
+ field,
10334
+ { before: void 0, after: value.after }
10335
+ ])
10336
+ ) : change.fields;
10337
+ merged[index] = {
10338
+ ...current,
10339
+ fields: { ...current.fields, ...capturedFields },
10340
+ after: change.after
10341
+ };
10342
+ }
10343
+ return merged;
10344
+ }
10126
10345
  function buildCorrelativeBeforeMap(shifts, touched) {
10127
10346
  if (!shifts || shifts.length === 0) return void 0;
10128
10347
  const correlativeBefore = /* @__PURE__ */ new Map();
@@ -10158,21 +10377,6 @@ function collectInsertedIds(captured) {
10158
10377
  );
10159
10378
  }
10160
10379
 
10161
- // src/dispatch/conversion.ts
10162
- function snapshotToLink(l) {
10163
- const view = {
10164
- id: String(l.id),
10165
- source: String(l.source),
10166
- target: String(l.target),
10167
- type: l.type,
10168
- lag: Number(l.lag)
10169
- };
10170
- if (l.proplannerId != null) view.proplannerId = Number(l.proplannerId);
10171
- if (l.ganttId != null) view.ganttId = Number(l.ganttId);
10172
- if (l.sectorId != null) view.sectorId = Number(l.sectorId);
10173
- return view;
10174
- }
10175
-
10176
10380
  // src/dispatch/shared/helpers.ts
10177
10381
  function buildLinkChangesForBatch(applied, beforeLinks, adapter) {
10178
10382
  const out = [];
@@ -10440,7 +10644,6 @@ async function dispatchInlineEdit(action, options, deps) {
10440
10644
  touchedIds,
10441
10645
  scheduledIds,
10442
10646
  sirDetection: "before-diff",
10443
- preview: options.preview,
10444
10647
  links: linkChanges,
10445
10648
  trackingEvents,
10446
10649
  ...constraintWarning ? { warnings: [constraintWarning] } : {},
@@ -10518,6 +10721,26 @@ function collectDeterministicIds(operations) {
10518
10721
  });
10519
10722
  return ids.size > 0 ? ids : void 0;
10520
10723
  }
10724
+ function resolveDeterministicIds(deterministicIds, deps) {
10725
+ if (deterministicIds === void 0) return void 0;
10726
+ const resolved = /* @__PURE__ */ new Map();
10727
+ const assigned = /* @__PURE__ */ new Set();
10728
+ const nextAvailableId = () => {
10729
+ let candidate = deps.linkIdGen.next();
10730
+ while (deps.adapter.getLink(candidate) !== null || deps.isLinkIdReserved(String(candidate)) || assigned.has(String(candidate))) {
10731
+ candidate = deps.linkIdGen.next();
10732
+ }
10733
+ return candidate;
10734
+ };
10735
+ for (const [operationIndex, explicitId] of deterministicIds) {
10736
+ const isReserved = deps.adapter.getLink(explicitId) !== null || deps.isLinkIdReserved(String(explicitId)) || assigned.has(String(explicitId));
10737
+ const linkId = isReserved ? nextAvailableId() : explicitId;
10738
+ if (!isReserved) deps.linkIdGen.bump(explicitId);
10739
+ assigned.add(String(linkId));
10740
+ resolved.set(operationIndex, linkId);
10741
+ }
10742
+ return resolved;
10743
+ }
10521
10744
  function batchOpToLinkOperation(op, hoursPerDay) {
10522
10745
  if (op.kind === "create") {
10523
10746
  return {
@@ -10550,7 +10773,7 @@ async function dispatchLinkBatch(operations, source, options, deps, deterministi
10550
10773
  operations,
10551
10774
  adapter,
10552
10775
  linkIdGen,
10553
- deterministicIds
10776
+ resolveDeterministicIds(deterministicIds, deps)
10554
10777
  );
10555
10778
  if (operations.length === 1 && applied[0] && applied[0].rejected !== null && source.kind !== "inline-edit") {
10556
10779
  return { ok: false, reason: applied[0].rejected };
@@ -10732,26 +10955,19 @@ function recomputeHhCascadeForParent(startParentId, adapter) {
10732
10955
  if (!startParentId || startParentId === "0") return;
10733
10956
  let currentParentId = String(startParentId);
10734
10957
  while (currentParentId !== null) {
10735
- const childIds = adapter.getChildren(currentParentId);
10736
- let hhSum = 0;
10737
- let rwSum = 0;
10738
- for (const childId of childIds) {
10739
- const child = adapter.getActivity(childId);
10740
- if (!child) continue;
10741
- const hh = readNumber(child, ACTIVITY_PROPERTY.WORK_HOURS);
10742
- const rw = readChildRealWork(childId, child, hh, adapter);
10743
- hhSum += hh;
10744
- rwSum += rw;
10745
- }
10958
+ const { workHours, realWorkHours } = computeParentHhRollup(
10959
+ currentParentId,
10960
+ adapter
10961
+ );
10746
10962
  adapter.setActivityField(
10747
10963
  currentParentId,
10748
10964
  ACTIVITY_PROPERTY.WORK_HOURS,
10749
- hhSum
10965
+ workHours
10750
10966
  );
10751
10967
  adapter.setActivityField(
10752
10968
  currentParentId,
10753
10969
  ACTIVITY_PROPERTY.REAL_WORK_HOURS,
10754
- rwSum
10970
+ realWorkHours
10755
10971
  );
10756
10972
  const parentSnap = adapter.getActivity(currentParentId);
10757
10973
  if (!parentSnap) break;
@@ -10767,6 +10983,40 @@ function recomputeHhCascadeForActivity(activityId, adapter) {
10767
10983
  if (!parentId || isRootParent(parentId)) return;
10768
10984
  recomputeHhCascadeForParent(parentId, adapter);
10769
10985
  }
10986
+ function recomputeRealWorkFromProgress(progressIds, parentIdsDeepestFirst, adapter) {
10987
+ for (const activityId of progressIds) {
10988
+ if (adapter.hasChildren(activityId)) continue;
10989
+ const activity = adapter.getActivity(activityId);
10990
+ if (!activity) continue;
10991
+ adapter.setActivityField(
10992
+ activityId,
10993
+ ACTIVITY_PROPERTY.REAL_WORK_HOURS,
10994
+ computeLeafRealWork(
10995
+ readNumber(activity, ACTIVITY_PROPERTY.WORK_HOURS),
10996
+ readNumber(activity, ACTIVITY_PROPERTY.PROGRESS)
10997
+ )
10998
+ );
10999
+ }
11000
+ for (const parentId of parentIdsDeepestFirst) {
11001
+ adapter.setActivityField(
11002
+ parentId,
11003
+ ACTIVITY_PROPERTY.REAL_WORK_HOURS,
11004
+ computeParentHhRollup(parentId, adapter).realWorkHours
11005
+ );
11006
+ }
11007
+ }
11008
+ function computeParentHhRollup(parentId, adapter) {
11009
+ let workHours = 0;
11010
+ let realWorkHours = 0;
11011
+ for (const childId of adapter.getChildren(parentId)) {
11012
+ const child = adapter.getActivity(childId);
11013
+ if (!child) continue;
11014
+ const childWorkHours = readNumber(child, ACTIVITY_PROPERTY.WORK_HOURS);
11015
+ workHours += childWorkHours;
11016
+ realWorkHours += readChildRealWork(childId, child, childWorkHours, adapter);
11017
+ }
11018
+ return { workHours, realWorkHours };
11019
+ }
10770
11020
  function readNumber(activity, key) {
10771
11021
  const v = activity[key];
10772
11022
  if (typeof v === "number" && Number.isFinite(v)) return v;
@@ -11139,6 +11389,36 @@ function computeFractionalCidAtIndex(adapter, parentKey, targetIndex, excludeId)
11139
11389
  return (before + after) / 2;
11140
11390
  }
11141
11391
 
11392
+ // src/dispatch/creation-overrides.ts
11393
+ var PUBLIC_CREATION_OVERRIDE_FIELDS = /* @__PURE__ */ new Set([
11394
+ ...[...CREATION_OVERRIDE_FIELDS].filter((field) => field !== "durationHours"),
11395
+ "durationDays"
11396
+ ]);
11397
+ function normalizeActivityCreationOverrides(raw, hoursPerDay) {
11398
+ if (raw === void 0) return { ok: true, value: {} };
11399
+ const prototype = Object.getPrototypeOf(raw);
11400
+ if (prototype !== null && prototype !== Object.prototype) {
11401
+ return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
11402
+ }
11403
+ for (const key of Reflect.ownKeys(raw)) {
11404
+ if (typeof key === "symbol" || !PUBLIC_CREATION_OVERRIDE_FIELDS.has(key)) {
11405
+ return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
11406
+ }
11407
+ }
11408
+ const { durationDays, ...rest } = raw;
11409
+ if (durationDays !== void 0 && (!Number.isFinite(durationDays) || durationDays < 0)) {
11410
+ return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
11411
+ }
11412
+ return {
11413
+ ok: true,
11414
+ value: {
11415
+ ...rest,
11416
+ ...durationDays === 0 && (rest.type === void 0 || rest.type === "task") ? { type: "milestone" } : {},
11417
+ ...durationDays === void 0 ? {} : { durationHours: durationToHours(durationDays, hoursPerDay) }
11418
+ }
11419
+ };
11420
+ }
11421
+
11142
11422
  // src/dispatch/handlers/activity-create.ts
11143
11423
  async function createActivityCore(action, deps, opts = {}) {
11144
11424
  const { adapter, scheduler, sector, calendars, activityIdGen, uidGen } = deps;
@@ -11146,10 +11426,15 @@ async function createActivityCore(action, deps, opts = {}) {
11146
11426
  const target = validateCreateTarget(action, adapter);
11147
11427
  if (!target.ok) return target;
11148
11428
  const { parent, anchorSiblingId, isRoot } = target;
11149
- const newId = resolveNewActivityId(action.activityId, adapter, activityIdGen);
11150
- const newUid = uidGen.next();
11151
- const activityInput = {
11152
- id: newId,
11429
+ const newId = resolveNewActivityId(
11430
+ action.activityId,
11431
+ adapter,
11432
+ activityIdGen,
11433
+ deps.isActivityIdReserved
11434
+ );
11435
+ const newUid = uidGen.next();
11436
+ const activityInput = {
11437
+ id: newId,
11153
11438
  parentId: isRoot ? null : action.parentId,
11154
11439
  sector: {
11155
11440
  id: sector.id,
@@ -11239,9 +11524,18 @@ function validateAnchorSibling(action, adapter, isRoot, anchorSiblingId) {
11239
11524
  }
11240
11525
  return null;
11241
11526
  }
11242
- function resolveNewActivityId(explicitId, adapter, activityIdGen) {
11243
- if (explicitId === void 0) return activityIdGen.next();
11244
- if (adapter.activityExists(explicitId)) return activityIdGen.next();
11527
+ function resolveNewActivityId(explicitId, adapter, activityIdGen, isActivityIdReserved) {
11528
+ const nextAvailableId = () => {
11529
+ let candidate = activityIdGen.next();
11530
+ while (adapter.activityExists(candidate) || isActivityIdReserved(String(candidate))) {
11531
+ candidate = activityIdGen.next();
11532
+ }
11533
+ return candidate;
11534
+ };
11535
+ if (explicitId === void 0) return nextAvailableId();
11536
+ if (adapter.activityExists(explicitId) || isActivityIdReserved(String(explicitId))) {
11537
+ return nextAvailableId();
11538
+ }
11245
11539
  activityIdGen.bump(explicitId);
11246
11540
  return explicitId;
11247
11541
  }
@@ -11320,7 +11614,15 @@ async function buildCreateChangeSet(adapter, action, coreResult, scheduledIds, h
11320
11614
  }
11321
11615
  async function dispatchActivityCreate(action, options, deps) {
11322
11616
  const { adapter, scheduler, sector } = deps;
11323
- const core = await createActivityCore(action, deps);
11617
+ const normalized2 = normalizeActivityCreationOverrides(
11618
+ action.overrides,
11619
+ sector.hoursPerDay
11620
+ );
11621
+ if (!normalized2.ok) return normalized2;
11622
+ const core = await createActivityCore(
11623
+ { ...action, overrides: normalized2.value },
11624
+ deps
11625
+ );
11324
11626
  if (!core.ok) return core;
11325
11627
  const { newId } = core;
11326
11628
  const createdAutoScheduling = core.newActivity.autoScheduling;
@@ -11532,7 +11834,7 @@ async function dispatchActivityDelete(action, options, deps) {
11532
11834
  for (const linkId of incidentLinkIds) {
11533
11835
  adapter.removeLink(linkId);
11534
11836
  }
11535
- const viewStateChanges = captureDeletedViewState(adapter, toDelete);
11837
+ const viewStateChanges2 = captureDeletedViewState(adapter, toDelete);
11536
11838
  let removed = 0;
11537
11839
  for (const id of toDelete) {
11538
11840
  removed += 1;
@@ -11614,7 +11916,6 @@ async function dispatchActivityDelete(action, options, deps) {
11614
11916
  touchedIds,
11615
11917
  scheduledIds,
11616
11918
  sirDetection: "after-diff",
11617
- preview: options.preview,
11618
11919
  links: buildLinkDeletions(beforeLinks),
11619
11920
  trackingEvents: [trackingEvent],
11620
11921
  hoursPerDay: sector.hoursPerDay,
@@ -11622,7 +11923,7 @@ async function dispatchActivityDelete(action, options, deps) {
11622
11923
  });
11623
11924
  return {
11624
11925
  ok: true,
11625
- changes: viewStateChanges.length > 0 ? { ...changes, viewState: viewStateChanges } : changes,
11926
+ changes: viewStateChanges2.length > 0 ? { ...changes, viewState: viewStateChanges2 } : changes,
11626
11927
  __beforeSnap: deletedRowsSnap,
11627
11928
  __beforeLinks: beforeLinksByStringId
11628
11929
  };
@@ -11730,7 +12031,7 @@ async function dispatchActivityBatch(action, options, deps) {
11730
12031
  source: link.source,
11731
12032
  target: link.target,
11732
12033
  type: link.type,
11733
- lag: options.inputUnit === "hours" ? link.lag : lagDaysToHours(link.lag, deps.sector.hoursPerDay)
12034
+ lag: lagDaysToHours(link.lag, deps.sector.hoursPerDay)
11734
12035
  },
11735
12036
  linkId: link.linkId
11736
12037
  }));
@@ -11775,7 +12076,6 @@ async function dispatchActivityBatch(action, options, deps) {
11775
12076
  links: buildLinkChangesForBatch(appliedLinks, beforeLinks, deps.adapter),
11776
12077
  hoursPerDay: deps.sector.hoursPerDay,
11777
12078
  correlativeShifts: existingCorrelativeShifts,
11778
- preview: options.preview,
11779
12079
  ...action.mode === "replace" ? { sirDetection: "after-diff" } : {}
11780
12080
  });
11781
12081
  return {
@@ -11803,7 +12103,7 @@ function validateBatch(action, deps) {
11803
12103
  }
11804
12104
  if (item.activityId !== void 0) {
11805
12105
  const activityId = String(item.activityId);
11806
- if (activityId.length === 0 || activityId === "0" || explicitActivityIds.has(activityId) || action.mode === "append" && deps.adapter.getActivity(activityId) !== null) {
12106
+ if (activityId.length === 0 || activityId === "0" || explicitActivityIds.has(activityId)) {
11807
12107
  return REJECTION_REASON.INVALID;
11808
12108
  }
11809
12109
  explicitActivityIds.add(activityId);
@@ -11825,7 +12125,7 @@ function validateBatch(action, deps) {
11825
12125
  }
11826
12126
  if (link.linkId !== void 0) {
11827
12127
  const linkId = String(link.linkId);
11828
- if (linkId.length === 0 || explicitLinkIds.has(linkId) || action.mode === "append" && deps.adapter.getLink(linkId) !== null) {
12128
+ if (linkId.length === 0 || explicitLinkIds.has(linkId)) {
11829
12129
  return REJECTION_REASON.INVALID;
11830
12130
  }
11831
12131
  explicitLinkIds.add(linkId);
@@ -11861,24 +12161,38 @@ function hasParentCycle(items) {
11861
12161
  return false;
11862
12162
  }
11863
12163
  function buildBatchPlan(action, deps) {
12164
+ const normalizedOverrides = /* @__PURE__ */ new Map();
11864
12165
  for (const item of action.items) {
11865
- if (item.activityId !== void 0) {
11866
- deps.activityIdGen.bump(item.activityId);
11867
- }
12166
+ const normalized2 = normalizeActivityCreationOverrides(
12167
+ item.overrides,
12168
+ deps.sector.hoursPerDay
12169
+ );
12170
+ if (!normalized2.ok) return normalized2;
12171
+ normalizedOverrides.set(item.batchId, normalized2.value);
11868
12172
  }
11869
12173
  const activityIdsByBatchId = /* @__PURE__ */ new Map();
12174
+ const plannedActivityIds = /* @__PURE__ */ new Set();
11870
12175
  for (const item of action.items) {
11871
- activityIdsByBatchId.set(
11872
- item.batchId,
11873
- item.activityId ?? deps.activityIdGen.next()
12176
+ const activityId = resolvePlannedActivityId(
12177
+ item.activityId,
12178
+ action.mode,
12179
+ plannedActivityIds,
12180
+ deps
11874
12181
  );
11875
- }
11876
- for (const link of action.links) {
11877
- if (link.linkId !== void 0) deps.linkIdGen.bump(link.linkId);
11878
- }
11879
- const linkIds = action.links.map(
11880
- (link) => link.linkId ?? deps.linkIdGen.next()
11881
- );
12182
+ plannedActivityIds.add(String(activityId));
12183
+ activityIdsByBatchId.set(item.batchId, activityId);
12184
+ }
12185
+ const plannedLinkIds = /* @__PURE__ */ new Set();
12186
+ const linkIds = action.links.map((link) => {
12187
+ const linkId = resolvePlannedLinkId(
12188
+ link.linkId,
12189
+ action.mode,
12190
+ plannedLinkIds,
12191
+ deps
12192
+ );
12193
+ plannedLinkIds.add(String(linkId));
12194
+ return linkId;
12195
+ });
11882
12196
  const insertionCorrelativeIds = planInsertionCorrelatives(
11883
12197
  action,
11884
12198
  activityIdsByBatchId,
@@ -11901,7 +12215,8 @@ function buildBatchPlan(action, deps) {
11901
12215
  activityId,
11902
12216
  parentId,
11903
12217
  parentBatchId,
11904
- insertionCorrelativeId
12218
+ insertionCorrelativeId,
12219
+ overrides: normalizedOverrides.get(item.batchId) ?? {}
11905
12220
  });
11906
12221
  }
11907
12222
  const links = [];
@@ -11922,6 +12237,30 @@ function buildBatchPlan(action, deps) {
11922
12237
  }
11923
12238
  return { ok: true, plan: { items, links } };
11924
12239
  }
12240
+ function resolvePlannedActivityId(explicitId, mode, plannedIds, deps) {
12241
+ const explicitIsAvailable = explicitId !== void 0 && (mode === "replace" || deps.adapter.getActivity(explicitId) === null && !deps.isActivityIdReserved(String(explicitId))) && !plannedIds.has(String(explicitId));
12242
+ if (explicitIsAvailable) {
12243
+ deps.activityIdGen.bump(explicitId);
12244
+ return explicitId;
12245
+ }
12246
+ let candidate = deps.activityIdGen.next();
12247
+ while (plannedIds.has(String(candidate)) || mode === "append" && (deps.adapter.getActivity(candidate) !== null || deps.isActivityIdReserved(String(candidate)))) {
12248
+ candidate = deps.activityIdGen.next();
12249
+ }
12250
+ return candidate;
12251
+ }
12252
+ function resolvePlannedLinkId(explicitId, mode, plannedIds, deps) {
12253
+ const explicitIsAvailable = explicitId !== void 0 && (mode === "replace" || deps.adapter.getLink(explicitId) === null && !deps.isLinkIdReserved(String(explicitId))) && !plannedIds.has(String(explicitId));
12254
+ if (explicitIsAvailable) {
12255
+ deps.linkIdGen.bump(explicitId);
12256
+ return explicitId;
12257
+ }
12258
+ let candidate = deps.linkIdGen.next();
12259
+ while (plannedIds.has(String(candidate)) || mode === "append" && (deps.adapter.getLink(candidate) !== null || deps.isLinkIdReserved(String(candidate)))) {
12260
+ candidate = deps.linkIdGen.next();
12261
+ }
12262
+ return candidate;
12263
+ }
11925
12264
  function planInsertionCorrelatives(action, activityIdsByBatchId, deps) {
11926
12265
  if (isSimpleRootAppend(action)) {
11927
12266
  const firstCorrelativeId = deps.adapter.activityCount();
@@ -12031,7 +12370,7 @@ async function createPlannedActivities(action, plannedItems, deps) {
12031
12370
  {
12032
12371
  parentId: item.parentId,
12033
12372
  activityId: item.activityId,
12034
- ...item.input.overrides === void 0 ? {} : { overrides: item.input.overrides },
12373
+ overrides: item.overrides,
12035
12374
  ...action.eventSource === void 0 ? {} : { eventSource: action.eventSource }
12036
12375
  },
12037
12376
  deps,
@@ -12108,301 +12447,280 @@ function dispatchSirSync(action, deps) {
12108
12447
  };
12109
12448
  }
12110
12449
 
12111
- // src/dispatch/handlers/bulk-edit.ts
12112
- var CUSTOM_ID_COLUMN = COLUMN.CUSTOM_ID;
12113
- async function dispatchBulkEdit(action, options, deps) {
12114
- const { adapter, scheduler, sector } = deps;
12115
- const editsIncludeDuration = action.edits.some(
12116
- (edit) => edit.column === COLUMN.DURATION
12450
+ // src/propagations/upward/progress-rollup.ts
12451
+ function recomputeAllProgressRollup(adapter) {
12452
+ for (const parentId of parentsDeepestFirst(adapter)) {
12453
+ rollupParent(parentId, adapter);
12454
+ }
12455
+ }
12456
+ function recomputeProgressRollupForParents(parentIdsDeepestFirst, adapter) {
12457
+ for (const parentId of parentIdsDeepestFirst) {
12458
+ rollupParent(parentId, adapter);
12459
+ }
12460
+ }
12461
+ function parentsDeepestFirst(adapter) {
12462
+ const parents = /* @__PURE__ */ new Set();
12463
+ adapter.forEachActivity((_activity, id) => {
12464
+ if (adapter.getChildren(id).length > 0) parents.add(id);
12465
+ });
12466
+ const depthById = /* @__PURE__ */ new Map();
12467
+ for (const id of parents) depthById.set(id, depthOf2(id, adapter));
12468
+ return [...parents].sort(
12469
+ (a, b) => (depthById.get(b) ?? 0) - (depthById.get(a) ?? 0)
12117
12470
  );
12118
- if (editsIncludeDuration) {
12119
- scheduler.invalidateExpandedLinksCache();
12471
+ }
12472
+ function depthOf2(id, adapter) {
12473
+ let depth = 0;
12474
+ let current = id;
12475
+ while (current && current !== "0") {
12476
+ if (!adapter.getActivity(current)) break;
12477
+ depth += 1;
12478
+ current = adapter.getParentId(current);
12120
12479
  }
12121
- const verdicts = [];
12122
- const beforeSnap = /* @__PURE__ */ new Map();
12123
- const constraintPriorsByActivity = /* @__PURE__ */ new Map();
12124
- const explicitDateEditActivities = /* @__PURE__ */ new Set();
12125
- const touchedIds = /* @__PURE__ */ new Set();
12126
- const trackingEvents = [];
12127
- const linkChanges = [];
12128
- const warnings = [];
12129
- let needsAutoSchedule = false;
12130
- for (const edit of action.edits) {
12131
- const outcome = applyOneEdit(edit, deps);
12132
- verdicts.push(outcome.verdict);
12133
- if (!outcome.verdict.ok || !outcome.changes) continue;
12134
- const constraintWarning = toConstraintWarning(
12135
- edit.activityId,
12136
- outcome.changes.constraintWarning
12137
- );
12138
- if (constraintWarning) warnings.push(constraintWarning);
12139
- const editTouched = collectTouchedIds(edit.activityId, outcome.changes);
12140
- mergeBeforeSnapshots(beforeSnap, editTouched, deps);
12141
- for (const touchedId of editTouched) touchedIds.add(touchedId);
12142
- recordConstraintPrior(constraintPriorsByActivity, edit, beforeSnap);
12143
- if (isExplicitConstraintDateEdit(edit.column)) {
12144
- explicitDateEditActivities.add(String(edit.activityId));
12145
- }
12146
- const beforeLinkLags = snapshotIncomingLinkLags(adapter, edit.activityId);
12147
- const preEditSnapshot = snapshotSingleActivity(
12148
- adapter,
12149
- String(edit.activityId)
12150
- );
12151
- const customIdBeforeApply = readLiveCustomId(edit, deps);
12152
- const changes = outcome.changes;
12153
- applyFieldChanges(adapter, edit.activityId, changes);
12154
- runPostProcessorsOnAdapter(
12155
- edit.activityId,
12156
- outcome.changes.postProcessors ?? [],
12157
- adapter,
12158
- preEditSnapshot
12159
- );
12160
- syncCustomIdTracker(edit, customIdBeforeApply, deps);
12161
- linkChanges.push(
12162
- ...diffIncomingLinkLagChanges(beforeLinkLags, adapter, edit.activityId)
12480
+ return depth;
12481
+ }
12482
+ function rollupParent(parentId, adapter) {
12483
+ const childIds = adapter.getChildren(parentId);
12484
+ if (childIds.length === 0) return;
12485
+ const children = [];
12486
+ for (const childId of childIds) {
12487
+ const child = adapter.getActivity(childId);
12488
+ if (!child) continue;
12489
+ children.push({
12490
+ progress: Number(child.progress ?? 0),
12491
+ ponderator: Number(child.ponderator ?? 0)
12492
+ });
12493
+ }
12494
+ const rollup = computeWeightedProgressRollup(children);
12495
+ if (rollup !== null) {
12496
+ adapter.setActivityField(
12497
+ parentId,
12498
+ COLUMN.PROGRESS,
12499
+ roundProgressPerLevel(rollup)
12163
12500
  );
12164
- needsAutoSchedule ||= Boolean(outcome.changes.needsAutoSchedule);
12165
- for (const trackingEvent of outcome.changes.trackingEvents ?? []) {
12166
- trackingEvents.push({
12167
- name: trackingEvent.name,
12168
- properties: trackingEvent.properties ?? {}
12169
- });
12170
- }
12171
12501
  }
12172
- const postMutationArgs = {
12173
- action,
12174
- autoscheduleFrom: needsAutoSchedule ? "roots" : null,
12175
- recomputeParentsFrom: touchedIds,
12176
- now: deps.now,
12177
- options
12178
- };
12179
- const batchHasConstraintEdits = constraintPriorsByActivity.size > 0;
12180
- const { scheduledIds } = await (batchHasConstraintEdits ? settleConstraintEdits(
12181
- {
12182
- adapter,
12183
- scheduler,
12184
- sector,
12185
- defaultBaseCalendarId: deps.defaultBaseCalendarId
12186
- },
12187
- {
12188
- postMutationArgs,
12189
- revertTargets: [...constraintPriorsByActivity].filter(
12190
- ([activityId]) => !explicitDateEditActivities.has(activityId)
12191
- ).map(([activityId, priorConstraintDate]) => ({
12192
- activityId,
12193
- priorConstraintDate
12194
- })),
12195
- revert: (activityId, priorConstraintDate) => revertNoOpConstraintEdit(
12196
- activityId,
12197
- priorConstraintDate,
12198
- adapter,
12199
- deps.ctx.calendars
12200
- )
12502
+ }
12503
+
12504
+ // src/dispatch/handlers/persistence-acknowledge.ts
12505
+ function persistedOwnersOf(entities) {
12506
+ const owners = /* @__PURE__ */ new Map();
12507
+ for (const entity of entities) {
12508
+ const proplannerId = entity.proplannerId;
12509
+ if (typeof proplannerId === "number" && Number.isSafeInteger(proplannerId) && proplannerId > 0) {
12510
+ owners.set(proplannerId, String(entity.id));
12201
12511
  }
12202
- ) : runPostMutation(
12203
- {
12204
- adapter,
12205
- scheduler,
12206
- sector,
12207
- defaultBaseCalendarId: deps.defaultBaseCalendarId
12208
- },
12209
- postMutationArgs
12210
- ));
12211
- return {
12212
- ok: true,
12213
- verdicts,
12214
- changes: await assembleChangeSet(adapter, {
12215
- source: action,
12216
- beforeSnap,
12217
- touchedIds,
12218
- scheduledIds,
12219
- sirDetection: "before-diff",
12220
- preview: options.preview,
12221
- links: linkChanges,
12222
- trackingEvents,
12223
- warnings,
12224
- hoursPerDay: sector.hoursPerDay
12225
- })
12226
- };
12512
+ }
12513
+ return owners;
12227
12514
  }
12228
- function applyOneEdit(edit, deps) {
12229
- const { ctx } = deps;
12230
- const activityId = edit.activityId;
12231
- const pipeline = findPipelineForColumn(edit.column);
12232
- if (!pipeline) {
12233
- return {
12234
- verdict: {
12235
- activityId,
12236
- ok: false,
12237
- reason: REJECTION_REASON.NO_PIPELINE_FOR_COLUMN
12238
- },
12239
- changes: null
12240
- };
12515
+ function assertAllNewEntitiesAcknowledged(entities, assignments, entityName) {
12516
+ const acknowledgedIds = new Set(assignments.map(({ id }) => String(id)));
12517
+ for (const entity of entities) {
12518
+ if ((entity.proplannerId == null || entity.proplannerId === 0) && !acknowledgedIds.has(String(entity.id))) {
12519
+ throw new Error(
12520
+ `persistence-acknowledge: missing ${entityName} proplannerId for "${String(entity.id)}"`
12521
+ );
12522
+ }
12241
12523
  }
12242
- const activitySnap = ctx.activityReader.getActivity(edit.activityId);
12243
- if (!activitySnap) {
12244
- return {
12245
- verdict: {
12246
- activityId,
12247
- ok: false,
12248
- reason: REJECTION_REASON.ACTIVITY_NOT_FOUND
12249
- },
12250
- changes: null
12251
- };
12524
+ }
12525
+ function assertValidAssignment(assignment, entityName, readCurrent, ownerByBackendId, seenIds, seenBackendIds) {
12526
+ const id = String(assignment.id);
12527
+ if (id.length === 0 || seenIds.has(id)) {
12528
+ throw new Error(
12529
+ `persistence-acknowledge: duplicate or empty ${entityName} id "${id}"`
12530
+ );
12252
12531
  }
12253
- const editGate = pipeline.canEdit(activitySnap, ctx.hierarchy);
12254
- if (!editGate.allowed) {
12255
- return {
12256
- verdict: {
12257
- activityId,
12258
- ok: false,
12259
- reason: editGate.reason ?? REJECTION_REASON.CANNOT_EDIT,
12260
- ...editGate.alertKey ? { alertKey: editGate.alertKey } : {}
12261
- },
12262
- changes: null
12263
- };
12532
+ if (!Number.isSafeInteger(assignment.proplannerId) || assignment.proplannerId <= 0) {
12533
+ throw new Error(
12534
+ `persistence-acknowledge: invalid ${entityName} proplannerId ${String(assignment.proplannerId)}`
12535
+ );
12264
12536
  }
12265
- const parsed2 = pipeline.parseInput(edit.newValue, activitySnap, ctx);
12266
- if (!parsed2.ok) {
12267
- return {
12268
- verdict: {
12269
- activityId,
12270
- ok: false,
12271
- reason: parsed2.reason ?? REJECTION_REASON.PARSE_ERROR
12272
- },
12273
- changes: null
12274
- };
12537
+ if (seenBackendIds.has(assignment.proplannerId)) {
12538
+ throw new Error(
12539
+ `persistence-acknowledge: duplicate ${entityName} proplannerId ${String(assignment.proplannerId)}`
12540
+ );
12275
12541
  }
12276
- const oldValue = Reflect.get(activitySnap, pipeline.targetField);
12277
- const validation = pipeline.validate(
12278
- activitySnap,
12279
- oldValue,
12280
- parsed2.value,
12281
- ctx
12282
- );
12283
- if (!validation.valid) {
12284
- return {
12285
- verdict: {
12286
- activityId,
12287
- ok: false,
12288
- reason: validation.reason ?? REJECTION_REASON.INVALID,
12289
- ...validation.notification?.messageKey ? { alertKey: validation.notification.messageKey } : {}
12290
- },
12291
- changes: null
12292
- };
12542
+ const state = readCurrent(id);
12543
+ if (!state.exists) {
12544
+ throw new Error(
12545
+ `persistence-acknowledge: unknown ${entityName} id "${id}"`
12546
+ );
12293
12547
  }
12294
- if (edit.column === CUSTOM_ID_COLUMN && parsed2.value !== null) {
12295
- const liveActivity = deps.adapter.getActivity(edit.activityId);
12296
- const oldCustomIdRaw = liveActivity ? liveActivity.customId : null;
12297
- const oldCustomId = typeof oldCustomIdRaw === "string" ? oldCustomIdRaw : null;
12298
- if (deps.customIdTracker.isCustomIdInUse(String(parsed2.value), oldCustomId)) {
12299
- return {
12300
- verdict: {
12301
- activityId,
12302
- ok: false,
12303
- reason: REJECTION_REASON.CUSTOM_ID_DUPLICATE
12304
- },
12305
- changes: null
12306
- };
12307
- }
12548
+ if (state.current !== null && state.current !== 0 && state.current !== assignment.proplannerId) {
12549
+ throw new Error(
12550
+ `persistence-acknowledge: ${entityName} "${id}" already has proplannerId ${String(state.current)}`
12551
+ );
12308
12552
  }
12309
- const changes = pipeline.transform(
12310
- activitySnap,
12311
- parsed2.value,
12312
- ctx,
12313
- parsed2.raw
12314
- );
12315
- return { verdict: { activityId, ok: true }, changes };
12316
- }
12317
- function readLiveCustomId(edit, deps) {
12318
- if (edit.column !== CUSTOM_ID_COLUMN) return null;
12319
- const liveActivity = deps.adapter.getActivity(edit.activityId);
12320
- const rawValue = liveActivity ? liveActivity.customId : null;
12321
- return typeof rawValue === "string" ? rawValue : null;
12322
- }
12323
- function syncCustomIdTracker(edit, oldCustomId, deps) {
12324
- if (edit.column !== CUSTOM_ID_COLUMN) return;
12325
- const liveActivity = deps.adapter.getActivity(edit.activityId);
12326
- const rawValue = liveActivity ? liveActivity.customId : null;
12327
- const newCustomId = typeof rawValue === "string" ? rawValue : null;
12328
- if (oldCustomId !== newCustomId) {
12329
- deps.customIdTracker.trackCustomIdChange(oldCustomId, newCustomId);
12553
+ const existingOwner = ownerByBackendId.get(assignment.proplannerId);
12554
+ if (existingOwner !== void 0 && existingOwner !== id) {
12555
+ throw new Error(
12556
+ `persistence-acknowledge: ${entityName} proplannerId ${String(assignment.proplannerId)} already belongs to "${existingOwner}"`
12557
+ );
12330
12558
  }
12559
+ seenIds.add(id);
12560
+ seenBackendIds.add(assignment.proplannerId);
12331
12561
  }
12332
- function recordConstraintPrior(priors, edit, beforeSnap) {
12333
- if (!isConstraintEditColumn(edit.column)) return;
12334
- const activityKey = String(edit.activityId);
12335
- if (priors.has(activityKey)) return;
12336
- const preBatchSnapshot = beforeSnap.get(activityKey);
12337
- if (!preBatchSnapshot) return;
12338
- priors.set(activityKey, preBatchSnapshot.constraintDate ?? null);
12339
- }
12340
- function mergeBeforeSnapshots(beforeSnap, ids, deps) {
12341
- const missing = /* @__PURE__ */ new Set();
12342
- for (const candidateId of ids) {
12343
- if (!beforeSnap.has(candidateId)) missing.add(candidateId);
12562
+ function assertValidAssignments(assignments, entityName, readCurrent, ownerByBackendId) {
12563
+ const seenIds = /* @__PURE__ */ new Set();
12564
+ const seenBackendIds = /* @__PURE__ */ new Set();
12565
+ for (const assignment of assignments) {
12566
+ assertValidAssignment(
12567
+ assignment,
12568
+ entityName,
12569
+ readCurrent,
12570
+ ownerByBackendId,
12571
+ seenIds,
12572
+ seenBackendIds
12573
+ );
12344
12574
  }
12345
- if (missing.size === 0) return;
12346
- for (const missingId of missing) {
12347
- const liveActivity = deps.adapter.getActivity(missingId);
12348
- if (liveActivity) {
12349
- beforeSnap.set(missingId, structuredCloneActivity(liveActivity));
12575
+ }
12576
+ function assertValidReconciledProgress(entries, deps) {
12577
+ const seen = /* @__PURE__ */ new Set();
12578
+ for (const entry of entries) {
12579
+ const id = String(entry.activityId);
12580
+ if (id.length === 0 || seen.has(id)) {
12581
+ throw new Error(
12582
+ `persistence-acknowledge: duplicate or empty reconciled activity id "${id}"`
12583
+ );
12584
+ }
12585
+ if (!Number.isFinite(entry.progress) || entry.progress < 0 || entry.progress > 100) {
12586
+ throw new Error(
12587
+ `persistence-acknowledge: invalid reconciled progress ${String(entry.progress)} for "${id}"`
12588
+ );
12589
+ }
12590
+ if (!deps.adapter.getActivity(id)) {
12591
+ throw new Error(
12592
+ `persistence-acknowledge: unknown reconciled activity id "${id}"`
12593
+ );
12350
12594
  }
12595
+ seen.add(id);
12351
12596
  }
12352
12597
  }
12353
-
12354
- // src/dispatch/handlers/sir-approve.ts
12355
- async function dispatchSirApprove(action, options, deps) {
12356
- const { adapter } = deps;
12357
- const snapshot = adapter.getActivity(action.activityId);
12358
- if (!snapshot) {
12359
- return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
12360
- }
12361
- const pending = snapshot.pendingRequestIds ?? [];
12362
- if (!pending.includes(action.approvedRequestId)) {
12363
- return { ok: false, reason: REJECTION_REASON.SIR_REQUEST_NOT_PENDING };
12598
+ async function applyReconciledProgress(entries, deps) {
12599
+ const progressToApply = entries.filter((entry) => {
12600
+ const current = deps.adapter.getActivity(String(entry.activityId));
12601
+ return current !== null && current.progress !== entry.progress;
12602
+ });
12603
+ const progressIds = new Set(
12604
+ progressToApply.map((entry) => String(entry.activityId))
12605
+ );
12606
+ const ancestors = findAncestorsOfDirty(deps.adapter, progressIds);
12607
+ const touched = new Set(progressIds);
12608
+ for (const parentId of ancestors) {
12609
+ touched.add(String(parentId));
12364
12610
  }
12365
- const remaining = pending.filter((id) => id !== action.approvedRequestId);
12366
- adapter.setActivityField(action.activityId, "pendingRequestIds", remaining);
12367
- const result = await dispatchBulkEdit(
12368
- {
12369
- kind: "bulk-edit",
12370
- edits: [
12371
- {
12372
- activityId: action.activityId,
12373
- column: COLUMN.START_DATE,
12374
- newValue: action.startDate
12375
- },
12376
- {
12377
- activityId: action.activityId,
12378
- column: COLUMN.END_DATE,
12379
- newValue: action.endDate
12380
- },
12381
- // asap ÚLTIMO: gana al SNET implícito (paridad legacy).
12382
- {
12383
- activityId: action.activityId,
12384
- column: COLUMN.CONSTRAINT_TYPE,
12385
- newValue: "asap"
12386
- }
12387
- ],
12388
- eventSource: action.eventSource
12611
+ const before = await snapshotActivities(deps.adapter, touched);
12612
+ deps.adapter.batchUpdate(() => {
12613
+ for (const entry of progressToApply) {
12614
+ deps.adapter.setActivityField(
12615
+ String(entry.activityId),
12616
+ "progress",
12617
+ entry.progress
12618
+ );
12619
+ }
12620
+ recomputeProgressRollupForParents(ancestors, deps.adapter);
12621
+ recomputeRealWorkFromProgress(progressIds, ancestors, deps.adapter);
12622
+ });
12623
+ return [...await buildActivityChanges(deps.adapter, before, touched)];
12624
+ }
12625
+ async function dispatchPersistenceAcknowledge(action, deps) {
12626
+ const activities = action.activities ?? [];
12627
+ const links = action.links ?? [];
12628
+ const reconciledProgress = action.reconciledProgress ?? [];
12629
+ assertValidReconciledProgress(reconciledProgress, deps);
12630
+ const currentActivities = deps.adapter.getAllActivities();
12631
+ const currentLinks = deps.adapter.getAllLinks();
12632
+ const activityOwnerByBackendId = persistedOwnersOf(currentActivities);
12633
+ const linkOwnerByBackendId = persistedOwnersOf(currentLinks);
12634
+ assertValidAssignments(
12635
+ activities,
12636
+ "activity",
12637
+ (id) => {
12638
+ const activity = deps.adapter.getActivity(id);
12639
+ return {
12640
+ exists: activity !== null,
12641
+ current: activity?.proplannerId ?? null
12642
+ };
12389
12643
  },
12390
- options,
12644
+ activityOwnerByBackendId
12645
+ );
12646
+ assertValidAssignments(
12647
+ links,
12648
+ "link",
12649
+ (id) => {
12650
+ const link = deps.adapter.getLink(id);
12651
+ return { exists: link !== null, current: link?.proplannerId ?? null };
12652
+ },
12653
+ linkOwnerByBackendId
12654
+ );
12655
+ assertAllNewEntitiesAcknowledged(currentActivities, activities, "activity");
12656
+ assertAllNewEntitiesAcknowledged(currentLinks, links, "link");
12657
+ const activityChanges = await applyReconciledProgress(
12658
+ reconciledProgress,
12391
12659
  deps
12392
12660
  );
12393
- if (!result.ok) return result;
12394
- const hardFailure = "verdicts" in result ? result.verdicts.find(
12395
- (verdict) => !verdict.ok && verdict.reason !== VALIDATION_REASON.UNCHANGED
12396
- ) : void 0;
12397
- if (hardFailure) {
12398
- return { ok: false, reason: hardFailure.reason };
12399
- }
12400
- return { ...result, changes: { ...result.changes, source: action } };
12661
+ const linkChanges = [];
12662
+ deps.adapter.batchUpdate(() => {
12663
+ for (const assignment of activities) {
12664
+ const before = deps.adapter.getActivity(assignment.id);
12665
+ if (!before || before.proplannerId === assignment.proplannerId) continue;
12666
+ const previousProplannerId = before.proplannerId;
12667
+ deps.adapter.setActivityField(
12668
+ assignment.id,
12669
+ "proplannerId",
12670
+ assignment.proplannerId
12671
+ );
12672
+ activityChanges.push({
12673
+ id: assignment.id,
12674
+ kind: "updated",
12675
+ fields: {
12676
+ proplannerId: {
12677
+ before: previousProplannerId,
12678
+ after: assignment.proplannerId
12679
+ }
12680
+ },
12681
+ after: { ...before, proplannerId: assignment.proplannerId }
12682
+ });
12683
+ }
12684
+ for (const assignment of links) {
12685
+ const before = deps.adapter.getLink(assignment.id);
12686
+ if (!before || before.proplannerId === assignment.proplannerId) continue;
12687
+ const previousProplannerId = before.proplannerId;
12688
+ deps.adapter.setLinkProplannerId(assignment.id, assignment.proplannerId);
12689
+ linkChanges.push({
12690
+ id: assignment.id,
12691
+ kind: "updated",
12692
+ fields: {
12693
+ proplannerId: {
12694
+ before: previousProplannerId,
12695
+ after: assignment.proplannerId
12696
+ }
12697
+ },
12698
+ after: { ...before, proplannerId: assignment.proplannerId }
12699
+ });
12700
+ }
12701
+ });
12702
+ return {
12703
+ ok: true,
12704
+ changes: {
12705
+ source: action,
12706
+ activities: activityChanges,
12707
+ links: linkChanges,
12708
+ calendars: [],
12709
+ trackingEvents: []
12710
+ }
12711
+ };
12401
12712
  }
12402
12713
 
12403
12714
  // src/dispatch/handlers/activity-lookahead-sync.ts
12404
- function dispatchActivityLookaheadSync(action, deps) {
12715
+ async function dispatchActivityLookaheadSync(action, options, deps) {
12716
+ if (hasInvalidPersistedProgress(action)) {
12717
+ return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
12718
+ }
12405
12719
  const ids = [...new Set(action.activityIds.map(String))];
12720
+ const acknowledgesUnknownPersistedActivity = action.acknowledgePersistedLookahead === true && ids.some((activityId) => !deps.isActivityIdReserved(activityId));
12721
+ if (acknowledgesUnknownPersistedActivity) {
12722
+ return { ok: false, reason: REJECTION_REASON.INVALID };
12723
+ }
12406
12724
  const snapshots = ids.map((id) => deps.adapter.getActivity(id));
12407
12725
  if (snapshots.some((snapshot) => !snapshot)) {
12408
12726
  return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
@@ -12442,6 +12760,39 @@ function dispatchActivityLookaheadSync(action, deps) {
12442
12760
  }
12443
12761
  });
12444
12762
  }
12763
+ if (action.persistedProgress !== void 0) {
12764
+ const progressChanges = await applyReconciledProgress(
12765
+ [{ activityId: ids[0], progress: action.persistedProgress }],
12766
+ deps
12767
+ );
12768
+ if (progressChanges.length > 0) {
12769
+ await runPostMutation(
12770
+ {
12771
+ adapter: deps.adapter,
12772
+ scheduler: deps.scheduler,
12773
+ sector: deps.sector,
12774
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
12775
+ },
12776
+ {
12777
+ action,
12778
+ autoscheduleFrom: null,
12779
+ recomputeParentsFrom: [],
12780
+ now: deps.now,
12781
+ options
12782
+ }
12783
+ );
12784
+ return {
12785
+ ok: true,
12786
+ changes: await assembleChangeSet(deps.adapter, {
12787
+ source: action,
12788
+ beforeSnap: /* @__PURE__ */ new Map(),
12789
+ touchedIds: [],
12790
+ scheduledIds: [],
12791
+ hoursPerDay: deps.sector.hoursPerDay
12792
+ })
12793
+ };
12794
+ }
12795
+ }
12445
12796
  return {
12446
12797
  ok: true,
12447
12798
  changes: {
@@ -12453,6 +12804,10 @@ function dispatchActivityLookaheadSync(action, deps) {
12453
12804
  }
12454
12805
  };
12455
12806
  }
12807
+ function hasInvalidPersistedProgress(action) {
12808
+ const progress = action.persistedProgress;
12809
+ return progress !== void 0 && (action.activityIds.length !== 1 || !Number.isFinite(progress) || progress < 0 || progress > 100);
12810
+ }
12456
12811
 
12457
12812
  // src/internal/hierarchy/invalid-links.ts
12458
12813
  function collectInvalidLinkIdsForCycle(movedActivityIds, adapter) {
@@ -12720,7 +13075,6 @@ async function dispatchActivityMove(action, options, deps) {
12720
13075
  touchedIds: touched,
12721
13076
  scheduledIds,
12722
13077
  sirDetection: "before-diff",
12723
- preview: options.preview,
12724
13078
  trackingEvents: [trackingEvent],
12725
13079
  hoursPerDay: sector.hoursPerDay
12726
13080
  })
@@ -13262,11 +13616,11 @@ async function dispatchDatesBatch(action, options, deps) {
13262
13616
  const warnings = [];
13263
13617
  let needsAutoSchedule = false;
13264
13618
  for (const edit of action.edits) {
13265
- const outcome = applyOneEdit2(edit, deps);
13619
+ const outcome = applyOneEdit(edit, deps);
13266
13620
  verdicts.push(outcome.verdict);
13267
13621
  if (!outcome.verdict.ok || !outcome.changes) continue;
13268
13622
  const editTouched = collectTouchedIds(edit.activityId, outcome.changes);
13269
- mergeBeforeSnapshots2(beforeSnap, editTouched, deps);
13623
+ mergeBeforeSnapshots(beforeSnap, editTouched, deps);
13270
13624
  for (const id of editTouched) touchedIds.add(id);
13271
13625
  const beforeLinkLags = snapshotIncomingLinkLags(adapter, edit.activityId);
13272
13626
  const preEditSnapshot = snapshotSingleActivity(
@@ -13320,7 +13674,6 @@ async function dispatchDatesBatch(action, options, deps) {
13320
13674
  touchedIds,
13321
13675
  scheduledIds,
13322
13676
  sirDetection: "before-diff",
13323
- preview: options.preview,
13324
13677
  links: linkChanges,
13325
13678
  trackingEvents,
13326
13679
  warnings,
@@ -13328,7 +13681,7 @@ async function dispatchDatesBatch(action, options, deps) {
13328
13681
  })
13329
13682
  };
13330
13683
  }
13331
- function applyOneEdit2(edit, deps) {
13684
+ function applyOneEdit(edit, deps) {
13332
13685
  const { ctx } = deps;
13333
13686
  const activityId = edit.activityId;
13334
13687
  const pipeline = findPipelineForColumn(edit.column);
@@ -13390,7 +13743,7 @@ function applyOneEdit2(edit, deps) {
13390
13743
  );
13391
13744
  return { verdict: { activityId, ok: true }, changes };
13392
13745
  }
13393
- function mergeBeforeSnapshots2(beforeSnap, ids, deps) {
13746
+ function mergeBeforeSnapshots(beforeSnap, ids, deps) {
13394
13747
  const missing = /* @__PURE__ */ new Set();
13395
13748
  for (const id of ids) {
13396
13749
  if (!beforeSnap.has(id)) missing.add(id);
@@ -13404,212 +13757,246 @@ function mergeBeforeSnapshots2(beforeSnap, ids, deps) {
13404
13757
  }
13405
13758
  }
13406
13759
 
13407
- // src/dispatch/handlers/persistence-acknowledge.ts
13408
- function assertValidAssignment(assignment, entityName, readCurrent, ownerByBackendId, seenIds, seenBackendIds) {
13409
- const id = String(assignment.id);
13410
- if (id.length === 0 || seenIds.has(id)) {
13411
- throw new Error(
13412
- `persistence-acknowledge: duplicate or empty ${entityName} id "${id}"`
13413
- );
13414
- }
13415
- if (!Number.isSafeInteger(assignment.proplannerId) || assignment.proplannerId <= 0) {
13416
- throw new Error(
13417
- `persistence-acknowledge: invalid ${entityName} proplannerId ${String(assignment.proplannerId)}`
13418
- );
13760
+ // src/dispatch/handlers/bulk-edit.ts
13761
+ var CUSTOM_ID_COLUMN = COLUMN.CUSTOM_ID;
13762
+ async function dispatchBulkEdit(action, options, deps) {
13763
+ const { adapter, scheduler, sector } = deps;
13764
+ const editsIncludeDuration = action.edits.some(
13765
+ (edit) => edit.column === COLUMN.DURATION
13766
+ );
13767
+ if (editsIncludeDuration) {
13768
+ scheduler.invalidateExpandedLinksCache();
13419
13769
  }
13420
- if (seenBackendIds.has(assignment.proplannerId)) {
13421
- throw new Error(
13422
- `persistence-acknowledge: duplicate ${entityName} proplannerId ${String(assignment.proplannerId)}`
13770
+ const verdicts = [];
13771
+ const beforeSnap = /* @__PURE__ */ new Map();
13772
+ const constraintPriorsByActivity = /* @__PURE__ */ new Map();
13773
+ const explicitDateEditActivities = /* @__PURE__ */ new Set();
13774
+ const touchedIds = /* @__PURE__ */ new Set();
13775
+ const trackingEvents = [];
13776
+ const linkChanges = [];
13777
+ const warnings = [];
13778
+ let needsAutoSchedule = false;
13779
+ for (const edit of action.edits) {
13780
+ const outcome = applyOneEdit2(edit, deps);
13781
+ verdicts.push(outcome.verdict);
13782
+ if (!outcome.verdict.ok || !outcome.changes) continue;
13783
+ const constraintWarning = toConstraintWarning(
13784
+ edit.activityId,
13785
+ outcome.changes.constraintWarning
13423
13786
  );
13424
- }
13425
- const state = readCurrent(id);
13426
- if (!state.exists) {
13427
- throw new Error(
13428
- `persistence-acknowledge: unknown ${entityName} id "${id}"`
13787
+ if (constraintWarning) warnings.push(constraintWarning);
13788
+ const editTouched = collectTouchedIds(edit.activityId, outcome.changes);
13789
+ mergeBeforeSnapshots2(beforeSnap, editTouched, deps);
13790
+ for (const touchedId of editTouched) touchedIds.add(touchedId);
13791
+ recordConstraintPrior(constraintPriorsByActivity, edit, beforeSnap);
13792
+ if (isExplicitConstraintDateEdit(edit.column)) {
13793
+ explicitDateEditActivities.add(String(edit.activityId));
13794
+ }
13795
+ const beforeLinkLags = snapshotIncomingLinkLags(adapter, edit.activityId);
13796
+ const preEditSnapshot = snapshotSingleActivity(
13797
+ adapter,
13798
+ String(edit.activityId)
13429
13799
  );
13430
- }
13431
- if (state.current !== null && state.current !== assignment.proplannerId) {
13432
- throw new Error(
13433
- `persistence-acknowledge: ${entityName} "${id}" already has proplannerId ${String(state.current)}`
13800
+ const customIdBeforeApply = readLiveCustomId(edit, deps);
13801
+ const changes = outcome.changes;
13802
+ applyFieldChanges(adapter, edit.activityId, changes);
13803
+ runPostProcessorsOnAdapter(
13804
+ edit.activityId,
13805
+ outcome.changes.postProcessors ?? [],
13806
+ adapter,
13807
+ preEditSnapshot
13434
13808
  );
13435
- }
13436
- const existingOwner = ownerByBackendId.get(assignment.proplannerId);
13437
- if (existingOwner !== void 0 && existingOwner !== id) {
13438
- throw new Error(
13439
- `persistence-acknowledge: ${entityName} proplannerId ${String(assignment.proplannerId)} already belongs to "${existingOwner}"`
13809
+ syncCustomIdTracker(edit, customIdBeforeApply, deps);
13810
+ linkChanges.push(
13811
+ ...diffIncomingLinkLagChanges(beforeLinkLags, adapter, edit.activityId)
13440
13812
  );
13813
+ needsAutoSchedule ||= Boolean(outcome.changes.needsAutoSchedule);
13814
+ for (const trackingEvent of outcome.changes.trackingEvents ?? []) {
13815
+ trackingEvents.push({
13816
+ name: trackingEvent.name,
13817
+ properties: trackingEvent.properties ?? {}
13818
+ });
13819
+ }
13441
13820
  }
13442
- seenIds.add(id);
13443
- seenBackendIds.add(assignment.proplannerId);
13821
+ const postMutationArgs = {
13822
+ action,
13823
+ autoscheduleFrom: needsAutoSchedule ? "roots" : null,
13824
+ recomputeParentsFrom: touchedIds,
13825
+ now: deps.now,
13826
+ options
13827
+ };
13828
+ const batchHasConstraintEdits = constraintPriorsByActivity.size > 0;
13829
+ const { scheduledIds } = await (batchHasConstraintEdits ? settleConstraintEdits(
13830
+ {
13831
+ adapter,
13832
+ scheduler,
13833
+ sector,
13834
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
13835
+ },
13836
+ {
13837
+ postMutationArgs,
13838
+ revertTargets: [...constraintPriorsByActivity].filter(
13839
+ ([activityId]) => !explicitDateEditActivities.has(activityId)
13840
+ ).map(([activityId, priorConstraintDate]) => ({
13841
+ activityId,
13842
+ priorConstraintDate
13843
+ })),
13844
+ revert: (activityId, priorConstraintDate) => revertNoOpConstraintEdit(
13845
+ activityId,
13846
+ priorConstraintDate,
13847
+ adapter,
13848
+ deps.ctx.calendars
13849
+ )
13850
+ }
13851
+ ) : runPostMutation(
13852
+ {
13853
+ adapter,
13854
+ scheduler,
13855
+ sector,
13856
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
13857
+ },
13858
+ postMutationArgs
13859
+ ));
13860
+ return {
13861
+ ok: true,
13862
+ verdicts,
13863
+ changes: await assembleChangeSet(adapter, {
13864
+ source: action,
13865
+ beforeSnap,
13866
+ touchedIds,
13867
+ scheduledIds,
13868
+ sirDetection: "before-diff",
13869
+ links: linkChanges,
13870
+ trackingEvents,
13871
+ warnings,
13872
+ hoursPerDay: sector.hoursPerDay
13873
+ })
13874
+ };
13444
13875
  }
13445
- function assertValidAssignments(assignments, entityName, readCurrent, ownerByBackendId) {
13446
- const seenIds = /* @__PURE__ */ new Set();
13447
- const seenBackendIds = /* @__PURE__ */ new Set();
13448
- for (const assignment of assignments) {
13449
- assertValidAssignment(
13450
- assignment,
13451
- entityName,
13452
- readCurrent,
13453
- ownerByBackendId,
13454
- seenIds,
13455
- seenBackendIds
13456
- );
13876
+ function applyOneEdit2(edit, deps) {
13877
+ const { ctx } = deps;
13878
+ const activityId = edit.activityId;
13879
+ const pipeline = findPipelineForColumn(edit.column);
13880
+ if (!pipeline) {
13881
+ return {
13882
+ verdict: {
13883
+ activityId,
13884
+ ok: false,
13885
+ reason: REJECTION_REASON.NO_PIPELINE_FOR_COLUMN
13886
+ },
13887
+ changes: null
13888
+ };
13457
13889
  }
13458
- }
13459
- function assertValidReconciledProgress(entries, deps) {
13460
- const seen = /* @__PURE__ */ new Set();
13461
- for (const entry of entries) {
13462
- const id = String(entry.activityId);
13463
- if (id.length === 0 || seen.has(id)) {
13464
- throw new Error(
13465
- `persistence-acknowledge: duplicate or empty reconciled activity id "${id}"`
13466
- );
13467
- }
13468
- if (!Number.isFinite(entry.progress) || entry.progress < 0 || entry.progress > 100) {
13469
- throw new Error(
13470
- `persistence-acknowledge: invalid reconciled progress ${String(entry.progress)} for "${id}"`
13471
- );
13472
- }
13473
- if (!deps.adapter.getActivity(id)) {
13474
- throw new Error(
13475
- `persistence-acknowledge: unknown reconciled activity id "${id}"`
13476
- );
13477
- }
13478
- seen.add(id);
13890
+ const activitySnap = ctx.activityReader.getActivity(edit.activityId);
13891
+ if (!activitySnap) {
13892
+ return {
13893
+ verdict: {
13894
+ activityId,
13895
+ ok: false,
13896
+ reason: REJECTION_REASON.ACTIVITY_NOT_FOUND
13897
+ },
13898
+ changes: null
13899
+ };
13479
13900
  }
13480
- }
13481
- async function dispatchPersistenceAcknowledge(action, deps) {
13482
- const activities = action.activities ?? [];
13483
- const links = action.links ?? [];
13484
- const reconciledProgress = action.reconciledProgress ?? [];
13485
- assertValidReconciledProgress(reconciledProgress, deps);
13486
- const activityOwnerByBackendId = /* @__PURE__ */ new Map();
13487
- for (const activity of deps.adapter.getAllActivities()) {
13488
- if (activity.proplannerId != null) {
13489
- activityOwnerByBackendId.set(activity.proplannerId, String(activity.id));
13490
- }
13901
+ const editGate = pipeline.canEdit(activitySnap, ctx.hierarchy);
13902
+ if (!editGate.allowed) {
13903
+ return {
13904
+ verdict: {
13905
+ activityId,
13906
+ ok: false,
13907
+ reason: editGate.reason ?? REJECTION_REASON.CANNOT_EDIT,
13908
+ ...editGate.alertKey ? { alertKey: editGate.alertKey } : {}
13909
+ },
13910
+ changes: null
13911
+ };
13491
13912
  }
13492
- const linkOwnerByBackendId = /* @__PURE__ */ new Map();
13493
- for (const link of deps.adapter.getAllLinks()) {
13494
- if (link.proplannerId != null) {
13495
- linkOwnerByBackendId.set(link.proplannerId, String(link.id));
13496
- }
13913
+ const parsed2 = pipeline.parseInput(edit.newValue, activitySnap, ctx);
13914
+ if (!parsed2.ok) {
13915
+ return {
13916
+ verdict: {
13917
+ activityId,
13918
+ ok: false,
13919
+ reason: parsed2.reason ?? REJECTION_REASON.PARSE_ERROR
13920
+ },
13921
+ changes: null
13922
+ };
13497
13923
  }
13498
- assertValidAssignments(
13499
- activities,
13500
- "activity",
13501
- (id) => {
13502
- const activity = deps.adapter.getActivity(id);
13924
+ const oldValue = Reflect.get(activitySnap, pipeline.targetField);
13925
+ const validation = pipeline.validate(
13926
+ activitySnap,
13927
+ oldValue,
13928
+ parsed2.value,
13929
+ ctx
13930
+ );
13931
+ if (!validation.valid) {
13932
+ return {
13933
+ verdict: {
13934
+ activityId,
13935
+ ok: false,
13936
+ reason: validation.reason ?? REJECTION_REASON.INVALID,
13937
+ ...validation.notification?.messageKey ? { alertKey: validation.notification.messageKey } : {}
13938
+ },
13939
+ changes: null
13940
+ };
13941
+ }
13942
+ if (edit.column === CUSTOM_ID_COLUMN && parsed2.value !== null) {
13943
+ const liveActivity = deps.adapter.getActivity(edit.activityId);
13944
+ const oldCustomIdRaw = liveActivity ? liveActivity.customId : null;
13945
+ const oldCustomId = typeof oldCustomIdRaw === "string" ? oldCustomIdRaw : null;
13946
+ if (deps.customIdTracker.isCustomIdInUse(String(parsed2.value), oldCustomId)) {
13503
13947
  return {
13504
- exists: activity !== null,
13505
- current: activity?.proplannerId ?? null
13948
+ verdict: {
13949
+ activityId,
13950
+ ok: false,
13951
+ reason: REJECTION_REASON.CUSTOM_ID_DUPLICATE
13952
+ },
13953
+ changes: null
13506
13954
  };
13507
- },
13508
- activityOwnerByBackendId
13509
- );
13510
- assertValidAssignments(
13511
- links,
13512
- "link",
13513
- (id) => {
13514
- const link = deps.adapter.getLink(id);
13515
- return { exists: link !== null, current: link?.proplannerId ?? null };
13516
- },
13517
- linkOwnerByBackendId
13518
- );
13519
- const activityChanges = [];
13520
- const linkChanges = [];
13521
- const progressToApply = reconciledProgress.filter((entry) => {
13522
- const current = deps.adapter.getActivity(String(entry.activityId));
13523
- return current !== null && current.progress !== entry.progress;
13524
- });
13525
- const touchedForProgress = new Set(
13526
- progressToApply.map((entry) => String(entry.activityId))
13527
- );
13528
- const parentsToRollup = /* @__PURE__ */ new Set();
13529
- for (const entry of progressToApply) {
13530
- const activity = deps.adapter.getActivity(String(entry.activityId));
13531
- if (activity && !isRootParent(activity.parentId)) {
13532
- parentsToRollup.add(String(activity.parentId));
13533
- touchedForProgress.add(String(activity.parentId));
13534
13955
  }
13535
13956
  }
13536
- const beforeProgressSnap = await snapshotActivities(
13537
- deps.adapter,
13538
- touchedForProgress
13957
+ const changes = pipeline.transform(
13958
+ activitySnap,
13959
+ parsed2.value,
13960
+ ctx,
13961
+ parsed2.raw
13539
13962
  );
13540
- deps.adapter.batchUpdate(() => {
13541
- for (const entry of progressToApply) {
13542
- deps.adapter.setActivityField(
13543
- String(entry.activityId),
13544
- "progress",
13545
- entry.progress
13546
- );
13547
- }
13548
- for (const parentId of parentsToRollup) {
13549
- recomputeRollupCascadesForParent(parentId, deps.adapter);
13550
- }
13551
- for (const id of touchedForProgress) {
13552
- const before = beforeProgressSnap.get(id);
13553
- const after = deps.adapter.getActivity(id);
13554
- if (!before || !after || before.progress === after.progress) continue;
13555
- activityChanges.push({
13556
- id,
13557
- kind: "updated",
13558
- fields: {
13559
- progress: { before: before.progress, after: after.progress }
13560
- },
13561
- after: { ...after }
13562
- });
13563
- }
13564
- for (const assignment of activities) {
13565
- const before = deps.adapter.getActivity(assignment.id);
13566
- if (!before || before.proplannerId === assignment.proplannerId) continue;
13567
- const previousProplannerId = before.proplannerId;
13568
- deps.adapter.setActivityField(
13569
- assignment.id,
13570
- "proplannerId",
13571
- assignment.proplannerId
13572
- );
13573
- activityChanges.push({
13574
- id: assignment.id,
13575
- kind: "updated",
13576
- fields: {
13577
- proplannerId: {
13578
- before: previousProplannerId,
13579
- after: assignment.proplannerId
13580
- }
13581
- },
13582
- after: { ...before, proplannerId: assignment.proplannerId }
13583
- });
13584
- }
13585
- for (const assignment of links) {
13586
- const before = deps.adapter.getLink(assignment.id);
13587
- if (!before || before.proplannerId === assignment.proplannerId) continue;
13588
- const previousProplannerId = before.proplannerId;
13589
- deps.adapter.setLinkProplannerId(assignment.id, assignment.proplannerId);
13590
- linkChanges.push({
13591
- id: assignment.id,
13592
- kind: "updated",
13593
- fields: {
13594
- proplannerId: {
13595
- before: previousProplannerId,
13596
- after: assignment.proplannerId
13597
- }
13598
- },
13599
- after: { ...before, proplannerId: assignment.proplannerId }
13600
- });
13601
- }
13602
- });
13603
- return {
13604
- ok: true,
13605
- changes: {
13606
- source: action,
13607
- activities: activityChanges,
13608
- links: linkChanges,
13609
- calendars: [],
13610
- trackingEvents: []
13963
+ return { verdict: { activityId, ok: true }, changes };
13964
+ }
13965
+ function readLiveCustomId(edit, deps) {
13966
+ if (edit.column !== CUSTOM_ID_COLUMN) return null;
13967
+ const liveActivity = deps.adapter.getActivity(edit.activityId);
13968
+ const rawValue = liveActivity ? liveActivity.customId : null;
13969
+ return typeof rawValue === "string" ? rawValue : null;
13970
+ }
13971
+ function syncCustomIdTracker(edit, oldCustomId, deps) {
13972
+ if (edit.column !== CUSTOM_ID_COLUMN) return;
13973
+ const liveActivity = deps.adapter.getActivity(edit.activityId);
13974
+ const rawValue = liveActivity ? liveActivity.customId : null;
13975
+ const newCustomId = typeof rawValue === "string" ? rawValue : null;
13976
+ if (oldCustomId !== newCustomId) {
13977
+ deps.customIdTracker.trackCustomIdChange(oldCustomId, newCustomId);
13978
+ }
13979
+ }
13980
+ function recordConstraintPrior(priors, edit, beforeSnap) {
13981
+ if (!isConstraintEditColumn(edit.column)) return;
13982
+ const activityKey = String(edit.activityId);
13983
+ if (priors.has(activityKey)) return;
13984
+ const preBatchSnapshot = beforeSnap.get(activityKey);
13985
+ if (!preBatchSnapshot) return;
13986
+ priors.set(activityKey, preBatchSnapshot.constraintDate ?? null);
13987
+ }
13988
+ function mergeBeforeSnapshots2(beforeSnap, ids, deps) {
13989
+ const missing = /* @__PURE__ */ new Set();
13990
+ for (const candidateId of ids) {
13991
+ if (!beforeSnap.has(candidateId)) missing.add(candidateId);
13992
+ }
13993
+ if (missing.size === 0) return;
13994
+ for (const missingId of missing) {
13995
+ const liveActivity = deps.adapter.getActivity(missingId);
13996
+ if (liveActivity) {
13997
+ beforeSnap.set(missingId, structuredCloneActivity(liveActivity));
13611
13998
  }
13612
- };
13999
+ }
13613
14000
  }
13614
14001
 
13615
14002
  // src/baselines/normalize-baseline-point.ts
@@ -13786,74 +14173,25 @@ function sumRecursiveBaselineDurationDays(activityId, adapter) {
13786
14173
  sum += sumRecursiveBaselineDurationDays(String(childId), adapter);
13787
14174
  } else if (type !== "milestone") {
13788
14175
  sum += toNumber(active.durationDays);
13789
- }
13790
- }
13791
- return sum;
13792
- }
13793
- function toNumber(v) {
13794
- const n = typeof v === "string" ? parseFloat(v) : v;
13795
- return typeof n === "number" && Number.isFinite(n) ? n : 0;
13796
- }
13797
-
13798
- // src/propagations/downward/recompute-all-ponderators.ts
13799
- function recomputeAllPonderators(criterion, adapter) {
13800
- adapter.forEachActivity((_a, id) => {
13801
- adapter.setActivityField(id, COLUMN.PONDERATOR, 0);
13802
- });
13803
- adapter.forEachActivity((_a, id) => {
13804
- if (adapter.getChildren(id).length > 0) {
13805
- recomputePonderatorsForParent(String(id), criterion, adapter);
13806
- }
13807
- });
13808
- }
13809
-
13810
- // src/propagations/upward/progress-rollup.ts
13811
- function recomputeAllProgressRollup(adapter) {
13812
- for (const parentId of parentsDeepestFirst(adapter)) {
13813
- rollupParent(parentId, adapter);
13814
- }
13815
- }
13816
- function parentsDeepestFirst(adapter) {
13817
- const parents = /* @__PURE__ */ new Set();
13818
- adapter.forEachActivity((_activity, id) => {
13819
- if (adapter.getChildren(id).length > 0) parents.add(id);
13820
- });
13821
- const depthById = /* @__PURE__ */ new Map();
13822
- for (const id of parents) depthById.set(id, depthOf2(id, adapter));
13823
- return [...parents].sort(
13824
- (a, b) => (depthById.get(b) ?? 0) - (depthById.get(a) ?? 0)
13825
- );
13826
- }
13827
- function depthOf2(id, adapter) {
13828
- let depth = 0;
13829
- let current = id;
13830
- while (current && current !== "0") {
13831
- if (!adapter.getActivity(current)) break;
13832
- depth += 1;
13833
- current = adapter.getParentId(current);
13834
- }
13835
- return depth;
13836
- }
13837
- function rollupParent(parentId, adapter) {
13838
- const childIds = adapter.getChildren(parentId);
13839
- if (childIds.length === 0) return;
13840
- const children = [];
13841
- for (const childId of childIds) {
13842
- const child = adapter.getActivity(childId);
13843
- if (!child) continue;
13844
- children.push({
13845
- progress: Number(child.progress ?? 0),
13846
- ponderator: Number(child.ponderator ?? 0)
13847
- });
13848
- }
13849
- const rollup = computeWeightedProgressRollup(children);
13850
- if (rollup !== null) {
13851
- adapter.setActivityField(
13852
- parentId,
13853
- COLUMN.PROGRESS,
13854
- roundProgressPerLevel(rollup)
13855
- );
14176
+ }
13856
14177
  }
14178
+ return sum;
14179
+ }
14180
+ function toNumber(v) {
14181
+ const n = typeof v === "string" ? parseFloat(v) : v;
14182
+ return typeof n === "number" && Number.isFinite(n) ? n : 0;
14183
+ }
14184
+
14185
+ // src/propagations/downward/recompute-all-ponderators.ts
14186
+ function recomputeAllPonderators(criterion, adapter) {
14187
+ adapter.forEachActivity((_a, id) => {
14188
+ adapter.setActivityField(id, COLUMN.PONDERATOR, 0);
14189
+ });
14190
+ adapter.forEachActivity((_a, id) => {
14191
+ if (adapter.getChildren(id).length > 0) {
14192
+ recomputePonderatorsForParent(String(id), criterion, adapter);
14193
+ }
14194
+ });
13857
14195
  }
13858
14196
 
13859
14197
  // src/dispatch/shared/baseline-weighted-fields.ts
@@ -14245,11 +14583,8 @@ async function performDispatch(action, options, deps) {
14245
14583
  if (action.kind === "sir-sync") {
14246
14584
  return dispatchSirSync(action, deps);
14247
14585
  }
14248
- if (action.kind === "sir-approve") {
14249
- return dispatchSirApprove(action, options, deps);
14250
- }
14251
14586
  if (action.kind === "activity-lookahead-sync") {
14252
- return dispatchActivityLookaheadSync(action, deps);
14587
+ return dispatchActivityLookaheadSync(action, options, deps);
14253
14588
  }
14254
14589
  if (action.kind === "inline-edit" && (action.column === COLUMN.CUSTOM_PREDECESSORS || action.column === COLUMN.CUSTOM_SUCESSORS)) {
14255
14590
  return dispatchInlineEditLinks(action, options, deps);
@@ -17399,7 +17734,7 @@ var ViewStateStore = class {
17399
17734
  };
17400
17735
 
17401
17736
  // src/internal/state/schedule-state.ts
17402
- var ScheduleState = class {
17737
+ var ScheduleState = class _ScheduleState {
17403
17738
  _activities = /* @__PURE__ */ new Map();
17404
17739
  _lastStartDate = /* @__PURE__ */ new Map();
17405
17740
  _promotionSnapshot = /* @__PURE__ */ new Map();
@@ -17421,22 +17756,52 @@ var ScheduleState = class {
17421
17756
  _promotionSnapshotBefore = null;
17422
17757
  _calendar;
17423
17758
  _flags;
17759
+ _calendarSnapshot;
17424
17760
  constructor(snapshot, buildOrderComparator = () => null) {
17425
17761
  this._buildOrderComparator = buildOrderComparator;
17762
+ this._calendarSnapshot = cloneDomainValue(snapshot.calendars);
17426
17763
  for (const activity of snapshot.activities) {
17427
17764
  this._activities.set(activity.id, activity);
17428
17765
  }
17429
17766
  for (const link of snapshot.links) {
17430
- this._links.set(String(link.id), link);
17431
- this._indexLink(link);
17767
+ const normalized2 = {
17768
+ ...link,
17769
+ freeSlackHours: link.freeSlackHours ?? null,
17770
+ isCritical: link.isCritical === true
17771
+ };
17772
+ this._links.set(String(link.id), normalized2);
17773
+ this._indexLink(normalized2);
17432
17774
  }
17433
17775
  this._calendar = new CalendarCalculator(
17434
- createCalendarSetup(snapshot.calendars)
17776
+ createCalendarSetup(this._calendarSnapshot)
17435
17777
  );
17436
17778
  this._flags = buildFlags(snapshot);
17437
17779
  this._hierarchy = new HierarchyIndex(this._activities);
17438
17780
  this._hierarchy.rebuild();
17439
17781
  }
17782
+ fork() {
17783
+ const fork = new _ScheduleState(
17784
+ {
17785
+ activities: this.getAllActivities().map(cloneCoreActivity),
17786
+ links: this.getAllLinks().map((link) => ({ ...link })),
17787
+ calendars: cloneDomainValue(this._calendarSnapshot)
17788
+ },
17789
+ this._buildOrderComparator
17790
+ );
17791
+ fork._viewState.restore(cloneDomainValue(this._viewState.snapshot()));
17792
+ fork._lastStartDate = new Map(
17793
+ Array.from(this._lastStartDate, ([id, date2]) => [id, new Date(date2)])
17794
+ );
17795
+ fork._promotionSnapshot = new Map(
17796
+ Array.from(this._promotionSnapshot, ([id, snapshot]) => [
17797
+ id,
17798
+ cloneDomainValue(snapshot)
17799
+ ])
17800
+ );
17801
+ fork._flags = cloneDomainValue(this._flags);
17802
+ fork._filterProjection = cloneDomainValue(this._filterProjection);
17803
+ return fork;
17804
+ }
17440
17805
  getAllActivities() {
17441
17806
  return Array.from(this._activities.values());
17442
17807
  }
@@ -17695,8 +18060,13 @@ var ScheduleState = class {
17695
18060
  this._writeCapture.noteLinkAdd(String(link.id));
17696
18061
  const superseded = this._links.get(String(link.id));
17697
18062
  if (superseded) this._deindexLink(superseded);
17698
- this._links.set(String(link.id), { ...link });
17699
- this._indexLink(link);
18063
+ const normalized2 = {
18064
+ ...link,
18065
+ freeSlackHours: link.freeSlackHours ?? null,
18066
+ isCritical: link.isCritical === true
18067
+ };
18068
+ this._links.set(String(link.id), normalized2);
18069
+ this._indexLink(normalized2);
17700
18070
  }
17701
18071
  _indexLink(link) {
17702
18072
  pushLinkRef(this._outgoing, String(link.source), link.id);
@@ -17849,7 +18219,7 @@ var ScheduleState = class {
17849
18219
  };
17850
18220
  var EMPTY_LINK_IDS = [];
17851
18221
  function isMutableLinkField(field) {
17852
- return field === LINK_PROPERTY.LAG || field === LINK_PROPERTY.TYPE;
18222
+ return field === LINK_PROPERTY.LAG || field === LINK_PROPERTY.TYPE || field === LINK_PROPERTY.FREE_SLACK_HOURS || field === LINK_PROPERTY.IS_CRITICAL;
17853
18223
  }
17854
18224
  function fieldAffectsVisualOrder(field) {
17855
18225
  return field === ACTIVITY_PROPERTY.PARENT || field === ACTIVITY_PROPERTY.CORRELATIVE_ID;
@@ -17970,12 +18340,7 @@ function buildPipelineContext(adapter, options) {
17970
18340
  activityReader: createStateBackedActivityReader(adapter),
17971
18341
  dateFormat: options.dateFormat ?? "YYYY-MM-DD",
17972
18342
  customHours: options.customHours ?? {},
17973
- hoursPerDay: options.hoursPerDay,
17974
- // ISSUE-057 (CP6): sin default. Un comando de duración sin inputUnit se
17975
- // rechaza en el pipeline; el resto de columnas no lo leen. La clave se
17976
- // OMITE cuando no se declaró (exactOptionalPropertyTypes), no se pone a
17977
- // undefined.
17978
- ...options.inputUnit !== void 0 ? { inputUnit: options.inputUnit } : {}
18343
+ hoursPerDay: options.hoursPerDay
17979
18344
  };
17980
18345
  }
17981
18346
 
@@ -17985,15 +18350,26 @@ function willRunCriticalPath(action) {
17985
18350
  }
17986
18351
 
17987
18352
  // src/dispatch/undo/undo-entry.ts
17988
- function buildHistoryChangeSet(changes) {
17989
- const activitiesWithoutAfter = changes.activities.map((entry) => ({
18353
+ var DERIVED_LINK_FIELDS = /* @__PURE__ */ new Set(["freeSlackHours", "isCritical"]);
18354
+ function withoutDerivedFields(entry, derivedFields) {
18355
+ const fields = Object.fromEntries(
18356
+ Object.entries(entry.fields ?? {}).filter(
18357
+ ([field]) => !derivedFields.has(field)
18358
+ )
18359
+ );
18360
+ if (entry.kind === "updated" && Object.keys(fields).length === 0) return null;
18361
+ return {
17990
18362
  ...entry,
18363
+ ...entry.fields === void 0 ? {} : { fields },
17991
18364
  after: null
17992
- }));
17993
- const linksWithoutAfter = changes.links.map((entry) => ({
18365
+ };
18366
+ }
18367
+ function buildHistoryChangeSet(changes) {
18368
+ const activitiesWithoutAfter = changes.activities.map((entry) => ({
17994
18369
  ...entry,
17995
18370
  after: null
17996
18371
  }));
18372
+ const linksWithoutAfter = changes.links.map((entry) => withoutDerivedFields(entry, DERIVED_LINK_FIELDS)).filter((entry) => entry !== null);
17997
18373
  const source = changes.source.kind === "activity-batch" ? { ...changes.source, items: [], links: [] } : changes.source;
17998
18374
  return cloneDomainValue({
17999
18375
  ...changes,
@@ -18003,7 +18379,22 @@ function buildHistoryChangeSet(changes) {
18003
18379
  });
18004
18380
  }
18005
18381
  function buildUndoEntry(changeSet, beforeSnap, beforeLinks, afterSnap, afterLinks) {
18006
- return { changeSet, beforeSnap, beforeLinks, afterSnap, afterLinks };
18382
+ return {
18383
+ changeSet,
18384
+ beforeSnap,
18385
+ beforeLinks: resetDerivedLinkSnapshots(beforeLinks),
18386
+ afterSnap,
18387
+ afterLinks: resetDerivedLinkSnapshots(afterLinks)
18388
+ };
18389
+ }
18390
+ function resetDerivedLinkSnapshots(snapshots) {
18391
+ if (!snapshots) return void 0;
18392
+ return new Map(
18393
+ Array.from(snapshots, ([id, link]) => [
18394
+ id,
18395
+ { ...link, freeSlackHours: null, isCritical: false }
18396
+ ])
18397
+ );
18007
18398
  }
18008
18399
  function captureCreatedSnapshots(state, changeSet) {
18009
18400
  const afterSnap = /* @__PURE__ */ new Map();
@@ -18055,8 +18446,7 @@ function mergeCoalesced(top, next) {
18055
18446
  };
18056
18447
  return buildUndoEntry(changeSet);
18057
18448
  }
18058
- function getDispatchHistoryPolicy(action, options) {
18059
- if (options?.preview === true) return "skip";
18449
+ function getDispatchHistoryPolicy(action) {
18060
18450
  if (action.kind === "persistence-acknowledge") return "clear-on-success";
18061
18451
  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") {
18062
18452
  return "skip";
@@ -18302,7 +18692,7 @@ function applyChangeSetSide(state, entry, side) {
18302
18692
  // src/dispatch/undo/undo-recorder.ts
18303
18693
  var DEFAULT_MAX_DEPTH = 100;
18304
18694
  var COALESCE_WINDOW_MS = 600;
18305
- var UndoRecorder = class {
18695
+ var UndoRecorder = class _UndoRecorder {
18306
18696
  constructor(maxDepth = DEFAULT_MAX_DEPTH) {
18307
18697
  this.maxDepth = maxDepth;
18308
18698
  }
@@ -18311,6 +18701,14 @@ var UndoRecorder = class {
18311
18701
  redoStack = [];
18312
18702
  _lastKey = null;
18313
18703
  _lastTime = 0;
18704
+ fork() {
18705
+ const fork = new _UndoRecorder(this.maxDepth);
18706
+ fork.undoStack.push(...cloneDomainValue(this.undoStack));
18707
+ fork.redoStack.push(...cloneDomainValue(this.redoStack));
18708
+ fork._lastKey = this._lastKey;
18709
+ fork._lastTime = this._lastTime;
18710
+ return fork;
18711
+ }
18314
18712
  record(entry, coalesceKey = null, now = 0) {
18315
18713
  const top = this.undoStack[this.undoStack.length - 1];
18316
18714
  if (coalesceKey != null && coalesceKey === this._lastKey && now - this._lastTime <= COALESCE_WINDOW_MS && top !== void 0 && canCoalesce(top, entry)) {
@@ -18434,40 +18832,56 @@ function buildStateSnapshot(activities, links, calendars, baseCalendars = []) {
18434
18832
  }
18435
18833
 
18436
18834
  // src/boundary/save/activity-changes.ts
18437
- var PERSISTABLE_ACTIVITY_FIELDS = [
18438
- "id",
18439
- "proplannerId",
18440
- "parentId",
18441
- "name",
18442
- "description",
18443
- "type",
18444
- "startDate",
18445
- "endDate",
18446
- "durationHours",
18447
- "constraintType",
18448
- "constraintDate",
18449
- "autoScheduling",
18450
- "calendarId",
18451
- "assignedDefaultCalendar",
18452
- "dateOrigin",
18453
- "progress",
18454
- "cost",
18455
- "usedCost",
18456
- "realCost",
18457
- "workHours",
18458
- "realWorkHours",
18459
- "ponderator",
18460
- "hasCustomPonderator",
18461
- "correlativeId",
18462
- "uniqueCorrelativeId",
18463
- "customId",
18464
- "subcontractId",
18465
- "isLookahead",
18466
- "newActivityIds",
18467
- "hasNewActivities",
18468
- "responsableIds",
18469
- "tagIds"
18470
- ];
18835
+ var ACTIVITY_SAVE_FIELD_POLICY = {
18836
+ id: "ignored",
18837
+ proplannerId: "ignored",
18838
+ parentId: "tracked",
18839
+ name: "tracked",
18840
+ description: "tracked",
18841
+ type: "tracked",
18842
+ startDate: "tracked",
18843
+ endDate: "tracked",
18844
+ durationHours: "tracked",
18845
+ constraintType: "tracked",
18846
+ constraintDate: "tracked",
18847
+ autoScheduling: "tracked",
18848
+ calendarId: "tracked",
18849
+ assignedDefaultCalendar: "ignored",
18850
+ dateOrigin: "tracked",
18851
+ progress: "tracked",
18852
+ cost: "tracked",
18853
+ usedCost: "tracked",
18854
+ realCost: "tracked",
18855
+ workHours: "tracked",
18856
+ realWorkHours: "tracked",
18857
+ ponderator: "tracked",
18858
+ hasCustomPonderator: "tracked",
18859
+ correlativeId: "tracked",
18860
+ uniqueCorrelativeId: "tracked",
18861
+ customId: "tracked",
18862
+ subcontractId: "tracked",
18863
+ isLookahead: "tracked",
18864
+ hasLookaheadTasks: "ignored",
18865
+ newActivityIds: "tracked",
18866
+ hasNewActivities: "tracked",
18867
+ isNewActivity: "ignored",
18868
+ pendingRequestIds: "ignored",
18869
+ responsableIds: "tracked",
18870
+ tagIds: "tracked",
18871
+ baselinePoints: "ignored",
18872
+ baselineSnapshot: "ignored",
18873
+ recursiveDurationHours: "ignored",
18874
+ isCritical: "ignored",
18875
+ freeSlackHours: "ignored",
18876
+ calendarDuration: "ignored",
18877
+ expectedProgress: "ignored",
18878
+ expectedProgressBaseline: "ignored",
18879
+ status: "ignored",
18880
+ criticalPath: "ignored"
18881
+ };
18882
+ var PERSISTABLE_ACTIVITY_FIELDS = Object.keys(ACTIVITY_SAVE_FIELD_POLICY).filter((field) => {
18883
+ return ACTIVITY_SAVE_FIELD_POLICY[field] === "tracked";
18884
+ });
18471
18885
  var DATE_FIELDS2 = /* @__PURE__ */ new Set([
18472
18886
  "startDate",
18473
18887
  "endDate",
@@ -18495,7 +18909,6 @@ function snapshotFieldValue(field, value) {
18495
18909
  return value;
18496
18910
  }
18497
18911
  function activityChangedSince(current, saved) {
18498
- if (current.assignedDefaultCalendar) return true;
18499
18912
  for (const field of PERSISTABLE_ACTIVITY_FIELDS) {
18500
18913
  if (!fieldEquals(field, current[field], saved[field])) return true;
18501
18914
  }
@@ -18513,15 +18926,35 @@ function dateEquals(current, saved) {
18513
18926
  return currentTime === savedTime;
18514
18927
  }
18515
18928
  function idArrayEquals(current, saved) {
18516
- const currentIds = Array.isArray(current) ? [...current].map(String).sort() : [];
18517
- const savedIds = Array.isArray(saved) ? [...saved].map(String).sort() : [];
18518
- if (currentIds.length !== savedIds.length) return false;
18519
- return currentIds.every((value, index) => value === savedIds[index]);
18929
+ const currentIds = new Set(Array.isArray(current) ? current.map(String) : []);
18930
+ const savedIds = new Set(Array.isArray(saved) ? saved.map(String) : []);
18931
+ if (currentIds.size !== savedIds.size) return false;
18932
+ for (const currentId of currentIds) {
18933
+ if (!savedIds.has(currentId)) return false;
18934
+ }
18935
+ return true;
18520
18936
  }
18521
18937
 
18522
18938
  // src/boundary/save/link-changes.ts
18939
+ var LINK_SAVE_FIELD_POLICY = {
18940
+ id: "ignored",
18941
+ source: "tracked",
18942
+ target: "tracked",
18943
+ type: "tracked",
18944
+ lag: "tracked",
18945
+ freeSlackHours: "ignored",
18946
+ isCritical: "ignored",
18947
+ ganttId: "ignored",
18948
+ sectorId: "ignored",
18949
+ proplannerId: "ignored"
18950
+ };
18951
+ var TRACKED_LINK_FIELDS = Object.keys(LINK_SAVE_FIELD_POLICY).filter((field) => {
18952
+ return LINK_SAVE_FIELD_POLICY[field] === "tracked";
18953
+ });
18523
18954
  function linkChangedSince(baseline, current) {
18524
- return baseline.source !== current.source || baseline.target !== current.target || baseline.lag !== current.lag || baseline.type !== current.type;
18955
+ return TRACKED_LINK_FIELDS.some(
18956
+ (field) => baseline[field] !== current[field]
18957
+ );
18525
18958
  }
18526
18959
  function checkNoUpdatedLinks(baseline, current) {
18527
18960
  const baselineById = new Map(
@@ -18535,23 +18968,73 @@ function checkNoUpdatedLinks(baseline, current) {
18535
18968
  }
18536
18969
 
18537
18970
  // src/boundary/save/save-tracker.ts
18538
- var SaveTracker = class {
18971
+ function hasPersistedIdentity(proplannerId) {
18972
+ const isNewIdentity = proplannerId == null || proplannerId === 0;
18973
+ if (isNewIdentity) return false;
18974
+ const isValidPersistedIdentity = typeof proplannerId === "number" && Number.isSafeInteger(proplannerId) && proplannerId > 0;
18975
+ if (!isValidPersistedIdentity) {
18976
+ throw new Error(`invalid proplannerId ${String(proplannerId)}`);
18977
+ }
18978
+ return true;
18979
+ }
18980
+ function assertValidPersistedIdentities(activities, links) {
18981
+ for (const activity of activities) {
18982
+ hasPersistedIdentity(activity.proplannerId);
18983
+ }
18984
+ for (const link of links) {
18985
+ hasPersistedIdentity(link.proplannerId);
18986
+ }
18987
+ }
18988
+ var SaveTracker = class _SaveTracker {
18989
+ trackingEnabled;
18539
18990
  initialized = false;
18540
18991
  dirtyActivityIds = /* @__PURE__ */ new Set();
18541
18992
  dirtyLinkIds = /* @__PURE__ */ new Set();
18993
+ initialCalendarRepairIds = /* @__PURE__ */ new Set();
18542
18994
  linksAtLastSave = /* @__PURE__ */ new Map();
18543
18995
  activitiesAtLastSave = /* @__PURE__ */ new Map();
18996
+ constructor(trackingEnabled = true) {
18997
+ this.trackingEnabled = trackingEnabled;
18998
+ }
18999
+ fork() {
19000
+ const fork = new _SaveTracker(false);
19001
+ fork.initialized = this.initialized;
19002
+ for (const [linkId, link] of this.linksAtLastSave) {
19003
+ fork.linksAtLastSave.set(linkId, cloneDomainValue(link));
19004
+ }
19005
+ for (const [activityId, activity] of this.activitiesAtLastSave) {
19006
+ fork.activitiesAtLastSave.set(activityId, cloneDomainValue(activity));
19007
+ }
19008
+ return fork;
19009
+ }
18544
19010
  init(activities, links) {
19011
+ if (!this.trackingEnabled) return;
19012
+ assertValidPersistedIdentities(activities, links);
18545
19013
  this.snapshotActivities(activities);
18546
19014
  this.snapshotLinks(links);
19015
+ for (const activity of activities) {
19016
+ if (activity.assignedDefaultCalendar) {
19017
+ const activityId = String(activity.id);
19018
+ this.initialCalendarRepairIds.add(activityId);
19019
+ this.dirtyActivityIds.add(activityId);
19020
+ }
19021
+ }
18547
19022
  this.initialized = true;
18548
19023
  }
19024
+ acknowledge(activities, links) {
19025
+ if (!this.trackingEnabled) return;
19026
+ assertValidPersistedIdentities(activities, links);
19027
+ this.snapshotActivities(activities);
19028
+ this.snapshotLinks(links);
19029
+ }
18549
19030
  snapshotActivities(activities) {
19031
+ if (!this.trackingEnabled) return;
18550
19032
  this.activitiesAtLastSave.clear();
18551
19033
  this.dirtyActivityIds.clear();
19034
+ this.initialCalendarRepairIds.clear();
18552
19035
  for (const activity of activities) {
18553
19036
  const activityId = String(activity.id);
18554
- if (activity.proplannerId == null) {
19037
+ if (!hasPersistedIdentity(activity.proplannerId)) {
18555
19038
  this.dirtyActivityIds.add(activityId);
18556
19039
  continue;
18557
19040
  }
@@ -18571,11 +19054,12 @@ var SaveTracker = class {
18571
19054
  }
18572
19055
  }
18573
19056
  snapshotLinks(links) {
19057
+ if (!this.trackingEnabled) return;
18574
19058
  this.linksAtLastSave.clear();
18575
19059
  this.dirtyLinkIds.clear();
18576
19060
  for (const link of links) {
18577
19061
  const linkId = String(link.id);
18578
- if (link.proplannerId == null) {
19062
+ if (!hasPersistedIdentity(link.proplannerId)) {
18579
19063
  this.dirtyLinkIds.add(linkId);
18580
19064
  continue;
18581
19065
  }
@@ -18590,12 +19074,11 @@ var SaveTracker = class {
18590
19074
  }
18591
19075
  }
18592
19076
  recordChanges(activities, links, getActivity, getLink) {
18593
- if (!this.initialized) return;
19077
+ if (!this.trackingEnabled || !this.initialized) return;
18594
19078
  for (const change of activities) {
18595
19079
  const activityId = String(change.id);
18596
19080
  const current = getActivity(activityId);
18597
- const saved = this.activitiesAtLastSave.get(activityId);
18598
- if (current === null ? saved !== void 0 : current.proplannerId == null || saved === void 0 || activityChangedSince(current, saved.persistable)) {
19081
+ if (this.activityIsDirty(activityId, current)) {
18599
19082
  this.dirtyActivityIds.add(activityId);
18600
19083
  } else {
18601
19084
  this.dirtyActivityIds.delete(activityId);
@@ -18604,81 +19087,147 @@ var SaveTracker = class {
18604
19087
  for (const change of links) {
18605
19088
  const linkId = String(change.id);
18606
19089
  const current = getLink(linkId);
18607
- const saved = this.linksAtLastSave.get(linkId);
18608
- if (current === null ? saved !== void 0 : current.proplannerId == null || saved === void 0 || linkChangedSince(saved, current)) {
19090
+ if (this.linkIsDirty(linkId, current)) {
18609
19091
  this.dirtyLinkIds.add(linkId);
18610
19092
  } else {
18611
19093
  this.dirtyLinkIds.delete(linkId);
18612
19094
  }
18613
19095
  }
18614
19096
  }
18615
- acknowledgePersistedLookahead(activityIds, getActivity) {
18616
- if (!this.initialized) return;
18617
- for (const activityId of activityIds) {
19097
+ activityIsDirty(activityId, current) {
19098
+ const saved = this.activitiesAtLastSave.get(activityId);
19099
+ if (current === null) return saved !== void 0;
19100
+ return this.initialCalendarRepairIds.has(activityId) || !hasPersistedIdentity(current.proplannerId) || saved === void 0 || activityChangedSince(current, saved.persistable);
19101
+ }
19102
+ linkIsDirty(linkId, current) {
19103
+ const saved = this.linksAtLastSave.get(linkId);
19104
+ if (current === null) return saved !== void 0;
19105
+ return !hasPersistedIdentity(current.proplannerId) || saved === void 0 || linkChangedSince(saved, current);
19106
+ }
19107
+ acknowledgePersistedLookahead(changes, getActivity) {
19108
+ if (!this.trackingEnabled || !this.initialized) return;
19109
+ const acknowledgements = changes.map((change) => {
19110
+ const activityId = typeof change === "string" ? change : String(change.id);
18618
19111
  const current = getActivity(activityId);
18619
19112
  const saved = this.activitiesAtLastSave.get(activityId);
18620
- if (current === null || saved === void 0) continue;
19113
+ const isUnknownPersistedActivity = current === null || saved === void 0;
19114
+ if (isUnknownPersistedActivity) {
19115
+ throw new Error(`unknown persisted activity "${activityId}"`);
19116
+ }
19117
+ return {
19118
+ activityId,
19119
+ acknowledgesLookahead: typeof change === "string" || Boolean(change.fields?.isLookahead),
19120
+ acknowledgesProgress: typeof change !== "string" && Boolean(change.fields?.progress),
19121
+ current,
19122
+ saved
19123
+ };
19124
+ });
19125
+ for (const {
19126
+ activityId,
19127
+ acknowledgesLookahead,
19128
+ acknowledgesProgress,
19129
+ current,
19130
+ saved
19131
+ } of acknowledgements) {
18621
19132
  const persistable = {
18622
19133
  ...saved.persistable,
18623
- isLookahead: current.isLookahead
19134
+ ...acknowledgesLookahead ? { isLookahead: current.isLookahead } : {},
19135
+ ...acknowledgesProgress ? { progress: current.progress } : {}
18624
19136
  };
18625
19137
  this.activitiesAtLastSave.set(activityId, {
18626
19138
  ...saved,
18627
19139
  persistable
18628
19140
  });
18629
- if (current.proplannerId == null || activityChangedSince(current, persistable)) {
19141
+ if (this.initialCalendarRepairIds.has(activityId) || !hasPersistedIdentity(current.proplannerId) || activityChangedSince(current, persistable)) {
18630
19142
  this.dirtyActivityIds.add(activityId);
18631
19143
  } else {
18632
19144
  this.dirtyActivityIds.delete(activityId);
18633
19145
  }
18634
19146
  }
18635
19147
  }
19148
+ hasCheckpointActivityId(activityId) {
19149
+ return this.activitiesAtLastSave.has(activityId);
19150
+ }
19151
+ hasCheckpointLinkId(linkId) {
19152
+ return this.linksAtLastSave.has(linkId);
19153
+ }
18636
19154
  hasUnsavedChanges() {
18637
- return this.initialized && (this.dirtyActivityIds.size > 0 || this.dirtyLinkIds.size > 0);
19155
+ return this.trackingEnabled && this.initialized && (this.dirtyActivityIds.size > 0 || this.dirtyLinkIds.size > 0);
19156
+ }
19157
+ newActivities(getActivity) {
19158
+ if (!this.trackingEnabled || !this.initialized) return [];
19159
+ const activities = [];
19160
+ for (const activityId of this.dirtyActivityIds) {
19161
+ const activity = getActivity(activityId);
19162
+ const isNewActivity = activity !== null && !hasPersistedIdentity(activity.proplannerId);
19163
+ if (isNewActivity) activities.push(activity);
19164
+ }
19165
+ return activities;
19166
+ }
19167
+ newLinks(getLink) {
19168
+ if (!this.trackingEnabled || !this.initialized) return [];
19169
+ const links = [];
19170
+ for (const linkId of this.dirtyLinkIds) {
19171
+ const link = getLink(linkId);
19172
+ const isNewLink = link !== null && !hasPersistedIdentity(link.proplannerId);
19173
+ if (isNewLink) links.push(link);
19174
+ }
19175
+ return links;
19176
+ }
19177
+ deletedActivities(getActivity) {
19178
+ if (!this.trackingEnabled || !this.initialized) return [];
19179
+ const deleted = [];
19180
+ for (const activityId of this.dirtyActivityIds) {
19181
+ const saved = this.activitiesAtLastSave.get(activityId);
19182
+ if (saved && getActivity(activityId) === null) {
19183
+ deleted.push({ ...saved.deletedSnapshot });
19184
+ }
19185
+ }
19186
+ return deleted;
18638
19187
  }
18639
- deletedActivities(activities) {
18640
- if (!this.initialized) return [];
18641
- const currentIds = new Set(
18642
- activities.map((activity) => String(activity.id))
18643
- );
19188
+ deletedLinks(getLink) {
19189
+ if (!this.trackingEnabled || !this.initialized) return [];
18644
19190
  const deleted = [];
18645
- for (const [activityId, saved] of this.activitiesAtLastSave) {
18646
- if (currentIds.has(activityId)) continue;
18647
- deleted.push({ ...saved.deletedSnapshot });
19191
+ for (const linkId of this.dirtyLinkIds) {
19192
+ const saved = this.linksAtLastSave.get(linkId);
19193
+ if (saved && getLink(linkId) === null) deleted.push({ ...saved });
18648
19194
  }
18649
19195
  return deleted;
18650
19196
  }
18651
- deletedLinks(links) {
18652
- if (!this.initialized) return [];
18653
- const currentIds = new Set(links.map((link) => String(link.id)));
18654
- return Array.from(
18655
- this.linksAtLastSave.entries(),
18656
- ([linkId, saved]) => currentIds.has(linkId) ? null : { ...saved }
18657
- ).filter((link) => link !== null);
18658
- }
18659
- modifiedActivities(activities) {
18660
- if (!this.initialized) return [];
18661
- return activities.filter((activity) => {
18662
- if (activity.proplannerId == null) return false;
18663
- if (activity.assignedDefaultCalendar) return true;
18664
- const saved = this.activitiesAtLastSave.get(String(activity.id));
18665
- if (!saved) return true;
18666
- return activityChangedSince(activity, saved.persistable);
18667
- });
19197
+ modifiedActivities(getActivity) {
19198
+ if (!this.trackingEnabled || !this.initialized) return [];
19199
+ const modified = [];
19200
+ for (const activityId of this.dirtyActivityIds) {
19201
+ const activity = getActivity(activityId);
19202
+ if (activity === null || !hasPersistedIdentity(activity.proplannerId)) {
19203
+ continue;
19204
+ }
19205
+ const saved = this.activitiesAtLastSave.get(activityId);
19206
+ if (saved === void 0 || this.initialCalendarRepairIds.has(activityId) || activityChangedSince(activity, saved.persistable)) {
19207
+ modified.push(activity);
19208
+ }
19209
+ }
19210
+ return modified;
18668
19211
  }
18669
- modifiedLinks(links) {
18670
- if (!this.initialized) return [];
18671
- return links.filter((link) => {
18672
- if (link.proplannerId == null) return false;
18673
- const saved = this.linksAtLastSave.get(String(link.id));
18674
- if (!saved) return true;
18675
- return linkChangedSince(saved, link);
18676
- });
19212
+ modifiedLinks(getLink) {
19213
+ if (!this.trackingEnabled || !this.initialized) return [];
19214
+ const modified = [];
19215
+ for (const linkId of this.dirtyLinkIds) {
19216
+ const link = getLink(linkId);
19217
+ if (link === null) continue;
19218
+ if (!hasPersistedIdentity(link.proplannerId)) continue;
19219
+ const saved = this.linksAtLastSave.get(linkId);
19220
+ if (saved === void 0 || linkChangedSince(saved, link)) {
19221
+ modified.push(link);
19222
+ }
19223
+ }
19224
+ return modified;
18677
19225
  }
18678
19226
  clear() {
18679
19227
  this.initialized = false;
18680
19228
  this.dirtyActivityIds.clear();
18681
19229
  this.dirtyLinkIds.clear();
19230
+ this.initialCalendarRepairIds.clear();
18682
19231
  this.linksAtLastSave.clear();
18683
19232
  this.activitiesAtLastSave.clear();
18684
19233
  }
@@ -18732,14 +19281,14 @@ function isAutoSchedulable(progress) {
18732
19281
  }
18733
19282
  function parseConstraintLabel(label, reporter) {
18734
19283
  if (!label) return null;
18735
- const normalized = CONSTRAINT_LABEL_MAP[label.toUpperCase()];
18736
- if (normalized === void 0) {
19284
+ const normalized2 = CONSTRAINT_LABEL_MAP[label.toUpperCase()];
19285
+ if (normalized2 === void 0) {
18737
19286
  reporter.warn(
18738
19287
  `[parseFromBackend] unknown constraint label "${label}" \u2014 defaulting to "asap"`
18739
19288
  );
18740
19289
  return DEFAULT_CONSTRAINT_TYPE;
18741
19290
  }
18742
- return normalized;
19291
+ return normalized2;
18743
19292
  }
18744
19293
  function resolveConstraint(raw, reporter) {
18745
19294
  const constraintType = parseConstraintLabel(raw.constraint, reporter);
@@ -18780,6 +19329,24 @@ function parseIdList(raw) {
18780
19329
  function parseParentId(rawParentId) {
18781
19330
  return rawParentId === ROOT_PARENT_ID ? null : String(rawParentId);
18782
19331
  }
19332
+ function normalizePersistedActivityId(rawId) {
19333
+ if (rawId == null) return null;
19334
+ const isValidIdentity = typeof rawId === "number" && (rawId === 0 || Number.isSafeInteger(rawId) && rawId > 0);
19335
+ if (!isValidIdentity) {
19336
+ throw new Error(`invalid activity proplannerId ${String(rawId)}`);
19337
+ }
19338
+ return rawId;
19339
+ }
19340
+ function parseCriticality(raw, reporter) {
19341
+ if (typeof raw.is_critical === "boolean") return raw.is_critical;
19342
+ reporter.error(
19343
+ "[parseFromBackend] invalid activity is_critical \u2014 defaulting to false",
19344
+ new TypeError(
19345
+ `activity "${raw.unique_id}" received ${typeof raw.is_critical}`
19346
+ )
19347
+ );
19348
+ return false;
19349
+ }
18783
19350
  function parseBaselinePoints(raw, reporter) {
18784
19351
  if (!Array.isArray(raw)) return [];
18785
19352
  return raw.map((point) => normalizeBaselinePoint(point, reporter)).filter((point) => point !== null);
@@ -18800,9 +19367,10 @@ function parseActivity(raw, context) {
18800
19367
  const startDate = parseBackendDate(raw.start_date);
18801
19368
  const endDate = isMilestone ? startDate : parseBackendDate(raw.end_date);
18802
19369
  const durationDays = isMilestone ? 0 : Number(raw.duration);
19370
+ const isCritical = parseCriticality(raw, context.reporter);
18803
19371
  return {
18804
19372
  id: String(raw.unique_id),
18805
- proplannerId: typeof raw.id === "number" ? raw.id : null,
19373
+ proplannerId: normalizePersistedActivityId(raw.id),
18806
19374
  parentId: parseParentId(raw.parent_id),
18807
19375
  name: raw.name,
18808
19376
  description: raw.description ?? "",
@@ -18847,7 +19415,7 @@ function parseActivity(raw, context) {
18847
19415
  startDate,
18848
19416
  endDate
18849
19417
  }),
18850
- isCritical: Boolean(raw.is_critical),
19418
+ isCritical,
18851
19419
  freeSlackHours: null,
18852
19420
  expectedProgress: null,
18853
19421
  expectedProgressBaseline: null,
@@ -19104,6 +19672,14 @@ function resolveLinkType(rawType) {
19104
19672
  if (isLinkTypeCode(rawType)) return rawType;
19105
19673
  return null;
19106
19674
  }
19675
+ function normalizePersistedLinkId(rawId) {
19676
+ if (rawId == null) return void 0;
19677
+ const isValidIdentity = typeof rawId === "number" && (rawId === 0 || Number.isSafeInteger(rawId) && rawId > 0);
19678
+ if (!isValidIdentity) {
19679
+ throw new Error(`invalid link proplannerId ${String(rawId)}`);
19680
+ }
19681
+ return rawId;
19682
+ }
19107
19683
  function normalizeLinks(rawLinks, context) {
19108
19684
  const parsedLinks = [];
19109
19685
  for (const rawLink of rawLinks) {
@@ -19142,16 +19718,18 @@ function parseLink(link, activityIds, hoursPerDay) {
19142
19718
  }
19143
19719
  const ganttIdRaw = link.ganttId;
19144
19720
  const sectorIdRaw = link.sectorId;
19145
- const proplannerIdRaw = link.id;
19721
+ const proplannerId = normalizePersistedLinkId(link.id);
19146
19722
  return {
19147
19723
  id,
19148
19724
  source,
19149
19725
  target,
19150
19726
  type,
19151
19727
  lag: lagDaysToHours(Number(link.lag ?? 0), hoursPerDay),
19728
+ freeSlackHours: null,
19729
+ isCritical: false,
19152
19730
  ...typeof ganttIdRaw === "number" ? { ganttId: ganttIdRaw } : {},
19153
19731
  ...typeof sectorIdRaw === "number" ? { sectorId: sectorIdRaw } : {},
19154
- ...typeof proplannerIdRaw === "number" ? { proplannerId: proplannerIdRaw } : {}
19732
+ ...proplannerId === void 0 ? {} : { proplannerId }
19155
19733
  };
19156
19734
  }
19157
19735
  function dedupeLinksBySignature(links) {
@@ -19278,6 +19856,9 @@ function createActivityIdGenerator(options = {}) {
19278
19856
  }
19279
19857
  }
19280
19858
  return {
19859
+ fork() {
19860
+ return createActivityIdGenerator({ seed: counter });
19861
+ },
19281
19862
  next() {
19282
19863
  counter += 1;
19283
19864
  return String(counter);
@@ -19309,6 +19890,9 @@ function createLinkIdGenerator(options = {}) {
19309
19890
  }
19310
19891
  }
19311
19892
  return {
19893
+ fork() {
19894
+ return createLinkIdGenerator({ seed: counter });
19895
+ },
19312
19896
  next() {
19313
19897
  counter += 1;
19314
19898
  return String(counter);
@@ -19344,6 +19928,9 @@ function createUniqueCorrelativeIdGenerator(options = {}) {
19344
19928
  }
19345
19929
  }
19346
19930
  return {
19931
+ fork() {
19932
+ return createUniqueCorrelativeIdGenerator({ seed: counter });
19933
+ },
19347
19934
  next() {
19348
19935
  counter += 1;
19349
19936
  return String(counter);
@@ -19471,7 +20058,7 @@ function selectGenerationStrategy(customId, useActivityCustomIdAsReference) {
19471
20058
  function cloneStats(stats) {
19472
20059
  return { ...stats, usedSuffixes: new Set(stats.usedSuffixes) };
19473
20060
  }
19474
- var CustomIdTracker = class {
20061
+ var CustomIdTracker = class _CustomIdTracker {
19475
20062
  prefixMap = /* @__PURE__ */ new Map();
19476
20063
  processedIds = /* @__PURE__ */ new Set();
19477
20064
  _journalPrefixBefore = null;
@@ -19532,6 +20119,19 @@ var CustomIdTracker = class {
19532
20119
  this.defaultPrefix = options.defaultPrefix ?? DEFAULT_PREFIX;
19533
20120
  this.suffixIncrement = options.suffixIncrement ?? DEFAULT_SUFFIX_INCREMENT;
19534
20121
  }
20122
+ fork() {
20123
+ const fork = new _CustomIdTracker({
20124
+ defaultPrefix: this.defaultPrefix,
20125
+ suffixIncrement: this.suffixIncrement
20126
+ });
20127
+ for (const [prefix, stats] of this.prefixMap) {
20128
+ fork.prefixMap.set(prefix, cloneStats(stats));
20129
+ }
20130
+ for (const customId of this.processedIds) {
20131
+ fork.processedIds.add(customId);
20132
+ }
20133
+ return fork;
20134
+ }
19535
20135
  extractPrefix(customId) {
19536
20136
  return extractPrefix(customId);
19537
20137
  }
@@ -20138,6 +20738,133 @@ function initializeCore(input) {
20138
20738
  };
20139
20739
  }
20140
20740
 
20741
+ // src/previsualization/canonical-digest.ts
20742
+ function normalized(value) {
20743
+ if (value === void 0) return ["undefined"];
20744
+ if (value === null) return ["null"];
20745
+ if (typeof value === "string") return ["string", value];
20746
+ if (typeof value === "boolean") return ["boolean", value];
20747
+ if (typeof value === "number") {
20748
+ return ["number", Object.is(value, -0) ? "-0" : String(value)];
20749
+ }
20750
+ if (typeof value === "bigint") return ["bigint", String(value)];
20751
+ if (value instanceof Date) return ["date", String(value.getTime())];
20752
+ if (Array.isArray(value)) return ["array", value.map(normalized)];
20753
+ if (value instanceof Map) {
20754
+ return [
20755
+ "map",
20756
+ Array.from(value, ([key, entry]) => [
20757
+ normalized(key),
20758
+ normalized(entry)
20759
+ ]).sort(
20760
+ (left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right))
20761
+ )
20762
+ ];
20763
+ }
20764
+ if (value instanceof Set) {
20765
+ return [
20766
+ "set",
20767
+ Array.from(value, normalized).sort(
20768
+ (left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right))
20769
+ )
20770
+ ];
20771
+ }
20772
+ if (typeof value === "object") {
20773
+ return [
20774
+ "object",
20775
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalized(entry)])
20776
+ ];
20777
+ }
20778
+ throw new TypeError(`Unsupported canonical value: ${typeof value}`);
20779
+ }
20780
+ function serializeCanonicalSchedule(value) {
20781
+ return JSON.stringify(normalized(value));
20782
+ }
20783
+
20784
+ // src/previsualization/reconciliation.ts
20785
+ function entityChanges(target, source) {
20786
+ const targetById = new Map(
20787
+ target.map((entity) => [String(entity.id), entity])
20788
+ );
20789
+ const sourceById = new Map(
20790
+ source.map((entity) => [String(entity.id), entity])
20791
+ );
20792
+ const ids = [.../* @__PURE__ */ new Set([...targetById.keys(), ...sourceById.keys()])].sort();
20793
+ const changes = [];
20794
+ for (const id of ids) {
20795
+ const before = sourceById.get(id);
20796
+ const after = targetById.get(id);
20797
+ if (after === void 0) {
20798
+ changes.push({ id, kind: "deleted", after: null });
20799
+ continue;
20800
+ }
20801
+ if (before === void 0) {
20802
+ changes.push({ id, kind: "created", after: cloneDomainValue(after) });
20803
+ continue;
20804
+ }
20805
+ const fields = changedFields(before, after);
20806
+ if (Object.keys(fields).length > 0) {
20807
+ changes.push({
20808
+ id,
20809
+ kind: "updated",
20810
+ fields,
20811
+ after: cloneDomainValue(after)
20812
+ });
20813
+ }
20814
+ }
20815
+ return changes;
20816
+ }
20817
+ function changedFields(before, after) {
20818
+ const fields = {};
20819
+ const keys = [
20820
+ .../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])
20821
+ ].sort();
20822
+ for (const key of keys) {
20823
+ if (serializeCanonicalSchedule(before[key]) !== serializeCanonicalSchedule(after[key])) {
20824
+ fields[key] = {
20825
+ before: cloneDomainValue(before[key]),
20826
+ after: cloneDomainValue(after[key])
20827
+ };
20828
+ }
20829
+ }
20830
+ return fields;
20831
+ }
20832
+ function viewStateChanges(target, source) {
20833
+ const selectedBefore = new Set(source.selectedIds.map(String));
20834
+ const selectedAfter = new Set(target.selectedIds.map(String));
20835
+ const hiddenBefore = new Set(source.hiddenIds.map(String));
20836
+ const hiddenAfter = new Set(target.hiddenIds.map(String));
20837
+ const targetIds = target.activities.map(({ id }) => String(id)).sort();
20838
+ const changes = [];
20839
+ for (const activityId of targetIds) {
20840
+ const checkedBefore = selectedBefore.has(activityId);
20841
+ const checkedAfter = selectedAfter.has(activityId);
20842
+ const visibleBefore = !hiddenBefore.has(activityId);
20843
+ const visibleAfter = !hiddenAfter.has(activityId);
20844
+ if (checkedBefore === checkedAfter && visibleBefore === visibleAfter)
20845
+ continue;
20846
+ changes.push({
20847
+ activityId,
20848
+ ...checkedBefore === checkedAfter ? {} : { checked: { before: checkedBefore, after: checkedAfter } },
20849
+ ...visibleBefore === visibleAfter ? {} : { visible: { before: visibleBefore, after: visibleAfter } }
20850
+ });
20851
+ }
20852
+ return changes;
20853
+ }
20854
+ function createProjectionChangeSet(target, source) {
20855
+ const orderChanged = serializeCanonicalSchedule(target.order) !== serializeCanonicalSchedule(source.order);
20856
+ return {
20857
+ source: { kind: "init" },
20858
+ activities: entityChanges(target.activities, source.activities),
20859
+ links: entityChanges(target.links, source.links),
20860
+ calendars: entityChanges(target.calendars, source.calendars),
20861
+ trackingEvents: [],
20862
+ effects: [],
20863
+ viewState: viewStateChanges(target, source),
20864
+ ...orderChanged ? { order: cloneDomainValue(target.order) } : {}
20865
+ };
20866
+ }
20867
+
20141
20868
  // src/propagations/upward/date-gesture-preview.ts
20142
20869
  var emptyFormula = () => ({
20143
20870
  constantStart: null,
@@ -20283,7 +21010,10 @@ function evaluateEnd(formula, proposedEdgeTimeMs) {
20283
21010
  }
20284
21011
 
20285
21012
  // src/init/schedule-core.ts
20286
- var PREVIEW_DISPATCH_KINDS = /* @__PURE__ */ new Set(["inline-edit", "bulk-edit", "dates-batch"]);
21013
+ var FORK_SOURCE = /* @__PURE__ */ Symbol("ScheduleCore.fork.source");
21014
+ function acknowledgesPersistence(action) {
21015
+ return action.kind === "persistence-acknowledge" || action.kind === "activity-lookahead-sync" && action.acknowledgePersistedLookahead === true;
21016
+ }
20287
21017
  function changesRequireFilterProjectionRebuild(changes) {
20288
21018
  for (const change of changes.activities) {
20289
21019
  if (change.kind === "deleted") return true;
@@ -20307,10 +21037,11 @@ function isCurrentFilterProjection(projection, filter, context) {
20307
21037
  const hasCurrentLocale = projection.context.locale === context.locale;
20308
21038
  return hasCurrentHours && hasCurrentLocale;
20309
21039
  }
20310
- var ScheduleCore = class {
21040
+ var ScheduleCore = class _ScheduleCore {
20311
21041
  _activityClipboard = { value: null };
20312
21042
  _status = SCHEDULE_CORE_STATUS.READY;
20313
21043
  coreRuntime;
21044
+ _isFork;
20314
21045
  _undo = new UndoRecorder();
20315
21046
  _opQueue = Promise.resolve();
20316
21047
  _scheduleRevision = 0;
@@ -20341,6 +21072,42 @@ var ScheduleCore = class {
20341
21072
  _criticalPathRevision = -1;
20342
21073
  _activeCriticalPath = null;
20343
21074
  constructor(input) {
21075
+ if (FORK_SOURCE in input) {
21076
+ this._isFork = true;
21077
+ const source = input[FORK_SOURCE];
21078
+ const state = source.coreRuntime.state.fork();
21079
+ this.coreRuntime = {
21080
+ state,
21081
+ scheduler: new AutoScheduler(state, source.coreRuntime.reporter),
21082
+ sector: cloneDomainValue(source.coreRuntime.sector),
21083
+ activityIdGenerator: source.coreRuntime.activityIdGenerator.fork(),
21084
+ linkIdGenerator: source.coreRuntime.linkIdGenerator.fork(),
21085
+ uniqueCorrelativeIdGenerator: source.coreRuntime.uniqueCorrelativeIdGenerator.fork(),
21086
+ customIdTracker: source.coreRuntime.customIdTracker.fork(),
21087
+ calendars: new Map(
21088
+ Array.from(source.coreRuntime.calendars, ([id, calendar]) => [
21089
+ id,
21090
+ cloneDomainValue(calendar)
21091
+ ])
21092
+ ),
21093
+ baseCalendars: cloneDomainValue(source.coreRuntime.baseCalendars),
21094
+ clock: source.coreRuntime.clock,
21095
+ reporter: source.coreRuntime.reporter
21096
+ };
21097
+ this._activityClipboard.value = cloneDomainValue(
21098
+ source._activityClipboard.value
21099
+ );
21100
+ this._undo = source._undo.fork();
21101
+ this._saveTracker = source._saveTracker.fork();
21102
+ this._scheduleRevision = source._scheduleRevision;
21103
+ this._pendingScheduleRevision = source._scheduleRevision;
21104
+ this._criticalPathRevision = source._criticalPathRevision;
21105
+ this._initPromise = Promise.resolve();
21106
+ this._criticalPathReady = Promise.resolve(null);
21107
+ this._ready = this._initPromise;
21108
+ return;
21109
+ }
21110
+ this._isFork = false;
20344
21111
  const initialized = initializeCore(input);
20345
21112
  this.coreRuntime = {
20346
21113
  state: initialized.state,
@@ -20377,6 +21144,15 @@ var ScheduleCore = class {
20377
21144
  get ready() {
20378
21145
  return this._ready;
20379
21146
  }
21147
+ fork() {
21148
+ return this._enqueue(async () => {
21149
+ await this._ready;
21150
+ this.assertReady();
21151
+ return new _ScheduleCore({
21152
+ [FORK_SOURCE]: this
21153
+ });
21154
+ });
21155
+ }
20380
21156
  get criticalPathReady() {
20381
21157
  return this._criticalPathReady;
20382
21158
  }
@@ -20429,6 +21205,21 @@ var ScheduleCore = class {
20429
21205
  this.assertReady();
20430
21206
  return readLink(this.coreRuntime.state, id);
20431
21207
  }
21208
+ getLinksForActivityView(activityId) {
21209
+ this.assertReady();
21210
+ const state = this.coreRuntime.state;
21211
+ const links = [];
21212
+ const seen = /* @__PURE__ */ new Set();
21213
+ const append = (linkId) => {
21214
+ if (seen.has(linkId)) return;
21215
+ seen.add(linkId);
21216
+ const link = readLink(state, linkId);
21217
+ if (link) links.push(link);
21218
+ };
21219
+ for (const linkId of state.getOutgoingLinkIds(activityId)) append(linkId);
21220
+ for (const linkId of state.getIncomingLinkIds(activityId)) append(linkId);
21221
+ return links;
21222
+ }
20432
21223
  getAllLinksView() {
20433
21224
  this.assertReady();
20434
21225
  return readAllLinks(this.coreRuntime.state);
@@ -20526,91 +21317,65 @@ var ScheduleCore = class {
20526
21317
  }
20527
21318
  getModifiedLinks() {
20528
21319
  this.assertReady();
20529
- return this._saveTracker.modifiedLinks(this.getAllLinksView());
21320
+ return this._saveTracker.modifiedLinks((linkId) => this.coreRuntime.state.getLink(linkId)).map(cloneDomainValue);
21321
+ }
21322
+ getNewLinks() {
21323
+ this.assertReady();
21324
+ return this._saveTracker.newLinks((linkId) => this.coreRuntime.state.getLink(linkId)).map(cloneDomainValue);
20530
21325
  }
20531
21326
  getModifiedActivities() {
20532
21327
  this.assertReady();
20533
- return this._saveTracker.modifiedActivities(this.coreRuntime.state.getAllActivities()).map(cloneDomainValue);
21328
+ return this._saveTracker.modifiedActivities(
21329
+ (activityId) => this.coreRuntime.state.getActivity(activityId)
21330
+ ).map(cloneDomainValue);
21331
+ }
21332
+ getNewActivities() {
21333
+ this.assertReady();
21334
+ return this._saveTracker.newActivities(
21335
+ (activityId) => this.coreRuntime.state.getActivity(activityId)
21336
+ ).map(cloneDomainValue);
20534
21337
  }
20535
21338
  hasUnsavedChanges() {
20536
21339
  this.assertReady();
20537
21340
  return this._saveTracker.hasUnsavedChanges();
20538
21341
  }
20539
- /**
20540
- * Read-only view of the internal schedule revision. It moves once per
20541
- * substantive CANONICAL mutation (dispatch, undo, redo). It does NOT move
20542
- * for a preview dispatch nor for `applyPreviewInverse`: comparing the value
20543
- * captured in `PreviewDispatchInfo.revision` against the current one
20544
- * answers "did the schedule change while the preview was open".
20545
- */
21342
+ /** Read-only revision; moves once per substantive mutation. */
20546
21343
  getScheduleRevision() {
20547
21344
  return this._scheduleRevision;
20548
21345
  }
20549
- /**
20550
- * Exit channel for a preview dispatch (`DispatchOptions.preview`): applies
20551
- * the inverse ChangeSet the preview returned, restoring the canonical
20552
- * state exactly. Like the preview itself it leaves no session trace: no
20553
- * save-tracker dirty, no undo entry, no effects, and the schedule revision
20554
- * does not move (see `getScheduleRevision`).
20555
- *
20556
- * Returns the ChangeSet to project to the view layer: the same changes as
20557
- * `inverse`, but with `after` images re-read from the restored state (the
20558
- * ones inside `inverse` were captured while the preview edit was applied).
20559
- */
20560
- applyPreviewInverse(inverse) {
20561
- return this._enqueue(async () => {
20562
- this.assertReady();
20563
- const state = this.coreRuntime.state;
20564
- const afterSnap = /* @__PURE__ */ new Map();
20565
- for (const change of inverse.activities) {
20566
- if (change.kind !== "deleted" && change.after) {
20567
- afterSnap.set(
20568
- String(change.id),
20569
- structuredCloneActivity(change.after)
20570
- );
20571
- }
20572
- }
20573
- const afterLinks = /* @__PURE__ */ new Map();
20574
- for (const change of inverse.links) {
20575
- if (change.kind !== "deleted" && change.after) {
20576
- afterLinks.set(String(change.id), { ...change.after });
20577
- }
20578
- }
20579
- const entry = buildUndoEntry(
20580
- inverse,
20581
- void 0,
20582
- void 0,
20583
- afterSnap,
20584
- afterLinks
20585
- );
20586
- applyRedo(state, entry);
20587
- if (needsResync(entry)) this._resyncCustomIdTrackerFromModel();
20588
- const restored = {
20589
- ...inverse,
20590
- activities: inverse.activities.map((change) => {
20591
- if (change.kind === "deleted") return { ...change, after: null };
20592
- const live = state.getActivity(String(change.id));
20593
- return {
20594
- ...change,
20595
- after: live ? structuredCloneActivity(live) : null
20596
- };
20597
- }),
20598
- links: inverse.links.map((change) => {
20599
- if (change.kind === "deleted") return { ...change, after: null };
20600
- const live = state.getLink(String(change.id));
20601
- return { ...change, after: live ? snapshotToLink(live) : null };
20602
- })
20603
- };
20604
- return this._reapplyViewState(restored) ?? restored;
21346
+ getCanonicalDigest() {
21347
+ this.assertReady();
21348
+ const byId = (left, right) => String(left.id).localeCompare(String(right.id));
21349
+ return serializeCanonicalSchedule({
21350
+ sector: this.getSector(),
21351
+ activities: [...this.getAllActivitiesView()].sort(byId),
21352
+ links: [...this.getAllLinksView()].sort(byId),
21353
+ calendars: [...this.getAllCalendars()].sort(byId),
21354
+ baseCalendars: [...this.getBaseCalendars()].sort(byId)
21355
+ });
21356
+ }
21357
+ createProjectionChangeSetFrom(source) {
21358
+ this.assertReady();
21359
+ source.assertReady();
21360
+ const projectionOf = (core) => ({
21361
+ activities: core.getAllActivitiesView(),
21362
+ links: core.getAllLinksView(),
21363
+ calendars: core.getAllCalendars(),
21364
+ selectedIds: core.getSelectedActivityIds(),
21365
+ hiddenIds: core.getHiddenActivityIds(),
21366
+ order: collectBranchOrder(core.coreRuntime.state)
20605
21367
  });
21368
+ return createProjectionChangeSet(projectionOf(this), projectionOf(source));
20606
21369
  }
20607
21370
  getDeletedActivitiesSinceLastSave() {
20608
21371
  this.assertReady();
20609
- return this._saveTracker.deletedActivities(this.coreRuntime.state.getAllActivities()).map(cloneDomainValue);
21372
+ return this._saveTracker.deletedActivities(
21373
+ (activityId) => this.coreRuntime.state.getActivity(activityId)
21374
+ ).map(cloneDomainValue);
20610
21375
  }
20611
21376
  getDeletedLinksSinceLastSave() {
20612
21377
  this.assertReady();
20613
- return this._saveTracker.deletedLinks(this.getAllLinksView()).map(cloneDomainValue);
21378
+ return this._saveTracker.deletedLinks((linkId) => this.coreRuntime.state.getLink(linkId)).map(cloneDomainValue);
20614
21379
  }
20615
21380
  _enqueue(work) {
20616
21381
  const workPromise = this._opQueue.then(work, work);
@@ -20686,11 +21451,11 @@ var ScheduleCore = class {
20686
21451
  });
20687
21452
  }
20688
21453
  async dispatch(action, options = {}) {
20689
- const admitted = options.preview === true ? false : this._beginScheduleMutation(action);
21454
+ const admitted = this._beginScheduleMutation(action);
20690
21455
  return this._enqueue(() => this._dispatchInner(action, options, admitted));
20691
21456
  }
20692
21457
  async applyActivityBatch(action, options = {}) {
20693
- const admitted = options.preview === true ? false : this._beginScheduleMutation(action);
21458
+ const admitted = this._beginScheduleMutation(action);
20694
21459
  return this._enqueue(() => this._dispatchInner(action, options, admitted));
20695
21460
  }
20696
21461
  async _dispatchInner(action, options = {}, admitted = false) {
@@ -20705,23 +21470,20 @@ var ScheduleCore = class {
20705
21470
  }
20706
21471
  async _runDispatch(action, options) {
20707
21472
  this.assertReady();
20708
- if (options.preview === true && !PREVIEW_DISPATCH_KINDS.has(action.kind)) {
21473
+ if (this._isFork && acknowledgesPersistence(action)) {
20709
21474
  return {
20710
21475
  result: {
20711
21476
  ok: false,
20712
- reason: REJECTION_REASON.PREVIEW_UNSUPPORTED_ACTION
21477
+ reason: REJECTION_REASON.SANDBOX_PERSISTENCE_FORBIDDEN
20713
21478
  },
20714
21479
  changedState: false
20715
21480
  };
20716
21481
  }
20717
21482
  const pipelineContext = buildPipelineContext(this.coreRuntime.state, {
20718
21483
  hoursPerDay: this.coreRuntime.sector.hoursPerDay,
20719
- dateFormat: this.coreRuntime.sector.dateFormat,
20720
- // ISSUE-057 (CP6): sin default silencioso 'days'; la clave se omite si
20721
- // no se declaró (exactOptionalPropertyTypes).
20722
- ...options.inputUnit !== void 0 ? { inputUnit: options.inputUnit } : {}
21484
+ dateFormat: this.coreRuntime.sector.dateFormat
20723
21485
  });
20724
- const historyPolicy = getDispatchHistoryPolicy(action, options);
21486
+ const historyPolicy = getDispatchHistoryPolicy(action);
20725
21487
  const generatorSnapshot = [
20726
21488
  this.coreRuntime.activityIdGenerator.snapshot(),
20727
21489
  this.coreRuntime.linkIdGenerator.snapshot(),
@@ -20739,6 +21501,8 @@ var ScheduleCore = class {
20739
21501
  calendars: this.coreRuntime.calendars,
20740
21502
  activityIdGen: this.coreRuntime.activityIdGenerator,
20741
21503
  linkIdGen: this.coreRuntime.linkIdGenerator,
21504
+ isActivityIdReserved: (activityId) => this._saveTracker.hasCheckpointActivityId(activityId),
21505
+ isLinkIdReserved: (linkId) => this._saveTracker.hasCheckpointLinkId(linkId),
20742
21506
  uidGen: this.coreRuntime.uniqueCorrelativeIdGenerator,
20743
21507
  customIdTracker: this.coreRuntime.customIdTracker,
20744
21508
  reporter: this.coreRuntime.reporter,
@@ -20775,17 +21539,20 @@ var ScheduleCore = class {
20775
21539
  }
20776
21540
  if (result.ok) {
20777
21541
  if (historyPolicy === "clear-on-success") {
20778
- this._saveTracker.snapshotActivities(
20779
- this.coreRuntime.state.getAllActivities()
21542
+ this._saveTracker.acknowledge(
21543
+ this.coreRuntime.state.getAllActivities(),
21544
+ this.coreRuntime.state.getAllLinks()
20780
21545
  );
20781
- this._saveTracker.snapshotLinks(this.getAllLinksView());
20782
21546
  this._undo.clear();
20783
21547
  } else if (action.kind === "activity-lookahead-sync" && action.acknowledgePersistedLookahead === true) {
20784
21548
  this._saveTracker.acknowledgePersistedLookahead(
20785
- action.activityIds,
21549
+ result.changes.activities,
20786
21550
  (activityId) => this.coreRuntime.state.getActivity(activityId)
20787
21551
  );
20788
- } else if (options.preview !== true) {
21552
+ if (action.persistedProgress !== void 0) {
21553
+ this._undo.clear();
21554
+ }
21555
+ } else {
20789
21556
  this._saveTracker.recordChanges(
20790
21557
  result.changes.activities,
20791
21558
  result.changes.links,
@@ -20796,20 +21563,6 @@ var ScheduleCore = class {
20796
21563
  }
20797
21564
  result = this._withReappliedViewState(action, result);
20798
21565
  if (!result.ok) return { result, changedState: false };
20799
- if (options.preview === true) {
20800
- const inverse = buildInverseChangeSet(
20801
- this.coreRuntime.state,
20802
- buildUndoEntry(buildHistoryChangeSet(result.changes)),
20803
- "before"
20804
- );
20805
- return {
20806
- result: {
20807
- ...toPublicDispatchResult(result),
20808
- preview: { inverse, revision: this._scheduleRevision }
20809
- },
20810
- changedState: false
20811
- };
20812
- }
20813
21566
  if (dispatchChangesSchedulingState(action) && changeSetIsSubstantive(result.changes)) {
20814
21567
  this._recordScheduleMutation(
20815
21568
  options.skipAutoSchedule !== true && options.skipCriticalPath !== true && resolveRunCriticalPath(action)
@@ -21062,13 +21815,20 @@ var ScheduleCore = class {
21062
21815
  this.coreRuntime.customIdTracker.beginCustomIdTransaction();
21063
21816
  let changes;
21064
21817
  try {
21065
- for (const [activityId, fields] of calculatedFields) {
21818
+ for (const [activityId, fields] of calculatedFields.activities) {
21066
21819
  setCriticalPathFieldsIfChanged(
21067
21820
  this.coreRuntime.state,
21068
21821
  activityId,
21069
21822
  fields
21070
21823
  );
21071
21824
  }
21825
+ for (const [linkId, fields] of calculatedFields.links) {
21826
+ setCriticalPathLinkFieldsIfChanged(
21827
+ this.coreRuntime.state,
21828
+ linkId,
21829
+ fields
21830
+ );
21831
+ }
21072
21832
  changes = await assembleChangeSet(this.coreRuntime.state, {
21073
21833
  source: { kind: "init" },
21074
21834
  beforeSnap: /* @__PURE__ */ new Map(),
@@ -21350,7 +22110,6 @@ var DISPATCH_ACTION_KIND = {
21350
22110
  FILTER_SET: "filter-set",
21351
22111
  SORT_SET: "sort-set",
21352
22112
  SIR_SYNC: "sir-sync",
21353
- SIR_APPROVE: "sir-approve",
21354
22113
  ACTIVITY_LOOKAHEAD_SYNC: "activity-lookahead-sync"
21355
22114
  };
21356
22115
  var KIND_CATALOG_COVERS_UNION = {
@@ -21380,7 +22139,6 @@ var KIND_CATALOG_COVERS_UNION = {
21380
22139
  [DISPATCH_ACTION_KIND.FILTER_SET]: true,
21381
22140
  [DISPATCH_ACTION_KIND.SORT_SET]: true,
21382
22141
  [DISPATCH_ACTION_KIND.SIR_SYNC]: true,
21383
- [DISPATCH_ACTION_KIND.SIR_APPROVE]: true,
21384
22142
  [DISPATCH_ACTION_KIND.ACTIVITY_LOOKAHEAD_SYNC]: true
21385
22143
  };
21386
22144
  var DISPATCH_ACTION_KINDS = Object.keys(KIND_CATALOG_COVERS_UNION);