@quolu/lattice 0.58.3 → 0.59.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 -3
- package/src/todo-cli.mjs +189 -26
- package/src/todo-contracts.mjs +39 -11
- package/src/todo-store.mjs +27 -10
- package/src/todo-structure-contracts.mjs +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quolu/lattice",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.59.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
|
@@ -113,8 +113,10 @@ Write commands:
|
|
|
113
113
|
# 検査済みplanned sourceをcanonical refへ保存する。compile成功までは有効化しない
|
|
114
114
|
structure compile --plan <key> --input <file>
|
|
115
115
|
# source graph・Git provenance・ToDo DAGを結合する。consistent時だけplanへimmutableに有効化する
|
|
116
|
+
structure realize --plan <key> --task <id> (--planned|--realized <actual-structure.json>) [--commit <HEAD|sha>]...
|
|
117
|
+
# AIはplannedどおりか実体構造だけを判断する。identity・HEAD・履歴鎖・digest・actor・時刻は機械生成する
|
|
116
118
|
structure realize --plan <key> --task <id> --input <file>
|
|
117
|
-
#
|
|
119
|
+
# 完全なrealization envelopeを移送・再生する互換入口
|
|
118
120
|
structure finalize --plan <key> --json
|
|
119
121
|
# 全対象task完了後、最終HEADと全realizationを再結合する。fresh consistentだけterminal受理へ進む
|
|
120
122
|
seam-proposal compile --plan <key> # 並列可否記録と実sensorからseam提案を記録する
|
|
@@ -236,7 +238,7 @@ const SUBCOMMAND_USAGE = Object.freeze({
|
|
|
236
238
|
'todo note list': 'todo note list --plan <key> [--task <id>] --json',
|
|
237
239
|
'todo bindings': 'todo bindings [--plan <key>] [--json]',
|
|
238
240
|
'todo independence': 'todo independence [--plan <key>] [--json] | compile --plan <key> --input <file> | witness migrate --plan <key>',
|
|
239
|
-
'todo structure': 'todo structure --schema --json | [--plan <key>] --json | input --plan <key> --input <file> [--dry-run --json] | compile --plan <key> --input <file> | realize --plan <key> --task <id> --input <
|
|
241
|
+
'todo structure': 'todo structure --schema --json | [--plan <key>] --json | input --plan <key> --input <file> [--dry-run --json] | compile --plan <key> --input <file> | realize --plan <key> --task <id> (--planned|--realized <actual-structure.json>) [--commit <HEAD|sha>]... | realize --plan <key> --task <id> --input <full-realization.json> | finalize --plan <key> --json',
|
|
240
242
|
'todo seam-profile': 'todo seam-profile --plan <key> --file <path> [--json]',
|
|
241
243
|
'todo seam-proposal': 'todo seam-proposal [--plan <key>] [--json] | compile --plan <key>',
|
|
242
244
|
'todo verify': 'todo verify [--plan <key>] [--json]',
|
|
@@ -260,7 +262,7 @@ const SUBCOMMAND_USAGE = Object.freeze({
|
|
|
260
262
|
'todo retract': 'todo retract --plan <key> --task <id> --reason <text>',
|
|
261
263
|
'todo block': 'todo block --plan <key> --task <id> --reason <text>',
|
|
262
264
|
'todo unblock': 'todo unblock --plan <key> --task <id>',
|
|
263
|
-
'todo done': 'todo done --plan <key> --task <id> --evidence <file>',
|
|
265
|
+
'todo done': 'todo done --plan <key> --task <id> --evidence <file> [--test-result <markdown-file>]',
|
|
264
266
|
'todo reopen': 'todo reopen --plan <key> --task <id> --reason <text> [--override-reason <text>]',
|
|
265
267
|
'todo evidence': 'todo evidence promote --plan <key> --task <id> --evidence <file>',
|
|
266
268
|
'todo evidence promote': 'todo evidence promote --plan <key> --task <id> --evidence <file>',
|
package/src/todo-cli.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { parseTree } from 'jsonc-parser';
|
|
|
10
10
|
import {
|
|
11
11
|
TODO_COORDINATION_MODES,
|
|
12
12
|
TODO_DESIGN_MEMO_PROMPT,
|
|
13
|
+
TODO_TEST_RESULT_CONTRACT_ID,
|
|
13
14
|
canonicalizeTodoArtifact,
|
|
14
15
|
digestTodoArtifact,
|
|
15
16
|
exactRecord,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
isTodoDesignMemo,
|
|
19
20
|
isTodoIdentifier,
|
|
20
21
|
isTodoRef,
|
|
22
|
+
isTodoTestResult,
|
|
21
23
|
todoSelfDigest,
|
|
22
24
|
validateEvidenceDescriptor,
|
|
23
25
|
} from './todo-contracts.mjs';
|
|
@@ -74,6 +76,10 @@ import {
|
|
|
74
76
|
} from './todo-store.mjs';
|
|
75
77
|
import {
|
|
76
78
|
TODO_STRUCTURE_BINDING_SCHEMA,
|
|
79
|
+
TODO_STRUCTURE_LIMITS,
|
|
80
|
+
TODO_STRUCTURE_REALIZATION_SCHEMA,
|
|
81
|
+
digestTodoStructureTransform,
|
|
82
|
+
explainTodoStructureTransform,
|
|
77
83
|
explainTodoStructureRealization,
|
|
78
84
|
explainTodoStructureSet,
|
|
79
85
|
} from './todo-structure-contracts.mjs';
|
|
@@ -329,6 +335,16 @@ async function readNoteTextInput(repoRoot, inputRef) {
|
|
|
329
335
|
catch { throw new TodoStoreError('INPUT_UNREADABLE', 'note_input_invalid_utf8'); }
|
|
330
336
|
}
|
|
331
337
|
|
|
338
|
+
async function readTestResultInput(repoRoot, inputRef) {
|
|
339
|
+
const result = await readNoteTextInput(repoRoot, inputRef);
|
|
340
|
+
if (!isTodoTestResult(result)) {
|
|
341
|
+
throw new TodoStoreError('INVALID_TEST_RESULT', 'test_result_must_be_non_empty_markdown', undefined, {
|
|
342
|
+
contract_id: TODO_TEST_RESULT_CONTRACT_ID,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
return result;
|
|
346
|
+
}
|
|
347
|
+
|
|
332
348
|
function taskRef(plan, taskId) {
|
|
333
349
|
return { project_id: plan.project_id, plan_key: plan.plan_key, task_id: taskId };
|
|
334
350
|
}
|
|
@@ -646,12 +662,15 @@ function terminalAuditDoneAdvisory(plan, phases) {
|
|
|
646
662
|
|
|
647
663
|
async function mutate({
|
|
648
664
|
repoRoot, env, planKey, taskId, kind, payload, evidenceRef, advisory = null,
|
|
649
|
-
noteContext = null, structureContext = null,
|
|
665
|
+
noteContext = null, structureContext = null, testResultRef = null,
|
|
650
666
|
}) {
|
|
651
667
|
const actor = mutationActor(env);
|
|
652
668
|
const evidence = evidenceRef === null ? null : await readEvidenceInput(repoRoot, evidenceRef);
|
|
669
|
+
const testResult = testResultRef === null ? null : await readTestResultInput(repoRoot, testResultRef);
|
|
653
670
|
let eventPayload = payload;
|
|
654
|
-
if (kind === 'done' && payload === 'authored')
|
|
671
|
+
if (kind === 'done' && payload === 'authored') {
|
|
672
|
+
eventPayload = { evidence, ...(testResult === null ? {} : { test_result: testResult }) };
|
|
673
|
+
}
|
|
655
674
|
if (kind === 'done' && payload === 'evidence_promotion') {
|
|
656
675
|
eventPayload = { done_mode: 'evidence_promotion', imported: true, evidence };
|
|
657
676
|
}
|
|
@@ -724,7 +743,10 @@ async function startStructureContext({ repoRoot, store, planKey, taskId }) {
|
|
|
724
743
|
structure_set_digest: source.structure_set_digest,
|
|
725
744
|
task: structuredClone(task),
|
|
726
745
|
next_actions: task.applicability === 'graph'
|
|
727
|
-
? [
|
|
746
|
+
? [
|
|
747
|
+
`lattice todo structure realize --plan ${planKey} --task ${taskId} --planned`,
|
|
748
|
+
`lattice todo structure realize --plan ${planKey} --task ${taskId} --realized <actual-structure.json>`,
|
|
749
|
+
]
|
|
728
750
|
: [],
|
|
729
751
|
};
|
|
730
752
|
}
|
|
@@ -1665,7 +1687,7 @@ async function todoDetail({ repoRoot, planKey, taskId }) {
|
|
|
1665
1687
|
repoRoot, store, planKey, taskId: task.task_id,
|
|
1666
1688
|
});
|
|
1667
1689
|
const result = {
|
|
1668
|
-
schema: 'lattice.todo_detail_result.
|
|
1690
|
+
schema: 'lattice.todo_detail_result.v3',
|
|
1669
1691
|
project_id: store.project_id,
|
|
1670
1692
|
plan_key: planKey,
|
|
1671
1693
|
plan_version: member.plan.plan_version,
|
|
@@ -1998,6 +2020,31 @@ function structurePlanMember(store, requestedPlanKey) {
|
|
|
1998
2020
|
return store.members[0];
|
|
1999
2021
|
}
|
|
2000
2022
|
|
|
2023
|
+
function parseAutomatedStructureRealizeArgs(argv) {
|
|
2024
|
+
if (argv[0] !== 'structure' || argv[1] !== 'realize'
|
|
2025
|
+
|| argv[2] !== '--plan' || !isTodoIdentifier(argv[3])
|
|
2026
|
+
|| argv[4] !== '--task' || !isTodoIdentifier(argv[5])) return null;
|
|
2027
|
+
let cursor;
|
|
2028
|
+
let usePlanned;
|
|
2029
|
+
let realizedRef = null;
|
|
2030
|
+
if (argv[6] === '--planned') {
|
|
2031
|
+
cursor = 7; usePlanned = true;
|
|
2032
|
+
} else if (argv[6] === '--realized' && isTodoRef(argv[7])) {
|
|
2033
|
+
cursor = 8; usePlanned = false; realizedRef = argv[7];
|
|
2034
|
+
} else {
|
|
2035
|
+
return null;
|
|
2036
|
+
}
|
|
2037
|
+
const commitRefs = [];
|
|
2038
|
+
while (cursor < argv.length) {
|
|
2039
|
+
if (argv[cursor] !== '--commit' || typeof argv[cursor + 1] !== 'string') return null;
|
|
2040
|
+
commitRefs.push(argv[cursor + 1]);
|
|
2041
|
+
cursor += 2;
|
|
2042
|
+
}
|
|
2043
|
+
return {
|
|
2044
|
+
planKey: argv[3], taskId: argv[5], usePlanned, realizedRef, commitRefs,
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2001
2048
|
function structureNextActions({ coverage, planKey, findings = [], staleReasons = [] }) {
|
|
2002
2049
|
const actions = [];
|
|
2003
2050
|
const add = (value) => { if (!actions.includes(value)) actions.push(value); };
|
|
@@ -2013,14 +2060,16 @@ function structureNextActions({ coverage, planKey, findings = [], staleReasons =
|
|
|
2013
2060
|
if (staleReasons.includes('realization_head_digest')) {
|
|
2014
2061
|
add(`lattice todo structure --plan ${planKey} --json`);
|
|
2015
2062
|
} else {
|
|
2016
|
-
add(`lattice todo structure realize --plan ${planKey} --task <task-id> --
|
|
2063
|
+
add(`lattice todo structure realize --plan ${planKey} --task <task-id> --planned`);
|
|
2064
|
+
add(`lattice todo structure realize --plan ${planKey} --task <task-id> --realized <actual-structure.json>`);
|
|
2017
2065
|
}
|
|
2018
2066
|
add(`lattice todo structure finalize --plan ${planKey} --json`);
|
|
2019
2067
|
} else if (coverage === 'superseded') {
|
|
2020
2068
|
add(`lattice todo structure input --plan ${planKey} --input <migrated-structure-set.json> --dry-run --json`);
|
|
2021
2069
|
} else if (coverage === 'consistent') {
|
|
2022
2070
|
add(`lattice todo structure --plan ${planKey} --json`);
|
|
2023
|
-
add(`lattice todo structure realize --plan ${planKey} --task <task-id> --
|
|
2071
|
+
add(`lattice todo structure realize --plan ${planKey} --task <task-id> --planned`);
|
|
2072
|
+
add(`lattice todo structure realize --plan ${planKey} --task <task-id> --realized <actual-structure.json>`);
|
|
2024
2073
|
add(`lattice todo structure finalize --plan ${planKey} --json`);
|
|
2025
2074
|
}
|
|
2026
2075
|
return actions;
|
|
@@ -2134,6 +2183,24 @@ async function readStructureRealizationInput(repoRoot, inputRef) {
|
|
|
2134
2183
|
});
|
|
2135
2184
|
}
|
|
2136
2185
|
|
|
2186
|
+
async function readStructureRealizedTransformInput(repoRoot, inputRef) {
|
|
2187
|
+
let explained = null;
|
|
2188
|
+
return readJsonInput(repoRoot, inputRef, {
|
|
2189
|
+
validate: (value) => {
|
|
2190
|
+
explained = explainTodoStructureTransform(value);
|
|
2191
|
+
return explained.valid;
|
|
2192
|
+
},
|
|
2193
|
+
invalidCode: 'INVALID_TODO_STRUCTURE_TRANSFORM',
|
|
2194
|
+
}).catch((error) => {
|
|
2195
|
+
if (error?.code === 'INVALID_TODO_STRUCTURE_TRANSFORM' && explained !== null) {
|
|
2196
|
+
throw new TodoStoreError(error.code, explained.reason, undefined, {
|
|
2197
|
+
input_ref: inputRef, path: explained.path,
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
throw error;
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2137
2204
|
async function readCurrentStructureEffective(repoRoot, structureSet) {
|
|
2138
2205
|
const realizations = [];
|
|
2139
2206
|
for (const task of structureSet.tasks.filter(({ applicability }) => applicability === 'graph')) {
|
|
@@ -2144,22 +2211,7 @@ async function readCurrentStructureEffective(repoRoot, structureSet) {
|
|
|
2144
2211
|
return projectTodoStructureEffective({ structureSet, realizations });
|
|
2145
2212
|
}
|
|
2146
2213
|
|
|
2147
|
-
async function
|
|
2148
|
-
const realization = await readStructureRealizationInput(repoRoot, inputRef);
|
|
2149
|
-
const actor = mutationActor(env);
|
|
2150
|
-
if (realization.plan_key !== planKey || realization.task_id !== taskId) {
|
|
2151
|
-
throw new TodoStoreError('STRUCTURE_REALIZATION_BINDING_MISMATCH',
|
|
2152
|
-
'cli_target_mismatch', undefined, {
|
|
2153
|
-
expected: { plan_key: planKey, task_id: taskId },
|
|
2154
|
-
actual: { plan_key: realization.plan_key, task_id: realization.task_id },
|
|
2155
|
-
});
|
|
2156
|
-
}
|
|
2157
|
-
if (canonicalizeTodoArtifact(realization.actor) !== canonicalizeTodoArtifact(actor)) {
|
|
2158
|
-
throw new TodoStoreError('STRUCTURE_REALIZATION_BINDING_MISMATCH',
|
|
2159
|
-
'actor_environment_mismatch', undefined, {
|
|
2160
|
-
expected_actor: actor, actual_actor: realization.actor,
|
|
2161
|
-
});
|
|
2162
|
-
}
|
|
2214
|
+
async function recordStructureRealization({ repoRoot, planKey, taskId, realization }) {
|
|
2163
2215
|
const appended = await appendTodoStructureRealization({ repoRoot, realization });
|
|
2164
2216
|
const structureSet = await readTodoStructureSource({ repoRoot, planKey });
|
|
2165
2217
|
const effective = await readCurrentStructureEffective(repoRoot, structureSet);
|
|
@@ -2180,6 +2232,103 @@ async function structureRealize({ repoRoot, env, planKey, taskId, inputRef }) {
|
|
|
2180
2232
|
return result;
|
|
2181
2233
|
}
|
|
2182
2234
|
|
|
2235
|
+
async function structureRealize({ repoRoot, env, planKey, taskId, inputRef }) {
|
|
2236
|
+
const realization = await readStructureRealizationInput(repoRoot, inputRef);
|
|
2237
|
+
const actor = mutationActor(env);
|
|
2238
|
+
if (realization.plan_key !== planKey || realization.task_id !== taskId) {
|
|
2239
|
+
throw new TodoStoreError('STRUCTURE_REALIZATION_BINDING_MISMATCH',
|
|
2240
|
+
'cli_target_mismatch', undefined, {
|
|
2241
|
+
expected: { plan_key: planKey, task_id: taskId },
|
|
2242
|
+
actual: { plan_key: realization.plan_key, task_id: realization.task_id },
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
if (canonicalizeTodoArtifact(realization.actor) !== canonicalizeTodoArtifact(actor)) {
|
|
2246
|
+
throw new TodoStoreError('STRUCTURE_REALIZATION_BINDING_MISMATCH',
|
|
2247
|
+
'actor_environment_mismatch', undefined, {
|
|
2248
|
+
expected_actor: actor, actual_actor: realization.actor,
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
return recordStructureRealization({ repoRoot, planKey, taskId, realization });
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
function resolveStructureRealizationCommits(repoRoot, refs) {
|
|
2255
|
+
const requested = refs.length === 0 ? ['HEAD'] : refs;
|
|
2256
|
+
if (requested.length > TODO_STRUCTURE_LIMITS.commitsPerRealization) {
|
|
2257
|
+
throw new TodoStoreError('STRUCTURE_REALIZATION_COMMIT_REF_INVALID',
|
|
2258
|
+
'commit_ref_count_exceeds_limit', undefined, {
|
|
2259
|
+
actual: requested.length, limit: TODO_STRUCTURE_LIMITS.commitsPerRealization,
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
const resolved = [];
|
|
2263
|
+
for (const ref of requested) {
|
|
2264
|
+
if (!(ref === 'HEAD' || /^[0-9a-f]{40}$/u.test(ref))) {
|
|
2265
|
+
throw new TodoStoreError('STRUCTURE_REALIZATION_COMMIT_REF_INVALID',
|
|
2266
|
+
'commit_ref_must_be_head_or_full_sha', undefined, { commit_ref: ref });
|
|
2267
|
+
}
|
|
2268
|
+
let oid;
|
|
2269
|
+
try {
|
|
2270
|
+
oid = gitSync(['rev-parse', '--verify', `${ref}^{commit}`], {
|
|
2271
|
+
cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
2272
|
+
}).trim();
|
|
2273
|
+
} catch {
|
|
2274
|
+
throw new TodoStoreError('STRUCTURE_REALIZATION_COMMIT_REF_INVALID',
|
|
2275
|
+
'commit_ref_unresolved', undefined, { commit_ref: ref });
|
|
2276
|
+
}
|
|
2277
|
+
if (!/^[0-9a-f]{40}$/u.test(oid)) {
|
|
2278
|
+
throw new TodoStoreError('STRUCTURE_REALIZATION_COMMIT_REF_INVALID',
|
|
2279
|
+
'commit_ref_resolved_invalid_oid', undefined, { commit_ref: ref });
|
|
2280
|
+
}
|
|
2281
|
+
if (resolved.includes(oid)) {
|
|
2282
|
+
throw new TodoStoreError('STRUCTURE_REALIZATION_COMMIT_REF_INVALID',
|
|
2283
|
+
'duplicate_commit_ref', undefined, { commit_ref: ref, commit_oid: oid });
|
|
2284
|
+
}
|
|
2285
|
+
resolved.push(oid);
|
|
2286
|
+
}
|
|
2287
|
+
return resolved.sort();
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
async function structureRealizeActual({
|
|
2291
|
+
repoRoot, env, planKey, taskId, realizedRef, usePlanned, commitRefs,
|
|
2292
|
+
}) {
|
|
2293
|
+
const structureSet = await readTodoStructureSource({ repoRoot, planKey });
|
|
2294
|
+
if (structureSet === null) {
|
|
2295
|
+
throw new TodoStoreError('STRUCTURE_REALIZATION_BINDING_MISMATCH',
|
|
2296
|
+
'planned_structure_source_missing', undefined, { plan_key: planKey });
|
|
2297
|
+
}
|
|
2298
|
+
const task = structureSet.tasks.find(({ task_id: id }) => id === taskId);
|
|
2299
|
+
if (task?.applicability !== 'graph') {
|
|
2300
|
+
throw new TodoStoreError('STRUCTURE_REALIZATION_BINDING_MISMATCH',
|
|
2301
|
+
'task_not_graph_applicable', undefined, { plan_key: planKey, task_id: taskId });
|
|
2302
|
+
}
|
|
2303
|
+
const realized = usePlanned
|
|
2304
|
+
? structuredClone(task.planned)
|
|
2305
|
+
: await readStructureRealizedTransformInput(repoRoot, realizedRef);
|
|
2306
|
+
const chain = await readTodoStructureRealizationChain({
|
|
2307
|
+
repoRoot, structureSet, taskId,
|
|
2308
|
+
});
|
|
2309
|
+
const previous = chain.at(-1) ?? null;
|
|
2310
|
+
const realization = {
|
|
2311
|
+
schema: TODO_STRUCTURE_REALIZATION_SCHEMA,
|
|
2312
|
+
project_id: structureSet.project_id,
|
|
2313
|
+
plan_key: structureSet.plan_key,
|
|
2314
|
+
plan_version: structureSet.plan_version,
|
|
2315
|
+
task_id: taskId,
|
|
2316
|
+
sequence: (previous?.sequence ?? 0) + 1,
|
|
2317
|
+
previous_digest: previous?.realization_digest ?? null,
|
|
2318
|
+
structure_set_digest: structureSet.structure_set_digest,
|
|
2319
|
+
planned_digest: digestTodoStructureTransform(task.planned),
|
|
2320
|
+
head_sha: currentHeadSha(repoRoot),
|
|
2321
|
+
commit_oids: resolveStructureRealizationCommits(repoRoot, commitRefs),
|
|
2322
|
+
realized,
|
|
2323
|
+
supersedes: previous?.realization_digest ?? null,
|
|
2324
|
+
actor: mutationActor(env),
|
|
2325
|
+
recorded_at: new Date().toISOString(),
|
|
2326
|
+
realization_digest: '',
|
|
2327
|
+
};
|
|
2328
|
+
realization.realization_digest = todoSelfDigest(realization, 'realization_digest');
|
|
2329
|
+
return recordStructureRealization({ repoRoot, planKey, taskId, realization });
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2183
2332
|
async function structureFinalize({ repoRoot, env, planKey }) {
|
|
2184
2333
|
const store = await readTodoStore({ repoRoot });
|
|
2185
2334
|
const member = structurePlanMember(store, planKey);
|
|
@@ -2215,7 +2364,7 @@ async function structureFinalize({ repoRoot, env, planKey }) {
|
|
|
2215
2364
|
throw new TodoStoreError('STRUCTURE_FINALIZATION_UNAVAILABLE',
|
|
2216
2365
|
'realization_missing', undefined, {
|
|
2217
2366
|
plan_key: planKey, task_id: task.task_id,
|
|
2218
|
-
next_action: `lattice todo structure realize --plan ${planKey} --task ${task.task_id} --
|
|
2367
|
+
next_action: `lattice todo structure realize --plan ${planKey} --task ${task.task_id} --realized <actual-structure.json>`,
|
|
2219
2368
|
});
|
|
2220
2369
|
}
|
|
2221
2370
|
realizations.push(...chain);
|
|
@@ -3479,6 +3628,14 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
3479
3628
|
});
|
|
3480
3629
|
}
|
|
3481
3630
|
|
|
3631
|
+
if (argv[0] === 'structure' && argv[1] === 'realize'
|
|
3632
|
+
&& argv[6] === '--realized' && typeof argv[7] === 'string' && path.isAbsolute(argv[7])) {
|
|
3633
|
+
return typedArgumentFailure(stderr, 'INPUT_OUTSIDE_REPOSITORY', 'absolute_input_path_rejected', {
|
|
3634
|
+
argument: '--realized', expected: 'repo-relative path', actual: 'absolute path',
|
|
3635
|
+
next_action: 'place_the_input_inside_the_repository_and_pass_a_repo_relative_path',
|
|
3636
|
+
});
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3482
3639
|
// `--schema --json`はstoreを読まない決定的な出力(`plan create --schema`と同じ規律)。
|
|
3483
3640
|
// 通常dispatchより前に処理し、repoRoot解決やdashboard daemon起動を経由させない。
|
|
3484
3641
|
if (argv.length === 3 && argv[1] === '--schema' && argv[2] === '--json'
|
|
@@ -3513,6 +3670,7 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
3513
3670
|
}
|
|
3514
3671
|
}
|
|
3515
3672
|
|
|
3673
|
+
const automatedStructureRealize = parseAutomatedStructureRealizeArgs(argv);
|
|
3516
3674
|
let action = null;
|
|
3517
3675
|
if ((argv.length === 1 && argv[0] === 'status')
|
|
3518
3676
|
|| (argv.length === 2 && argv[0] === 'status' && argv[1] === '--json')) {
|
|
@@ -3615,6 +3773,10 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
3615
3773
|
action = (repoRoot) => structureRealize({
|
|
3616
3774
|
repoRoot, env, planKey: argv[3], taskId: argv[5], inputRef: argv[7],
|
|
3617
3775
|
});
|
|
3776
|
+
} else if (automatedStructureRealize !== null) {
|
|
3777
|
+
action = (repoRoot) => structureRealizeActual({
|
|
3778
|
+
repoRoot, env, ...automatedStructureRealize,
|
|
3779
|
+
});
|
|
3618
3780
|
} else if (argv.length === 5 && argv[0] === 'structure' && argv[1] === 'finalize'
|
|
3619
3781
|
&& argv[2] === '--plan' && isTodoIdentifier(argv[3]) && argv[4] === '--json') {
|
|
3620
3782
|
action = (repoRoot) => structureFinalize({ repoRoot, env, planKey: argv[3] });
|
|
@@ -3779,12 +3941,13 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
3779
3941
|
&& argv[3] === '--task' && isTodoIdentifier(argv[4])) {
|
|
3780
3942
|
action = (repoRoot) => mutate({ repoRoot, env, planKey: argv[2], taskId: argv[4],
|
|
3781
3943
|
kind: 'unblock', payload: {}, evidenceRef: null });
|
|
3782
|
-
} else if (argv.length === 7 && argv[0] === 'done'
|
|
3944
|
+
} else if ((argv.length === 7 || argv.length === 9) && argv[0] === 'done'
|
|
3783
3945
|
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
3784
3946
|
&& argv[3] === '--task' && isTodoIdentifier(argv[4])
|
|
3785
|
-
&& argv[5] === '--evidence' && isTodoRef(argv[6])
|
|
3947
|
+
&& argv[5] === '--evidence' && isTodoRef(argv[6])
|
|
3948
|
+
&& (argv.length === 7 || (argv[7] === '--test-result' && isTodoRef(argv[8])))) {
|
|
3786
3949
|
action = (repoRoot) => mutate({ repoRoot, env, planKey: argv[2], taskId: argv[4],
|
|
3787
|
-
kind: 'done', payload: 'authored', evidenceRef: argv[6] });
|
|
3950
|
+
kind: 'done', payload: 'authored', evidenceRef: argv[6], testResultRef: argv[8] ?? null });
|
|
3788
3951
|
} else if (argv.length === 8 && argv[0] === 'evidence' && argv[1] === 'promote'
|
|
3789
3952
|
&& argv[2] === '--plan' && isTodoIdentifier(argv[3])
|
|
3790
3953
|
&& argv[4] === '--task' && isTodoIdentifier(argv[5])
|
package/src/todo-contracts.mjs
CHANGED
|
@@ -62,10 +62,14 @@ const CONTROL = /[\u0000-\u001f\u007f]/u;
|
|
|
62
62
|
const NOTE_FORBIDDEN_CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u;
|
|
63
63
|
|
|
64
64
|
export const TODO_DESIGN_MEMO_PROMPT = 'あなたがこのToDoに対して、何も考えていないならば、設計メモに `NO_PLAN` と書いてください';
|
|
65
|
+
export const TODO_TEST_RESULT_CONTRACT_ID = 'lattice.todo_test_result.v1';
|
|
65
66
|
|
|
66
67
|
export const isTodoDigest = (value) => typeof value === 'string' && DIGEST.test(value);
|
|
67
68
|
export const isTodoIdentifier = (value) => typeof value === 'string' && IDENTIFIER.test(value);
|
|
68
69
|
export const isNonNegativeSafeInteger = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
70
|
+
export const isTodoTestResult = (value) => typeof value === 'string' && value.trim().length > 0
|
|
71
|
+
&& Buffer.byteLength(value, 'utf8') <= TODO_LIMITS.noteBodyBytes
|
|
72
|
+
&& !NOTE_FORBIDDEN_CONTROL.test(value);
|
|
69
73
|
|
|
70
74
|
export function isStrictTodoTimestamp(value) {
|
|
71
75
|
return isCanonicalUtcTimestamp(value);
|
|
@@ -527,8 +531,10 @@ function validPayload(event) {
|
|
|
527
531
|
if (event.kind === 'block') return exactRecord(payload, ['reason']) && nullableText(payload.reason) && payload.reason !== null;
|
|
528
532
|
if (event.kind === 'unblock') return exactRecord(payload, []);
|
|
529
533
|
if (event.kind === 'done' && payload?.done_mode === 'authored') {
|
|
530
|
-
return exactRecord(payload, ['done_mode', 'imported', 'evidence'])
|
|
531
|
-
|
|
534
|
+
return (exactRecord(payload, ['done_mode', 'imported', 'evidence'])
|
|
535
|
+
|| exactRecord(payload, ['done_mode', 'imported', 'evidence', 'test_result']))
|
|
536
|
+
&& payload.imported === false && evidence(payload.evidence)
|
|
537
|
+
&& (payload.test_result === undefined || isTodoTestResult(payload.test_result));
|
|
532
538
|
}
|
|
533
539
|
if (event.kind === 'done' && payload?.done_mode === 'historical_import') {
|
|
534
540
|
return exactRecord(payload, ['done_mode', 'imported', 'status', 'completed_at', 'evidence'])
|
|
@@ -569,22 +575,29 @@ function validPayload(event) {
|
|
|
569
575
|
}
|
|
570
576
|
|
|
571
577
|
function validCarriedState(value) {
|
|
572
|
-
|
|
578
|
+
const legacy = exactRecord(value, [
|
|
573
579
|
'status', 'started_at', 'done_at', 'blocked_reason', 'evidence', 'imported',
|
|
574
|
-
])
|
|
580
|
+
]);
|
|
581
|
+
const resultAware = exactRecord(value, [
|
|
582
|
+
'status', 'started_at', 'done_at', 'blocked_reason', 'evidence', 'imported', 'test_result',
|
|
583
|
+
]);
|
|
584
|
+
if ((!legacy && !resultAware) || !['pending', 'in-progress', 'blocked', 'done'].includes(value.status)
|
|
575
585
|
|| (value.started_at !== null && !isStrictTodoTimestamp(value.started_at))
|
|
576
586
|
|| (value.done_at !== null && !isStrictTodoTimestamp(value.done_at))
|
|
577
587
|
|| (value.blocked_reason !== null && !nullableText(value.blocked_reason))
|
|
578
588
|
|| typeof value.imported !== 'boolean') return false;
|
|
589
|
+
const testResult = resultAware ? value.test_result : null;
|
|
590
|
+
if (testResult !== null && !isTodoTestResult(testResult)) return false;
|
|
579
591
|
if (value.status === 'pending') return value.started_at === null && value.done_at === null
|
|
580
|
-
&& value.blocked_reason === null && value.evidence === null && value.imported === false
|
|
592
|
+
&& value.blocked_reason === null && value.evidence === null && value.imported === false
|
|
593
|
+
&& testResult === null;
|
|
581
594
|
const activeEvidenceValid = value.imported
|
|
582
595
|
? value.evidence === null || validateTodoImportSource(value.evidence)
|
|
583
596
|
: value.evidence === null;
|
|
584
597
|
if (value.status === 'in-progress') return value.done_at === null && value.blocked_reason === null
|
|
585
|
-
&& activeEvidenceValid;
|
|
598
|
+
&& activeEvidenceValid && testResult === null;
|
|
586
599
|
if (value.status === 'blocked') return value.done_at === null && value.blocked_reason !== null
|
|
587
|
-
&& activeEvidenceValid;
|
|
600
|
+
&& activeEvidenceValid && testResult === null;
|
|
588
601
|
return value.blocked_reason === null && value.evidence !== null
|
|
589
602
|
&& (value.imported ? validateTodoImportSource(value.evidence) : evidence(value.evidence));
|
|
590
603
|
}
|
|
@@ -700,20 +713,35 @@ export function validateTodoSnapshot(value) {
|
|
|
700
713
|
'schema', 'project_id', 'plan_key', 'plan_version', 'projection_version', 'through_sequence',
|
|
701
714
|
'journal_head_digest', 'tasks', 'phases', 'snapshot_digest',
|
|
702
715
|
]);
|
|
703
|
-
|
|
716
|
+
const v3 = value?.schema === 'lattice.todo_snapshot.v3' && exactRecord(value, [
|
|
717
|
+
'schema', 'project_id', 'plan_key', 'plan_version', 'projection_version', 'through_sequence',
|
|
718
|
+
'journal_head_digest', 'tasks', 'snapshot_digest',
|
|
719
|
+
]);
|
|
720
|
+
const v4 = value?.schema === 'lattice.todo_snapshot.v4' && exactRecord(value, [
|
|
721
|
+
'schema', 'project_id', 'plan_key', 'plan_version', 'projection_version', 'through_sequence',
|
|
722
|
+
'journal_head_digest', 'tasks', 'phases', 'snapshot_digest',
|
|
723
|
+
]);
|
|
724
|
+
const resultAware = v3 || v4;
|
|
725
|
+
const phaseAware = v2 || v4;
|
|
726
|
+
return (v1 || v2 || v3 || v4) && isTodoIdentifier(value.project_id)
|
|
704
727
|
&& isTodoIdentifier(value.plan_key) && isTodoIdentifier(value.plan_version)
|
|
705
|
-
&& value.projection_version === (v1 ? 1 : 2
|
|
728
|
+
&& value.projection_version === (v1 ? 1 : v2 ? 2 : v3 ? 3 : 4)
|
|
729
|
+
&& isNonNegativeSafeInteger(value.through_sequence)
|
|
706
730
|
&& isTodoDigest(value.journal_head_digest) && Array.isArray(value.tasks)
|
|
707
731
|
&& value.tasks.length > 0 && value.tasks.length <= TODO_LIMITS.tasksPerPlan
|
|
708
732
|
&& value.tasks.every((entry) => exactRecord(entry, [
|
|
709
733
|
'task_id', 'status', 'started_at', 'done_at', 'blocked_reason', 'evidence', 'evidence_unverified', 'imported',
|
|
734
|
+
...(resultAware ? ['test_result'] : []),
|
|
710
735
|
]) && isTodoIdentifier(entry.task_id) && ['pending', 'in-progress', 'blocked', 'done'].includes(entry.status)
|
|
711
736
|
&& (entry.started_at === null || isStrictTodoTimestamp(entry.started_at))
|
|
712
737
|
&& (entry.done_at === null || isStrictTodoTimestamp(entry.done_at)) && nullableText(entry.blocked_reason)
|
|
713
738
|
&& (entry.evidence === null || evidence(entry.evidence) || validateTodoImportSource(entry.evidence))
|
|
714
|
-
&& typeof entry.evidence_unverified === 'boolean' && typeof entry.imported === 'boolean'
|
|
739
|
+
&& typeof entry.evidence_unverified === 'boolean' && typeof entry.imported === 'boolean'
|
|
740
|
+
&& (!resultAware || (entry.status === 'done'
|
|
741
|
+
? entry.test_result === null || isTodoTestResult(entry.test_result)
|
|
742
|
+
: entry.test_result === null)))
|
|
715
743
|
&& value.tasks.every((entry, index) => index === 0 || value.tasks[index - 1].task_id < entry.task_id)
|
|
716
|
-
&& (!
|
|
744
|
+
&& (!phaseAware || (Array.isArray(value.phases) && value.phases.length > 0
|
|
717
745
|
&& value.phases.every((entry) => exactRecord(entry, [
|
|
718
746
|
'phase_id', 'status', 'review_event_digest', 'decision_event_digest', 'decision_evidence',
|
|
719
747
|
]) && isTodoIdentifier(entry.phase_id)
|
package/src/todo-store.mjs
CHANGED
|
@@ -302,7 +302,7 @@ async function readPlanScopedJournal(repoRoot, journalRef) {
|
|
|
302
302
|
|
|
303
303
|
function taskState(taskId) {
|
|
304
304
|
return { task_id: taskId, status: 'pending', started_at: null, done_at: null, blocked_reason: null,
|
|
305
|
-
evidence: null, evidence_unverified: false, imported: false };
|
|
305
|
+
evidence: null, evidence_unverified: false, imported: false, test_result: null };
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
function emptyPhaseState(phaseId) {
|
|
@@ -685,6 +685,7 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
|
|
|
685
685
|
plan_key: plan.plan_key, task_id: event.task_id,
|
|
686
686
|
});
|
|
687
687
|
state.status = 'done'; state.done_at = event.recorded_at; state.evidence = event.payload.evidence;
|
|
688
|
+
state.test_result = event.payload.test_result ?? null;
|
|
688
689
|
state.imported = false;
|
|
689
690
|
completion.set(event.task_id, { mode: 'authored', completed_at: event.recorded_at });
|
|
690
691
|
} else if (event.payload.done_mode === 'historical_import') {
|
|
@@ -731,7 +732,7 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
|
|
|
731
732
|
}
|
|
732
733
|
const startedSuccessor = localSuccessors(plan, event.task_id).some((id) => states.get(id).status !== 'pending');
|
|
733
734
|
if (startedSuccessor && event.payload.override_reason === null) fail('STORE_INCONSISTENT', 'reopen_has_started_successor');
|
|
734
|
-
state.status = 'in-progress'; state.done_at = null; state.evidence = null;
|
|
735
|
+
state.status = 'in-progress'; state.done_at = null; state.evidence = null; state.test_result = null;
|
|
735
736
|
completion.delete(event.task_id);
|
|
736
737
|
}
|
|
737
738
|
}
|
|
@@ -774,11 +775,21 @@ function snapshotFor(plan, events, tasks) {
|
|
|
774
775
|
// ——ADR 0147以降の暗黙terminal-audit Phaseの状態は、この関数の外(readTodoStore/
|
|
775
776
|
// appendTodoEventが返す`phases`という導出ビュー)で供給する。
|
|
776
777
|
const phasePlan = isPhaseTodoPlanSchema(plan.schema);
|
|
778
|
+
const resultAware = events.some((event) => event.kind === 'done'
|
|
779
|
+
&& typeof event.payload?.test_result === 'string')
|
|
780
|
+
|| events.some((event) => event.kind === 'plan_genesis'
|
|
781
|
+
&& event.state_migration?.some(({ state }) => typeof state?.test_result === 'string'));
|
|
782
|
+
const snapshotTasks = resultAware ? tasks.map((task) => ({ ...task, test_result: task.test_result ?? null }))
|
|
783
|
+
: tasks.map(({ test_result: _testResult, ...task }) => task);
|
|
777
784
|
const snapshot = {
|
|
778
|
-
schema:
|
|
785
|
+
schema: resultAware
|
|
786
|
+
? (phasePlan ? 'lattice.todo_snapshot.v4' : 'lattice.todo_snapshot.v3')
|
|
787
|
+
: (phasePlan ? 'lattice.todo_snapshot.v2' : 'lattice.todo_snapshot.v1'),
|
|
779
788
|
project_id: plan.project_id, plan_key: plan.plan_key,
|
|
780
|
-
plan_version: plan.plan_version,
|
|
781
|
-
|
|
789
|
+
plan_version: plan.plan_version,
|
|
790
|
+
projection_version: resultAware ? (phasePlan ? 4 : 3) : (phasePlan ? 2 : 1),
|
|
791
|
+
through_sequence: head.sequence,
|
|
792
|
+
journal_head_digest: head.event_digest, tasks: snapshotTasks, snapshot_digest: '',
|
|
782
793
|
...(phasePlan ? { phases: projectPhaseStates(plan, events,
|
|
783
794
|
new Map(tasks.map((task) => [task.task_id, task]))) } : {}),
|
|
784
795
|
};
|
|
@@ -1515,9 +1526,14 @@ export async function rebuildTodoSnapshot(options = {}) {
|
|
|
1515
1526
|
|
|
1516
1527
|
function nextEvent(input, storeMember) {
|
|
1517
1528
|
const previous = storeMember.journal.events.at(-1);
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1529
|
+
let payload = input.payload;
|
|
1530
|
+
if (input.kind === 'done' && (exactRecord(input.payload, ['evidence'])
|
|
1531
|
+
|| exactRecord(input.payload, ['evidence', 'test_result']))) {
|
|
1532
|
+
payload = {
|
|
1533
|
+
done_mode: 'authored', imported: false, evidence: input.payload.evidence,
|
|
1534
|
+
...(input.payload.test_result === undefined ? {} : { test_result: input.payload.test_result }),
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1521
1537
|
const phaseCapablePlan = isPhaseTodoPlanSchema(storeMember.plan.schema);
|
|
1522
1538
|
const phaseKind = ['phase_review', 'phase_accept', 'phase_reject', 'phase_reopen', 'phase_close_unaudited']
|
|
1523
1539
|
.includes(input.kind);
|
|
@@ -1640,7 +1656,7 @@ async function enforceTodoStructureLifecycleGate(repoRoot, member, eventInput) {
|
|
|
1640
1656
|
if (latest === undefined) {
|
|
1641
1657
|
fail('STRUCTURE_REALIZATION_REQUIRED', 'fresh_realization_missing', {
|
|
1642
1658
|
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} --
|
|
1659
|
+
next_action: `lattice todo structure realize --plan ${member.plan.plan_key} --task ${eventInput.task_id} --realized <actual-structure.json>`,
|
|
1644
1660
|
});
|
|
1645
1661
|
}
|
|
1646
1662
|
let currentHead;
|
|
@@ -1655,7 +1671,7 @@ async function enforceTodoStructureLifecycleGate(repoRoot, member, eventInput) {
|
|
|
1655
1671
|
fail('STRUCTURE_REALIZATION_REQUIRED', 'realization_head_stale', {
|
|
1656
1672
|
plan_key: member.plan.plan_key, task_id: eventInput.task_id,
|
|
1657
1673
|
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} --
|
|
1674
|
+
next_action: `lattice todo structure realize --plan ${member.plan.plan_key} --task ${eventInput.task_id} --realized <actual-structure.json>`,
|
|
1659
1675
|
});
|
|
1660
1676
|
}
|
|
1661
1677
|
}
|
|
@@ -2613,6 +2629,7 @@ function stateMigrationFor(previous, revision) {
|
|
|
2613
2629
|
return { ...migration, state: {
|
|
2614
2630
|
status: state.status, started_at: state.started_at, done_at: state.done_at,
|
|
2615
2631
|
blocked_reason: state.blocked_reason, evidence: state.evidence, imported: state.imported,
|
|
2632
|
+
...(typeof state.test_result === 'string' ? { test_result: state.test_result } : {}),
|
|
2616
2633
|
} };
|
|
2617
2634
|
});
|
|
2618
2635
|
}
|
|
@@ -467,6 +467,11 @@ export const digestTodoStructureTransform = (value) => todoSelfDigest(
|
|
|
467
467
|
{ schema: 'lattice.todo_structure_transform.v1', transform: value, digest: '' }, 'digest',
|
|
468
468
|
);
|
|
469
469
|
|
|
470
|
+
/** AIが判断して渡す実体構造だけを、realization envelopeとは独立に検証する。 */
|
|
471
|
+
export function explainTodoStructureTransform(value) {
|
|
472
|
+
return transform(value, '');
|
|
473
|
+
}
|
|
474
|
+
|
|
470
475
|
export function explainTodoStructureRealization(value, { structureSet = null, previous = null,
|
|
471
476
|
priorDigests = null } = {}) {
|
|
472
477
|
try {
|