@quolu/lattice 0.38.0 → 0.39.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 +22 -16
- package/README.md +11 -7
- package/bin/lattice-mcp.mjs +0 -0
- package/bin/lattice.mjs +1 -1
- package/docs/schemas/lattice.phase_todo_revision.v3.schema.json +49 -1
- package/docs/schemas/lattice.plan_create_input.v4.schema.json +85 -0
- package/docs/schemas/lattice.todo_extraction.v3.schema.json +146 -0
- package/docs/schemas/lattice.todo_revision.v2.schema.json +41 -1
- package/package.json +3 -1
- package/sensor/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/sensor/dist/extraction/tree-sitter.js +68 -16
- package/sensor/dist/extraction/tree-sitter.js.map +1 -1
- package/src/cli-help.mjs +7 -7
- package/src/project-cli.mjs +32 -15
- package/src/seam-cost.mjs +3 -3
- package/src/todo-cli.mjs +360 -218
- package/src/todo-contracts.mjs +46 -8
- package/src/todo-dashboard-registry.mjs +17 -3
- package/src/todo-gantt-html-independence.mjs +14 -3
- package/src/todo-gantt-html-shared.mjs +5 -2
- package/src/todo-gantt-layout.mjs +1 -1
- package/src/todo-gantt-scope.mjs +1 -1
- package/src/todo-migration.mjs +46 -9
- package/src/todo-revision.mjs +10 -8
- package/src/todo-status.mjs +3 -3
- package/src/todo-store.mjs +74 -31
package/src/todo-store.mjs
CHANGED
|
@@ -252,10 +252,18 @@ function emptyPhaseState(phaseId) {
|
|
|
252
252
|
// と衝突しない予約名として固定する。
|
|
253
253
|
export const TERMINAL_AUDIT_PHASE_ID = 'terminal-audit';
|
|
254
254
|
|
|
255
|
+
export function isPhaseTodoPlanSchema(schema) {
|
|
256
|
+
return ['lattice.todo_plan.v4', 'lattice.todo_plan.v5', 'lattice.todo_plan.v7'].includes(schema);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function isDecoupledPhaseTodoPlanSchema(schema) {
|
|
260
|
+
return ['lattice.todo_plan.v5', 'lattice.todo_plan.v7'].includes(schema);
|
|
261
|
+
}
|
|
262
|
+
|
|
255
263
|
// CLI側(migrate/plan create/最後のdone)が「終端重監査が要る」ことを通知するために
|
|
256
264
|
// 同じ判定を再利用する。判定基準を二重管理しないよう、ここから公開する。
|
|
257
265
|
export function isPhaselessTodoPlanSchema(schema) {
|
|
258
|
-
return !
|
|
266
|
+
return !isPhaseTodoPlanSchema(schema);
|
|
259
267
|
}
|
|
260
268
|
|
|
261
269
|
function terminalAuditPhase() {
|
|
@@ -358,7 +366,13 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
|
|
|
358
366
|
if (!isStrictTodoTimestamp(event.recorded_at)) fail('STORE_CORRUPT', 'timestamp_invalid');
|
|
359
367
|
const time = Date.parse(event.recorded_at);
|
|
360
368
|
if (previousTime !== null && time < previousTime) fail('STORE_INCONSISTENT', 'clock_reversal');
|
|
361
|
-
if (time > now.valueOf() + MAX_FUTURE_SKEW_MS)
|
|
369
|
+
if (time > now.valueOf() + MAX_FUTURE_SKEW_MS) {
|
|
370
|
+
fail('STORE_INCONSISTENT', 'future_clock_skew', {
|
|
371
|
+
recorded_at: event.recorded_at,
|
|
372
|
+
current_time: now.toISOString(),
|
|
373
|
+
max_future_skew_ms: MAX_FUTURE_SKEW_MS,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
362
376
|
previousTime = time;
|
|
363
377
|
if (event.kind === 'plan_genesis') {
|
|
364
378
|
if (event.payload.plan_digest !== plan.plan_digest || event.payload.topology_digest !== plan.topology_digest
|
|
@@ -386,8 +400,12 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
|
|
|
386
400
|
Object.assign(state, structuredClone(migration.state), { evidence_unverified: false });
|
|
387
401
|
if (state.evidence !== null) {
|
|
388
402
|
if (state.imported) {
|
|
389
|
-
if (verifyImportSource) verifyImportSource(state.evidence
|
|
390
|
-
|
|
403
|
+
if (verifyImportSource) verifyImportSource(state.evidence, {
|
|
404
|
+
plan_key: plan.plan_key, task_id: migration.to_task_id,
|
|
405
|
+
});
|
|
406
|
+
} else if (verifyEvidence) verifyEvidence(state.evidence, {
|
|
407
|
+
plan_key: plan.plan_key, task_id: migration.to_task_id,
|
|
408
|
+
});
|
|
391
409
|
}
|
|
392
410
|
if (state.status === 'done') {
|
|
393
411
|
doneDigest.set(migration.to_task_id, event.event_digest);
|
|
@@ -484,7 +502,9 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
|
|
|
484
502
|
if (!importedGenesis || state.status !== 'pending') {
|
|
485
503
|
fail('STORE_INCONSISTENT', 'invalid_historical_import_start_transition');
|
|
486
504
|
}
|
|
487
|
-
if (verifyImportSource) verifyImportSource(event.payload.evidence
|
|
505
|
+
if (verifyImportSource) verifyImportSource(event.payload.evidence, {
|
|
506
|
+
plan_key: plan.plan_key, task_id: event.task_id,
|
|
507
|
+
});
|
|
488
508
|
state.status = 'in-progress';
|
|
489
509
|
state.started_at = event.payload.started_at === 'unknown_requires_evidence'
|
|
490
510
|
? null : event.payload.started_at;
|
|
@@ -509,13 +529,17 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
|
|
|
509
529
|
} else if (event.kind === 'done') {
|
|
510
530
|
if (event.payload.done_mode === 'authored') {
|
|
511
531
|
if (state.status !== 'in-progress' || !dependenciesDone) fail('STORE_INCONSISTENT', 'invalid_done_transition');
|
|
512
|
-
if (verifyEvidence) verifyEvidence(event.payload.evidence
|
|
532
|
+
if (verifyEvidence) verifyEvidence(event.payload.evidence, {
|
|
533
|
+
plan_key: plan.plan_key, task_id: event.task_id,
|
|
534
|
+
});
|
|
513
535
|
state.status = 'done'; state.done_at = event.recorded_at; state.evidence = event.payload.evidence;
|
|
514
536
|
state.imported = false;
|
|
515
537
|
completion.set(event.task_id, { mode: 'authored', completed_at: event.recorded_at });
|
|
516
538
|
} else if (event.payload.done_mode === 'historical_import') {
|
|
517
539
|
if (!importedGenesis || state.status !== 'pending') fail('STORE_INCONSISTENT', 'invalid_historical_import_transition');
|
|
518
|
-
if (verifyImportSource) verifyImportSource(event.payload.evidence
|
|
540
|
+
if (verifyImportSource) verifyImportSource(event.payload.evidence, {
|
|
541
|
+
plan_key: plan.plan_key, task_id: event.task_id,
|
|
542
|
+
});
|
|
519
543
|
state.status = 'done'; state.done_at = event.payload.completed_at === 'unknown_requires_evidence'
|
|
520
544
|
? null : event.payload.completed_at;
|
|
521
545
|
state.evidence = event.payload.evidence; state.imported = true;
|
|
@@ -527,7 +551,9 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
|
|
|
527
551
|
|| doneDigest.get(event.task_id) !== event.payload.target_done_digest) {
|
|
528
552
|
fail('STORE_INCONSISTENT', 'invalid_evidence_promotion');
|
|
529
553
|
}
|
|
530
|
-
if (verifyEvidence) verifyEvidence(event.payload.evidence
|
|
554
|
+
if (verifyEvidence) verifyEvidence(event.payload.evidence, {
|
|
555
|
+
plan_key: plan.plan_key, task_id: event.task_id,
|
|
556
|
+
});
|
|
531
557
|
state.evidence = event.payload.evidence;
|
|
532
558
|
completion.set(event.task_id, { mode: 'evidence_promotion', completed_at: current.completed_at });
|
|
533
559
|
}
|
|
@@ -566,7 +592,7 @@ function snapshotFor(plan, events, tasks) {
|
|
|
566
592
|
// snapshot_staleになりforWrite(start/done/revise等)が丸ごとSTORE_WRITE_REFUSEDへ落ちる
|
|
567
593
|
// ——ADR 0147以降の暗黙terminal-audit Phaseの状態は、この関数の外(readTodoStore/
|
|
568
594
|
// appendTodoEventが返す`phases`という導出ビュー)で供給する。
|
|
569
|
-
const phasePlan =
|
|
595
|
+
const phasePlan = isPhaseTodoPlanSchema(plan.schema);
|
|
570
596
|
const snapshot = {
|
|
571
597
|
schema: phasePlan ? 'lattice.todo_snapshot.v2' : 'lattice.todo_snapshot.v1',
|
|
572
598
|
project_id: plan.project_id, plan_key: plan.plan_key,
|
|
@@ -587,7 +613,7 @@ function validateMergedGraph(members) {
|
|
|
587
613
|
const adjacency = new Map([...tasks.keys()].map((key) => [key, []]));
|
|
588
614
|
const phases = new Map();
|
|
589
615
|
for (const member of members) {
|
|
590
|
-
if (member.plan.schema
|
|
616
|
+
if (!isDecoupledPhaseTodoPlanSchema(member.plan.schema)) continue;
|
|
591
617
|
for (const phase of member.plan.phases) {
|
|
592
618
|
const key = `phase\0${member.plan.project_id}\0${member.plan.plan_key}\0${phase.phase_id}`;
|
|
593
619
|
phases.set(key, member.plan.topology_digest);
|
|
@@ -630,7 +656,7 @@ function validateMergedGraph(members) {
|
|
|
630
656
|
adjacency.get(after).push(before);
|
|
631
657
|
}
|
|
632
658
|
}
|
|
633
|
-
if (plan.schema
|
|
659
|
+
if (isDecoupledPhaseTodoPlanSchema(plan.schema)) {
|
|
634
660
|
for (const task of plan.tasks) {
|
|
635
661
|
const taskKey = `${plan.project_id}\0${plan.plan_key}\0${task.task_id}`;
|
|
636
662
|
const phaseKey = `phase\0${plan.project_id}\0${plan.plan_key}\0${task.phase_id}`;
|
|
@@ -675,7 +701,7 @@ function mergedTaskPhase(store, taskKey) {
|
|
|
675
701
|
project_id: member.plan.project_id, plan_key: member.plan.plan_key, task_id: entry.task_id,
|
|
676
702
|
}) === taskKey);
|
|
677
703
|
if (task === undefined) continue;
|
|
678
|
-
if (!
|
|
704
|
+
if (!isPhaseTodoPlanSchema(member.plan.schema)) return null;
|
|
679
705
|
return { schema: member.plan.schema, plan_key: member.plan.plan_key, phase_id: task.phase_id,
|
|
680
706
|
status: member.snapshot.phases.find(({ phase_id }) => phase_id === task.phase_id)?.status };
|
|
681
707
|
}
|
|
@@ -687,7 +713,7 @@ function mergedDependencyPhaseReady(store, predecessorKey, targetKey) {
|
|
|
687
713
|
const target = mergedTaskPhase(store, targetKey);
|
|
688
714
|
if (predecessor === undefined) return false;
|
|
689
715
|
if (predecessor === null) return true;
|
|
690
|
-
if (predecessor.schema
|
|
716
|
+
if (isDecoupledPhaseTodoPlanSchema(predecessor.schema)) return true;
|
|
691
717
|
if (target !== null && target !== undefined && predecessor.plan_key === target.plan_key
|
|
692
718
|
&& predecessor.phase_id === target.phase_id) return true;
|
|
693
719
|
return predecessor.status === 'accepted';
|
|
@@ -695,7 +721,7 @@ function mergedDependencyPhaseReady(store, predecessorKey, targetKey) {
|
|
|
695
721
|
|
|
696
722
|
function mergedPhaseAcceptReady(store, targetKey) {
|
|
697
723
|
for (const member of store.members) {
|
|
698
|
-
if (member.plan.schema
|
|
724
|
+
if (!isDecoupledPhaseTodoPlanSchema(member.plan.schema)) continue;
|
|
699
725
|
for (const edge of member.plan.phase_accept_dependencies) {
|
|
700
726
|
if (mergedTaskKey(edge.to) !== targetKey) continue;
|
|
701
727
|
const source = store.members.find((candidate) => candidate.plan.project_id === edge.from.project_id
|
|
@@ -766,7 +792,7 @@ function validateMergedTransition(store, member, event) {
|
|
|
766
792
|
plan_key: member.plan.plan_key, task_id }))
|
|
767
793
|
.flatMap((key) => mergedSuccessorKeys(store, key))
|
|
768
794
|
.some((key) => states.get(key) !== 'pending')
|
|
769
|
-
: store.members.some((candidate) => candidate.plan.schema
|
|
795
|
+
: store.members.some((candidate) => isDecoupledPhaseTodoPlanSchema(candidate.plan.schema)
|
|
770
796
|
&& candidate.plan.phase_accept_dependencies.some((edge) => (
|
|
771
797
|
edge.from.project_id === member.plan.project_id
|
|
772
798
|
&& edge.from.plan_key === member.plan.plan_key && edge.from.phase_id === event.phase_id
|
|
@@ -806,7 +832,7 @@ function reachableObjects(absoluteRepo) {
|
|
|
806
832
|
|
|
807
833
|
function evidenceVerifier(manifest, repoRoot, hard) {
|
|
808
834
|
const repositories = new Map(manifest.repositories.map((repo) => [repo.repo_id, repo.path]));
|
|
809
|
-
return (descriptor) => {
|
|
835
|
+
return (descriptor, context = {}) => {
|
|
810
836
|
if (!validateEvidenceDescriptor(descriptor)) fail('STORE_INCONSISTENT', 'evidence_descriptor_invalid');
|
|
811
837
|
const repoRef = repositories.get(descriptor.repo_id);
|
|
812
838
|
if (repoRef === undefined) fail('STORE_INCONSISTENT', 'evidence_repo_missing');
|
|
@@ -829,7 +855,12 @@ function evidenceVerifier(manifest, repoRoot, hard) {
|
|
|
829
855
|
if (sha256Bytes(bytes) !== descriptor.content_digest) throw new Error('digest mismatch');
|
|
830
856
|
return true;
|
|
831
857
|
} catch {
|
|
832
|
-
if (hard) fail('STORE_INCONSISTENT', 'evidence_unverified'
|
|
858
|
+
if (hard) fail('STORE_INCONSISTENT', 'evidence_unverified', {
|
|
859
|
+
...context,
|
|
860
|
+
next_action: context.plan_key === undefined
|
|
861
|
+
? 'verify_the_evidence_descriptor_and_retry'
|
|
862
|
+
: `lattice todo verify --plan ${context.plan_key} --json`,
|
|
863
|
+
});
|
|
833
864
|
return false;
|
|
834
865
|
}
|
|
835
866
|
};
|
|
@@ -886,13 +917,16 @@ function liveReplacementPreservesListStructure(lineBytes, replacement) {
|
|
|
886
917
|
}
|
|
887
918
|
|
|
888
919
|
function importSourceVerifier(repoRoot, hard, cache = null) {
|
|
889
|
-
return (descriptor) => {
|
|
920
|
+
return (descriptor, context = {}) => {
|
|
890
921
|
if (!validateTodoImportSource(descriptor)) fail('STORE_INCONSISTENT', 'import_source_descriptor_invalid');
|
|
891
922
|
try {
|
|
892
923
|
pinnedSourceLine(repoRoot, descriptor, cache);
|
|
893
924
|
return true;
|
|
894
925
|
} catch {
|
|
895
|
-
if (hard) fail('STORE_INCONSISTENT', 'import_source_unverified'
|
|
926
|
+
if (hard) fail('STORE_INCONSISTENT', 'import_source_unverified', {
|
|
927
|
+
...context,
|
|
928
|
+
next_action: 'verify_source_commit_origin_path_and_line_then_retry',
|
|
929
|
+
});
|
|
896
930
|
return false;
|
|
897
931
|
}
|
|
898
932
|
};
|
|
@@ -911,7 +945,8 @@ function narrativeAnchorSource(value) {
|
|
|
911
945
|
|
|
912
946
|
function materializeImportedNarrativeAnchors(repoRoot, planInput, sources) {
|
|
913
947
|
if (sources === undefined) return planInput;
|
|
914
|
-
if (
|
|
948
|
+
if (!['lattice.todo_plan.v2', 'lattice.todo_plan.v6'].includes(planInput?.schema)
|
|
949
|
+
|| !Array.isArray(sources)
|
|
915
950
|
|| sources.length > TODO_LIMITS.tasksPerPlan || !sources.every(narrativeAnchorSource)) {
|
|
916
951
|
throw new TypeError('imported narrative anchor sources are invalid');
|
|
917
952
|
}
|
|
@@ -947,8 +982,10 @@ function materializeImportedNarrativeAnchors(repoRoot, planInput, sources) {
|
|
|
947
982
|
}
|
|
948
983
|
|
|
949
984
|
function verifyPlanNarrativeAnchors(repoRoot, plan, trustedPlan = null) {
|
|
950
|
-
if (!['lattice.todo_plan.v2', 'lattice.todo_plan.v3', 'lattice.todo_plan.v4',
|
|
951
|
-
|
|
985
|
+
if (!['lattice.todo_plan.v2', 'lattice.todo_plan.v3', 'lattice.todo_plan.v4',
|
|
986
|
+
'lattice.todo_plan.v5', 'lattice.todo_plan.v6', 'lattice.todo_plan.v7'].includes(plan.schema)) return;
|
|
987
|
+
const trusted = new Map((['lattice.todo_plan.v2', 'lattice.todo_plan.v3', 'lattice.todo_plan.v4',
|
|
988
|
+
'lattice.todo_plan.v5', 'lattice.todo_plan.v6', 'lattice.todo_plan.v7'].includes(trustedPlan?.schema)
|
|
952
989
|
? trustedPlan.tasks : [])
|
|
953
990
|
.map((task) => [task.task_id, task.narrative_anchor]));
|
|
954
991
|
for (const task of plan.tasks) {
|
|
@@ -1196,7 +1233,7 @@ function nextEvent(input, storeMember) {
|
|
|
1196
1233
|
const payload = input.kind === 'done' && exactRecord(input.payload, ['evidence'])
|
|
1197
1234
|
? { done_mode: 'authored', imported: false, evidence: input.payload.evidence }
|
|
1198
1235
|
: input.payload;
|
|
1199
|
-
const phaseCapablePlan =
|
|
1236
|
+
const phaseCapablePlan = isPhaseTodoPlanSchema(storeMember.plan.schema);
|
|
1200
1237
|
const phaseKind = ['phase_review', 'phase_accept', 'phase_reject', 'phase_reopen', 'phase_close_unaudited']
|
|
1201
1238
|
.includes(input.kind);
|
|
1202
1239
|
// ADR 0147: phase無しplanでも、暗黙のterminal-audit Phaseへのphase_*eventだけは
|
|
@@ -1361,8 +1398,8 @@ export function buildTodoPlan(input) {
|
|
|
1361
1398
|
plan.topology_digest = digestTodoArtifact({
|
|
1362
1399
|
project_id: plan.project_id, plan_key: plan.plan_key, plan_version: plan.plan_version,
|
|
1363
1400
|
tasks: plan.tasks, hard_dependencies: plan.hard_dependencies, joins: plan.joins,
|
|
1364
|
-
...(
|
|
1365
|
-
...(plan.schema
|
|
1401
|
+
...(isPhaseTodoPlanSchema(plan.schema) ? { phases: plan.phases } : {}),
|
|
1402
|
+
...(isDecoupledPhaseTodoPlanSchema(plan.schema)
|
|
1366
1403
|
? { phase_accept_dependencies: plan.phase_accept_dependencies } : {}),
|
|
1367
1404
|
});
|
|
1368
1405
|
plan.plan_digest = todoSelfDigest(plan, 'plan_digest');
|
|
@@ -1372,12 +1409,12 @@ export function buildTodoPlan(input) {
|
|
|
1372
1409
|
|
|
1373
1410
|
export function buildPlanGenesis(plan, input) {
|
|
1374
1411
|
const event = {
|
|
1375
|
-
schema:
|
|
1412
|
+
schema: isPhaseTodoPlanSchema(plan.schema)
|
|
1376
1413
|
? 'lattice.todo_event.v3' : 'lattice.todo_event.v1',
|
|
1377
1414
|
project_id: plan.project_id, plan_key: plan.plan_key,
|
|
1378
1415
|
plan_version: plan.plan_version, sequence: 0, previous_digest: input.previous_digest ?? null,
|
|
1379
1416
|
kind: 'plan_genesis', task_id: null,
|
|
1380
|
-
...(
|
|
1417
|
+
...(isPhaseTodoPlanSchema(plan.schema) ? { phase_id: null } : {}),
|
|
1381
1418
|
actor: input.actor, recorded_at: input.recorded_at,
|
|
1382
1419
|
provenance: input.provenance ?? null,
|
|
1383
1420
|
payload: { plan_digest: plan.plan_digest, topology_digest: plan.topology_digest,
|
|
@@ -1920,7 +1957,7 @@ function taskSemantics(plan, taskId, idMap, { reconciliationMetadata = false } =
|
|
|
1920
1957
|
.sort((left, right) => canonicalizeTodoArtifact(left) < canonicalizeTodoArtifact(right) ? -1 : 1),
|
|
1921
1958
|
before: mappedNodeRef(join.before, plan, idMap),
|
|
1922
1959
|
})).sort((left, right) => left.id < right.id ? -1 : 1);
|
|
1923
|
-
const phaseAcceptDependencies = plan.schema
|
|
1960
|
+
const phaseAcceptDependencies = isDecoupledPhaseTodoPlanSchema(plan.schema)
|
|
1924
1961
|
? plan.phase_accept_dependencies
|
|
1925
1962
|
.filter(({ to }) => localTaskRef(to, plan, taskId))
|
|
1926
1963
|
.map(({ from, to }) => ({ from, to: mappedNodeRef(to, plan, idMap) }))
|
|
@@ -1966,7 +2003,7 @@ function phaseV3CarrySemantics(plan, taskId, taskIdMap, phaseIdMap,
|
|
|
1966
2003
|
if (localTaskRef(after, plan, taskId)) outgoing.push(tuple);
|
|
1967
2004
|
}
|
|
1968
2005
|
}
|
|
1969
|
-
if (plan.schema
|
|
2006
|
+
if (isDecoupledPhaseTodoPlanSchema(plan.schema)) for (const edge of plan.phase_accept_dependencies) {
|
|
1970
2007
|
if (localTaskRef(edge.to, plan, taskId)) incoming.push({ kind: 'phase_accept',
|
|
1971
2008
|
from: mapPhaseRef(edge.from), to: mapTaskRef(edge.to) });
|
|
1972
2009
|
}
|
|
@@ -2079,7 +2116,7 @@ function stateMigrationFor(previous, revision) {
|
|
|
2079
2116
|
}
|
|
2080
2117
|
|
|
2081
2118
|
function phaseStateMigrationFor(previous, revision) {
|
|
2082
|
-
const predecessorPhases =
|
|
2119
|
+
const predecessorPhases = isPhaseTodoPlanSchema(previous.plan.schema)
|
|
2083
2120
|
? previous.plan.phases : [];
|
|
2084
2121
|
const sourceIds = revision.phase_migration
|
|
2085
2122
|
.filter(({ from_phase_id }) => from_phase_id !== null).map(({ from_phase_id }) => from_phase_id);
|
|
@@ -3685,7 +3722,9 @@ export async function createSuccessorTodoPlan(options = {}) {
|
|
|
3685
3722
|
}
|
|
3686
3723
|
const schemaRank = new Map([
|
|
3687
3724
|
['lattice.todo_plan.v1', 1], ['lattice.todo_plan.v2', 2], ['lattice.todo_plan.v3', 3],
|
|
3688
|
-
|
|
3725
|
+
// v6はphase無しv3へdesign_memoを追加した枝であり、phase世代より後という意味ではない。
|
|
3726
|
+
['lattice.todo_plan.v4', 4], ['lattice.todo_plan.v5', 5], ['lattice.todo_plan.v6', 3],
|
|
3727
|
+
['lattice.todo_plan.v7', 5],
|
|
3689
3728
|
]);
|
|
3690
3729
|
if (schemaRank.get(plan.schema) < schemaRank.get(previous.plan.schema)) {
|
|
3691
3730
|
throw new TypeError(previous.plan.schema === 'lattice.todo_plan.v2'
|
|
@@ -3693,6 +3732,10 @@ export async function createSuccessorTodoPlan(options = {}) {
|
|
|
3693
3732
|
? 'todo plan schema cannot regress from v2 to v1'
|
|
3694
3733
|
: 'todo plan schema cannot regress');
|
|
3695
3734
|
}
|
|
3735
|
+
if (['lattice.todo_plan.v6', 'lattice.todo_plan.v7'].includes(previous.plan.schema)
|
|
3736
|
+
&& !['lattice.todo_plan.v6', 'lattice.todo_plan.v7'].includes(plan.schema)) {
|
|
3737
|
+
throw new TypeError('todo plan design_memo cannot be removed by successor schema');
|
|
3738
|
+
}
|
|
3696
3739
|
verifyPlanNarrativeAnchors(repoRoot, plan, previous.plan);
|
|
3697
3740
|
const migration = options.genesis.task_migration ?? [];
|
|
3698
3741
|
const oldIds = previous.plan.tasks.map(({ task_id }) => task_id).sort();
|