@quolu/lattice 0.57.3 → 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.
- package/README.ja.md +32 -0
- package/README.md +33 -0
- package/docs/schemas/lattice.todo_structure_binding.v1.schema.json +47 -0
- package/docs/schemas/lattice.todo_structure_realization.v1.schema.json +55 -0
- package/docs/schemas/lattice.todo_structure_set.v1.schema.json +264 -0
- package/package.json +4 -1
- package/sensor/dist/bin/exact-traversal.d.ts +20 -0
- package/sensor/dist/bin/exact-traversal.d.ts.map +1 -0
- package/sensor/dist/bin/exact-traversal.js +17 -0
- package/sensor/dist/bin/exact-traversal.js.map +1 -0
- package/sensor/dist/bin/lattice-sensor.js +27 -8
- package/sensor/package.json +1 -1
- package/src/cli-help.mjs +16 -3
- package/src/dag-chain.mjs +37 -0
- package/src/project-cli.mjs +9 -3
- package/src/sensor-adapter.mjs +8 -2
- package/src/todo-chain.mjs +19 -5
- package/src/todo-cli.mjs +609 -7
- package/src/todo-gantt-html-independence.mjs +9 -1
- package/src/todo-gantt-html-style.mjs +8 -0
- package/src/todo-gantt-html.mjs +14 -4
- package/src/todo-gantt-structure.mjs +134 -0
- package/src/todo-status.mjs +32 -6
- package/src/todo-store.mjs +608 -14
- package/src/todo-structure-contracts.mjs +706 -0
- package/src/todo-structure-git-adapter.mjs +452 -0
- package/src/todo-structure-overlay.mjs +599 -0
- package/src/todo-structure-presentation.mjs +163 -0
- package/src/todo-structure-source-adapter.mjs +417 -0
- package/src/todo-structure-store.mjs +475 -0
package/src/todo-store.mjs
CHANGED
|
@@ -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
|
|
1396
|
-
//
|
|
1397
|
-
//
|
|
1398
|
-
//
|
|
1399
|
-
//
|
|
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
|
-
&&
|
|
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')) {
|
|
@@ -4403,3 +4500,500 @@ export async function writeTodoWitnessSet(options = {}) {
|
|
|
4403
4500
|
await atomicWrite(path.resolve(repoRoot, ref), canonicalLine(witnessSet));
|
|
4404
4501
|
return { ref, witnessSet };
|
|
4405
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
|
+
}
|