@quolu/lattice 0.57.2 → 0.58.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.
@@ -28,6 +28,18 @@ import {
28
28
  validateTodoWitnessSet,
29
29
  } from './todo-independence-contracts.mjs';
30
30
  import { validateSeamProposal } from './seam-proposal-contracts.mjs';
31
+ import {
32
+ digestTodoStructureRealizationHeads,
33
+ explainTodoStructureSet,
34
+ explainTodoStructureRealization,
35
+ validateTodoStructureBinding,
36
+ validateTodoStructureCompileArtifact,
37
+ validateTodoStructureSet,
38
+ } from './todo-structure-contracts.mjs';
39
+ import {
40
+ bindTodoStructureRealizationCommits,
41
+ collectTodoStructureGitProvenance,
42
+ } from './todo-structure-git-adapter.mjs';
31
43
  import { sha256Bytes, verifyLinearHashChain } from './hash-chain.mjs';
32
44
  import { gitCatFileBatch, gitSync } from './git-process.mjs';
33
45
  import {
@@ -1392,17 +1404,11 @@ export async function readTodoStoreStable(options = {}) {
1392
1404
  if (!Number.isSafeInteger(maximumAttempts) || maximumAttempts < 1 || maximumAttempts > 16) {
1393
1405
  throw new TypeError('maximumAttempts must be 1..16');
1394
1406
  }
1395
- // `manifest_journal_head_mismatch`/`manifest_plan_binding_mismatch` are treated as a
1396
- // transient in-flight write and retried. That is correct while a concurrent writer is
1397
- // mid-commit, but a crashed writer can leave the SAME mismatch permanently — retrying
1398
- // forever against a manifest that never changes just burns attempts and then reports
1399
- // a content-free STORE_BUSY, hiding the real STORE_INCONSISTENT reason the caller needs
1400
- // to actually recover (2026-08-10 P0: a crashed `todo start` left exactly this behind).
1401
- // Track the manifest digest seen at the START of the previous attempt: if it is
1402
- // unchanged going into this attempt too, no writer completed anything in between, so
1403
- // the "transient" classification no longer has evidence behind it — surface the real
1404
- // error instead of exhausting the budget on a window that was never closing.
1405
- let previousAttemptManifestDigest = null;
1407
+ // `manifest_journal_head_mismatch`/`manifest_plan_binding_mismatch`は、manifestを最後に
1408
+ // publishする並行writerの一時窓でも、crash後に残った恒久的な千切れでも同じ形に見える。
1409
+ // 同じdigestを2回観測しただけでは両者を判別できないため、この2 reasonだけはattempt
1410
+ // 上限まで待つ。上限内に閉じれば正常storeを返し、閉じなければ最後のtyped errorを返す。
1411
+ // これにより一時窓を早計に破損扱いせず、恒久破損もSTORE_BUSYへ丸めない。
1406
1412
  let lastError = null;
1407
1413
  for (let attempt = 1; attempt <= maximumAttempts; attempt += 1) {
1408
1414
  const before = await readArtifact(repoRoot, MANIFEST_REF, {
@@ -1423,12 +1429,10 @@ export async function readTodoStoreStable(options = {}) {
1423
1429
  const transientWriteWindow = error.code === 'STORE_INCONSISTENT'
1424
1430
  && ['manifest_journal_head_mismatch', 'manifest_plan_binding_mismatch']
1425
1431
  .includes(error.detail.reason);
1426
- const stableAcrossAttempts = previousAttemptManifestDigest === before.manifest_digest;
1427
1432
  if (before.manifest_digest === after.manifest_digest
1428
- && (!transientWriteWindow || stableAcrossAttempts)) throw error;
1433
+ && !transientWriteWindow) throw error;
1429
1434
  lastError = error;
1430
1435
  }
1431
- previousAttemptManifestDigest = before.manifest_digest;
1432
1436
  if (attempt < maximumAttempts) {
1433
1437
  await new Promise((resolve) => setTimeout(resolve, Math.min(16, 2 ** attempt)));
1434
1438
  }
@@ -1617,6 +1621,98 @@ function resolveCanonicalTaskId(plan, requestedTaskId) {
1617
1621
  return matches[0].task_id;
1618
1622
  }
1619
1623
 
1624
+ async function enforceTodoStructureLifecycleGate(repoRoot, member, eventInput) {
1625
+ const binding = await readTodoStructureBinding({
1626
+ repoRoot, planKey: member.plan.plan_key, planVersion: member.plan.plan_version,
1627
+ });
1628
+ if (binding === null) return;
1629
+ const source = await readTodoStructureSource({ repoRoot, planKey: member.plan.plan_key });
1630
+ if (source === null || source.structure_set_digest !== binding.structure_set_digest) {
1631
+ fail('STRUCTURE_LIFECYCLE_GATE_FAILED', 'enabled_structure_source_unreadable');
1632
+ }
1633
+ if (eventInput.kind === 'done') {
1634
+ const task = source.tasks.find(({ task_id: id }) => id === eventInput.task_id);
1635
+ if (task?.applicability !== 'graph') return;
1636
+ const chain = await readTodoStructureRealizationChain({
1637
+ repoRoot, structureSet: source, taskId: eventInput.task_id,
1638
+ });
1639
+ const latest = chain.at(-1);
1640
+ if (latest === undefined) {
1641
+ fail('STRUCTURE_REALIZATION_REQUIRED', 'fresh_realization_missing', {
1642
+ plan_key: member.plan.plan_key, task_id: eventInput.task_id,
1643
+ next_action: `lattice todo structure realize --plan ${member.plan.plan_key} --task ${eventInput.task_id} --input <realization.json>`,
1644
+ });
1645
+ }
1646
+ let currentHead;
1647
+ try {
1648
+ currentHead = gitSync(['rev-parse', '--verify', 'HEAD^{commit}'], {
1649
+ cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
1650
+ }).trim();
1651
+ } catch {
1652
+ fail('STRUCTURE_GIT_HEAD_UNAVAILABLE', 'current_head_unavailable');
1653
+ }
1654
+ if (latest.head_sha !== currentHead) {
1655
+ fail('STRUCTURE_REALIZATION_REQUIRED', 'realization_head_stale', {
1656
+ plan_key: member.plan.plan_key, task_id: eventInput.task_id,
1657
+ realization_head_sha: latest.head_sha, current_head_sha: currentHead,
1658
+ next_action: `lattice todo structure realize --plan ${member.plan.plan_key} --task ${eventInput.task_id} --input <realization.json>`,
1659
+ });
1660
+ }
1661
+ }
1662
+ if (!['phase_accept', 'phase_close_unaudited'].includes(eventInput.kind)
1663
+ || member.tasks.some(({ status }) => status !== 'done')) return;
1664
+ const finalization = await readTodoStructureFinalization({
1665
+ repoRoot, planKey: member.plan.plan_key, planVersion: member.plan.plan_version,
1666
+ });
1667
+ if (finalization === null) {
1668
+ fail('STRUCTURE_FINALIZATION_REQUIRED', 'fresh_consistent_finalization_missing', {
1669
+ plan_key: member.plan.plan_key,
1670
+ next_action: `lattice todo structure finalize --plan ${member.plan.plan_key} --json`,
1671
+ });
1672
+ }
1673
+ let currentHead;
1674
+ try {
1675
+ currentHead = gitSync(['rev-parse', '--verify', 'HEAD^{commit}'], {
1676
+ cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
1677
+ }).trim();
1678
+ } catch {
1679
+ fail('STRUCTURE_GIT_HEAD_UNAVAILABLE', 'current_head_unavailable');
1680
+ }
1681
+ const realizationHeads = [];
1682
+ for (const task of source.tasks.filter(({ applicability }) => applicability === 'graph')) {
1683
+ const chain = await readTodoStructureRealizationChain({
1684
+ repoRoot, structureSet: source, taskId: task.task_id,
1685
+ });
1686
+ const head = chain.at(-1);
1687
+ if (head !== undefined) realizationHeads.push({
1688
+ task_id: task.task_id, sequence: head.sequence,
1689
+ realization_digest: head.realization_digest,
1690
+ });
1691
+ }
1692
+ const staleReasons = [];
1693
+ if (finalization.project_id !== member.plan.project_id) staleReasons.push('project_id');
1694
+ if (finalization.plan_key !== member.plan.plan_key) staleReasons.push('plan_key');
1695
+ if (finalization.plan_version !== member.plan.plan_version) staleReasons.push('plan_version');
1696
+ if (finalization.topology_digest !== member.plan.topology_digest) {
1697
+ staleReasons.push('topology_digest');
1698
+ }
1699
+ if (finalization.structure_set_digest !== source.structure_set_digest) {
1700
+ staleReasons.push('structure_set_digest');
1701
+ }
1702
+ if (finalization.current_head_sha !== currentHead) staleReasons.push('current_head_sha');
1703
+ if (finalization.realization_head_digest
1704
+ !== digestTodoStructureRealizationHeads(realizationHeads)) {
1705
+ staleReasons.push('realization_head_digest');
1706
+ }
1707
+ if (finalization.overlay.verdict !== 'consistent') staleReasons.push('verdict');
1708
+ if (staleReasons.length > 0) {
1709
+ fail('STRUCTURE_FINALIZATION_REQUIRED', 'finalization_stale', {
1710
+ plan_key: member.plan.plan_key, stale_reasons: staleReasons.sort(),
1711
+ next_action: `lattice todo structure finalize --plan ${member.plan.plan_key} --json`,
1712
+ });
1713
+ }
1714
+ }
1715
+
1620
1716
  export async function appendTodoEvent(options = {}) {
1621
1717
  requireWriter(options.writer, 'g5-authoring');
1622
1718
  const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
@@ -1641,6 +1737,7 @@ export async function appendTodoEvent(options = {}) {
1641
1737
  task_id: resolveCanonicalTaskId(member.plan, options.event.task_id),
1642
1738
  recorded_at: options.event.recorded_at ?? new Date().toISOString(),
1643
1739
  }, member);
1740
+ await enforceTodoStructureLifecycleGate(repoRoot, member, input);
1644
1741
  const event = nextEvent(input, member);
1645
1742
  if ((event.kind === 'start' && event.payload.start_mode === 'historical_import')
1646
1743
  || (event.kind === 'done' && event.payload.done_mode === 'historical_import')) {
@@ -2286,8 +2383,19 @@ function localTaskRef(ref, plan, taskId) {
2286
2383
  && ref.task_id === taskId;
2287
2384
  }
2288
2385
 
2386
+ function removedTaskIdsFor(revision) {
2387
+ return new Set(revision.task_migration
2388
+ .filter(({ to_task_id, state_policy }) => to_task_id === 'removed' && state_policy === 'removed')
2389
+ .map(({ from_task_id }) => from_task_id));
2390
+ }
2391
+
2392
+ function removedTaskRef(ref, plan, removedTaskIds) {
2393
+ return ref.project_id === plan.project_id && ref.plan_key === plan.plan_key
2394
+ && removedTaskIds.has(ref.task_id);
2395
+ }
2396
+
2289
2397
  function taskSemantics(plan, taskId, idMap, {
2290
- reconciliationMetadata = false, includeDesignMemo = false,
2398
+ reconciliationMetadata = false, includeDesignMemo = false, removedTaskIds = new Set(),
2291
2399
  } = {}) {
2292
2400
  const task = plan.tasks.find(({ task_id }) => task_id === taskId);
2293
2401
  if (!task) return null;
@@ -2302,17 +2410,23 @@ function taskSemantics(plan, taskId, idMap, {
2302
2410
  compile_binding: task.compile_binding, parent_task_id: mapId(task.parent_task_id ?? null),
2303
2411
  };
2304
2412
  const edges = plan.hard_dependencies
2305
- .filter(({ from, to }) => localTaskRef(from, plan, taskId) || localTaskRef(to, plan, taskId))
2413
+ .filter(({ from, to }) => !removedTaskRef(from, plan, removedTaskIds)
2414
+ && !removedTaskRef(to, plan, removedTaskIds)
2415
+ && (localTaskRef(from, plan, taskId) || localTaskRef(to, plan, taskId)))
2306
2416
  .map(({ from, to }) => ({ from: mappedNodeRef(from, plan, idMap), to: mappedNodeRef(to, plan, idMap) }))
2307
2417
  .sort((left, right) => canonicalizeTodoArtifact(left) < canonicalizeTodoArtifact(right) ? -1 : 1);
2308
2418
  const joins = plan.joins
2309
- .filter(({ after, before }) => localTaskRef(before, plan, taskId)
2310
- || after.some((ref) => localTaskRef(ref, plan, taskId)))
2311
- .map((join) => ({ ...join,
2312
- after: join.after.map((ref) => mappedNodeRef(ref, plan, idMap))
2419
+ .flatMap((join) => {
2420
+ if (removedTaskRef(join.before, plan, removedTaskIds)) return [];
2421
+ const after = join.after.filter((ref) => !removedTaskRef(ref, plan, removedTaskIds));
2422
+ if (after.length === 0 || (!localTaskRef(join.before, plan, taskId)
2423
+ && !after.some((ref) => localTaskRef(ref, plan, taskId)))) return [];
2424
+ return [{ ...join,
2425
+ after: after.map((ref) => mappedNodeRef(ref, plan, idMap))
2313
2426
  .sort((left, right) => canonicalizeTodoArtifact(left) < canonicalizeTodoArtifact(right) ? -1 : 1),
2314
- before: mappedNodeRef(join.before, plan, idMap),
2315
- })).sort((left, right) => left.id < right.id ? -1 : 1);
2427
+ before: mappedNodeRef(join.before, plan, idMap),
2428
+ }];
2429
+ }).sort((left, right) => left.id < right.id ? -1 : 1);
2316
2430
  const phaseAcceptDependencies = isDecoupledPhaseTodoPlanSchema(plan.schema)
2317
2431
  ? plan.phase_accept_dependencies
2318
2432
  .filter(({ to }) => localTaskRef(to, plan, taskId))
@@ -2323,7 +2437,7 @@ function taskSemantics(plan, taskId, idMap, {
2323
2437
  }
2324
2438
 
2325
2439
  function phaseV3CarrySemantics(plan, taskId, taskIdMap, phaseIdMap,
2326
- { reconciliationMetadata = false, includeDesignMemo = false } = {}) {
2440
+ { reconciliationMetadata = false, includeDesignMemo = false, removedTaskIds = new Set() } = {}) {
2327
2441
  const task = plan.tasks.find(({ task_id: id }) => id === taskId);
2328
2442
  if (task === undefined) return null;
2329
2443
  const mapTaskRef = (ref) => ref.project_id === plan.project_id && ref.plan_key === plan.plan_key
@@ -2351,13 +2465,17 @@ function phaseV3CarrySemantics(plan, taskId, taskIdMap, phaseIdMap,
2351
2465
  const incoming = [];
2352
2466
  const outgoing = [];
2353
2467
  for (const edge of plan.hard_dependencies) {
2468
+ if (removedTaskRef(edge.from, plan, removedTaskIds)
2469
+ || removedTaskRef(edge.to, plan, removedTaskIds)) continue;
2354
2470
  const mapped = { kind: 'hard', from: mapTaskRef(edge.from), to: mapTaskRef(edge.to) };
2355
2471
  if (localTaskRef(edge.to, plan, taskId)) incoming.push(mapped);
2356
2472
  if (localTaskRef(edge.from, plan, taskId)) outgoing.push(mapped);
2357
2473
  }
2358
2474
  for (const join of plan.joins) {
2475
+ if (removedTaskRef(join.before, plan, removedTaskIds)) continue;
2359
2476
  const to = mapTaskRef(join.before);
2360
2477
  for (const after of join.after) {
2478
+ if (removedTaskRef(after, plan, removedTaskIds)) continue;
2361
2479
  const tuple = { kind: 'join', join_id: join.id, from: mapTaskRef(after), to };
2362
2480
  if (localTaskRef(join.before, plan, taskId)) incoming.push(tuple);
2363
2481
  if (localTaskRef(after, plan, taskId)) outgoing.push(tuple);
@@ -2375,11 +2493,12 @@ function validatePhaseV3Carry(previous, revision, migration, idMap, state) {
2375
2493
  const reconciliationMetadata = migration.state_policy === 'carry_reconciled_metadata';
2376
2494
  const predecessorTask = previous.plan.tasks.find(({ task_id }) => task_id === migration.from_task_id);
2377
2495
  const includeDesignMemo = !reconciliationMetadata && typeof predecessorTask?.design_memo === 'string';
2496
+ const removedTaskIds = removedTaskIdsFor(revision);
2378
2497
  const phaseIdMap = new Map(revision.phase_migration
2379
2498
  .filter(({ from_phase_id, to_phase_id }) => from_phase_id !== null && to_phase_id !== 'removed')
2380
2499
  .map(({ from_phase_id, to_phase_id }) => [from_phase_id, to_phase_id]));
2381
2500
  const before = phaseV3CarrySemantics(previous.plan, migration.from_task_id, idMap, phaseIdMap,
2382
- { reconciliationMetadata, includeDesignMemo });
2501
+ { reconciliationMetadata, includeDesignMemo, removedTaskIds });
2383
2502
  const after = phaseV3CarrySemantics(revision.desired_plan, migration.to_task_id,
2384
2503
  new Map(), new Map(),
2385
2504
  { reconciliationMetadata, includeDesignMemo });
@@ -2412,8 +2531,9 @@ function validateAcquirePhaseCarry(previous, revision, migration, idMap) {
2412
2531
  .map(({ from_phase_id, to_phase_id }) => [from_phase_id, to_phase_id]));
2413
2532
  const predecessorTask = previous.plan.tasks.find(({ task_id }) => task_id === migration.from_task_id);
2414
2533
  const includeDesignMemo = typeof predecessorTask?.design_memo === 'string';
2534
+ const removedTaskIds = removedTaskIdsFor(revision);
2415
2535
  const before = phaseV3CarrySemantics(previous.plan, migration.from_task_id, idMap, phaseIdMap,
2416
- { includeDesignMemo });
2536
+ { includeDesignMemo, removedTaskIds });
2417
2537
  const after = phaseV3CarrySemantics(revision.desired_plan, migration.to_task_id, new Map(), new Map(),
2418
2538
  { includeDesignMemo });
2419
2539
  if (before.task.phase_id !== null) {
@@ -2448,6 +2568,7 @@ function stateMigrationFor(previous, revision) {
2448
2568
  const idMap = new Map(revision.task_migration
2449
2569
  .filter(({ to_task_id }) => to_task_id !== 'removed')
2450
2570
  .map(({ from_task_id, to_task_id }) => [from_task_id, to_task_id]));
2571
+ const removedTaskIds = removedTaskIdsFor(revision);
2451
2572
  const states = new Map(previous.tasks.map((state) => [state.task_id, state]));
2452
2573
  return revision.task_migration.map((migration) => {
2453
2574
  const carriesState = ['carry', 'carry_reconciled_metadata', 'acquire_phase'].includes(migration.state_policy);
@@ -2467,7 +2588,7 @@ function stateMigrationFor(previous, revision) {
2467
2588
  .find(({ task_id }) => task_id === migration.from_task_id);
2468
2589
  const includeDesignMemo = typeof predecessorTask?.design_memo === 'string';
2469
2590
  const before = taskSemantics(previous.plan, migration.from_task_id, idMap,
2470
- { includeDesignMemo });
2591
+ { includeDesignMemo, removedTaskIds });
2471
2592
  const after = taskSemantics(revision.desired_plan, migration.to_task_id, new Map(),
2472
2593
  { includeDesignMemo });
2473
2594
  if (canonicalizeTodoArtifact(before) !== canonicalizeTodoArtifact(after)) {
@@ -2482,7 +2603,7 @@ function stateMigrationFor(previous, revision) {
2482
2603
  const includeDesignMemo = !reconciliationMetadata
2483
2604
  && typeof predecessorTask?.design_memo === 'string';
2484
2605
  const before = taskSemantics(previous.plan, migration.from_task_id, idMap,
2485
- { reconciliationMetadata, includeDesignMemo });
2606
+ { reconciliationMetadata, includeDesignMemo, removedTaskIds });
2486
2607
  const after = taskSemantics(revision.desired_plan, migration.to_task_id, new Map(),
2487
2608
  { reconciliationMetadata, includeDesignMemo });
2488
2609
  if (canonicalizeTodoArtifact(before) !== canonicalizeTodoArtifact(after)) {
@@ -4379,3 +4500,500 @@ export async function writeTodoWitnessSet(options = {}) {
4379
4500
  await atomicWrite(path.resolve(repoRoot, ref), canonicalLine(witnessSet));
4380
4501
  return { ref, witnessSet };
4381
4502
  }
4503
+
4504
+ const STRUCTURE_SOURCE_BYTES = 8_388_608;
4505
+ const STRUCTURE_COMPILE_ARTIFACT_BYTES = 67_108_864;
4506
+ const STRUCTURE_REALIZATION_CHAIN_BYTES = 8_388_608;
4507
+
4508
+ /** AI-authored planned sourceの正規ref。derived artifact/bindingとは所有を分ける。 */
4509
+ export function todoStructureSourceRef(planKey) {
4510
+ if (!isTodoIdentifier(planKey)) throw new TypeError('planKey must be a todo identifier');
4511
+ return `${STORE_ROOT_REF}/structure/${planKey}.json`;
4512
+ }
4513
+
4514
+ /** plan versionごとのimmutable activation binding ref。sourceだけでは有効化しない。 */
4515
+ export function todoStructureBindingRef(planKey, planVersion) {
4516
+ if (!isTodoIdentifier(planKey) || !isTodoIdentifier(planVersion)) {
4517
+ throw new TypeError('planKey and planVersion must be todo identifiers');
4518
+ }
4519
+ return `${STORE_ROOT_REF}/plans/${planKey}/${planVersion}/structure/binding.json`;
4520
+ }
4521
+
4522
+ /** plan versionへ並置するderived compile artifactの正規ref。 */
4523
+ export function todoStructureCompileArtifactRef(planKey, planVersion) {
4524
+ if (!isTodoIdentifier(planKey) || !isTodoIdentifier(planVersion)) {
4525
+ throw new TypeError('planKey and planVersion must be todo identifiers');
4526
+ }
4527
+ return `${STORE_ROOT_REF}/plans/${planKey}/${planVersion}/structure/compile.json`;
4528
+ }
4529
+
4530
+ /** plan終端で再compileした最終構造artifactの正規ref。 */
4531
+ export function todoStructureFinalizationRef(planKey, planVersion) {
4532
+ if (!isTodoIdentifier(planKey) || !isTodoIdentifier(planVersion)) {
4533
+ throw new TypeError('planKey and planVersion must be todo identifiers');
4534
+ }
4535
+ return `${STORE_ROOT_REF}/plans/${planKey}/${planVersion}/structure/finalization.json`;
4536
+ }
4537
+
4538
+ /** task単位append-only realization chainの正規ref。 */
4539
+ export function todoStructureRealizationRef(planKey, planVersion, taskId) {
4540
+ if (![planKey, planVersion, taskId].every(isTodoIdentifier)) {
4541
+ throw new TypeError('planKey, planVersion and taskId must be todo identifiers');
4542
+ }
4543
+ return `${STORE_ROOT_REF}/plans/${planKey}/${planVersion}/structure/realizations/${taskId}.jsonl`;
4544
+ }
4545
+
4546
+ /** sourceが無ければnull、壊れていればtyped failure。missingをinvalidへ丸めない。 */
4547
+ export async function readTodoStructureSource(options = {}) {
4548
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
4549
+ return readArtifact(repoRoot, todoStructureSourceRef(options.planKey), {
4550
+ code: 'INVALID_TODO_STRUCTURE_SET', maxBytes: STRUCTURE_SOURCE_BYTES,
4551
+ validate: validateTodoStructureSet, missing: true,
4552
+ });
4553
+ }
4554
+
4555
+ /**
4556
+ * dry-run済みplanned sourceをcanonical JSON+LFで保存する唯一のwriter。
4557
+ * bindingは発行しない——authoritative compileが成功するまではdraftであり、完了gateを有効化しない。
4558
+ */
4559
+ export async function writeTodoStructureSource(options = {}) {
4560
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
4561
+ const { structureSet } = options;
4562
+ const explained = explainTodoStructureSet(structureSet);
4563
+ if (!explained.valid) {
4564
+ fail('INVALID_TODO_STRUCTURE_SET', explained.reason, { path: explained.path });
4565
+ }
4566
+ return withLock(repoRoot, async () => {
4567
+ const store = await readTodoStore({ repoRoot, forWrite: true, now: options.now });
4568
+ if (store.project_id !== structureSet.project_id) {
4569
+ fail('STRUCTURE_BINDING_MISMATCH', 'project_id_mismatch', {
4570
+ expected: store.project_id, actual: structureSet.project_id,
4571
+ });
4572
+ }
4573
+ const member = activeMember(store, structureSet.plan_key);
4574
+ if (member.plan.plan_version !== structureSet.plan_version) {
4575
+ fail('STRUCTURE_BINDING_MISMATCH', 'plan_version_mismatch', {
4576
+ expected: member.plan.plan_version, actual: structureSet.plan_version,
4577
+ });
4578
+ }
4579
+ if (member.plan.topology_digest !== structureSet.topology_digest) {
4580
+ fail('STRUCTURE_BINDING_MISMATCH', 'topology_digest_mismatch', {
4581
+ expected: member.plan.topology_digest, actual: structureSet.topology_digest,
4582
+ });
4583
+ }
4584
+ const expectedTaskIds = member.tasks.filter(({ status }) => status !== 'done')
4585
+ .map(({ task_id: taskId }) => taskId);
4586
+ const coverage = explainTodoStructureSet(structureSet, { expectedTaskIds });
4587
+ if (!coverage.valid) {
4588
+ fail('STRUCTURE_BINDING_MISMATCH', coverage.reason, {
4589
+ path: coverage.path, ...(coverage.detail ?? {}),
4590
+ });
4591
+ }
4592
+ const bindingRef = todoStructureBindingRef(structureSet.plan_key, structureSet.plan_version);
4593
+ if (await exactFileOrNull(path.resolve(repoRoot, bindingRef)) !== null) {
4594
+ fail('STRUCTURE_ALREADY_ENABLED', 'immutable_binding_exists', {
4595
+ binding_ref: bindingRef,
4596
+ next_action: 'revise_the_plan_or_run_authoritative_compile_with_the_existing_source',
4597
+ });
4598
+ }
4599
+ const ref = todoStructureSourceRef(structureSet.plan_key);
4600
+ try {
4601
+ await ensureSafeStoreDirectory(repoRoot, path.dirname(path.resolve(repoRoot, ref)));
4602
+ } catch (error) {
4603
+ if (error instanceof TodoStoreError && error.code === 'REVISION_CONFLICT') {
4604
+ fail('INVALID_TODO_STRUCTURE_SET', 'structure_source_directory_unsafe', { source_ref: ref });
4605
+ }
4606
+ throw error;
4607
+ }
4608
+ await atomicWrite(path.resolve(repoRoot, ref), canonicalLine(structureSet));
4609
+ return { ref, structureSet };
4610
+ });
4611
+ }
4612
+
4613
+ /** bindingが無ければnull、壊れていればtyped failure。 */
4614
+ export async function readTodoStructureBinding(options = {}) {
4615
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
4616
+ return readArtifact(repoRoot, todoStructureBindingRef(options.planKey, options.planVersion), {
4617
+ code: 'INVALID_TODO_STRUCTURE_BINDING', maxBytes: TODO_LIMITS.snapshotBytes,
4618
+ validate: validateTodoStructureBinding, missing: true,
4619
+ });
4620
+ }
4621
+
4622
+ /** derived artifactが無ければnull、壊れていればtyped failure。 */
4623
+ export async function readTodoStructureCompileArtifact(options = {}) {
4624
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
4625
+ return readArtifact(repoRoot,
4626
+ todoStructureCompileArtifactRef(options.planKey, options.planVersion), {
4627
+ code: 'INVALID_TODO_STRUCTURE_COMPILE_ARTIFACT',
4628
+ maxBytes: STRUCTURE_COMPILE_ARTIFACT_BYTES,
4629
+ validate: validateTodoStructureCompileArtifact,
4630
+ missing: true,
4631
+ });
4632
+ }
4633
+
4634
+ /** finalizationが無ければnull、壊れていればtyped failure。 */
4635
+ export async function readTodoStructureFinalization(options = {}) {
4636
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
4637
+ return readArtifact(repoRoot, todoStructureFinalizationRef(options.planKey, options.planVersion), {
4638
+ code: 'INVALID_TODO_STRUCTURE_FINALIZATION',
4639
+ maxBytes: STRUCTURE_COMPILE_ARTIFACT_BYTES,
4640
+ validate: validateTodoStructureCompileArtifact,
4641
+ missing: true,
4642
+ });
4643
+ }
4644
+
4645
+ /**
4646
+ * active planとplanned sourceへ束縛したderived artifactを書く。
4647
+ * activation前は再生成可能artifactとして置換できるが、binding発行後は固定する。
4648
+ */
4649
+ export async function writeTodoStructureCompileArtifact(options = {}) {
4650
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
4651
+ const { artifact } = options;
4652
+ if (!validateTodoStructureCompileArtifact(artifact)) {
4653
+ fail('INVALID_TODO_STRUCTURE_COMPILE_ARTIFACT', 'compile_artifact_invalid');
4654
+ }
4655
+ return withLock(repoRoot, async () => {
4656
+ const store = await readTodoStore({ repoRoot, forWrite: true, now: options.now });
4657
+ if (store.project_id !== artifact.project_id) {
4658
+ fail('STRUCTURE_BINDING_MISMATCH', 'project_id_mismatch');
4659
+ }
4660
+ const member = activeMember(store, artifact.plan_key);
4661
+ if (member.plan.plan_version !== artifact.plan_version) {
4662
+ fail('STRUCTURE_BINDING_MISMATCH', 'plan_version_mismatch');
4663
+ }
4664
+ if (member.plan.topology_digest !== artifact.topology_digest) {
4665
+ fail('STRUCTURE_BINDING_MISMATCH', 'topology_digest_mismatch');
4666
+ }
4667
+ const source = await readTodoStructureSource({ repoRoot, planKey: artifact.plan_key });
4668
+ if (source === null) fail('STRUCTURE_SOURCE_MISSING', 'planned_source_missing');
4669
+ if (source.structure_set_digest !== artifact.structure_set_digest
4670
+ || source.baseline_sha !== artifact.baseline_sha
4671
+ || source.plan_version !== artifact.plan_version
4672
+ || source.topology_digest !== artifact.topology_digest) {
4673
+ fail('STRUCTURE_BINDING_MISMATCH', 'planned_source_mismatch');
4674
+ }
4675
+ let currentHead;
4676
+ try {
4677
+ currentHead = gitSync(['rev-parse', '--verify', 'HEAD^{commit}'], {
4678
+ cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
4679
+ }).trim();
4680
+ } catch {
4681
+ fail('STRUCTURE_GIT_HEAD_UNAVAILABLE', 'current_head_unavailable');
4682
+ }
4683
+ if (currentHead !== artifact.current_head_sha) {
4684
+ fail('STRUCTURE_COMPILE_STALE', 'current_head_changed_before_store');
4685
+ }
4686
+ const realizationHeads = [];
4687
+ for (const task of source.tasks.filter(({ applicability }) => applicability === 'graph')) {
4688
+ const chain = await readTodoStructureRealizationChain({
4689
+ repoRoot, structureSet: source, taskId: task.task_id,
4690
+ });
4691
+ const head = chain.at(-1);
4692
+ if (head !== undefined) realizationHeads.push({
4693
+ task_id: task.task_id, sequence: head.sequence,
4694
+ realization_digest: head.realization_digest,
4695
+ });
4696
+ }
4697
+ if (artifact.realization_head_digest
4698
+ !== digestTodoStructureRealizationHeads(realizationHeads)) {
4699
+ fail('STRUCTURE_COMPILE_STALE', 'realization_changed_before_store');
4700
+ }
4701
+ const ref = todoStructureCompileArtifactRef(artifact.plan_key, artifact.plan_version);
4702
+ const bindingRef = todoStructureBindingRef(artifact.plan_key, artifact.plan_version);
4703
+ if (await exactFileOrNull(path.resolve(repoRoot, bindingRef)) !== null) {
4704
+ fail('STRUCTURE_ALREADY_ENABLED', 'immutable_binding_exists', {
4705
+ binding_ref: bindingRef,
4706
+ });
4707
+ }
4708
+ await ensureSafeStoreDirectory(repoRoot, path.dirname(path.resolve(repoRoot, ref)));
4709
+ await atomicWrite(path.resolve(repoRoot, ref), canonicalLine(artifact));
4710
+ return { ref, artifact };
4711
+ });
4712
+ }
4713
+
4714
+ /** compile artifactをexactに指すimmutable activation bindingを書く。 */
4715
+ export async function writeTodoStructureBinding(options = {}) {
4716
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
4717
+ const { binding } = options;
4718
+ if (!validateTodoStructureBinding(binding)) {
4719
+ fail('INVALID_TODO_STRUCTURE_BINDING', 'structure_binding_invalid');
4720
+ }
4721
+ return withLock(repoRoot, async () => {
4722
+ const store = await readTodoStore({ repoRoot, forWrite: true, now: options.now });
4723
+ if (store.project_id !== binding.project_id) {
4724
+ fail('STRUCTURE_BINDING_MISMATCH', 'project_id_mismatch');
4725
+ }
4726
+ const member = activeMember(store, binding.plan_key);
4727
+ if (member.plan.plan_version !== binding.plan_version
4728
+ || member.plan.topology_digest !== binding.topology_digest) {
4729
+ fail('STRUCTURE_BINDING_MISMATCH', 'active_plan_mismatch');
4730
+ }
4731
+ const source = await readTodoStructureSource({ repoRoot, planKey: binding.plan_key });
4732
+ const artifact = await readTodoStructureCompileArtifact({
4733
+ repoRoot, planKey: binding.plan_key, planVersion: binding.plan_version,
4734
+ });
4735
+ if (source === null || artifact === null
4736
+ || source.structure_set_digest !== binding.structure_set_digest
4737
+ || source.baseline_sha !== binding.baseline_sha
4738
+ || artifact.artifact_digest !== binding.compile_artifact_digest
4739
+ || artifact.current_head_sha !== binding.compiled_head_sha) {
4740
+ fail('STRUCTURE_BINDING_MISMATCH', 'source_or_compile_artifact_mismatch');
4741
+ }
4742
+ let currentHead;
4743
+ try {
4744
+ currentHead = gitSync(['rev-parse', '--verify', 'HEAD^{commit}'], {
4745
+ cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
4746
+ }).trim();
4747
+ } catch {
4748
+ fail('STRUCTURE_GIT_HEAD_UNAVAILABLE', 'current_head_unavailable');
4749
+ }
4750
+ if (currentHead !== artifact.current_head_sha) {
4751
+ fail('STRUCTURE_COMPILE_STALE', 'current_head_changed_before_activation');
4752
+ }
4753
+ const realizationHeads = [];
4754
+ for (const task of source.tasks.filter(({ applicability }) => applicability === 'graph')) {
4755
+ const chain = await readTodoStructureRealizationChain({
4756
+ repoRoot, structureSet: source, taskId: task.task_id,
4757
+ });
4758
+ const head = chain.at(-1);
4759
+ if (head !== undefined) realizationHeads.push({
4760
+ task_id: task.task_id, sequence: head.sequence,
4761
+ realization_digest: head.realization_digest,
4762
+ });
4763
+ }
4764
+ if (artifact.realization_head_digest
4765
+ !== digestTodoStructureRealizationHeads(realizationHeads)) {
4766
+ fail('STRUCTURE_COMPILE_STALE', 'realization_changed_before_activation');
4767
+ }
4768
+ const ref = todoStructureBindingRef(binding.plan_key, binding.plan_version);
4769
+ if (await exactFileOrNull(path.resolve(repoRoot, ref)) !== null) {
4770
+ fail('STRUCTURE_ALREADY_ENABLED', 'immutable_binding_exists', { binding_ref: ref });
4771
+ }
4772
+ await ensureSafeStoreDirectory(repoRoot, path.dirname(path.resolve(repoRoot, ref)));
4773
+ await atomicWrite(path.resolve(repoRoot, ref), canonicalLine(binding));
4774
+ return { ref, binding };
4775
+ });
4776
+ }
4777
+
4778
+ /** 全task done後のfresh consistent artifactだけをfinalization refへ固定する。 */
4779
+ export async function writeTodoStructureFinalization(options = {}) {
4780
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
4781
+ const { artifact } = options;
4782
+ if (!validateTodoStructureCompileArtifact(artifact) || artifact.overlay.verdict !== 'consistent') {
4783
+ fail('INVALID_TODO_STRUCTURE_FINALIZATION', 'finalization_not_consistent');
4784
+ }
4785
+ return withLock(repoRoot, async () => {
4786
+ const store = await readTodoStore({ repoRoot, forWrite: true, now: options.now });
4787
+ const member = activeMember(store, artifact.plan_key);
4788
+ if (store.project_id !== artifact.project_id || member.plan.project_id !== artifact.project_id) {
4789
+ fail('STRUCTURE_BINDING_MISMATCH', 'project_id_mismatch');
4790
+ }
4791
+ if (member.plan.plan_version !== artifact.plan_version
4792
+ || member.plan.topology_digest !== artifact.topology_digest
4793
+ || member.tasks.some(({ status }) => status !== 'done')) {
4794
+ fail('STRUCTURE_FINALIZATION_UNAVAILABLE', 'plan_not_fully_done');
4795
+ }
4796
+ const source = await readTodoStructureSource({ repoRoot, planKey: artifact.plan_key });
4797
+ const binding = await readTodoStructureBinding({
4798
+ repoRoot, planKey: artifact.plan_key, planVersion: artifact.plan_version,
4799
+ });
4800
+ if (source === null || binding === null
4801
+ || source.structure_set_digest !== artifact.structure_set_digest
4802
+ || binding.structure_set_digest !== artifact.structure_set_digest) {
4803
+ fail('STRUCTURE_FINALIZATION_UNAVAILABLE', 'structure_not_enabled_or_stale');
4804
+ }
4805
+ const heads = [];
4806
+ for (const task of source.tasks.filter(({ applicability }) => applicability === 'graph')) {
4807
+ const chain = await readTodoStructureRealizationChain({
4808
+ repoRoot, structureSet: source, taskId: task.task_id,
4809
+ });
4810
+ const head = chain.at(-1);
4811
+ if (head === undefined) {
4812
+ fail('STRUCTURE_FINALIZATION_UNAVAILABLE', 'realization_missing', { task_id: task.task_id });
4813
+ }
4814
+ heads.push({
4815
+ task_id: task.task_id, sequence: head.sequence,
4816
+ realization_digest: head.realization_digest,
4817
+ });
4818
+ }
4819
+ if (artifact.realization_head_digest !== digestTodoStructureRealizationHeads(heads)) {
4820
+ fail('STRUCTURE_FINALIZATION_STALE', 'realization_head_changed');
4821
+ }
4822
+ let currentHead;
4823
+ try {
4824
+ currentHead = gitSync(['rev-parse', '--verify', 'HEAD^{commit}'], {
4825
+ cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
4826
+ }).trim();
4827
+ } catch {
4828
+ fail('STRUCTURE_GIT_HEAD_UNAVAILABLE', 'current_head_unavailable');
4829
+ }
4830
+ if (artifact.current_head_sha !== currentHead) {
4831
+ fail('STRUCTURE_FINALIZATION_STALE', 'current_head_changed');
4832
+ }
4833
+ const ref = todoStructureFinalizationRef(artifact.plan_key, artifact.plan_version);
4834
+ await ensureSafeStoreDirectory(repoRoot, path.dirname(path.resolve(repoRoot, ref)));
4835
+ await atomicWrite(path.resolve(repoRoot, ref), canonicalLine(artifact));
4836
+ return { ref, artifact };
4837
+ });
4838
+ }
4839
+
4840
+ function parseTodoStructureRealizationChain(bytes, { structureSet, taskId }) {
4841
+ if (bytes.length === 0 || bytes.length > STRUCTURE_REALIZATION_CHAIN_BYTES) {
4842
+ fail('INVALID_TODO_STRUCTURE_REALIZATION_CHAIN',
4843
+ bytes.length > STRUCTURE_REALIZATION_CHAIN_BYTES ? 'size_limit_exceeded' : 'chain_empty');
4844
+ }
4845
+ const text = decodeUtf8(bytes, 'INVALID_TODO_STRUCTURE_REALIZATION_CHAIN', 'invalid_utf8');
4846
+ if (!text.endsWith('\n') || text.includes('\r') || text.startsWith('\uFEFF')) {
4847
+ fail('INVALID_TODO_STRUCTURE_REALIZATION_CHAIN', 'chain_byte_contract');
4848
+ }
4849
+ const records = []; let previous = null; const priorDigests = new Set();
4850
+ for (const [index, line] of text.slice(0, -1).split('\n').entries()) {
4851
+ if (line.length === 0) fail('INVALID_TODO_STRUCTURE_REALIZATION_CHAIN', 'empty_record');
4852
+ let value;
4853
+ try { value = JSON.parse(line); } catch {
4854
+ fail('INVALID_TODO_STRUCTURE_REALIZATION_CHAIN', 'record_json_invalid', { index });
4855
+ }
4856
+ if (line !== canonicalizeTodoArtifact(value)) {
4857
+ fail('INVALID_TODO_STRUCTURE_REALIZATION_CHAIN', 'record_non_canonical', { index });
4858
+ }
4859
+ const explained = explainTodoStructureRealization(value, {
4860
+ structureSet, previous, priorDigests,
4861
+ });
4862
+ if (!explained.valid || value.task_id !== taskId) {
4863
+ fail('INVALID_TODO_STRUCTURE_REALIZATION_CHAIN',
4864
+ explained.valid ? 'task_id_mismatch' : explained.reason,
4865
+ { index, path: explained.path ?? '/task_id' });
4866
+ }
4867
+ records.push(value); previous = value; priorDigests.add(value.realization_digest);
4868
+ }
4869
+ return records;
4870
+ }
4871
+
4872
+ /** chain未作成は[]、存在するchainの破損はtyped failure。 */
4873
+ export async function readTodoStructureRealizationChain(options = {}) {
4874
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
4875
+ if (!validateTodoStructureSet(options.structureSet)) {
4876
+ throw new TypeError('structureSet must be valid');
4877
+ }
4878
+ const ref = todoStructureRealizationRef(
4879
+ options.structureSet.plan_key, options.structureSet.plan_version, options.taskId,
4880
+ );
4881
+ const state = await pathState(repoRoot, ref, 'INVALID_TODO_STRUCTURE_REALIZATION_CHAIN', {
4882
+ missing: true,
4883
+ });
4884
+ if (state === null) return [];
4885
+ return parseTodoStructureRealizationChain(await readFile(state.absolute), {
4886
+ structureSet: options.structureSet, taskId: options.taskId,
4887
+ });
4888
+ }
4889
+
4890
+ /**
4891
+ * active binding配下のtask realizationをappend-onlyで追記する。
4892
+ * Git objectと他task chainを追記前に照合し、拒否時はchain bytesを変えない。
4893
+ */
4894
+ export async function appendTodoStructureRealization(options = {}) {
4895
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
4896
+ const { realization } = options;
4897
+ const initial = explainTodoStructureRealization(realization);
4898
+ if (!initial.valid) {
4899
+ fail('INVALID_TODO_STRUCTURE_REALIZATION', initial.reason, { path: initial.path });
4900
+ }
4901
+ return withLock(repoRoot, async () => {
4902
+ const store = await readTodoStore({ repoRoot, forWrite: true, now: options.now });
4903
+ const member = activeMember(store, realization.plan_key);
4904
+ if (store.project_id !== realization.project_id
4905
+ || member.plan.plan_version !== realization.plan_version) {
4906
+ fail('STRUCTURE_REALIZATION_BINDING_MISMATCH', 'active_plan_identity_mismatch');
4907
+ }
4908
+ const structureSet = await readTodoStructureSource({
4909
+ repoRoot, planKey: realization.plan_key,
4910
+ });
4911
+ const binding = await readTodoStructureBinding({
4912
+ repoRoot, planKey: realization.plan_key, planVersion: realization.plan_version,
4913
+ });
4914
+ if (structureSet === null || binding === null
4915
+ || structureSet.structure_set_digest !== binding.structure_set_digest
4916
+ || realization.structure_set_digest !== binding.structure_set_digest) {
4917
+ fail('STRUCTURE_REALIZATION_BINDING_MISMATCH', 'structure_not_enabled_or_stale');
4918
+ }
4919
+ const task = structureSet.tasks.find(({ task_id: id }) => id === realization.task_id);
4920
+ if (task?.applicability !== 'graph') {
4921
+ fail('STRUCTURE_REALIZATION_BINDING_MISMATCH', 'task_not_graph_applicable');
4922
+ }
4923
+ let currentHead;
4924
+ try {
4925
+ currentHead = gitSync(['rev-parse', '--verify', 'HEAD^{commit}'], {
4926
+ cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
4927
+ }).trim();
4928
+ } catch {
4929
+ fail('STRUCTURE_GIT_HEAD_UNAVAILABLE', 'current_head_unavailable');
4930
+ }
4931
+ if (realization.head_sha !== currentHead) {
4932
+ fail('STRUCTURE_REALIZATION_STALE', 'realization_head_mismatch', {
4933
+ expected: currentHead, actual: realization.head_sha,
4934
+ });
4935
+ }
4936
+ const chain = await readTodoStructureRealizationChain({
4937
+ repoRoot, structureSet, taskId: realization.task_id,
4938
+ });
4939
+ const priorDigests = new Set(chain.map(({ realization_digest: digest }) => digest));
4940
+ const explained = explainTodoStructureRealization(realization, {
4941
+ structureSet, previous: chain.at(-1) ?? null, priorDigests,
4942
+ });
4943
+ if (!explained.valid) {
4944
+ fail('INVALID_TODO_STRUCTURE_REALIZATION_CHAIN', explained.reason, {
4945
+ path: explained.path, task_id: realization.task_id,
4946
+ });
4947
+ }
4948
+
4949
+ for (const otherTask of structureSet.tasks.filter(({ applicability, task_id: taskId }) => (
4950
+ applicability === 'graph' && taskId !== realization.task_id
4951
+ ))) {
4952
+ const otherChain = await readTodoStructureRealizationChain({
4953
+ repoRoot, structureSet, taskId: otherTask.task_id,
4954
+ });
4955
+ const claimed = new Set(otherChain.flatMap(({ commit_oids: commitOids }) => commitOids));
4956
+ const reused = realization.commit_oids.filter((commitOid) => claimed.has(commitOid));
4957
+ if (reused.length > 0) {
4958
+ fail('STRUCTURE_REALIZATION_COMMIT_CLAIMED', 'commit_claimed_by_other_task', {
4959
+ task_id: realization.task_id, other_task_id: otherTask.task_id,
4960
+ commit_oids: reused,
4961
+ });
4962
+ }
4963
+ }
4964
+
4965
+ const provenance = collectTodoStructureGitProvenance({
4966
+ repoRoot, structureSet, requireClean: false,
4967
+ });
4968
+ bindTodoStructureRealizationCommits({ provenance, realizations: [realization] });
4969
+ const declared = new Set(realization.commit_oids);
4970
+ const changedPaths = new Set(provenance.changesets
4971
+ .filter(({ commit_oid: commitOid }) => declared.has(commitOid))
4972
+ .flatMap(({ changes }) => changes.flatMap(({ path: changedPath, previous_path: previousPath }) => (
4973
+ previousPath === null ? [changedPath] : [changedPath, previousPath]
4974
+ ))));
4975
+ const mutatingAnchors = realization.realized.code_anchors
4976
+ .filter(({ effect }) => ['create', 'modify', 'delete'].includes(effect));
4977
+ const unboundAnchors = mutatingAnchors.filter(({ path: anchorPath }) => !changedPaths.has(anchorPath));
4978
+ if (mutatingAnchors.length === 0 || unboundAnchors.length > 0) {
4979
+ fail('STRUCTURE_REALIZATION_ANCHOR_UNBOUND',
4980
+ mutatingAnchors.length === 0 ? 'mutating_code_anchor_missing' : 'commit_does_not_touch_anchor', {
4981
+ task_id: realization.task_id,
4982
+ anchor_ids: unboundAnchors.map(({ anchor_id: anchorId }) => anchorId).sort(),
4983
+ changed_paths: [...changedPaths].sort(),
4984
+ });
4985
+ }
4986
+
4987
+ const ref = todoStructureRealizationRef(
4988
+ structureSet.plan_key, structureSet.plan_version, realization.task_id,
4989
+ );
4990
+ const absolute = path.resolve(repoRoot, ref);
4991
+ await ensureSafeStoreDirectory(repoRoot, path.dirname(absolute));
4992
+ const before = await exactFileOrNull(absolute);
4993
+ await atomicWrite(absolute, Buffer.concat([before ?? Buffer.alloc(0), canonicalLine(realization)]));
4994
+ return {
4995
+ ref, realization, history_length: chain.length + 1,
4996
+ previous_realization_digest: chain.at(-1)?.realization_digest ?? null,
4997
+ };
4998
+ });
4999
+ }