@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.58.3",
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
- # 実装後の構造をappend-onlyで記録してからtodo doneへ進む
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 <file> | finalize --plan <key> --json',
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') eventPayload = { evidence };
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
- ? [`lattice todo structure realize --plan ${planKey} --task ${taskId} --input <realization.json>`]
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.v2',
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> --input <realization.json>`);
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> --input <realization.json>`);
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 structureRealize({ repoRoot, env, planKey, taskId, inputRef }) {
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} --input <realization.json>`,
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])
@@ -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
- && payload.imported === false && evidence(payload.evidence);
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
- if (!exactRecord(value, [
578
+ const legacy = exactRecord(value, [
573
579
  'status', 'started_at', 'done_at', 'blocked_reason', 'evidence', 'imported',
574
- ]) || !['pending', 'in-progress', 'blocked', 'done'].includes(value.status)
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
- return (v1 || v2) && isTodoIdentifier(value.project_id)
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) && isNonNegativeSafeInteger(value.through_sequence)
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
- && (!v2 || (Array.isArray(value.phases) && value.phases.length > 0
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)
@@ -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: phasePlan ? 'lattice.todo_snapshot.v2' : 'lattice.todo_snapshot.v1',
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, projection_version: phasePlan ? 2 : 1, through_sequence: head.sequence,
781
- journal_head_digest: head.event_digest, tasks, snapshot_digest: '',
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
- const payload = input.kind === 'done' && exactRecord(input.payload, ['evidence'])
1519
- ? { done_mode: 'authored', imported: false, evidence: input.payload.evidence }
1520
- : input.payload;
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} --input <realization.json>`,
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} --input <realization.json>`,
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 {