@quolu/lattice 0.52.3 → 0.53.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.
Files changed (188) hide show
  1. package/LICENSE +147 -147
  2. package/README.ja.md +355 -355
  3. package/README.md +258 -258
  4. package/bin/lattice-bridge.mjs +25 -0
  5. package/bin/lattice-hub.mjs +67 -0
  6. package/bin/lattice-mcp.mjs +0 -0
  7. package/bin/lattice-scripted-adapter.mjs +0 -0
  8. package/bin/lattice-scripted-worker.mjs +0 -0
  9. package/bin/lattice-work-order-adapter.mjs +0 -0
  10. package/bin/lattice.mjs +0 -0
  11. package/docs/bridge-setup.md +132 -132
  12. package/docs/schemas/lattice.executor_packet.v1.schema.json +57 -57
  13. package/docs/schemas/lattice.executor_receipt.v1.schema.json +66 -66
  14. package/docs/schemas/lattice.phase_todo_revision.v3.schema.json +360 -360
  15. package/docs/schemas/lattice.plan_create_input.v1.schema.json +56 -56
  16. package/docs/schemas/lattice.plan_create_input.v2.schema.json +72 -72
  17. package/docs/schemas/lattice.plan_create_input.v3.schema.json +81 -81
  18. package/docs/schemas/lattice.plan_create_input.v4.schema.json +85 -85
  19. package/docs/schemas/lattice.run_request.v1.schema.json +238 -238
  20. package/docs/schemas/lattice.runtime_adapter_capabilities.v2.schema.json +55 -55
  21. package/docs/schemas/lattice.runtime_adapter_registration_input.v1.schema.json +78 -78
  22. package/docs/schemas/lattice.runtime_adapter_registration_input.v2.schema.json +86 -86
  23. package/docs/schemas/lattice.todo_extraction.v2.schema.json +298 -298
  24. package/docs/schemas/lattice.todo_extraction.v3.schema.json +146 -146
  25. package/docs/schemas/lattice.todo_revision.v2.schema.json +260 -260
  26. package/docs/schemas/lattice.todo_revision_set.v3.schema.json +363 -363
  27. package/package.json +103 -103
  28. package/sensor/LICENSE +21 -21
  29. package/sensor/NOTICE +19 -19
  30. package/sensor/dist/bin/lattice-sensor.js +9 -9
  31. package/sensor/dist/db/index.js +24 -24
  32. package/sensor/dist/db/migrations.js +41 -41
  33. package/sensor/dist/db/queries.js +164 -164
  34. package/sensor/dist/db/schema.sql +205 -205
  35. package/sensor/dist/directory.js +5 -5
  36. package/sensor/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
  37. package/sensor/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
  38. package/sensor/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
  39. package/sensor/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
  40. package/sensor/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
  41. package/sensor/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
  42. package/sensor/dist/extraction/wasm/tree-sitter-go.wasm +0 -0
  43. package/sensor/dist/extraction/wasm/tree-sitter-java.wasm +0 -0
  44. package/sensor/dist/extraction/wasm/tree-sitter-javascript.wasm +0 -0
  45. package/sensor/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
  46. package/sensor/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
  47. package/sensor/dist/extraction/wasm/tree-sitter-python.wasm +0 -0
  48. package/sensor/dist/extraction/wasm/tree-sitter-tsx.wasm +0 -0
  49. package/sensor/dist/extraction/wasm/tree-sitter-typescript.wasm +0 -0
  50. package/sensor/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
  51. package/sensor/dist/mcp/liveness-watchdog.js +53 -53
  52. package/sensor/dist/mcp/server-instructions.js +95 -95
  53. package/sensor/package.json +56 -56
  54. package/src/artifact-contracts-v2.mjs +325 -325
  55. package/src/artifact-contracts.mjs +895 -895
  56. package/src/boundary-compiler.mjs +712 -712
  57. package/src/boundary-observation-compiler-v2.mjs +344 -344
  58. package/src/bounded-seam.mjs +230 -230
  59. package/src/bridge-address.mjs +107 -107
  60. package/src/bridge-cli.mjs +303 -297
  61. package/src/bridge-config.mjs +382 -346
  62. package/src/bridge-daemon.mjs +378 -378
  63. package/src/bridge-hub-heartbeat.mjs +170 -0
  64. package/src/bridge-hub-protocol.mjs +198 -0
  65. package/src/bridge-hub-server.mjs +544 -0
  66. package/src/bridge-launch-agent.mjs +323 -323
  67. package/src/bridge-registrar.mjs +102 -102
  68. package/src/bridge-server.mjs +376 -376
  69. package/src/cli-help.mjs +308 -308
  70. package/src/cli-stdio.mjs +40 -40
  71. package/src/control-compiler.mjs +532 -532
  72. package/src/dag-chain.mjs +261 -261
  73. package/src/factory-diagnostics.mjs +188 -188
  74. package/src/git-process.mjs +72 -0
  75. package/src/hash-chain.mjs +76 -76
  76. package/src/hooks-cli.mjs +1057 -1057
  77. package/src/isolation-runner.mjs +409 -409
  78. package/src/node-version-guard.mjs +44 -44
  79. package/src/project-cli.mjs +3 -3
  80. package/src/project-identity.mjs +130 -130
  81. package/src/rc1-black-box-oracle.mjs +906 -906
  82. package/src/rc1-comparison.mjs +154 -154
  83. package/src/rc1-evidence-bundle.mjs +456 -456
  84. package/src/rc1-v4-campaign.mjs +708 -708
  85. package/src/rc1-v4-transform.mjs +467 -467
  86. package/src/rc1-v5-artifact-set.mjs +855 -855
  87. package/src/rc1-v5-behavior-evidence.mjs +594 -594
  88. package/src/rc1-v5-campaign.mjs +797 -797
  89. package/src/rc1-v5-transform.mjs +421 -421
  90. package/src/rc1-v6-artifact-set.mjs +807 -807
  91. package/src/rc1-v6-behavior-evidence.mjs +273 -273
  92. package/src/rc1-v6-campaign.mjs +623 -623
  93. package/src/rc1-v6-causal-binding.mjs +473 -473
  94. package/src/rc1-v6-measurement.mjs +313 -313
  95. package/src/rc2-artifact-set.mjs +1584 -1584
  96. package/src/rc2-campaign.mjs +1506 -1506
  97. package/src/rc2-delivery-policy-front-end.mjs +1079 -1079
  98. package/src/rc2-delivery-policy-oracle.mjs +134 -134
  99. package/src/rc2-delivery-policy-transform.mjs +1127 -1127
  100. package/src/rc2-rc1-transfer-front-end.mjs +511 -511
  101. package/src/rc3-actual-dogfood.mjs +652 -652
  102. package/src/rc3-dogfood-scaffold.mjs +315 -315
  103. package/src/rc3-scripted-campaign.mjs +1383 -1383
  104. package/src/rc4-stage1-dogfood.mjs +673 -673
  105. package/src/runtime-adapter-registry.mjs +524 -524
  106. package/src/runtime-cli.mjs +4588 -4588
  107. package/src/runtime-contracts.mjs +824 -824
  108. package/src/runtime-control-store.mjs +604 -604
  109. package/src/runtime-controller-protocol.mjs +587 -587
  110. package/src/runtime-decision-verifier.mjs +701 -701
  111. package/src/runtime-diff-observer.mjs +361 -361
  112. package/src/runtime-direct-os-observer.mjs +301 -301
  113. package/src/runtime-driver-state.mjs +166 -166
  114. package/src/runtime-engine.mjs +779 -779
  115. package/src/runtime-errors.mjs +356 -356
  116. package/src/runtime-event-store.mjs +189 -189
  117. package/src/runtime-front-end.mjs +925 -925
  118. package/src/runtime-gate-store.mjs +481 -481
  119. package/src/runtime-hold-recompile.mjs +916 -916
  120. package/src/runtime-io-sentinel.mjs +391 -391
  121. package/src/runtime-lifecycle-lock.mjs +294 -294
  122. package/src/runtime-managed-supervisor.mjs +1490 -1490
  123. package/src/runtime-multi-epoch-store.mjs +838 -838
  124. package/src/runtime-projection.mjs +269 -269
  125. package/src/runtime-pull-intake.mjs +1192 -1192
  126. package/src/runtime-scripted-adapter-controller.mjs +1160 -1160
  127. package/src/runtime-scripted-executor.mjs +163 -163
  128. package/src/runtime-scripted-worktree.mjs +104 -104
  129. package/src/runtime-seam-resolve.mjs +428 -428
  130. package/src/runtime-seam-treatment.mjs +173 -173
  131. package/src/runtime-socket-owner.mjs +125 -125
  132. package/src/runtime-work-order-contracts.mjs +91 -91
  133. package/src/runtime-work-order-controller.mjs +1171 -1171
  134. package/src/runtime-worktree-executor.mjs +199 -199
  135. package/src/schedulability-compiler-v2.mjs +303 -303
  136. package/src/schedulability-verifier-v2.mjs +317 -317
  137. package/src/seam-apply.mjs +549 -549
  138. package/src/seam-commit-shared.mjs +22 -22
  139. package/src/seam-commit-transform.mjs +81 -81
  140. package/src/seam-commit.mjs +18 -18
  141. package/src/seam-cost.mjs +322 -322
  142. package/src/seam-derivation.mjs +188 -188
  143. package/src/seam-gate.mjs +146 -146
  144. package/src/seam-proposal-contracts.mjs +446 -446
  145. package/src/seam-proposal-queries.mjs +521 -521
  146. package/src/seam-proposal.mjs +2011 -2011
  147. package/src/seam-ref.mjs +33 -33
  148. package/src/seam-rewrite.mjs +286 -286
  149. package/src/seam-transform.mjs +554 -554
  150. package/src/seam-verification.mjs +260 -260
  151. package/src/sensor-adapter.mjs +432 -432
  152. package/src/sensor-cli.mjs +139 -139
  153. package/src/sensor-diff.mjs +661 -661
  154. package/src/sensor-node-runtime.mjs +53 -53
  155. package/src/sensor-runtime.mjs +52 -52
  156. package/src/timestamp-contract.mjs +8 -8
  157. package/src/todo-audit-pending.mjs +91 -91
  158. package/src/todo-chain.mjs +178 -178
  159. package/src/todo-cli.mjs +8 -8
  160. package/src/todo-contracts.mjs +728 -728
  161. package/src/todo-dashboard-registry.mjs +573 -573
  162. package/src/todo-dispatch-shape.mjs +190 -190
  163. package/src/todo-gantt-html-independence.mjs +239 -239
  164. package/src/todo-gantt-html-shared.mjs +226 -226
  165. package/src/todo-gantt-html-style.mjs +131 -131
  166. package/src/todo-gantt-html.mjs +248 -248
  167. package/src/todo-gantt-layout.mjs +974 -974
  168. package/src/todo-gantt-live.mjs +361 -361
  169. package/src/todo-gantt-nested.mjs +263 -263
  170. package/src/todo-gantt-presentation.mjs +217 -217
  171. package/src/todo-gantt-scope.mjs +123 -123
  172. package/src/todo-gantt-svg.mjs +353 -353
  173. package/src/todo-independence-contracts.mjs +595 -595
  174. package/src/todo-independence-guidance.mjs +322 -322
  175. package/src/todo-independence.mjs +640 -640
  176. package/src/todo-markdown-renderer.mjs +260 -260
  177. package/src/todo-migration.mjs +448 -448
  178. package/src/todo-narrative-anchor.mjs +130 -130
  179. package/src/todo-note-store.mjs +629 -629
  180. package/src/todo-parallel-candidates.mjs +114 -114
  181. package/src/todo-revision.mjs +995 -995
  182. package/src/todo-split.mjs +472 -472
  183. package/src/todo-status.mjs +690 -690
  184. package/src/todo-store-git-transaction.mjs +418 -418
  185. package/src/todo-store.mjs +42 -21
  186. package/src/treatment-compiler.mjs +728 -728
  187. package/src/treatment-runner.mjs +656 -656
  188. package/src/witness-scaffold.mjs +180 -180
