@quolu/lattice 0.57.2 → 0.58.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/src/todo-cli.mjs CHANGED
@@ -31,6 +31,7 @@ import { readProjectExternalPane, resolveProjectIdentity } from './project-ident
31
31
  import { layoutTodoGantt } from './todo-gantt-layout.mjs';
32
32
  import { TODO_GANTT_SCOPES } from './todo-gantt-scope.mjs';
33
33
  import { loadTodoGanttPresentation } from './todo-gantt-presentation.mjs';
34
+ import { loadTodoStructurePresentation } from './todo-structure-presentation.mjs';
34
35
  import { startTodoGanttLiveServer } from './todo-gantt-live.mjs';
35
36
  import {
36
37
  renderTodoGanttHtml,
@@ -39,6 +40,7 @@ import {
39
40
  import { verifyNarrativeAnchors } from './todo-narrative-anchor.mjs';
40
41
  import {
41
42
  appendTodoEvent,
43
+ appendTodoStructureRealization,
42
44
  applyPhaseTodoRevision,
43
45
  applyTodoRevision,
44
46
  applyTodoRevisionSet,
@@ -51,6 +53,10 @@ import {
51
53
  readTodoIndependenceArtifact,
52
54
  readTodoSeamProposalArtifact,
53
55
  readTodoStore,
56
+ readTodoStructureBinding,
57
+ readTodoStructureRealizationChain,
58
+ readTodoStructureSource,
59
+ todoStructureSourceRef,
54
60
  resolveTodoStartRetractionBinding,
55
61
  readTodoWitnessSet,
56
62
  todoWitnessRef,
@@ -59,9 +65,28 @@ import {
59
65
  rebuildTodoSnapshot,
60
66
  writeTodoIndependenceArtifact,
61
67
  writeTodoSeamProposalArtifact,
68
+ writeTodoStructureBinding,
69
+ writeTodoStructureCompileArtifact,
70
+ writeTodoStructureFinalization,
71
+ writeTodoStructureSource,
62
72
  verifyEffectivePhaseTodoRevisionSources,
63
73
  verifyTodoRevisionSources,
64
74
  } from './todo-store.mjs';
75
+ import {
76
+ TODO_STRUCTURE_BINDING_SCHEMA,
77
+ explainTodoStructureRealization,
78
+ explainTodoStructureSet,
79
+ } from './todo-structure-contracts.mjs';
80
+ import { collectTodoStructureSourceEvidence } from './todo-structure-source-adapter.mjs';
81
+ import { collectTodoStructureGitProvenance } from './todo-structure-git-adapter.mjs';
82
+ import { compileTodoStructureOverlay } from './todo-structure-overlay.mjs';
83
+ import {
84
+ buildTodoStructureCompileArtifact,
85
+ projectTodoStructureEffective,
86
+ readTodoStructureFinalizationState,
87
+ readTodoStructureFinalizationsForStatus,
88
+ readTodoStructureState,
89
+ } from './todo-structure-store.mjs';
65
90
  import { withStartRetractionGuard } from './runtime-pull-intake.mjs';
66
91
  import {
67
92
  appendTodoExtraction,
@@ -159,6 +184,9 @@ const TODO_SCHEMA_COMMANDS = Object.freeze({
159
184
  title: 'lattice.phase_todo_revision.v3', file: 'lattice.phase_todo_revision.v3.schema.json',
160
185
  },
161
186
  migrate: { title: 'lattice.todo_extraction.v3', file: 'lattice.todo_extraction.v3.schema.json' },
187
+ structure: {
188
+ title: 'lattice.todo_structure_set.v1', file: 'lattice.todo_structure_set.v1.schema.json',
189
+ },
162
190
  });
163
191
 
164
192
  async function runTodoSchemaCommand(command, stdout) {
@@ -207,7 +235,7 @@ function internalFailure(stderr, error) {
207
235
  }
208
236
 
209
237
  const TODO_COMMAND_NAMES = Object.freeze([
210
- 'status', 'show', 'note', 'bindings', 'independence', 'seam-profile', 'seam-proposal',
238
+ 'status', 'show', 'note', 'bindings', 'independence', 'structure', 'seam-profile', 'seam-proposal',
211
239
  'verify', 'snapshot', 'gantt', 'dashboard', 'phase', 'migrate', 'start', 'block',
212
240
  'unblock', 'done', 'reopen', 'evidence', 'split', 'revise', 'revise-phase', 'revise-set',
213
241
  ]);
@@ -553,6 +581,14 @@ async function readWitnessSetInput(repoRoot, inputRef) {
553
581
  return witnessSet;
554
582
  }
555
583
 
584
+ /** structure draftはpointer診断のためcontract違反でもparseし、判定はdry-runへ返す。 */
585
+ async function readStructureSetDraft(repoRoot, inputRef) {
586
+ return readJsonInput(repoRoot, inputRef, {
587
+ validate: () => true,
588
+ invalidCode: 'INVALID_TODO_STRUCTURE_SET',
589
+ });
590
+ }
591
+
556
592
  function mutationActor(env) {
557
593
  const entries = ACTOR_ENV_KEYS.map((key) => ({ key, value: env[key] }));
558
594
  const missingEnvironment = entries
@@ -1016,6 +1052,9 @@ async function dependencyConnect({
1016
1052
  async function phaseStatus({ repoRoot, planKey }) {
1017
1053
  const store = await readTodoStore({ repoRoot });
1018
1054
  const [member] = selectMembers(store, planKey);
1055
+ const structureFinalization = await readTodoStructureFinalizationState({
1056
+ repoRoot, store, planKey: member.plan.plan_key,
1057
+ });
1019
1058
  // ADR 0147以降、phase無しplan(v1/v2/v3)もreadTodoStoreが導出済みの暗黙terminal-audit
1020
1059
  // Phaseをmember.phasesへ積んでいる(snapshot artifactの形式は変えない・v1にはphasesキーが
1021
1060
  // 無いのでsnapshot.phasesは直接読まない)。ここでPHASE_UNAVAILABLEへ拒否せず、その暗黙Phase
@@ -1027,13 +1066,26 @@ async function phaseStatus({ repoRoot, planKey }) {
1027
1066
  const phases = member.phases.map((phase) => ({
1028
1067
  ...phase,
1029
1068
  guidance: phase.status === 'gate_ready'
1030
- ? auditGateGuidance(member.plan.plan_key, phase.phase_id) : null,
1069
+ ? [
1070
+ auditGateGuidance(member.plan.plan_key, phase.phase_id),
1071
+ ...(structureFinalization.required && structureFinalization.status !== 'fresh'
1072
+ ? [`構造finalizationが${structureFinalization.status}です。先に lattice todo structure finalize --plan ${member.plan.plan_key} --json を実行してください。`]
1073
+ : []),
1074
+ ].join('\n') : null,
1031
1075
  }));
1032
1076
  const result = {
1033
- schema: 'lattice.phase_status_result.v1', project_id: store.project_id,
1077
+ schema: 'lattice.phase_status_result.v2', project_id: store.project_id,
1034
1078
  plan_key: member.plan.plan_key, plan_version: member.plan.plan_version,
1035
1079
  journal_head_digest: member.journal.events.at(-1).event_digest,
1036
- implicit, phases, result_digest: '',
1080
+ implicit, phases,
1081
+ structure_finalization: {
1082
+ enabled: structureFinalization.enabled, required: structureFinalization.required,
1083
+ status: structureFinalization.status, reason: structureFinalization.reason,
1084
+ stale_reasons: structureFinalization.stale_reasons,
1085
+ next_command: structureFinalization.required && structureFinalization.status !== 'fresh'
1086
+ ? `lattice todo structure finalize --plan ${member.plan.plan_key} --json` : null,
1087
+ },
1088
+ result_digest: '',
1037
1089
  };
1038
1090
  result.result_digest = todoSelfDigest(result, 'result_digest');
1039
1091
  return result;
@@ -1544,6 +1596,7 @@ async function status({ repoRoot }) {
1544
1596
  parallelCandidates: await readTodoParallelCandidatesForStatus({
1545
1597
  repoRoot, store, gitHead: currentHeadSha, changedPathsSince,
1546
1598
  }),
1599
+ structureFinalizations: await readTodoStructureFinalizationsForStatus({ repoRoot, store }),
1547
1600
  });
1548
1601
  }
1549
1602
 
@@ -1737,6 +1790,498 @@ function requireCleanWorktree(repoRoot) {
1737
1790
  }
1738
1791
  }
1739
1792
 
1793
+ function structureGitIdentity(repoRoot, baselineSha, violations) {
1794
+ let currentHeadSha = null;
1795
+ try {
1796
+ currentHeadSha = gitSync(['rev-parse', 'HEAD'], {
1797
+ cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
1798
+ }).replace(/\r?\n$/u, '');
1799
+ } catch {
1800
+ violations.push({
1801
+ code: 'git_head_unresolved', path: '/baseline_sha',
1802
+ expected: 'current repository HEAD commit', actual: null,
1803
+ next_action: 'repair_git_head_then_rerun_structure_input_dry_run',
1804
+ });
1805
+ return { currentHeadSha: null };
1806
+ }
1807
+ if (!/^[0-9a-f]{40}$/u.test(currentHeadSha)) {
1808
+ violations.push({
1809
+ code: 'git_head_invalid', path: '/baseline_sha',
1810
+ expected: '40 lowercase hex commit OID', actual: currentHeadSha,
1811
+ next_action: 'repair_git_head_then_rerun_structure_input_dry_run',
1812
+ });
1813
+ return { currentHeadSha: null };
1814
+ }
1815
+ try {
1816
+ gitSync(['cat-file', '-e', `${baselineSha}^{commit}`], {
1817
+ cwd: repoRoot, stdio: ['ignore', 'ignore', 'ignore'],
1818
+ });
1819
+ } catch {
1820
+ violations.push({
1821
+ code: 'baseline_commit_unresolved', path: '/baseline_sha',
1822
+ expected: 'commit object reachable in this repository', actual: baselineSha,
1823
+ next_action: 'fetch_the_baseline_or_correct_baseline_sha',
1824
+ });
1825
+ return { currentHeadSha };
1826
+ }
1827
+ try {
1828
+ gitSync(['merge-base', '--is-ancestor', baselineSha, currentHeadSha], {
1829
+ cwd: repoRoot, stdio: ['ignore', 'ignore', 'ignore'],
1830
+ });
1831
+ } catch {
1832
+ violations.push({
1833
+ code: 'baseline_not_ancestor', path: '/baseline_sha',
1834
+ expected: { ancestor_of: currentHeadSha }, actual: baselineSha,
1835
+ next_action: 'select_an_ancestor_baseline_or_rebase_the_structure_input',
1836
+ });
1837
+ }
1838
+ return { currentHeadSha };
1839
+ }
1840
+
1841
+ async function analyzeStructureInput({ repoRoot, planKey, inputRef }) {
1842
+ const structureSet = await readStructureSetDraft(repoRoot, inputRef);
1843
+ const violations = [];
1844
+ const explained = explainTodoStructureSet(structureSet);
1845
+ if (!explained.valid) {
1846
+ violations.push({
1847
+ code: explained.reason,
1848
+ path: explained.path,
1849
+ ...(explained.detail === undefined ? {} : { actual: explained.detail }),
1850
+ next_action: 'correct_the_reported_pointer_then_rerun_structure_input_dry_run',
1851
+ });
1852
+ }
1853
+
1854
+ const store = await readTodoStore({ repoRoot });
1855
+ const member = store.members.find(({ descriptor }) => descriptor.plan_key === planKey) ?? null;
1856
+ if (member === null) {
1857
+ violations.push({
1858
+ code: 'plan_not_active', path: '/plan_key', expected: planKey,
1859
+ actual: typeof structureSet?.plan_key === 'string' ? structureSet.plan_key : null,
1860
+ next_action: 'define_the_plan_before_adding_structure_input',
1861
+ });
1862
+ }
1863
+
1864
+ let currentHeadSha = null;
1865
+ if (explained.valid && member !== null) {
1866
+ const identities = [
1867
+ ['project_id', store.project_id, structureSet.project_id],
1868
+ ['plan_key', planKey, structureSet.plan_key],
1869
+ ['plan_version', member.plan.plan_version, structureSet.plan_version],
1870
+ ['topology_digest', member.plan.topology_digest, structureSet.topology_digest],
1871
+ ];
1872
+ for (const [field, expected, actual] of identities) {
1873
+ if (actual !== expected) {
1874
+ violations.push({
1875
+ code: `${field}_mismatch`, path: `/${field}`, expected, actual,
1876
+ next_action: 'regenerate_structure_input_for_the_active_plan_identity',
1877
+ });
1878
+ }
1879
+ }
1880
+ const expectedTaskIds = member.tasks.filter(({ status }) => status !== 'done')
1881
+ .map(({ task_id: taskId }) => taskId);
1882
+ const coverage = explainTodoStructureSet(structureSet, { expectedTaskIds });
1883
+ if (!coverage.valid && ['coverage_missing', 'coverage_extra'].includes(coverage.reason)) {
1884
+ violations.push({
1885
+ code: coverage.reason, path: coverage.path,
1886
+ expected: { task_ids: [...expectedTaskIds].sort() },
1887
+ actual: coverage.detail,
1888
+ next_action: 'list_every_unfinished_task_as_graph_or_excluded',
1889
+ });
1890
+ }
1891
+ ({ currentHeadSha } = structureGitIdentity(repoRoot, structureSet.baseline_sha, violations));
1892
+ }
1893
+
1894
+ const bounded = violations.slice(0, 64);
1895
+ const result = {
1896
+ schema: 'lattice.todo_structure_input_dry_run_result.v1',
1897
+ valid: bounded.length === 0,
1898
+ project_id: member?.plan.project_id ?? null,
1899
+ plan_key: member?.plan.plan_key ?? planKey,
1900
+ plan_version: member?.plan.plan_version ?? null,
1901
+ input_ref: inputRef,
1902
+ source_ref: todoStructureSourceRef(planKey),
1903
+ current_head_sha: currentHeadSha,
1904
+ violations: bounded,
1905
+ overflow_count: Math.max(0, violations.length - bounded.length),
1906
+ next_action: bounded.length === 0
1907
+ ? `lattice todo structure input --plan ${planKey} --input ${inputRef}`
1908
+ : 'correct_the_reported_violations_then_rerun_structure_input_dry_run',
1909
+ result_digest: '',
1910
+ };
1911
+ result.result_digest = todoSelfDigest(result, 'result_digest');
1912
+ return { result, structureSet };
1913
+ }
1914
+
1915
+ async function structureInputDryRun({ repoRoot, planKey, inputRef }) {
1916
+ return (await analyzeStructureInput({ repoRoot, planKey, inputRef })).result;
1917
+ }
1918
+
1919
+ async function structureInput({ repoRoot, planKey, inputRef }) {
1920
+ const analyzed = await analyzeStructureInput({ repoRoot, planKey, inputRef });
1921
+ if (!analyzed.result.valid) {
1922
+ throw new TodoStoreError('INVALID_TODO_STRUCTURE_SET', 'structure_input_dry_run_failed', undefined, {
1923
+ violations: analyzed.result.violations,
1924
+ overflow_count: analyzed.result.overflow_count,
1925
+ next_action: `lattice todo structure input --plan ${planKey} --input ${inputRef} --dry-run --json`,
1926
+ });
1927
+ }
1928
+ const written = await writeTodoStructureSource({ repoRoot, structureSet: analyzed.structureSet });
1929
+ const result = {
1930
+ schema: 'lattice.todo_structure_input_result.v1',
1931
+ project_id: analyzed.structureSet.project_id,
1932
+ plan_key: analyzed.structureSet.plan_key,
1933
+ plan_version: analyzed.structureSet.plan_version,
1934
+ topology_digest: analyzed.structureSet.topology_digest,
1935
+ baseline_sha: analyzed.structureSet.baseline_sha,
1936
+ structure_set_digest: analyzed.structureSet.structure_set_digest,
1937
+ source_ref: written.ref,
1938
+ enabled: false,
1939
+ next_action: `commit ${written.ref}, then run lattice todo structure compile --plan ${planKey} --input ${written.ref}`,
1940
+ result_digest: '',
1941
+ };
1942
+ result.result_digest = todoSelfDigest(result, 'result_digest');
1943
+ return result;
1944
+ }
1945
+
1946
+ function structurePlanMember(store, requestedPlanKey) {
1947
+ if (requestedPlanKey !== null) {
1948
+ const member = store.members.find(({ plan }) => plan.plan_key === requestedPlanKey);
1949
+ if (member === undefined) {
1950
+ throw new TodoStoreError('STRUCTURE_PLAN_NOT_FOUND', 'plan_not_active', undefined, {
1951
+ plan_key: requestedPlanKey,
1952
+ });
1953
+ }
1954
+ return member;
1955
+ }
1956
+ if (store.members.length !== 1) {
1957
+ throw new TodoStoreError('STRUCTURE_PLAN_AMBIGUOUS', 'plan_selection_ambiguous', undefined, {
1958
+ plan_keys: store.members.map(({ plan }) => plan.plan_key).sort(),
1959
+ next_action: 'rerun_with_plan_flag',
1960
+ });
1961
+ }
1962
+ return store.members[0];
1963
+ }
1964
+
1965
+ function structureNextActions({ coverage, planKey, findings = [], staleReasons = [] }) {
1966
+ const actions = [];
1967
+ const add = (value) => { if (!actions.includes(value)) actions.push(value); };
1968
+ if (coverage === 'missing') {
1969
+ add(`lattice todo structure input --plan ${planKey} --input <structure-set.json> --dry-run --json`);
1970
+ add(`lattice todo structure input --plan ${planKey} --input <structure-set.json>`);
1971
+ add(`lattice todo structure compile --plan ${planKey} --input .lattice/todo/structure/${planKey}.json`);
1972
+ } else if (['inconsistent', 'unknown'].includes(coverage)) {
1973
+ findings.forEach(({ next_action: nextAction }) => add(nextAction));
1974
+ add(`lattice todo structure input --plan ${planKey} --input <corrected-structure-set.json> --dry-run --json`);
1975
+ add(`lattice todo structure compile --plan ${planKey} --input .lattice/todo/structure/${planKey}.json`);
1976
+ } else if (coverage === 'stale') {
1977
+ if (staleReasons.includes('realization_head_digest')) {
1978
+ add(`lattice todo structure --plan ${planKey} --json`);
1979
+ } else {
1980
+ add(`lattice todo structure realize --plan ${planKey} --task <task-id> --input <realization.json>`);
1981
+ }
1982
+ add(`lattice todo structure finalize --plan ${planKey} --json`);
1983
+ } else if (coverage === 'superseded') {
1984
+ add(`lattice todo structure input --plan ${planKey} --input <migrated-structure-set.json> --dry-run --json`);
1985
+ } else if (coverage === 'consistent') {
1986
+ add(`lattice todo structure --plan ${planKey} --json`);
1987
+ add(`lattice todo structure realize --plan ${planKey} --task <task-id> --input <realization.json>`);
1988
+ add(`lattice todo structure finalize --plan ${planKey} --json`);
1989
+ }
1990
+ return actions;
1991
+ }
1992
+
1993
+ async function structureCompile({ repoRoot, env, planKey, inputRef }) {
1994
+ const structureSet = await readStructureSetDraft(repoRoot, inputRef);
1995
+ const explained = explainTodoStructureSet(structureSet);
1996
+ if (!explained.valid) {
1997
+ throw new TodoStoreError('INVALID_TODO_STRUCTURE_SET', explained.reason, undefined, {
1998
+ input_ref: inputRef, path: explained.path,
1999
+ next_action: `lattice todo structure input --plan ${planKey} --input ${inputRef} --dry-run --json`,
2000
+ });
2001
+ }
2002
+ const store = await readTodoStore({ repoRoot });
2003
+ const member = structurePlanMember(store, planKey);
2004
+ const source = await readTodoStructureSource({ repoRoot, planKey });
2005
+ if (source === null) {
2006
+ throw new TodoStoreError('STRUCTURE_SOURCE_MISSING', 'planned_source_missing', undefined, {
2007
+ plan_key: planKey,
2008
+ next_action: `lattice todo structure input --plan ${planKey} --input ${inputRef}`,
2009
+ });
2010
+ }
2011
+ if (canonicalizeTodoArtifact(source) !== canonicalizeTodoArtifact(structureSet)) {
2012
+ throw new TodoStoreError('STRUCTURE_SOURCE_MISMATCH', 'compile_input_is_not_saved_source', undefined, {
2013
+ input_ref: inputRef, source_ref: todoStructureSourceRef(planKey),
2014
+ next_action: `lattice todo structure input --plan ${planKey} --input ${inputRef}`,
2015
+ });
2016
+ }
2017
+ const existingBinding = await readTodoStructureBinding({
2018
+ repoRoot, planKey, planVersion: member.plan.plan_version,
2019
+ });
2020
+ if (existingBinding !== null) {
2021
+ throw new TodoStoreError('STRUCTURE_ALREADY_ENABLED', 'immutable_binding_exists', undefined, {
2022
+ plan_key: planKey, plan_version: member.plan.plan_version,
2023
+ next_action: `lattice todo structure --plan ${planKey} --json`,
2024
+ });
2025
+ }
2026
+ const actor = mutationActor(env);
2027
+ const sourceEvidence = await collectTodoStructureSourceEvidence({ cwd: repoRoot, structureSet });
2028
+ const gitProvenance = collectTodoStructureGitProvenance({ repoRoot, structureSet });
2029
+ const realizations = [];
2030
+ for (const task of structureSet.tasks.filter(({ applicability }) => applicability === 'graph')) {
2031
+ realizations.push(...await readTodoStructureRealizationChain({
2032
+ repoRoot, structureSet, taskId: task.task_id,
2033
+ }));
2034
+ }
2035
+ const overlay = compileTodoStructureOverlay({
2036
+ structureSet,
2037
+ topology: mergedTopology(store),
2038
+ taskStates: structureSet.tasks.map(({ task_id: taskId }) => ({
2039
+ task_id: taskId,
2040
+ status: member.tasks.find(({ task_id: id }) => id === taskId)?.status ?? 'pending',
2041
+ })),
2042
+ sourceProjection: sourceEvidence.projection,
2043
+ gitProvenance,
2044
+ realizations,
2045
+ });
2046
+ const compiledAt = new Date().toISOString();
2047
+ const artifact = buildTodoStructureCompileArtifact({
2048
+ structureSet, sourceProjection: sourceEvidence.projection, gitProvenance, overlay,
2049
+ realizations, compiledAt, actor,
2050
+ });
2051
+ const written = await writeTodoStructureCompileArtifact({ repoRoot, artifact });
2052
+ let bindingRef = null;
2053
+ if (overlay.verdict === 'consistent') {
2054
+ const binding = {
2055
+ schema: TODO_STRUCTURE_BINDING_SCHEMA,
2056
+ project_id: structureSet.project_id, plan_key: structureSet.plan_key,
2057
+ plan_version: structureSet.plan_version, topology_digest: structureSet.topology_digest,
2058
+ profile: structureSet.profile, baseline_sha: structureSet.baseline_sha,
2059
+ structure_set_digest: structureSet.structure_set_digest,
2060
+ compiled_head_sha: artifact.current_head_sha,
2061
+ compile_artifact_digest: artifact.artifact_digest,
2062
+ activated_at: compiledAt, actor, binding_digest: '',
2063
+ };
2064
+ binding.binding_digest = todoSelfDigest(binding, 'binding_digest');
2065
+ bindingRef = (await writeTodoStructureBinding({ repoRoot, binding })).ref;
2066
+ }
2067
+ const nextActions = structureNextActions({
2068
+ coverage: overlay.verdict, planKey, findings: overlay.findings,
2069
+ });
2070
+ const result = {
2071
+ schema: 'lattice.todo_structure_compile_result.v1',
2072
+ project_id: structureSet.project_id, plan_key: planKey,
2073
+ plan_version: structureSet.plan_version, topology_digest: structureSet.topology_digest,
2074
+ structure_set_digest: structureSet.structure_set_digest,
2075
+ current_head_sha: artifact.current_head_sha,
2076
+ verdict: overlay.verdict, enabled: bindingRef !== null,
2077
+ artifact_ref: written.ref, binding_ref: bindingRef,
2078
+ finding_summary: overlay.finding_summary, findings: overlay.findings,
2079
+ next_actions: nextActions, result_digest: '',
2080
+ };
2081
+ result.result_digest = todoSelfDigest(result, 'result_digest');
2082
+ return result;
2083
+ }
2084
+
2085
+ async function readStructureRealizationInput(repoRoot, inputRef) {
2086
+ let explained = null;
2087
+ return readJsonInput(repoRoot, inputRef, {
2088
+ validate: (value) => {
2089
+ explained = explainTodoStructureRealization(value);
2090
+ return explained.valid;
2091
+ },
2092
+ invalidCode: 'INVALID_TODO_STRUCTURE_REALIZATION',
2093
+ }).catch((error) => {
2094
+ if (error?.code === 'INVALID_TODO_STRUCTURE_REALIZATION' && explained !== null) {
2095
+ throw new TodoStoreError(error.code, explained.reason, undefined, {
2096
+ input_ref: inputRef, path: explained.path,
2097
+ });
2098
+ }
2099
+ throw error;
2100
+ });
2101
+ }
2102
+
2103
+ async function readCurrentStructureEffective(repoRoot, structureSet) {
2104
+ const realizations = [];
2105
+ for (const task of structureSet.tasks.filter(({ applicability }) => applicability === 'graph')) {
2106
+ realizations.push(...await readTodoStructureRealizationChain({
2107
+ repoRoot, structureSet, taskId: task.task_id,
2108
+ }));
2109
+ }
2110
+ return projectTodoStructureEffective({ structureSet, realizations });
2111
+ }
2112
+
2113
+ async function structureRealize({ repoRoot, env, planKey, taskId, inputRef }) {
2114
+ const realization = await readStructureRealizationInput(repoRoot, inputRef);
2115
+ const actor = mutationActor(env);
2116
+ if (realization.plan_key !== planKey || realization.task_id !== taskId) {
2117
+ throw new TodoStoreError('STRUCTURE_REALIZATION_BINDING_MISMATCH',
2118
+ 'cli_target_mismatch', undefined, {
2119
+ expected: { plan_key: planKey, task_id: taskId },
2120
+ actual: { plan_key: realization.plan_key, task_id: realization.task_id },
2121
+ });
2122
+ }
2123
+ if (canonicalizeTodoArtifact(realization.actor) !== canonicalizeTodoArtifact(actor)) {
2124
+ throw new TodoStoreError('STRUCTURE_REALIZATION_BINDING_MISMATCH',
2125
+ 'actor_environment_mismatch', undefined, {
2126
+ expected_actor: actor, actual_actor: realization.actor,
2127
+ });
2128
+ }
2129
+ const appended = await appendTodoStructureRealization({ repoRoot, realization });
2130
+ const structureSet = await readTodoStructureSource({ repoRoot, planKey });
2131
+ const effective = await readCurrentStructureEffective(repoRoot, structureSet);
2132
+ const taskProjection = effective.tasks.find(({ task_id: id }) => id === taskId);
2133
+ const result = {
2134
+ schema: 'lattice.todo_structure_realize_result.v1',
2135
+ project_id: realization.project_id, plan_key: planKey,
2136
+ plan_version: realization.plan_version, task_id: taskId,
2137
+ realization_ref: appended.ref,
2138
+ realization_digest: realization.realization_digest,
2139
+ previous_realization_digest: appended.previous_realization_digest,
2140
+ history_length: appended.history_length,
2141
+ effective: taskProjection,
2142
+ next_action: `lattice todo done --plan ${planKey} --task ${taskId} --evidence <file>`,
2143
+ result_digest: '',
2144
+ };
2145
+ result.result_digest = todoSelfDigest(result, 'result_digest');
2146
+ return result;
2147
+ }
2148
+
2149
+ async function structureFinalize({ repoRoot, env, planKey }) {
2150
+ const store = await readTodoStore({ repoRoot });
2151
+ const member = structurePlanMember(store, planKey);
2152
+ const source = await readTodoStructureSource({ repoRoot, planKey });
2153
+ const binding = await readTodoStructureBinding({
2154
+ repoRoot, planKey, planVersion: member.plan.plan_version,
2155
+ });
2156
+ if (source === null || binding === null
2157
+ || source.structure_set_digest !== binding.structure_set_digest) {
2158
+ throw new TodoStoreError('STRUCTURE_FINALIZATION_UNAVAILABLE',
2159
+ 'structure_not_enabled_or_stale', undefined, {
2160
+ plan_key: planKey,
2161
+ next_action: `lattice todo structure compile --plan ${planKey} --input .lattice/todo/structure/${planKey}.json`,
2162
+ });
2163
+ }
2164
+ const incompleteTaskIds = member.tasks
2165
+ .filter(({ status }) => status !== 'done')
2166
+ .map(({ task_id: taskId }) => taskId).sort();
2167
+ if (incompleteTaskIds.length > 0) {
2168
+ throw new TodoStoreError('STRUCTURE_FINALIZATION_UNAVAILABLE',
2169
+ 'plan_not_fully_done', undefined, {
2170
+ plan_key: planKey, incomplete_task_ids: incompleteTaskIds,
2171
+ });
2172
+ }
2173
+ const realizations = [];
2174
+ const effectiveTransforms = new Map();
2175
+ for (const task of source.tasks.filter(({ applicability }) => applicability === 'graph')) {
2176
+ const chain = await readTodoStructureRealizationChain({
2177
+ repoRoot, structureSet: source, taskId: task.task_id,
2178
+ });
2179
+ const latest = chain.at(-1);
2180
+ if (latest === undefined) {
2181
+ throw new TodoStoreError('STRUCTURE_FINALIZATION_UNAVAILABLE',
2182
+ 'realization_missing', undefined, {
2183
+ plan_key: planKey, task_id: task.task_id,
2184
+ next_action: `lattice todo structure realize --plan ${planKey} --task ${task.task_id} --input <realization.json>`,
2185
+ });
2186
+ }
2187
+ realizations.push(...chain);
2188
+ effectiveTransforms.set(task.task_id, latest.realized);
2189
+ }
2190
+ const actor = mutationActor(env);
2191
+ const sourceEvidence = await collectTodoStructureSourceEvidence({
2192
+ cwd: repoRoot, structureSet: source, effectiveTransforms,
2193
+ });
2194
+ const gitProvenance = collectTodoStructureGitProvenance({ repoRoot, structureSet: source });
2195
+ const overlay = compileTodoStructureOverlay({
2196
+ structureSet: source,
2197
+ topology: mergedTopology(store),
2198
+ taskStates: source.tasks.map(({ task_id: taskId }) => ({ task_id: taskId, status: 'done' })),
2199
+ sourceProjection: sourceEvidence.projection,
2200
+ gitProvenance,
2201
+ realizations,
2202
+ });
2203
+ const artifact = buildTodoStructureCompileArtifact({
2204
+ structureSet: source, sourceProjection: sourceEvidence.projection,
2205
+ gitProvenance, overlay, realizations,
2206
+ compiledAt: new Date().toISOString(), actor,
2207
+ });
2208
+ let finalizationRef = null;
2209
+ if (overlay.verdict === 'consistent') {
2210
+ finalizationRef = (await writeTodoStructureFinalization({ repoRoot, artifact })).ref;
2211
+ }
2212
+ const finalizationNextActions = [...new Set([
2213
+ ...overlay.findings.map(({ next_action: nextAction }) => nextAction),
2214
+ `lattice todo structure finalize --plan ${planKey} --json`,
2215
+ ])];
2216
+ const result = {
2217
+ schema: 'lattice.todo_structure_finalize_result.v1',
2218
+ project_id: source.project_id, plan_key: source.plan_key,
2219
+ plan_version: source.plan_version, topology_digest: source.topology_digest,
2220
+ structure_set_digest: source.structure_set_digest,
2221
+ current_head_sha: artifact.current_head_sha,
2222
+ realization_head_digest: artifact.realization_head_digest,
2223
+ verdict: overlay.verdict, finalized: finalizationRef !== null,
2224
+ finalization_ref: finalizationRef,
2225
+ finalization_digest: finalizationRef === null ? null : artifact.artifact_digest,
2226
+ finding_summary: overlay.finding_summary, findings: overlay.findings,
2227
+ next_actions: finalizationRef === null
2228
+ ? finalizationNextActions
2229
+ : [`lattice todo phase status --plan ${planKey}`],
2230
+ result_digest: '',
2231
+ };
2232
+ result.result_digest = todoSelfDigest(result, 'result_digest');
2233
+ return result;
2234
+ }
2235
+
2236
+ async function structure({ repoRoot, requestedPlanKey }) {
2237
+ const store = await readTodoStore({ repoRoot });
2238
+ const member = structurePlanMember(store, requestedPlanKey);
2239
+ const state = await readTodoStructureState({
2240
+ repoRoot, store, planKey: member.plan.plan_key,
2241
+ });
2242
+ const unboundVerdict = state.status === 'missing'
2243
+ && state.reason === 'activation_binding_missing'
2244
+ && ['inconsistent', 'unknown'].includes(state.compiled_verdict)
2245
+ ? state.compiled_verdict : null;
2246
+ const coverage = unboundVerdict
2247
+ ?? (state.status === 'fresh' ? state.effective_verdict : state.status);
2248
+ const findings = state.artifact?.overlay?.findings ?? [];
2249
+ const source = await readTodoStructureSource({ repoRoot, planKey: member.plan.plan_key });
2250
+ const effective = source !== null && source.plan_version === member.plan.plan_version
2251
+ ? await readCurrentStructureEffective(repoRoot, source) : null;
2252
+ const nextActions = structureNextActions({
2253
+ coverage, planKey: member.plan.plan_key, findings, staleReasons: state.stale_reasons,
2254
+ });
2255
+ const finalization = await readTodoStructureFinalizationState({
2256
+ repoRoot, store, planKey: member.plan.plan_key,
2257
+ });
2258
+ const result = {
2259
+ schema: 'lattice.todo_structure_projection.v1',
2260
+ project_id: store.project_id, plan_key: member.plan.plan_key,
2261
+ plan_version: member.plan.plan_version, topology_digest: member.plan.topology_digest,
2262
+ coverage, freshness: state.status,
2263
+ enabled: state.binding_digest !== null,
2264
+ structure_set_digest: state.structure_set_digest,
2265
+ artifact_digest: state.artifact_digest,
2266
+ compiled_verdict: state.compiled_verdict,
2267
+ verdict: unboundVerdict ?? state.effective_verdict,
2268
+ stale_reasons: state.stale_reasons,
2269
+ graph: state.artifact?.overlay?.graph ?? null,
2270
+ finding_summary: state.artifact?.overlay?.finding_summary ?? null,
2271
+ findings,
2272
+ effective,
2273
+ finalization: {
2274
+ required: finalization.required, status: finalization.status,
2275
+ reason: finalization.reason, finalization_digest: finalization.finalization_digest,
2276
+ stale_reasons: finalization.stale_reasons,
2277
+ },
2278
+ next_actions: nextActions,
2279
+ result_digest: '',
2280
+ };
2281
+ result.result_digest = todoSelfDigest(result, 'result_digest');
2282
+ return result;
2283
+ }
2284
+
1740
2285
  async function independenceCompile({ repoRoot, planKey, inputRef }) {
1741
2286
  const witnessSet = await readWitnessSetInput(repoRoot, inputRef);
1742
2287
  requireCleanWorktree(repoRoot);
@@ -1759,6 +2304,9 @@ async function independenceCompile({ repoRoot, planKey, inputRef }) {
1759
2304
  previousArtifact,
1760
2305
  });
1761
2306
  const { ref } = await writeTodoIndependenceArtifact({ repoRoot, artifact });
2307
+ // compile成功後にだけ、受理した宣言をruntime readerが要求するcanonical bytesで固定する。
2308
+ // inputがstore自身でも外部draftでも、唯一の正規writerを通して同じ契約へ収束させる。
2309
+ await writeTodoWitnessSet({ repoRoot, witnessSet });
1762
2310
 
1763
2311
  const result = {
1764
2312
  schema: 'lattice.todo_independence_compile_result.v2',
@@ -2375,13 +2923,14 @@ async function loadNarratives(store, repoRoot) {
2375
2923
  * 一つの関数だけが組む。
2376
2924
  */
2377
2925
  export async function ganttLiveHeadDigest({ repoRoot, store }) {
2378
- const [independence, seamProposals, noteHeads] = await Promise.all([
2926
+ const [independence, seamProposals, noteHeads, structures] = await Promise.all([
2379
2927
  independenceForGantt({ repoRoot, store }),
2380
2928
  seamProposalsForGantt({ repoRoot, store }),
2381
2929
  noteHeadsForGantt({ repoRoot, store }),
2930
+ loadTodoStructurePresentation({ repoRoot, readModel: store }),
2382
2931
  ]);
2383
2932
  return digestTodoArtifact({
2384
- schema: 'lattice.todo_gantt_live_head.v2',
2933
+ schema: 'lattice.todo_gantt_live_head.v3',
2385
2934
  manifest_digest: store.manifest.manifest_digest,
2386
2935
  independence: independence === null ? null : independence.map((entry) => ({
2387
2936
  plan_key: entry.plan_key,
@@ -2394,6 +2943,7 @@ export async function ganttLiveHeadDigest({ repoRoot, store }) {
2394
2943
  projection_digest: digestTodoArtifact(entry),
2395
2944
  })),
2396
2945
  note_heads: noteHeads,
2946
+ structure_projection_digest: structures.projection_digest,
2397
2947
  });
2398
2948
  }
2399
2949
 
@@ -2585,10 +3135,11 @@ export async function renderTodoGanttForProject({
2585
3135
  const presentation = await loadTodoGanttPresentation({ repoRoot, readModel: store });
2586
3136
  const topology = mergedTopology(store);
2587
3137
  const chain = projectTodoChainV1(topology);
2588
- const [independence, seamProposals, notes] = await Promise.all([
3138
+ const [independence, seamProposals, notes, structures] = await Promise.all([
2589
3139
  independenceForGantt({ repoRoot, store }),
2590
3140
  seamProposalsForGantt({ repoRoot, store }),
2591
3141
  notesForGantt({ repoRoot, store }),
3142
+ loadTodoStructurePresentation({ repoRoot, readModel: store }),
2592
3143
  ]);
2593
3144
  const layout = layoutTodoGantt(store, chain, { scope, independence, seamProposals });
2594
3145
  // When the diagram hides history, the page also carries the full diagram so
@@ -2623,6 +3174,7 @@ export async function renderTodoGanttForProject({
2623
3174
  chain_digest: digestTodoArtifact(chain),
2624
3175
  layout_digest: digestTodoArtifact(layout),
2625
3176
  note_bindings_digest: digestTodoArtifact(notes.headBindings),
3177
+ structure_projection_digest: structures.projection_digest,
2626
3178
  renderer_version: TODO_GANTT_RENDERER_VERSION,
2627
3179
  project_display_name: identity.displayName,
2628
3180
  folded_task_count: layout.scope.folded_task_count,
@@ -2637,6 +3189,7 @@ export async function renderTodoGanttForProject({
2637
3189
  metadata,
2638
3190
  noteContexts: notes.contexts,
2639
3191
  noteWarnings: notes.warnings,
3192
+ structurePresentation: structures,
2640
3193
  });
2641
3194
  return { store, metadata, memberBindings, rendered };
2642
3195
  }
@@ -2709,6 +3262,8 @@ function writesTodoStore(argv) {
2709
3262
  case 'snapshot': return argv.includes('--rebuild');
2710
3263
  case 'independence': return second === 'compile'
2711
3264
  || (second === 'witness' && ['migrate', 'scaffold'].includes(third));
3265
+ case 'structure': return ['compile', 'realize', 'finalize'].includes(second)
3266
+ || (second === 'input' && !argv.includes('--dry-run'));
2712
3267
  case 'seam-proposal': return ['compile', 'apply', 'land'].includes(second);
2713
3268
  case 'evidence': return second === 'promote';
2714
3269
  case 'dependency': return second === 'connect';
@@ -2873,6 +3428,22 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
2873
3428
  });
2874
3429
  }
2875
3430
 
3431
+ if (argv[0] === 'structure' && ['input', 'compile'].includes(argv[1])
3432
+ && argv[4] === '--input' && typeof argv[5] === 'string' && path.isAbsolute(argv[5])) {
3433
+ return typedArgumentFailure(stderr, 'INPUT_OUTSIDE_REPOSITORY', 'absolute_input_path_rejected', {
3434
+ argument: '--input', expected: 'repo-relative path', actual: 'absolute path',
3435
+ next_action: 'place_the_input_inside_the_repository_and_pass_a_repo_relative_path',
3436
+ });
3437
+ }
3438
+
3439
+ if (argv[0] === 'structure' && argv[1] === 'realize'
3440
+ && argv[6] === '--input' && typeof argv[7] === 'string' && path.isAbsolute(argv[7])) {
3441
+ return typedArgumentFailure(stderr, 'INPUT_OUTSIDE_REPOSITORY', 'absolute_input_path_rejected', {
3442
+ argument: '--input', expected: 'repo-relative path', actual: 'absolute path',
3443
+ next_action: 'place_the_input_inside_the_repository_and_pass_a_repo_relative_path',
3444
+ });
3445
+ }
3446
+
2876
3447
  // `--schema --json`はstoreを読まない決定的な出力(`plan create --schema`と同じ規律)。
2877
3448
  // 通常dispatchより前に処理し、repoRoot解決やdashboard daemon起動を経由させない。
2878
3449
  if (argv.length === 3 && argv[1] === '--schema' && argv[2] === '--json'
@@ -2983,6 +3554,40 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
2983
3554
  action = (repoRoot) => independenceCompile({
2984
3555
  repoRoot, planKey: argv[3], inputRef: argv[5],
2985
3556
  });
3557
+ } else if (argv.length === 8 && argv[0] === 'structure' && argv[1] === 'input'
3558
+ && argv[2] === '--plan' && isTodoIdentifier(argv[3])
3559
+ && argv[4] === '--input' && isTodoRef(argv[5])
3560
+ && argv[6] === '--dry-run' && argv[7] === '--json') {
3561
+ action = (repoRoot) => structureInputDryRun({
3562
+ repoRoot, planKey: argv[3], inputRef: argv[5],
3563
+ });
3564
+ } else if (argv.length === 6 && argv[0] === 'structure' && argv[1] === 'input'
3565
+ && argv[2] === '--plan' && isTodoIdentifier(argv[3])
3566
+ && argv[4] === '--input' && isTodoRef(argv[5])) {
3567
+ action = (repoRoot) => structureInput({
3568
+ repoRoot, planKey: argv[3], inputRef: argv[5],
3569
+ });
3570
+ } else if (argv.length === 6 && argv[0] === 'structure' && argv[1] === 'compile'
3571
+ && argv[2] === '--plan' && isTodoIdentifier(argv[3])
3572
+ && argv[4] === '--input' && isTodoRef(argv[5])) {
3573
+ action = (repoRoot) => structureCompile({
3574
+ repoRoot, env, planKey: argv[3], inputRef: argv[5],
3575
+ });
3576
+ } else if (argv.length === 8 && argv[0] === 'structure' && argv[1] === 'realize'
3577
+ && argv[2] === '--plan' && isTodoIdentifier(argv[3])
3578
+ && argv[4] === '--task' && isTodoIdentifier(argv[5])
3579
+ && argv[6] === '--input' && isTodoRef(argv[7])) {
3580
+ action = (repoRoot) => structureRealize({
3581
+ repoRoot, env, planKey: argv[3], taskId: argv[5], inputRef: argv[7],
3582
+ });
3583
+ } else if (argv.length === 5 && argv[0] === 'structure' && argv[1] === 'finalize'
3584
+ && argv[2] === '--plan' && isTodoIdentifier(argv[3]) && argv[4] === '--json') {
3585
+ action = (repoRoot) => structureFinalize({ repoRoot, env, planKey: argv[3] });
3586
+ } else if (argv.length === 2 && argv[0] === 'structure' && argv[1] === '--json') {
3587
+ action = (repoRoot) => structure({ repoRoot, requestedPlanKey: null });
3588
+ } else if (argv.length === 4 && argv[0] === 'structure'
3589
+ && argv[1] === '--plan' && isTodoIdentifier(argv[2]) && argv[3] === '--json') {
3590
+ action = (repoRoot) => structure({ repoRoot, requestedPlanKey: argv[2] });
2986
3591
  } else if ((argv.length === 1 && argv[0] === 'independence')
2987
3592
  || (argv.length === 2 && argv[0] === 'independence' && argv[1] === '--json')) {
2988
3593
  action = (repoRoot) => independence({ repoRoot, requestedPlanKey: null });