@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.
- 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/runtime-pull-intake.mjs +8 -4
- package/src/sensor-adapter.mjs +8 -2
- package/src/todo-chain.mjs +19 -5
- package/src/todo-cli.mjs +612 -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 +645 -27
- 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')) {
|
|
@@ -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 }) =>
|
|
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
|
-
.
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
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
|
-
|
|
2315
|
-
|
|
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
|
+
}
|