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