@outbuild-company/schedule-core 1.28.0 → 1.29.1

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
@@ -513,6 +513,8 @@ function createActivitiesApi(engine) {
513
513
  childIds: (parentId) => engine.getChildrenIds(parentId),
514
514
  hasChildren: (parentId) => engine.hasChild(parentId),
515
515
  indentTargetParentIds: (ids) => engine.getIndentTargetParentIds(ids),
516
+ leafParentIdsPromotedBy: (actions) => engine.getLeafParentIdsPromotedBy(actions),
517
+ lookaheadConfirmationFor: (actions, options) => engine.getLookaheadConfirmation(actions, options),
516
518
  isCustomIdInUse: (customId, currentCustomId) => engine.isCustomIdInUse(customId, currentCustomId ?? null),
517
519
  clipboard: () => engine.getActivityClipboardView(),
518
520
  create: (input, options) => engine.dispatch(
@@ -583,11 +585,14 @@ function createActivitiesApi(engine) {
583
585
  newValue: progress
584
586
  }),
585
587
  copy: (context = {}) => engine.dispatch({ ...context, kind: DISPATCH_ACTION_KIND.ACTIVITY_COPY }),
586
- paste: (afterSiblingId, context = {}) => engine.dispatch({
587
- ...context,
588
- kind: DISPATCH_ACTION_KIND.ACTIVITY_PASTE_COPIED,
589
- destination: { afterSiblingId }
590
- }),
588
+ paste: (afterSiblingId, context = {}, options) => {
589
+ const action = {
590
+ ...context,
591
+ kind: DISPATCH_ACTION_KIND.ACTIVITY_PASTE_COPIED,
592
+ destination: { afterSiblingId }
593
+ };
594
+ return options === void 0 ? engine.dispatch(action) : engine.dispatch(action, options);
595
+ },
591
596
  duplicate: (activityIds, context = {}) => engine.dispatch({
592
597
  ...context,
593
598
  kind: DISPATCH_ACTION_KIND.ACTIVITY_DUPLICATE,
@@ -927,11 +932,6 @@ function diffSelections(before, after) {
927
932
  return { checked, unchecked };
928
933
  }
929
934
 
930
- // src/internal/selection/is-frozen.ts
931
- function isFrozen(activity) {
932
- return (activity.pendingRequestIds?.length ?? 0) > 0;
933
- }
934
-
935
935
  // src/dispatch/reasons.ts
936
936
  var REJECTION_REASON = {
937
937
  CANNOT_EDIT: "cannot_edit",
@@ -969,10 +969,6 @@ var REJECTION_REASON = {
969
969
  };
970
970
 
971
971
  // src/dispatch/selection.ts
972
- var snapshotIsFrozen = (adapter, id) => {
973
- const snapshot = adapter.getActivity(id);
974
- return snapshot != null && isFrozen(snapshot);
975
- };
976
972
  var buildSelectionChangeSet = (action, adapter, changed2) => {
977
973
  const viewState = [];
978
974
  const emit = (id, nextChecked) => {
@@ -997,8 +993,8 @@ function dispatchSelectionToggle(action, deps) {
997
993
  if (!adapter.getActivity(clickedId)) {
998
994
  return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
999
995
  }
1000
- const frozenSet = action.frozenIds ? new Set(action.frozenIds) : null;
1001
- const isActivityFrozen = frozenSet ? (id) => frozenSet.has(id) : (id) => snapshotIsFrozen(adapter, id);
996
+ const frozenSet = new Set((action.frozenIds ?? []).map(String));
997
+ const isActivityFrozen = (id) => frozenSet.has(id);
1002
998
  const update = computeSelectionUpdate({
1003
999
  clickedId,
1004
1000
  isChecked: action.isChecked,
@@ -1022,7 +1018,7 @@ function dispatchSelectionToggle(action, deps) {
1022
1018
  function dispatchSelectionReplace(action, deps) {
1023
1019
  const { adapter } = deps;
1024
1020
  const target = new Set(
1025
- action.activityIds.map(String).filter((id) => adapter.activityExists(id)).filter((id) => !snapshotIsFrozen(adapter, id))
1021
+ action.activityIds.map(String).filter((id) => adapter.activityExists(id))
1026
1022
  );
1027
1023
  const current = new Set(adapter.checkedIds().map(String));
1028
1024
  const changed2 = {
@@ -12469,6 +12465,11 @@ function cloneLink(link) {
12469
12465
  return { ...link };
12470
12466
  }
12471
12467
 
12468
+ // src/internal/selection/is-frozen.ts
12469
+ function isFrozen(activity) {
12470
+ return (activity.pendingRequestIds?.length ?? 0) > 0;
12471
+ }
12472
+
12472
12473
  // src/dispatch/sir-auto-reject.ts
12473
12474
  function detectSirAutoReject(beforeSnap, adapter, touchedIds) {
12474
12475
  const effects = [];
@@ -14845,6 +14846,184 @@ function resolveStartDate(parent, fallback, override) {
14845
14846
  return fallback ? new Date(fallback) : /* @__PURE__ */ new Date();
14846
14847
  }
14847
14848
 
14849
+ // src/activity-copy-paste/paste.ts
14850
+ function resolvePasteDestination(afterSiblingId, adapter) {
14851
+ const sibling = adapter.getActivity(afterSiblingId);
14852
+ if (!sibling) {
14853
+ return { ok: false, reason: REJECTION_REASON.ANCHOR_SIBLING_NOT_FOUND };
14854
+ }
14855
+ if (sibling.parentId == null) {
14856
+ return adapter.getRootIds().length === 1 ? {
14857
+ ok: true,
14858
+ parentId: sibling.id,
14859
+ beforeSiblingId: adapter.getChildren(sibling.id)[0]
14860
+ } : { ok: true, parentId: ROOT_PARENT_ID, afterSiblingId };
14861
+ }
14862
+ if (adapter.hasChildren(sibling.id)) {
14863
+ return {
14864
+ ok: true,
14865
+ parentId: sibling.id,
14866
+ beforeSiblingId: adapter.getChildren(sibling.id)[0]
14867
+ };
14868
+ }
14869
+ return {
14870
+ ok: true,
14871
+ parentId: String(sibling.parentId),
14872
+ afterSiblingId
14873
+ };
14874
+ }
14875
+ var invariantFailure = () => ({
14876
+ ok: false,
14877
+ reason: REJECTION_REASON.ACTIVITY_PASTE_INVARIANT_FAILED
14878
+ });
14879
+ async function dispatchActivityPasteCopied(action, options, deps) {
14880
+ const clipboard = deps.activityClipboard.value;
14881
+ if (clipboard === null) {
14882
+ return { ok: false, reason: REJECTION_REASON.CLIPBOARD_EMPTY };
14883
+ }
14884
+ const destination = resolvePasteDestination(
14885
+ action.destination.afterSiblingId,
14886
+ deps.adapter
14887
+ );
14888
+ if (!destination.ok) return destination;
14889
+ const beforeSnapshot = await snapshotActivities(
14890
+ deps.adapter,
14891
+ /* @__PURE__ */ new Set()
14892
+ );
14893
+ const touched = /* @__PURE__ */ new Set();
14894
+ const originalToNew = /* @__PURE__ */ new Map();
14895
+ const createdIds = [];
14896
+ let previousRootId;
14897
+ for (const copied of clipboard.activities) {
14898
+ const mappedParent = originalToNew.get(copied.originalParentId);
14899
+ const afterSiblingId = mappedParent === void 0 ? previousRootId ?? destination.afterSiblingId : void 0;
14900
+ const beforeSiblingId = mappedParent === void 0 && afterSiblingId === void 0 ? destination.beforeSiblingId : void 0;
14901
+ const created = await createActivityCore(
14902
+ {
14903
+ kind: DISPATCH_ACTION_KIND.ACTIVITY_CREATE,
14904
+ parentId: mappedParent ?? destination.parentId,
14905
+ ...afterSiblingId === void 0 ? {} : { afterSiblingId },
14906
+ ...beforeSiblingId === void 0 ? {} : { beforeSiblingId },
14907
+ overrides: cloneDomainValue(copied.overrides),
14908
+ eventSource: action.eventSource
14909
+ },
14910
+ deps,
14911
+ {
14912
+ skipCorrelativeRecompute: true,
14913
+ skipBeforeSnap: true,
14914
+ customIdReferenceId: action.destination.afterSiblingId,
14915
+ confirmedLookaheadParentIds: options.confirmedLookaheadParentIds,
14916
+ lookaheadTasksByActivityId: options.lookaheadTasksByActivityId
14917
+ }
14918
+ );
14919
+ if (!created.ok) return created;
14920
+ originalToNew.set(copied.originalId, created.newId);
14921
+ createdIds.push(created.newId);
14922
+ if (mappedParent === void 0) previousRootId = created.newId;
14923
+ }
14924
+ const correlativeShifts = recomputeCorrelativeIds(deps.adapter);
14925
+ const createdIdSet = new Set(createdIds.map(String));
14926
+ foldCorrelativeShifts(
14927
+ deps.adapter,
14928
+ correlativeShifts,
14929
+ createdIdSet,
14930
+ beforeSnapshot,
14931
+ touched
14932
+ );
14933
+ for (const createdId of createdIds) touched.add(String(createdId));
14934
+ const appliedLinks = [];
14935
+ for (const copiedLink of clipboard.links) {
14936
+ const source = originalToNew.get(copiedLink.source);
14937
+ const target = originalToNew.get(copiedLink.target);
14938
+ if (source === void 0 || target === void 0) return invariantFailure();
14939
+ const operation = {
14940
+ kind: LINK_OPERATION_KIND.CREATE,
14941
+ source,
14942
+ target,
14943
+ type: copiedLink.type,
14944
+ lag: copiedLink.lag
14945
+ };
14946
+ const result = applyLinkOperation(operation, {
14947
+ port: deps.adapter,
14948
+ newLinkId: deps.linkIdGen.next
14949
+ });
14950
+ if (!result.applied) return invariantFailure();
14951
+ appliedLinks.push({
14952
+ op: operation,
14953
+ finalLinkId: result.linkId,
14954
+ rejected: null
14955
+ });
14956
+ }
14957
+ const { scheduledIds } = await runPostMutation(
14958
+ {
14959
+ adapter: deps.adapter,
14960
+ scheduler: deps.scheduler,
14961
+ sector: deps.sector,
14962
+ defaultBaseCalendarId: deps.defaultBaseCalendarId,
14963
+ runAutomaticP6Schedule: deps.runAutomaticP6Schedule
14964
+ },
14965
+ {
14966
+ action,
14967
+ autoscheduleFrom: createdIds[0] ?? null,
14968
+ recomputeParentsFrom: createdIds,
14969
+ now: deps.now,
14970
+ options
14971
+ }
14972
+ );
14973
+ const linkChanges = buildLinkChangesForBatch(
14974
+ appliedLinks,
14975
+ /* @__PURE__ */ new Map(),
14976
+ deps.adapter
14977
+ );
14978
+ return {
14979
+ ok: true,
14980
+ changes: await assembleChangeSet(deps.adapter, {
14981
+ source: action,
14982
+ beforeSnap: beforeSnapshot,
14983
+ touchedIds: touched,
14984
+ scheduledIds,
14985
+ links: linkChanges,
14986
+ hoursPerDay: deps.sector.hoursPerDay
14987
+ })
14988
+ };
14989
+ }
14990
+
14991
+ // src/dispatch/confirmations.ts
14992
+ var LOOKAHEAD_CONFIRMATION_KIND = {
14993
+ HAS_LOOKAHEAD_TASKS: "has_lookahead_tasks"
14994
+ };
14995
+
14996
+ // src/dispatch/shared/hierarchy-cycle-guard.ts
14997
+ function reparentClosesEffectiveCycle(adapter, reparented) {
14998
+ const links = adapter.getAllLinks();
14999
+ const verdict = validateTopologyCommit({
15000
+ port: adapter,
15001
+ currentLinks: links,
15002
+ proposedLinks: links,
15003
+ proposal: { reparented }
15004
+ });
15005
+ return !verdict.ok;
15006
+ }
15007
+ function collectCyclicReparents(planned, adapter) {
15008
+ const cyclic = /* @__PURE__ */ new Set();
15009
+ if (planned.length === 0) return cyclic;
15010
+ const combined = new Map(
15011
+ planned.map((reparent) => [reparent.activityId, reparent.newParentId])
15012
+ );
15013
+ if (!reparentClosesEffectiveCycle(adapter, combined)) return cyclic;
15014
+ const accepted2 = /* @__PURE__ */ new Map();
15015
+ for (const reparent of planned) {
15016
+ const trial = new Map(accepted2);
15017
+ trial.set(reparent.activityId, reparent.newParentId);
15018
+ if (reparentClosesEffectiveCycle(adapter, trial)) {
15019
+ cyclic.add(reparent.activityId);
15020
+ continue;
15021
+ }
15022
+ accepted2.set(reparent.activityId, reparent.newParentId);
15023
+ }
15024
+ return cyclic;
15025
+ }
15026
+
14848
15027
  // src/dispatch/shared/positional.ts
14849
15028
  function computeInsertionCorrelativeId(adapter, parentId, afterSiblingId, beforeSiblingId) {
14850
15029
  const parentKey = parentId;
@@ -14929,6 +15108,161 @@ function computeFractionalCidAtIndex(adapter, parentKey, targetIndex, excludeId)
14929
15108
  return (before + after) / 2;
14930
15109
  }
14931
15110
 
15111
+ // src/dispatch/shared/indent-planning.ts
15112
+ function planIndentMoves(activityIds, adapter) {
15113
+ const selectedSet = new Set(activityIds);
15114
+ const failed = [];
15115
+ const moves = [];
15116
+ const plannedIds = /* @__PURE__ */ new Set();
15117
+ const blockingSet = new Set(selectedSet);
15118
+ const sortedIds = [...activityIds].sort((a, b) => {
15119
+ const aSnap = adapter.getActivity(a);
15120
+ const bSnap = adapter.getActivity(b);
15121
+ const ac = aSnap ? readCorrelativeId(aSnap) : NOT_SET_SORT_VALUE2;
15122
+ const bc = bSnap ? readCorrelativeId(bSnap) : NOT_SET_SORT_VALUE2;
15123
+ return ac - bc;
15124
+ });
15125
+ for (const id of sortedIds) {
15126
+ const activity = adapter.getActivity(id);
15127
+ if (!activity) {
15128
+ failed.push({
15129
+ activityId: id,
15130
+ reason: REJECTION_REASON.ACTIVITY_NOT_FOUND
15131
+ });
15132
+ blockingSet.delete(id);
15133
+ continue;
15134
+ }
15135
+ if (hasAncestorInSet(id, plannedIds, adapter)) continue;
15136
+ const targetParentId = findPreviousNonSelectedSibling(
15137
+ id,
15138
+ blockingSet,
15139
+ adapter
15140
+ );
15141
+ if (targetParentId == null) {
15142
+ failed.push({
15143
+ activityId: id,
15144
+ reason: REJECTION_REASON.INDENT_NO_ELIGIBLE_SIBLING
15145
+ });
15146
+ blockingSet.delete(id);
15147
+ continue;
15148
+ }
15149
+ const oldParentRaw = adapter.getActivity(id)?.parentId;
15150
+ const oldParentId = isRootParent(oldParentRaw) ? ROOT_PARENT_ID : String(oldParentRaw);
15151
+ moves.push({ activityId: id, newParentId: targetParentId, oldParentId });
15152
+ plannedIds.add(id);
15153
+ }
15154
+ const cyclicMoveIds = collectCyclicReparents(moves, adapter);
15155
+ for (const cyclicId of cyclicMoveIds) {
15156
+ failed.push({
15157
+ activityId: cyclicId,
15158
+ reason: REJECTION_REASON.HIERARCHY_CYCLE
15159
+ });
15160
+ }
15161
+ const allowedMoves = moves.filter(
15162
+ (move) => !cyclicMoveIds.has(move.activityId)
15163
+ );
15164
+ return { moves, cyclicMoveIds, allowedMoves, failed };
15165
+ }
15166
+
15167
+ // src/dispatch/shared/lookahead-parent-confirmation.ts
15168
+ function lookaheadParentsConfirmation(leafParents, options) {
15169
+ const confirmed = new Set(options.confirmedLookaheadParentIds);
15170
+ const facts = options.lookaheadTasksByActivityId;
15171
+ const blocking = [
15172
+ ...new Set(
15173
+ leafParents.filter(
15174
+ (parent) => !confirmed.has(String(parent.id)) && (facts?.[String(parent.id)] ?? parent.hasLookaheadTasks)
15175
+ ).map((parent) => String(parent.id))
15176
+ )
15177
+ ];
15178
+ const [first] = blocking;
15179
+ if (first === void 0) return null;
15180
+ return {
15181
+ ok: false,
15182
+ reason: REJECTION_REASON.CONFIRMATION_REQUIRED,
15183
+ confirmation: {
15184
+ kind: LOOKAHEAD_CONFIRMATION_KIND.HAS_LOOKAHEAD_TASKS,
15185
+ activityId: first,
15186
+ ...blocking.length > 1 ? { activityIds: blocking } : {}
15187
+ }
15188
+ };
15189
+ }
15190
+ function leafParentsPromotedBy(parentIds, adapter) {
15191
+ const promoted = /* @__PURE__ */ new Map();
15192
+ for (const parentId of parentIds) {
15193
+ const parent = adapter.getActivity(parentId);
15194
+ if (!parent) continue;
15195
+ if (adapter.getChildren(parentId).length === 0)
15196
+ promoted.set(String(parent.id), parent);
15197
+ }
15198
+ return [...promoted.values()];
15199
+ }
15200
+ function leafParentIdsPromotedByActions(actions, adapter, hasActivityClipboard) {
15201
+ return leafParentsPromotedByActions(
15202
+ actions,
15203
+ adapter,
15204
+ hasActivityClipboard
15205
+ ).map((parent) => String(parent.id));
15206
+ }
15207
+ function lookaheadConfirmationForActions(actions, options, adapter, hasActivityClipboard) {
15208
+ const rejection = lookaheadParentsConfirmation(
15209
+ leafParentsPromotedByActions(actions, adapter, hasActivityClipboard),
15210
+ options
15211
+ );
15212
+ return rejection === null ? null : rejection.confirmation;
15213
+ }
15214
+ function leafParentsPromotedByActions(actions, adapter, hasActivityClipboard) {
15215
+ const parentIds = [];
15216
+ for (const action of actions) {
15217
+ parentIds.push(
15218
+ ...targetParentIds(action, adapter, hasActivityClipboard).map(String)
15219
+ );
15220
+ }
15221
+ return leafParentsPromotedBy(parentIds, adapter);
15222
+ }
15223
+ function targetParentIds(action, adapter, hasActivityClipboard) {
15224
+ switch (action.kind) {
15225
+ case DISPATCH_ACTION_KIND.ACTIVITY_CREATE:
15226
+ return [action.parentId];
15227
+ case DISPATCH_ACTION_KIND.ACTIVITY_MOVE: {
15228
+ const activity = adapter.getActivity(action.activityId);
15229
+ if (!activity) return [];
15230
+ if (normalizeParentKey(activity.parentId) === String(action.parentId)) {
15231
+ return [];
15232
+ }
15233
+ return movesIntoOwnBranch(action, adapter) ? [] : [action.parentId];
15234
+ }
15235
+ case DISPATCH_ACTION_KIND.ACTIVITY_INDENT:
15236
+ return planIndentMoves(action.activityIds, adapter).allowedMoves.map(
15237
+ (move) => move.newParentId
15238
+ );
15239
+ case DISPATCH_ACTION_KIND.ACTIVITY_PASTE_COPIED: {
15240
+ if (!hasActivityClipboard) return [];
15241
+ const destination = resolvePasteDestination(
15242
+ action.destination.afterSiblingId,
15243
+ adapter
15244
+ );
15245
+ return destination.ok ? [destination.parentId] : [];
15246
+ }
15247
+ case ACTIVITY_BATCH_ACTION_KIND:
15248
+ return action.mode === ACTIVITY_BATCH_MODE.APPEND ? action.items.flatMap(
15249
+ (item) => typeof item.parent === "string" ? [item.parent] : []
15250
+ ) : [];
15251
+ default:
15252
+ return [];
15253
+ }
15254
+ }
15255
+ function movesIntoOwnBranch(action, adapter) {
15256
+ if (isRootParent(action.parentId)) return false;
15257
+ if (action.parentId === action.activityId || adapter.isChildOf(action.parentId, action.activityId)) {
15258
+ return true;
15259
+ }
15260
+ return reparentClosesEffectiveCycle(
15261
+ adapter,
15262
+ /* @__PURE__ */ new Map([[action.activityId, action.parentId]])
15263
+ );
15264
+ }
15265
+
14932
15266
  // src/dispatch/creation-overrides.ts
14933
15267
  var PUBLIC_CREATION_OVERRIDE_FIELDS = /* @__PURE__ */ new Set([
14934
15268
  ...[...CREATION_OVERRIDE_FIELDS].filter(
@@ -15395,7 +15729,7 @@ function removeIncidentLinks(activityId, deps) {
15395
15729
  async function createActivityCore(action, deps, opts = {}) {
15396
15730
  const { adapter, scheduler, sector, calendars, activityIdGen, uidGen } = deps;
15397
15731
  if (!opts.skipCacheInvalidation) scheduler.invalidateAllCaches();
15398
- const target = validateCreateTarget(action, adapter);
15732
+ const target = validateCreateTarget(action, adapter, opts);
15399
15733
  if (!target.ok) return target;
15400
15734
  const { parent, anchorSiblingId, isRoot } = target;
15401
15735
  const newId = resolveNewActivityId(
@@ -15479,7 +15813,7 @@ async function createActivityCore(action, deps, opts = {}) {
15479
15813
  removedParentLinks
15480
15814
  };
15481
15815
  }
15482
- function validateCreateTarget(action, adapter) {
15816
+ function validateCreateTarget(action, adapter, opts) {
15483
15817
  const isRoot = isRootParent(action.parentId);
15484
15818
  const parent = isRoot ? null : adapter.getActivity(action.parentId);
15485
15819
  if (!isRoot && !parent) {
@@ -15501,6 +15835,13 @@ function validateCreateTarget(action, adapter) {
15501
15835
  anchorSiblingId
15502
15836
  );
15503
15837
  if (anchorError) return anchorError;
15838
+ if (parent !== null && !opts.parentsAlreadyChecked) {
15839
+ const rejection = lookaheadParentsConfirmation(
15840
+ leafParentsPromotedBy([action.parentId], adapter),
15841
+ opts
15842
+ );
15843
+ if (rejection) return rejection;
15844
+ }
15504
15845
  return { ok: true, parent, anchorSiblingId, isRoot };
15505
15846
  }
15506
15847
  function validateAnchorSibling(action, adapter, isRoot, anchorSiblingId) {
@@ -15599,7 +15940,11 @@ async function dispatchActivityCreate(action, options, deps) {
15599
15940
  if (!normalized2.ok) return normalized2;
15600
15941
  const core = await createActivityCore(
15601
15942
  { ...action, overrides: normalized2.value },
15602
- deps
15943
+ deps,
15944
+ {
15945
+ confirmedLookaheadParentIds: options.confirmedLookaheadParentIds,
15946
+ lookaheadTasksByActivityId: options.lookaheadTasksByActivityId
15947
+ }
15603
15948
  );
15604
15949
  if (!core.ok) return core;
15605
15950
  const { newId } = core;
@@ -15656,6 +16001,13 @@ async function dispatchActivityBatch(action, options, deps) {
15656
16001
  if (validationError) return { ok: false, reason: validationError };
15657
16002
  const plan = buildBatchPlan(action, deps);
15658
16003
  if (!plan.ok) return plan;
16004
+ if (action.mode === ACTIVITY_BATCH_MODE.APPEND) {
16005
+ const rejection = lookaheadParentsConfirmation(
16006
+ leafParentsPromotedBy(existingParentIds(plan.plan.items), deps.adapter),
16007
+ options
16008
+ );
16009
+ if (rejection) return rejection;
16010
+ }
15659
16011
  const existingActivityIds = action.mode === ACTIVITY_BATCH_MODE.REPLACE ? new Set(deps.adapter.getAllIds().map((id) => String(id))) : /* @__PURE__ */ new Set();
15660
16012
  const beforeSnap = action.mode === ACTIVITY_BATCH_MODE.REPLACE ? await snapshotActivities(deps.adapter, existingActivityIds) : /* @__PURE__ */ new Map();
15661
16013
  const beforeLinks = action.mode === ACTIVITY_BATCH_MODE.REPLACE ? snapshotAllLinks(deps) : /* @__PURE__ */ new Map();
@@ -15780,6 +16132,11 @@ async function dispatchActivityBatch(action, options, deps) {
15780
16132
  } : {}
15781
16133
  };
15782
16134
  }
16135
+ function existingParentIds(items) {
16136
+ return items.filter(
16137
+ (item) => item.parentBatchId === null && item.parentId !== ROOT_PARENT_ID
16138
+ ).map((item) => String(item.parentId));
16139
+ }
15783
16140
  function validateBatch(action, deps) {
15784
16141
  if (action.mode !== ACTIVITY_BATCH_MODE.APPEND && action.mode !== ACTIVITY_BATCH_MODE.REPLACE) {
15785
16142
  return REJECTION_REASON.INVALID;
@@ -16077,7 +16434,8 @@ async function createPlannedActivities(action, plannedItems, deps) {
16077
16434
  skipBeforeSnap: true,
16078
16435
  skipCacheInvalidation: true,
16079
16436
  skipRollupCascades: true,
16080
- insertionCorrelativeId: item.insertionCorrelativeId
16437
+ insertionCorrelativeId: item.insertionCorrelativeId,
16438
+ parentsAlreadyChecked: true
16081
16439
  }
16082
16440
  );
16083
16441
  if (!core.ok) return core;
@@ -16840,37 +17198,6 @@ function isAncestor(descendantId, candidateAncestorId, adapter) {
16840
17198
  return false;
16841
17199
  }
16842
17200
 
16843
- // src/dispatch/shared/hierarchy-cycle-guard.ts
16844
- function reparentClosesEffectiveCycle(adapter, reparented) {
16845
- const links = adapter.getAllLinks();
16846
- const verdict = validateTopologyCommit({
16847
- port: adapter,
16848
- currentLinks: links,
16849
- proposedLinks: links,
16850
- proposal: { reparented }
16851
- });
16852
- return !verdict.ok;
16853
- }
16854
- function collectCyclicReparents(planned, adapter) {
16855
- const cyclic = /* @__PURE__ */ new Set();
16856
- if (planned.length === 0) return cyclic;
16857
- const combined = new Map(
16858
- planned.map((reparent) => [reparent.activityId, reparent.newParentId])
16859
- );
16860
- if (!reparentClosesEffectiveCycle(adapter, combined)) return cyclic;
16861
- const accepted2 = /* @__PURE__ */ new Map();
16862
- for (const reparent of planned) {
16863
- const trial = new Map(accepted2);
16864
- trial.set(reparent.activityId, reparent.newParentId);
16865
- if (reparentClosesEffectiveCycle(adapter, trial)) {
16866
- cyclic.add(reparent.activityId);
16867
- continue;
16868
- }
16869
- accepted2.set(reparent.activityId, reparent.newParentId);
16870
- }
16871
- return cyclic;
16872
- }
16873
-
16874
17201
  // src/dispatch/handlers/activity-move.ts
16875
17202
  async function dispatchActivityMove(action, options, deps) {
16876
17203
  const { adapter, scheduler, sector } = deps;
@@ -16923,6 +17250,13 @@ async function dispatchActivityMove(action, options, deps) {
16923
17250
  return { ok: false, reason: REJECTION_REASON.HIERARCHY_CYCLE };
16924
17251
  }
16925
17252
  }
17253
+ if (parentChanged && !newParentIsRoot) {
17254
+ const rejection = lookaheadParentsConfirmation(
17255
+ leafParentsPromotedBy([action.parentId], adapter),
17256
+ options
17257
+ );
17258
+ if (rejection) return rejection;
17259
+ }
16926
17260
  const touched = /* @__PURE__ */ new Set([action.activityId]);
16927
17261
  if (parentChanged) {
16928
17262
  if (oldParentKey !== ROOT_PARENT_ID) touched.add(oldParentKey);
@@ -17082,62 +17416,6 @@ async function dispatchActivityMove(action, options, deps) {
17082
17416
  };
17083
17417
  }
17084
17418
 
17085
- // src/dispatch/shared/indent-planning.ts
17086
- function planIndentMoves(activityIds, adapter) {
17087
- const selectedSet = new Set(activityIds);
17088
- const failed = [];
17089
- const moves = [];
17090
- const plannedIds = /* @__PURE__ */ new Set();
17091
- const blockingSet = new Set(selectedSet);
17092
- const sortedIds = [...activityIds].sort((a, b) => {
17093
- const aSnap = adapter.getActivity(a);
17094
- const bSnap = adapter.getActivity(b);
17095
- const ac = aSnap ? readCorrelativeId(aSnap) : NOT_SET_SORT_VALUE2;
17096
- const bc = bSnap ? readCorrelativeId(bSnap) : NOT_SET_SORT_VALUE2;
17097
- return ac - bc;
17098
- });
17099
- for (const id of sortedIds) {
17100
- const activity = adapter.getActivity(id);
17101
- if (!activity) {
17102
- failed.push({
17103
- activityId: id,
17104
- reason: REJECTION_REASON.ACTIVITY_NOT_FOUND
17105
- });
17106
- blockingSet.delete(id);
17107
- continue;
17108
- }
17109
- if (hasAncestorInSet(id, plannedIds, adapter)) continue;
17110
- const targetParentId = findPreviousNonSelectedSibling(
17111
- id,
17112
- blockingSet,
17113
- adapter
17114
- );
17115
- if (targetParentId == null) {
17116
- failed.push({
17117
- activityId: id,
17118
- reason: REJECTION_REASON.INDENT_NO_ELIGIBLE_SIBLING
17119
- });
17120
- blockingSet.delete(id);
17121
- continue;
17122
- }
17123
- const oldParentRaw = adapter.getActivity(id)?.parentId;
17124
- const oldParentId = isRootParent(oldParentRaw) ? ROOT_PARENT_ID : String(oldParentRaw);
17125
- moves.push({ activityId: id, newParentId: targetParentId, oldParentId });
17126
- plannedIds.add(id);
17127
- }
17128
- const cyclicMoveIds = collectCyclicReparents(moves, adapter);
17129
- for (const cyclicId of cyclicMoveIds) {
17130
- failed.push({
17131
- activityId: cyclicId,
17132
- reason: REJECTION_REASON.HIERARCHY_CYCLE
17133
- });
17134
- }
17135
- const allowedMoves = moves.filter(
17136
- (move) => !cyclicMoveIds.has(move.activityId)
17137
- );
17138
- return { moves, cyclicMoveIds, allowedMoves, failed };
17139
- }
17140
-
17141
17419
  // src/dispatch/handlers/activity-indent.ts
17142
17420
  async function dispatchActivityIndent(action, options, deps) {
17143
17421
  const { adapter, scheduler, sector } = deps;
@@ -17159,6 +17437,14 @@ async function dispatchActivityIndent(action, options, deps) {
17159
17437
  }
17160
17438
  }
17161
17439
  const allowedMoves = plan.allowedMoves;
17440
+ const rejection = lookaheadParentsConfirmation(
17441
+ leafParentsPromotedBy(
17442
+ allowedMoves.map((move) => move.newParentId),
17443
+ adapter
17444
+ ),
17445
+ options
17446
+ );
17447
+ if (rejection) return rejection;
17162
17448
  const beforeSnap = await snapshotActivities(adapter, touched);
17163
17449
  for (const { activityId, newParentId } of allowedMoves) {
17164
17450
  adapter.setActivityField(activityId, ACTIVITY_PROPERTY.PARENT, newParentId);
@@ -18956,146 +19242,6 @@ function dispatchActivityClipboardImport(action, deps) {
18956
19242
  return clipboardOnlyResult(action);
18957
19243
  }
18958
19244
 
18959
- // src/activity-copy-paste/paste.ts
18960
- function resolveDestination(afterSiblingId, adapter) {
18961
- const sibling = adapter.getActivity(afterSiblingId);
18962
- if (!sibling) {
18963
- return { ok: false, reason: REJECTION_REASON.ANCHOR_SIBLING_NOT_FOUND };
18964
- }
18965
- if (sibling.parentId == null) {
18966
- return adapter.getRootIds().length === 1 ? {
18967
- ok: true,
18968
- parentId: sibling.id,
18969
- beforeSiblingId: adapter.getChildren(sibling.id)[0]
18970
- } : { ok: true, parentId: ROOT_PARENT_ID, afterSiblingId };
18971
- }
18972
- if (adapter.hasChildren(sibling.id)) {
18973
- return {
18974
- ok: true,
18975
- parentId: sibling.id,
18976
- beforeSiblingId: adapter.getChildren(sibling.id)[0]
18977
- };
18978
- }
18979
- return {
18980
- ok: true,
18981
- parentId: String(sibling.parentId),
18982
- afterSiblingId
18983
- };
18984
- }
18985
- var invariantFailure = () => ({
18986
- ok: false,
18987
- reason: REJECTION_REASON.ACTIVITY_PASTE_INVARIANT_FAILED
18988
- });
18989
- async function dispatchActivityPasteCopied(action, options, deps) {
18990
- const clipboard = deps.activityClipboard.value;
18991
- if (clipboard === null) {
18992
- return { ok: false, reason: REJECTION_REASON.CLIPBOARD_EMPTY };
18993
- }
18994
- const destination = resolveDestination(
18995
- action.destination.afterSiblingId,
18996
- deps.adapter
18997
- );
18998
- if (!destination.ok) return destination;
18999
- const beforeSnapshot = await snapshotActivities(
19000
- deps.adapter,
19001
- /* @__PURE__ */ new Set()
19002
- );
19003
- const touched = /* @__PURE__ */ new Set();
19004
- const originalToNew = /* @__PURE__ */ new Map();
19005
- const createdIds = [];
19006
- let previousRootId;
19007
- for (const copied of clipboard.activities) {
19008
- const mappedParent = originalToNew.get(copied.originalParentId);
19009
- const afterSiblingId = mappedParent === void 0 ? previousRootId ?? destination.afterSiblingId : void 0;
19010
- const beforeSiblingId = mappedParent === void 0 && afterSiblingId === void 0 ? destination.beforeSiblingId : void 0;
19011
- const created = await createActivityCore(
19012
- {
19013
- kind: DISPATCH_ACTION_KIND.ACTIVITY_CREATE,
19014
- parentId: mappedParent ?? destination.parentId,
19015
- ...afterSiblingId === void 0 ? {} : { afterSiblingId },
19016
- ...beforeSiblingId === void 0 ? {} : { beforeSiblingId },
19017
- overrides: cloneDomainValue(copied.overrides),
19018
- eventSource: action.eventSource
19019
- },
19020
- deps,
19021
- {
19022
- skipCorrelativeRecompute: true,
19023
- skipBeforeSnap: true,
19024
- customIdReferenceId: action.destination.afterSiblingId
19025
- }
19026
- );
19027
- if (!created.ok) return created;
19028
- originalToNew.set(copied.originalId, created.newId);
19029
- createdIds.push(created.newId);
19030
- if (mappedParent === void 0) previousRootId = created.newId;
19031
- }
19032
- const correlativeShifts = recomputeCorrelativeIds(deps.adapter);
19033
- const createdIdSet = new Set(createdIds.map(String));
19034
- foldCorrelativeShifts(
19035
- deps.adapter,
19036
- correlativeShifts,
19037
- createdIdSet,
19038
- beforeSnapshot,
19039
- touched
19040
- );
19041
- for (const createdId of createdIds) touched.add(String(createdId));
19042
- const appliedLinks = [];
19043
- for (const copiedLink of clipboard.links) {
19044
- const source = originalToNew.get(copiedLink.source);
19045
- const target = originalToNew.get(copiedLink.target);
19046
- if (source === void 0 || target === void 0) return invariantFailure();
19047
- const operation = {
19048
- kind: LINK_OPERATION_KIND.CREATE,
19049
- source,
19050
- target,
19051
- type: copiedLink.type,
19052
- lag: copiedLink.lag
19053
- };
19054
- const result = applyLinkOperation(operation, {
19055
- port: deps.adapter,
19056
- newLinkId: deps.linkIdGen.next
19057
- });
19058
- if (!result.applied) return invariantFailure();
19059
- appliedLinks.push({
19060
- op: operation,
19061
- finalLinkId: result.linkId,
19062
- rejected: null
19063
- });
19064
- }
19065
- const { scheduledIds } = await runPostMutation(
19066
- {
19067
- adapter: deps.adapter,
19068
- scheduler: deps.scheduler,
19069
- sector: deps.sector,
19070
- defaultBaseCalendarId: deps.defaultBaseCalendarId,
19071
- runAutomaticP6Schedule: deps.runAutomaticP6Schedule
19072
- },
19073
- {
19074
- action,
19075
- autoscheduleFrom: createdIds[0] ?? null,
19076
- recomputeParentsFrom: createdIds,
19077
- now: deps.now,
19078
- options
19079
- }
19080
- );
19081
- const linkChanges = buildLinkChangesForBatch(
19082
- appliedLinks,
19083
- /* @__PURE__ */ new Map(),
19084
- deps.adapter
19085
- );
19086
- return {
19087
- ok: true,
19088
- changes: await assembleChangeSet(deps.adapter, {
19089
- source: action,
19090
- beforeSnap: beforeSnapshot,
19091
- touchedIds: touched,
19092
- scheduledIds,
19093
- links: linkChanges,
19094
- hoursPerDay: deps.sector.hoursPerDay
19095
- })
19096
- };
19097
- }
19098
-
19099
19245
  // src/activity-copy-paste/duplicate.ts
19100
19246
  var CLONE_YIELD_EVERY_N = 200;
19101
19247
  var CREATE_OPTIONS = {
@@ -32112,6 +32258,41 @@ var ScheduleCore = class _ScheduleCore {
32112
32258
  ...new Set(plan.allowedMoves.map((move) => String(move.newParentId)))
32113
32259
  ];
32114
32260
  }
32261
+ /**
32262
+ * Lists the leaf activities the actions would turn into parents.
32263
+ *
32264
+ * Each action is checked against the current state, not against the state
32265
+ * left by the actions before it.
32266
+ *
32267
+ * @param actions - Actions the caller is about to dispatch.
32268
+ */
32269
+ getLeafParentIdsPromotedBy(actions) {
32270
+ this.assertReady();
32271
+ return leafParentIdsPromotedByActions(
32272
+ actions,
32273
+ this.coreRuntime.state,
32274
+ this._activityClipboard.value !== null
32275
+ );
32276
+ }
32277
+ /**
32278
+ * Returns the lookahead confirmation the dispatch rule would raise for the
32279
+ * actions, or `null` when none is needed. The rule code is the dispatch rule
32280
+ * and nothing changes in the schedule. Like
32281
+ * {@link ScheduleCore.getLeafParentIdsPromotedBy}, it checks each action
32282
+ * against the current state.
32283
+ *
32284
+ * @param actions - Actions the caller is about to dispatch.
32285
+ * @param options - The `confirmedLookaheadParentIds` and `lookaheadTasksByActivityId` the dispatch would receive.
32286
+ */
32287
+ getLookaheadConfirmation(actions, options = {}) {
32288
+ this.assertReady();
32289
+ return lookaheadConfirmationForActions(
32290
+ actions,
32291
+ options,
32292
+ this.coreRuntime.state,
32293
+ this._activityClipboard.value !== null
32294
+ );
32295
+ }
32115
32296
  /**
32116
32297
  * Reads defensive direct-child snapshots in correlative order.
32117
32298
  *
@@ -34856,7 +35037,6 @@ var DEFAULT_HOURS_PER_DAY = 8;
34856
35037
  // src/creation/creation-kinds.ts
34857
35038
  var CREATION_KIND = {
34858
35039
  CHILD: "child",
34859
- CHILD_CONFIRMED: "child-confirmed",
34860
35040
  LINE: "line",
34861
35041
  ROOT: "root"
34862
35042
  };
@@ -34903,6 +35083,7 @@ exports.LINK_REJECTION_REASON = LINK_REJECTION_REASON;
34903
35083
  exports.LINK_RELATIONSHIP = LINK_RELATIONSHIP;
34904
35084
  exports.LINK_TYPE = LINK_TYPE;
34905
35085
  exports.LINK_TYPE_CODE = LINK_TYPE_CODE;
35086
+ exports.LOOKAHEAD_CONFIRMATION_KIND = LOOKAHEAD_CONFIRMATION_KIND;
34906
35087
  exports.LookaheadActualTransitionError = LookaheadActualTransitionError;
34907
35088
  exports.MINIMUM_GROUP_DEPTH = MINIMUM_GROUP_DEPTH;
34908
35089
  exports.MINIMUM_GROUP_LEVEL = MINIMUM_GROUP_LEVEL;