@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.
- package/LICENSE +147 -147
- package/README.ja.md +355 -355
- package/README.md +258 -258
- package/bin/lattice-bridge.mjs +25 -0
- package/bin/lattice-hub.mjs +67 -0
- package/bin/lattice-mcp.mjs +0 -0
- package/bin/lattice-scripted-adapter.mjs +0 -0
- package/bin/lattice-scripted-worker.mjs +0 -0
- package/bin/lattice-work-order-adapter.mjs +0 -0
- package/bin/lattice.mjs +0 -0
- package/docs/bridge-setup.md +132 -132
- package/docs/schemas/lattice.executor_packet.v1.schema.json +57 -57
- package/docs/schemas/lattice.executor_receipt.v1.schema.json +66 -66
- package/docs/schemas/lattice.phase_todo_revision.v3.schema.json +360 -360
- package/docs/schemas/lattice.plan_create_input.v1.schema.json +56 -56
- package/docs/schemas/lattice.plan_create_input.v2.schema.json +72 -72
- package/docs/schemas/lattice.plan_create_input.v3.schema.json +81 -81
- package/docs/schemas/lattice.plan_create_input.v4.schema.json +85 -85
- package/docs/schemas/lattice.run_request.v1.schema.json +238 -238
- package/docs/schemas/lattice.runtime_adapter_capabilities.v2.schema.json +55 -55
- package/docs/schemas/lattice.runtime_adapter_registration_input.v1.schema.json +78 -78
- package/docs/schemas/lattice.runtime_adapter_registration_input.v2.schema.json +86 -86
- package/docs/schemas/lattice.todo_extraction.v2.schema.json +298 -298
- package/docs/schemas/lattice.todo_extraction.v3.schema.json +146 -146
- package/docs/schemas/lattice.todo_revision.v2.schema.json +260 -260
- package/docs/schemas/lattice.todo_revision_set.v3.schema.json +363 -363
- package/package.json +103 -103
- package/sensor/LICENSE +21 -21
- package/sensor/NOTICE +19 -19
- package/sensor/dist/bin/lattice-sensor.js +9 -9
- package/sensor/dist/db/index.js +24 -24
- package/sensor/dist/db/migrations.js +41 -41
- package/sensor/dist/db/queries.js +164 -164
- package/sensor/dist/db/schema.sql +205 -205
- package/sensor/dist/directory.js +5 -5
- package/sensor/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-go.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-java.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-javascript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-python.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-tsx.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-typescript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
- package/sensor/dist/mcp/liveness-watchdog.js +53 -53
- package/sensor/dist/mcp/server-instructions.js +95 -95
- package/sensor/package.json +56 -56
- package/src/artifact-contracts-v2.mjs +325 -325
- package/src/artifact-contracts.mjs +895 -895
- package/src/boundary-compiler.mjs +712 -712
- package/src/boundary-observation-compiler-v2.mjs +344 -344
- package/src/bounded-seam.mjs +230 -230
- package/src/bridge-address.mjs +107 -107
- package/src/bridge-cli.mjs +303 -297
- package/src/bridge-config.mjs +382 -346
- package/src/bridge-daemon.mjs +378 -378
- package/src/bridge-hub-heartbeat.mjs +170 -0
- package/src/bridge-hub-protocol.mjs +198 -0
- package/src/bridge-hub-server.mjs +544 -0
- package/src/bridge-launch-agent.mjs +323 -323
- package/src/bridge-registrar.mjs +102 -102
- package/src/bridge-server.mjs +376 -376
- package/src/cli-help.mjs +308 -308
- package/src/cli-stdio.mjs +40 -40
- package/src/control-compiler.mjs +532 -532
- package/src/dag-chain.mjs +261 -261
- package/src/factory-diagnostics.mjs +188 -188
- package/src/git-process.mjs +72 -0
- package/src/hash-chain.mjs +76 -76
- package/src/hooks-cli.mjs +1057 -1057
- package/src/isolation-runner.mjs +409 -409
- package/src/node-version-guard.mjs +44 -44
- package/src/project-cli.mjs +3 -3
- package/src/project-identity.mjs +130 -130
- package/src/rc1-black-box-oracle.mjs +906 -906
- package/src/rc1-comparison.mjs +154 -154
- package/src/rc1-evidence-bundle.mjs +456 -456
- package/src/rc1-v4-campaign.mjs +708 -708
- package/src/rc1-v4-transform.mjs +467 -467
- package/src/rc1-v5-artifact-set.mjs +855 -855
- package/src/rc1-v5-behavior-evidence.mjs +594 -594
- package/src/rc1-v5-campaign.mjs +797 -797
- package/src/rc1-v5-transform.mjs +421 -421
- package/src/rc1-v6-artifact-set.mjs +807 -807
- package/src/rc1-v6-behavior-evidence.mjs +273 -273
- package/src/rc1-v6-campaign.mjs +623 -623
- package/src/rc1-v6-causal-binding.mjs +473 -473
- package/src/rc1-v6-measurement.mjs +313 -313
- package/src/rc2-artifact-set.mjs +1584 -1584
- package/src/rc2-campaign.mjs +1506 -1506
- package/src/rc2-delivery-policy-front-end.mjs +1079 -1079
- package/src/rc2-delivery-policy-oracle.mjs +134 -134
- package/src/rc2-delivery-policy-transform.mjs +1127 -1127
- package/src/rc2-rc1-transfer-front-end.mjs +511 -511
- package/src/rc3-actual-dogfood.mjs +652 -652
- package/src/rc3-dogfood-scaffold.mjs +315 -315
- package/src/rc3-scripted-campaign.mjs +1383 -1383
- package/src/rc4-stage1-dogfood.mjs +673 -673
- package/src/runtime-adapter-registry.mjs +524 -524
- package/src/runtime-cli.mjs +4588 -4588
- package/src/runtime-contracts.mjs +824 -824
- package/src/runtime-control-store.mjs +604 -604
- package/src/runtime-controller-protocol.mjs +587 -587
- package/src/runtime-decision-verifier.mjs +701 -701
- package/src/runtime-diff-observer.mjs +361 -361
- package/src/runtime-direct-os-observer.mjs +301 -301
- package/src/runtime-driver-state.mjs +166 -166
- package/src/runtime-engine.mjs +779 -779
- package/src/runtime-errors.mjs +356 -356
- package/src/runtime-event-store.mjs +189 -189
- package/src/runtime-front-end.mjs +925 -925
- package/src/runtime-gate-store.mjs +481 -481
- package/src/runtime-hold-recompile.mjs +916 -916
- package/src/runtime-io-sentinel.mjs +391 -391
- package/src/runtime-lifecycle-lock.mjs +294 -294
- package/src/runtime-managed-supervisor.mjs +1490 -1490
- package/src/runtime-multi-epoch-store.mjs +838 -838
- package/src/runtime-projection.mjs +269 -269
- package/src/runtime-pull-intake.mjs +1192 -1192
- package/src/runtime-scripted-adapter-controller.mjs +1160 -1160
- package/src/runtime-scripted-executor.mjs +163 -163
- package/src/runtime-scripted-worktree.mjs +104 -104
- package/src/runtime-seam-resolve.mjs +428 -428
- package/src/runtime-seam-treatment.mjs +173 -173
- package/src/runtime-socket-owner.mjs +125 -125
- package/src/runtime-work-order-contracts.mjs +91 -91
- package/src/runtime-work-order-controller.mjs +1171 -1171
- package/src/runtime-worktree-executor.mjs +199 -199
- package/src/schedulability-compiler-v2.mjs +303 -303
- package/src/schedulability-verifier-v2.mjs +317 -317
- package/src/seam-apply.mjs +549 -549
- package/src/seam-commit-shared.mjs +22 -22
- package/src/seam-commit-transform.mjs +81 -81
- package/src/seam-commit.mjs +18 -18
- package/src/seam-cost.mjs +322 -322
- package/src/seam-derivation.mjs +188 -188
- package/src/seam-gate.mjs +146 -146
- package/src/seam-proposal-contracts.mjs +446 -446
- package/src/seam-proposal-queries.mjs +521 -521
- package/src/seam-proposal.mjs +2011 -2011
- package/src/seam-ref.mjs +33 -33
- package/src/seam-rewrite.mjs +286 -286
- package/src/seam-transform.mjs +554 -554
- package/src/seam-verification.mjs +260 -260
- package/src/sensor-adapter.mjs +432 -432
- package/src/sensor-cli.mjs +139 -139
- package/src/sensor-diff.mjs +661 -661
- package/src/sensor-node-runtime.mjs +53 -53
- package/src/sensor-runtime.mjs +52 -52
- package/src/timestamp-contract.mjs +8 -8
- package/src/todo-audit-pending.mjs +91 -91
- package/src/todo-chain.mjs +178 -178
- package/src/todo-cli.mjs +8 -8
- package/src/todo-contracts.mjs +728 -728
- package/src/todo-dashboard-registry.mjs +573 -573
- package/src/todo-dispatch-shape.mjs +190 -190
- package/src/todo-gantt-html-independence.mjs +239 -239
- package/src/todo-gantt-html-shared.mjs +226 -226
- package/src/todo-gantt-html-style.mjs +131 -131
- package/src/todo-gantt-html.mjs +248 -248
- package/src/todo-gantt-layout.mjs +974 -974
- package/src/todo-gantt-live.mjs +361 -361
- package/src/todo-gantt-nested.mjs +263 -263
- package/src/todo-gantt-presentation.mjs +217 -217
- package/src/todo-gantt-scope.mjs +123 -123
- package/src/todo-gantt-svg.mjs +353 -353
- package/src/todo-independence-contracts.mjs +595 -595
- package/src/todo-independence-guidance.mjs +322 -322
- package/src/todo-independence.mjs +640 -640
- package/src/todo-markdown-renderer.mjs +260 -260
- package/src/todo-migration.mjs +448 -448
- package/src/todo-narrative-anchor.mjs +130 -130
- package/src/todo-note-store.mjs +629 -629
- package/src/todo-parallel-candidates.mjs +114 -114
- package/src/todo-revision.mjs +995 -995
- package/src/todo-split.mjs +472 -472
- package/src/todo-status.mjs +690 -690
- package/src/todo-store-git-transaction.mjs +418 -418
- package/src/todo-store.mjs +42 -21
- package/src/treatment-compiler.mjs +728 -728
- package/src/treatment-runner.mjs +656 -656
- package/src/witness-scaffold.mjs +180 -180
package/src/todo-gantt-html.mjs
CHANGED
|
@@ -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
|
+
}
|