@quolu/lattice 0.40.0 → 0.41.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/package.json +1 -1
- package/src/cli-help.mjs +5 -1
- package/src/runtime-cli.mjs +175 -25
- package/src/runtime-multi-epoch-store.mjs +2 -1
- package/src/todo-note-store.mjs +7 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quolu/lattice",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.0",
|
|
4
4
|
"description": "Schedulability compiler for multi-agent development: observe real code boundaries, refactor the conflicting seam, recompile the plan for parallel execution",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Quo / クオ at kitepon.dev",
|
package/src/cli-help.mjs
CHANGED
|
@@ -30,14 +30,18 @@ Commands:
|
|
|
30
30
|
create --schema --json # 既定は最新版(v4)のJSON Schemaを返す
|
|
31
31
|
create --schema-version <1|2|3|4> --json
|
|
32
32
|
show <plan_key> --json # task・依存・phase・状態をplan本体から1コマンドで投影する
|
|
33
|
-
compile --request <request.json>
|
|
33
|
+
compile --request <request.json> [--todo-plan <key>]
|
|
34
34
|
compile --schema --json # lattice.run_request.v1 の JSON Schema を出す
|
|
35
35
|
verify --request <request.json> --plan <plan.json>
|
|
36
36
|
`,
|
|
37
37
|
run: `Usage: lattice run <command> [options]
|
|
38
38
|
|
|
39
39
|
Commands:
|
|
40
|
+
start --request <request.json> --plan <compile-artifact.json> --executor <adapter>
|
|
41
|
+
# TODO storeと連携して同一repo writerを並列起動する場合のgate付き入口。
|
|
42
|
+
# artifactはplan compile --todo-plan <key>で現revisionへ束縛して発行する
|
|
40
43
|
start --request <request.json> --executor <adapter>
|
|
44
|
+
# artifactを消費しない既存runtime互換入口
|
|
41
45
|
start --schema --json # lattice.run_request.v1 の JSON Schema を出す
|
|
42
46
|
adapter register --input <descriptor.json>
|
|
43
47
|
adapter register --schema --json # 登録入力の JSON Schema を出す
|
package/src/runtime-cli.mjs
CHANGED
|
@@ -81,6 +81,8 @@ import { createRuntimeControlRequest, validateRuntimeControlResponse } from './r
|
|
|
81
81
|
import { createRuntimeControlStore } from './runtime-control-store.mjs';
|
|
82
82
|
import { createRuntimeGateStore } from './runtime-gate-store.mjs';
|
|
83
83
|
import { acquireRuntimeLifecycleLock } from './runtime-lifecycle-lock.mjs';
|
|
84
|
+
import { readTodoIndependenceArtifact, readTodoStore } from './todo-store.mjs';
|
|
85
|
+
import { projectIndependenceFrontier } from './todo-independence.mjs';
|
|
84
86
|
import {
|
|
85
87
|
AdapterRegistryError,
|
|
86
88
|
listRuntimeAdapters,
|
|
@@ -130,6 +132,47 @@ const CLI_ERROR_SCHEMA = 'lattice.cli_error.v2';
|
|
|
130
132
|
const RUN_STORE_ROOT = ['.lattice', 'runs'];
|
|
131
133
|
const RUN_REF = /^\.lattice\/runs\/([0-9A-Za-z](?:[0-9A-Za-z._-]{0,127}))$/u;
|
|
132
134
|
const KNOWN_ADAPTERS = Object.freeze(['scripted', 'isolated-worktree', 'actual-agent']);
|
|
135
|
+
|
|
136
|
+
function sameTextSet(left, right) {
|
|
137
|
+
return Array.isArray(left) && Array.isArray(right)
|
|
138
|
+
&& left.length === right.length
|
|
139
|
+
&& [...left].sort().every((value, index) => value === [...right].sort()[index]);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function compileArtifactBodyIsValid(artifact, request) {
|
|
143
|
+
const keys = ['schema', 'request_digest', 'plan', 'manifests', 'schedule', 'graph_digest',
|
|
144
|
+
'todo_plan_binding', 'result_digest'];
|
|
145
|
+
if (artifact === null || typeof artifact !== 'object' || Array.isArray(artifact)
|
|
146
|
+
|| Object.keys(artifact).sort().join('\0') !== keys.sort().join('\0')
|
|
147
|
+
|| artifact.schema !== COMPILE_RESULT_SCHEMA) return false;
|
|
148
|
+
const { result_digest: claimedDigest, ...body } = artifact;
|
|
149
|
+
if (claimedDigest !== digestArtifact(body) || artifact.request_digest !== request.request_digest
|
|
150
|
+
|| !validateRuntimePlan(artifact.plan)
|
|
151
|
+
|| !verifyRuntimePlanBinding({ plan: artifact.plan, request })) return false;
|
|
152
|
+
const nodeIds = artifact.plan.nodes.map((node) => node.todo_id);
|
|
153
|
+
if (artifact.manifests === null || typeof artifact.manifests !== 'object'
|
|
154
|
+
|| Array.isArray(artifact.manifests)
|
|
155
|
+
|| !sameTextSet(Object.keys(artifact.manifests), nodeIds)) return false;
|
|
156
|
+
return nodeIds.every((todoId) => validateRuntimeBoundaryManifest(artifact.manifests[todoId])
|
|
157
|
+
&& artifact.manifests[todoId].manifest_digest === artifact.plan.manifest_digests[todoId]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function todoPlanBindingFor(member) {
|
|
161
|
+
return {
|
|
162
|
+
project_id: member.plan.project_id,
|
|
163
|
+
plan_key: member.plan.plan_key,
|
|
164
|
+
plan_version: member.plan.plan_version,
|
|
165
|
+
topology_digest: member.plan.topology_digest,
|
|
166
|
+
plan_digest: member.plan.plan_digest,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function sameTodoPlanBinding(left, right) {
|
|
171
|
+
return left !== null && typeof left === 'object' && !Array.isArray(left)
|
|
172
|
+
&& Object.keys(left).sort().join('\0') === ['project_id', 'plan_key', 'plan_version',
|
|
173
|
+
'topology_digest', 'plan_digest'].sort().join('\0')
|
|
174
|
+
&& Object.entries(right).every(([key, value]) => left[key] === value);
|
|
175
|
+
}
|
|
133
176
|
/**
|
|
134
177
|
* 自動escalationがlifecycle lockを待つ上限(ADR 0143)。
|
|
135
178
|
*
|
|
@@ -396,9 +439,27 @@ async function compileFromRepo({ request, cwd, planRef, planEpoch, predecessorRe
|
|
|
396
439
|
});
|
|
397
440
|
}
|
|
398
441
|
|
|
399
|
-
async function planCompile({ requestPath, cwd, stdout }) {
|
|
442
|
+
async function planCompile({ requestPath, todoPlanKey = null, cwd, stdout }) {
|
|
400
443
|
const request = await loadRequest(requestPath);
|
|
401
444
|
await resolveRepoBinding(cwd, request);
|
|
445
|
+
let todoPlanBinding = null;
|
|
446
|
+
if (todoPlanKey !== null) {
|
|
447
|
+
const store = await readTodoStore({ repoRoot: cwd });
|
|
448
|
+
const member = store.members.find(({ descriptor }) => descriptor.plan_key === todoPlanKey);
|
|
449
|
+
if (member === undefined) {
|
|
450
|
+
throw new CliContractError('TODO_PLAN_NOT_ACTIVE', '指定されたTODO planはactiveではない', {
|
|
451
|
+
plan_key: todoPlanKey, next_action: 'select_an_active_todo_plan',
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
if (!sameTextSet(member.plan.tasks.map(({ task_id: taskId }) => taskId),
|
|
455
|
+
request.todos.map(({ todo_id: todoId }) => todoId))) {
|
|
456
|
+
throw new CliContractError('TODO_PLAN_TASK_MISMATCH',
|
|
457
|
+
'run requestのTODO集合がactive TODO planと一致しない', {
|
|
458
|
+
plan_key: todoPlanKey, next_action: 'compile_a_request_for_the_current_plan_revision',
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
todoPlanBinding = todoPlanBindingFor(member);
|
|
462
|
+
}
|
|
402
463
|
const result = await compileFromRepo({
|
|
403
464
|
request,
|
|
404
465
|
cwd,
|
|
@@ -416,6 +477,7 @@ async function planCompile({ requestPath, cwd, stdout }) {
|
|
|
416
477
|
manifests: result.manifests,
|
|
417
478
|
schedule: result.schedule,
|
|
418
479
|
graph_digest: result.graph_digest,
|
|
480
|
+
todo_plan_binding: todoPlanBinding,
|
|
419
481
|
};
|
|
420
482
|
artifact.result_digest = digestArtifact(artifact);
|
|
421
483
|
stdout.write(`${JSON.stringify(artifact)}\n`);
|
|
@@ -526,7 +588,8 @@ async function readRunStore(runDir) {
|
|
|
526
588
|
path.join(runDir, 'plan-compile-result.json'), 'plan compile result',
|
|
527
589
|
);
|
|
528
590
|
const request = await readBoundedJson(path.join(runDir, 'request.json'), 'run request');
|
|
529
|
-
const compileKeys = ['schema', 'request_digest', 'plan', 'manifests', 'schedule', 'graph_digest',
|
|
591
|
+
const compileKeys = ['schema', 'request_digest', 'plan', 'manifests', 'schedule', 'graph_digest',
|
|
592
|
+
'todo_plan_binding', 'result_digest'];
|
|
530
593
|
const metaKeys = ['schema', 'run_id', 'executor_adapter', 'plan_digest'];
|
|
531
594
|
const { result_digest: claimedCompileDigest, ...compileBody } = compileArtifact ?? {};
|
|
532
595
|
const legacyMetaValid = meta !== null && typeof meta === 'object' && !Array.isArray(meta)
|
|
@@ -1090,7 +1153,76 @@ function isDistributedScriptedControllerActivation(activation) {
|
|
|
1090
1153
|
));
|
|
1091
1154
|
}
|
|
1092
1155
|
|
|
1093
|
-
async function
|
|
1156
|
+
async function verifyRunStartGate({ request, compileArtifact, repoRoot }) {
|
|
1157
|
+
if (!compileArtifactBodyIsValid(compileArtifact, request)) {
|
|
1158
|
+
throw new CliContractError('INVALID_PLAN_ARTIFACT',
|
|
1159
|
+
'compile artifactのdigest・request binding・plan/manifest bindingが不正', {
|
|
1160
|
+
next_action: 'rerun_lattice_plan_compile_then_pass_its_artifact',
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
if (compileArtifact.todo_plan_binding === null) {
|
|
1164
|
+
throw new CliContractError('COMPILE_ARTIFACT_UNBOUND',
|
|
1165
|
+
'compile artifactがTODO plan revisionへ束縛されていない', {
|
|
1166
|
+
next_action: 'rerun_lattice_plan_compile_with_todo_plan',
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
const store = await readTodoStore({ repoRoot });
|
|
1170
|
+
const binding = compileArtifact.todo_plan_binding;
|
|
1171
|
+
const member = store.members.find(({ descriptor }) => descriptor.plan_key === binding.plan_key);
|
|
1172
|
+
if (member === undefined || !sameTodoPlanBinding(binding, todoPlanBindingFor(member))) {
|
|
1173
|
+
throw new CliContractError('STALE_TODO_PLAN_BINDING',
|
|
1174
|
+
'compile artifactのTODO plan version/digestが現在のactive revisionと一致しない', {
|
|
1175
|
+
plan_key: binding.plan_key,
|
|
1176
|
+
next_action: 'recompile_for_the_current_todo_plan_revision',
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
const taskIds = compileArtifact.plan.nodes.map(({ todo_id: todoId }) => todoId);
|
|
1180
|
+
if (!sameTextSet(member.plan.tasks.map(({ task_id: taskId }) => taskId), taskIds)) {
|
|
1181
|
+
throw new CliContractError('TODO_PLAN_TASK_MISMATCH',
|
|
1182
|
+
'compile artifactのtask集合が現在のTODO planと一致しない', {
|
|
1183
|
+
plan_key: binding.plan_key,
|
|
1184
|
+
next_action: 'recompile_for_the_current_todo_plan_revision',
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
const independenceArtifact = await readTodoIndependenceArtifact({
|
|
1188
|
+
repoRoot, store, planKey: binding.plan_key,
|
|
1189
|
+
});
|
|
1190
|
+
const currentHead = await runGit(['rev-parse', 'HEAD'], repoRoot);
|
|
1191
|
+
if (currentHead.code !== 0) throw new CliContractError('REPO_UNRESOLVED', 'cwdのgit HEADを解決できない');
|
|
1192
|
+
const currentBaseSha = currentHead.stdout.trim();
|
|
1193
|
+
const projected = projectIndependenceFrontier({
|
|
1194
|
+
artifact: independenceArtifact,
|
|
1195
|
+
readyTaskIds: taskIds,
|
|
1196
|
+
activeTaskIds: [],
|
|
1197
|
+
plan: member.plan,
|
|
1198
|
+
currentBaseSha,
|
|
1199
|
+
changedPaths: null,
|
|
1200
|
+
});
|
|
1201
|
+
if (projected.coverage !== 'verified') {
|
|
1202
|
+
throw new CliContractError('PARALLEL_GROUP_UNVERIFIED',
|
|
1203
|
+
'TODO independence recordがverifiedではないため並列writerを起動できない', {
|
|
1204
|
+
coverage: projected.coverage,
|
|
1205
|
+
next_action: 'lattice todo independence compile --plan ' + binding.plan_key + ' --input <witness.json>',
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
for (const wave of compileArtifact.schedule.waves) {
|
|
1209
|
+
if (wave.todo_ids.length < 2) continue;
|
|
1210
|
+
const verified = projected.frontier.parallel_groups.some((group) => (
|
|
1211
|
+
sameTextSet(group.task_ids, wave.todo_ids)
|
|
1212
|
+
));
|
|
1213
|
+
if (!verified) {
|
|
1214
|
+
throw new CliContractError('PARALLEL_GROUP_UNVERIFIED',
|
|
1215
|
+
'compile artifactが同時dispatchするtask群はverified parallel groupではない', {
|
|
1216
|
+
task_ids: wave.todo_ids,
|
|
1217
|
+
coverage: projected.coverage,
|
|
1218
|
+
unknown: projected.frontier.unknown,
|
|
1219
|
+
next_action: 'compile_independence_for_the_current_plan_then_recompile_the_run_plan',
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
async function runStart({ requestPath, planPath = null, executorAdapter, cwd, stdout }) {
|
|
1094
1226
|
// --executor省略時の暗黙fallbackは持たない(Decision 8)。未知adapterはtyped reject。
|
|
1095
1227
|
if (!KNOWN_ADAPTERS.includes(executorAdapter)) {
|
|
1096
1228
|
throw new CliContractError('UNKNOWN_ADAPTER', `未知のexecutor adapter: ${executorAdapter}`);
|
|
@@ -1104,30 +1236,32 @@ async function runStart({ requestPath, executorAdapter, cwd, stdout }) {
|
|
|
1104
1236
|
await requireSafeRunAncestors(repoRoot);
|
|
1105
1237
|
await requireIgnoredRunStore(repoRoot);
|
|
1106
1238
|
await resolveRepoBinding(repoRoot, request);
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1239
|
+
let compileArtifact;
|
|
1240
|
+
if (planPath === null) {
|
|
1241
|
+
// 旧public入口はartifactを消費しないため、このgateの対象ではない。互換入口を
|
|
1242
|
+
// 改変せず残し、TODO storeを持つ統合経路は下の--plan必須入口を使う。
|
|
1243
|
+
const result = await compileFromRepo({
|
|
1244
|
+
request, cwd: repoRoot, planRef: `plan-${request.request_id}-e1`, planEpoch: 1,
|
|
1245
|
+
predecessorRefs: [],
|
|
1246
|
+
});
|
|
1247
|
+
if (result.outcome !== 'dispatchable') {
|
|
1248
|
+
throw new CliContractError(result.code, 'dispatchable planを発行できない', result.detail);
|
|
1249
|
+
}
|
|
1250
|
+
compileArtifact = {
|
|
1251
|
+
schema: COMPILE_RESULT_SCHEMA, request_digest: request.request_digest, plan: result.plan,
|
|
1252
|
+
manifests: result.manifests, schedule: result.schedule, graph_digest: result.graph_digest,
|
|
1253
|
+
todo_plan_binding: null,
|
|
1254
|
+
};
|
|
1255
|
+
compileArtifact.result_digest = digestArtifact(compileArtifact);
|
|
1256
|
+
} else {
|
|
1257
|
+
compileArtifact = await readBoundedJson(planPath, 'plan artifact');
|
|
1258
|
+
await verifyRunStartGate({ request, compileArtifact, repoRoot });
|
|
1116
1259
|
}
|
|
1117
|
-
const compileArtifact = {
|
|
1118
|
-
schema: COMPILE_RESULT_SCHEMA,
|
|
1119
|
-
request_digest: request.request_digest,
|
|
1120
|
-
plan: result.plan,
|
|
1121
|
-
manifests: result.manifests,
|
|
1122
|
-
schedule: result.schedule,
|
|
1123
|
-
graph_digest: result.graph_digest,
|
|
1124
|
-
};
|
|
1125
|
-
compileArtifact.result_digest = digestArtifact(compileArtifact);
|
|
1126
1260
|
const events = initializeRunEvents({
|
|
1127
1261
|
runId: request.request_id,
|
|
1128
1262
|
request,
|
|
1129
|
-
plan:
|
|
1130
|
-
manifests:
|
|
1263
|
+
plan: compileArtifact.plan,
|
|
1264
|
+
manifests: compileArtifact.manifests,
|
|
1131
1265
|
recordedAt: new Date().toISOString().replace(/\.\d+Z$/u, '.000Z'),
|
|
1132
1266
|
});
|
|
1133
1267
|
const runDir = runStorePath(repoRoot, request.request_id);
|
|
@@ -1145,7 +1279,7 @@ async function runStart({ requestPath, executorAdapter, cwd, stdout }) {
|
|
|
1145
1279
|
schema: 'lattice.run_meta.v1',
|
|
1146
1280
|
run_id: request.request_id,
|
|
1147
1281
|
executor_adapter: executorAdapter,
|
|
1148
|
-
plan_digest:
|
|
1282
|
+
plan_digest: compileArtifact.plan.plan_digest,
|
|
1149
1283
|
};
|
|
1150
1284
|
try {
|
|
1151
1285
|
await writeJsonFile(path.join(temporaryDir, 'request.json'), request);
|
|
@@ -1165,7 +1299,7 @@ async function runStart({ requestPath, executorAdapter, cwd, stdout }) {
|
|
|
1165
1299
|
run_id: request.request_id,
|
|
1166
1300
|
run_dir: path.relative(repoRoot, runDir),
|
|
1167
1301
|
executor_adapter: executorAdapter,
|
|
1168
|
-
plan_digest:
|
|
1302
|
+
plan_digest: compileArtifact.plan.plan_digest,
|
|
1169
1303
|
events_digest: digestArtifact(events.map(({ event_digest: digest }) => digest)),
|
|
1170
1304
|
};
|
|
1171
1305
|
output.result_digest = digestArtifact(output);
|
|
@@ -3901,6 +4035,13 @@ export async function runRuntimeCli({ argv, cwd, stdout, stderr }) {
|
|
|
3901
4035
|
&& argv[0] === 'plan' && argv[1] === 'compile' && argv[2] === '--request'
|
|
3902
4036
|
&& typeof argv[3] === 'string' && argv[3].length > 0) {
|
|
3903
4037
|
action = () => planCompile({ requestPath: path.resolve(cwd, argv[3]), cwd, stdout });
|
|
4038
|
+
} else if (argv.length === 6
|
|
4039
|
+
&& argv[0] === 'plan' && argv[1] === 'compile' && argv[2] === '--request'
|
|
4040
|
+
&& typeof argv[3] === 'string' && argv[3].length > 0
|
|
4041
|
+
&& argv[4] === '--todo-plan' && typeof argv[5] === 'string' && argv[5].length > 0) {
|
|
4042
|
+
action = () => planCompile({
|
|
4043
|
+
requestPath: path.resolve(cwd, argv[3]), todoPlanKey: argv[5], cwd, stdout,
|
|
4044
|
+
});
|
|
3904
4045
|
} else if (argv.length === 6
|
|
3905
4046
|
&& argv[0] === 'plan' && argv[1] === 'verify'
|
|
3906
4047
|
&& argv[2] === '--request' && typeof argv[3] === 'string' && argv[3].length > 0
|
|
@@ -3921,6 +4062,15 @@ export async function runRuntimeCli({ argv, cwd, stdout, stderr }) {
|
|
|
3921
4062
|
cwd,
|
|
3922
4063
|
stdout,
|
|
3923
4064
|
});
|
|
4065
|
+
} else if (argv.length === 8
|
|
4066
|
+
&& argv[0] === 'run' && argv[1] === 'start'
|
|
4067
|
+
&& argv[2] === '--request' && typeof argv[3] === 'string' && argv[3].length > 0
|
|
4068
|
+
&& argv[4] === '--plan' && typeof argv[5] === 'string' && argv[5].length > 0
|
|
4069
|
+
&& argv[6] === '--executor' && typeof argv[7] === 'string' && argv[7].length > 0) {
|
|
4070
|
+
action = () => runStart({
|
|
4071
|
+
requestPath: path.resolve(cwd, argv[3]), planPath: path.resolve(cwd, argv[5]),
|
|
4072
|
+
executorAdapter: argv[7], cwd, stdout,
|
|
4073
|
+
});
|
|
3924
4074
|
} else if (argv.length === 4
|
|
3925
4075
|
&& argv[0] === 'run' && argv[1] === 'activate'
|
|
3926
4076
|
&& argv[2] === '--run' && typeof argv[3] === 'string' && argv[3].length > 0) {
|
|
@@ -215,7 +215,8 @@ async function replaceDurableJson(directory, name, value) {
|
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
function validateLegacyInputs({ request, compileArtifact, legacyMeta }) {
|
|
218
|
-
const compileKeys = ['schema', 'request_digest', 'plan', 'manifests', 'schedule', 'graph_digest',
|
|
218
|
+
const compileKeys = ['schema', 'request_digest', 'plan', 'manifests', 'schedule', 'graph_digest',
|
|
219
|
+
'todo_plan_binding', 'result_digest'];
|
|
219
220
|
const metaKeys = ['schema', 'run_id', 'executor_adapter', 'plan_digest'];
|
|
220
221
|
const compileBody = { ...compileArtifact };
|
|
221
222
|
delete compileBody.result_digest;
|
package/src/todo-note-store.mjs
CHANGED
|
@@ -399,7 +399,13 @@ async function readNoteMigrations(repoRoot, planKey, eventVersions) {
|
|
|
399
399
|
fail('NOTE_PROJECTION_INVALID', 'note_plan_history_inventory_invalid', { plan_key: planKey });
|
|
400
400
|
}
|
|
401
401
|
const versionRoot = path.join(base, entry.name);
|
|
402
|
-
|
|
402
|
+
// A revision directory is published before manifest activation. A crash during
|
|
403
|
+
// that window can leave an unreferenced directory containing only a journal
|
|
404
|
+
// stub. It is not a historical plan until its canonical plan artifact exists.
|
|
405
|
+
// Do not make that unreachable residue poison note projection; if a note event
|
|
406
|
+
// names this version, the origin-version check below still fails closed.
|
|
407
|
+
const plan = await readCanonicalJson(path.join(versionRoot, 'plan.json'), { missing: true });
|
|
408
|
+
if (plan === null) continue;
|
|
403
409
|
if (!validateTodoPlan(plan) || plan.plan_key !== planKey || plan.plan_version !== entry.name) {
|
|
404
410
|
fail('NOTE_PROJECTION_INVALID', 'note_historical_plan_invalid', { plan_version: entry.name });
|
|
405
411
|
}
|