@outbuild-company/schedule-core 1.23.0 → 1.25.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
@@ -6287,7 +6287,9 @@ var P6_PERCENT_COMPLETE_TYPE = {
6287
6287
  var P6_ACTIVITY_TYPE = {
6288
6288
  TASK: "task",
6289
6289
  LEVEL_OF_EFFORT: "level_of_effort",
6290
- START_MILESTONE: "start_milestone"};
6290
+ START_MILESTONE: "start_milestone",
6291
+ RESOURCE_DEPENDENT: "resource_dependent"
6292
+ };
6291
6293
  var P6_EXECUTION_STATUS = {
6292
6294
  NOT_STARTED: "not_started",
6293
6295
  IN_PROGRESS: "in_progress",
@@ -11979,14 +11981,65 @@ function createOutbuildP6ActivityDraft(activity) {
11979
11981
  if (state === void 0) return void 0;
11980
11982
  return {
11981
11983
  ...state,
11982
- remainingDurationHours: draftRemainingDuration(activity)
11984
+ ...outbuildProgressExecution(activity, activity.durationHours)
11983
11985
  };
11984
11986
  }
11985
- function draftRemainingDuration(activity) {
11987
+ var COMPLETED_PHYSICAL_PERCENT = 100;
11988
+ var NOT_STARTED_PHYSICAL_PERCENT = 0;
11989
+ function outbuildProgressExecution(activity, originalDurationHours) {
11986
11990
  if (activity.progress === NOT_STARTED_PROGRESS) {
11987
- return activity.durationHours;
11991
+ return {
11992
+ executionStatus: P6_EXECUTION_STATUS.NOT_STARTED,
11993
+ actualStart: null,
11994
+ actualFinish: null,
11995
+ remainingDurationHours: originalDurationHours,
11996
+ physicalPercentComplete: NOT_STARTED_PHYSICAL_PERCENT,
11997
+ actualDurationHours: 0,
11998
+ atCompletionDurationHours: originalDurationHours,
11999
+ durationPercentComplete: 0
12000
+ };
11988
12001
  }
11989
- return isCompletedProgress(activity.progress) ? 0 : null;
12002
+ const actualStart = cloneValidDate(activity.startDate, "actualStart");
12003
+ if (isCompletedProgress(activity.progress)) {
12004
+ const hasMeasurableDuration = originalDurationHours > 0;
12005
+ return {
12006
+ executionStatus: P6_EXECUTION_STATUS.COMPLETED,
12007
+ actualStart,
12008
+ actualFinish: cloneValidDate(activity.endDate, "actualFinish"),
12009
+ remainingDurationHours: 0,
12010
+ physicalPercentComplete: COMPLETED_PHYSICAL_PERCENT,
12011
+ actualDurationHours: originalDurationHours,
12012
+ atCompletionDurationHours: originalDurationHours,
12013
+ durationPercentComplete: hasMeasurableDuration ? COMPLETED_PHYSICAL_PERCENT : NOT_STARTED_PHYSICAL_PERCENT
12014
+ };
12015
+ }
12016
+ return {
12017
+ executionStatus: P6_EXECUTION_STATUS.IN_PROGRESS,
12018
+ actualStart,
12019
+ actualFinish: null,
12020
+ remainingDurationHours: originalDurationHours,
12021
+ physicalPercentComplete: activity.progress,
12022
+ actualDurationHours: 0,
12023
+ atCompletionDurationHours: originalDurationHours,
12024
+ durationPercentComplete: 0
12025
+ };
12026
+ }
12027
+ function awaitsOutbuildExecution(draft) {
12028
+ return draft.executionStatus === P6_EXECUTION_STATUS.NOT_STARTED && draft.actualStart === null;
12029
+ }
12030
+ function reconcileOutbuildProgress(activity, draft) {
12031
+ const isProgressed = activity.progress !== NOT_STARTED_PROGRESS;
12032
+ if (!isProgressed || !awaitsOutbuildExecution(draft)) return draft;
12033
+ const inferred = outbuildProgressExecution(
12034
+ activity,
12035
+ draft.originalDurationHours
12036
+ );
12037
+ const keepsRecordedRemaining = inferred.executionStatus === P6_EXECUTION_STATUS.IN_PROGRESS && draft.remainingDurationHours !== null;
12038
+ return {
12039
+ ...draft,
12040
+ ...inferred,
12041
+ ...keepsRecordedRemaining ? { remainingDurationHours: draft.remainingDurationHours } : {}
12042
+ };
11990
12043
  }
11991
12044
 
11992
12045
  // src/p6/inactive-drafts.ts
@@ -12039,17 +12092,41 @@ function ordinaryInputChanges(before, activity) {
12039
12092
  ...before.startDate.getTime() !== activity.startDate.getTime() ? { plannedStart: new Date(activity.startDate) } : {},
12040
12093
  ...before.endDate.getTime() !== activity.endDate.getTime() ? { plannedFinish: new Date(activity.endDate) } : {},
12041
12094
  ...durationChanged ? { originalDurationHours: activity.durationHours } : {},
12042
- ...durationChanged || before.progress !== activity.progress ? remainingInputChanges(before, activity) : {}
12095
+ ...durationChanged || before.progress !== activity.progress ? executionInputChanges(before, activity, durationChanged) : {}
12043
12096
  };
12044
12097
  }
12045
- function remainingInputChanges(before, activity) {
12098
+ function executionInputChanges(before, activity, durationChanged) {
12046
12099
  const current = activity.p6;
12047
- if (current.actualStart !== null || current.remainingDurationHours !== before.p6.remainingDurationHours)
12048
- return {};
12049
- const remaining = draftRemainingDuration(activity);
12100
+ const hasExplicitRemainingCorrection = current.remainingDurationHours !== before.p6.remainingDurationHours;
12101
+ if (hasExplicitRemainingCorrection) return {};
12102
+ const progressChanged = before.progress !== activity.progress;
12103
+ const resetsToNotStarted = progressChanged && activity.progress === NOT_STARTED_PROGRESS;
12104
+ if (awaitsOutbuildExecution(current) || resetsToNotStarted) {
12105
+ const originalDurationHours = durationChanged ? activity.durationHours : current.originalDurationHours;
12106
+ return outbuildProgressExecution(activity, originalDurationHours);
12107
+ }
12108
+ const isStarted = current.actualStart !== null;
12109
+ if (!isStarted || !progressChanged) return {};
12110
+ return startedProgressChanges(current, activity);
12111
+ }
12112
+ function startedProgressChanges(current, activity) {
12113
+ const mirrorsPhysicalPercent = current.percentCompleteType === P6_PERCENT_COMPLETE_TYPE.PHYSICAL;
12114
+ if (!isCompletedProgress(activity.progress)) {
12115
+ return {
12116
+ ...current.actualFinish === null ? {} : {
12117
+ executionStatus: P6_EXECUTION_STATUS.IN_PROGRESS,
12118
+ actualFinish: null
12119
+ },
12120
+ ...mirrorsPhysicalPercent ? { physicalPercentComplete: activity.progress } : {}
12121
+ };
12122
+ }
12123
+ const finishesAfterActualStart = current.actualStart !== null && activity.endDate.getTime() >= current.actualStart.getTime();
12124
+ if (current.actualFinish !== null || !finishesAfterActualStart) return {};
12050
12125
  return {
12051
- remainingDurationHours: remaining,
12052
- ...remaining !== null ? { atCompletionDurationHours: remaining } : {}
12126
+ executionStatus: P6_EXECUTION_STATUS.COMPLETED,
12127
+ actualFinish: new Date(activity.endDate),
12128
+ remainingDurationHours: 0,
12129
+ physicalPercentComplete: COMPLETED_PROGRESS
12053
12130
  };
12054
12131
  }
12055
12132
 
@@ -17941,6 +18018,7 @@ var LOAD_DIAGNOSTIC_SAMPLE_LIMIT = 10;
17941
18018
  var LOAD_DIAGNOSTIC_CODE = {
17942
18019
  INVALID_BASELINE_POINT: "invalid_baseline_point",
17943
18020
  UNKNOWN_CONSTRAINT: "unknown_constraint",
18021
+ PROGRESS_OUT_OF_RANGE: "progress_out_of_range",
17944
18022
  ORPHAN_ACTIVITY: "orphan_activity",
17945
18023
  INVALID_LINK: "invalid_link",
17946
18024
  DUPLICATE_LINK: "duplicate_link"
@@ -17948,6 +18026,7 @@ var LOAD_DIAGNOSTIC_CODE = {
17948
18026
  var LOAD_DIAGNOSTIC_CODE_ORDER = [
17949
18027
  LOAD_DIAGNOSTIC_CODE.INVALID_BASELINE_POINT,
17950
18028
  LOAD_DIAGNOSTIC_CODE.UNKNOWN_CONSTRAINT,
18029
+ LOAD_DIAGNOSTIC_CODE.PROGRESS_OUT_OF_RANGE,
17951
18030
  LOAD_DIAGNOSTIC_CODE.ORPHAN_ACTIVITY,
17952
18031
  LOAD_DIAGNOSTIC_CODE.INVALID_LINK,
17953
18032
  LOAD_DIAGNOSTIC_CODE.DUPLICATE_LINK
@@ -24753,12 +24832,21 @@ function normalizePersistedActivityId(rawId) {
24753
24832
  }
24754
24833
  return rawId;
24755
24834
  }
24756
- function parseProgress(raw) {
24835
+ function parseProgress(raw, reporter, recordDiagnostic, activityId) {
24757
24836
  const progress = Number(raw);
24758
- if (!Number.isFinite(progress) || progress < NOT_STARTED_PROGRESS || progress > MAX_TOLERATED_PROGRESS) {
24837
+ if (!Number.isFinite(progress)) {
24759
24838
  throw new TypeError(`invalid activity progress ${String(raw)}`);
24760
24839
  }
24761
- return progress;
24840
+ const isBelowNotStarted = progress < NOT_STARTED_PROGRESS;
24841
+ const isAboveCompleted = progress > MAX_TOLERATED_PROGRESS;
24842
+ const isWithinRange = !isBelowNotStarted && !isAboveCompleted;
24843
+ if (isWithinRange) return progress;
24844
+ const clamped = isBelowNotStarted ? NOT_STARTED_PROGRESS : COMPLETED_PROGRESS;
24845
+ recordDiagnostic(LOAD_DIAGNOSTIC_CODE.PROGRESS_OUT_OF_RANGE, activityId);
24846
+ reporter.warn(
24847
+ `[parseFromBackend] activity "${activityId}" progress ${String(raw)} is out of range \u2014 clamping to ${clamped}`
24848
+ );
24849
+ return clamped;
24762
24850
  }
24763
24851
  function parseCriticality(raw, reporter) {
24764
24852
  if (typeof raw.is_critical === "boolean") return raw.is_critical;
@@ -24791,7 +24879,12 @@ function parseActivity(raw, context) {
24791
24879
  context.defaultCalendarId
24792
24880
  );
24793
24881
  const inheritedDefaultCalendar = raw.calendarId == null || !context.knownCalendarIds.has(String(raw.calendarId));
24794
- const progress = parseProgress(raw.progress);
24882
+ const progress = parseProgress(
24883
+ raw.progress,
24884
+ context.reporter,
24885
+ context.recordDiagnostic,
24886
+ String(raw.unique_id)
24887
+ );
24795
24888
  const { constraintType, constraintDate } = resolveConstraint(
24796
24889
  raw,
24797
24890
  context.reporter,
@@ -26245,8 +26338,8 @@ var P6ActivityStatusError = class extends Error {
26245
26338
  }
26246
26339
  code;
26247
26340
  };
26248
- var COMPLETED_PHYSICAL_PERCENT = 100;
26249
- var NOT_STARTED_PHYSICAL_PERCENT = 0;
26341
+ var COMPLETED_PHYSICAL_PERCENT2 = 100;
26342
+ var NOT_STARTED_PHYSICAL_PERCENT2 = 0;
26250
26343
  function validateP6ActivityStatus(activity) {
26251
26344
  validateRemainingDuration(activity.remainingDurationHours);
26252
26345
  validateDates(activity);
@@ -26272,11 +26365,11 @@ function normalizeP6ActualDatePatch(current, patch) {
26272
26365
  executionStatus,
26273
26366
  ...actualFinish !== null ? {
26274
26367
  remainingDurationHours: 0,
26275
- physicalPercentComplete: COMPLETED_PHYSICAL_PERCENT
26368
+ physicalPercentComplete: COMPLETED_PHYSICAL_PERCENT2
26276
26369
  } : {},
26277
26370
  ...unStarting ? {
26278
26371
  remainingDurationHours: current.originalDurationHours,
26279
- physicalPercentComplete: NOT_STARTED_PHYSICAL_PERCENT
26372
+ physicalPercentComplete: NOT_STARTED_PHYSICAL_PERCENT2
26280
26373
  } : {}
26281
26374
  };
26282
26375
  validateP6ActivityDraftStatus({ ...current, ...normalized2 });
@@ -30020,6 +30113,39 @@ function blockedActivities(code, message, activityIds, missingInputs = []) {
30020
30113
  };
30021
30114
  }
30022
30115
 
30116
+ // src/p6/rolled-expected-finish.ts
30117
+ var PINNABLE_ACTIVITY_TYPES = /* @__PURE__ */ new Set([
30118
+ P6_ACTIVITY_TYPE.TASK,
30119
+ P6_ACTIVITY_TYPE.RESOURCE_DEPENDENT
30120
+ ]);
30121
+ var NO_ROLLED_EXPECTED_FINISHES = /* @__PURE__ */ new Map();
30122
+ function planRolledExpectedFinishes(input) {
30123
+ const adoptsShownFinishes = input.settings.useExpectedFinishDates && rollsDataDateForward(input.previousSettings, input.settings);
30124
+ if (!adoptsShownFinishes) return NO_ROLLED_EXPECTED_FINISHES;
30125
+ const pinned = /* @__PURE__ */ new Map();
30126
+ for (const [activityId, state] of input.activities) {
30127
+ const shownFinish = state.remainingEarlyFinish;
30128
+ if (shownFinish === null) continue;
30129
+ if (!needsExpectedFinishPin(state)) continue;
30130
+ pinned.set(activityId, new Date(shownFinish));
30131
+ }
30132
+ return pinned;
30133
+ }
30134
+ function rollsDataDateForward(previous, next) {
30135
+ const previousDataDate = previous.dataDate;
30136
+ const nextDataDate = next.dataDate;
30137
+ if (previousDataDate === null || nextDataDate === null) return false;
30138
+ return nextDataDate.getTime() > previousDataDate.getTime();
30139
+ }
30140
+ function needsExpectedFinishPin(state) {
30141
+ return state.executionStatus === P6_EXECUTION_STATUS.IN_PROGRESS && state.expectedFinish === null && hasPinnableActivityType(state);
30142
+ }
30143
+ function hasPinnableActivityType(state) {
30144
+ const { activityType } = state;
30145
+ if (activityType === void 0) return true;
30146
+ return PINNABLE_ACTIVITY_TYPES.has(activityType);
30147
+ }
30148
+
30023
30149
  // src/p6/commands.ts
30024
30150
  function dataDateScheduleReadiness(input) {
30025
30151
  const preflight = getP6SchedulePreflight({
@@ -30133,8 +30259,17 @@ function prepareDataDateSchedule(input) {
30133
30259
  preflight.settings,
30134
30260
  beforeProjectState
30135
30261
  );
30262
+ const rolledExpectedFinishes = planRolledExpectedFinishes({
30263
+ previousSettings: preflight.previousSettings,
30264
+ settings: preflight.settings,
30265
+ activities: preflight.activities
30266
+ });
30136
30267
  const calculated = calculateP6Schedule({
30137
- adapter: calculationState(input.state, preflight.activities),
30268
+ adapter: calculationState(
30269
+ input.state,
30270
+ preflight.activities,
30271
+ rolledExpectedFinishes
30272
+ ),
30138
30273
  settings: preflight.settings,
30139
30274
  project: {
30140
30275
  projectId: String(input.sector.projectId),
@@ -30180,11 +30315,19 @@ function requireProjectState(projectState) {
30180
30315
  "P6 capability is not enabled for this Schedule"
30181
30316
  );
30182
30317
  }
30183
- function calculationState(current, schedulableActivities) {
30318
+ function calculationState(current, schedulableActivities, rolledExpectedFinishes) {
30184
30319
  const state = current.fork();
30185
30320
  state.forEachActivity((activity) => {
30186
- if (schedulableActivities.has(activity.id)) return;
30187
- state.setActivityField(activity.id, "p6", void 0);
30321
+ if (!schedulableActivities.has(activity.id)) {
30322
+ state.setActivityField(activity.id, "p6", void 0);
30323
+ return;
30324
+ }
30325
+ const rolledExpectedFinish = rolledExpectedFinishes.get(activity.id);
30326
+ if (rolledExpectedFinish === void 0 || activity.p6 === void 0) return;
30327
+ state.setActivityField(activity.id, "p6", {
30328
+ ...activity.p6,
30329
+ expectedFinish: rolledExpectedFinish
30330
+ });
30188
30331
  });
30189
30332
  return state;
30190
30333
  }
@@ -30309,7 +30452,7 @@ function initializeDataDate(input, activities) {
30309
30452
  function attachImportedDataDate(activities, imported, settings) {
30310
30453
  for (const activity of activities) {
30311
30454
  const importedP6 = Object.hasOwn(imported, activity.id) ? imported[activity.id] : void 0;
30312
- const p6 = importedP6 ?? createOutbuildP6ActivityDraft(activity);
30455
+ const p6 = importedP6 === void 0 ? createOutbuildP6ActivityDraft(activity) : reconcileOutbuildProgress(activity, importedP6);
30313
30456
  if (p6 === void 0) continue;
30314
30457
  activity.p6 = p6;
30315
30458
  if (!hasActiveDataDate(settings)) continue;