@quolu/lattice 0.12.7 → 0.12.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.12.7",
3
+ "version": "0.12.8",
4
4
  "description": "Lattice — phase-aware TODO graph compiler and conflict-aware orchestration runtime",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/todo-cli.mjs CHANGED
@@ -307,7 +307,7 @@ async function mutate({ repoRoot, env, planKey, taskId, kind, payload, evidenceR
307
307
  planKey,
308
308
  event: { kind, task_id: taskId, actor, payload: eventPayload },
309
309
  });
310
- const task = snapshot.tasks.find(({ task_id: current }) => current === taskId);
310
+ const task = snapshot.tasks.find(({ task_id: current }) => current === event.task_id);
311
311
  const result = {
312
312
  schema: 'lattice.todo_mutation_result.v1',
313
313
  project_id: event.project_id,
@@ -328,9 +328,10 @@ async function mutate({ repoRoot, env, planKey, taskId, kind, payload, evidenceR
328
328
 
329
329
  async function startTask({ repoRoot, env, planKey, taskId, overrideReason, parallelFrontier }) {
330
330
  const projection = projectTodoStatus(await readTodoStore({ repoRoot }));
331
- const targetReady = projection.next_ready.some((task) => (
332
- task.plan_key === planKey && task.task_id === taskId
331
+ const readyTask = projection.next_ready.find((task) => (
332
+ task.plan_key === planKey && task.task_id.toLowerCase() === taskId.toLowerCase()
333
333
  ));
334
+ const targetReady = readyTask !== undefined;
334
335
  if (parallelFrontier && !targetReady) {
335
336
  throw new TodoStoreError('PARALLEL_DISPATCH_INVALID', 'parallel_frontier_not_applicable');
336
337
  }
@@ -344,7 +345,7 @@ async function startTask({ repoRoot, env, planKey, taskId, overrideReason, paral
344
345
  serial_reason_flag: '--override-reason',
345
346
  });
346
347
  }
347
- return mutate({ repoRoot, env, planKey, taskId, kind: 'start',
348
+ return mutate({ repoRoot, env, planKey, taskId: readyTask?.task_id ?? taskId, kind: 'start',
348
349
  payload: { override_reason: overrideReason }, evidenceRef: null });
349
350
  }
350
351
 
@@ -1117,6 +1117,22 @@ function resolveTargetedEvent(input, storeMember) {
1117
1117
  return input;
1118
1118
  }
1119
1119
 
1120
+ function resolveCanonicalTaskId(plan, requestedTaskId) {
1121
+ if (requestedTaskId === null || requestedTaskId === undefined) return requestedTaskId;
1122
+ const exact = plan.tasks.find(({ task_id: taskId }) => taskId === requestedTaskId);
1123
+ if (exact) return exact.task_id;
1124
+ const folded = requestedTaskId.toLowerCase();
1125
+ const matches = plan.tasks.filter(({ task_id: taskId }) => taskId.toLowerCase() === folded);
1126
+ if (matches.length === 0) fail('TASK_NOT_FOUND', 'task_not_found', {
1127
+ requested_task_id: requestedTaskId,
1128
+ });
1129
+ if (matches.length > 1) fail('TASK_ID_AMBIGUOUS', 'task_id_case_ambiguous', {
1130
+ requested_task_id: requestedTaskId,
1131
+ matching_task_ids: matches.map(({ task_id: taskId }) => taskId).sort(),
1132
+ });
1133
+ return matches[0].task_id;
1134
+ }
1135
+
1120
1136
  export async function appendTodoEvent(options = {}) {
1121
1137
  requireWriter(options.writer, 'g5-authoring');
1122
1138
  const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
@@ -1126,6 +1142,7 @@ export async function appendTodoEvent(options = {}) {
1126
1142
  if (!member) fail('STORE_INCONSISTENT', 'plan_not_active');
1127
1143
  const input = resolveTargetedEvent({
1128
1144
  ...options.event,
1145
+ task_id: resolveCanonicalTaskId(member.plan, options.event.task_id),
1129
1146
  recorded_at: options.event.recorded_at ?? new Date().toISOString(),
1130
1147
  }, member);
1131
1148
  const event = nextEvent(input, member);
@@ -2575,7 +2592,19 @@ function validatePhaseV3SourceInventoryDiff(previousInventory, revision) {
2575
2592
  for (const entry of previousInventory.active) {
2576
2593
  const active = desired.active.find(({ task_id }) => task_id === entry.task_id);
2577
2594
  const continued = active?.source_ref === entry.source_ref && active.source_digest === entry.source_digest;
2578
- if (!continued && !desiredTombstoneKeys.has(`${entry.source_ref}\0${entry.source_digest}`)) {
2595
+ const relocated = revision.source_cutover_batch.operations.some((operation, index) => {
2596
+ if (operation.source_ref !== entry.source_ref || operation.source_digest !== entry.source_digest) {
2597
+ return false;
2598
+ }
2599
+ const archiveRef = todoCutoverArchiveSourceRef(revision.source_cutover_batch, index);
2600
+ return operation.disposition === 'active'
2601
+ ? desired.active.some((candidate) => candidate.task_id === operation.task_id
2602
+ && candidate.source_ref === archiveRef && candidate.source_digest === operation.source_digest)
2603
+ : desired.excluded_tombstones.some((candidate) => candidate.source_ref === archiveRef
2604
+ && candidate.source_digest === operation.source_digest);
2605
+ });
2606
+ if (!continued && !relocated
2607
+ && !desiredTombstoneKeys.has(`${entry.source_ref}\0${entry.source_digest}`)) {
2579
2608
  fail('REVISION_INVALID', 'predecessor_source_silently_dropped', { task_id: entry.task_id });
2580
2609
  }
2581
2610
  }