@quolu/lattice 0.12.7 → 0.12.9

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.9",
4
4
  "description": "Lattice — phase-aware TODO graph compiler and conflict-aware orchestration runtime",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -49,6 +49,8 @@
49
49
  },
50
50
  "scripts": {
51
51
  "prepack": "npm --prefix sensor run build",
52
+ "verify:release-commit": "node scripts/verify-release-commit.mjs",
53
+ "prepublishOnly": "npm run verify:release-commit",
52
54
  "test": "node scripts/run-product-tests.mjs",
53
55
  "test:sensor": "npm --prefix sensor test",
54
56
  "precheck": "node --check src/bridge-launch-agent.mjs",
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);
@@ -1747,13 +1764,17 @@ function phaseV3CarrySemantics(plan, taskId, taskIdMap, phaseIdMap,
1747
1764
  ? { ...ref, task_id: taskIdMap.get(ref.task_id) ?? ref.task_id } : ref;
1748
1765
  const mapPhaseRef = (ref) => ref.project_id === plan.project_id && ref.plan_key === plan.plan_key
1749
1766
  ? { ...ref, phase_id: phaseIdMap.get(ref.phase_id) ?? ref.phase_id } : ref;
1767
+ // Phaseを持たない世代(todo_plan.v3)のtaskはphase_idを持たない。undefinedのまま
1768
+ // canonicalizeするとJSON treeでないとしてTypeErrorになり、typedな拒否理由が失われる。
1769
+ // nullへ正規化してcarry比較へ渡し、Phase割当ての獲得をcarry_semantics_changedとして拒否する。
1770
+ const mappedPhaseId = phaseIdMap.get(task.phase_id) ?? task.phase_id ?? null;
1750
1771
  const mappedTask = reconciliationMetadata ? {
1751
1772
  task_id: taskIdMap.get(task.task_id) ?? task.task_id, title: task.title, lane: task.lane,
1752
1773
  compile_binding: task.compile_binding,
1753
- phase_id: phaseIdMap.get(task.phase_id) ?? task.phase_id,
1774
+ phase_id: mappedPhaseId,
1754
1775
  } : { ...task,
1755
1776
  task_id: taskIdMap.get(task.task_id) ?? task.task_id,
1756
- phase_id: phaseIdMap.get(task.phase_id) ?? task.phase_id,
1777
+ phase_id: mappedPhaseId,
1757
1778
  parent_task_id: task.parent_task_id === null ? null
1758
1779
  : taskIdMap.get(task.parent_task_id) ?? task.parent_task_id,
1759
1780
  };
@@ -2575,7 +2596,19 @@ function validatePhaseV3SourceInventoryDiff(previousInventory, revision) {
2575
2596
  for (const entry of previousInventory.active) {
2576
2597
  const active = desired.active.find(({ task_id }) => task_id === entry.task_id);
2577
2598
  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}`)) {
2599
+ const relocated = revision.source_cutover_batch.operations.some((operation, index) => {
2600
+ if (operation.source_ref !== entry.source_ref || operation.source_digest !== entry.source_digest) {
2601
+ return false;
2602
+ }
2603
+ const archiveRef = todoCutoverArchiveSourceRef(revision.source_cutover_batch, index);
2604
+ return operation.disposition === 'active'
2605
+ ? desired.active.some((candidate) => candidate.task_id === operation.task_id
2606
+ && candidate.source_ref === archiveRef && candidate.source_digest === operation.source_digest)
2607
+ : desired.excluded_tombstones.some((candidate) => candidate.source_ref === archiveRef
2608
+ && candidate.source_digest === operation.source_digest);
2609
+ });
2610
+ if (!continued && !relocated
2611
+ && !desiredTombstoneKeys.has(`${entry.source_ref}\0${entry.source_digest}`)) {
2579
2612
  fail('REVISION_INVALID', 'predecessor_source_silently_dropped', { task_id: entry.task_id });
2580
2613
  }
2581
2614
  }
@@ -2904,7 +2937,12 @@ export async function applyPhaseTodoRevision(options = {}) {
2904
2937
  const descriptor = currentManifest.members.find(({ plan_key }) => plan_key === revision.plan_key);
2905
2938
  Object.assign(descriptor, { active_plan_version: revision.desired_plan.plan_version,
2906
2939
  plan_ref: planRef, journal_ref: journalRef, snapshot_ref: snapshotRef,
2907
- topology_digest: revision.desired_plan.topology_digest, journal_head_digest: genesis.event_digest });
2940
+ topology_digest: revision.desired_plan.topology_digest, journal_head_digest: genesis.event_digest,
2941
+ // manifest v2はactive_revision_digestがgenesisのrevision_digestと一致することを読み取り時に
2942
+ // 要求する。v3昇格後のstoreでここを据え置くと、書込みは成功したように見えるのに以後の
2943
+ // readがmanifest_revision_binding_mismatchで落ちる。v1 memberはこのkeyを持てない。
2944
+ ...(currentManifest.schema === 'lattice.todo_manifest.v2'
2945
+ ? { active_revision_digest: revision.revision_digest } : {}) });
2908
2946
  currentManifest.manifest_digest = todoSelfDigest(currentManifest, 'manifest_digest');
2909
2947
  await atomicWrite(path.resolve(repoRoot, MANIFEST_REF), canonicalLine(currentManifest));
2910
2948
  await protocolStage(options, 'phase_revision_manifest_activated');