@quolu/lattice 0.57.3 → 0.58.1

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,27 @@ 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 { collectTodoStructureAuthoritativeObservation } from './todo-structure-authoritative-observation.mjs';
81
+ import { compileTodoStructureOverlay } from './todo-structure-overlay.mjs';
82
+ import {
83
+ buildTodoStructureCompileArtifact,
84
+ projectTodoStructureEffective,
85
+ readTodoStructureFinalizationState,
86
+ readTodoStructureFinalizationsForStatus,
87
+ readTodoStructureState,
88
+ } from './todo-structure-store.mjs';
65
89
  import { withStartRetractionGuard } from './runtime-pull-intake.mjs';
66
90
  import {
67
91
  appendTodoExtraction,
@@ -159,6 +183,9 @@ const TODO_SCHEMA_COMMANDS = Object.freeze({
159
183
  title: 'lattice.phase_todo_revision.v3', file: 'lattice.phase_todo_revision.v3.schema.json',
160
184
  },
161
185
  migrate: { title: 'lattice.todo_extraction.v3', file: 'lattice.todo_extraction.v3.schema.json' },
186
+ structure: {
187
+ title: 'lattice.todo_structure_set.v1', file: 'lattice.todo_structure_set.v1.schema.json',
188
+ },
162
189
  });
163
190
 