@@ -1,248 +1,248 @@
1
- import { createHash } from 'node:crypto';
2
-
3
- import { auditPendingPhasesOf } from './todo-audit-pending.mjs';
4
- import { serializeJsonForScript } from './todo-markdown-renderer.mjs';
5
- import { renderTodoGanttSvg, TODO_GANTT_STATUS_PRESENTATION } from './todo-gantt-svg.mjs';
6
- import { renderDiagramLegend, renderRightPane } from './todo-gantt-html-independence.mjs';
7
- import { escapeHtmlAttribute, escapeHtmlText, refKey } from './todo-gantt-html-shared.mjs';
8
- import { CSS, NESTED_CSS } from './todo-gantt-html-style.mjs';
9
-
10
- export const TODO_GANTT_RENDERER_VERSION = 'lattice.todo_gantt_renderer.v19';
11
- export const TODO_GANTT_PROSE_MAX_BYTES = 8 * 1024 * 1024;
12
- export const TODO_GANTT_HTML_MAX_BYTES = 24 * 1024 * 1024;
13
-
14
- function projectDisplayName(readModel, metadata) {
15
- for (const candidate of [metadata?.project_display_name, metadata?.project_name]) {
16
- if (typeof candidate === 'string' && candidate.trim() !== '') return candidate.trim();
17
- }
18
- return readModel.project_id;
19
- }
20
-
21
- /**
22
- * 監査待ちPhaseをヘッダへ出す(ADR 0147/0148)。
23
- *
24
- * CLIのstdoutは1行のversioned JSONというADR 0049の公理があり、人が読むテキスト面が無い。
25
- * 人が実際に見るのはこの図とdashboard(同じHTMLを配信する)なので、監査待ちはここへ出す。
26
- * 全taskがdoneでも監査が済んでいなければ工程は閉じていない——図が「全部緑」に見えるのに
27
- * ヘッダが無言なら、読み手は完了したと読む。
28
- *
29
- * 判断の着いたPhase(accepted/closed_unaudited)しか無ければ、この札は出ない。
30
- *
31
- * **出す文字列を有界にする。** ツールバーはpane幅で制約されていないので、長い札は縮まずに
32
- * 行を押し広げ、ズーム操作を画面外へ追い出す(2026-08-08に実測)。CSSの`text-overflow`は
33
- * ここでは発動しないため、頼れない。全plan名を並べる代わりに先頭1件+残件数だけを出し、
34
- * 全文は`title`に残す。件数(`監査待ち N件`)は常に見える——省略するのは内訳であって、
35
- * 監査待ちの存在そのものではない。
36
- */
37
- function renderAuditPendingChip(readModel) {
38
- const pending = auditPendingPhasesOf(readModel);
39
- if (pending.length === 0) return '';
40
- const label = ({ plan_key: planKey, phase_id: phaseId, status }) => `${planKey}/${phaseId} (${status})`;
41
- // 残件数は書かない——先頭の`N件`が既に全体を数えており、「ほかM件」はその引き算でしかない。
42
- // 幅は有限なので、同じことを二度言う分だけplan名が削れる。
43
- const text = `監査待ち ${pending.length}件: ${label(pending[0])}`;
44
- const full = `監査待ち ${pending.length}件: ${pending.map(label).join(' · ')}`;
45
- return `<span class="audit-pending-chip" title="${escapeHtmlAttribute(full)}">${escapeHtmlText(text)}</span>`;
46
- }
47
-
48
- export class TodoGanttRenderError extends Error {
49
- constructor(code, message, detail = {}) {
50
- super(message);
51
- this.name = 'TodoGanttRenderError';
52
- this.code = code;
53
- this.detail = detail;
54
- }
55
- }
56
-
57
- function digest(value) {
58
- return createHash('sha256').update(value, 'utf8').digest('hex');
59
- }
60
-
61
- /**
62
- * Sections carry one row per ToDo, plus the prose accounting for the narratives
63
- * behind them.
64
- *
65
- * The right pane shows the store, not the Markdown: per the 2026-07-19 ruling
66
- * the "全工程" list is the plan rendered from the ToDo store, and the original
67
- * documents are reached through each ToDo's source reference. The narratives are
68
- * still read, because anchor verification needs them, and their size is still
69
- * bounded — but they are not rendered into the page.
70
- */
71
- function normalizeSections(readModel, narratives, anchorOutcomes, noteContexts) {
72
- const supplied = new Map(narratives.map((entry) => [refKey(entry.ref), entry]));
73
- const outcomes = new Map(anchorOutcomes.map((entry) => [refKey(entry.ref), entry]));
74
- const notes = noteContexts === null ? null : new Map(noteContexts.map((entry) => [
75
- refKey({ project_id: entry.project_id, plan_key: entry.plan_key, task_id: entry.task_id }), entry,
76
- ]));
77
- const result = [];
78
- const counted = new Set();
79
- let proseBytes = 0;
80
- for (const member of readModel.members) {
81
- const states = new Map(member.tasks.map((state) => [state.task_id, state]));
82
- for (const task of member.plan.tasks) {
83
- const ref = { project_id: member.plan.project_id, plan_key: member.plan.plan_key, task_id: task.task_id };
84
- const narrative = supplied.get(refKey(ref));
85
- const markdown = narrative?.markdown ?? '';
86
- const narrativeRef = narrative?.narrative_ref ?? task.narrative_ref;
87
- if (typeof task.design_memo === 'string') {
88
- proseBytes += Buffer.byteLength(task.design_memo, 'utf8');
89
- if (proseBytes > TODO_GANTT_PROSE_MAX_BYTES) {
90
- throw new TodoGanttRenderError('TODO_SCALE_EXCEEDED', 'todo gantt embedded prose limit exceeded', {
91
- prose_bytes: proseBytes, prose_limit: TODO_GANTT_PROSE_MAX_BYTES,
92
- });
93
- }
94
- }
95
- // ToDos that share one narrative document count its bytes once.
96
- const documentKey = narrativeRef === null
97
- ? refKey(ref) : JSON.stringify([member.plan.plan_key, narrativeRef, digest(markdown)]);
98
- if (!counted.has(documentKey)) {
99
- counted.add(documentKey);
100
- proseBytes += Buffer.byteLength(markdown, 'utf8');
101
- if (proseBytes > TODO_GANTT_PROSE_MAX_BYTES) {
102
- throw new TodoGanttRenderError('TODO_SCALE_EXCEEDED', 'todo gantt embedded prose limit exceeded', {
103
- prose_bytes: proseBytes, prose_limit: TODO_GANTT_PROSE_MAX_BYTES,
104
- });
105
- }
106
- }
107
- const anchorOutcome = outcomes.get(refKey(ref)) ?? {
108
- ref, narrative_ref: narrativeRef, anchored: false, reason: 'anchor_missing',
109
- origin_line: task.narrative_anchor?.origin_line ?? null,
110
- };
111
- const noteContext = notes?.get(refKey(ref)) ?? null;
112
- if (noteContext !== null) {
113
- proseBytes += noteContext.notes.reduce((bytes, note) => (
114
- bytes + Buffer.byteLength(note.body, 'utf8')
115
- ), 0);
116
- if (proseBytes > TODO_GANTT_PROSE_MAX_BYTES) {
117
- throw new TodoGanttRenderError('TODO_SCALE_EXCEEDED', 'todo gantt embedded prose limit exceeded', {
118
- prose_bytes: proseBytes, prose_limit: TODO_GANTT_PROSE_MAX_BYTES,
119
- });
120
- }
121
- }
122
- result.push({
123
- ref, task, state: states.get(task.task_id), narrativeRef, anchorOutcome, noteContext,
124
- });
125
- }
126
- }
127
- return { sections: result, proseBytes };
128
- }
129
-
130
- const CONTROLLER = `
131
- (()=>{
132
- const root=document.querySelector('[data-gantt-root]');if(!root)return;
133
- const shell=root.querySelector('.shell');
134
- const divider=root.querySelector('[data-pane-divider]');
135
- const overviewPanel=root.querySelector('[data-right-panel="overview"]');
136
- const detailsPanel=root.querySelector('[data-right-panel="details"]');
137
- const taskIndexPanel=root.querySelector('[data-right-panel="task-index"]');
138
- const selectedReturnButton=root.querySelector('[data-show-selected]');
139
- const detailPanels=[...root.querySelectorAll('[data-detail-key]')];
140
- const nodes=[...root.querySelectorAll('[data-node-key]')];
141
- const edges=[...root.querySelectorAll('[data-edge-id]')];
142
- const laneChips=[...root.querySelectorAll('.summary-lane[data-lane-key]')];
143
- const diagrams=[...root.querySelectorAll('[data-diagram]')];
144
- const expandToggle=root.querySelector('[data-toggle-expanded]');
145
- const toggleLabel=root.querySelector('[data-toggle-label]');
146
- const scroller=root.querySelector('[data-diagram-scroll]');
147
- const zoomOutput=root.querySelector('[data-zoom-output]');
148
- let svg=root.querySelector('[data-diagram]:not([hidden]) [data-gantt-svg]')??root.querySelector('[data-gantt-svg]');
149
- let baseWidth=Number(svg?.dataset.svgWidth??0);
150
- let baseHeight=Number(svg?.dataset.svgHeight??0);
151
- let zoom=1;let activeLaneKey=null;let selectedKey=null;let resizePointerId=null;let expanded=false;
152
- const stacked=()=>window.matchMedia('(max-width:900px)').matches;
153
- const setSplit=(clientX)=>{if(!shell||stacked())return;const bounds=shell.getBoundingClientRect();if(bounds.width<=0)return;const percent=Math.max(30,Math.min(75,(clientX-bounds.left)/bounds.width*100));shell.style.setProperty('--split',percent+'%');};
154
- const finishResize=(event)=>{if(event.pointerId!==resizePointerId)return;if(divider?.hasPointerCapture(event.pointerId))divider.releasePointerCapture(event.pointerId);resizePointerId=null;};
155
- const setZoom=(value,minimum=.2)=>{if(!svg||!Number.isFinite(value)||value<=0)return;zoom=Math.max(minimum,Math.min(2,value));svg.setAttribute('width',String(Math.max(1,Math.round(baseWidth*zoom))));svg.setAttribute('height',String(Math.max(1,Math.round(baseHeight*zoom))));if(zoomOutput){const percent=zoom>=.1?String(Math.round(zoom*100)):String(Number((zoom*100).toFixed(1)));zoomOutput.textContent=percent+'%';}};
156
- const applyLane=(key)=>{activeLaneKey=key;for(const chip of laneChips)chip.setAttribute('aria-pressed',String(chip.dataset.laneKey===key));for(const node of nodes)node.classList.toggle('lane-dimmed',key!==null&&node.dataset.laneKey!==key);for(const edge of edges)edge.classList.toggle('lane-dimmed',key!==null&&edge.dataset.fromLaneKey!==key&&edge.dataset.toLaneKey!==key);};
157
- const toggleLane=(key)=>applyLane(activeLaneKey===key?null:key);
158
- // Both diagrams ship in the page; the badge picks which one is on screen.
159
- const setExpanded=(next)=>{if(diagrams.length<2)return;expanded=next;for(const diagram of diagrams)diagram.hidden=(diagram.dataset.diagram==='expanded')!==expanded;svg=diagrams.find(diagram=>!diagram.hidden)?.querySelector('[data-gantt-svg]')??svg;baseWidth=Number(svg?.dataset.svgWidth??0);baseHeight=Number(svg?.dataset.svgHeight??0);if(expandToggle){expandToggle.setAttribute('aria-expanded',String(expanded));}if(toggleLabel){toggleLabel.textContent=expanded?toggleLabel.dataset.expandedLabel:toggleLabel.dataset.collapsedLabel;}setZoom(zoom);applyLane(activeLaneKey);syncSelection();scroller?.scrollTo(0,0);};
160
- const showPanel=(name)=>{if(overviewPanel)overviewPanel.hidden=name!=='overview';if(detailsPanel)detailsPanel.hidden=name!=='details';if(taskIndexPanel)taskIndexPanel.hidden=name!=='task-index';if(selectedReturnButton)selectedReturnButton.hidden=name!=='task-index'||selectedKey===null;root.dataset.viewState=name;};
161
- const syncSelection=()=>{for(const detail of detailPanels)detail.hidden=detail.dataset.detailKey!==selectedKey;for(const node of nodes){const selected=node.dataset.nodeKey===selectedKey;node.setAttribute('aria-selected',String(selected));node.classList.toggle('selected-node',selected);}for(const edge of edges){const selected=selectedKey!==null;edge.classList.toggle('selected-incident-edge',selected&&(edge.dataset.fromNodeKey===selectedKey||edge.dataset.toNodeKey===selectedKey));}};
162
- const showOverview=()=>{selectedKey=null;syncSelection();showPanel('overview');};
163
- const showTaskIndex=()=>showPanel('task-index');
164
- const showSelected=()=>{if(selectedKey===null){showOverview();return;}const target=detailPanels.find(detail=>detail.dataset.detailKey===selectedKey);showPanel('details');target?.scrollIntoView({block:'start'});};
165
- const select=(key)=>{const target=detailPanels.find(detail=>detail.dataset.detailKey===key);if(!target)return;selectedKey=key;syncSelection();showPanel('details');target.scrollIntoView({block:'start'});};
166
- root.addEventListener('click',event=>{
167
- const overviewButton=event.target.closest('[data-show-overview]');if(overviewButton&&root.contains(overviewButton)){showOverview();return;}
168
- const selectedButton=event.target.closest('[data-show-selected]');if(selectedButton&&root.contains(selectedButton)){showSelected();return;}
169
- const taskIndexButton=event.target.closest('[data-show-task-index]');if(taskIndexButton&&root.contains(taskIndexButton)){showTaskIndex();return;}
170
- const expandButton=event.target.closest('[data-toggle-expanded]');if(expandButton&&root.contains(expandButton)){setExpanded(!expanded);return;}
171
- const selectButton=event.target.closest('[data-select-node-key]');if(selectButton&&root.contains(selectButton)){select(selectButton.dataset.selectNodeKey);return;}
172
- const zoomButton=event.target.closest('[data-zoom-action]');if(zoomButton&&root.contains(zoomButton)){const action=zoomButton.dataset.zoomAction;if(action==='in')setZoom(zoom<1&&zoom*1.25>=1?1:zoom*1.25,.001);else if(action==='out')setZoom(zoom>1&&zoom/1.25<=1?1:zoom/1.25,.001);else if(action==='reset')setZoom(1);else if(action==='fit'&&scroller){setZoom(Math.min(1,(scroller.clientWidth-16)/baseWidth),.001);scroller.scrollTo(0,0);}return;}
173
- const laneChip=event.target.closest('.summary-lane[data-lane-key]');if(laneChip&&root.contains(laneChip)){toggleLane(laneChip.dataset.laneKey);return;}
174
- const node=event.target.closest('[data-node-key]');if(node&&root.contains(node))select(node.dataset.nodeKey);
175
- });
176
- root.addEventListener('dblclick',event=>{const paneDivider=event.target.closest('[data-pane-divider]');if(paneDivider&&root.contains(paneDivider)&&shell&&!stacked())shell.style.setProperty('--split','58%');});
177
- root.addEventListener('pointerdown',event=>{const paneDivider=event.target.closest('[data-pane-divider]');if(!paneDivider||!root.contains(paneDivider)||stacked())return;resizePointerId=event.pointerId;paneDivider.setPointerCapture(event.pointerId);setSplit(event.clientX);event.preventDefault();});
178
- root.addEventListener('pointermove',event=>{if(event.pointerId===resizePointerId)setSplit(event.clientX);});
179
- root.addEventListener('pointerup',finishResize);root.addEventListener('pointercancel',finishResize);
180
- root.addEventListener('keydown',event=>{if(event.key==='Escape'){event.preventDefault();showOverview();applyLane(null);return;}const laneChip=event.target.closest('.summary-lane[data-lane-key]');if(laneChip&&(event.key==='Enter'||event.key===' ')){event.preventDefault();toggleLane(laneChip.dataset.laneKey);return;}const node=event.target.closest('[data-node-key]');if(node&&(event.key==='Enter'||event.key===' ')){event.preventDefault();select(node.dataset.nodeKey);}});
181
- // 記録時刻はUTCで埋まっている。閲覧者の地域とlocaleを知っているのはブラウザだけなので、
182
- // ここで現地時刻へ直す。scriptが動かなければ埋め込んだUTCがそのまま読める。
183
- for(const stamp of root.querySelectorAll('time[data-utc-stamp]')){
184
- const parsed=new Date(stamp.getAttribute('datetime'));
185
- if(!Number.isNaN(parsed.getTime()))stamp.textContent=parsed.toLocaleString();
186
- }
187
- showPanel('overview');
188
- })();
189
- `;
190
-
191
- const NESTED_CONTROLLER = `
192
- (()=>{
193
- const root=document.querySelector('[data-gantt-root]');if(!root)return;
194
- const toggles=[...root.querySelectorAll('[data-nested-toggle-for]')];
195
- const panels=[...root.querySelectorAll('[data-nested-panel-for]')];
196
- const panelFor=(key)=>panels.find(panel=>panel.dataset.nestedPanelFor===key);
197
- const toggle=(control)=>{const key=control.dataset.nestedToggleFor;const panel=panelFor(key);if(!panel)return;const open=panel.hasAttribute('hidden');panel.toggleAttribute('hidden',!open);const link=[...root.querySelectorAll('[data-nested-link-for]')].find(candidate=>candidate.dataset.nestedLinkFor===key);link?.toggleAttribute('hidden',!open);control.setAttribute('aria-expanded',String(open));const mark=control.querySelector('text');if(mark)mark.textContent=open?'−':'+';};
198
- root.addEventListener('click',event=>{const control=event.target.closest('[data-nested-toggle-for]');if(!control||!root.contains(control))return;event.preventDefault();event.stopPropagation();toggle(control);});
199
- root.addEventListener('keydown',event=>{const control=event.target.closest('[data-nested-toggle-for]');if(!control||!root.contains(control)||(event.key!=='Enter'&&event.key!==' '))return;event.preventDefault();event.stopPropagation();toggle(control);});
200
- })();
201
- `;
202
-
203
- export function renderTodoGanttHtml({
204
- readModel, layout, narratives = [], anchorOutcomes = [], presentation = null, metadata = {},
205
- expandedLayout = null, noteContexts = null, noteWarnings = [],
206
- }) {
207
- if (readModel?.schema !== 'lattice.todo_store_read.v1' || !Array.isArray(readModel.members)) {
208
- throw new TypeError('readModel must be lattice.todo_store_read.v1');
209
- }
210
- if (!Array.isArray(narratives)) throw new TypeError('narratives must be an array');
211
- if (!Array.isArray(anchorOutcomes)) throw new TypeError('anchorOutcomes must be an array');
212
- if (!(noteContexts === null || Array.isArray(noteContexts))) {
213
- throw new TypeError('noteContexts must be null or an array');
214
- }
215
- if (!Array.isArray(noteWarnings)) throw new TypeError('noteWarnings must be an array');
216
- if (presentation !== null
217
- && (presentation?.schema !== 'lattice.todo_gantt_presentation_model.v1'
218
- || presentation.project_id !== readModel.project_id)) {
219
- throw new TypeError('presentation must be lattice.todo_gantt_presentation_model.v1');
220
- }
221
- const normalized = normalizeSections(readModel, narratives, anchorOutcomes, noteContexts);
222
- const displayName = projectDisplayName(readModel, metadata);
223
- const hasHierarchy = layout?.hierarchy?.schema === 'lattice.todo_gantt_hierarchy.v1';
224
- const svg = renderTodoGanttSvg(layout, { presentation });
225
- // The expanded diagram travels with the page so the badge can bring the
226
- // history back without a round trip. A file:// artifact has nowhere to ask.
227
- const expandedSvg = expandedLayout === null ? '' : renderTodoGanttSvg(expandedLayout, { presentation });
228
- const diagrams = expandedSvg === ''
229
- ? `<div data-diagram="live">${svg}</div>`
230
- : `<div data-diagram="live">${svg}</div><div data-diagram="expanded" hidden>${expandedSvg}</div>`;
231
- const rightPane = renderRightPane(
232
- normalized.sections, layout, presentation, readModel, noteContexts !== null, noteWarnings,
233
- expandedSvg !== '',
234
- );
235
- const staticData = serializeJsonForScript({
236
- renderer_version: TODO_GANTT_RENDERER_VERSION,
237
- metadata,
238
- presentation,
239
- });
240
- const html = `<!doctype html><html lang="ja"><head><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Lattice — ${escapeHtmlText(displayName)} 依存工程図</title><style>${CSS}${hasHierarchy ? NESTED_CSS : ''}</style></head><body data-gantt-root data-view-state="overview"><main class="shell"><section class="gantt-pane" aria-label="${escapeHtmlAttribute(displayName)} 依存工程図"><div class="diagram-toolbar" role="group" aria-label="図のズーム"><strong class="project-heading">${escapeHtmlText(displayName)} 依存工程図</strong>${renderAuditPendingChip(readModel)}<button type="button" data-zoom-action="out" aria-label="縮小">−</button><button type="button" data-zoom-action="reset">等倍</button><button type="button" data-zoom-action="in" aria-label="拡大">+</button><button type="button" data-zoom-action="fit">全体表示</button><output class="zoom-readout" data-zoom-output aria-live="polite">100%</output><span class="diagram-note">縦=依存段階(時間ではない)</span></div>${renderDiagramLegend(presentation, layout, expandedSvg !== '')}<div class="diagram-scroll" data-diagram-scroll tabindex="0" aria-label="縦方向を主にスクロール可能な依存工程図">${diagrams}</div></section><div class="pane-divider" data-pane-divider aria-hidden="true"></div><aside class="narrative-pane" aria-label="選択工程の詳細と全工程一覧">${rightPane}</aside></main><script type="application/json" id="todo-gantt-data">${staticData}</script><script>${CONTROLLER}${hasHierarchy ? NESTED_CONTROLLER : ''}</script></body></html>`;
241
- const htmlBytes = Buffer.byteLength(html, 'utf8');
242
- if (htmlBytes > TODO_GANTT_HTML_MAX_BYTES) {
243
- throw new TodoGanttRenderError('TODO_SCALE_EXCEEDED', 'todo gantt HTML limit exceeded', {
244
- html_bytes: htmlBytes, html_limit: TODO_GANTT_HTML_MAX_BYTES,
245
- });
246
- }
247
- return { html, html_digest: digest(html), prose_bytes: normalized.proseBytes, html_bytes: htmlBytes };
248
- }
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import { auditPendingPhasesOf } from './todo-audit-pending.mjs';
4
+ import { serializeJsonForScript } from './todo-markdown-renderer.mjs';
5
+ import { renderTodoGanttSvg, TODO_GANTT_STATUS_PRESENTATION } from './todo-gantt-svg.mjs';
6
+ import { renderDiagramLegend, renderRightPane } from './todo-gantt-html-independence.mjs';
7
+ import { escapeHtmlAttribute, escapeHtmlText, refKey } from './todo-gantt-html-shared.mjs';
8
+ import { CSS, NESTED_CSS } from './todo-gantt-html-style.mjs';
9
+
10
+ export const TODO_GANTT_RENDERER_VERSION = 'lattice.todo_gantt_renderer.v19';
11
+ export const TODO_GANTT_PROSE_MAX_BYTES = 8 * 1024 * 1024;
12
+ export const TODO_GANTT_HTML_MAX_BYTES = 24 * 1024 * 1024;
13
+
14
+ function projectDisplayName(readModel, metadata) {
15
+ for (const candidate of [metadata?.project_display_name, metadata?.project_name]) {
16
+ if (typeof candidate === 'string' && candidate.trim() !== '') return candidate.trim();
17
+ }
18
+ return readModel.project_id;
19
+ }
20
+
21
+ /**
22
+ * 監査待ちPhaseをヘッダへ出す(ADR 0147/0148)。
23
+ *
24
+ * CLIのstdoutは1行のversioned JSONというADR 0049の公理があり、人が読むテキスト面が無い。
25
+ * 人が実際に見るのはこの図とdashboard(同じHTMLを配信する)なので、監査待ちはここへ出す。
26
+ * 全taskがdoneでも監査が済んでいなければ工程は閉じていない——図が「全部緑」に見えるのに
27
+ * ヘッダが無言なら、読み手は完了したと読む。
28
+ *
29
+ * 判断の着いたPhase(accepted/closed_unaudited)しか無ければ、この札は出ない。
30
+ *
31
+ * **出す文字列を有界にする。** ツールバーはpane幅で制約されていないので、長い札は縮まずに
32
+ * 行を押し広げ、ズーム操作を画面外へ追い出す(2026-08-08に実測)。CSSの`text-overflow`は
33
+ * ここでは発動しないため、頼れない。全plan名を並べる代わりに先頭1件+残件数だけを出し、
34
+ * 全文は`title`に残す。件数(`監査待ち N件`)は常に見える——省略するのは内訳であって、
35
+ * 監査待ちの存在そのものではない。
36
+ */
37
+ function renderAuditPendingChip(readModel) {
38
+ const pending = auditPendingPhasesOf(readModel);
39
+ if (pending.length === 0) return '';
40
+ const label = ({ plan_key: planKey, phase_id: phaseId, status }) => `${planKey}/${phaseId} (${status})`;
41
+ // 残件数は書かない——先頭の`N件`が既に全体を数えており、「ほかM件」はその引き算でしかない。
42
+ // 幅は有限なので、同じことを二度言う分だけplan名が削れる。
43
+ const text = `監査待ち ${pending.length}件: ${label(pending[0])}`;
44
+ const full = `監査待ち ${pending.length}件: ${pending.map(label).join(' · ')}`;
45
+ return `<span class="audit-pending-chip" title="${escapeHtmlAttribute(full)}">${escapeHtmlText(text)}</span>`;
46
+ }
47
+
48
+ export class TodoGanttRenderError extends Error {
49
+ constructor(code, message, detail = {}) {
50
+ super(message);
51
+ this.name = 'TodoGanttRenderError';
52
+ this.code = code;
53
+ this.detail = detail;
54
+ }
55
+ }
56
+
57
+ function digest(value) {
58
+ return createHash('sha256').update(value, 'utf8').digest('hex');
59
+ }
60
+
61
+ /**
62
+ * Sections carry one row per ToDo, plus the prose accounting for the narratives
63
+ * behind them.
64
+ *
65
+ * The right pane shows the store, not the Markdown: per the 2026-07-19 ruling
66
+ * the "全工程" list is the plan rendered from the ToDo store, and the original
67
+ * documents are reached through each ToDo's source reference. The narratives are
68
+ * still read, because anchor verification needs them, and their size is still
69
+ * bounded — but they are not rendered into the page.
70
+ */
71
+ function normalizeSections(readModel, narratives, anchorOutcomes, noteContexts) {
72
+ const supplied = new Map(narratives.map((entry) => [refKey(entry.ref), entry]));
73
+ const outcomes = new Map(anchorOutcomes.map((entry) => [refKey(entry.ref), entry]));
74
+ const notes = noteContexts === null ? null : new Map(noteContexts.map((entry) => [
75
+ refKey({ project_id: entry.project_id, plan_key: entry.plan_key, task_id: entry.task_id }), entry,
76
+ ]));
77
+ const result = [];
78
+ const counted = new Set();
79
+ let proseBytes = 0;
80
+ for (const member of readModel.members) {
81
+ const states = new Map(member.tasks.map((state) => [state.task_id, state]));
82
+ for (const task of member.plan.tasks) {
83
+ const ref = { project_id: member.plan.project_id, plan_key: member.plan.plan_key, task_id: task.task_id };
84
+ const narrative = supplied.get(refKey(ref));
85
+ const markdown = narrative?.markdown ?? '';
86
+ const narrativeRef = narrative?.narrative_ref ?? task.narrative_ref;
87
+ if (typeof task.design_memo === 'string') {
88
+ proseBytes += Buffer.byteLength(task.design_memo, 'utf8');
89
+ if (proseBytes > TODO_GANTT_PROSE_MAX_BYTES) {
90
+ throw new TodoGanttRenderError('TODO_SCALE_EXCEEDED', 'todo gantt embedded prose limit exceeded', {
91
+ prose_bytes: proseBytes, prose_limit: TODO_GANTT_PROSE_MAX_BYTES,
92
+ });
93
+ }
94
+ }
95
+ // ToDos that share one narrative document count its bytes once.
96
+ const documentKey = narrativeRef === null
97
+ ? refKey(ref) : JSON.stringify([member.plan.plan_key, narrativeRef, digest(markdown)]);
98
+ if (!counted.has(documentKey)) {
99
+ counted.add(documentKey);
100
+ proseBytes += Buffer.byteLength(markdown, 'utf8');
101
+ if (proseBytes > TODO_GANTT_PROSE_MAX_BYTES) {
102
+ throw new TodoGanttRenderError('TODO_SCALE_EXCEEDED', 'todo gantt embedded prose limit exceeded', {
103
+ prose_bytes: proseBytes, prose_limit: TODO_GANTT_PROSE_MAX_BYTES,
104
+ });
105
+ }
106
+ }
107
+ const anchorOutcome = outcomes.get(refKey(ref)) ?? {
108
+ ref, narrative_ref: narrativeRef, anchored: false, reason: 'anchor_missing',
109
+ origin_line: task.narrative_anchor?.origin_line ?? null,
110
+ };
111
+ const noteContext = notes?.get(refKey(ref)) ?? null;
112
+ if (noteContext !== null) {
113
+ proseBytes += noteContext.notes.reduce((bytes, note) => (
114
+ bytes + Buffer.byteLength(note.body, 'utf8')
115
+ ), 0);
116
+ if (proseBytes > TODO_GANTT_PROSE_MAX_BYTES) {
117
+ throw new TodoGanttRenderError('TODO_SCALE_EXCEEDED', 'todo gantt embedded prose limit exceeded', {
118
+ prose_bytes: proseBytes, prose_limit: TODO_GANTT_PROSE_MAX_BYTES,
119
+ });
120
+ }
121
+ }
122
+ result.push({
123
+ ref, task, state: states.get(task.task_id), narrativeRef, anchorOutcome, noteContext,
124
+ });
125
+ }
126
+ }
127
+ return { sections: result, proseBytes };
128
+ }
129
+
130
+ const CONTROLLER = `
131
+ (()=>{
132
+ const root=document.querySelector('[data-gantt-root]');if(!root)return;
133
+ const shell=root.querySelector('.shell');
134
+ const divider=root.querySelector('[data-pane-divider]');
135
+ const overviewPanel=root.querySelector('[data-right-panel="overview"]');
136
+ const detailsPanel=root.querySelector('[data-right-panel="details"]');
137
+ const taskIndexPanel=root.querySelector('[data-right-panel="task-index"]');
138
+ const selectedReturnButton=root.querySelector('[data-show-selected]');
139
+ const detailPanels=[...root.querySelectorAll('[data-detail-key]')];
140
+ const nodes=[...root.querySelectorAll('[data-node-key]')];
141
+ const edges=[...root.querySelectorAll('[data-edge-id]')];
142
+ const laneChips=[...root.querySelectorAll('.summary-lane[data-lane-key]')];
143
+ const diagrams=[...root.querySelectorAll('[data-diagram]')];
144
+ const expandToggle=root.querySelector('[data-toggle-expanded]');
145
+ const toggleLabel=root.querySelector('[data-toggle-label]');
146
+ const scroller=root.querySelector('[data-diagram-scroll]');
147
+ const zoomOutput=root.querySelector('[data-zoom-output]');
148
+ let svg=root.querySelector('[data-diagram]:not([hidden]) [data-gantt-svg]')??root.querySelector('[data-gantt-svg]');
149
+ let baseWidth=Number(svg?.dataset.svgWidth??0);
150
+ let baseHeight=Number(svg?.dataset.svgHeight??0);
151
+ let zoom=1;let activeLaneKey=null;let selectedKey=null;let resizePointerId=null;let expanded=false;
152
+ const stacked=()=>window.matchMedia('(max-width:900px)').matches;
153
+ const setSplit=(clientX)=>{if(!shell||stacked())return;const bounds=shell.getBoundingClientRect();if(bounds.width<=0)return;const percent=Math.max(30,Math.min(75,(clientX-bounds.left)/bounds.width*100));shell.style.setProperty('--split',percent+'%');};
154
+ const finishResize=(event)=>{if(event.pointerId!==resizePointerId)return;if(divider?.hasPointerCapture(event.pointerId))divider.releasePointerCapture(event.pointerId);resizePointerId=null;};
155
+ const setZoom=(value,minimum=.2)=>{if(!svg||!Number.isFinite(value)||value<=0)return;zoom=Math.max(minimum,Math.min(2,value));svg.setAttribute('width',String(Math.max(1,Math.round(baseWidth*zoom))));svg.setAttribute('height',String(Math.max(1,Math.round(baseHeight*zoom))));if(zoomOutput){const percent=zoom>=.1?String(Math.round(zoom*100)):String(Number((zoom*100).toFixed(1)));zoomOutput.textContent=percent+'%';}};
156
+ const applyLane=(key)=>{activeLaneKey=key;for(const chip of laneChips)chip.setAttribute('aria-pressed',String(chip.dataset.laneKey===key));for(const node of nodes)node.classList.toggle('lane-dimmed',key!==null&&node.dataset.laneKey!==key);for(const edge of edges)edge.classList.toggle('lane-dimmed',key!==null&&edge.dataset.fromLaneKey!==key&&edge.dataset.toLaneKey!==key);};
157
+ const toggleLane=(key)=>applyLane(activeLaneKey===key?null:key);
158
+ // Both diagrams ship in the page; the badge picks which one is on screen.
159
+ const setExpanded=(next)=>{if(diagrams.length<2)return;expanded=next;for(const diagram of diagrams)diagram.hidden=(diagram.dataset.diagram==='expanded')!==expanded;svg=diagrams.find(diagram=>!diagram.hidden)?.querySelector('[data-gantt-svg]')??svg;baseWidth=Number(svg?.dataset.svgWidth??0);baseHeight=Number(svg?.dataset.svgHeight??0);if(expandToggle){expandToggle.setAttribute('aria-expanded',String(expanded));}if(toggleLabel){toggleLabel.textContent=expanded?toggleLabel.dataset.expandedLabel:toggleLabel.dataset.collapsedLabel;}setZoom(zoom);applyLane(activeLaneKey);syncSelection();scroller?.scrollTo(0,0);};
160
+ const showPanel=(name)=>{if(overviewPanel)overviewPanel.hidden=name!=='overview';if(detailsPanel)detailsPanel.hidden=name!=='details';if(taskIndexPanel)taskIndexPanel.hidden=name!=='task-index';if(selectedReturnButton)selectedReturnButton.hidden=name!=='task-index'||selectedKey===null;root.dataset.viewState=name;};
161
+ const syncSelection=()=>{for(const detail of detailPanels)detail.hidden=detail.dataset.detailKey!==selectedKey;for(const node of nodes){const selected=node.dataset.nodeKey===selectedKey;node.setAttribute('aria-selected',String(selected));node.classList.toggle('selected-node',selected);}for(const edge of edges){const selected=selectedKey!==null;edge.classList.toggle('selected-incident-edge',selected&&(edge.dataset.fromNodeKey===selectedKey||edge.dataset.toNodeKey===selectedKey));}};
162
+ const showOverview=()=>{selectedKey=null;syncSelection();showPanel('overview');};
163
+ const showTaskIndex=()=>showPanel('task-index');
164
+ const showSelected=()=>{if(selectedKey===null){showOverview();return;}const target=detailPanels.find(detail=>detail.dataset.detailKey===selectedKey);showPanel('details');target?.scrollIntoView({block:'start'});};
165
+ const select=(key)=>{const target=detailPanels.find(detail=>detail.dataset.detailKey===key);if(!target)return;selectedKey=key;syncSelection();showPanel('details');target.scrollIntoView({block:'start'});};
166
+ root.addEventListener('click',event=>{
167
+ const overviewButton=event.target.closest('[data-show-overview]');if(overviewButton&&root.contains(overviewButton)){showOverview();return;}
168
+ const selectedButton=event.target.closest('[data-show-selected]');if(selectedButton&&root.contains(selectedButton)){showSelected();return;}
169
+ const taskIndexButton=event.target.closest('[data-show-task-index]');if(taskIndexButton&&root.contains(taskIndexButton)){showTaskIndex();return;}
170
+ const expandButton=event.target.closest('[data-toggle-expanded]');if(expandButton&&root.contains(expandButton)){setExpanded(!expanded);return;}
171
+ const selectButton=event.target.closest('[data-select-node-key]');if(selectButton&&root.contains(selectButton)){select(selectButton.dataset.selectNodeKey);return;}
172
+ const zoomButton=event.target.closest('[data-zoom-action]');if(zoomButton&&root.contains(zoomButton)){const action=zoomButton.dataset.zoomAction;if(action==='in')setZoom(zoom<1&&zoom*1.25>=1?1:zoom*1.25,.001);else if(action==='out')setZoom(zoom>1&&zoom/1.25<=1?1:zoom/1.25,.001);else if(action==='reset')setZoom(1);else if(action==='fit'&&scroller){setZoom(Math.min(1,(scroller.clientWidth-16)/baseWidth),.001);scroller.scrollTo(0,0);}return;}
173
+ const laneChip=event.target.closest('.summary-lane[data-lane-key]');if(laneChip&&root.contains(laneChip)){toggleLane(laneChip.dataset.laneKey);return;}
174
+ const node=event.target.closest('[data-node-key]');if(node&&root.contains(node))select(node.dataset.nodeKey);
175
+ });
176
+ root.addEventListener('dblclick',event=>{const paneDivider=event.target.closest('[data-pane-divider]');if(paneDivider&&root.contains(paneDivider)&&shell&&!stacked())shell.style.setProperty('--split','58%');});
177
+ root.addEventListener('pointerdown',event=>{const paneDivider=event.target.closest('[data-pane-divider]');if(!paneDivider||!root.contains(paneDivider)||stacked())return;resizePointerId=event.pointerId;paneDivider.setPointerCapture(event.pointerId);setSplit(event.clientX);event.preventDefault();});
178
+ root.addEventListener('pointermove',event=>{if(event.pointerId===resizePointerId)setSplit(event.clientX);});
179
+ root.addEventListener('pointerup',finishResize);root.addEventListener('pointercancel',finishResize);
180
+ root.addEventListener('keydown',event=>{if(event.key==='Escape'){event.preventDefault();showOverview();applyLane(null);return;}const laneChip=event.target.closest('.summary-lane[data-lane-key]');if(laneChip&&(event.key==='Enter'||event.key===' ')){event.preventDefault();toggleLane(laneChip.dataset.laneKey);return;}const node=event.target.closest('[data-node-key]');if(node&&(event.key==='Enter'||event.key===' ')){event.preventDefault();select(node.dataset.nodeKey);}});
181
+ // 記録時刻はUTCで埋まっている。閲覧者の地域とlocaleを知っているのはブラウザだけなので、
182
+ // ここで現地時刻へ直す。scriptが動かなければ埋め込んだUTCがそのまま読める。
183
+ for(const stamp of root.querySelectorAll('time[data-utc-stamp]')){
184
+ const parsed=new Date(stamp.getAttribute('datetime'));
185
+ if(!Number.isNaN(parsed.getTime()))stamp.textContent=parsed.toLocaleString();
186
+ }
187
+ showPanel('overview');
188
+ })();
189
+ `;
190
+
191
+ const NESTED_CONTROLLER = `
192
+ (()=>{
193
+ const root=document.querySelector('[data-gantt-root]');if(!root)return;
194
+ const toggles=[...root.querySelectorAll('[data-nested-toggle-for]')];
195
+ const panels=[...root.querySelectorAll('[data-nested-panel-for]')];
196
+ const panelFor=(key)=>panels.find(panel=>panel.dataset.nestedPanelFor===key);
197
+ const toggle=(control)=>{const key=control.dataset.nestedToggleFor;const panel=panelFor(key);if(!panel)return;const open=panel.hasAttribute('hidden');panel.toggleAttribute('hidden',!open);const link=[...root.querySelectorAll('[data-nested-link-for]')].find(candidate=>candidate.dataset.nestedLinkFor===key);link?.toggleAttribute('hidden',!open);control.setAttribute('aria-expanded',String(open));const mark=control.querySelector('text');if(mark)mark.textContent=open?'−':'+';};
198
+ root.addEventListener('click',event=>{const control=event.target.closest('[data-nested-toggle-for]');if(!control||!root.contains(control))return;event.preventDefault();event.stopPropagation();toggle(control);});
199
+ root.addEventListener('keydown',event=>{const control=event.target.closest('[data-nested-toggle-for]');if(!control||!root.contains(control)||(event.key!=='Enter'&&event.key!==' '))return;event.preventDefault();event.stopPropagation();toggle(control);});
200
+ })();
201
+ `;
202
+
203
+ export function renderTodoGanttHtml({
204
+ readModel, layout, narratives = [], anchorOutcomes = [], presentation = null, metadata = {},
205
+ expandedLayout = null, noteContexts = null, noteWarnings = [],
206
+ }) {
207
+ if (readModel?.schema !== 'lattice.todo_store_read.v1' || !Array.isArray(readModel.members)) {
208
+ throw new TypeError('readModel must be lattice.todo_store_read.v1');
209
+ }
210
+ if (!Array.isArray(narratives)) throw new TypeError('narratives must be an array');
211
+ if (!Array.isArray(anchorOutcomes)) throw new TypeError('anchorOutcomes must be an array');
212
+ if (!(noteContexts === null || Array.isArray(noteContexts))) {
213
+ throw new TypeError('noteContexts must be null or an array');
214
+ }
215
+ if (!Array.isArray(noteWarnings)) throw new TypeError('noteWarnings must be an array');
216
+ if (presentation !== null
217
+ && (presentation?.schema !== 'lattice.todo_gantt_presentation_model.v1'
218
+ || presentation.project_id !== readModel.project_id)) {
219
+ throw new TypeError('presentation must be lattice.todo_gantt_presentation_model.v1');
220
+ }
221
+ const normalized = normalizeSections(readModel, narratives, anchorOutcomes, noteContexts);
222
+ const displayName = projectDisplayName(readModel, metadata);
223
+ const hasHierarchy = layout?.hierarchy?.schema === 'lattice.todo_gantt_hierarchy.v1';
224
+ const svg = renderTodoGanttSvg(layout, { presentation });
225
+ // The expanded diagram travels with the page so the badge can bring the
226
+ // history back without a round trip. A file:// artifact has nowhere to ask.
227
+ const expandedSvg = expandedLayout === null ? '' : renderTodoGanttSvg(expandedLayout, { presentation });
228
+ const diagrams = expandedSvg === ''
229
+ ? `<div data-diagram="live">${svg}</div>`
230
+ : `<div data-diagram="live">${svg}</div><div data-diagram="expanded" hidden>${expandedSvg}</div>`;
231
+ const rightPane = renderRightPane(
232
+ normalized.sections, layout, presentation, readModel, noteContexts !== null, noteWarnings,
233
+ expandedSvg !== '',
234
+ );
235
+ const staticData = serializeJsonForScript({
236
+ renderer_version: TODO_GANTT_RENDERER_VERSION,
237
+ metadata,
238
+ presentation,
239
+ });
240
+ const html = `<!doctype html><html lang="ja"><head><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Lattice — ${escapeHtmlText(displayName)} 依存工程図</title><style>${CSS}${hasHierarchy ? NESTED_CSS : ''}</style></head><body data-gantt-root data-view-state="overview"><main class="shell"><section class="gantt-pane" aria-label="${escapeHtmlAttribute(displayName)} 依存工程図"><div class="diagram-toolbar" role="group" aria-label="図のズーム"><strong class="project-heading">${escapeHtmlText(displayName)} 依存工程図</strong>${renderAuditPendingChip(readModel)}<button type="button" data-zoom-action="out" aria-label="縮小">−</button><button type="button" data-zoom-action="reset">等倍</button><button type="button" data-zoom-action="in" aria-label="拡大">+</button><button type="button" data-zoom-action="fit">全体表示</button><output class="zoom-readout" data-zoom-output aria-live="polite">100%</output><span class="diagram-note">縦=依存段階(時間ではない)</span></div>${renderDiagramLegend(presentation, layout, expandedSvg !== '')}<div class="diagram-scroll" data-diagram-scroll tabindex="0" aria-label="縦方向を主にスクロール可能な依存工程図">${diagrams}</div></section><div class="pane-divider" data-pane-divider aria-hidden="true"></div><aside class="narrative-pane" aria-label="選択工程の詳細と全工程一覧">${rightPane}</aside></main><script type="application/json" id="todo-gantt-data">${staticData}</script><script>${CONTROLLER}${hasHierarchy ? NESTED_CONTROLLER : ''}</script></body></html>`;
241
+ const htmlBytes = Buffer.byteLength(html, 'utf8');
242
+ if (htmlBytes > TODO_GANTT_HTML_MAX_BYTES) {
243
+ throw new TodoGanttRenderError('TODO_SCALE_EXCEEDED', 'todo gantt HTML limit exceeded', {
244
+ html_bytes: htmlBytes, html_limit: TODO_GANTT_HTML_MAX_BYTES,
245
+ });
246
+ }
247
+ return { html, html_digest: digest(html), prose_bytes: normalized.proseBytes, html_bytes: htmlBytes };
248
+ }