@outbuild-company/schedule-core 1.32.0 → 1.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -344,6 +344,7 @@ var DISPATCH_ACTION_KIND = {
344
344
  CELL_CLIPBOARD_CLEAR: "cell-clipboard-clear",
345
345
  CELL_PASTE_COPIED: "cell-paste-copied",
346
346
  PERSISTENCE_ACKNOWLEDGE: "persistence-acknowledge",
347
+ PERSISTENCE_REBASE: "persistence-rebase",
347
348
  BASELINE_APPLY: "baseline-apply",
348
349
  PONDERATOR_CRITERION_SET: "ponderator-criterion-set",
349
350
  STATUS_CRITERIA_SET: "status-criteria-set",
@@ -789,6 +790,10 @@ function createScheduleNamespaces(engine) {
789
790
  acknowledge: (checkpoint) => engine.dispatch({
790
791
  kind: DISPATCH_ACTION_KIND.PERSISTENCE_ACKNOWLEDGE,
791
792
  checkpoint
793
+ }),
794
+ rebase: (remote) => engine.dispatch({
795
+ kind: DISPATCH_ACTION_KIND.PERSISTENCE_REBASE,
796
+ remote
792
797
  })
793
798
  },
794
799
  history: {
@@ -2445,6 +2450,7 @@ var ACTIVITY_PROPERTY = {
2445
2450
  DURATION_HOURS: "durationHours",
2446
2451
  CONSTRAINT_TYPE: "constraintType",
2447
2452
  CONSTRAINT_DATE: "constraintDate",
2453
+ AUTO_SCHEDULING: "autoScheduling",
2448
2454
  CALENDAR_ID: "calendarId",
2449
2455
  PROGRESS: "progress",
2450
2456
  COST: "cost",
@@ -2452,7 +2458,9 @@ var ACTIVITY_PROPERTY = {
2452
2458
  REAL_COST: "realCost",
2453
2459
  WORK_HOURS: "workHours",
2454
2460
  REAL_WORK_HOURS: "realWorkHours",
2461
+ PONDERATOR: "ponderator",
2455
2462
  CORRELATIVE_ID: "correlativeId",
2463
+ UNIQUE_CORRELATIVE_ID: "uniqueCorrelativeId",
2456
2464
  CUSTOM_ID: "customId",
2457
2465
  NEW_ACTIVITIES_ARRAY: "newActivityIds",
2458
2466
  HAS_NEW_ACTIVITIES: "hasNewActivities",
@@ -6849,6 +6857,21 @@ function findPipelineForColumn(columnName) {
6849
6857
  }
6850
6858
 
6851
6859
  // src/dispatch/types.ts
6860
+ var PERSISTENCE_REBASE_CONFLICT_KIND = {
6861
+ FIELD: "field",
6862
+ DELETED_REMOTELY: "deleted-remotely",
6863
+ DELETED_LOCALLY: "deleted-locally",
6864
+ PARENT_REMOVED: "parent-removed",
6865
+ BECAME_PARENT_REMOTELY: "became-parent-remotely",
6866
+ BECAME_PARENT_LOCALLY: "became-parent-locally",
6867
+ DUPLICATE_LINK: "duplicate-link",
6868
+ ORDER: "order",
6869
+ LINK_CYCLE: "link-cycle"
6870
+ };
6871
+ var PERSISTENCE_REBASE_ENTITY = {
6872
+ ACTIVITY: "activity",
6873
+ LINK: "link"
6874
+ };
6852
6875
  var ACTIVITY_BATCH_MODE = {
6853
6876
  APPEND: "append",
6854
6877
  REPLACE: "replace"
@@ -18894,284 +18917,2184 @@ function normalizeBaselinePoint(point, reporter, diagnostic2, isMilestone = fals
18894
18917
  };
18895
18918
  }
18896
18919
 
18897
- // src/baselines/apply-baseline-points.ts
18898
- function applyBaselinePoints(adapter, points, reporter, recordDiagnostic = noopLoadDiagnosticRecorder) {
18899
- const idByProplanner = /* @__PURE__ */ new Map();
18900
- adapter.forEachActivity((a, id) => {
18901
- const pid = a.proplannerId;
18902
- if (typeof pid === "number") idByProplanner.set(pid, id);
18920
+ // src/boundary/backend/constants.ts
18921
+ var DAY_MS = 24 * 60 * 60 * 1e3;
18922
+ var EXCEPTION_DATE_FORMAT_REGEX = /^(\d{4})[/-](\d{1,2})[/-](\d{1,2})/;
18923
+ var EXCEPTION_TIME_FORMAT_REGEX = /^\d{4}[/-]\d{1,2}[/-]\d{1,2}[ T](\d{1,2}):(\d{2})/;
18924
+ var WEEKDAYS_PER_WEEK = 7;
18925
+ var EXCEPTION_EVERY_TYPE = {
18926
+ DAILY: "DAILY",
18927
+ WEEKLY: "WEEKLY",
18928
+ MONTHLY: "MONTHLY",
18929
+ YEARLY: "YEARLY"
18930
+ };
18931
+ var MONTHS_PER_YEAR = 12;
18932
+ var CALENDAR_FALLBACK_WORKING_DAYS = [false, true, true, true, true, true, false];
18933
+ var WORKING_DAY_TRUE_VALUES = /* @__PURE__ */ new Set([
18934
+ "1",
18935
+ "true"
18936
+ ]);
18937
+ var DEFAULT_ACTIVITY_CRETER = ACTIVITY_PONDERATOR_CRITERION.DURATION;
18938
+ var DEFAULT_CONSTRAINT_TYPE = CONSTRAINT_TYPE.ASAP;
18939
+ var CONSTRAINT_LABEL_MAP = {
18940
+ ASAP: CONSTRAINT_TYPE.ASAP,
18941
+ ALAP: CONSTRAINT_TYPE.ALAP,
18942
+ SNET: CONSTRAINT_TYPE.SNET,
18943
+ SNLT: CONSTRAINT_TYPE.SNLT,
18944
+ FNET: CONSTRAINT_TYPE.FNET,
18945
+ FNLT: CONSTRAINT_TYPE.FNLT,
18946
+ MSO: CONSTRAINT_TYPE.MSO,
18947
+ MFO: CONSTRAINT_TYPE.MFO,
18948
+ "AS SOON AS POSSIBLE": CONSTRAINT_TYPE.ASAP,
18949
+ "AS LATE AS POSSIBLE": CONSTRAINT_TYPE.ALAP,
18950
+ "START NO EARLIER THAN": CONSTRAINT_TYPE.SNET,
18951
+ "START NO LATER THAN": CONSTRAINT_TYPE.SNLT,
18952
+ "FINISH NO EARLIER THAN": CONSTRAINT_TYPE.FNET,
18953
+ "FINISH NO LATER THAN": CONSTRAINT_TYPE.FNLT,
18954
+ "MUST START ON": CONSTRAINT_TYPE.MSO,
18955
+ "MUST FINISH ON": CONSTRAINT_TYPE.MFO
18956
+ };
18957
+
18958
+ // src/boundary/backend/normalize-activity.ts
18959
+ function normalizeActivities(rawActivities, context) {
18960
+ const hiddenIds = [];
18961
+ const activities = rawActivities.map((raw) => {
18962
+ const activity = parseActivity(raw, context);
18963
+ if (raw.should_be_showed === false) hiddenIds.push(activity.id);
18964
+ return activity;
18903
18965
  });
18904
- const grouped = /* @__PURE__ */ new Map();
18905
- for (const p of points) {
18906
- const activityId = p.activityId;
18907
- if (typeof activityId !== "number") {
18908
- recordDiagnostic(
18909
- LOAD_DIAGNOSTIC_CODE.INVALID_BASELINE_POINT,
18910
- String(activityId ?? "unknown")
18911
- );
18912
- continue;
18913
- }
18914
- const coreId = idByProplanner.get(activityId);
18915
- if (coreId === void 0) {
18916
- recordDiagnostic(
18917
- LOAD_DIAGNOSTIC_CODE.INVALID_BASELINE_POINT,
18918
- String(activityId)
18919
- );
18920
- continue;
18921
- }
18922
- const arr = grouped.get(coreId);
18923
- if (arr) arr.push(p);
18924
- else grouped.set(coreId, [p]);
18925
- }
18926
- const changed2 = [];
18927
- for (const [coreId, group] of grouped) {
18928
- const isMilestone = adapter.getActivity(coreId)?.type === ACTIVITY_TYPE.MILESTONE;
18929
- adapter.setActivityField(
18930
- coreId,
18931
- "baselinePoints",
18932
- group.map(
18933
- (point) => normalizeBaselinePoint(
18934
- point,
18935
- reporter,
18936
- {
18937
- record: recordDiagnostic,
18938
- sampleId: coreId
18939
- },
18940
- isMilestone
18941
- )
18942
- ).filter((point) => point !== null)
18966
+ forceRootProjectToRoot(activities);
18967
+ const activityIds = new Set(activities.map((activity) => activity.id));
18968
+ reparentOrphansToRoot(
18969
+ activities,
18970
+ activityIds,
18971
+ context.reporter,
18972
+ context.recordDiagnostic
18973
+ );
18974
+ return { activities, activityIds, hiddenIds };
18975
+ }
18976
+ function parseConstraintLabel(label, reporter, recordDiagnostic, activityId) {
18977
+ if (!label) return null;
18978
+ const normalized2 = CONSTRAINT_LABEL_MAP[label.toUpperCase()];
18979
+ if (normalized2 === void 0) {
18980
+ recordDiagnostic(LOAD_DIAGNOSTIC_CODE.UNKNOWN_CONSTRAINT, activityId);
18981
+ reporter.warn(
18982
+ `[parseFromBackend] unknown constraint label "${label}" \u2014 defaulting to "asap"`
18943
18983
  );
18944
- changed2.push(coreId);
18984
+ return DEFAULT_CONSTRAINT_TYPE;
18945
18985
  }
18946
- adapter.forEachActivity((a, id) => {
18947
- if (grouped.has(id)) return;
18948
- const existing = a.baselinePoints;
18949
- if (Array.isArray(existing) && existing.length > 0) {
18950
- adapter.setActivityField(id, "baselinePoints", []);
18951
- changed2.push(id);
18952
- }
18953
- });
18954
- return changed2;
18986
+ return normalized2;
18955
18987
  }
18956
-
18957
- // src/baselines/baseline-snapshot.ts
18958
- function buildBaselineSnapshot(activity) {
18959
- const active = getActiveBaseline(activity);
18960
- if (!active) return null;
18961
- return {
18962
- startDate: active.startDate,
18963
- endDate: active.endDate,
18964
- durationDays: active.durationDays,
18965
- cost: active.cost,
18966
- workHours: active.workHours
18967
- };
18988
+ function resolveConstraint(raw, reporter, recordDiagnostic) {
18989
+ const constraintType = parseConstraintLabel(
18990
+ raw.constraint,
18991
+ reporter,
18992
+ recordDiagnostic,
18993
+ String(raw.unique_id)
18994
+ );
18995
+ const constraintTakesDate = constraintType !== CONSTRAINT_TYPE.ASAP && constraintType !== CONSTRAINT_TYPE.ALAP;
18996
+ const constraintDate = constraintTakesDate && raw.constraint_date ? parseBackendDate(raw.constraint_date) : null;
18997
+ return { constraintType, constraintDate };
18968
18998
  }
18969
- function emitBaselineSnapshotColumns(state) {
18970
- const changed2 = [];
18971
- state.forEachActivity((activity, id) => {
18972
- const snapshot = buildBaselineSnapshot(activity);
18973
- if (!snapshot) {
18974
- if (activity.baselineSnapshot !== null) {
18975
- state.setActivityField(id, "baselineSnapshot", null);
18976
- changed2.push(id);
18977
- }
18978
- return;
18979
- }
18980
- state.setActivityField(id, "baselineSnapshot", snapshot);
18981
- changed2.push(id);
18982
- });
18983
- return changed2;
18999
+ function resolveCalendarId(rawCalendarId, knownCalendarIds, defaultCalendarId) {
19000
+ if (rawCalendarId == null) return defaultCalendarId;
19001
+ const coerced = String(rawCalendarId);
19002
+ return knownCalendarIds.has(coerced) ? coerced : defaultCalendarId;
18984
19003
  }
18985
-
18986
- // src/baselines/apply-baseline-overlay.ts
18987
- function applyBaselineOverlay(state, points, now, reporter, defaultBaseCalendarId = null, recordDiagnostic = noopLoadDiagnosticRecorder) {
18988
- const changed2 = /* @__PURE__ */ new Set();
18989
- for (const id of applyBaselinePoints(
18990
- state,
18991
- points,
18992
- reporter,
18993
- recordDiagnostic
18994
- )) {
18995
- changed2.add(id);
19004
+ function parseNewActivityIds(raw) {
19005
+ if (raw == null || raw === "") return [];
19006
+ if (Array.isArray(raw)) return raw.map(String);
19007
+ if (typeof raw !== "string") return [];
19008
+ try {
19009
+ const parsed2 = JSON.parse(raw);
19010
+ return Array.isArray(parsed2) ? parsed2.map(String) : [];
19011
+ } catch {
19012
+ return [];
18996
19013
  }
18997
- if (now) {
18998
- for (const id of runExpectedProgressBase(state, now, defaultBaseCalendarId))
18999
- changed2.add(id);
19014
+ }
19015
+ function hasUsableId(entry) {
19016
+ if (typeof entry !== "object" || entry === null) return false;
19017
+ if (!("id" in entry)) return false;
19018
+ const value = entry.id;
19019
+ return typeof value === "string" || typeof value === "number";
19020
+ }
19021
+ function parseIdList(raw) {
19022
+ if (!Array.isArray(raw)) return [];
19023
+ const ids = [];
19024
+ for (const entry of raw) {
19025
+ if (hasUsableId(entry)) ids.push(String(entry.id));
19000
19026
  }
19001
- for (const id of emitBaselineSnapshotColumns(state)) changed2.add(id);
19002
- for (const id of emitRealCost(state)) changed2.add(id);
19003
- return [...changed2];
19027
+ return ids;
19004
19028
  }
19005
-
19006
- // src/propagations/downward/recompute-all-ponderators.ts
19007
- function recomputeAllPonderators(criterion, adapter) {
19008
- const basisById = /* @__PURE__ */ new Map();
19009
- adapter.forEachActivity((_a, id) => {
19010
- adapter.setActivityField(id, COLUMN.PONDERATOR, 0);
19011
- });
19012
- adapter.forEachActivity((_a, id) => {
19013
- if (adapter.getChildren(id).length > 0) {
19014
- recomputePonderatorsForParent(String(id), criterion, adapter, basisById);
19015
- }
19016
- });
19029
+ function parseParentId(rawParentId) {
19030
+ return rawParentId === ROOT_PARENT_ID ? null : String(rawParentId);
19017
19031
  }
19018
-
19019
- // src/dispatch/shared/baseline-weighted-fields.ts
19020
- function recomputeBaselineWeightedFields(deps, criterion) {
19021
- recomputeAllPonderators(criterion, deps.adapter);
19022
- recomputeAllProgressRollup(deps.adapter, criterion);
19023
- if (deps.now) {
19024
- runExpectedProgressBase(
19025
- deps.adapter,
19026
- deps.now,
19027
- deps.defaultBaseCalendarId ?? null
19028
- );
19029
- applyExpectedProgressLive(deps.adapter, deps.now);
19032
+ function normalizePersistedActivityId(rawId) {
19033
+ if (rawId == null) return null;
19034
+ const isValidIdentity = typeof rawId === "number" && (rawId === 0 || Number.isSafeInteger(rawId) && rawId > 0);
19035
+ if (!isValidIdentity) {
19036
+ throw new Error(`invalid activity proplannerId ${String(rawId)}`);
19030
19037
  }
19031
- emitRealCost(deps.adapter);
19032
- if (deps.now) applyStatusPass(deps.adapter, deps.sector.statusCriteria);
19038
+ return rawId;
19033
19039
  }
19034
-
19035
- // src/dispatch/handlers/baseline-apply.ts
19036
- function activeBaselineSignature(deps) {
19037
- return JSON.stringify(
19038
- deps.adapter.getAllIds().map((id) => {
19039
- const point = deps.adapter.getActivity(id)?.baselinePoints.find((candidate) => candidate.isActiveVersion);
19040
- return point ? [
19041
- String(id),
19042
- point.versionId,
19043
- point.startDate?.getTime() ?? null,
19044
- point.endDate?.getTime() ?? null,
19045
- point.durationDays,
19046
- point.cost,
19047
- point.workHours,
19048
- point.baseCalendarId,
19049
- point.hoursPerDay,
19050
- point.hoursPerWeek
19051
- ] : [String(id), null];
19052
- })
19040
+ function parseProgress(raw, reporter, recordDiagnostic, activityId) {
19041
+ const progress = Number(raw);
19042
+ if (!Number.isFinite(progress)) {
19043
+ throw new TypeError(`invalid activity progress ${String(raw)}`);
19044
+ }
19045
+ const isBelowNotStarted = progress < NOT_STARTED_PROGRESS;
19046
+ const isAboveCompleted = progress > MAX_TOLERATED_PROGRESS;
19047
+ const isWithinRange = !isBelowNotStarted && !isAboveCompleted;
19048
+ if (isWithinRange) return progress;
19049
+ const clamped = isBelowNotStarted ? NOT_STARTED_PROGRESS : COMPLETED_PROGRESS;
19050
+ recordDiagnostic(LOAD_DIAGNOSTIC_CODE.PROGRESS_OUT_OF_RANGE, activityId);
19051
+ reporter.warn(
19052
+ `[parseFromBackend] activity "${activityId}" progress ${String(raw)} is out of range \u2014 clamping to ${clamped}`
19053
19053
  );
19054
+ return clamped;
19054
19055
  }
19055
- async function dispatchBaselineApply(action, deps) {
19056
- const beforeSnap = await snapshotActivities(
19057
- deps.adapter,
19058
- new Set(deps.adapter.getAllIds())
19056
+ function parseCriticality(raw, reporter) {
19057
+ if (typeof raw.is_critical === "boolean") return raw.is_critical;
19058
+ reporter.error(
19059
+ "[parseFromBackend] invalid activity is_critical \u2014 defaulting to false",
19060
+ new TypeError(
19061
+ `activity "${raw.unique_id}" received ${typeof raw.is_critical}`
19062
+ )
19059
19063
  );
19060
- const activeBaselineBefore = activeBaselineSignature(deps);
19061
- const changedIds = applyBaselineOverlay(
19062
- deps.adapter,
19063
- action.points,
19064
- deps.now,
19065
- deps.reporter,
19066
- deps.defaultBaseCalendarId ?? null,
19067
- void 0
19064
+ return false;
19065
+ }
19066
+ function parseBaselinePoints(raw, reporter, recordDiagnostic, activityId, isMilestone) {
19067
+ if (!Array.isArray(raw)) return [];
19068
+ return raw.map(
19069
+ (point) => normalizeBaselinePoint(
19070
+ point,
19071
+ reporter,
19072
+ {
19073
+ record: recordDiagnostic,
19074
+ sampleId: activityId
19075
+ },
19076
+ isMilestone
19077
+ )
19078
+ ).filter((point) => point !== null);
19079
+ }
19080
+ function parseActivity(raw, context) {
19081
+ const calendarId = resolveCalendarId(
19082
+ raw.calendarId,
19083
+ context.knownCalendarIds,
19084
+ context.defaultCalendarId
19068
19085
  );
19069
- applyBaselineVariancePass(deps.adapter, deps.sector.hoursPerDay);
19070
- applyRoleBaselineVariancePass(deps.adapter, deps.sector.hoursPerDay);
19071
- if (activeBaselineSignature(deps) !== activeBaselineBefore) {
19072
- recomputeBaselineWeightedFields(
19073
- deps,
19074
- deps.sector.activityCreter ?? ACTIVITY_PONDERATOR_CRITERION.DURATION
19075
- );
19076
- }
19086
+ const inheritedDefaultCalendar = raw.calendarId == null || !context.knownCalendarIds.has(String(raw.calendarId));
19087
+ const progress = parseProgress(
19088
+ raw.progress,
19089
+ context.reporter,
19090
+ context.recordDiagnostic,
19091
+ String(raw.unique_id)
19092
+ );
19093
+ const { constraintType, constraintDate } = resolveConstraint(
19094
+ raw,
19095
+ context.reporter,
19096
+ context.recordDiagnostic
19097
+ );
19098
+ const isMilestone = raw.type === ACTIVITY_TYPE.MILESTONE;
19099
+ const startDate = parseBackendDate(raw.start_date);
19100
+ const endDate = isMilestone ? startDate : parseBackendDate(raw.end_date);
19101
+ const durationDays = isMilestone ? 0 : Number(raw.duration);
19102
+ const isCritical = parseCriticality(raw, context.reporter);
19077
19103
  return {
19078
- ok: true,
19079
- changes: await assembleChangeSet(deps.adapter, {
19080
- source: action,
19081
- beforeSnap,
19082
- touchedIds: changedIds,
19083
- scheduledIds: [],
19084
- hoursPerDay: deps.sector.hoursPerDay
19085
- })
19104
+ id: String(raw.unique_id),
19105
+ proplannerId: normalizePersistedActivityId(raw.id),
19106
+ parentId: parseParentId(raw.parent_id),
19107
+ name: raw.name,
19108
+ description: raw.description ?? "",
19109
+ type: raw.type,
19110
+ startDate,
19111
+ endDate,
19112
+ durationHours: durationToHours(durationDays, context.hoursPerDay),
19113
+ constraintType,
19114
+ constraintDate,
19115
+ autoScheduling: isAutoSchedulingEnabledForProgress(progress),
19116
+ calendarId,
19117
+ assignedDefaultCalendar: calendarId != null && inheritedDefaultCalendar,
19118
+ dateOrigin: raw.date_origin == null ? null : parseBackendDate(raw.date_origin),
19119
+ progress,
19120
+ cost: Number(raw.cost),
19121
+ usedCost: Number(raw.used_cost ?? 0),
19122
+ realCost: Number(raw.real_cost ?? 0),
19123
+ workHours: Number(raw.hhWorkTime),
19124
+ realWorkHours: Number(raw.real_work ?? 0),
19125
+ ponderator: Number(raw.ponderator ?? 0),
19126
+ hasCustomPonderator: raw.hasCustomPonderator,
19127
+ correlativeId: raw.correlative_id,
19128
+ uniqueCorrelativeId: raw.unique_correlative_id,
19129
+ customId: normalizeCustomId(raw),
19130
+ subcontractId: raw.subcontractId,
19131
+ isLookahead: raw.isOnLookahead,
19132
+ hasLookaheadTasks: Array.isArray(raw.tasks) && raw.tasks.length > 0,
19133
+ newActivityIds: parseNewActivityIds(raw.newActivitiesArray),
19134
+ hasNewActivities: raw.hasNewActivities ?? false,
19135
+ ...raw.isNewActivity === true ? { isNewActivity: true } : {},
19136
+ pendingRequestIds: parseIdList(raw.pendingRequests),
19137
+ responsableIds: parseIdList(raw.responsables),
19138
+ tagIds: parseIdList(raw.tags),
19139
+ baselinePoints: parseBaselinePoints(
19140
+ raw.baseline_points,
19141
+ context.reporter,
19142
+ context.recordDiagnostic,
19143
+ String(raw.unique_id),
19144
+ isMilestone
19145
+ ),
19146
+ baselineSnapshot: null,
19147
+ baselineVarianceDays: null,
19148
+ projectBaselineVarianceDays: null,
19149
+ primaryBaselineVarianceDays: null,
19150
+ recursiveDurationHours: durationToHours(
19151
+ Number(raw.sumOfDurationRecursively ?? 0),
19152
+ context.hoursPerDay
19153
+ ),
19154
+ calendarDuration: computeCalendarDuration({
19155
+ type: raw.type,
19156
+ startDate,
19157
+ endDate
19158
+ }),
19159
+ isCritical,
19160
+ freeSlackHours: null,
19161
+ expectedProgress: null,
19162
+ expectedProgressBaseline: null,
19163
+ status: null,
19164
+ criticalPath: null
19086
19165
  };
19087
19166
  }
19088
-
19089
- // src/dispatch/handlers/ponderator-criterion-set.ts
19090
- function isValidCriterion(value) {
19091
- return Object.values(ACTIVITY_PONDERATOR_CRITERION).includes(
19092
- value
19167
+ function normalizeCustomId(raw) {
19168
+ return raw.has_childs ? null : raw.custom_id;
19169
+ }
19170
+ function forceRootProjectToRoot(activities) {
19171
+ const rootProject = activities.find(
19172
+ (activity) => activity.correlativeId === 0 && activity.type === ACTIVITY_TYPE.PROJECT
19093
19173
  );
19174
+ if (rootProject) rootProject.parentId = null;
19094
19175
  }
19095
- async function dispatchPonderatorCriterionSet(action, deps) {
19096
- if (!isValidCriterion(action.criterion)) {
19097
- return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
19176
+ function reparentOrphansToRoot(activities, ids, reporter, recordDiagnostic) {
19177
+ for (const activity of activities) {
19178
+ if (activity.parentId === null) continue;
19179
+ if (!ids.has(activity.parentId)) {
19180
+ reporter.warn(
19181
+ `[parseFromBackend] activity "${activity.id}" references unknown parent "${activity.parentId}" \u2014 re-parenting to root (null)`
19182
+ );
19183
+ recordDiagnostic(LOAD_DIAGNOSTIC_CODE.ORPHAN_ACTIVITY, activity.id);
19184
+ activity.parentId = null;
19185
+ }
19098
19186
  }
19099
- const allIds = new Set(deps.adapter.getAllIds());
19100
- const beforeSnap = await snapshotActivities(deps.adapter, allIds);
19101
- recomputeBaselineWeightedFields(deps, action.criterion);
19102
- deps.sector.activityCreter = action.criterion;
19103
- return {
19104
- ok: true,
19105
- changes: await assembleChangeSet(deps.adapter, {
19106
- source: action,
19107
- beforeSnap,
19108
- touchedIds: allIds,
19109
- scheduledIds: [],
19110
- hoursPerDay: deps.sector.hoursPerDay
19111
- })
19112
- };
19113
19187
  }
19114
19188
 
19115
- // src/dispatch/handlers/status-criteria-set.ts
19116
- async function dispatchStatusCriteriaSet(action, deps) {
19117
- const resolved = resolveStatusCriteria(action.criteria);
19118
- const allIds = new Set(deps.adapter.getAllIds());
19119
- const beforeSnap = await snapshotActivities(deps.adapter, allIds);
19120
- const changedIds = applyStatusPass(deps.adapter, resolved);
19121
- deps.sector.statusCriteria = resolved;
19122
- return {
19123
- ok: true,
19124
- changes: await assembleChangeSet(deps.adapter, {
19125
- source: action,
19126
- beforeSnap,
19127
- touchedIds: changedIds,
19128
- scheduledIds: [],
19129
- hoursPerDay: deps.sector.hoursPerDay
19130
- })
19131
- };
19189
+ // src/boundary/backend/normalize-link.ts
19190
+ function resolveLinkType(rawType) {
19191
+ if (typeof rawType !== "string") return null;
19192
+ const fromLabel = parseLinkTypeCode(rawType);
19193
+ if (fromLabel) return fromLabel;
19194
+ if (isLinkType(rawType)) return rawType;
19195
+ return null;
19132
19196
  }
19133
-
19134
- // src/dispatch/handlers/calendar-reconcile.ts
19135
- async function dispatchCalendarReconcile(action, options, deps) {
19136
- const beforeSnap = /* @__PURE__ */ new Map();
19137
- const touchedIds = /* @__PURE__ */ new Set();
19138
- for (const activity of deps.adapter.getAllActivities()) {
19139
- if (!shouldReconcile(activity, deps)) continue;
19140
- const calendar = deps.ctx.calendars.getCalendar(activity.calendarId);
19141
- if (!calendar) continue;
19142
- const startReference = calendar.isWorkTime(
19143
- activity.startDate,
19144
- CALENDAR_UNIT.HOUR
19145
- ) ? startOfUtcDay4(activity.startDate) : activity.startDate;
19146
- const startDate = calendar.getClosestWorkTime({
19147
- date: startReference,
19148
- direction: WORK_TIME_DIRECTION.FUTURE,
19149
- unit: CALENDAR_UNIT.HOUR
19150
- });
19151
- const endDate = calendar.calculateEndDate(
19152
- startDate,
19153
- activity.durationHours
19154
- );
19155
- if (sameInstant(startDate, activity.startDate) && sameInstant(endDate, activity.endDate)) {
19156
- continue;
19157
- }
19158
- const activityId = String(activity.id);
19159
- beforeSnap.set(activityId, structuredCloneActivity(activity));
19160
- touchedIds.add(activityId);
19161
- deps.adapter.setActivityFields(activityId, { startDate, endDate });
19197
+ function normalizePersistedLinkId(rawId) {
19198
+ if (rawId == null) return void 0;
19199
+ const isValidIdentity = typeof rawId === "number" && (rawId === 0 || Number.isSafeInteger(rawId) && rawId > 0);
19200
+ if (!isValidIdentity) {
19201
+ throw new Error(`invalid link proplannerId ${String(rawId)}`);
19162
19202
  }
19163
- const { scheduledIds } = await runPostMutation(
19164
- {
19165
- adapter: deps.adapter,
19166
- scheduler: deps.scheduler,
19167
- sector: deps.sector,
19168
- defaultBaseCalendarId: deps.defaultBaseCalendarId,
19169
- runAutomaticP6Schedule: deps.runAutomaticP6Schedule
19170
- },
19171
- {
19172
- action,
19173
- autoscheduleFrom: AUTOSCHEDULE_SCOPE.ROOTS,
19174
- recomputeParentsFrom: touchedIds,
19203
+ return rawId;
19204
+ }
19205
+ function normalizeLinks(rawLinks, context) {
19206
+ const parsedLinks = [];
19207
+ for (const rawLink of rawLinks) {
19208
+ try {
19209
+ parsedLinks.push(
19210
+ parseLink(rawLink, context.knownActivityIds, context.hoursPerDay)
19211
+ );
19212
+ } catch (parseError2) {
19213
+ context.recordDiagnostic(
19214
+ LOAD_DIAGNOSTIC_CODE.INVALID_LINK,
19215
+ String(rawLink.unique_id ?? rawLink.id)
19216
+ );
19217
+ context.reporter.warn(
19218
+ "[parseFromBackend] dropping invalid link",
19219
+ parseError2
19220
+ );
19221
+ }
19222
+ }
19223
+ return dedupeLinksBySignature(parsedLinks, context.recordDiagnostic);
19224
+ }
19225
+ function parseLink(link, activityIds, hoursPerDay) {
19226
+ const id = String(link.unique_id ?? link.id);
19227
+ const source = String(link.source_id ?? link.source ?? "");
19228
+ const target = String(link.target_id ?? link.target ?? "");
19229
+ if (!activityIds.has(source)) {
19230
+ throw new Error(
19231
+ `[parseFromBackend] link "${id}" source "${source}" does not exist`
19232
+ );
19233
+ }
19234
+ if (!activityIds.has(target)) {
19235
+ throw new Error(
19236
+ `[parseFromBackend] link "${id}" target "${target}" does not exist`
19237
+ );
19238
+ }
19239
+ const type = resolveLinkType(link.type);
19240
+ if (type === null) {
19241
+ throw new Error(
19242
+ `[parseFromBackend] link "${id}" has invalid type "${String(link.type)}"`
19243
+ );
19244
+ }
19245
+ const ganttIdRaw = link.ganttId;
19246
+ const sectorIdRaw = link.sectorId;
19247
+ const proplannerId = normalizePersistedLinkId(link.id);
19248
+ return {
19249
+ id,
19250
+ source,
19251
+ target,
19252
+ type,
19253
+ lag: lagDaysToHours(Number(link.lag ?? 0), hoursPerDay),
19254
+ freeSlackHours: null,
19255
+ isCritical: false,
19256
+ ...typeof ganttIdRaw === "number" ? { ganttId: ganttIdRaw } : {},
19257
+ ...typeof sectorIdRaw === "number" ? { sectorId: sectorIdRaw } : {},
19258
+ ...proplannerId === void 0 ? {} : { proplannerId }
19259
+ };
19260
+ }
19261
+ function dedupeLinksBySignature(links, recordDiagnostic) {
19262
+ const bySignature = /* @__PURE__ */ new Map();
19263
+ for (const link of links) {
19264
+ const signature = `${link.source}-${link.target}-${link.type}`;
19265
+ const existing = bySignature.get(signature);
19266
+ if (!existing) {
19267
+ bySignature.set(signature, link);
19268
+ continue;
19269
+ }
19270
+ const incomingWins = linkIdAsNumber(link) > linkIdAsNumber(existing);
19271
+ const discarded = incomingWins ? existing : link;
19272
+ recordDiagnostic(LOAD_DIAGNOSTIC_CODE.DUPLICATE_LINK, discarded.id);
19273
+ if (incomingWins) {
19274
+ bySignature.set(signature, link);
19275
+ }
19276
+ }
19277
+ return [...bySignature.values()];
19278
+ }
19279
+ function linkIdAsNumber(link) {
19280
+ const numeric = Number(link.id);
19281
+ return Number.isFinite(numeric) ? numeric : -Infinity;
19282
+ }
19283
+
19284
+ // src/boundary/save/field-policy.ts
19285
+ var SAVE_FIELD_POLICY = {
19286
+ TRACKED: "tracked",
19287
+ IGNORED: "ignored"
19288
+ };
19289
+
19290
+ // src/boundary/save/activity-changes.ts
19291
+ var ACTIVITY_SAVE_FIELD_POLICY = {
19292
+ id: SAVE_FIELD_POLICY.IGNORED,
19293
+ proplannerId: SAVE_FIELD_POLICY.IGNORED,
19294
+ parentId: SAVE_FIELD_POLICY.TRACKED,
19295
+ name: SAVE_FIELD_POLICY.TRACKED,
19296
+ description: SAVE_FIELD_POLICY.TRACKED,
19297
+ type: SAVE_FIELD_POLICY.TRACKED,
19298
+ startDate: SAVE_FIELD_POLICY.TRACKED,
19299
+ endDate: SAVE_FIELD_POLICY.TRACKED,
19300
+ durationHours: SAVE_FIELD_POLICY.TRACKED,
19301
+ constraintType: SAVE_FIELD_POLICY.TRACKED,
19302
+ constraintDate: SAVE_FIELD_POLICY.TRACKED,
19303
+ autoScheduling: SAVE_FIELD_POLICY.TRACKED,
19304
+ calendarId: SAVE_FIELD_POLICY.TRACKED,
19305
+ assignedDefaultCalendar: SAVE_FIELD_POLICY.IGNORED,
19306
+ dateOrigin: SAVE_FIELD_POLICY.TRACKED,
19307
+ progress: SAVE_FIELD_POLICY.TRACKED,
19308
+ cost: SAVE_FIELD_POLICY.TRACKED,
19309
+ usedCost: SAVE_FIELD_POLICY.TRACKED,
19310
+ realCost: SAVE_FIELD_POLICY.TRACKED,
19311
+ workHours: SAVE_FIELD_POLICY.TRACKED,
19312
+ realWorkHours: SAVE_FIELD_POLICY.TRACKED,
19313
+ ponderator: SAVE_FIELD_POLICY.TRACKED,
19314
+ hasCustomPonderator: SAVE_FIELD_POLICY.TRACKED,
19315
+ correlativeId: SAVE_FIELD_POLICY.TRACKED,
19316
+ uniqueCorrelativeId: SAVE_FIELD_POLICY.TRACKED,
19317
+ customId: SAVE_FIELD_POLICY.TRACKED,
19318
+ subcontractId: SAVE_FIELD_POLICY.TRACKED,
19319
+ isLookahead: SAVE_FIELD_POLICY.TRACKED,
19320
+ hasLookaheadTasks: SAVE_FIELD_POLICY.IGNORED,
19321
+ newActivityIds: SAVE_FIELD_POLICY.TRACKED,
19322
+ hasNewActivities: SAVE_FIELD_POLICY.TRACKED,
19323
+ isNewActivity: SAVE_FIELD_POLICY.IGNORED,
19324
+ pendingRequestIds: SAVE_FIELD_POLICY.IGNORED,
19325
+ responsableIds: SAVE_FIELD_POLICY.TRACKED,
19326
+ tagIds: SAVE_FIELD_POLICY.TRACKED,
19327
+ baselinePoints: SAVE_FIELD_POLICY.IGNORED,
19328
+ baselineSnapshot: SAVE_FIELD_POLICY.IGNORED,
19329
+ recursiveDurationHours: SAVE_FIELD_POLICY.IGNORED,
19330
+ isCritical: SAVE_FIELD_POLICY.IGNORED,
19331
+ freeSlackHours: SAVE_FIELD_POLICY.IGNORED,
19332
+ calendarDuration: SAVE_FIELD_POLICY.IGNORED,
19333
+ baselineVarianceDays: SAVE_FIELD_POLICY.IGNORED,
19334
+ projectBaselineVarianceDays: SAVE_FIELD_POLICY.IGNORED,
19335
+ primaryBaselineVarianceDays: SAVE_FIELD_POLICY.IGNORED,
19336
+ expectedProgress: SAVE_FIELD_POLICY.IGNORED,
19337
+ expectedProgressBaseline: SAVE_FIELD_POLICY.IGNORED,
19338
+ status: SAVE_FIELD_POLICY.IGNORED,
19339
+ criticalPath: SAVE_FIELD_POLICY.IGNORED,
19340
+ p6: SAVE_FIELD_POLICY.IGNORED
19341
+ };
19342
+ var PERSISTABLE_ACTIVITY_FIELDS = Object.keys(ACTIVITY_SAVE_FIELD_POLICY).filter((field) => {
19343
+ return ACTIVITY_SAVE_FIELD_POLICY[field] === SAVE_FIELD_POLICY.TRACKED;
19344
+ });
19345
+ var DATE_FIELDS = /* @__PURE__ */ new Set([
19346
+ "startDate",
19347
+ "endDate",
19348
+ "constraintDate",
19349
+ "dateOrigin"
19350
+ ]);
19351
+ var ID_ARRAY_FIELDS = /* @__PURE__ */ new Set([
19352
+ "newActivityIds",
19353
+ "responsableIds",
19354
+ "tagIds"
19355
+ ]);
19356
+ function projectPersistableActivity(activity) {
19357
+ const entries = PERSISTABLE_ACTIVITY_FIELDS.map(
19358
+ (field) => [field, snapshotFieldValue(field, activity[field])]
19359
+ );
19360
+ return Object.fromEntries(entries);
19361
+ }
19362
+ function snapshotFieldValue(field, value) {
19363
+ if (DATE_FIELDS.has(field)) {
19364
+ return value instanceof Date ? new Date(value) : null;
19365
+ }
19366
+ if (ID_ARRAY_FIELDS.has(field)) {
19367
+ return Array.isArray(value) ? [...value] : [];
19368
+ }
19369
+ return value;
19370
+ }
19371
+ function activityChangedSince(current, saved) {
19372
+ return changedPersistableActivityFields(current, saved).length > 0;
19373
+ }
19374
+ function changedPersistableActivityFields(current, saved) {
19375
+ return PERSISTABLE_ACTIVITY_FIELDS.filter(
19376
+ (field) => !fieldEquals(field, current[field], saved[field])
19377
+ );
19378
+ }
19379
+ function fieldEquals(field, currentValue, savedValue) {
19380
+ if (DATE_FIELDS.has(field)) return dateEquals(currentValue, savedValue);
19381
+ if (ID_ARRAY_FIELDS.has(field))
19382
+ return idArrayEquals(currentValue, savedValue);
19383
+ return (currentValue ?? null) === (savedValue ?? null);
19384
+ }
19385
+ function dateEquals(current, saved) {
19386
+ const currentTime = current instanceof Date ? current.getTime() : null;
19387
+ const savedTime = saved instanceof Date ? saved.getTime() : null;
19388
+ return currentTime === savedTime;
19389
+ }
19390
+ function idArrayEquals(current, saved) {
19391
+ const currentIds = new Set(Array.isArray(current) ? current.map(String) : []);
19392
+ const savedIds = new Set(Array.isArray(saved) ? saved.map(String) : []);
19393
+ if (currentIds.size !== savedIds.size) return false;
19394
+ for (const currentId of currentIds) {
19395
+ if (!savedIds.has(currentId)) return false;
19396
+ }
19397
+ return true;
19398
+ }
19399
+
19400
+ // src/boundary/save/link-changes.ts
19401
+ var LINK_SAVE_FIELD_POLICY = {
19402
+ id: SAVE_FIELD_POLICY.IGNORED,
19403
+ source: SAVE_FIELD_POLICY.TRACKED,
19404
+ target: SAVE_FIELD_POLICY.TRACKED,
19405
+ type: SAVE_FIELD_POLICY.TRACKED,
19406
+ lag: SAVE_FIELD_POLICY.TRACKED,
19407
+ freeSlackHours: SAVE_FIELD_POLICY.IGNORED,
19408
+ isCritical: SAVE_FIELD_POLICY.IGNORED,
19409
+ ganttId: SAVE_FIELD_POLICY.IGNORED,
19410
+ sectorId: SAVE_FIELD_POLICY.IGNORED,
19411
+ proplannerId: SAVE_FIELD_POLICY.IGNORED
19412
+ };
19413
+ var TRACKED_LINK_FIELDS = Object.keys(LINK_SAVE_FIELD_POLICY).filter((field) => {
19414
+ return LINK_SAVE_FIELD_POLICY[field] === SAVE_FIELD_POLICY.TRACKED;
19415
+ });
19416
+ function linkChangedSince(baseline, current) {
19417
+ return TRACKED_LINK_FIELDS.some(
19418
+ (field) => baseline[field] !== current[field]
19419
+ );
19420
+ }
19421
+ function checkNoUpdatedLinks(baseline, current) {
19422
+ const baselineById = new Map(
19423
+ baseline.map((link) => [String(link.id), link])
19424
+ );
19425
+ return current.filter((link) => {
19426
+ const base = baselineById.get(String(link.id));
19427
+ if (!base) return false;
19428
+ return linkChangedSince(base, link);
19429
+ });
19430
+ }
19431
+
19432
+ // src/dispatch/handlers/persistence-rebase-topology.ts
19433
+ var parentKeyOf2 = (parentId) => isRootParent(parentId) ? null : String(parentId);
19434
+ function mergedParents(input) {
19435
+ const parents = /* @__PURE__ */ new Map();
19436
+ for (const local of input.localActivities) {
19437
+ parents.set(String(local.id), parentKeyOf2(local.parentId));
19438
+ }
19439
+ for (const created of input.createActivities) {
19440
+ parents.set(String(created.id), parentKeyOf2(created.parentId));
19441
+ }
19442
+ for (const update of input.updateActivities) {
19443
+ if (update.fields.parentId === void 0) continue;
19444
+ parents.set(update.activityId, parentKeyOf2(update.fields.parentId));
19445
+ }
19446
+ for (const removedId of input.removeActivityIds) parents.delete(removedId);
19447
+ return parents;
19448
+ }
19449
+ function findParentCycle(parents) {
19450
+ const settled = /* @__PURE__ */ new Set();
19451
+ for (const start of parents.keys()) {
19452
+ const path = [];
19453
+ const onPath = /* @__PURE__ */ new Set();
19454
+ let current = start;
19455
+ while (current !== null && current !== void 0 && !settled.has(current)) {
19456
+ if (onPath.has(current)) return path.slice(path.indexOf(current));
19457
+ onPath.add(current);
19458
+ path.push(current);
19459
+ current = parents.get(current);
19460
+ }
19461
+ for (const visited of path) settled.add(visited);
19462
+ }
19463
+ return null;
19464
+ }
19465
+ function restoreRemoteParent(activityId, remote, input) {
19466
+ const existing = input.updateActivities.find(
19467
+ (update) => update.activityId === activityId
19468
+ );
19469
+ if (existing) {
19470
+ const index = input.updateActivities.indexOf(existing);
19471
+ input.updateActivities[index] = {
19472
+ activityId,
19473
+ fields: { ...existing.fields, parentId: remote.parentId }
19474
+ };
19475
+ return;
19476
+ }
19477
+ input.updateActivities.push({
19478
+ activityId,
19479
+ fields: { parentId: remote.parentId }
19480
+ });
19481
+ }
19482
+ function repairParentCycles(input) {
19483
+ const remoteById = new Map(
19484
+ input.remoteActivities.map((remote) => [String(remote.id), remote])
19485
+ );
19486
+ const conflicts = [];
19487
+ let parents = mergedParents(input);
19488
+ let cycle = findParentCycle(parents);
19489
+ while (cycle !== null) {
19490
+ const localChoice = cycle.find((activityId) => {
19491
+ const remote2 = remoteById.get(activityId);
19492
+ return remote2 !== void 0 && parentKeyOf2(remote2.parentId) !== parents.get(activityId);
19493
+ });
19494
+ const remote = localChoice === void 0 ? void 0 : remoteById.get(localChoice);
19495
+ if (localChoice === void 0 || remote === void 0) break;
19496
+ conflicts.push({
19497
+ entity: PERSISTENCE_REBASE_ENTITY.ACTIVITY,
19498
+ id: localChoice,
19499
+ kind: PERSISTENCE_REBASE_CONFLICT_KIND.FIELD,
19500
+ field: ACTIVITY_PROPERTY.PARENT,
19501
+ mine: parents.get(localChoice) ?? null,
19502
+ theirs: parentKeyOf2(remote.parentId)
19503
+ });
19504
+ restoreRemoteParent(localChoice, remote, input);
19505
+ parents = mergedParents(input);
19506
+ cycle = findParentCycle(parents);
19507
+ }
19508
+ return conflicts;
19509
+ }
19510
+ function cyclicActivityCount(adapter) {
19511
+ const edges = projectEffectiveEdges(adapter, adapter.getAllLinks());
19512
+ return circularComponents(edges).reduce(
19513
+ (total, component) => total + component.length,
19514
+ 0
19515
+ );
19516
+ }
19517
+ function breakDependencyCycles(adapter, localLinkIdsInCreationOrder, changes) {
19518
+ const removed = /* @__PURE__ */ new Map();
19519
+ const conflicts = [];
19520
+ if (cyclicActivityCount(adapter) === 0) return { conflicts, removed };
19521
+ const withdrawn = [];
19522
+ for (const linkId of localLinkIdsInCreationOrder) {
19523
+ const link = adapter.getLink(linkId);
19524
+ if (!link) continue;
19525
+ withdrawn.push({ ...link });
19526
+ adapter.removeLink(linkId);
19527
+ }
19528
+ let cyclic = cyclicActivityCount(adapter);
19529
+ for (const link of withdrawn) {
19530
+ adapter.addLink(link);
19531
+ const withLink = cyclicActivityCount(adapter);
19532
+ if (withLink <= cyclic) {
19533
+ cyclic = withLink;
19534
+ continue;
19535
+ }
19536
+ adapter.removeLink(String(link.id));
19537
+ removed.set(String(link.id), link);
19538
+ conflicts.push({
19539
+ entity: PERSISTENCE_REBASE_ENTITY.LINK,
19540
+ id: String(link.id),
19541
+ kind: PERSISTENCE_REBASE_CONFLICT_KIND.LINK_CYCLE,
19542
+ field: null,
19543
+ mine: snapshotToLink(link),
19544
+ theirs: null
19545
+ });
19546
+ }
19547
+ changes.push(...buildLinkDeletions(new Map(removed)));
19548
+ return { conflicts, removed };
19549
+ }
19550
+
19551
+ // src/dispatch/handlers/persistence-rebase-plan.ts
19552
+ var ORDER_FIELDS = /* @__PURE__ */ new Set([
19553
+ ACTIVITY_PROPERTY.CORRELATIVE_ID,
19554
+ ACTIVITY_PROPERTY.UNIQUE_CORRELATIVE_ID
19555
+ ]);
19556
+ var RECOMPUTED_FIELDS = /* @__PURE__ */ new Set([
19557
+ ACTIVITY_PROPERTY.END_DATE
19558
+ ]);
19559
+ var RECOMPUTED_PARENT_FIELDS = /* @__PURE__ */ new Set(
19560
+ [
19561
+ ACTIVITY_PROPERTY.START_DATE,
19562
+ ACTIVITY_PROPERTY.DURATION_HOURS,
19563
+ ACTIVITY_PROPERTY.COST,
19564
+ ACTIVITY_PROPERTY.USED_COST,
19565
+ ACTIVITY_PROPERTY.WORK_HOURS
19566
+ ]
19567
+ );
19568
+ var FOLLOWS_REMOTE_FIELDS = /* @__PURE__ */ new Set([
19569
+ ACTIVITY_PROPERTY.REAL_WORK_HOURS,
19570
+ ACTIVITY_PROPERTY.HAS_NEW_ACTIVITIES
19571
+ ]);
19572
+ var PARENT_DERIVED_FIELDS = /* @__PURE__ */ new Set([
19573
+ ...RECOMPUTED_PARENT_FIELDS,
19574
+ ACTIVITY_PROPERTY.PROGRESS,
19575
+ ACTIVITY_PROPERTY.TYPE,
19576
+ ACTIVITY_PROPERTY.REAL_COST,
19577
+ ACTIVITY_PROPERTY.PONDERATOR,
19578
+ ACTIVITY_PROPERTY.AUTO_SCHEDULING
19579
+ ]);
19580
+ var UNION_MERGED_FIELDS = /* @__PURE__ */ new Set([
19581
+ ACTIVITY_PROPERTY.NEW_ACTIVITIES_ARRAY
19582
+ ]);
19583
+ function planPersistenceRebase(input) {
19584
+ const plan = {
19585
+ createActivities: [],
19586
+ updateActivities: [],
19587
+ removeActivityIds: [],
19588
+ createLinks: [],
19589
+ updateLinks: [],
19590
+ removeLinkIds: [],
19591
+ remoteChanges: [],
19592
+ conflicts: []
19593
+ };
19594
+ planActivities(input, plan);
19595
+ plan.conflicts.push(
19596
+ ...repairParentCycles({
19597
+ localActivities: input.localActivities,
19598
+ remoteActivities: input.remoteActivities,
19599
+ createActivities: plan.createActivities,
19600
+ removeActivityIds: plan.removeActivityIds,
19601
+ updateActivities: plan.updateActivities
19602
+ })
19603
+ );
19604
+ planLinks(input, plan);
19605
+ return plan;
19606
+ }
19607
+ function collectParentIds2(activities) {
19608
+ const parentIds = /* @__PURE__ */ new Set();
19609
+ for (const activity of activities) {
19610
+ if (isRootParent(activity.parentId)) continue;
19611
+ parentIds.add(String(activity.parentId));
19612
+ }
19613
+ return parentIds;
19614
+ }
19615
+ function collectLinkTargetIds(links) {
19616
+ return new Set(links.map((link) => String(link.target)));
19617
+ }
19618
+ function indexStructure(input) {
19619
+ return {
19620
+ localParentIds: collectParentIds2(input.localActivities),
19621
+ remoteParentIds: collectParentIds2(input.remoteActivities),
19622
+ localTargetIds: collectLinkTargetIds(input.localLinks),
19623
+ remoteTargetIds: collectLinkTargetIds(input.remoteLinks)
19624
+ };
19625
+ }
19626
+ function mergeContextFor(activityId, structure) {
19627
+ return {
19628
+ local: {
19629
+ isParent: structure.localParentIds.has(activityId),
19630
+ hasPredecessors: structure.localTargetIds.has(activityId)
19631
+ },
19632
+ remote: {
19633
+ isParent: structure.remoteParentIds.has(activityId),
19634
+ hasPredecessors: structure.remoteTargetIds.has(activityId)
19635
+ }
19636
+ };
19637
+ }
19638
+ function isRecomputedOnSide(field, side, activity) {
19639
+ if (RECOMPUTED_FIELDS.has(field)) return true;
19640
+ if (side.isParent && RECOMPUTED_PARENT_FIELDS.has(field)) return true;
19641
+ const isScheduledStart = field === ACTIVITY_PROPERTY.START_DATE && side.hasPredecessors && activity.autoScheduling;
19642
+ if (isScheduledStart) return true;
19643
+ return field === ACTIVITY_PROPERTY.PONDERATOR && !activity.hasCustomPonderator;
19644
+ }
19645
+ function isDerivedOnSide(field, side, activity) {
19646
+ if (isRecomputedOnSide(field, side, activity)) return true;
19647
+ if (FOLLOWS_REMOTE_FIELDS.has(field)) return true;
19648
+ return side.isParent && PARENT_DERIVED_FIELDS.has(field);
19649
+ }
19650
+ function sourceEditsOf(activity, base, side) {
19651
+ if (base === null) return /* @__PURE__ */ new Set();
19652
+ return new Set(
19653
+ changedPersistableActivityFields(activity, base).filter(
19654
+ (field) => !isDerivedOnSide(field, side, activity)
19655
+ )
19656
+ );
19657
+ }
19658
+ function planActivities(input, plan) {
19659
+ const structure = indexStructure(input);
19660
+ const localById = new Map(
19661
+ input.localActivities.map((activity) => [String(activity.id), activity])
19662
+ );
19663
+ const remoteIds = /* @__PURE__ */ new Set();
19664
+ for (const remote of input.remoteActivities) {
19665
+ const activityId = String(remote.id);
19666
+ remoteIds.add(activityId);
19667
+ const base = input.checkpoint.checkpointActivity(activityId);
19668
+ const local = localById.get(activityId) ?? null;
19669
+ const context = mergeContextFor(activityId, structure);
19670
+ planRemoteActivity(activityId, remote, base, local, context, plan);
19671
+ }
19672
+ for (const activityId of input.checkpoint.checkpointActivityIds()) {
19673
+ if (remoteIds.has(activityId)) continue;
19674
+ const local = localById.get(activityId);
19675
+ if (!local) continue;
19676
+ const base = input.checkpoint.checkpointActivity(activityId);
19677
+ planRemotelyDeletedActivity(activityId, base, local, plan);
19678
+ }
19679
+ }
19680
+ function planRemoteActivity(activityId, remote, base, local, context, plan) {
19681
+ if (base === null) {
19682
+ if (local === null) {
19683
+ plan.createActivities.push(remote);
19684
+ return;
19685
+ }
19686
+ planActivityFields(activityId, remote, null, local, context, plan);
19687
+ return;
19688
+ }
19689
+ const remoteFields = changedPersistableActivityFields(remote, base);
19690
+ if (local === null) {
19691
+ if (remoteFields.length === 0) return;
19692
+ plan.createActivities.push(remote);
19693
+ plan.conflicts.push({
19694
+ entity: PERSISTENCE_REBASE_ENTITY.ACTIVITY,
19695
+ id: activityId,
19696
+ kind: PERSISTENCE_REBASE_CONFLICT_KIND.DELETED_LOCALLY,
19697
+ field: null,
19698
+ mine: null,
19699
+ theirs: projectPersistableActivity(remote)
19700
+ });
19701
+ return;
19702
+ }
19703
+ planActivityFields(activityId, remote, base, local, context, plan);
19704
+ }
19705
+ var MERGE_SIDE = { LOCAL: "local", REMOTE: "remote" };
19706
+ function typeSideOf(context) {
19707
+ if (context.local.isParent) return MERGE_SIDE.LOCAL;
19708
+ if (context.remote.isParent) return MERGE_SIDE.REMOTE;
19709
+ return null;
19710
+ }
19711
+ function winningSide(merge, field) {
19712
+ const remoteChanged = merge.remoteFields.includes(field);
19713
+ const localChanged = merge.localChangedFields.has(field);
19714
+ if (field === ACTIVITY_PROPERTY.TYPE) {
19715
+ if (!localChanged) return MERGE_SIDE.REMOTE;
19716
+ if (!remoteChanged) return MERGE_SIDE.LOCAL;
19717
+ return typeSideOf(merge.context) ?? MERGE_SIDE.REMOTE;
19718
+ }
19719
+ if (isRecomputedOnSide(field, merge.context.local, merge.local)) {
19720
+ return MERGE_SIDE.REMOTE;
19721
+ }
19722
+ if (!localChanged) return MERGE_SIDE.REMOTE;
19723
+ if (merge.remoteSourceFields.has(field)) return MERGE_SIDE.REMOTE;
19724
+ const isParentOnBothSides = merge.context.local.isParent && merge.context.remote.isParent;
19725
+ const followsRemote = FOLLOWS_REMOTE_FIELDS.has(field) || isParentOnBothSides && PARENT_DERIVED_FIELDS.has(field);
19726
+ if (remoteChanged && followsRemote) return MERGE_SIDE.REMOTE;
19727
+ return MERGE_SIDE.LOCAL;
19728
+ }
19729
+ function planActivityFields(activityId, remote, base, local, context, plan) {
19730
+ const localPersistable = projectPersistableActivity(local);
19731
+ const remotePersistable = projectPersistableActivity(remote);
19732
+ const differsFromLocal = changedPersistableActivityFields(
19733
+ remote,
19734
+ localPersistable
19735
+ );
19736
+ if (differsFromLocal.length === 0) return;
19737
+ const remoteFields = base === null ? differsFromLocal : changedPersistableActivityFields(remote, base);
19738
+ const merge = {
19739
+ activityId,
19740
+ remote,
19741
+ local,
19742
+ remotePersistable,
19743
+ localPersistable,
19744
+ remoteFields,
19745
+ localFields: sourceEditsOf(local, base, context.local),
19746
+ localChangedFields: new Set(
19747
+ base === null ? [] : changedPersistableActivityFields(local, base)
19748
+ ),
19749
+ remoteSourceFields: base === null ? new Set(differsFromLocal) : sourceEditsOf(remote, base, context.remote),
19750
+ context
19751
+ };
19752
+ planBecameParentConflicts(merge, plan);
19753
+ const fields = mergeFields(merge, differsFromLocal, plan);
19754
+ if (Object.keys(fields).length === 0) return;
19755
+ plan.updateActivities.push({ activityId, fields });
19756
+ }
19757
+ function mergeFields(merge, differsFromLocal, plan) {
19758
+ const { remotePersistable } = merge;
19759
+ const fields = {};
19760
+ for (const field of differsFromLocal) {
19761
+ if (ORDER_FIELDS.has(field) || field === ACTIVITY_PROPERTY.AUTO_SCHEDULING) {
19762
+ continue;
19763
+ }
19764
+ if (UNION_MERGED_FIELDS.has(field)) {
19765
+ mergeUnionField(merge, field, fields);
19766
+ continue;
19767
+ }
19768
+ if (field === ACTIVITY_PROPERTY.HAS_NEW_ACTIVITIES && fields.hasNewActivities !== void 0) {
19769
+ continue;
19770
+ }
19771
+ if (winningSide(merge, field) === MERGE_SIDE.LOCAL) continue;
19772
+ Reflect.set(fields, field, remotePersistable[field]);
19773
+ if (merge.remoteSourceFields.has(field)) {
19774
+ reportSourceField(merge, field, plan);
19775
+ }
19776
+ }
19777
+ if (fields.progress !== void 0 && !merge.context.local.isParent) {
19778
+ fields.autoScheduling = isAutoSchedulingEnabledForProgress(fields.progress);
19779
+ }
19780
+ keepMilestoneDurationConsistent(merge, fields, plan);
19781
+ return fields;
19782
+ }
19783
+ function mergeUnionField(merge, field, fields) {
19784
+ const { localPersistable, remotePersistable } = merge;
19785
+ const merged = unionOfIds(localPersistable[field], remotePersistable[field]);
19786
+ Reflect.set(fields, field, merged);
19787
+ Reflect.set(
19788
+ fields,
19789
+ ACTIVITY_PROPERTY.HAS_NEW_ACTIVITIES,
19790
+ merged.length > 0 || localPersistable.hasNewActivities || remotePersistable.hasNewActivities
19791
+ );
19792
+ }
19793
+ function keepMilestoneDurationConsistent(merge, fields, plan) {
19794
+ const finalType = fields.type ?? merge.local.type;
19795
+ const finalDuration = fields.durationHours ?? merge.local.durationHours;
19796
+ if (finalType !== ACTIVITY_TYPE.MILESTONE || finalDuration === 0) return;
19797
+ const remoteSetDuration = fields.durationHours !== void 0;
19798
+ const field = remoteSetDuration ? ACTIVITY_PROPERTY.TYPE : ACTIVITY_PROPERTY.DURATION_HOURS;
19799
+ if (remoteSetDuration) {
19800
+ fields.type = merge.remote.type;
19801
+ } else {
19802
+ fields.durationHours = 0;
19803
+ }
19804
+ plan.conflicts.push({
19805
+ entity: PERSISTENCE_REBASE_ENTITY.ACTIVITY,
19806
+ id: merge.activityId,
19807
+ kind: PERSISTENCE_REBASE_CONFLICT_KIND.FIELD,
19808
+ field,
19809
+ mine: merge.localPersistable[field],
19810
+ theirs: remoteSetDuration ? merge.remote.type : 0
19811
+ });
19812
+ }
19813
+ function reportSourceField(merge, field, plan) {
19814
+ const mine = merge.localPersistable[field];
19815
+ const theirs = merge.remotePersistable[field];
19816
+ if (merge.localFields.has(field)) {
19817
+ plan.conflicts.push({
19818
+ entity: PERSISTENCE_REBASE_ENTITY.ACTIVITY,
19819
+ id: merge.activityId,
19820
+ kind: PERSISTENCE_REBASE_CONFLICT_KIND.FIELD,
19821
+ field,
19822
+ mine,
19823
+ theirs
19824
+ });
19825
+ return;
19826
+ }
19827
+ plan.remoteChanges.push({
19828
+ entity: PERSISTENCE_REBASE_ENTITY.ACTIVITY,
19829
+ id: merge.activityId,
19830
+ field,
19831
+ before: mine,
19832
+ after: theirs
19833
+ });
19834
+ }
19835
+ function unionOfIds(mine, theirs) {
19836
+ const merged = /* @__PURE__ */ new Set();
19837
+ for (const value of Array.isArray(mine) ? mine : [])
19838
+ merged.add(String(value));
19839
+ for (const value of Array.isArray(theirs) ? theirs : [])
19840
+ merged.add(String(value));
19841
+ return [...merged];
19842
+ }
19843
+ function leafValuesOf(fields, activity) {
19844
+ const picked = {};
19845
+ for (const field of fields) {
19846
+ if (!PARENT_DERIVED_FIELDS.has(field)) continue;
19847
+ if (field === ACTIVITY_PROPERTY.TYPE) continue;
19848
+ Reflect.set(picked, field, activity[field]);
19849
+ }
19850
+ return picked;
19851
+ }
19852
+ function planBecameParentConflicts(merge, plan) {
19853
+ const { context, localPersistable, remotePersistable } = merge;
19854
+ if (context.local.isParent === context.remote.isParent) return;
19855
+ const droppedLeafValues = context.remote.isParent ? leafValuesOf(merge.localFields, localPersistable) : leafValuesOf(merge.remoteFields, remotePersistable);
19856
+ if (Object.keys(droppedLeafValues).length === 0) return;
19857
+ const conflict = context.remote.isParent ? {
19858
+ kind: PERSISTENCE_REBASE_CONFLICT_KIND.BECAME_PARENT_REMOTELY,
19859
+ mine: droppedLeafValues,
19860
+ theirs: remotePersistable
19861
+ } : {
19862
+ kind: PERSISTENCE_REBASE_CONFLICT_KIND.BECAME_PARENT_LOCALLY,
19863
+ mine: localPersistable,
19864
+ theirs: droppedLeafValues
19865
+ };
19866
+ plan.conflicts.push({
19867
+ entity: PERSISTENCE_REBASE_ENTITY.ACTIVITY,
19868
+ id: merge.activityId,
19869
+ field: null,
19870
+ ...conflict
19871
+ });
19872
+ }
19873
+ function planRemotelyDeletedActivity(activityId, base, local, plan) {
19874
+ plan.removeActivityIds.push(activityId);
19875
+ const localEdited = base !== null && changedPersistableActivityFields(local, base).length > 0;
19876
+ if (!localEdited) return;
19877
+ plan.conflicts.push({
19878
+ entity: PERSISTENCE_REBASE_ENTITY.ACTIVITY,
19879
+ id: activityId,
19880
+ kind: PERSISTENCE_REBASE_CONFLICT_KIND.DELETED_REMOTELY,
19881
+ field: null,
19882
+ mine: projectPersistableActivity(local),
19883
+ theirs: null
19884
+ });
19885
+ }
19886
+ function planLinks(input, plan) {
19887
+ const localById = new Map(
19888
+ input.localLinks.map((link) => [String(link.id), link])
19889
+ );
19890
+ const remoteIds = /* @__PURE__ */ new Set();
19891
+ const createdRemotely = [];
19892
+ for (const remote of input.remoteLinks) {
19893
+ const linkId = String(remote.id);
19894
+ remoteIds.add(linkId);
19895
+ const base = input.checkpoint.checkpointLink(linkId);
19896
+ const local = localById.get(linkId) ?? null;
19897
+ if (base === null && local === null) createdRemotely.push(remote);
19898
+ planRemoteLink(linkId, remote, base, local, plan);
19899
+ }
19900
+ for (const linkId of input.checkpoint.checkpointLinkIds()) {
19901
+ if (remoteIds.has(linkId)) continue;
19902
+ const local = localById.get(linkId);
19903
+ if (!local) continue;
19904
+ const base = input.checkpoint.checkpointLink(linkId);
19905
+ planRemotelyDeletedLink(linkId, base, local, plan);
19906
+ }
19907
+ planDuplicateLinks(input, createdRemotely, remoteIds, plan);
19908
+ }
19909
+ function endpointPairKey(link) {
19910
+ return [String(link.source), String(link.target)].sort().join("|");
19911
+ }
19912
+ function planDuplicateLinks(input, createdRemotely, remoteIds, plan) {
19913
+ if (createdRemotely.length === 0) return;
19914
+ const remoteByPair = new Map(
19915
+ createdRemotely.map((remote) => [endpointPairKey(remote), remote])
19916
+ );
19917
+ for (const local of input.localLinks) {
19918
+ const linkId = String(local.id);
19919
+ const isUnsavedLocally = !remoteIds.has(linkId) && input.checkpoint.checkpointLink(linkId) === null;
19920
+ if (!isUnsavedLocally) continue;
19921
+ const remote = remoteByPair.get(endpointPairKey(local));
19922
+ if (!remote) continue;
19923
+ plan.removeLinkIds.push(linkId);
19924
+ const mine = persistableLinkOf(local);
19925
+ const theirs = persistableLinkOf(remote);
19926
+ if (changedLinkFields(mine, theirs).length === 0) continue;
19927
+ plan.conflicts.push({
19928
+ entity: PERSISTENCE_REBASE_ENTITY.LINK,
19929
+ id: String(remote.id),
19930
+ kind: PERSISTENCE_REBASE_CONFLICT_KIND.DUPLICATE_LINK,
19931
+ field: null,
19932
+ mine,
19933
+ theirs
19934
+ });
19935
+ }
19936
+ }
19937
+ function changedLinkFields(current, saved) {
19938
+ return TRACKED_LINK_FIELDS.filter((field) => current[field] !== saved[field]);
19939
+ }
19940
+ function planRemoteLink(linkId, remote, base, local, plan) {
19941
+ if (base === null) {
19942
+ if (local === null) {
19943
+ plan.createLinks.push(remote);
19944
+ return;
19945
+ }
19946
+ planLinkFields(linkId, remote, null, local, plan);
19947
+ return;
19948
+ }
19949
+ if (local === null) {
19950
+ if (changedLinkFields(remote, base).length === 0) return;
19951
+ plan.createLinks.push(remote);
19952
+ plan.conflicts.push({
19953
+ entity: PERSISTENCE_REBASE_ENTITY.LINK,
19954
+ id: linkId,
19955
+ kind: PERSISTENCE_REBASE_CONFLICT_KIND.DELETED_LOCALLY,
19956
+ field: null,
19957
+ mine: null,
19958
+ theirs: persistableLinkOf(remote)
19959
+ });
19960
+ return;
19961
+ }
19962
+ planLinkFields(linkId, remote, base, local, plan);
19963
+ }
19964
+ function persistableLinkOf(link) {
19965
+ return {
19966
+ source: String(link.source),
19967
+ target: String(link.target),
19968
+ type: link.type,
19969
+ lag: Number(link.lag)
19970
+ };
19971
+ }
19972
+ function planLinkFields(linkId, remote, base, local, plan) {
19973
+ const remoteLink = persistableLinkOf(remote);
19974
+ const localLink = persistableLinkOf(local);
19975
+ const remoteFields = base === null ? changedLinkFields(remoteLink, localLink) : changedLinkFields(remoteLink, base);
19976
+ if (remoteFields.length === 0) return;
19977
+ const localFields = new Set(
19978
+ base === null ? [] : changedLinkFields(localLink, base)
19979
+ );
19980
+ const differsFromLocal = new Set(
19981
+ changedLinkFields(remoteLink, localLink)
19982
+ );
19983
+ const fields = {};
19984
+ for (const field of remoteFields) {
19985
+ if (!differsFromLocal.has(field)) continue;
19986
+ Reflect.set(fields, field, remoteLink[field]);
19987
+ if (localFields.has(field)) {
19988
+ plan.conflicts.push({
19989
+ entity: PERSISTENCE_REBASE_ENTITY.LINK,
19990
+ id: linkId,
19991
+ kind: PERSISTENCE_REBASE_CONFLICT_KIND.FIELD,
19992
+ field,
19993
+ mine: localLink[field],
19994
+ theirs: remoteLink[field]
19995
+ });
19996
+ continue;
19997
+ }
19998
+ plan.remoteChanges.push({
19999
+ entity: PERSISTENCE_REBASE_ENTITY.LINK,
20000
+ id: linkId,
20001
+ field,
20002
+ before: localLink[field],
20003
+ after: remoteLink[field]
20004
+ });
20005
+ }
20006
+ if (Object.keys(fields).length === 0) return;
20007
+ plan.updateLinks.push({ linkId, fields });
20008
+ }
20009
+ function planRemotelyDeletedLink(linkId, base, local, plan) {
20010
+ plan.removeLinkIds.push(linkId);
20011
+ const localEdited = base !== null && changedLinkFields(persistableLinkOf(local), base).length > 0;
20012
+ if (!localEdited) return;
20013
+ plan.conflicts.push({
20014
+ entity: PERSISTENCE_REBASE_ENTITY.LINK,
20015
+ id: linkId,
20016
+ kind: PERSISTENCE_REBASE_CONFLICT_KIND.DELETED_REMOTELY,
20017
+ field: null,
20018
+ mine: persistableLinkOf(local),
20019
+ theirs: null
20020
+ });
20021
+ }
20022
+
20023
+ // src/dispatch/handlers/persistence-rebase-order.ts
20024
+ var parentKeyOf3 = (parentId) => isRootParent(parentId) ? ROOT_PARENT_ID : String(parentId);
20025
+ var sortValueOf = (correlativeId) => {
20026
+ const value = Number(correlativeId);
20027
+ return Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER;
20028
+ };
20029
+ function siblingOrdersOf(entries) {
20030
+ const grouped = /* @__PURE__ */ new Map();
20031
+ for (const entry of entries) {
20032
+ const key = parentKeyOf3(entry.parentId);
20033
+ const siblings = grouped.get(key) ?? [];
20034
+ siblings.push(entry);
20035
+ grouped.set(key, siblings);
20036
+ }
20037
+ const orders = /* @__PURE__ */ new Map();
20038
+ for (const [key, siblings] of grouped) {
20039
+ siblings.sort(
20040
+ (left, right) => sortValueOf(left.correlativeId) - sortValueOf(right.correlativeId) || left.id.localeCompare(right.id)
20041
+ );
20042
+ orders.set(
20043
+ key,
20044
+ siblings.map((entry) => entry.id)
20045
+ );
20046
+ }
20047
+ return orders;
20048
+ }
20049
+ var entryOf = (activity) => ({
20050
+ id: String(activity.id),
20051
+ parentId: activity.parentId,
20052
+ correlativeId: activity.correlativeId
20053
+ });
20054
+ function captureSiblingOrders(localActivities, remoteActivities, checkpoint) {
20055
+ const baseEntries = [];
20056
+ for (const activityId of checkpoint.checkpointActivityIds()) {
20057
+ const saved = checkpoint.checkpointActivity(activityId);
20058
+ if (saved === null) continue;
20059
+ baseEntries.push({
20060
+ id: activityId,
20061
+ parentId: saved.parentId,
20062
+ correlativeId: saved.correlativeId
20063
+ });
20064
+ }
20065
+ return {
20066
+ local: siblingOrdersOf(localActivities.map(entryOf)),
20067
+ remote: siblingOrdersOf(remoteActivities.map(entryOf)),
20068
+ base: siblingOrdersOf(baseEntries)
20069
+ };
20070
+ }
20071
+ function isReordered(side, base) {
20072
+ const inBase = new Set(base);
20073
+ const inSide = new Set(side);
20074
+ const sideCommon = side.filter((activityId) => inBase.has(activityId));
20075
+ const baseCommon = base.filter((activityId) => inSide.has(activityId));
20076
+ return sideCommon.some(
20077
+ (activityId, index) => baseCommon[index] !== activityId
20078
+ );
20079
+ }
20080
+ function weave(primary, secondary, members) {
20081
+ const merged = primary.filter((activityId) => members.has(activityId));
20082
+ let anchorIndex = -1;
20083
+ for (const activityId of secondary) {
20084
+ const placedAt = merged.indexOf(activityId);
20085
+ if (placedAt !== -1) {
20086
+ anchorIndex = placedAt;
20087
+ continue;
20088
+ }
20089
+ if (!members.has(activityId)) continue;
20090
+ merged.splice(anchorIndex + 1, 0, activityId);
20091
+ anchorIndex += 1;
20092
+ }
20093
+ for (const activityId of members) {
20094
+ if (!merged.includes(activityId)) merged.push(activityId);
20095
+ }
20096
+ return merged;
20097
+ }
20098
+ function planParentOrder(parentKey, members, sources) {
20099
+ const local = sources.local.get(parentKey) ?? [];
20100
+ const remote = sources.remote.get(parentKey) ?? [];
20101
+ const base = sources.base.get(parentKey) ?? [];
20102
+ const localReordered = isReordered(local, base);
20103
+ const remoteReordered = isReordered(remote, base);
20104
+ const keepsLocalOrder = localReordered && !remoteReordered;
20105
+ const order = keepsLocalOrder ? weave(local, remote, members) : weave(remote, local, members);
20106
+ const overridesLocalOrder = localReordered && remoteReordered && isReordered(local, order);
20107
+ return {
20108
+ order,
20109
+ conflict: overridesLocalOrder ? {
20110
+ entity: PERSISTENCE_REBASE_ENTITY.ACTIVITY,
20111
+ id: parentKey,
20112
+ kind: PERSISTENCE_REBASE_CONFLICT_KIND.ORDER,
20113
+ field: ACTIVITY_PROPERTY.CORRELATIVE_ID,
20114
+ mine: local.filter((activityId) => members.has(activityId)),
20115
+ theirs: order
20116
+ } : null
20117
+ };
20118
+ }
20119
+ function currentSiblings(adapter) {
20120
+ return siblingOrdersOf(adapter.getAllActivities().map(entryOf));
20121
+ }
20122
+ function applySiblingOrder(adapter, sources) {
20123
+ const { orders, conflicts } = planSiblingOrders(adapter, sources);
20124
+ return { shifts: renumberCorrelatives(adapter, orders), conflicts };
20125
+ }
20126
+ function planSiblingOrders(adapter, sources) {
20127
+ const orders = /* @__PURE__ */ new Map();
20128
+ const conflicts = [];
20129
+ for (const [parentKey, siblings] of currentSiblings(adapter)) {
20130
+ const planned = planParentOrder(parentKey, new Set(siblings), sources);
20131
+ orders.set(parentKey, planned.order);
20132
+ if (planned.conflict !== null) conflicts.push(planned.conflict);
20133
+ }
20134
+ return { orders, conflicts };
20135
+ }
20136
+ function renumberCorrelatives(adapter, orders) {
20137
+ const shifts = [];
20138
+ let counter = 0;
20139
+ const pending = [...orders.get(ROOT_PARENT_ID) ?? []].reverse();
20140
+ while (pending.length > 0) {
20141
+ const activityId = pending.pop();
20142
+ if (activityId === void 0) break;
20143
+ const shift = writeCorrelative(adapter, activityId, counter);
20144
+ if (shift !== null) shifts.push(shift);
20145
+ counter += 1;
20146
+ pushChildrenInReverse(pending, orders.get(activityId) ?? []);
20147
+ }
20148
+ return shifts;
20149
+ }
20150
+ function writeCorrelative(adapter, activityId, correlativeId) {
20151
+ const activity = adapter.getActivity(activityId);
20152
+ if (!activity || activity.correlativeId === correlativeId) return null;
20153
+ const before = Number(activity.correlativeId);
20154
+ adapter.setActivityField(
20155
+ activityId,
20156
+ ACTIVITY_PROPERTY.CORRELATIVE_ID,
20157
+ correlativeId
20158
+ );
20159
+ return { activityId, before: Number.isFinite(before) ? before : void 0 };
20160
+ }
20161
+ function pushChildrenInReverse(pending, children) {
20162
+ for (let index = children.length - 1; index >= 0; index -= 1) {
20163
+ const childId = children[index];
20164
+ if (childId !== void 0) pending.push(childId);
20165
+ }
20166
+ }
20167
+
20168
+ // src/dispatch/handlers/persistence-rebase-rekey.ts
20169
+ var hasNoPersistedIdentity = (proplannerId) => proplannerId === null || proplannerId === void 0 || proplannerId === 0;
20170
+ var isUnpersisted = (activity) => hasNoPersistedIdentity(activity.proplannerId);
20171
+ function nextFreeLinkId(deps, taken) {
20172
+ let candidate = deps.linkIdGen.next();
20173
+ const isTaken = (linkId) => deps.adapter.getLink(linkId) !== null || deps.isLinkIdReserved(String(linkId)) || taken.has(String(linkId));
20174
+ while (isTaken(candidate)) candidate = deps.linkIdGen.next();
20175
+ return candidate;
20176
+ }
20177
+ function planLinkRekeys(remoteLinks, deps) {
20178
+ const remoteLinkIds = /* @__PURE__ */ new Set();
20179
+ for (const remote of remoteLinks) {
20180
+ remoteLinkIds.add(String(remote.id));
20181
+ deps.linkIdGen.bump(remote.id);
20182
+ }
20183
+ const taken = new Set(remoteLinkIds);
20184
+ const rekeys = [];
20185
+ for (const local of deps.adapter.getAllLinks()) {
20186
+ const linkId = String(local.id);
20187
+ if (!remoteLinkIds.has(linkId)) continue;
20188
+ if (!hasNoPersistedIdentity(local.proplannerId)) continue;
20189
+ const nextId = String(nextFreeLinkId(deps, taken));
20190
+ taken.add(nextId);
20191
+ rekeys.push({ from: linkId, to: nextId });
20192
+ }
20193
+ return rekeys;
20194
+ }
20195
+ function remoteCustomIdOwners(remoteActivities) {
20196
+ const owners = /* @__PURE__ */ new Map();
20197
+ for (const remote of remoteActivities) {
20198
+ if (remote.customId) owners.set(remote.customId, String(remote.id));
20199
+ }
20200
+ return owners;
20201
+ }
20202
+ function isLocalCustomIdEdit(local, deps) {
20203
+ const saved = deps.persistenceCheckpoint.checkpointActivity(String(local.id));
20204
+ return saved === null || saved.customId !== local.customId;
20205
+ }
20206
+ function planCustomIdRekeys(remoteActivities, deps) {
20207
+ const owners = remoteCustomIdOwners(remoteActivities);
20208
+ deps.customIdTracker.populateFromActivities(remoteActivities);
20209
+ const rekeys = [];
20210
+ for (const local of deps.adapter.getVisualOrderIds()) {
20211
+ const activity = deps.adapter.getActivity(local);
20212
+ if (!activity?.customId) continue;
20213
+ const owner = owners.get(activity.customId);
20214
+ if (owner === void 0) continue;
20215
+ const isSameEntity = owner === String(activity.id) && !isUnpersisted(activity);
20216
+ if (isSameEntity) continue;
20217
+ if (!isLocalCustomIdEdit(activity, deps)) continue;
20218
+ rekeys.push({
20219
+ activityId: activity.id,
20220
+ nextCustomId: deps.customIdTracker.generateCustomIdForRestoredChild(
20221
+ activity.customId
20222
+ )
20223
+ });
20224
+ }
20225
+ return rekeys;
20226
+ }
20227
+ function nextFreeActivityId(deps, claimed) {
20228
+ let candidate = deps.activityIdGen.next();
20229
+ const isTaken = (activityId) => deps.adapter.activityExists(activityId) || deps.isActivityIdReserved(String(activityId)) || claimed.has(String(activityId));
20230
+ while (isTaken(candidate)) candidate = deps.activityIdGen.next();
20231
+ return candidate;
20232
+ }
20233
+ function planLocalRekeys(remote, deps) {
20234
+ const remoteActivities = remote.activities;
20235
+ const remoteIds = /* @__PURE__ */ new Set();
20236
+ const remoteUniqueCorrelativeIds = /* @__PURE__ */ new Set();
20237
+ for (const remote2 of remoteActivities) {
20238
+ remoteIds.add(String(remote2.id));
20239
+ remoteUniqueCorrelativeIds.add(String(remote2.uniqueCorrelativeId));
20240
+ deps.activityIdGen.bump(remote2.id);
20241
+ deps.uidGen.bump(remote2.uniqueCorrelativeId);
20242
+ }
20243
+ const claimed = /* @__PURE__ */ new Set();
20244
+ const activities = [];
20245
+ const uniqueCorrelativeIds = [];
20246
+ for (const local of deps.adapter.getAllActivities()) {
20247
+ if (!isUnpersisted(local)) continue;
20248
+ const localId = String(local.id);
20249
+ if (remoteIds.has(localId)) {
20250
+ const nextId = nextFreeActivityId(deps, claimed);
20251
+ claimed.add(String(nextId));
20252
+ activities.push({
20253
+ from: localId,
20254
+ to: String(nextId),
20255
+ parentId: deps.adapter.getParent(local.id)?.id ?? null,
20256
+ childIds: [...deps.adapter.getChildren(local.id)],
20257
+ outgoingLinkIds: [...deps.adapter.getOutgoingLinkIds(local.id)],
20258
+ incomingLinkIds: [...deps.adapter.getIncomingLinkIds(local.id)]
20259
+ });
20260
+ }
20261
+ if (remoteUniqueCorrelativeIds.has(String(local.uniqueCorrelativeId))) {
20262
+ uniqueCorrelativeIds.push({
20263
+ activityId: local.id,
20264
+ nextUniqueCorrelativeId: deps.uidGen.next()
20265
+ });
20266
+ }
20267
+ }
20268
+ return {
20269
+ activities,
20270
+ links: planLinkRekeys(remote.links, deps),
20271
+ uniqueCorrelativeIds,
20272
+ customIds: planCustomIdRekeys(remoteActivities, deps)
20273
+ };
20274
+ }
20275
+ function collectRekeyTouchedIds(plan) {
20276
+ const touched = /* @__PURE__ */ new Set();
20277
+ for (const rekey of plan.activities) {
20278
+ touched.add(rekey.from);
20279
+ touched.add(rekey.to);
20280
+ if (rekey.parentId !== null) touched.add(String(rekey.parentId));
20281
+ for (const childId of rekey.childIds) touched.add(String(childId));
20282
+ }
20283
+ for (const rekey of plan.uniqueCorrelativeIds) {
20284
+ touched.add(String(rekey.activityId));
20285
+ }
20286
+ for (const rekey of plan.customIds) touched.add(String(rekey.activityId));
20287
+ return touched;
20288
+ }
20289
+ function replaceId(ids, from, to) {
20290
+ return ids.map((candidate) => String(candidate) === from ? to : candidate);
20291
+ }
20292
+ function moveActivityIdentity(rekey, adapter) {
20293
+ const current = adapter.getActivity(rekey.from);
20294
+ if (!current) return;
20295
+ const relocated = structuredCloneActivity(current);
20296
+ relocated.id = rekey.to;
20297
+ const wasChecked = adapter.isChecked(rekey.from);
20298
+ const wasVisible = adapter.isVisible(rekey.from);
20299
+ const lastStartDate = adapter.getLastStartDate(rekey.from);
20300
+ const promotionSnapshot = adapter.getPromotionSnapshot(rekey.from);
20301
+ const progressFallback = adapter.getProgressFallback(rekey.from);
20302
+ adapter.removeActivity(rekey.from);
20303
+ adapter.addActivity(relocated);
20304
+ if (wasChecked) adapter.setChecked(rekey.to, true);
20305
+ if (!wasVisible) adapter.setVisible(rekey.to, false);
20306
+ if (lastStartDate) adapter.setLastStartDate(rekey.to, lastStartDate);
20307
+ if (promotionSnapshot) {
20308
+ adapter.setPromotionSnapshot(rekey.to, promotionSnapshot);
20309
+ }
20310
+ if (progressFallback !== null) {
20311
+ adapter.setProgressFallback(rekey.to, progressFallback);
20312
+ }
20313
+ for (const childId of rekey.childIds) {
20314
+ adapter.setActivityFields(childId, { parentId: rekey.to });
20315
+ }
20316
+ for (const linkId of rekey.outgoingLinkIds) {
20317
+ const link = adapter.getLink(linkId);
20318
+ if (link) adapter.setLinkEndpoints(linkId, rekey.to, link.target);
20319
+ }
20320
+ for (const linkId of rekey.incomingLinkIds) {
20321
+ const link = adapter.getLink(linkId);
20322
+ if (link) adapter.setLinkEndpoints(linkId, link.source, rekey.to);
20323
+ }
20324
+ const parent = adapter.getParent(rekey.to);
20325
+ if (!parent) return;
20326
+ const parentTracksNewActivity = parent.newActivityIds.some(
20327
+ (candidate) => String(candidate) === rekey.from
20328
+ );
20329
+ if (!parentTracksNewActivity) return;
20330
+ adapter.setActivityFields(parent.id, {
20331
+ newActivityIds: replaceId(parent.newActivityIds, rekey.from, rekey.to)
20332
+ });
20333
+ }
20334
+ function moveLinkIdentities(rekeys, adapter) {
20335
+ const before = /* @__PURE__ */ new Map();
20336
+ const after = [];
20337
+ for (const rekey of rekeys) {
20338
+ const current = adapter.getLink(rekey.from);
20339
+ if (!current) continue;
20340
+ before.set(rekey.from, { ...current });
20341
+ adapter.removeLink(rekey.from);
20342
+ const relocated = { ...current, id: rekey.to };
20343
+ adapter.addLink(relocated);
20344
+ after.push(relocated);
20345
+ }
20346
+ return { before, after };
20347
+ }
20348
+ function applyLocalRekeys(plan, adapter) {
20349
+ for (const rekey of plan.uniqueCorrelativeIds) {
20350
+ adapter.setActivityField(
20351
+ rekey.activityId,
20352
+ ACTIVITY_PROPERTY.UNIQUE_CORRELATIVE_ID,
20353
+ rekey.nextUniqueCorrelativeId
20354
+ );
20355
+ }
20356
+ for (const rekey of plan.customIds) {
20357
+ adapter.setActivityField(
20358
+ rekey.activityId,
20359
+ ACTIVITY_PROPERTY.CUSTOM_ID,
20360
+ rekey.nextCustomId
20361
+ );
20362
+ }
20363
+ for (const rekey of plan.activities) moveActivityIdentity(rekey, adapter);
20364
+ return moveLinkIdentities(plan.links, adapter);
20365
+ }
20366
+
20367
+ // src/dispatch/handlers/persistence-rebase.ts
20368
+ function pickDefaultCalendarId(calendars) {
20369
+ for (const calendar of calendars.values()) {
20370
+ if (calendar.is_default) return String(calendar.id);
20371
+ }
20372
+ return null;
20373
+ }
20374
+ function namesUnknownCalendar(action, calendars) {
20375
+ return action.remote.activities.some(
20376
+ (row2) => row2.calendarId != null && !calendars.has(String(row2.calendarId))
20377
+ );
20378
+ }
20379
+ function normalizeRemote(action, deps) {
20380
+ const { activities, activityIds } = normalizeActivities(
20381
+ [...action.remote.activities],
20382
+ {
20383
+ knownCalendarIds: new Set(deps.calendars.keys()),
20384
+ defaultCalendarId: pickDefaultCalendarId(deps.calendars),
20385
+ hoursPerDay: deps.sector.hoursPerDay,
20386
+ reporter: deps.reporter,
20387
+ recordDiagnostic: noopLoadDiagnosticRecorder
20388
+ }
20389
+ );
20390
+ const links = normalizeLinks([...action.remote.links], {
20391
+ knownActivityIds: activityIds,
20392
+ hoursPerDay: deps.sector.hoursPerDay,
20393
+ reporter: deps.reporter,
20394
+ recordDiagnostic: noopLoadDiagnosticRecorder
20395
+ });
20396
+ return { activities, links };
20397
+ }
20398
+ function hasPersistedRows(remote) {
20399
+ return remote.activities.every(
20400
+ (activity) => typeof activity.proplannerId === "number" && activity.proplannerId > 0
20401
+ );
20402
+ }
20403
+ async function dispatchPersistenceRebase(action, options, deps) {
20404
+ const { adapter, scheduler, sector } = deps;
20405
+ if (namesUnknownCalendar(action, deps.calendars)) {
20406
+ return { ok: false, reason: REJECTION_REASON.INVALID };
20407
+ }
20408
+ const remote = normalizeRemote(action, deps);
20409
+ if (!hasPersistedRows(remote)) {
20410
+ return { ok: false, reason: REJECTION_REASON.INVALID };
20411
+ }
20412
+ scheduler.invalidateAllCaches();
20413
+ const localLinkOrder = unsavedLinkIdsInCreationOrder(adapter);
20414
+ const rekeys = await rekeyCollidingLocalRows(remote, deps);
20415
+ const plan = planPersistenceRebase({
20416
+ remoteActivities: remote.activities,
20417
+ remoteLinks: remote.links,
20418
+ localActivities: adapter.getAllActivities(),
20419
+ localLinks: adapter.getAllLinks(),
20420
+ checkpoint: deps.persistenceCheckpoint
20421
+ });
20422
+ const siblingSources = captureSiblingOrders(
20423
+ adapter.getAllActivities(),
20424
+ remote.activities,
20425
+ deps.persistenceCheckpoint
20426
+ );
20427
+ const removedActivityIds = new Set(plan.removeActivityIds);
20428
+ const touchedIds = collectTouchedActivityIds(plan, adapter);
20429
+ const beforeSnap = await snapshotBeforeRekeys(adapter, touchedIds, rekeys);
20430
+ for (const activityId of rekeys.touchedIds) touchedIds.add(activityId);
20431
+ const beforeLinks = captureLinksBefore(plan, removedActivityIds, adapter);
20432
+ const viewStateChanges2 = captureDeletedViewState(adapter, removedActivityIds);
20433
+ for (const linkId of beforeLinks.keys()) adapter.removeLink(linkId);
20434
+ for (const activityId of removedActivityIds)
20435
+ adapter.removeActivity(activityId);
20436
+ for (const activity of plan.createActivities) {
20437
+ adapter.addActivity(structuredCloneActivity(activity));
20438
+ deps.uidGen.bump(activity.uniqueCorrelativeId);
20439
+ deps.customIdTracker.populateFromActivities([activity]);
20440
+ }
20441
+ for (const merge of plan.updateActivities) {
20442
+ adapter.setActivityFields(merge.activityId, merge.fields);
20443
+ }
20444
+ deriveLeafEndDates(plan, adapter);
20445
+ const orphanConflicts = reparentOrphans(
20446
+ adapter,
20447
+ deps.persistenceCheckpoint,
20448
+ touchedIds
20449
+ );
20450
+ demoteEmptiedParents(deps, touchedIds);
20451
+ normalizeMergedParents(adapter, touchedIds);
20452
+ recomputeTouchedRollups(adapter, touchedIds);
20453
+ const linkChanges = [
20454
+ ...rekeyedLinkChanges(rekeys.links),
20455
+ ...applyLinkPlan(plan, beforeLinks, adapter)
20456
+ ];
20457
+ const droppedLinkConflicts = dropLinksWithoutEndpoints(adapter, linkChanges);
20458
+ const cycleRepair = breakDependencyCycles(
20459
+ adapter,
20460
+ followRekeys(localLinkOrder, rekeys.plan.links),
20461
+ linkChanges
20462
+ );
20463
+ const createdIds = new Set(
20464
+ plan.createActivities.map((activity) => String(activity.id))
20465
+ );
20466
+ const insertedIds = /* @__PURE__ */ new Set([
20467
+ ...createdIds,
20468
+ ...rekeys.plan.activities.map((rekey) => rekey.to)
20469
+ ]);
20470
+ const siblingOrder = applySiblingOrder(adapter, siblingSources);
20471
+ const correlativeShifts = [
20472
+ ...siblingOrder.shifts,
20473
+ ...recomputeCorrelativeIds(adapter)
20474
+ ].filter((shift) => !insertedIds.has(shift.activityId));
20475
+ const { scheduledIds } = await runPostMutation(
20476
+ {
20477
+ adapter,
20478
+ scheduler,
20479
+ sector,
20480
+ defaultBaseCalendarId: deps.defaultBaseCalendarId,
20481
+ runAutomaticP6Schedule: deps.runAutomaticP6Schedule
20482
+ },
20483
+ {
20484
+ action,
20485
+ autoscheduleFrom: AUTOSCHEDULE_SCOPE.ROOTS,
20486
+ recomputeParentsFrom: withChildrenOfParents(touchedIds, adapter),
20487
+ recomputeParentProgress: true,
20488
+ now: deps.now,
20489
+ options
20490
+ }
20491
+ );
20492
+ alignParentAutoScheduling(adapter, touchedIds);
20493
+ const changes = await assembleChangeSet(adapter, {
20494
+ source: action,
20495
+ beforeSnap,
20496
+ touchedIds,
20497
+ scheduledIds,
20498
+ links: linkChanges,
20499
+ hoursPerDay: sector.hoursPerDay,
20500
+ correlativeShifts
20501
+ });
20502
+ const report = {
20503
+ remoteChanges: plan.remoteChanges,
20504
+ conflicts: [
20505
+ ...plan.conflicts,
20506
+ ...orphanConflicts,
20507
+ ...siblingOrder.conflicts,
20508
+ ...droppedLinkConflicts,
20509
+ ...cycleRepair.conflicts
20510
+ ],
20511
+ createdActivityIds: [...createdIds],
20512
+ removedActivityIds: [...removedActivityIds],
20513
+ rekeyedActivities: rekeys.plan.activities.map(({ from, to }) => ({
20514
+ from,
20515
+ to
20516
+ })),
20517
+ rekeyedLinks: rekeys.plan.links,
20518
+ createdLinkIds: plan.createLinks.map((link) => String(link.id)),
20519
+ removedLinkIds: linkIdsGoneFromState(linkChanges, adapter)
20520
+ };
20521
+ return {
20522
+ ok: true,
20523
+ changes: viewStateChanges2.length > 0 ? { ...changes, viewState: viewStateChanges2 } : changes,
20524
+ rebase: report,
20525
+ __beforeSnap: beforeSnap,
20526
+ __beforeLinks: new Map([
20527
+ ...rekeys.links.before,
20528
+ ...beforeLinks,
20529
+ ...cycleRepair.removed
20530
+ ]),
20531
+ __rebaseCheckpoint: { activities: remote.activities, links: remote.links }
20532
+ };
20533
+ }
20534
+ function linkIdsGoneFromState(linkChanges, adapter) {
20535
+ return linkChanges.filter((change) => change.kind === ENTITY_CHANGE_KIND.DELETED).map((change) => String(change.id)).filter((linkId) => !adapter.getLink(linkId));
20536
+ }
20537
+ async function rekeyCollidingLocalRows(remote, deps) {
20538
+ const plan = planLocalRekeys(remote, deps);
20539
+ const touchedIds = collectRekeyTouchedIds(plan);
20540
+ const beforeSnap = await snapshotActivities(deps.adapter, touchedIds);
20541
+ const links = applyLocalRekeys(plan, deps.adapter);
20542
+ return { plan, touchedIds, beforeSnap, links };
20543
+ }
20544
+ function rekeyedLinkChanges(links) {
20545
+ const changes = [
20546
+ ...buildLinkDeletions(new Map(links.before))
20547
+ ];
20548
+ for (const after of links.after) {
20549
+ changes.push({
20550
+ id: String(after.id),
20551
+ kind: ENTITY_CHANGE_KIND.CREATED,
20552
+ fields: {
20553
+ source: { before: void 0, after: String(after.source) },
20554
+ target: { before: void 0, after: String(after.target) },
20555
+ type: { before: void 0, after: after.type },
20556
+ lag: { before: void 0, after: Number(after.lag) }
20557
+ },
20558
+ after: snapshotToLink(after)
20559
+ });
20560
+ }
20561
+ return changes;
20562
+ }
20563
+ async function snapshotBeforeRekeys(adapter, touchedIds, rekeys) {
20564
+ const touchedOutsideRekey = new Set(
20565
+ [...touchedIds].filter((activityId) => !rekeys.touchedIds.has(activityId))
20566
+ );
20567
+ return new Map([
20568
+ ...await snapshotActivities(adapter, touchedOutsideRekey),
20569
+ ...rekeys.beforeSnap
20570
+ ]);
20571
+ }
20572
+ function deriveLeafEndDates(plan, adapter) {
20573
+ for (const merge of plan.updateActivities) {
20574
+ const activity = adapter.getActivity(merge.activityId);
20575
+ if (!activity || adapter.hasChildren(activity.id)) continue;
20576
+ adapter.setActivityField(
20577
+ activity.id,
20578
+ ACTIVITY_PROPERTY.END_DATE,
20579
+ adapter.calculateEndDate({
20580
+ startDate: activity.startDate,
20581
+ durationHours: activity.durationHours,
20582
+ task: activity
20583
+ })
20584
+ );
20585
+ }
20586
+ }
20587
+ function unsavedLinkIdsInCreationOrder(adapter) {
20588
+ return adapter.getAllLinks().filter((link) => {
20589
+ const proplannerId = link.proplannerId;
20590
+ return proplannerId === null || proplannerId === void 0 || proplannerId === 0;
20591
+ }).map((link) => String(link.id)).sort((left, right) => Number(left) - Number(right));
20592
+ }
20593
+ function followRekeys(linkIds, rekeys) {
20594
+ const renamed = new Map(rekeys.map(({ from, to }) => [from, to]));
20595
+ return linkIds.map((linkId) => renamed.get(linkId) ?? linkId);
20596
+ }
20597
+ function alignParentAutoScheduling(adapter, touchedIds) {
20598
+ for (const activityId of touchedIds) {
20599
+ if (!adapter.hasChildren(activityId)) continue;
20600
+ const activity = adapter.getActivity(activityId);
20601
+ if (!activity) continue;
20602
+ const autoScheduling = isAutoSchedulingEnabledForProgress(
20603
+ activity.progress
20604
+ );
20605
+ if (activity.autoScheduling === autoScheduling) continue;
20606
+ adapter.setActivityField(
20607
+ activityId,
20608
+ ACTIVITY_PROPERTY.AUTO_SCHEDULING,
20609
+ autoScheduling
20610
+ );
20611
+ }
20612
+ }
20613
+ function normalizeMergedParents(adapter, touchedIds) {
20614
+ for (const activityId of touchedIds) {
20615
+ if (!adapter.hasChildren(activityId)) continue;
20616
+ const activity = adapter.getActivity(activityId);
20617
+ if (!activity) continue;
20618
+ if (activity.type !== ACTIVITY_TYPE.PROJECT) {
20619
+ adapter.setActivityField(
20620
+ activityId,
20621
+ ACTIVITY_PROPERTY.TYPE,
20622
+ ACTIVITY_TYPE.PROJECT
20623
+ );
20624
+ }
20625
+ if (activity.customId) {
20626
+ adapter.setActivityField(activityId, ACTIVITY_PROPERTY.CUSTOM_ID, null);
20627
+ }
20628
+ }
20629
+ }
20630
+ function demoteEmptiedParents(deps, touchedIds) {
20631
+ const { adapter } = deps;
20632
+ for (const activityId of touchedIds) {
20633
+ const activity = adapter.getActivity(activityId);
20634
+ if (!activity || activity.type !== ACTIVITY_TYPE.PROJECT) continue;
20635
+ if (isRootParent(activity.parentId) || adapter.hasChildren(activityId)) {
20636
+ continue;
20637
+ }
20638
+ const demotion = buildParentDemotionMutations({
20639
+ parent: activity,
20640
+ remainingChildIds: [],
20641
+ defaultDurationHours: deps.sector.hoursPerDay,
20642
+ promotionSnapshot: adapter.getPromotionSnapshot(activityId),
20643
+ computeEndDate: (startDate, durationHours) => adapter.calculateEndDate({ startDate, durationHours, task: activity }),
20644
+ dataDateCapability: deps.dataDateCapability,
20645
+ hasCompleteSchedulingState: deps.hasCompleteSchedulingState
20646
+ });
20647
+ if (!demotion) continue;
20648
+ adapter.setActivityFields(activityId, demotion.fields);
20649
+ const previousSiblingId = findPrevSiblingHelper(activityId, adapter);
20650
+ const previousCustomId = previousSiblingId ? adapter.getActivity(previousSiblingId)?.customId : null;
20651
+ adapter.setActivityField(
20652
+ activityId,
20653
+ ACTIVITY_PROPERTY.CUSTOM_ID,
20654
+ deps.customIdTracker.generateCustomIdForRestoredChild(
20655
+ previousCustomId || null
20656
+ )
20657
+ );
20658
+ }
20659
+ }
20660
+ function recomputeTouchedRollups(adapter, touchedIds) {
20661
+ const parentIds = /* @__PURE__ */ new Set();
20662
+ for (const activityId of touchedIds) {
20663
+ const activity = adapter.getActivity(activityId);
20664
+ if (!activity) continue;
20665
+ if (adapter.hasChildren(activityId)) parentIds.add(activityId);
20666
+ if (!isRootParent(activity.parentId)) {
20667
+ parentIds.add(String(activity.parentId));
20668
+ }
20669
+ }
20670
+ for (const parentId of parentIds) {
20671
+ recomputeRollupCascadesForParent(parentId, adapter);
20672
+ }
20673
+ }
20674
+ function withChildrenOfParents(activityIds, adapter) {
20675
+ const seeds = new Set(activityIds);
20676
+ for (const activityId of activityIds) {
20677
+ for (const childId of adapter.getChildren(activityId)) {
20678
+ seeds.add(String(childId));
20679
+ }
20680
+ }
20681
+ return seeds;
20682
+ }
20683
+ function collectTouchedActivityIds(plan, adapter) {
20684
+ const touched = /* @__PURE__ */ new Set();
20685
+ const touchParent = (parentId) => {
20686
+ if (parentId === null || parentId === void 0) return;
20687
+ if (isRootParent(parentId)) return;
20688
+ touched.add(String(parentId));
20689
+ };
20690
+ for (const activityId of plan.removeActivityIds) {
20691
+ touched.add(activityId);
20692
+ touchParent(adapter.getActivity(activityId)?.parentId);
20693
+ }
20694
+ for (const activity of plan.createActivities) {
20695
+ touched.add(String(activity.id));
20696
+ touchParent(activity.parentId);
20697
+ }
20698
+ for (const merge of plan.updateActivities) {
20699
+ touched.add(merge.activityId);
20700
+ touchParent(adapter.getActivity(merge.activityId)?.parentId);
20701
+ if (merge.fields.parentId !== void 0) touchParent(merge.fields.parentId);
20702
+ }
20703
+ return touched;
20704
+ }
20705
+ function captureLinksBefore(plan, removedActivityIds, adapter) {
20706
+ const before = /* @__PURE__ */ new Map();
20707
+ const incident = collectIncidentLinkIds(removedActivityIds, adapter);
20708
+ for (const linkId of [...plan.removeLinkIds, ...incident]) {
20709
+ const link = adapter.getLink(linkId);
20710
+ if (link) before.set(String(linkId), { ...link });
20711
+ }
20712
+ return before;
20713
+ }
20714
+ function nearestSurvivingAncestor(missingParentId, adapter, checkpoint) {
20715
+ const visited = /* @__PURE__ */ new Set();
20716
+ let current = missingParentId;
20717
+ while (current !== null && !visited.has(current)) {
20718
+ if (adapter.activityExists(current)) return current;
20719
+ visited.add(current);
20720
+ const saved = checkpoint.checkpointActivity(current);
20721
+ if (!saved || isRootParent(saved.parentId)) return null;
20722
+ current = String(saved.parentId);
20723
+ }
20724
+ return null;
20725
+ }
20726
+ function reparentOrphans(adapter, checkpoint, touchedIds) {
20727
+ const conflicts = [];
20728
+ for (const activity of adapter.getAllActivities()) {
20729
+ if (isRootParent(activity.parentId)) continue;
20730
+ if (adapter.activityExists(String(activity.parentId))) continue;
20731
+ const previousParentId = String(activity.parentId);
20732
+ const adoptiveParentId = nearestSurvivingAncestor(
20733
+ previousParentId,
20734
+ adapter,
20735
+ checkpoint
20736
+ );
20737
+ adapter.setActivityField(
20738
+ activity.id,
20739
+ ACTIVITY_PROPERTY.PARENT,
20740
+ adoptiveParentId
20741
+ );
20742
+ touchedIds.add(String(activity.id));
20743
+ if (adoptiveParentId !== null) touchedIds.add(adoptiveParentId);
20744
+ conflicts.push({
20745
+ entity: PERSISTENCE_REBASE_ENTITY.ACTIVITY,
20746
+ id: String(activity.id),
20747
+ kind: PERSISTENCE_REBASE_CONFLICT_KIND.PARENT_REMOVED,
20748
+ field: ACTIVITY_PROPERTY.PARENT,
20749
+ mine: previousParentId,
20750
+ theirs: adoptiveParentId ?? ROOT_PARENT_ID
20751
+ });
20752
+ }
20753
+ return conflicts;
20754
+ }
20755
+ function applyLinkPlan(plan, beforeLinks, adapter) {
20756
+ const changes = [
20757
+ ...buildLinkDeletions(new Map(beforeLinks))
20758
+ ];
20759
+ for (const link of plan.createLinks) {
20760
+ if (adapter.getLink(link.id)) continue;
20761
+ adapter.addLink({ ...link, freeSlackHours: null, isCritical: false });
20762
+ const after = adapter.getLink(link.id);
20763
+ if (!after) continue;
20764
+ changes.push({
20765
+ id: String(link.id),
20766
+ kind: ENTITY_CHANGE_KIND.CREATED,
20767
+ fields: {
20768
+ source: { before: void 0, after: String(after.source) },
20769
+ target: { before: void 0, after: String(after.target) },
20770
+ type: { before: void 0, after: after.type },
20771
+ lag: { before: void 0, after: Number(after.lag) }
20772
+ },
20773
+ after: snapshotToLink(after)
20774
+ });
20775
+ }
20776
+ for (const merge of plan.updateLinks) {
20777
+ const before = adapter.getLink(merge.linkId);
20778
+ if (!before) continue;
20779
+ const beforeView = snapshotToLink(before);
20780
+ const fields = {};
20781
+ for (const [field, value] of Object.entries(merge.fields)) {
20782
+ adapter.setLinkFieldDynamic(merge.linkId, field, value);
20783
+ fields[field] = { before: Reflect.get(beforeView, field), after: value };
20784
+ }
20785
+ const after = adapter.getLink(merge.linkId);
20786
+ if (!after) continue;
20787
+ changes.push({
20788
+ id: String(merge.linkId),
20789
+ kind: ENTITY_CHANGE_KIND.UPDATED,
20790
+ fields,
20791
+ after: snapshotToLink(after)
20792
+ });
20793
+ }
20794
+ return changes;
20795
+ }
20796
+ function dropLinksWithoutEndpoints(adapter, changes) {
20797
+ const conflicts = [];
20798
+ for (const link of adapter.getAllLinks()) {
20799
+ const missingEndpoint = [link.source, link.target].find(
20800
+ (activityId) => !adapter.activityExists(String(activityId))
20801
+ );
20802
+ if (missingEndpoint === void 0) continue;
20803
+ const linkId = String(link.id);
20804
+ const before = { ...link };
20805
+ const sourceIsMissing = String(link.source) === String(missingEndpoint);
20806
+ adapter.removeLink(linkId);
20807
+ changes.push(...buildLinkDeletions(/* @__PURE__ */ new Map([[linkId, before]])));
20808
+ conflicts.push({
20809
+ entity: PERSISTENCE_REBASE_ENTITY.LINK,
20810
+ id: linkId,
20811
+ kind: PERSISTENCE_REBASE_CONFLICT_KIND.DELETED_REMOTELY,
20812
+ field: sourceIsMissing ? LINK_PROPERTY.SOURCE : LINK_PROPERTY.TARGET,
20813
+ mine: snapshotToLink(before),
20814
+ theirs: null
20815
+ });
20816
+ }
20817
+ return conflicts;
20818
+ }
20819
+
20820
+ // src/baselines/apply-baseline-points.ts
20821
+ function applyBaselinePoints(adapter, points, reporter, recordDiagnostic = noopLoadDiagnosticRecorder) {
20822
+ const idByProplanner = /* @__PURE__ */ new Map();
20823
+ adapter.forEachActivity((a, id) => {
20824
+ const pid = a.proplannerId;
20825
+ if (typeof pid === "number") idByProplanner.set(pid, id);
20826
+ });
20827
+ const grouped = /* @__PURE__ */ new Map();
20828
+ for (const p of points) {
20829
+ const activityId = p.activityId;
20830
+ if (typeof activityId !== "number") {
20831
+ recordDiagnostic(
20832
+ LOAD_DIAGNOSTIC_CODE.INVALID_BASELINE_POINT,
20833
+ String(activityId ?? "unknown")
20834
+ );
20835
+ continue;
20836
+ }
20837
+ const coreId = idByProplanner.get(activityId);
20838
+ if (coreId === void 0) {
20839
+ recordDiagnostic(
20840
+ LOAD_DIAGNOSTIC_CODE.INVALID_BASELINE_POINT,
20841
+ String(activityId)
20842
+ );
20843
+ continue;
20844
+ }
20845
+ const arr = grouped.get(coreId);
20846
+ if (arr) arr.push(p);
20847
+ else grouped.set(coreId, [p]);
20848
+ }
20849
+ const changed2 = [];
20850
+ for (const [coreId, group] of grouped) {
20851
+ const isMilestone = adapter.getActivity(coreId)?.type === ACTIVITY_TYPE.MILESTONE;
20852
+ adapter.setActivityField(
20853
+ coreId,
20854
+ "baselinePoints",
20855
+ group.map(
20856
+ (point) => normalizeBaselinePoint(
20857
+ point,
20858
+ reporter,
20859
+ {
20860
+ record: recordDiagnostic,
20861
+ sampleId: coreId
20862
+ },
20863
+ isMilestone
20864
+ )
20865
+ ).filter((point) => point !== null)
20866
+ );
20867
+ changed2.push(coreId);
20868
+ }
20869
+ adapter.forEachActivity((a, id) => {
20870
+ if (grouped.has(id)) return;
20871
+ const existing = a.baselinePoints;
20872
+ if (Array.isArray(existing) && existing.length > 0) {
20873
+ adapter.setActivityField(id, "baselinePoints", []);
20874
+ changed2.push(id);
20875
+ }
20876
+ });
20877
+ return changed2;
20878
+ }
20879
+
20880
+ // src/baselines/baseline-snapshot.ts
20881
+ function buildBaselineSnapshot(activity) {
20882
+ const active = getActiveBaseline(activity);
20883
+ if (!active) return null;
20884
+ return {
20885
+ startDate: active.startDate,
20886
+ endDate: active.endDate,
20887
+ durationDays: active.durationDays,
20888
+ cost: active.cost,
20889
+ workHours: active.workHours
20890
+ };
20891
+ }
20892
+ function emitBaselineSnapshotColumns(state) {
20893
+ const changed2 = [];
20894
+ state.forEachActivity((activity, id) => {
20895
+ const snapshot = buildBaselineSnapshot(activity);
20896
+ if (!snapshot) {
20897
+ if (activity.baselineSnapshot !== null) {
20898
+ state.setActivityField(id, "baselineSnapshot", null);
20899
+ changed2.push(id);
20900
+ }
20901
+ return;
20902
+ }
20903
+ state.setActivityField(id, "baselineSnapshot", snapshot);
20904
+ changed2.push(id);
20905
+ });
20906
+ return changed2;
20907
+ }
20908
+
20909
+ // src/baselines/apply-baseline-overlay.ts
20910
+ function applyBaselineOverlay(state, points, now, reporter, defaultBaseCalendarId = null, recordDiagnostic = noopLoadDiagnosticRecorder) {
20911
+ const changed2 = /* @__PURE__ */ new Set();
20912
+ for (const id of applyBaselinePoints(
20913
+ state,
20914
+ points,
20915
+ reporter,
20916
+ recordDiagnostic
20917
+ )) {
20918
+ changed2.add(id);
20919
+ }
20920
+ if (now) {
20921
+ for (const id of runExpectedProgressBase(state, now, defaultBaseCalendarId))
20922
+ changed2.add(id);
20923
+ }
20924
+ for (const id of emitBaselineSnapshotColumns(state)) changed2.add(id);
20925
+ for (const id of emitRealCost(state)) changed2.add(id);
20926
+ return [...changed2];
20927
+ }
20928
+
20929
+ // src/propagations/downward/recompute-all-ponderators.ts
20930
+ function recomputeAllPonderators(criterion, adapter) {
20931
+ const basisById = /* @__PURE__ */ new Map();
20932
+ adapter.forEachActivity((_a, id) => {
20933
+ adapter.setActivityField(id, COLUMN.PONDERATOR, 0);
20934
+ });
20935
+ adapter.forEachActivity((_a, id) => {
20936
+ if (adapter.getChildren(id).length > 0) {
20937
+ recomputePonderatorsForParent(String(id), criterion, adapter, basisById);
20938
+ }
20939
+ });
20940
+ }
20941
+
20942
+ // src/dispatch/shared/baseline-weighted-fields.ts
20943
+ function recomputeBaselineWeightedFields(deps, criterion) {
20944
+ recomputeAllPonderators(criterion, deps.adapter);
20945
+ recomputeAllProgressRollup(deps.adapter, criterion);
20946
+ if (deps.now) {
20947
+ runExpectedProgressBase(
20948
+ deps.adapter,
20949
+ deps.now,
20950
+ deps.defaultBaseCalendarId ?? null
20951
+ );
20952
+ applyExpectedProgressLive(deps.adapter, deps.now);
20953
+ }
20954
+ emitRealCost(deps.adapter);
20955
+ if (deps.now) applyStatusPass(deps.adapter, deps.sector.statusCriteria);
20956
+ }
20957
+
20958
+ // src/dispatch/handlers/baseline-apply.ts
20959
+ function activeBaselineSignature(deps) {
20960
+ return JSON.stringify(
20961
+ deps.adapter.getAllIds().map((id) => {
20962
+ const point = deps.adapter.getActivity(id)?.baselinePoints.find((candidate) => candidate.isActiveVersion);
20963
+ return point ? [
20964
+ String(id),
20965
+ point.versionId,
20966
+ point.startDate?.getTime() ?? null,
20967
+ point.endDate?.getTime() ?? null,
20968
+ point.durationDays,
20969
+ point.cost,
20970
+ point.workHours,
20971
+ point.baseCalendarId,
20972
+ point.hoursPerDay,
20973
+ point.hoursPerWeek
20974
+ ] : [String(id), null];
20975
+ })
20976
+ );
20977
+ }
20978
+ async function dispatchBaselineApply(action, deps) {
20979
+ const beforeSnap = await snapshotActivities(
20980
+ deps.adapter,
20981
+ new Set(deps.adapter.getAllIds())
20982
+ );
20983
+ const activeBaselineBefore = activeBaselineSignature(deps);
20984
+ const changedIds = applyBaselineOverlay(
20985
+ deps.adapter,
20986
+ action.points,
20987
+ deps.now,
20988
+ deps.reporter,
20989
+ deps.defaultBaseCalendarId ?? null,
20990
+ void 0
20991
+ );
20992
+ applyBaselineVariancePass(deps.adapter, deps.sector.hoursPerDay);
20993
+ applyRoleBaselineVariancePass(deps.adapter, deps.sector.hoursPerDay);
20994
+ if (activeBaselineSignature(deps) !== activeBaselineBefore) {
20995
+ recomputeBaselineWeightedFields(
20996
+ deps,
20997
+ deps.sector.activityCreter ?? ACTIVITY_PONDERATOR_CRITERION.DURATION
20998
+ );
20999
+ }
21000
+ return {
21001
+ ok: true,
21002
+ changes: await assembleChangeSet(deps.adapter, {
21003
+ source: action,
21004
+ beforeSnap,
21005
+ touchedIds: changedIds,
21006
+ scheduledIds: [],
21007
+ hoursPerDay: deps.sector.hoursPerDay
21008
+ })
21009
+ };
21010
+ }
21011
+
21012
+ // src/dispatch/handlers/ponderator-criterion-set.ts
21013
+ function isValidCriterion(value) {
21014
+ return Object.values(ACTIVITY_PONDERATOR_CRITERION).includes(
21015
+ value
21016
+ );
21017
+ }
21018
+ async function dispatchPonderatorCriterionSet(action, deps) {
21019
+ if (!isValidCriterion(action.criterion)) {
21020
+ return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
21021
+ }
21022
+ const allIds = new Set(deps.adapter.getAllIds());
21023
+ const beforeSnap = await snapshotActivities(deps.adapter, allIds);
21024
+ recomputeBaselineWeightedFields(deps, action.criterion);
21025
+ deps.sector.activityCreter = action.criterion;
21026
+ return {
21027
+ ok: true,
21028
+ changes: await assembleChangeSet(deps.adapter, {
21029
+ source: action,
21030
+ beforeSnap,
21031
+ touchedIds: allIds,
21032
+ scheduledIds: [],
21033
+ hoursPerDay: deps.sector.hoursPerDay
21034
+ })
21035
+ };
21036
+ }
21037
+
21038
+ // src/dispatch/handlers/status-criteria-set.ts
21039
+ async function dispatchStatusCriteriaSet(action, deps) {
21040
+ const resolved = resolveStatusCriteria(action.criteria);
21041
+ const allIds = new Set(deps.adapter.getAllIds());
21042
+ const beforeSnap = await snapshotActivities(deps.adapter, allIds);
21043
+ const changedIds = applyStatusPass(deps.adapter, resolved);
21044
+ deps.sector.statusCriteria = resolved;
21045
+ return {
21046
+ ok: true,
21047
+ changes: await assembleChangeSet(deps.adapter, {
21048
+ source: action,
21049
+ beforeSnap,
21050
+ touchedIds: changedIds,
21051
+ scheduledIds: [],
21052
+ hoursPerDay: deps.sector.hoursPerDay
21053
+ })
21054
+ };
21055
+ }
21056
+
21057
+ // src/dispatch/handlers/calendar-reconcile.ts
21058
+ async function dispatchCalendarReconcile(action, options, deps) {
21059
+ const beforeSnap = /* @__PURE__ */ new Map();
21060
+ const touchedIds = /* @__PURE__ */ new Set();
21061
+ for (const activity of deps.adapter.getAllActivities()) {
21062
+ if (!shouldReconcile(activity, deps)) continue;
21063
+ const calendar = deps.ctx.calendars.getCalendar(activity.calendarId);
21064
+ if (!calendar) continue;
21065
+ const startReference = calendar.isWorkTime(
21066
+ activity.startDate,
21067
+ CALENDAR_UNIT.HOUR
21068
+ ) ? startOfUtcDay4(activity.startDate) : activity.startDate;
21069
+ const startDate = calendar.getClosestWorkTime({
21070
+ date: startReference,
21071
+ direction: WORK_TIME_DIRECTION.FUTURE,
21072
+ unit: CALENDAR_UNIT.HOUR
21073
+ });
21074
+ const endDate = calendar.calculateEndDate(
21075
+ startDate,
21076
+ activity.durationHours
21077
+ );
21078
+ if (sameInstant(startDate, activity.startDate) && sameInstant(endDate, activity.endDate)) {
21079
+ continue;
21080
+ }
21081
+ const activityId = String(activity.id);
21082
+ beforeSnap.set(activityId, structuredCloneActivity(activity));
21083
+ touchedIds.add(activityId);
21084
+ deps.adapter.setActivityFields(activityId, { startDate, endDate });
21085
+ }
21086
+ const { scheduledIds } = await runPostMutation(
21087
+ {
21088
+ adapter: deps.adapter,
21089
+ scheduler: deps.scheduler,
21090
+ sector: deps.sector,
21091
+ defaultBaseCalendarId: deps.defaultBaseCalendarId,
21092
+ runAutomaticP6Schedule: deps.runAutomaticP6Schedule
21093
+ },
21094
+ {
21095
+ action,
21096
+ autoscheduleFrom: AUTOSCHEDULE_SCOPE.ROOTS,
21097
+ recomputeParentsFrom: touchedIds,
19175
21098
  now: deps.now,
19176
21099
  options
19177
21100
  }
@@ -19869,6 +21792,9 @@ async function performDispatch(action, options, deps) {
19869
21792
  if (action.kind === DISPATCH_ACTION_KIND.PERSISTENCE_ACKNOWLEDGE) {
19870
21793
  return dispatchPersistenceAcknowledge(action, deps);
19871
21794
  }
21795
+ if (action.kind === DISPATCH_ACTION_KIND.PERSISTENCE_REBASE) {
21796
+ return dispatchPersistenceRebase(action, options, deps);
21797
+ }
19872
21798
  if (action.kind === DISPATCH_ACTION_KIND.BASELINE_APPLY) {
19873
21799
  return dispatchBaselineApply(action, deps);
19874
21800
  }
@@ -19975,18 +21901,63 @@ function selectionDeps(deps) {
19975
21901
  }
19976
21902
 
19977
21903
  // src/columns/shared/field-types.ts
19978
- var DATE_FIELDS = /* @__PURE__ */ new Set([
21904
+ var DATE_FIELDS2 = /* @__PURE__ */ new Set([
19979
21905
  "startDate",
19980
21906
  "endDate",
19981
21907
  "constraintDate"
19982
21908
  ]);
19983
21909
  function coerceActivityFieldValue(field, value) {
19984
- if (DATE_FIELDS.has(field) && typeof value === "string") {
21910
+ if (DATE_FIELDS2.has(field) && typeof value === "string") {
19985
21911
  return new Date(value);
19986
21912
  }
19987
21913
  return value;
19988
21914
  }
19989
21915
 
21916
+ // src/calendar/closest-past-work-time.ts
21917
+ var MS_PER_MINUTE = 60 * 1e3;
21918
+ var MS_PER_DAY = 24 * 60 * MS_PER_MINUTE;
21919
+ var MINUTES_PER_HOUR = 60;
21920
+ var MAX_LOOKBACK_DAYS = 3660;
21921
+ var LAST_MINUTE_OF_DAY_MS = MS_PER_DAY - MS_PER_MINUTE;
21922
+ var WINDOW_SEPARATOR = "-";
21923
+ var TIME_SEPARATOR = ":";
21924
+ function closestPastWorkTime(calendar, date2, unit) {
21925
+ if (calendar.isWorkTime({ date: date2, unit })) return date2;
21926
+ const dayStart = utcDayStart(date2);
21927
+ if (unit !== CALENDAR_UNIT.DAY) {
21928
+ const elapsed = date2.getTime() - dayStart;
21929
+ const sameDayEnd = latestWindowEnd(calendar, dayStart, elapsed);
21930
+ if (sameDayEnd !== null) return new Date(dayStart + sameDayEnd);
21931
+ }
21932
+ const searchesFromWorkingDay = calendar.getWorkHours(date2).length > 0;
21933
+ for (let back = 1; back <= MAX_LOOKBACK_DAYS; back++) {
21934
+ const candidate = dayStart - back * MS_PER_DAY;
21935
+ const lastEnd = latestWindowEnd(calendar, candidate, MS_PER_DAY);
21936
+ if (lastEnd === null) continue;
21937
+ if (unit !== CALENDAR_UNIT.DAY) {
21938
+ const end = searchesFromWorkingDay ? Math.min(lastEnd, LAST_MINUTE_OF_DAY_MS) : lastEnd;
21939
+ return new Date(candidate + end);
21940
+ }
21941
+ return new Date(candidate + MS_PER_DAY);
21942
+ }
21943
+ throw new Error(
21944
+ `No working day found within ${MAX_LOOKBACK_DAYS} days before ${date2.toISOString()}`
21945
+ );
21946
+ }
21947
+ function utcDayStart(date2) {
21948
+ return Date.UTC(date2.getUTCFullYear(), date2.getUTCMonth(), date2.getUTCDate());
21949
+ }
21950
+ function latestWindowEnd(calendar, dayStart, atOrBefore) {
21951
+ const ends = calendar.getWorkHours(new Date(dayStart)).map(windowEndMs).filter((end) => end <= atOrBefore);
21952
+ return ends.length ? Math.max(...ends) : null;
21953
+ }
21954
+ function windowEndMs(window2) {
21955
+ const end = window2.split(WINDOW_SEPARATOR)[1] ?? "";
21956
+ const [hours = "0", minutes = "0"] = end.split(TIME_SEPARATOR);
21957
+ const endMs = (Number(hours) * MINUTES_PER_HOUR + Number(minutes)) * MS_PER_MINUTE;
21958
+ return endMs;
21959
+ }
21960
+
19990
21961
  // src/calendar/adapter.ts
19991
21962
  var WORK_CALENDAR_UNIT = { MINUTE: "minute" };
19992
21963
  function createWorkCalendarAdapter(instance, options = {}) {
@@ -20002,6 +21973,9 @@ function createWorkCalendarAdapter(instance, options = {}) {
20002
21973
  function wrapWorkCalendar(calendar, defaultUnit) {
20003
21974
  return {
20004
21975
  getClosestWorkTime(query) {
21976
+ if (query.direction === WORK_TIME_DIRECTION.PAST) {
21977
+ return closestPastWorkTime(calendar, query.date, query.unit);
21978
+ }
20005
21979
  return calendar.getClosestWorkTime({
20006
21980
  date: query.date,
20007
21981
  dir: query.direction,
@@ -22898,7 +24872,7 @@ var CalendarCalculator = class {
22898
24872
  this.reader = setup.reader;
22899
24873
  }
22900
24874
  resolveCalendar(task) {
22901
- const calendarId = resolveCalendarId(task);
24875
+ const calendarId = resolveCalendarId2(task);
22902
24876
  return this.reader.getCalendar(calendarId ?? DEFAULT_CALENDAR_ID);
22903
24877
  }
22904
24878
  getClosestWorkTime(params) {
@@ -22955,14 +24929,14 @@ function memoize(cache, key, compute) {
22955
24929
  return result;
22956
24930
  }
22957
24931
  var DEFAULT_CALENDAR_KEY = "__default__";
22958
- function resolveCalendarId(task) {
24932
+ function resolveCalendarId2(task) {
22959
24933
  if (!task) return null;
22960
24934
  const calendarId = task.calendarId;
22961
24935
  if (calendarId == null) return null;
22962
24936
  return String(calendarId);
22963
24937
  }
22964
24938
  function calendarKey(task) {
22965
- return resolveCalendarId(task) ?? DEFAULT_CALENDAR_KEY;
24939
+ return resolveCalendarId2(task) ?? DEFAULT_CALENDAR_KEY;
22966
24940
  }
22967
24941
 
22968
24942
  // src/internal/state/hierarchy-index.ts
@@ -24247,43 +26221,6 @@ function computeProjectWorkHours(calendars) {
24247
26221
  return { startHour: earliestTime(starts), endHour: latestTime(ends) };
24248
26222
  }
24249
26223
 
24250
- // src/boundary/backend/constants.ts
24251
- var DAY_MS = 24 * 60 * 60 * 1e3;
24252
- var EXCEPTION_DATE_FORMAT_REGEX = /^(\d{4})[/-](\d{1,2})[/-](\d{1,2})/;
24253
- var WEEKDAYS_PER_WEEK = 7;
24254
- var EXCEPTION_EVERY_TYPE = {
24255
- DAILY: "DAILY",
24256
- WEEKLY: "WEEKLY",
24257
- MONTHLY: "MONTHLY",
24258
- YEARLY: "YEARLY"
24259
- };
24260
- var MONTHS_PER_YEAR = 12;
24261
- var CALENDAR_FALLBACK_WORKING_DAYS = [false, true, true, true, true, true, false];
24262
- var WORKING_DAY_TRUE_VALUES = /* @__PURE__ */ new Set([
24263
- "1",
24264
- "true"
24265
- ]);
24266
- var DEFAULT_ACTIVITY_CRETER = ACTIVITY_PONDERATOR_CRITERION.DURATION;
24267
- var DEFAULT_CONSTRAINT_TYPE = CONSTRAINT_TYPE.ASAP;
24268
- var CONSTRAINT_LABEL_MAP = {
24269
- ASAP: CONSTRAINT_TYPE.ASAP,
24270
- ALAP: CONSTRAINT_TYPE.ALAP,
24271
- SNET: CONSTRAINT_TYPE.SNET,
24272
- SNLT: CONSTRAINT_TYPE.SNLT,
24273
- FNET: CONSTRAINT_TYPE.FNET,
24274
- FNLT: CONSTRAINT_TYPE.FNLT,
24275
- MSO: CONSTRAINT_TYPE.MSO,
24276
- MFO: CONSTRAINT_TYPE.MFO,
24277
- "AS SOON AS POSSIBLE": CONSTRAINT_TYPE.ASAP,
24278
- "AS LATE AS POSSIBLE": CONSTRAINT_TYPE.ALAP,
24279
- "START NO EARLIER THAN": CONSTRAINT_TYPE.SNET,
24280
- "START NO LATER THAN": CONSTRAINT_TYPE.SNLT,
24281
- "FINISH NO EARLIER THAN": CONSTRAINT_TYPE.FNET,
24282
- "FINISH NO LATER THAN": CONSTRAINT_TYPE.FNLT,
24283
- "MUST START ON": CONSTRAINT_TYPE.MSO,
24284
- "MUST FINISH ON": CONSTRAINT_TYPE.MFO
24285
- };
24286
-
24287
26224
  // src/boundary/backend/normalize-calendar.ts
24288
26225
  function normalizeCalendars(schedulingCalendars, sectorCalendars, rawBaseCalendars, exceptionRange = null) {
24289
26226
  const hasSchedulingCalendars = schedulingCalendars !== void 0 && schedulingCalendars.length > 0;
@@ -24298,10 +26235,10 @@ function normalizeCalendars(schedulingCalendars, sectorCalendars, rawBaseCalenda
24298
26235
  calendars,
24299
26236
  baseCalendars,
24300
26237
  knownCalendarIds: new Set(calendars.map((calendar) => calendar.id)),
24301
- defaultCalendarId: pickDefaultCalendarId(calendars)
26238
+ defaultCalendarId: pickDefaultCalendarId2(calendars)
24302
26239
  };
24303
26240
  }
24304
- function pickDefaultCalendarId(calendars) {
26241
+ function pickDefaultCalendarId2(calendars) {
24305
26242
  let firstCalendarId = null;
24306
26243
  for (const calendar of calendars) {
24307
26244
  firstCalendarId ??= calendar.id;
@@ -24366,14 +26303,24 @@ function parseExceptionDays(exceptions, range) {
24366
26303
  }
24367
26304
  function expandRangeToUtcKeys(from, to, range, everyType) {
24368
26305
  const fromTs = parseExceptionDate(from);
24369
- const toTs = parseExceptionDate(to);
24370
- if (fromTs === null || toTs === null) return [];
26306
+ const toDay = parseExceptionDate(to);
26307
+ if (fromTs === null || toDay === null) return [];
26308
+ const toTs = endsAtMidnightAfterStart(from, to, fromTs, toDay) ? toDay - DAY_MS : toDay;
24371
26309
  if (range && (toTs < range.start || fromTs > range.end)) return [];
24372
26310
  const first = range ? Math.max(fromTs, range.start) : fromTs;
24373
26311
  const last = range ? Math.min(toTs, range.end) : toTs;
24374
26312
  const timestamps = steppedTimestamps(fromTs, first, last, everyType);
24375
26313
  return timestamps.map(String);
24376
26314
  }
26315
+ function endsAtMidnightAfterStart(from, to, fromDay, toDay) {
26316
+ if (toDay <= fromDay) return false;
26317
+ return isMidnight(to) && !isMidnight(from);
26318
+ }
26319
+ function isMidnight(raw) {
26320
+ if (typeof raw !== "string") return false;
26321
+ const match = EXCEPTION_TIME_FORMAT_REGEX.exec(raw.trim());
26322
+ return match !== null && Number(match[1]) === 0 && Number(match[2]) === 0;
26323
+ }
24377
26324
  function steppedTimestamps(anchor, first, last, everyType) {
24378
26325
  switch (everyType) {
24379
26326
  case EXCEPTION_EVERY_TYPE.DAILY:
@@ -24548,7 +26495,7 @@ function buildPipelineContext(adapter, options) {
24548
26495
  return {
24549
26496
  calendars: adapter.calendarReader,
24550
26497
  hasCalendar: (calendarId) => options.calendars.has(calendarId),
24551
- getDefaultCalendarId: () => pickDefaultCalendarId(options.calendars.values()),
26498
+ getDefaultCalendarId: () => pickDefaultCalendarId2(options.calendars.values()),
24552
26499
  hierarchy: createStateBackedHierarchyReader(adapter),
24553
26500
  activityReader: createStateBackedActivityReader(adapter),
24554
26501
  dateFormat: options.dateFormat ?? "YYYY-MM-DD",
@@ -24665,7 +26612,8 @@ function mergeCoalesced(top, next) {
24665
26612
  var DISPATCH_HISTORY_POLICY = {
24666
26613
  RECORD: "record",
24667
26614
  SKIP: "skip",
24668
- CHECKPOINT_ON_SUCCESS: "checkpoint-on-success"
26615
+ CHECKPOINT_ON_SUCCESS: "checkpoint-on-success",
26616
+ REBASE_ON_SUCCESS: "rebase-on-success"
24669
26617
  };
24670
26618
  var CHANGE_SET_SIDE = {
24671
26619
  BEFORE: "before",
@@ -24675,6 +26623,9 @@ function getDispatchHistoryPolicy(action) {
24675
26623
  if (action.kind === DISPATCH_ACTION_KIND.PERSISTENCE_ACKNOWLEDGE) {
24676
26624
  return DISPATCH_HISTORY_POLICY.CHECKPOINT_ON_SUCCESS;
24677
26625
  }
26626
+ if (action.kind === DISPATCH_ACTION_KIND.PERSISTENCE_REBASE) {
26627
+ return DISPATCH_HISTORY_POLICY.REBASE_ON_SUCCESS;
26628
+ }
24678
26629
  if (action.kind === DISPATCH_ACTION_KIND.SIR_SYNC || action.kind === DISPATCH_ACTION_KIND.CALENDAR_RECONCILE || action.kind === DISPATCH_ACTION_KIND.ACTIVITY_LOOKAHEAD_SYNC || action.kind === DISPATCH_ACTION_KIND.BASELINE_APPLY || action.kind === DISPATCH_ACTION_KIND.PONDERATOR_CRITERION_SET || action.kind === DISPATCH_ACTION_KIND.STATUS_CRITERIA_SET || action.kind === DISPATCH_ACTION_KIND.ACTIVITY_CLIPBOARD_CLEAR || action.kind === DISPATCH_ACTION_KIND.ACTIVITY_CLIPBOARD_IMPORT || action.kind === DISPATCH_ACTION_KIND.ACTIVITY_COPY || action.kind === DISPATCH_ACTION_KIND.SELECTION_TOGGLE || action.kind === DISPATCH_ACTION_KIND.SELECTION_REPLACE || action.kind === DISPATCH_ACTION_KIND.VISIBILITY_SET || action.kind === DISPATCH_ACTION_KIND.FILTER_SET || action.kind === DISPATCH_ACTION_KIND.SORT_SET || action.kind === DISPATCH_ACTION_KIND.VIEW_ATTRIBUTES_SET || action.kind === DISPATCH_ACTION_KIND.GROUP_SET || action.kind === DISPATCH_ACTION_KIND.WBS_DEPTH_SET) {
24679
26630
  return DISPATCH_HISTORY_POLICY.SKIP;
24680
26631
  }
@@ -25090,7 +27041,7 @@ function collectResequencedParents(changes, parentOf) {
25090
27041
  if (change.kind === ENTITY_CHANGE_KIND.DELETED) {
25091
27042
  const deletedFields = change.fields;
25092
27043
  if (deletedFields && "parentId" in deletedFields) {
25093
- touched.add(parentKeyOf2(deletedFields.parentId?.before));
27044
+ touched.add(parentKeyOf4(deletedFields.parentId?.before));
25094
27045
  }
25095
27046
  continue;
25096
27047
  }
@@ -25102,12 +27053,12 @@ function collectResequencedParents(changes, parentOf) {
25102
27053
  if (!movedPositionally) continue;
25103
27054
  addParentOf(activityId);
25104
27055
  if ("parentId" in fields) {
25105
- touched.add(parentKeyOf2(fields.parentId?.before));
27056
+ touched.add(parentKeyOf4(fields.parentId?.before));
25106
27057
  }
25107
27058
  }
25108
27059
  return touched;
25109
27060
  }
25110
- function parentKeyOf2(parentId) {
27061
+ function parentKeyOf4(parentId) {
25111
27062
  if (parentId === null || parentId === void 0) return ROOT_PARENT_ID;
25112
27063
  return String(parentId);
25113
27064
  }
@@ -25151,154 +27102,6 @@ function buildStateSnapshot(activities, links, calendars, baseCalendars = []) {
25151
27102
  };
25152
27103
  }
25153
27104
 
25154
- // src/boundary/save/field-policy.ts
25155
- var SAVE_FIELD_POLICY = {
25156
- TRACKED: "tracked",
25157
- IGNORED: "ignored"
25158
- };
25159
-
25160
- // src/boundary/save/activity-changes.ts
25161
- var ACTIVITY_SAVE_FIELD_POLICY = {
25162
- id: SAVE_FIELD_POLICY.IGNORED,
25163
- proplannerId: SAVE_FIELD_POLICY.IGNORED,
25164
- parentId: SAVE_FIELD_POLICY.TRACKED,
25165
- name: SAVE_FIELD_POLICY.TRACKED,
25166
- description: SAVE_FIELD_POLICY.TRACKED,
25167
- type: SAVE_FIELD_POLICY.TRACKED,
25168
- startDate: SAVE_FIELD_POLICY.TRACKED,
25169
- endDate: SAVE_FIELD_POLICY.TRACKED,
25170
- durationHours: SAVE_FIELD_POLICY.TRACKED,
25171
- constraintType: SAVE_FIELD_POLICY.TRACKED,
25172
- constraintDate: SAVE_FIELD_POLICY.TRACKED,
25173
- autoScheduling: SAVE_FIELD_POLICY.TRACKED,
25174
- calendarId: SAVE_FIELD_POLICY.TRACKED,
25175
- assignedDefaultCalendar: SAVE_FIELD_POLICY.IGNORED,
25176
- dateOrigin: SAVE_FIELD_POLICY.TRACKED,
25177
- progress: SAVE_FIELD_POLICY.TRACKED,
25178
- cost: SAVE_FIELD_POLICY.TRACKED,
25179
- usedCost: SAVE_FIELD_POLICY.TRACKED,
25180
- realCost: SAVE_FIELD_POLICY.TRACKED,
25181
- workHours: SAVE_FIELD_POLICY.TRACKED,
25182
- realWorkHours: SAVE_FIELD_POLICY.TRACKED,
25183
- ponderator: SAVE_FIELD_POLICY.TRACKED,
25184
- hasCustomPonderator: SAVE_FIELD_POLICY.TRACKED,
25185
- correlativeId: SAVE_FIELD_POLICY.TRACKED,
25186
- uniqueCorrelativeId: SAVE_FIELD_POLICY.TRACKED,
25187
- customId: SAVE_FIELD_POLICY.TRACKED,
25188
- subcontractId: SAVE_FIELD_POLICY.TRACKED,
25189
- isLookahead: SAVE_FIELD_POLICY.TRACKED,
25190
- hasLookaheadTasks: SAVE_FIELD_POLICY.IGNORED,
25191
- newActivityIds: SAVE_FIELD_POLICY.TRACKED,
25192
- hasNewActivities: SAVE_FIELD_POLICY.TRACKED,
25193
- isNewActivity: SAVE_FIELD_POLICY.IGNORED,
25194
- pendingRequestIds: SAVE_FIELD_POLICY.IGNORED,
25195
- responsableIds: SAVE_FIELD_POLICY.TRACKED,
25196
- tagIds: SAVE_FIELD_POLICY.TRACKED,
25197
- baselinePoints: SAVE_FIELD_POLICY.IGNORED,
25198
- baselineSnapshot: SAVE_FIELD_POLICY.IGNORED,
25199
- recursiveDurationHours: SAVE_FIELD_POLICY.IGNORED,
25200
- isCritical: SAVE_FIELD_POLICY.IGNORED,
25201
- freeSlackHours: SAVE_FIELD_POLICY.IGNORED,
25202
- calendarDuration: SAVE_FIELD_POLICY.IGNORED,
25203
- baselineVarianceDays: SAVE_FIELD_POLICY.IGNORED,
25204
- projectBaselineVarianceDays: SAVE_FIELD_POLICY.IGNORED,
25205
- primaryBaselineVarianceDays: SAVE_FIELD_POLICY.IGNORED,
25206
- expectedProgress: SAVE_FIELD_POLICY.IGNORED,
25207
- expectedProgressBaseline: SAVE_FIELD_POLICY.IGNORED,
25208
- status: SAVE_FIELD_POLICY.IGNORED,
25209
- criticalPath: SAVE_FIELD_POLICY.IGNORED,
25210
- p6: SAVE_FIELD_POLICY.IGNORED
25211
- };
25212
- var PERSISTABLE_ACTIVITY_FIELDS = Object.keys(ACTIVITY_SAVE_FIELD_POLICY).filter((field) => {
25213
- return ACTIVITY_SAVE_FIELD_POLICY[field] === SAVE_FIELD_POLICY.TRACKED;
25214
- });
25215
- var DATE_FIELDS2 = /* @__PURE__ */ new Set([
25216
- "startDate",
25217
- "endDate",
25218
- "constraintDate",
25219
- "dateOrigin"
25220
- ]);
25221
- var ID_ARRAY_FIELDS = /* @__PURE__ */ new Set([
25222
- "newActivityIds",
25223
- "responsableIds",
25224
- "tagIds"
25225
- ]);
25226
- function projectPersistableActivity(activity) {
25227
- const entries = PERSISTABLE_ACTIVITY_FIELDS.map(
25228
- (field) => [field, snapshotFieldValue(field, activity[field])]
25229
- );
25230
- return Object.fromEntries(entries);
25231
- }
25232
- function snapshotFieldValue(field, value) {
25233
- if (DATE_FIELDS2.has(field)) {
25234
- return value instanceof Date ? new Date(value) : null;
25235
- }
25236
- if (ID_ARRAY_FIELDS.has(field)) {
25237
- return Array.isArray(value) ? [...value] : [];
25238
- }
25239
- return value;
25240
- }
25241
- function activityChangedSince(current, saved) {
25242
- return changedPersistableActivityFields(current, saved).length > 0;
25243
- }
25244
- function changedPersistableActivityFields(current, saved) {
25245
- return PERSISTABLE_ACTIVITY_FIELDS.filter(
25246
- (field) => !fieldEquals(field, current[field], saved[field])
25247
- );
25248
- }
25249
- function fieldEquals(field, currentValue, savedValue) {
25250
- if (DATE_FIELDS2.has(field)) return dateEquals(currentValue, savedValue);
25251
- if (ID_ARRAY_FIELDS.has(field))
25252
- return idArrayEquals(currentValue, savedValue);
25253
- return (currentValue ?? null) === (savedValue ?? null);
25254
- }
25255
- function dateEquals(current, saved) {
25256
- const currentTime = current instanceof Date ? current.getTime() : null;
25257
- const savedTime = saved instanceof Date ? saved.getTime() : null;
25258
- return currentTime === savedTime;
25259
- }
25260
- function idArrayEquals(current, saved) {
25261
- const currentIds = new Set(Array.isArray(current) ? current.map(String) : []);
25262
- const savedIds = new Set(Array.isArray(saved) ? saved.map(String) : []);
25263
- if (currentIds.size !== savedIds.size) return false;
25264
- for (const currentId of currentIds) {
25265
- if (!savedIds.has(currentId)) return false;
25266
- }
25267
- return true;
25268
- }
25269
-
25270
- // src/boundary/save/link-changes.ts
25271
- var LINK_SAVE_FIELD_POLICY = {
25272
- id: SAVE_FIELD_POLICY.IGNORED,
25273
- source: SAVE_FIELD_POLICY.TRACKED,
25274
- target: SAVE_FIELD_POLICY.TRACKED,
25275
- type: SAVE_FIELD_POLICY.TRACKED,
25276
- lag: SAVE_FIELD_POLICY.TRACKED,
25277
- freeSlackHours: SAVE_FIELD_POLICY.IGNORED,
25278
- isCritical: SAVE_FIELD_POLICY.IGNORED,
25279
- ganttId: SAVE_FIELD_POLICY.IGNORED,
25280
- sectorId: SAVE_FIELD_POLICY.IGNORED,
25281
- proplannerId: SAVE_FIELD_POLICY.IGNORED
25282
- };
25283
- var TRACKED_LINK_FIELDS = Object.keys(LINK_SAVE_FIELD_POLICY).filter((field) => {
25284
- return LINK_SAVE_FIELD_POLICY[field] === SAVE_FIELD_POLICY.TRACKED;
25285
- });
25286
- function linkChangedSince(baseline, current) {
25287
- return TRACKED_LINK_FIELDS.some(
25288
- (field) => baseline[field] !== current[field]
25289
- );
25290
- }
25291
- function checkNoUpdatedLinks(baseline, current) {
25292
- const baselineById = new Map(
25293
- baseline.map((link) => [String(link.id), link])
25294
- );
25295
- return current.filter((link) => {
25296
- const base = baselineById.get(String(link.id));
25297
- if (!base) return false;
25298
- return linkChangedSince(base, link);
25299
- });
25300
- }
25301
-
25302
27105
  // src/previsualization/canonical-digest.ts
25303
27106
  function normalized(value) {
25304
27107
  if (value === void 0) return ["undefined"];
@@ -25749,6 +27552,30 @@ var SaveTracker = class _SaveTracker {
25749
27552
  hasCheckpointLinkId(linkId) {
25750
27553
  return this.linksAtLastSave.has(linkId);
25751
27554
  }
27555
+ rebaseBaseline(activities, links) {
27556
+ return {
27557
+ activities: activities.map((activity) => {
27558
+ const saved = this.p6ActivitiesAtLastSave.get(String(activity.id));
27559
+ return saved === void 0 ? activity : { ...activity, p6: cloneDomainValue(saved) };
27560
+ }),
27561
+ links,
27562
+ p6Settings: cloneDomainValue(this.p6SettingsAtLastSave),
27563
+ p6ProjectState: cloneDomainValue(this.p6ProjectStateAtLastSave),
27564
+ p6LatestRunLog: cloneDomainValue(this.p6LatestRunLogAtLastSave)
27565
+ };
27566
+ }
27567
+ checkpointActivity(activityId) {
27568
+ return this.activitiesAtLastSave.get(activityId)?.persistable ?? null;
27569
+ }
27570
+ checkpointLink(linkId) {
27571
+ return this.linksAtLastSave.get(linkId) ?? null;
27572
+ }
27573
+ checkpointActivityIds() {
27574
+ return [...this.activitiesAtLastSave.keys()];
27575
+ }
27576
+ checkpointLinkIds() {
27577
+ return [...this.linksAtLastSave.keys()];
27578
+ }
25752
27579
  hasUnsavedChanges() {
25753
27580
  return this.trackingEnabled && this.initialized && (this.dirtyActivityIds.size > 0 || this.dirtyLinkIds.size > 0 || this.p6TrackingEnabled && (this.dirtyP6ActivityIds.size > 0 || this.p6SettingsIsDirty() || this.p6ProjectStateDirty || this.p6LatestRunLogDirty));
25754
27581
  }
@@ -25868,332 +27695,6 @@ var SaveTracker = class _SaveTracker {
25868
27695
  }
25869
27696
  };
25870
27697
 
25871
- // src/boundary/backend/normalize-activity.ts
25872
- function normalizeActivities(rawActivities, context) {
25873
- const hiddenIds = [];
25874
- const activities = rawActivities.map((raw) => {
25875
- const activity = parseActivity(raw, context);
25876
- if (raw.should_be_showed === false) hiddenIds.push(activity.id);
25877
- return activity;
25878
- });
25879
- forceRootProjectToRoot(activities);
25880
- const activityIds = new Set(activities.map((activity) => activity.id));
25881
- reparentOrphansToRoot(
25882
- activities,
25883
- activityIds,
25884
- context.reporter,
25885
- context.recordDiagnostic
25886
- );
25887
- return { activities, activityIds, hiddenIds };
25888
- }
25889
- function parseConstraintLabel(label, reporter, recordDiagnostic, activityId) {
25890
- if (!label) return null;
25891
- const normalized2 = CONSTRAINT_LABEL_MAP[label.toUpperCase()];
25892
- if (normalized2 === void 0) {
25893
- recordDiagnostic(LOAD_DIAGNOSTIC_CODE.UNKNOWN_CONSTRAINT, activityId);
25894
- reporter.warn(
25895
- `[parseFromBackend] unknown constraint label "${label}" \u2014 defaulting to "asap"`
25896
- );
25897
- return DEFAULT_CONSTRAINT_TYPE;
25898
- }
25899
- return normalized2;
25900
- }
25901
- function resolveConstraint(raw, reporter, recordDiagnostic) {
25902
- const constraintType = parseConstraintLabel(
25903
- raw.constraint,
25904
- reporter,
25905
- recordDiagnostic,
25906
- String(raw.unique_id)
25907
- );
25908
- const constraintTakesDate = constraintType !== CONSTRAINT_TYPE.ASAP && constraintType !== CONSTRAINT_TYPE.ALAP;
25909
- const constraintDate = constraintTakesDate && raw.constraint_date ? parseBackendDate(raw.constraint_date) : null;
25910
- return { constraintType, constraintDate };
25911
- }
25912
- function resolveCalendarId2(rawCalendarId, knownCalendarIds, defaultCalendarId) {
25913
- if (rawCalendarId == null) return defaultCalendarId;
25914
- const coerced = String(rawCalendarId);
25915
- return knownCalendarIds.has(coerced) ? coerced : defaultCalendarId;
25916
- }
25917
- function parseNewActivityIds(raw) {
25918
- if (raw == null || raw === "") return [];
25919
- if (Array.isArray(raw)) return raw.map(String);
25920
- if (typeof raw !== "string") return [];
25921
- try {
25922
- const parsed2 = JSON.parse(raw);
25923
- return Array.isArray(parsed2) ? parsed2.map(String) : [];
25924
- } catch {
25925
- return [];
25926
- }
25927
- }
25928
- function hasUsableId(entry) {
25929
- if (typeof entry !== "object" || entry === null) return false;
25930
- if (!("id" in entry)) return false;
25931
- const value = entry.id;
25932
- return typeof value === "string" || typeof value === "number";
25933
- }
25934
- function parseIdList(raw) {
25935
- if (!Array.isArray(raw)) return [];
25936
- const ids = [];
25937
- for (const entry of raw) {
25938
- if (hasUsableId(entry)) ids.push(String(entry.id));
25939
- }
25940
- return ids;
25941
- }
25942
- function parseParentId(rawParentId) {
25943
- return rawParentId === ROOT_PARENT_ID ? null : String(rawParentId);
25944
- }
25945
- function normalizePersistedActivityId(rawId) {
25946
- if (rawId == null) return null;
25947
- const isValidIdentity = typeof rawId === "number" && (rawId === 0 || Number.isSafeInteger(rawId) && rawId > 0);
25948
- if (!isValidIdentity) {
25949
- throw new Error(`invalid activity proplannerId ${String(rawId)}`);
25950
- }
25951
- return rawId;
25952
- }
25953
- function parseProgress(raw, reporter, recordDiagnostic, activityId) {
25954
- const progress = Number(raw);
25955
- if (!Number.isFinite(progress)) {
25956
- throw new TypeError(`invalid activity progress ${String(raw)}`);
25957
- }
25958
- const isBelowNotStarted = progress < NOT_STARTED_PROGRESS;
25959
- const isAboveCompleted = progress > MAX_TOLERATED_PROGRESS;
25960
- const isWithinRange = !isBelowNotStarted && !isAboveCompleted;
25961
- if (isWithinRange) return progress;
25962
- const clamped = isBelowNotStarted ? NOT_STARTED_PROGRESS : COMPLETED_PROGRESS;
25963
- recordDiagnostic(LOAD_DIAGNOSTIC_CODE.PROGRESS_OUT_OF_RANGE, activityId);
25964
- reporter.warn(
25965
- `[parseFromBackend] activity "${activityId}" progress ${String(raw)} is out of range \u2014 clamping to ${clamped}`
25966
- );
25967
- return clamped;
25968
- }
25969
- function parseCriticality(raw, reporter) {
25970
- if (typeof raw.is_critical === "boolean") return raw.is_critical;
25971
- reporter.error(
25972
- "[parseFromBackend] invalid activity is_critical \u2014 defaulting to false",
25973
- new TypeError(
25974
- `activity "${raw.unique_id}" received ${typeof raw.is_critical}`
25975
- )
25976
- );
25977
- return false;
25978
- }
25979
- function parseBaselinePoints(raw, reporter, recordDiagnostic, activityId, isMilestone) {
25980
- if (!Array.isArray(raw)) return [];
25981
- return raw.map(
25982
- (point) => normalizeBaselinePoint(
25983
- point,
25984
- reporter,
25985
- {
25986
- record: recordDiagnostic,
25987
- sampleId: activityId
25988
- },
25989
- isMilestone
25990
- )
25991
- ).filter((point) => point !== null);
25992
- }
25993
- function parseActivity(raw, context) {
25994
- const calendarId = resolveCalendarId2(
25995
- raw.calendarId,
25996
- context.knownCalendarIds,
25997
- context.defaultCalendarId
25998
- );
25999
- const inheritedDefaultCalendar = raw.calendarId == null || !context.knownCalendarIds.has(String(raw.calendarId));
26000
- const progress = parseProgress(
26001
- raw.progress,
26002
- context.reporter,
26003
- context.recordDiagnostic,
26004
- String(raw.unique_id)
26005
- );
26006
- const { constraintType, constraintDate } = resolveConstraint(
26007
- raw,
26008
- context.reporter,
26009
- context.recordDiagnostic
26010
- );
26011
- const isMilestone = raw.type === ACTIVITY_TYPE.MILESTONE;
26012
- const startDate = parseBackendDate(raw.start_date);
26013
- const endDate = isMilestone ? startDate : parseBackendDate(raw.end_date);
26014
- const durationDays = isMilestone ? 0 : Number(raw.duration);
26015
- const isCritical = parseCriticality(raw, context.reporter);
26016
- return {
26017
- id: String(raw.unique_id),
26018
- proplannerId: normalizePersistedActivityId(raw.id),
26019
- parentId: parseParentId(raw.parent_id),
26020
- name: raw.name,
26021
- description: raw.description ?? "",
26022
- type: raw.type,
26023
- startDate,
26024
- endDate,
26025
- durationHours: durationToHours(durationDays, context.hoursPerDay),
26026
- constraintType,
26027
- constraintDate,
26028
- autoScheduling: isAutoSchedulingEnabledForProgress(progress),
26029
- calendarId,
26030
- assignedDefaultCalendar: calendarId != null && inheritedDefaultCalendar,
26031
- dateOrigin: raw.date_origin == null ? null : parseBackendDate(raw.date_origin),
26032
- progress,
26033
- cost: Number(raw.cost),
26034
- usedCost: Number(raw.used_cost ?? 0),
26035
- realCost: Number(raw.real_cost ?? 0),
26036
- workHours: Number(raw.hhWorkTime),
26037
- realWorkHours: Number(raw.real_work ?? 0),
26038
- ponderator: Number(raw.ponderator ?? 0),
26039
- hasCustomPonderator: raw.hasCustomPonderator,
26040
- correlativeId: raw.correlative_id,
26041
- uniqueCorrelativeId: raw.unique_correlative_id,
26042
- customId: normalizeCustomId(raw),
26043
- subcontractId: raw.subcontractId,
26044
- isLookahead: raw.isOnLookahead,
26045
- hasLookaheadTasks: Array.isArray(raw.tasks) && raw.tasks.length > 0,
26046
- newActivityIds: parseNewActivityIds(raw.newActivitiesArray),
26047
- hasNewActivities: raw.hasNewActivities ?? false,
26048
- ...raw.isNewActivity === true ? { isNewActivity: true } : {},
26049
- pendingRequestIds: parseIdList(raw.pendingRequests),
26050
- responsableIds: parseIdList(raw.responsables),
26051
- tagIds: parseIdList(raw.tags),
26052
- baselinePoints: parseBaselinePoints(
26053
- raw.baseline_points,
26054
- context.reporter,
26055
- context.recordDiagnostic,
26056
- String(raw.unique_id),
26057
- isMilestone
26058
- ),
26059
- baselineSnapshot: null,
26060
- baselineVarianceDays: null,
26061
- projectBaselineVarianceDays: null,
26062
- primaryBaselineVarianceDays: null,
26063
- recursiveDurationHours: durationToHours(
26064
- Number(raw.sumOfDurationRecursively ?? 0),
26065
- context.hoursPerDay
26066
- ),
26067
- calendarDuration: computeCalendarDuration({
26068
- type: raw.type,
26069
- startDate,
26070
- endDate
26071
- }),
26072
- isCritical,
26073
- freeSlackHours: null,
26074
- expectedProgress: null,
26075
- expectedProgressBaseline: null,
26076
- status: null,
26077
- criticalPath: null
26078
- };
26079
- }
26080
- function normalizeCustomId(raw) {
26081
- return raw.has_childs ? null : raw.custom_id;
26082
- }
26083
- function forceRootProjectToRoot(activities) {
26084
- const rootProject = activities.find(
26085
- (activity) => activity.correlativeId === 0 && activity.type === ACTIVITY_TYPE.PROJECT
26086
- );
26087
- if (rootProject) rootProject.parentId = null;
26088
- }
26089
- function reparentOrphansToRoot(activities, ids, reporter, recordDiagnostic) {
26090
- for (const activity of activities) {
26091
- if (activity.parentId === null) continue;
26092
- if (!ids.has(activity.parentId)) {
26093
- reporter.warn(
26094
- `[parseFromBackend] activity "${activity.id}" references unknown parent "${activity.parentId}" \u2014 re-parenting to root (null)`
26095
- );
26096
- recordDiagnostic(LOAD_DIAGNOSTIC_CODE.ORPHAN_ACTIVITY, activity.id);
26097
- activity.parentId = null;
26098
- }
26099
- }
26100
- }
26101
-
26102
- // src/boundary/backend/normalize-link.ts
26103
- function resolveLinkType(rawType) {
26104
- if (typeof rawType !== "string") return null;
26105
- const fromLabel = parseLinkTypeCode(rawType);
26106
- if (fromLabel) return fromLabel;
26107
- if (isLinkType(rawType)) return rawType;
26108
- return null;
26109
- }
26110
- function normalizePersistedLinkId(rawId) {
26111
- if (rawId == null) return void 0;
26112
- const isValidIdentity = typeof rawId === "number" && (rawId === 0 || Number.isSafeInteger(rawId) && rawId > 0);
26113
- if (!isValidIdentity) {
26114
- throw new Error(`invalid link proplannerId ${String(rawId)}`);
26115
- }
26116
- return rawId;
26117
- }
26118
- function normalizeLinks(rawLinks, context) {
26119
- const parsedLinks = [];
26120
- for (const rawLink of rawLinks) {
26121
- try {
26122
- parsedLinks.push(
26123
- parseLink(rawLink, context.knownActivityIds, context.hoursPerDay)
26124
- );
26125
- } catch (parseError2) {
26126
- context.recordDiagnostic(
26127
- LOAD_DIAGNOSTIC_CODE.INVALID_LINK,
26128
- String(rawLink.unique_id ?? rawLink.id)
26129
- );
26130
- context.reporter.warn(
26131
- "[parseFromBackend] dropping invalid link",
26132
- parseError2
26133
- );
26134
- }
26135
- }
26136
- return dedupeLinksBySignature(parsedLinks, context.recordDiagnostic);
26137
- }
26138
- function parseLink(link, activityIds, hoursPerDay) {
26139
- const id = String(link.unique_id ?? link.id);
26140
- const source = String(link.source_id ?? link.source ?? "");
26141
- const target = String(link.target_id ?? link.target ?? "");
26142
- if (!activityIds.has(source)) {
26143
- throw new Error(
26144
- `[parseFromBackend] link "${id}" source "${source}" does not exist`
26145
- );
26146
- }
26147
- if (!activityIds.has(target)) {
26148
- throw new Error(
26149
- `[parseFromBackend] link "${id}" target "${target}" does not exist`
26150
- );
26151
- }
26152
- const type = resolveLinkType(link.type);
26153
- if (type === null) {
26154
- throw new Error(
26155
- `[parseFromBackend] link "${id}" has invalid type "${String(link.type)}"`
26156
- );
26157
- }
26158
- const ganttIdRaw = link.ganttId;
26159
- const sectorIdRaw = link.sectorId;
26160
- const proplannerId = normalizePersistedLinkId(link.id);
26161
- return {
26162
- id,
26163
- source,
26164
- target,
26165
- type,
26166
- lag: lagDaysToHours(Number(link.lag ?? 0), hoursPerDay),
26167
- freeSlackHours: null,
26168
- isCritical: false,
26169
- ...typeof ganttIdRaw === "number" ? { ganttId: ganttIdRaw } : {},
26170
- ...typeof sectorIdRaw === "number" ? { sectorId: sectorIdRaw } : {},
26171
- ...proplannerId === void 0 ? {} : { proplannerId }
26172
- };
26173
- }
26174
- function dedupeLinksBySignature(links, recordDiagnostic) {
26175
- const bySignature = /* @__PURE__ */ new Map();
26176
- for (const link of links) {
26177
- const signature = `${link.source}-${link.target}-${link.type}`;
26178
- const existing = bySignature.get(signature);
26179
- if (!existing) {
26180
- bySignature.set(signature, link);
26181
- continue;
26182
- }
26183
- const incomingWins = linkIdAsNumber(link) > linkIdAsNumber(existing);
26184
- const discarded = incomingWins ? existing : link;
26185
- recordDiagnostic(LOAD_DIAGNOSTIC_CODE.DUPLICATE_LINK, discarded.id);
26186
- if (incomingWins) {
26187
- bySignature.set(signature, link);
26188
- }
26189
- }
26190
- return [...bySignature.values()];
26191
- }
26192
- function linkIdAsNumber(link) {
26193
- const numeric = Number(link.id);
26194
- return Number.isFinite(numeric) ? numeric : -Infinity;
26195
- }
26196
-
26197
27698
  // src/boundary/backend/normalize-sector.ts
26198
27699
  function parseActivityCreter(raw) {
26199
27700
  if (typeof raw !== "string") return DEFAULT_ACTIVITY_CRETER;
@@ -29245,8 +30746,8 @@ function normalizeLatestConstraintDate(input) {
29245
30746
  });
29246
30747
  }
29247
30748
  function isMidnightFinishConstraint(constraintType, constraintDate) {
29248
- const isMidnight = constraintDate.getUTCHours() === 0 && constraintDate.getUTCMinutes() === 0 && constraintDate.getUTCSeconds() === 0 && constraintDate.getUTCMilliseconds() === 0;
29249
- return isMidnight && (constraintType === "fnlt" || constraintType === "mfo");
30749
+ const isMidnight2 = constraintDate.getUTCHours() === 0 && constraintDate.getUTCMinutes() === 0 && constraintDate.getUTCSeconds() === 0 && constraintDate.getUTCMilliseconds() === 0;
30750
+ return isMidnight2 && (constraintType === "fnlt" || constraintType === "mfo");
29250
30751
  }
29251
30752
  function constrainLatestFinish(activity, latestFinish, adapter) {
29252
30753
  const bounds = computeConstraintBounds(activity, adapter);
@@ -32025,8 +33526,36 @@ function emptyChangeSet2(source) {
32025
33526
  trackingEvents: []
32026
33527
  };
32027
33528
  }
33529
+ function resolvePersistenceAcknowledgement(action, result, saveTracker) {
33530
+ if (action.kind === DISPATCH_ACTION_KIND.PERSISTENCE_ACKNOWLEDGE) {
33531
+ return {
33532
+ checkpoint: action.checkpoint,
33533
+ historyBoundary: action.checkpoint?.historyBoundary,
33534
+ persistedActivities: action.activities,
33535
+ persistedLinks: action.links,
33536
+ clearsHistory: false
33537
+ };
33538
+ }
33539
+ if (action.kind === DISPATCH_ACTION_KIND.PERSISTENCE_REBASE) {
33540
+ const rebaseCheckpoint = result.__rebaseCheckpoint;
33541
+ if (rebaseCheckpoint === void 0) {
33542
+ throw new Error("rebase-on-success requires a rebase checkpoint");
33543
+ }
33544
+ return {
33545
+ checkpoint: saveTracker.rebaseBaseline(
33546
+ rebaseCheckpoint.activities,
33547
+ rebaseCheckpoint.links
33548
+ ),
33549
+ historyBoundary: void 0,
33550
+ persistedActivities: void 0,
33551
+ persistedLinks: void 0,
33552
+ clearsHistory: true
33553
+ };
33554
+ }
33555
+ throw new Error("checkpoint-on-success requires persistence acknowledge");
33556
+ }
32028
33557
  function acknowledgesPersistence(action) {
32029
- return action.kind === DISPATCH_ACTION_KIND.PERSISTENCE_ACKNOWLEDGE || action.kind === DISPATCH_ACTION_KIND.ACTIVITY_LOOKAHEAD_SYNC && action.acknowledgePersistedLookahead === true;
33558
+ return action.kind === DISPATCH_ACTION_KIND.PERSISTENCE_ACKNOWLEDGE || action.kind === DISPATCH_ACTION_KIND.PERSISTENCE_REBASE || action.kind === DISPATCH_ACTION_KIND.ACTIVITY_LOOKAHEAD_SYNC && action.acknowledgePersistedLookahead === true;
32030
33559
  }
32031
33560
  function changesRequireFilterProjectionRebuild(changes) {
32032
33561
  for (const change of changes.activities) {
@@ -33671,6 +35200,7 @@ var ScheduleCore = class _ScheduleCore {
33671
35200
  linkIdGen: this.coreRuntime.linkIdGenerator,
33672
35201
  isActivityIdReserved: (activityId) => this._saveTracker.hasCheckpointActivityId(activityId),
33673
35202
  isLinkIdReserved: (linkId) => this._saveTracker.hasCheckpointLinkId(linkId),
35203
+ persistenceCheckpoint: this._saveTracker,
33674
35204
  uidGen: this.coreRuntime.uniqueCorrelativeIdGenerator,
33675
35205
  customIdTracker: this.coreRuntime.customIdTracker,
33676
35206
  reporter: this.coreRuntime.reporter,
@@ -33760,32 +35290,37 @@ var ScheduleCore = class _ScheduleCore {
33760
35290
  );
33761
35291
  }
33762
35292
  if (result.ok) {
33763
- if (historyPolicy === DISPATCH_HISTORY_POLICY.CHECKPOINT_ON_SUCCESS) {
33764
- if (action.kind !== DISPATCH_ACTION_KIND.PERSISTENCE_ACKNOWLEDGE) {
33765
- throw new Error(
33766
- "checkpoint-on-success requires persistence acknowledge"
33767
- );
33768
- }
35293
+ if (historyPolicy === DISPATCH_HISTORY_POLICY.CHECKPOINT_ON_SUCCESS || historyPolicy === DISPATCH_HISTORY_POLICY.REBASE_ON_SUCCESS) {
35294
+ const acknowledgement = resolvePersistenceAcknowledgement(
35295
+ action,
35296
+ result,
35297
+ this._saveTracker
35298
+ );
33769
35299
  const currentActivities = this.coreRuntime.state.getAllActivities();
33770
35300
  const currentLinks = this.coreRuntime.state.getAllLinks();
33771
- const checkpoint = action.checkpoint;
33772
- const boundary = checkpoint?.historyBoundary ?? this._undo.capturePersistenceBoundary();
35301
+ const checkpoint = acknowledgement.checkpoint;
33773
35302
  this._saveTracker.acknowledge(
33774
35303
  checkpoint?.activities ?? currentActivities,
33775
35304
  checkpoint?.links ?? currentLinks,
33776
- action.activities,
33777
- action.links,
35305
+ acknowledgement.persistedActivities,
35306
+ acknowledgement.persistedLinks,
33778
35307
  currentActivities,
33779
35308
  currentLinks,
33780
35309
  checkpoint?.p6Settings ?? this.coreRuntime.state.dataDateSettings(),
33781
35310
  checkpoint?.p6ProjectState === void 0 ? this.coreRuntime.state.dataDateProjectState() : checkpoint.p6ProjectState,
33782
35311
  checkpoint?.p6LatestRunLog === void 0 ? this.coreRuntime.state.dataDateLatestRunLog() : checkpoint.p6LatestRunLog
33783
35312
  );
33784
- this._undo.discardThrough(boundary);
33785
- this._undo.applyPersistedIdentities(
33786
- action.activities ?? [],
33787
- action.links ?? []
33788
- );
35313
+ if (acknowledgement.clearsHistory) {
35314
+ this._undo.clear();
35315
+ } else {
35316
+ this._undo.discardThrough(
35317
+ acknowledgement.historyBoundary ?? this._undo.capturePersistenceBoundary()
35318
+ );
35319
+ this._undo.applyPersistedIdentities(
35320
+ acknowledgement.persistedActivities ?? [],
35321
+ acknowledgement.persistedLinks ?? []
35322
+ );
35323
+ }
33789
35324
  } else if (action.kind === DISPATCH_ACTION_KIND.ACTIVITY_LOOKAHEAD_SYNC && action.acknowledgePersistedLookahead === true) {
33790
35325
  this._saveTracker.acknowledgePersistedLookahead(
33791
35326
  result.changes.activities,
@@ -34349,6 +35884,7 @@ function toPublicDispatchResult(result) {
34349
35884
  const {
34350
35885
  __beforeSnap: droppedSnapshots,
34351
35886
  __beforeLinks: droppedLinks,
35887
+ __rebaseCheckpoint: droppedRebaseCheckpoint,
34352
35888
  ...publicResult
34353
35889
  } = result;
34354
35890
  return publicResult;
@@ -35230,6 +36766,8 @@ exports.NOT_STARTED_PROGRESS = NOT_STARTED_PROGRESS;
35230
36766
  exports.ORDER_DIRECTION = ORDER_DIRECTION;
35231
36767
  exports.ORDER_MODE = ORDER_MODE;
35232
36768
  exports.PASTE_PLACEMENT = PASTE_PLACEMENT;
36769
+ exports.PERSISTENCE_REBASE_CONFLICT_KIND = PERSISTENCE_REBASE_CONFLICT_KIND;
36770
+ exports.PERSISTENCE_REBASE_ENTITY = PERSISTENCE_REBASE_ENTITY;
35233
36771
  exports.REJECTION_REASON = REJECTION_REASON;
35234
36772
  exports.ROOT_PARENT_ID = ROOT_PARENT_ID;
35235
36773
  exports.SCHEDULE_CORE_STATUS = SCHEDULE_CORE_STATUS;