164
191
  async function runTodoSchemaCommand(command, stdout) {
@@ -207,7 +234,7 @@ function internalFailure(stderr, error) {
207
234
  }
208
235
 
209
236
  const TODO_COMMAND_NAMES = Object.freeze([
210
- 'status', 'show', 'note', 'bindings', 'independence', 'seam-profile', 'seam-proposal',
237
+ 'status', 'show', 'note', 'bindings', 'independence', 'structure', 'seam-profile', 'seam-proposal',
211
238
  'verify', 'snapshot', 'gantt', 'dashboard', 'phase', 'migrate', 'start', 'block',
212
239
  'unblock', 'done', 'reopen', 'evidence', 'split', 'revise', 'revise-phase', 'revise-set',
213
240
  ]);
@@ -553,6 +580,14 @@ async function readWitnessSetInput(repoRoot, inputRef) {
553
580
  return witnessSet;
554
581
  }
555
582
 
583
+ /** structure draftはpointer診断のためcontract違反でもparseし、判定はdry-runへ返す。 */
584
+ async function readStructureSetDraft(repoRoot, inputRef) {
585
+ return readJsonInput(repoRoot, inputRef, {
586
+ validate: () => true,
587
+ invalidCode: 'INVALID_TODO_STRUCTURE_SET',
588
+ });
589
+ }
590
+
556
591
  function mutationActor(env) {
557
592
  const entries = ACTOR_ENV_KEYS.map((key) => ({ key, value: env[key] }));
558
593
  const missingEnvironment = entries
@@ -1016,6 +1051,9 @@ async function dependencyConnect({
1016
1051
  async function phaseStatus({ repoRoot, planKey }) {
1017
1052
  const store = await readTodoStore({ repoRoot });
1018
1053
  const [member] = selectMembers(store, planKey);
1054
+ const structureFinalization = await readTodoStructureFinalizationState({
1055
+ repoRoot, store, planKey: member.plan.plan_key,
1056
+ });
1019
1057
  // ADR 0147以降、phase無しplan(v1/v2/v3)もreadTodoStoreが導出済みの暗黙terminal-audit
1020
1058
  // Phaseをmember.phasesへ積んでいる(snapshot artifactの形式は変えない・v1にはphasesキーが
1021
1059
  // 無いのでsnapshot.phasesは直接読まない)。ここでPHASE_UNAVAILABLEへ拒否せず、その暗黙Phase
@@ -1027,13 +1065,26 @@ async function phaseStatus({ repoRoot, planKey }) {
1027
1065
  const phases = member.phases.map((phase) => ({
1028
1066
  ...phase,
1029
1067
  guidance: phase.status === 'gate_ready'
1030
- ? auditGateGuidance(member.plan.plan_key, phase.phase_id) : null,
1068
+ ? [
1069
+ auditGateGuidance(member.plan.plan_key, phase.phase_id),
1070
+ ...(structureFinalization.required && structureFinalization.status !== 'fresh'
1071
+ ? [`構造finalizationが${structureFinalization.status}です。先に lattice todo structure finalize --plan ${member.plan.plan_key} --json を実行してください。`]
1072
+ : []),
1073
+ ].join('\n') : null,
1031
1074
  }));
1032
1075
  const result = {
1033
- schema: 'lattice.phase_status_result.v1', project_id: store.project_id,
1076
+ schema: 'lattice.phase_status_result.v2', project_id: store.project_id,
1034
1077
  plan_key: member.plan.plan_key, plan_version: member.plan.plan_version,
1035
1078
  journal_head_digest: member.journal.events.at(-1).event_digest,
1036
- implicit, phases, result_digest: '',
1079
+ implicit, phases,
1080
+ structure_finalization: {
1081
+ enabled: structureFinalization.enabled, required: structureFinalization.required,
1082
+ status: structureFinalization.status, reason: structureFinalization.reason,
1083
+ stale_reasons: structureFinalization.stale_reasons,
1084
+ next_command: structureFinalization.required && structureFinalization.status !== 'fresh'
1085
+ ? `lattice todo structure finalize --plan ${member.plan.plan_key} --json` : null,
1086
+ },
1087
+ result_digest: '',
1037
1088
  };
1038
1089
  result.result_digest = todoSelfDigest(result, 'result_digest');
1039
1090
  return result;
@@ -1544,6 +1595,7 @@ async function status({ repoRoot }) {
1544
1595
  parallelCandidates: await readTodoParallelCandidatesForStatus({
1545
1596
  repoRoot, store, gitHead: currentHeadSha, changedPathsSince,
1546
1597
  }),
1598
+ structureFinalizations: await readTodoStructureFinalizationsForStatus({ repoRoot, store }),
1547
1599
  });
1548
1600
  }
1549
1601
 
@@ -1737,6 +1789,500 @@ function requireCleanWorktree(repoRoot) {
1737
1789
  }
1738
1790
  }
1739
1791
 
1792
+ function structureGitIdentity(repoRoot, baselineSha, violations) {
1793
+ let currentHeadSha = null;
1794
+ try {
1795
+ currentHeadSha = gitSync(['rev-parse', 'HEAD'], {
1796
+ cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
1797
+ }).replace(/\r?\n$/u, '');
1798
+ } catch {
1799
+ violations.push({
1800
+ code: 'git_head_unresolved', path: '/baseline_sha',
1801
+ expected: 'current repository HEAD commit', actual: null,
1802
+ next_action: 'repair_git_head_then_rerun_structure_input_dry_run',
1803
+ });
1804
+ return { currentHeadSha: null };
1805
+ }
1806
+ if (!/^[0-9a-f]{40}$/u.test(currentHeadSha)) {
1807
+ violations.push({
1808
+ code: 'git_head_invalid', path: '/baseline_sha',
1809
+ expected: '40 lowercase hex commit OID', actual: currentHeadSha,
1810
+ next_action: 'repair_git_head_then_rerun_structure_input_dry_run',
1811
+ });
1812
+ return { currentHeadSha: null };
1813
+ }
1814
+ try {
1815
+ gitSync(['cat-file', '-e', `${baselineSha}^{commit}`], {
1816
+ cwd: repoRoot, stdio: ['ignore', 'ignore', 'ignore'],
1817
+ });
1818
+ } catch {
1819
+ violations.push({
1820
+ code: 'baseline_commit_unresolved', path: '/baseline_sha',
1821
+ expected: 'commit object reachable in this repository', actual: baselineSha,
1822
+ next_action: 'fetch_the_baseline_or_correct_baseline_sha',
1823
+ });
1824
+ return { currentHeadSha };
1825
+ }
1826
+ try {
1827
+ gitSync(['merge-base', '--is-ancestor', baselineSha, currentHeadSha], {
1828
+ cwd: repoRoot, stdio: ['ignore', 'ignore', 'ignore'],
1829
+ });
1830
+ } catch {
1831
+ violations.push({
1832
+ code: 'baseline_not_ancestor', path: '/baseline_sha',
1833
+ expected: { ancestor_of: currentHeadSha }, actual: baselineSha,
1834
+ next_action: 'select_an_ancestor_baseline_or_rebase_the_structure_input',
1835
+ });
1836
+ }
1837
+ return { currentHeadSha };
1838
+ }
1839
+
1840
+ async function analyzeStructureInput({ repoRoot, planKey, inputRef }) {
1841
+ const structureSet = await readStructureSetDraft(repoRoot, inputRef);
1842
+ const violations = [];
1843
+ const explained = explainTodoStructureSet(structureSet);
1844
+ if (!explained.valid) {
1845
+ violations.push({
1846
+ code: explained.reason,
1847
+ path: explained.path,
1848
+ ...(explained.detail === undefined ? {} : { actual: explained.detail }),
1849
+ next_action: 'correct_the_reported_pointer_then_rerun_structure_input_dry_run',
1850
+ });
1851
+ }
1852
+
1853
+ const store = await readTodoStore({ repoRoot });
1854
+ const member = store.members.find(({ descriptor }) => descriptor.plan_key === planKey) ?? null;
1855
+ if (member === null) {
1856
+ violations.push({
1857
+ code: 'plan_not_active', path: '/plan_key', expected: planKey,
1858
+ actual: typeof structureSet?.plan_key === 'string' ? structureSet.plan_key : null,
1859
+ next_action: 'define_the_plan_before_adding_structure_input',
1860
+ });
1861
+ }
1862
+
1863
+ let currentHeadSha = null;
1864
+ if (explained.valid && member !== null) {
1865
+ const identities = [
1866
+ ['project_id', store.project_id, structureSet.project_id],
1867
+ ['plan_key', planKey, structureSet.plan_key],
1868
+ ['plan_version', member.plan.plan_version, structureSet.plan_version],
1869
+ ['topology_digest', member.plan.topology_digest, structureSet.topology_digest],
1870
+ ];
1871
+ for (const [field, expected, actual] of identities) {
1872
+ if (actual !== expected) {
1873
+ violations.push({
1874
+ code: `${field}_mismatch`, path: `/${field}`, expected, actual,
1875
+ next_action: 'regenerate_structure_input_for_the_active_plan_identity',
1876
+ });
1877
+ }
1878
+ }
1879
+ const expectedTaskIds = member.tasks.filter(({ status }) => status !== 'done')
1880
+ .map(({ task_id: taskId }) => taskId);
1881
+ const coverage = explainTodoStructureSet(structureSet, { expectedTaskIds });
1882
+ if (!coverage.valid && ['coverage_missing', 'coverage_extra'].includes(coverage.reason)) {
1883
+ violations.push({
1884
+ code: coverage.reason, path: coverage.path,
1885
+ expected: { task_ids: [...expectedTaskIds].sort() },
1886
+ actual: coverage.detail,
1887
+ next_action: 'list_every_unfinished_task_as_graph_or_excluded',
1888
+ });
1889
+ }
1890
+ ({ currentHeadSha } = structureGitIdentity(repoRoot, structureSet.baseline_sha, violations));
1891
+ }
1892
+
1893
+ const bounded = violations.slice(0, 64);
1894
+ const result = {
1895
+ schema: 'lattice.todo_structure_input_dry_run_result.v1',
1896
+ valid: bounded.length === 0,
1897
+ project_id: member?.plan.project_id ?? null,
1898
+ plan_key: member?.plan.plan_key ?? planKey,
1899
+ plan_version: member?.plan.plan_version ?? null,
1900
+ input_ref: inputRef,
1901
+ source_ref: todoStructureSourceRef(planKey),
1902
+ current_head_sha: currentHeadSha,
1903
+ violations: bounded,
1904
+ overflow_count: Math.max(0, violations.length - bounded.length),
1905
+ next_action: bounded.length === 0
1906
+ ? `lattice todo structure input --plan ${planKey} --input ${inputRef}`
1907
+ : 'correct_the_reported_violations_then_rerun_structure_input_dry_run',
1908
+ result_digest: '',
1909
+ };
1910
+ result.result_digest = todoSelfDigest(result, 'result_digest');
1911
+ return { result, structureSet };
1912
+ }
1913
+
1914
+ async function structureInputDryRun({ repoRoot, planKey, inputRef }) {
1915
+ return (await analyzeStructureInput({ repoRoot, planKey, inputRef })).result;
1916
+ }
1917
+
1918
+ async function structureInput({ repoRoot, planKey, inputRef }) {
1919
+ const analyzed = await analyzeStructureInput({ repoRoot, planKey, inputRef });
1920
+ if (!analyzed.result.valid) {
1921
+ throw new TodoStoreError('INVALID_TODO_STRUCTURE_SET', 'structure_input_dry_run_failed', undefined, {
1922
+ violations: analyzed.result.violations,
1923
+ overflow_count: analyzed.result.overflow_count,
1924
+ next_action: `lattice todo structure input --plan ${planKey} --input ${inputRef} --dry-run --json`,
1925
+ });
1926
+ }
1927
+ const written = await writeTodoStructureSource({ repoRoot, structureSet: analyzed.structureSet });
1928
+ const result = {
1929
+ schema: 'lattice.todo_structure_input_result.v1',
1930
+ project_id: analyzed.structureSet.project_id,
1931
+ plan_key: analyzed.structureSet.plan_key,
1932
+ plan_version: analyzed.structureSet.plan_version,
1933
+ topology_digest: analyzed.structureSet.topology_digest,
1934
+ baseline_sha: analyzed.structureSet.baseline_sha,
1935
+ structure_set_digest: analyzed.structureSet.structure_set_digest,
1936
+ source_ref: written.ref,
1937
+ enabled: false,
1938
+ next_action: `commit ${written.ref}, then run lattice todo structure compile --plan ${planKey} --input ${written.ref}`,
1939
+ result_digest: '',
1940
+ };
1941
+ result.result_digest = todoSelfDigest(result, 'result_digest');
1942
+ return result;
1943
+ }
1944
+
1945
+ function structurePlanMember(store, requestedPlanKey) {
1946
+ if (requestedPlanKey !== null) {
1947
+ const member = store.members.find(({ plan }) => plan.plan_key === requestedPlanKey);
1948
+ if (member === undefined) {
1949
+ throw new TodoStoreError('STRUCTURE_PLAN_NOT_FOUND', 'plan_not_active', undefined, {
1950
+ plan_key: requestedPlanKey,
1951
+ });
1952
+ }
1953
+ return member;
1954
+ }
1955
+ if (store.members.length !== 1) {
1956
+ throw new TodoStoreError('STRUCTURE_PLAN_AMBIGUOUS', 'plan_selection_ambiguous', undefined, {
1957
+ plan_keys: store.members.map(({ plan }) => plan.plan_key).sort(),
1958
+ next_action: 'rerun_with_plan_flag',
1959
+ });
1960
+ }
1961
+ return store.members[0];
1962
+ }
1963
+
1964
+ function structureNextActions({ coverage, planKey, findings = [], staleReasons = [] }) {
1965
+ const actions = [];
1966
+ const add = (value) => { if (!actions.includes(value)) actions.push(value); };
1967
+ if (coverage === 'missing') {
1968
+ add(`lattice todo structure input --plan ${planKey} --input <structure-set.json> --dry-run --json`);
1969
+ add(`lattice todo structure input --plan ${planKey} --input <structure-set.json>`);
1970
+ add(`lattice todo structure compile --plan ${planKey} --input .lattice/todo/structure/${planKey}.json`);
1971
+ } else if (['inconsistent', 'unknown'].includes(coverage)) {
1972
+ findings.forEach(({ next_action: nextAction }) => add(nextAction));
1973
+ add(`lattice todo structure input --plan ${planKey} --input <corrected-structure-set.json> --dry-run --json`);
1974
+ add(`lattice todo structure compile --plan ${planKey} --input .lattice/todo/structure/${planKey}.json`);
1975
+ } else if (coverage === 'stale') {
1976
+ if (staleReasons.includes('realization_head_digest')) {
1977
+ add(`lattice todo structure --plan ${planKey} --json`);
1978
+ } else {
1979
+ add(`lattice todo structure realize --plan ${planKey} --task <task-id> --input <realization.json>`);
1980
+ }
1981
+ add(`lattice todo structure finalize --plan ${planKey} --json`);
1982
+ } else if (coverage === 'superseded') {
1983
+ add(`lattice todo structure input --plan ${planKey} --input <migrated-structure-set.json> --dry-run --json`);
1984
+ } else if (coverage === 'consistent') {
1985
+ add(`lattice todo structure --plan ${planKey} --json`);
1986
+ add(`lattice todo structure realize --plan ${planKey} --task <task-id> --input <realization.json>`);
1987
+ add(`lattice todo structure finalize --plan ${planKey} --json`);
1988
+ }
1989
+ return actions;
1990
+ }
1991
+
1992
+ async function structureCompile({ repoRoot, env, planKey, inputRef }) {
1993
+ const structureSet = await readStructureSetDraft(repoRoot, inputRef);
1994
+ const explained = explainTodoStructureSet(structureSet);
1995
+ if (!explained.valid) {
1996
+ throw new TodoStoreError('INVALID_TODO_STRUCTURE_SET', explained.reason, undefined, {
1997
+ input_ref: inputRef, path: explained.path,
1998
+ next_action: `lattice todo structure input --plan ${planKey} --input ${inputRef} --dry-run --json`,
1999
+ });
2000
+ }
2001
+ const store = await readTodoStore({ repoRoot });
2002
+ const member = structurePlanMember(store, planKey);
2003
+ const source = await readTodoStructureSource({ repoRoot, planKey });
2004
+ if (source === null) {
2005
+ throw new TodoStoreError('STRUCTURE_SOURCE_MISSING', 'planned_source_missing', undefined, {
2006
+ plan_key: planKey,
2007
+ next_action: `lattice todo structure input --plan ${planKey} --input ${inputRef}`,
2008
+ });
2009
+ }
2010
+ if (canonicalizeTodoArtifact(source) !== canonicalizeTodoArtifact(structureSet)) {
2011
+ throw new TodoStoreError('STRUCTURE_SOURCE_MISMATCH', 'compile_input_is_not_saved_source', undefined, {
2012
+ input_ref: inputRef, source_ref: todoStructureSourceRef(planKey),
2013
+ next_action: `lattice todo structure input --plan ${planKey} --input ${inputRef}`,
2014
+ });
2015
+ }
2016
+ const existingBinding = await readTodoStructureBinding({
2017
+ repoRoot, planKey, planVersion: member.plan.plan_version,
2018
+ });
2019
+ if (existingBinding !== null) {
2020
+ throw new TodoStoreError('STRUCTURE_ALREADY_ENABLED', 'immutable_binding_exists', undefined, {
2021
+ plan_key: planKey, plan_version: member.plan.plan_version,
2022
+ next_action: `lattice todo structure --plan ${planKey} --json`,
2023
+ });
2024
+ }
2025
+ const actor = mutationActor(env);
2026
+ const observation = await collectTodoStructureAuthoritativeObservation({ repoRoot, structureSet });
2027
+ const sourceEvidence = observation.source_evidence;
2028
+ const gitProvenance = observation.git_provenance;
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 observation = await collectTodoStructureAuthoritativeObservation({
2192
+ repoRoot, structureSet: source, effectiveTransforms,
2193
+ });
2194
+ const sourceEvidence = observation.source_evidence;
2195
+ const gitProvenance = observation.git_provenance;
2196
+ const overlay = compileTodoStructureOverlay({
2197
+ structureSet: source,
2198
+ topology: mergedTopology(store),
2199
+ taskStates: source.tasks.map(({ task_id: taskId }) => ({ task_id: taskId, status: 'done' })),
2200
+ sourceProjection: sourceEvidence.projection,
2201
+ gitProvenance,
2202
+ realizations,
2203
+ });
2204
+ const artifact = buildTodoStructureCompileArtifact({
2205
+ structureSet: source, sourceProjection: sourceEvidence.projection,
2206
+ gitProvenance, overlay, realizations,
2207
+ compiledAt: new Date().toISOString(), actor,
2208
+ });
2209
+ let finalizationRef = null;
2210
+ if (overlay.verdict === 'consistent') {
2211
+ finalizationRef = (await writeTodoStructureFinalization({ repoRoot, artifact })).ref;
2212
+ }
2213
+ const finalizationNextActions = [...new Set([
2214
+ ...overlay.findings.map(({ next_action: nextAction }) => nextAction),
2215
+ `lattice todo structure finalize --plan ${planKey} --json`,
2216
+ ])];
2217
+ const result = {
2218
+ schema: 'lattice.todo_structure_finalize_result.v1',
2219
+ project_id: source.project_id, plan_key: source.plan_key,
2220
+ plan_version: source.plan_version, topology_digest: source.topology_digest,
2221
+ structure_set_digest: source.structure_set_digest,
2222
+ current_head_sha: artifact.current_head_sha,
2223
+ realization_head_digest: artifact.realization_head_digest,
2224
+ verdict: overlay.verdict, finalized: finalizationRef !== null,
2225
+ finalization_ref: finalizationRef,
2226
+ finalization_digest: finalizationRef === null ? null : artifact.artifact_digest,
2227
+ finding_summary: overlay.finding_summary, findings: overlay.findings,
2228
+ next_actions: finalizationRef === null
2229
+ ? finalizationNextActions
2230
+ : [`lattice todo phase status --plan ${planKey}`],
2231
+ result_digest: '',
2232
+ };
2233
+ result.result_digest = todoSelfDigest(result, 'result_digest');
2234
+ return result;
2235
+ }
2236
+
2237
+ async function structure({ repoRoot, requestedPlanKey }) {
2238
+ const store = await readTodoStore({ repoRoot });
2239
+ const member = structurePlanMember(store, requestedPlanKey);
2240
+ const state = await readTodoStructureState({
2241
+ repoRoot, store, planKey: member.plan.plan_key,
2242
+ });
2243
+ const unboundVerdict = state.status === 'missing'
2244
+ && state.reason === 'activation_binding_missing'
2245
+ && ['inconsistent', 'unknown'].includes(state.compiled_verdict)
2246
+ ? state.compiled_verdict : null;
2247
+ const coverage = unboundVerdict
2248
+ ?? (state.status === 'fresh' ? state.effective_verdict : state.status);
2249
+ const findings = state.artifact?.overlay?.findings ?? [];
2250
+ const source = await readTodoStructureSource({ repoRoot, planKey: member.plan.plan_key });
2251
+ const effective = source !== null && source.plan_version === member.plan.plan_version
2252
+ ? await readCurrentStructureEffective(repoRoot, source) : null;
2253
+ const nextActions = structureNextActions({
2254
+ coverage, planKey: member.plan.plan_key, findings, staleReasons: state.stale_reasons,
2255
+ });
2256
+ const finalization = await readTodoStructureFinalizationState({
2257
+ repoRoot, store, planKey: member.plan.plan_key,
2258
+ });
2259
+ const result = {
2260
+ schema: 'lattice.todo_structure_projection.v1',
2261
+ project_id: store.project_id, plan_key: member.plan.plan_key,
2262
+ plan_version: member.plan.plan_version, topology_digest: member.plan.topology_digest,
2263
+ coverage, freshness: state.status,
2264
+ enabled: state.binding_digest !== null,
2265
+ structure_set_digest: state.structure_set_digest,
2266
+ artifact_digest: state.artifact_digest,
2267
+ compiled_verdict: state.compiled_verdict,
2268
+ verdict: unboundVerdict ?? state.effective_verdict,
2269
+ stale_reasons: state.stale_reasons,
2270
+ graph: state.artifact?.overlay?.graph ?? null,
2271
+ finding_summary: state.artifact?.overlay?.finding_summary ?? null,
2272
+ findings,
2273
+ effective,
2274
+ finalization: {
2275
+ required: finalization.required, status: finalization.status,
2276
+ reason: finalization.reason, finalization_digest: finalization.finalization_digest,
2277
+ stale_reasons: finalization.stale_reasons,
2278
+ },
2279
+ next_actions: nextActions,
2280
+ result_digest: '',
2281
+ };
2282
+ result.result_digest = todoSelfDigest(result, 'result_digest');
2283
+ return result;
2284
+ }
2285
+
1740
2286
  async function independenceCompile({ repoRoot, planKey, inputRef }) {
1741
2287
  const witnessSet = await readWitnessSetInput(repoRoot, inputRef);
1742
2288
  requireCleanWorktree(repoRoot);
@@ -2378,13 +2924,14 @@ async function loadNarratives(store, repoRoot) {
2378
2924
  * 一つの関数だけが組む。
2379
2925
  */
2380
2926
  export async function ganttLiveHeadDigest({ repoRoot, store }) {
2381
- const [independence, seamProposals, noteHeads] = await Promise.all([
2927
+ const [independence, seamProposals, noteHeads, structures] = await Promise.all([
2382
2928
  independenceForGantt({ repoRoot, store }),
2383
2929
  seamProposalsForGantt({ repoRoot, store }),
2384
2930
  noteHeadsForGantt({ repoRoot, store }),
2931
+ loadTodoStructurePresentation({ repoRoot, readModel: store }),
2385
2932
  ]);
2386
2933
  return digestTodoArtifact({
2387
- schema: 'lattice.todo_gantt_live_head.v2',
2934
+ schema: 'lattice.todo_gantt_live_head.v3',
2388
2935
  manifest_digest: store.manifest.manifest_digest,
2389
2936
  independence: independence === null ? null : independence.map((entry) => ({
2390
2937
  plan_key: entry.plan_key,
@@ -2397,6 +2944,7 @@ export async function ganttLiveHeadDigest({ repoRoot, store }) {
2397
2944
  projection_digest: digestTodoArtifact(entry),
2398
2945
  })),
2399
2946
  note_heads: noteHeads,
2947
+ structure_projection_digest: structures.projection_digest,
2400
2948
  });
2401
2949
  }
2402
2950
 
@@ -2588,10 +3136,11 @@ export async function renderTodoGanttForProject({
2588
3136
  const presentation = await loadTodoGanttPresentation({ repoRoot, readModel: store });
2589
3137
  const topology = mergedTopology(store);
2590
3138
  const chain = projectTodoChainV1(topology);
2591
- const [independence, seamProposals, notes] = await Promise.all([
3139
+ const [independence, seamProposals, notes, structures] = await Promise.all([
2592
3140
  independenceForGantt({ repoRoot, store }),
2593
3141
  seamProposalsForGantt({ repoRoot, store }),
2594
3142
  notesForGantt({ repoRoot, store }),
3143
+ loadTodoStructurePresentation({ repoRoot, readModel: store }),
2595
3144
  ]);
2596
3145
  const layout = layoutTodoGantt(store, chain, { scope, independence, seamProposals });
2597
3146
  // When the diagram hides history, the page also carries the full diagram so
@@ -2626,6 +3175,7 @@ export async function renderTodoGanttForProject({
2626
3175
  chain_digest: digestTodoArtifact(chain),
2627
3176
  layout_digest: digestTodoArtifact(layout),
2628
3177
  note_bindings_digest: digestTodoArtifact(notes.headBindings),
3178
+ structure_projection_digest: structures.projection_digest,
2629
3179
  renderer_version: TODO_GANTT_RENDERER_VERSION,
2630
3180
  project_display_name: identity.displayName,
2631
3181
  folded_task_count: layout.scope.folded_task_count,
@@ -2640,6 +3190,7 @@ export async function renderTodoGanttForProject({
2640
3190
  metadata,
2641
3191
  noteContexts: notes.contexts,
2642
3192
  noteWarnings: notes.warnings,
3193
+ structurePresentation: structures,
2643
3194
  });
2644
3195
  return { store, metadata, memberBindings, rendered };
2645
3196
  }
@@ -2712,6 +3263,8 @@ function writesTodoStore(argv) {
2712
3263
  case 'snapshot': return argv.includes('--rebuild');
2713
3264
  case 'independence': return second === 'compile'
2714
3265
  || (second === 'witness' && ['migrate', 'scaffold'].includes(third));
3266
+ case 'structure': return ['compile', 'realize', 'finalize'].includes(second)
3267
+ || (second === 'input' && !argv.includes('--dry-run'));
2715
3268
  case 'seam-proposal': return ['compile', 'apply', 'land'].includes(second);
2716
3269
  case 'evidence': return second === 'promote';
2717
3270
  case 'dependency': return second === 'connect';
@@ -2876,6 +3429,22 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
2876
3429
  });
2877
3430
  }
2878
3431
 
3432
+ if (argv[0] === 'structure' && ['input', 'compile'].includes(argv[1])
3433
+ && argv[4] === '--input' && typeof argv[5] === 'string' && path.isAbsolute(argv[5])) {
3434
+ return typedArgumentFailure(stderr, 'INPUT_OUTSIDE_REPOSITORY', 'absolute_input_path_rejected', {
3435
+ argument: '--input', expected: 'repo-relative path', actual: 'absolute path',
3436
+ next_action: 'place_the_input_inside_the_repository_and_pass_a_repo_relative_path',
3437
+ });
3438
+ }
3439
+
3440
+ if (argv[0] === 'structure' && argv[1] === 'realize'
3441
+ && argv[6] === '--input' && typeof argv[7] === 'string' && path.isAbsolute(argv[7])) {
3442
+ return typedArgumentFailure(stderr, 'INPUT_OUTSIDE_REPOSITORY', 'absolute_input_path_rejected', {
3443
+ argument: '--input', expected: 'repo-relative path', actual: 'absolute path',
3444
+ next_action: 'place_the_input_inside_the_repository_and_pass_a_repo_relative_path',
3445
+ });
3446
+ }
3447
+
2879
3448
  // `--schema --json`はstoreを読まない決定的な出力(`plan create --schema`と同じ規律)。
2880
3449
  // 通常dispatchより前に処理し、repoRoot解決やdashboard daemon起動を経由させない。
2881
3450
  if (argv.length === 3 && argv[1] === '--schema' && argv[2] === '--json'
@@ -2986,6 +3555,40 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
2986
3555
  action = (repoRoot) => independenceCompile({
2987
3556
  repoRoot, planKey: argv[3], inputRef: argv[5],
2988
3557
  });
3558
+ } else if (argv.length === 8 && argv[0] === 'structure' && argv[1] === 'input'
3559
+ && argv[2] === '--plan' && isTodoIdentifier(argv[3])
3560
+ && argv[4] === '--input' && isTodoRef(argv[5])
3561
+ && argv[6] === '--dry-run' && argv[7] === '--json') {
3562
+ action = (repoRoot) => structureInputDryRun({
3563
+ repoRoot, planKey: argv[3], inputRef: argv[5],
3564
+ });
3565
+ } else if (argv.length === 6 && argv[0] === 'structure' && argv[1] === 'input'
3566
+ && argv[2] === '--plan' && isTodoIdentifier(argv[3])
3567
+ && argv[4] === '--input' && isTodoRef(argv[5])) {
3568
+ action = (repoRoot) => structureInput({
3569
+ repoRoot, planKey: argv[3], inputRef: argv[5],
3570
+ });
3571
+ } else if (argv.length === 6 && argv[0] === 'structure' && argv[1] === 'compile'
3572
+ && argv[2] === '--plan' && isTodoIdentifier(argv[3])
3573
+ && argv[4] === '--input' && isTodoRef(argv[5])) {
3574
+ action = (repoRoot) => structureCompile({
3575
+ repoRoot, env, planKey: argv[3], inputRef: argv[5],
3576
+ });
3577
+ } else if (argv.length === 8 && argv[0] === 'structure' && argv[1] === 'realize'
3578
+ && argv[2] === '--plan' && isTodoIdentifier(argv[3])
3579
+ && argv[4] === '--task' && isTodoIdentifier(argv[5])
3580
+ && argv[6] === '--input' && isTodoRef(argv[7])) {
3581
+ action = (repoRoot) => structureRealize({
3582
+ repoRoot, env, planKey: argv[3], taskId: argv[5], inputRef: argv[7],
3583
+ });
3584
+ } else if (argv.length === 5 && argv[0] === 'structure' && argv[1] === 'finalize'
3585
+ && argv[2] === '--plan' && isTodoIdentifier(argv[3]) && argv[4] === '--json') {
3586
+ action = (repoRoot) => structureFinalize({ repoRoot, env, planKey: argv[3] });
3587
+ } else if (argv.length === 2 && argv[0] === 'structure' && argv[1] === '--json') {
3588
+ action = (repoRoot) => structure({ repoRoot, requestedPlanKey: null });
3589
+ } else if (argv.length === 4 && argv[0] === 'structure'
3590
+ && argv[1] === '--plan' && isTodoIdentifier(argv[2]) && argv[3] === '--json') {
3591
+ action = (repoRoot) => structure({ repoRoot, requestedPlanKey: argv[2] });
2989
3592
  } else if ((argv.length === 1 && argv[0] === 'independence')
2990
3593
  || (argv.length === 2 && argv[0] === 'independence' && argv[1] === '--json')) {
2991
3594
  action = (repoRoot) => independence({ repoRoot, requestedPlanKey: null });