@quolu/lattice 0.78.1 → 0.79.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 +21 -3
- package/README.md +22 -0
- package/bin/lattice.mjs +31 -0
- package/docs/schemas/lattice.plan_backchain.v1.schema.json +103 -0
- package/package.json +4 -3
- package/src/cli-help.mjs +4 -0
- package/src/hooks-cli.mjs +19 -9
- package/src/plan-backchain.mjs +628 -0
- package/src/project-cli.mjs +3 -2
- package/src/todo-authoring-input.mjs +2 -0
- package/src/todo-cli.mjs +19 -6
- package/src/todo-revision.mjs +47 -7
- package/src/todo-store.mjs +118 -3
package/src/todo-cli.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cliCommandKind } from './cli-command-kind.mjs';
|
|
2
2
|
import { cliFailureClass } from './cli-failure-class.mjs';
|
|
3
|
+
import { readTaskBackchainContext } from './plan-backchain.mjs';
|
|
3
4
|
import { gitSync } from './git-process.mjs';
|
|
4
5
|
import { createHash } from 'node:crypto';
|
|
5
6
|
import {
|
|
@@ -636,7 +637,8 @@ function terminalAuditDoneAdvisory(plan, phases) {
|
|
|
636
637
|
|
|
637
638
|
async function mutate({
|
|
638
639
|
repoRoot, env, planKey, taskId, kind, payload, evidenceRef, evidenceMessage = null,
|
|
639
|
-
advisory = null, noteContext = null, structureContext = null,
|
|
640
|
+
advisory = null, noteContext = null, structureContext = null, featureContext = null,
|
|
641
|
+
testResultRef = null,
|
|
640
642
|
}) {
|
|
641
643
|
const actor = mutationActor(env);
|
|
642
644
|
const evidence = evidenceRef === null && evidenceMessage === null
|
|
@@ -683,7 +685,9 @@ async function mutate({
|
|
|
683
685
|
throw new TypeError('start mutation requires note context');
|
|
684
686
|
}
|
|
685
687
|
const result = {
|
|
686
|
-
schema: includesNoteContext
|
|
688
|
+
schema: includesNoteContext
|
|
689
|
+
? featureContext === null ? 'lattice.todo_mutation_result.v5' : 'lattice.todo_mutation_result.v6'
|
|
690
|
+
: 'lattice.todo_mutation_result.v2',
|
|
687
691
|
project_id: event.project_id,
|
|
688
692
|
plan_key: event.plan_key,
|
|
689
693
|
plan_version: event.plan_version,
|
|
@@ -699,6 +703,7 @@ async function mutate({
|
|
|
699
703
|
design_memo: designMemoProjection(authoredTask),
|
|
700
704
|
note_context: noteContext,
|
|
701
705
|
structure_context: structureContext,
|
|
706
|
+
...(featureContext === null ? {} : { feature_context: featureContext }),
|
|
702
707
|
} : {}),
|
|
703
708
|
result_digest: '',
|
|
704
709
|
};
|
|
@@ -906,9 +911,12 @@ async function startTask({
|
|
|
906
911
|
const structureContext = await startStructureContext({
|
|
907
912
|
repoRoot, store, planKey, taskId: resolvedTaskId,
|
|
908
913
|
});
|
|
914
|
+
const featureContext = await readTaskBackchainContext({
|
|
915
|
+
repoRoot, plan: member.plan, taskId: resolvedTaskId,
|
|
916
|
+
});
|
|
909
917
|
return mutate({ repoRoot, env, planKey, taskId: resolvedTaskId, kind: 'start',
|
|
910
918
|
payload: { override_reason: overrideReason }, evidenceRef: null, advisory, noteContext,
|
|
911
|
-
structureContext });
|
|
919
|
+
structureContext, featureContext });
|
|
912
920
|
}
|
|
913
921
|
|
|
914
922
|
// plan改訂でcarryされたin-progress ToDoの再束縛入口(ADR 0191)。task状態・開始時刻は
|
|
@@ -916,7 +924,7 @@ async function startTask({
|
|
|
916
924
|
// literal startだけを束縛に使いcarryを推定しないため、改訂を跨いだ進行中工程の
|
|
917
925
|
// 再開はこの入口だけが正規経路になる。advisory・independence gateは初回startで
|
|
918
926
|
// 済んでいるため再適用しない。
|
|
919
|
-
async function rebindStart({ repoRoot, env, planKey, taskId, reason }) {
|
|
927
|
+
export async function rebindStart({ repoRoot, env, planKey, taskId, reason }) {
|
|
920
928
|
const store = await readTodoStore({ repoRoot });
|
|
921
929
|
const member = store.members.find(({ descriptor }) => descriptor.plan_key === planKey);
|
|
922
930
|
if (member === undefined) throw new TodoStoreError('STORE_INCONSISTENT', 'plan_not_active');
|
|
@@ -1816,8 +1824,11 @@ async function todoDetail({ repoRoot, planKey, taskId }) {
|
|
|
1816
1824
|
const { context } = await readTodoNoteContext({
|
|
1817
1825
|
repoRoot, store, planKey, taskId: task.task_id,
|
|
1818
1826
|
});
|
|
1827
|
+
const featureContext = await readTaskBackchainContext({
|
|
1828
|
+
repoRoot, plan: member.plan, taskId: task.task_id,
|
|
1829
|
+
});
|
|
1819
1830
|
const result = {
|
|
1820
|
-
schema: 'lattice.todo_detail_result.v3',
|
|
1831
|
+
schema: featureContext === null ? 'lattice.todo_detail_result.v3' : 'lattice.todo_detail_result.v4',
|
|
1821
1832
|
project_id: store.project_id,
|
|
1822
1833
|
plan_key: planKey,
|
|
1823
1834
|
plan_version: member.plan.plan_version,
|
|
@@ -1825,6 +1836,7 @@ async function todoDetail({ repoRoot, planKey, taskId }) {
|
|
|
1825
1836
|
design_memo: designMemoProjection(task),
|
|
1826
1837
|
state,
|
|
1827
1838
|
note_context: context,
|
|
1839
|
+
...(featureContext === null ? {} : { feature_context: featureContext }),
|
|
1828
1840
|
result_digest: '',
|
|
1829
1841
|
};
|
|
1830
1842
|
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
@@ -3658,6 +3670,7 @@ async function verify({ repoRoot, requestedPlanKey }) {
|
|
|
3658
3670
|
case 'lattice.phase_todo_revision.v1':
|
|
3659
3671
|
case 'lattice.phase_todo_revision.v2':
|
|
3660
3672
|
case 'lattice.phase_todo_revision.v3':
|
|
3673
|
+
case 'lattice.phase_todo_revision.v4':
|
|
3661
3674
|
verifiedSourceInventories.set(member.descriptor.plan_key,
|
|
3662
3675
|
await verifyEffectivePhaseTodoRevisionSources({ repoRoot, member }));
|
|
3663
3676
|
break;
|
|
@@ -3670,7 +3683,7 @@ async function verify({ repoRoot, requestedPlanKey }) {
|
|
|
3670
3683
|
const reconciled = member.revision !== null;
|
|
3671
3684
|
const sourceInventory = verifiedSourceInventories.get(member.descriptor.plan_key) ?? null;
|
|
3672
3685
|
const phaseRevision = ['lattice.phase_todo_revision.v1', 'lattice.phase_todo_revision.v2',
|
|
3673
|
-
'lattice.phase_todo_revision.v3']
|
|
3686
|
+
'lattice.phase_todo_revision.v3', 'lattice.phase_todo_revision.v4']
|
|
3674
3687
|
.includes(member.revision?.schema);
|
|
3675
3688
|
return {
|
|
3676
3689
|
plan_key: member.descriptor.plan_key,
|
package/src/todo-revision.mjs
CHANGED
|
@@ -23,6 +23,7 @@ const PHASE_REVISION_V3_KEYS = [
|
|
|
23
23
|
'runtime_task_migration', 'task_migration', 'phase_migration', 'source_inventory',
|
|
24
24
|
'reconciliation', 'source_cutover_batch', 'revision_digest',
|
|
25
25
|
];
|
|
26
|
+
const PHASE_REVISION_V4_KEYS = [...PHASE_REVISION_KEYS, 'backchain'];
|
|
26
27
|
|
|
27
28
|
const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
28
29
|
const boundedText = (value) => typeof value === 'string' && value.length > 0
|
|
@@ -64,9 +65,11 @@ export function todoRevisionPlanVersion({
|
|
|
64
65
|
|
|
65
66
|
export function phaseTodoRevisionPlanVersion({
|
|
66
67
|
projectId, planKey, predecessor, desiredPlan, taskMigration, phaseMigration,
|
|
68
|
+
featureGraphDigest = null,
|
|
67
69
|
}) {
|
|
68
70
|
const versionDigest = digestTodoArtifact({
|
|
69
|
-
schema:
|
|
71
|
+
schema: featureGraphDigest !== null ? 'lattice.phase_todo_revision_version.v3'
|
|
72
|
+
: ['lattice.todo_plan.v5', 'lattice.todo_plan.v7'].includes(desiredPlan.schema)
|
|
70
73
|
? 'lattice.phase_todo_revision_version.v2' : 'lattice.phase_todo_revision_version.v1', project_id: projectId,
|
|
71
74
|
plan_key: planKey, predecessor, desired_topology: {
|
|
72
75
|
schema: desiredPlan.schema, project_id: desiredPlan.project_id,
|
|
@@ -76,6 +79,7 @@ export function phaseTodoRevisionPlanVersion({
|
|
|
76
79
|
...(['lattice.todo_plan.v5', 'lattice.todo_plan.v7'].includes(desiredPlan.schema)
|
|
77
80
|
? { phase_accept_dependencies: desiredPlan.phase_accept_dependencies } : {}),
|
|
78
81
|
}, task_migration: taskMigration, phase_migration: phaseMigration,
|
|
82
|
+
...(featureGraphDigest === null ? {} : { feature_graph_digest: featureGraphDigest }),
|
|
79
83
|
});
|
|
80
84
|
return `rev-${versionDigest.slice(0, 24)}`;
|
|
81
85
|
}
|
|
@@ -291,22 +295,51 @@ export function validatePhaseTodoRevision(value) {
|
|
|
291
295
|
const revisionV1 = value?.schema === 'lattice.phase_todo_revision.v1';
|
|
292
296
|
const revisionV2 = value?.schema === 'lattice.phase_todo_revision.v2';
|
|
293
297
|
const revisionV3 = value?.schema === 'lattice.phase_todo_revision.v3';
|
|
294
|
-
const
|
|
295
|
-
|
|
298
|
+
const revisionV4 = value?.schema === 'lattice.phase_todo_revision.v4';
|
|
299
|
+
const keys = revisionV3 ? PHASE_REVISION_V3_KEYS
|
|
300
|
+
: revisionV4 ? PHASE_REVISION_V4_KEYS : PHASE_REVISION_KEYS;
|
|
301
|
+
if (!exactRecord(value, keys) || (!revisionV1 && !revisionV2 && !revisionV3 && !revisionV4)) return false;
|
|
296
302
|
if (!isTodoIdentifier(value.project_id) || !isTodoIdentifier(value.plan_key)) return false;
|
|
297
303
|
if (!validPredecessor(value.predecessor) || !validateTodoPlan(value.desired_plan)
|
|
298
304
|
|| !(revisionV3
|
|
299
305
|
? ['lattice.todo_plan.v5', 'lattice.todo_plan.v7'].includes(value.desired_plan.schema)
|
|
300
|
-
: value.desired_plan.schema === (revisionV2 ? 'lattice.todo_plan.v5'
|
|
306
|
+
: value.desired_plan.schema === (revisionV2 ? 'lattice.todo_plan.v5'
|
|
307
|
+
: revisionV4 ? 'lattice.todo_plan.v7' : 'lattice.todo_plan.v4'))
|
|
301
308
|
|| value.desired_plan.project_id !== value.project_id
|
|
302
309
|
|| value.desired_plan.plan_key !== value.plan_key
|
|
303
310
|
|| value.desired_plan.predecessor_plan_digest !== value.predecessor.plan_digest
|
|
304
311
|
|| !validTaskMigration(value.task_migration)
|
|
305
312
|
|| !validPhaseMigration(value.phase_migration, value.desired_plan)) return false;
|
|
313
|
+
const backchainKeys = [
|
|
314
|
+
'schema', 'authoring_ref', 'project_id', 'plan_key', 'plan_version',
|
|
315
|
+
'goal', 'features', 'tasks', 'chain_digest',
|
|
316
|
+
];
|
|
317
|
+
if (revisionV4 && (!(exactRecord(value.backchain, backchainKeys)
|
|
318
|
+
|| exactRecord(value.backchain, [...backchainKeys, 'continuity']))
|
|
319
|
+
|| value.backchain.schema !== 'lattice.plan_backchain.v1'
|
|
320
|
+
|| value.backchain.project_id !== value.project_id
|
|
321
|
+
|| value.backchain.plan_key !== value.plan_key
|
|
322
|
+
|| value.backchain.plan_version !== value.desired_plan.plan_version
|
|
323
|
+
|| (value.backchain.continuity !== undefined
|
|
324
|
+
&& (!Array.isArray(value.backchain.continuity)
|
|
325
|
+
|| value.backchain.continuity.length > 512
|
|
326
|
+
|| !value.backchain.continuity.every((entry) => exactRecord(entry,
|
|
327
|
+
['task_id', 'policy', 'reason']) && isTodoIdentifier(entry.task_id)
|
|
328
|
+
&& ['carry', 'reset', 'retire'].includes(entry.policy)
|
|
329
|
+
&& typeof entry.reason === 'string' && entry.reason.trim().length > 0)
|
|
330
|
+
|| new Set(value.backchain.continuity.map(({ task_id: id }) => id)).size
|
|
331
|
+
!== value.backchain.continuity.length))
|
|
332
|
+
|| value.backchain.chain_digest !== todoSelfDigest(value.backchain, 'chain_digest')
|
|
333
|
+
|| canonicalizeForCompare(value.backchain.tasks.map(({ task_id: id }) => id))
|
|
334
|
+
!== canonicalizeForCompare(value.desired_plan.tasks.map(({ task_id: id }) => id)))) return false;
|
|
306
335
|
if (value.desired_plan.plan_version !== phaseTodoRevisionPlanVersion({
|
|
307
336
|
projectId: value.project_id, planKey: value.plan_key, predecessor: value.predecessor,
|
|
308
337
|
desiredPlan: value.desired_plan, taskMigration: value.task_migration,
|
|
309
338
|
phaseMigration: value.phase_migration,
|
|
339
|
+
...(revisionV4 ? { featureGraphDigest: digestTodoArtifact({
|
|
340
|
+
goal: value.backchain.goal, features: value.backchain.features,
|
|
341
|
+
tasks: value.backchain.tasks,
|
|
342
|
+
}) } : {}),
|
|
310
343
|
})) return false;
|
|
311
344
|
if (revisionV3) {
|
|
312
345
|
if (!validRuntimeTaskMigration(value.runtime_task_migration)
|
|
@@ -810,8 +843,10 @@ export function explainPhaseTodoRevision(value) {
|
|
|
810
843
|
const revisionV1 = value.schema === 'lattice.phase_todo_revision.v1';
|
|
811
844
|
const revisionV2 = value.schema === 'lattice.phase_todo_revision.v2';
|
|
812
845
|
const revisionV3 = value.schema === 'lattice.phase_todo_revision.v3';
|
|
813
|
-
|
|
814
|
-
|
|
846
|
+
const revisionV4 = value.schema === 'lattice.phase_todo_revision.v4';
|
|
847
|
+
if (!revisionV1 && !revisionV2 && !revisionV3 && !revisionV4) return reject('schema_mismatch', '/schema');
|
|
848
|
+
const keyCheck = explainKeys(value, revisionV3 ? PHASE_REVISION_V3_KEYS
|
|
849
|
+
: revisionV4 ? PHASE_REVISION_V4_KEYS : PHASE_REVISION_KEYS, '');
|
|
815
850
|
if (!keyCheck.valid) return keyCheck;
|
|
816
851
|
if (!isTodoIdentifier(value.project_id)) return reject('invalid_identifier', '/project_id');
|
|
817
852
|
if (!isTodoIdentifier(value.plan_key)) return reject('invalid_identifier', '/plan_key');
|
|
@@ -819,7 +854,8 @@ export function explainPhaseTodoRevision(value) {
|
|
|
819
854
|
if (!predecessorCheck.valid) return predecessorCheck;
|
|
820
855
|
if (!validateTodoPlan(value.desired_plan)) return reject('desired_plan_invalid', '/desired_plan');
|
|
821
856
|
const expectedPlanSchemas = revisionV3 ? ['lattice.todo_plan.v5', 'lattice.todo_plan.v7']
|
|
822
|
-
: [revisionV2 ? 'lattice.todo_plan.v5'
|
|
857
|
+
: [revisionV2 ? 'lattice.todo_plan.v5'
|
|
858
|
+
: revisionV4 ? 'lattice.todo_plan.v7' : 'lattice.todo_plan.v4'];
|
|
823
859
|
if (!expectedPlanSchemas.includes(value.desired_plan.schema)) {
|
|
824
860
|
return reject('desired_plan_schema_mismatch', '/desired_plan/schema');
|
|
825
861
|
}
|
|
@@ -840,6 +876,10 @@ export function explainPhaseTodoRevision(value) {
|
|
|
840
876
|
projectId: value.project_id, planKey: value.plan_key, predecessor: value.predecessor,
|
|
841
877
|
desiredPlan: value.desired_plan, taskMigration: value.task_migration,
|
|
842
878
|
phaseMigration: value.phase_migration,
|
|
879
|
+
...(revisionV4 ? { featureGraphDigest: digestTodoArtifact({
|
|
880
|
+
goal: value.backchain.goal, features: value.backchain.features,
|
|
881
|
+
tasks: value.backchain.tasks,
|
|
882
|
+
}) } : {}),
|
|
843
883
|
});
|
|
844
884
|
if (value.desired_plan.plan_version !== expectedPlanVersion) {
|
|
845
885
|
return reject('plan_version_mismatch', '/desired_plan/plan_version');
|
package/src/todo-store.mjs
CHANGED
|
@@ -69,7 +69,7 @@ const TODO_REVISION_SCHEMAS = Object.freeze([
|
|
|
69
69
|
]);
|
|
70
70
|
const PHASE_TODO_REVISION_SCHEMAS = Object.freeze([
|
|
71
71
|
'lattice.phase_todo_revision.v1', 'lattice.phase_todo_revision.v2',
|
|
72
|
-
'lattice.phase_todo_revision.v3',
|
|
72
|
+
'lattice.phase_todo_revision.v3', 'lattice.phase_todo_revision.v4',
|
|
73
73
|
]);
|
|
74
74
|
|
|
75
75
|
export class TodoStoreError extends Error {
|
|
@@ -2207,6 +2207,26 @@ export function buildTodoPlan(input) {
|
|
|
2207
2207
|
return plan;
|
|
2208
2208
|
}
|
|
2209
2209
|
|
|
2210
|
+
function boundPlanBackchain(plan, chain) {
|
|
2211
|
+
if (chain === null || chain === undefined) return null;
|
|
2212
|
+
if (chain.schema !== 'lattice.plan_backchain.v1'
|
|
2213
|
+
|| chain.project_id !== plan.project_id || chain.plan_key !== plan.plan_key
|
|
2214
|
+
|| chain.plan_version !== plan.plan_version
|
|
2215
|
+
|| !isTodoDigest(chain.chain_digest)
|
|
2216
|
+
|| chain.chain_digest !== todoSelfDigest(chain, 'chain_digest')) {
|
|
2217
|
+
fail('STORE_INCONSISTENT', 'backchain_plan_binding_invalid');
|
|
2218
|
+
}
|
|
2219
|
+
const binding = {
|
|
2220
|
+
schema: 'lattice.plan_backchain_binding.v1',
|
|
2221
|
+
plan_digest: plan.plan_digest, chain, binding_digest: '',
|
|
2222
|
+
};
|
|
2223
|
+
binding.binding_digest = todoSelfDigest(binding, 'binding_digest');
|
|
2224
|
+
if (Buffer.byteLength(JSON.stringify(binding), 'utf8') > TODO_LIMITS.snapshotBytes) {
|
|
2225
|
+
fail('STORE_WRITE_REFUSED', 'backchain_binding_too_large');
|
|
2226
|
+
}
|
|
2227
|
+
return binding;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2210
2230
|
export function buildPlanGenesis(plan, input) {
|
|
2211
2231
|
const event = {
|
|
2212
2232
|
schema: isPhaseTodoPlanSchema(plan.schema)
|
|
@@ -2347,6 +2367,7 @@ async function bootstrapImportedPlan(repoRoot, options) {
|
|
|
2347
2367
|
}
|
|
2348
2368
|
const prepared = prepareImportedArtifacts(repoRoot, options, initialization.projectId, []);
|
|
2349
2369
|
const { plan, genesis, events, snapshot } = prepared;
|
|
2370
|
+
const backchain = boundPlanBackchain(plan, options.featureChain);
|
|
2350
2371
|
const base = `${STORE_ROOT_REF}/plans/${plan.plan_key}/${plan.plan_version}`;
|
|
2351
2372
|
const planRef = `${base}/plan.json`;
|
|
2352
2373
|
const journalRef = `${base}/journal/active.jsonl`;
|
|
@@ -2370,6 +2391,7 @@ async function bootstrapImportedPlan(repoRoot, options) {
|
|
|
2370
2391
|
await atomicWrite(path.join(stagedBase, 'plan.json'), canonicalLine(plan));
|
|
2371
2392
|
await atomicWrite(path.join(stagedBase, 'journal', 'active.jsonl'), Buffer.concat(events.map(canonicalLine)));
|
|
2372
2393
|
await atomicWrite(path.join(stagedBase, 'snapshot.json'), canonicalLine(snapshot));
|
|
2394
|
+
if (backchain !== null) await atomicWrite(path.join(stagedBase, 'backchain.json'), canonicalLine(backchain));
|
|
2373
2395
|
await atomicWrite(path.join(stage, 'manifest.json'), canonicalLine(manifest));
|
|
2374
2396
|
await protocolStage(options, 'bootstrap_staged');
|
|
2375
2397
|
|
|
@@ -2753,6 +2775,7 @@ export async function appendImportedPlan(options = {}) {
|
|
|
2753
2775
|
repoRoot, options, store.project_id, store.members.map(({ plan: memberPlan }) => ({ plan: memberPlan })),
|
|
2754
2776
|
);
|
|
2755
2777
|
const { plan, genesis, events, snapshot, tasks } = prepared;
|
|
2778
|
+
const backchain = boundPlanBackchain(plan, options.featureChain);
|
|
2756
2779
|
|
|
2757
2780
|
const base = `${STORE_ROOT_REF}/plans/${plan.plan_key}/${plan.plan_version}`;
|
|
2758
2781
|
const planRef = `${base}/plan.json`;
|
|
@@ -2780,6 +2803,7 @@ export async function appendImportedPlan(options = {}) {
|
|
|
2780
2803
|
await atomicWrite(stagedPlan, canonicalLine(plan));
|
|
2781
2804
|
await atomicWrite(stagedJournal, Buffer.concat(events.map(canonicalLine)));
|
|
2782
2805
|
await atomicWrite(stagedSnapshot, canonicalLine(snapshot));
|
|
2806
|
+
if (backchain !== null) await atomicWrite(path.join(stagedBase, 'backchain.json'), canonicalLine(backchain));
|
|
2783
2807
|
for (const [index, artifact] of importedCrossPlan.artifacts.entries()) {
|
|
2784
2808
|
artifact.stagedAbsolute = path.join(transactionAbsolute, 'plan-scoped', `${index}.jsonl`);
|
|
2785
2809
|
await atomicWrite(artifact.stagedAbsolute, artifact.finalBytes);
|
|
@@ -2824,6 +2848,9 @@ export async function appendImportedPlan(options = {}) {
|
|
|
2824
2848
|
await rename(stagedPlan, path.resolve(repoRoot, planRef));
|
|
2825
2849
|
await rename(stagedJournal, path.resolve(repoRoot, journalRef));
|
|
2826
2850
|
await rename(stagedSnapshot, path.resolve(repoRoot, snapshotRef));
|
|
2851
|
+
if (backchain !== null) {
|
|
2852
|
+
await rename(path.join(stagedBase, 'backchain.json'), path.join(finalBaseAbsolute, 'backchain.json'));
|
|
2853
|
+
}
|
|
2827
2854
|
await fsyncDirectory(path.dirname(path.resolve(repoRoot, planRef)));
|
|
2828
2855
|
await fsyncDirectory(path.dirname(path.resolve(repoRoot, journalRef)));
|
|
2829
2856
|
}
|
|
@@ -2911,14 +2938,16 @@ export async function initializeAuthoredTodoStore(options = {}) {
|
|
|
2911
2938
|
const optionKeys = [
|
|
2912
2939
|
'repoRoot', 'writer', 'projectId', 'repositories', 'plan', 'genesis',
|
|
2913
2940
|
];
|
|
2914
|
-
|
|
2915
|
-
|
|
2941
|
+
const allowedOptions = options.featureChain === undefined ? optionKeys : [...optionKeys, 'featureChain'];
|
|
2942
|
+
if (!(exactRecord(options, allowedOptions)
|
|
2943
|
+
|| (exactRecord(options, [...allowedOptions, 'onProtocolStage'])
|
|
2916
2944
|
&& typeof options.onProtocolStage === 'function'))
|
|
2917
2945
|
|| options.projectId !== options.plan?.project_id
|
|
2918
2946
|
|| options.plan?.predecessor_plan_digest !== null) {
|
|
2919
2947
|
throw new TypeError('authored store initialization input invalid');
|
|
2920
2948
|
}
|
|
2921
2949
|
const plan = buildTodoPlan(options.plan);
|
|
2950
|
+
const backchain = boundPlanBackchain(plan, options.featureChain);
|
|
2922
2951
|
verifyPlanNarrativeAnchors(repoRoot, plan);
|
|
2923
2952
|
const genesis = buildPlanGenesis(plan, options.genesis);
|
|
2924
2953
|
const tasks = replay(plan, [genesis]);
|
|
@@ -2962,6 +2991,7 @@ export async function initializeAuthoredTodoStore(options = {}) {
|
|
|
2962
2991
|
await atomicWrite(path.join(stage, base, 'plan.json'), canonicalLine(plan));
|
|
2963
2992
|
await atomicWrite(path.join(stage, base, 'journal', 'active.jsonl'), canonicalLine(genesis));
|
|
2964
2993
|
await atomicWrite(path.join(stage, base, 'snapshot.json'), canonicalLine(snapshot));
|
|
2994
|
+
if (backchain !== null) await atomicWrite(path.join(stage, base, 'backchain.json'), canonicalLine(backchain));
|
|
2965
2995
|
await atomicWrite(path.join(stage, 'manifest.json'), canonicalLine(manifest));
|
|
2966
2996
|
|
|
2967
2997
|
try {
|
|
@@ -3245,6 +3275,83 @@ function validatePhaseV3Carry(previous, revision, migration, idMap, state) {
|
|
|
3245
3275
|
}
|
|
3246
3276
|
}
|
|
3247
3277
|
|
|
3278
|
+
function validateBackchainCarry(previous, revision, migration, idMap) {
|
|
3279
|
+
const decision = revision.backchain.continuity?.find(({ task_id: id }) =>
|
|
3280
|
+
id === migration.from_task_id);
|
|
3281
|
+
if (decision?.policy === 'carry') return;
|
|
3282
|
+
const reconciliationMetadata = migration.state_policy === 'carry_reconciled_metadata';
|
|
3283
|
+
const predecessorTask = previous.plan.tasks.find(({ task_id }) => task_id === migration.from_task_id);
|
|
3284
|
+
const phaseIdMap = new Map(revision.phase_migration
|
|
3285
|
+
.filter(({ from_phase_id, to_phase_id }) => from_phase_id !== null && to_phase_id !== 'removed')
|
|
3286
|
+
.map(({ from_phase_id, to_phase_id }) => [from_phase_id, to_phase_id]));
|
|
3287
|
+
const before = phaseV3CarrySemantics(previous.plan, migration.from_task_id, idMap, phaseIdMap, {
|
|
3288
|
+
reconciliationMetadata, includeDesignMemo: !reconciliationMetadata
|
|
3289
|
+
&& typeof predecessorTask?.design_memo === 'string',
|
|
3290
|
+
removedTaskIds: removedTaskIdsFor(revision),
|
|
3291
|
+
});
|
|
3292
|
+
const after = phaseV3CarrySemantics(revision.desired_plan, migration.to_task_id,
|
|
3293
|
+
new Map(), new Map(), {
|
|
3294
|
+
reconciliationMetadata, includeDesignMemo: !reconciliationMetadata
|
|
3295
|
+
&& typeof predecessorTask?.design_memo === 'string',
|
|
3296
|
+
});
|
|
3297
|
+
// 利用先の増減は提供済み機能の意味を変えない。入力・提供機能の同一性は
|
|
3298
|
+
// backchain契約で別に照合し、工程の入力辺と本体の意味だけをここで検査する。
|
|
3299
|
+
if (canonicalizeTodoArtifact(before.task) !== canonicalizeTodoArtifact(after.task)
|
|
3300
|
+
|| canonicalizeTodoArtifact(before.incoming) !== canonicalizeTodoArtifact(after.incoming)) {
|
|
3301
|
+
fail('REVISION_INVALID', 'carry_semantics_changed', { from_task_id: migration.from_task_id });
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
function backchainTaskSignature(chain, taskId) {
|
|
3306
|
+
const task = chain?.tasks?.find(({ task_id: id }) => id === taskId);
|
|
3307
|
+
if (task === undefined) return null;
|
|
3308
|
+
const featureIds = [...new Set([...task.requires_feature_ids, ...task.provides_feature_ids])].sort();
|
|
3309
|
+
const byId = new Map(chain.features.map((feature) => [feature.feature_id, feature]));
|
|
3310
|
+
return canonicalizeTodoArtifact({
|
|
3311
|
+
requires_feature_ids: task.requires_feature_ids,
|
|
3312
|
+
provides_feature_ids: task.provides_feature_ids,
|
|
3313
|
+
features: featureIds.map((id) => byId.get(id)),
|
|
3314
|
+
});
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
async function assertBackchainCarryContracts(repoRoot, previous, revision) {
|
|
3318
|
+
const decisions = new Map((revision.backchain.continuity ?? [])
|
|
3319
|
+
.map((entry) => [entry.task_id, entry]));
|
|
3320
|
+
for (const decision of decisions.values()) {
|
|
3321
|
+
const migration = revision.task_migration.find(({ from_task_id: id }) => id === decision.task_id);
|
|
3322
|
+
if (migration === undefined || (decision.policy === 'carry'
|
|
3323
|
+
&& !['carry', 'carry_reconciled_metadata'].includes(migration.state_policy))
|
|
3324
|
+
|| (decision.policy === 'reset' && migration.state_policy !== 'reset_pending')
|
|
3325
|
+
|| (decision.policy === 'retire' && migration.state_policy !== 'removed')) {
|
|
3326
|
+
fail('REVISION_INVALID', 'backchain_continuity_mismatch', { task_id: decision.task_id });
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
const carried = revision.task_migration.filter(({ state_policy }) =>
|
|
3330
|
+
['carry', 'carry_reconciled_metadata', 'acquire_phase'].includes(state_policy));
|
|
3331
|
+
if (carried.length === 0) return;
|
|
3332
|
+
const oldRef = path.resolve(repoRoot, STORE_ROOT_REF, 'plans', previous.plan.plan_key,
|
|
3333
|
+
previous.plan.plan_version, 'backchain.json');
|
|
3334
|
+
const bytes = await exactFileOrNull(oldRef);
|
|
3335
|
+
if (bytes === null) fail('REVISION_INVALID', 'backchain_predecessor_missing');
|
|
3336
|
+
let binding;
|
|
3337
|
+
try { binding = JSON.parse(decodeUtf8(bytes, 'REVISION_INVALID', 'backchain_predecessor_invalid')); }
|
|
3338
|
+
catch { fail('REVISION_INVALID', 'backchain_predecessor_invalid'); }
|
|
3339
|
+
if (binding?.schema !== 'lattice.plan_backchain_binding.v1'
|
|
3340
|
+
|| binding.plan_digest !== previous.plan.plan_digest
|
|
3341
|
+
|| binding.binding_digest !== todoSelfDigest(binding, 'binding_digest')) {
|
|
3342
|
+
fail('REVISION_INVALID', 'backchain_predecessor_invalid');
|
|
3343
|
+
}
|
|
3344
|
+
for (const migration of carried) {
|
|
3345
|
+
if (decisions.get(migration.from_task_id)?.policy === 'carry') continue;
|
|
3346
|
+
if (backchainTaskSignature(binding.chain, migration.from_task_id)
|
|
3347
|
+
!== backchainTaskSignature(revision.backchain, migration.to_task_id)) {
|
|
3348
|
+
fail('REVISION_INVALID', 'backchain_carry_feature_changed', {
|
|
3349
|
+
from_task_id: migration.from_task_id, to_task_id: migration.to_task_id,
|
|
3350
|
+
});
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3248
3355
|
/**
|
|
3249
3356
|
* acquire_phase専用のcarry検証(ADR 0147裁定4)。既存carry/carry_reconciled_metadataの
|
|
3250
3357
|
* 分岐(validatePhaseV3Carry)には一切手を入れず、「Phase割当ての獲得だけ」を許す別分岐として
|
|
@@ -3330,6 +3437,8 @@ function stateMigrationFor(previous, revision) {
|
|
|
3330
3437
|
}
|
|
3331
3438
|
} else if (revision.schema === 'lattice.phase_todo_revision.v3') {
|
|
3332
3439
|
validatePhaseV3Carry(previous, revision, migration, idMap, state);
|
|
3440
|
+
} else if (revision.schema === 'lattice.phase_todo_revision.v4') {
|
|
3441
|
+
validateBackchainCarry(previous, revision, migration, idMap);
|
|
3333
3442
|
} else {
|
|
3334
3443
|
const predecessorTask = previous.plan.tasks
|
|
3335
3444
|
.find(({ task_id }) => task_id === migration.from_task_id);
|
|
@@ -4373,6 +4482,9 @@ export async function applyPhaseTodoRevision(options = {}) {
|
|
|
4373
4482
|
fail('STORE_WRITE_CONFLICT', 'stale_predecessor');
|
|
4374
4483
|
}
|
|
4375
4484
|
verifyPlanNarrativeAnchors(repoRoot, revision.desired_plan, previous.plan);
|
|
4485
|
+
if (revision.schema === 'lattice.phase_todo_revision.v4') {
|
|
4486
|
+
await assertBackchainCarryContracts(repoRoot, previous, revision);
|
|
4487
|
+
}
|
|
4376
4488
|
const stateMigration = stateMigrationFor(previous, revision);
|
|
4377
4489
|
const phaseStateMigration = phaseStateMigrationFor(previous, revision);
|
|
4378
4490
|
const prospective = store.members.map((member) => member === previous
|
|
@@ -4417,6 +4529,9 @@ export async function applyPhaseTodoRevision(options = {}) {
|
|
|
4417
4529
|
['phase_revision_plan_durable', planRef, canonicalLine(revision.desired_plan)],
|
|
4418
4530
|
['phase_revision_genesis_durable', journalRef, canonicalLine(genesis)],
|
|
4419
4531
|
['phase_revision_snapshot_durable', snapshotRef, canonicalLine(snapshot)],
|
|
4532
|
+
...(revision.schema === 'lattice.phase_todo_revision.v4'
|
|
4533
|
+
? [['phase_revision_backchain_durable', `${base}/backchain.json`,
|
|
4534
|
+
canonicalLine(boundPlanBackchain(revision.desired_plan, revision.backchain))]] : []),
|
|
4420
4535
|
];
|
|
4421
4536
|
for (const [stage, ref, bytes] of artifacts) {
|
|
4422
4537
|
const absolute = path.resolve(repoRoot, ref);
|