gsd-pi 2.26.0 → 2.27.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/README.md +43 -6
- package/dist/cli.js +4 -2
- package/dist/headless.d.ts +3 -0
- package/dist/headless.js +136 -8
- package/dist/help-text.js +3 -0
- package/dist/loader.js +33 -4
- package/dist/resources/extensions/bg-shell/index.ts +19 -2
- package/dist/resources/extensions/bg-shell/process-manager.ts +45 -0
- package/dist/resources/extensions/bg-shell/types.ts +21 -1
- package/dist/resources/extensions/gsd/auto/session.ts +224 -0
- package/dist/resources/extensions/gsd/auto-budget.ts +32 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +63 -10
- package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
- package/dist/resources/extensions/gsd/auto-dispatch.ts +23 -10
- package/dist/resources/extensions/gsd/auto-model-selection.ts +179 -0
- package/dist/resources/extensions/gsd/auto-observability.ts +74 -0
- package/dist/resources/extensions/gsd/auto-prompts.ts +0 -1
- package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
- package/dist/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
- package/dist/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
- package/dist/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
- package/dist/resources/extensions/gsd/auto.ts +977 -1551
- package/dist/resources/extensions/gsd/commands.ts +3 -3
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +47 -72
- package/dist/resources/extensions/gsd/doctor-proactive.ts +9 -4
- package/dist/resources/extensions/gsd/export-html.ts +1001 -0
- package/dist/resources/extensions/gsd/export.ts +49 -1
- package/dist/resources/extensions/gsd/git-service.ts +6 -0
- package/dist/resources/extensions/gsd/gitignore.ts +4 -1
- package/dist/resources/extensions/gsd/guided-flow.ts +24 -5
- package/dist/resources/extensions/gsd/index.ts +54 -1
- package/dist/resources/extensions/gsd/native-git-bridge.ts +30 -2
- package/dist/resources/extensions/gsd/observability-validator.ts +21 -0
- package/dist/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
- package/dist/resources/extensions/gsd/preferences.ts +62 -1
- package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -3
- package/dist/resources/extensions/gsd/prompts/system.md +1 -1
- package/dist/resources/extensions/gsd/reports.ts +510 -0
- package/dist/resources/extensions/gsd/roadmap-slices.ts +1 -1
- package/dist/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
- package/dist/resources/extensions/gsd/state.ts +30 -0
- package/dist/resources/extensions/gsd/templates/task-summary.md +9 -0
- package/dist/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
- package/dist/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
- package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
- package/dist/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
- package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
- package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
- package/dist/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
- package/dist/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
- package/dist/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
- package/dist/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
- package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
- package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
- package/dist/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
- package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
- package/dist/resources/extensions/gsd/tests/verification-evidence.test.ts +743 -0
- package/dist/resources/extensions/gsd/tests/verification-gate.test.ts +965 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
- package/dist/resources/extensions/gsd/tests/worktree.test.ts +3 -1
- package/dist/resources/extensions/gsd/types.ts +38 -0
- package/dist/resources/extensions/gsd/verification-evidence.ts +183 -0
- package/dist/resources/extensions/gsd/verification-gate.ts +567 -0
- package/dist/resources/extensions/gsd/visualizer-data.ts +25 -3
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +31 -21
- package/dist/resources/extensions/gsd/visualizer-views.ts +15 -66
- package/dist/resources/extensions/search-the-web/tool-search.ts +26 -0
- package/dist/resources/extensions/shared/format-utils.ts +85 -0
- package/dist/resources/extensions/shared/tests/format-utils.test.ts +153 -0
- package/dist/resources/extensions/subagent/index.ts +46 -1
- package/dist/resources/extensions/subagent/isolation.ts +9 -6
- package/package.json +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +7 -4
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/src/providers/openai-completions.ts +7 -4
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js +7 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/config.js +9 -2
- package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
- package/packages/pi-coding-agent/src/core/lsp/client.ts +8 -0
- package/packages/pi-coding-agent/src/core/lsp/config.ts +9 -2
- package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/editor.js +1 -1
- package/packages/pi-tui/dist/components/editor.js.map +1 -1
- package/packages/pi-tui/src/components/editor.ts +3 -1
- package/scripts/link-workspace-packages.cjs +22 -6
- package/src/resources/extensions/bg-shell/index.ts +19 -2
- package/src/resources/extensions/bg-shell/process-manager.ts +45 -0
- package/src/resources/extensions/bg-shell/types.ts +21 -1
- package/src/resources/extensions/gsd/auto/session.ts +224 -0
- package/src/resources/extensions/gsd/auto-budget.ts +32 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +63 -10
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +23 -10
- package/src/resources/extensions/gsd/auto-model-selection.ts +179 -0
- package/src/resources/extensions/gsd/auto-observability.ts +74 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +0 -1
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
- package/src/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
- package/src/resources/extensions/gsd/auto.ts +977 -1551
- package/src/resources/extensions/gsd/commands.ts +3 -3
- package/src/resources/extensions/gsd/dashboard-overlay.ts +47 -72
- package/src/resources/extensions/gsd/doctor-proactive.ts +9 -4
- package/src/resources/extensions/gsd/export-html.ts +1001 -0
- package/src/resources/extensions/gsd/export.ts +49 -1
- package/src/resources/extensions/gsd/git-service.ts +6 -0
- package/src/resources/extensions/gsd/gitignore.ts +4 -1
- package/src/resources/extensions/gsd/guided-flow.ts +24 -5
- package/src/resources/extensions/gsd/index.ts +54 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +30 -2
- package/src/resources/extensions/gsd/observability-validator.ts +21 -0
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
- package/src/resources/extensions/gsd/preferences.ts +62 -1
- package/src/resources/extensions/gsd/prompts/execute-task.md +4 -3
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/reports.ts +510 -0
- package/src/resources/extensions/gsd/roadmap-slices.ts +1 -1
- package/src/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
- package/src/resources/extensions/gsd/state.ts +30 -0
- package/src/resources/extensions/gsd/templates/task-summary.md +9 -0
- package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
- package/src/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
- package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
- package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
- package/src/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
- package/src/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
- package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
- package/src/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
- package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
- package/src/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +743 -0
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +965 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
- package/src/resources/extensions/gsd/tests/worktree.test.ts +3 -1
- package/src/resources/extensions/gsd/types.ts +38 -0
- package/src/resources/extensions/gsd/verification-evidence.ts +183 -0
- package/src/resources/extensions/gsd/verification-gate.ts +567 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +25 -3
- package/src/resources/extensions/gsd/visualizer-overlay.ts +31 -21
- package/src/resources/extensions/gsd/visualizer-views.ts +15 -66
- package/src/resources/extensions/search-the-web/tool-search.ts +26 -0
- package/src/resources/extensions/shared/format-utils.ts +85 -0
- package/src/resources/extensions/shared/tests/format-utils.test.ts +153 -0
- package/src/resources/extensions/subagent/index.ts +46 -1
- package/src/resources/extensions/subagent/isolation.ts +9 -6
|
@@ -0,0 +1,1001 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD HTML Report Generator
|
|
3
|
+
*
|
|
4
|
+
* Produces a single self-contained HTML file with:
|
|
5
|
+
* - Branding header (project name, path, GSD version, generated timestamp)
|
|
6
|
+
* - Project summary & overall progress
|
|
7
|
+
* - Progress tree (milestones → slices → tasks, with critical path)
|
|
8
|
+
* - Execution timeline (chronological unit history)
|
|
9
|
+
* - Slice dependency graph (SVG DAG per milestone)
|
|
10
|
+
* - Cost & token metrics (bar charts, phase/slice/model/tier breakdowns)
|
|
11
|
+
* - Health & configuration overview
|
|
12
|
+
* - Changelog (completed slice summaries + file modifications)
|
|
13
|
+
* - Knowledge base (rules, patterns, lessons)
|
|
14
|
+
* - Captures log
|
|
15
|
+
* - Artifacts & milestone planning / discussion state
|
|
16
|
+
*
|
|
17
|
+
* No external dependencies — all CSS and JS is inlined.
|
|
18
|
+
* Printable to PDF from any browser.
|
|
19
|
+
*
|
|
20
|
+
* Design: Linear-inspired — restrained palette, geometric status, no emoji.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type {
|
|
24
|
+
VisualizerData,
|
|
25
|
+
VisualizerMilestone,
|
|
26
|
+
VisualizerSlice,
|
|
27
|
+
} from './visualizer-data.js';
|
|
28
|
+
import { formatDuration } from './history.js';
|
|
29
|
+
import { formatCost, formatTokenCount } from './metrics.js';
|
|
30
|
+
|
|
31
|
+
// ─── Public API ────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export interface HtmlReportOptions {
|
|
34
|
+
projectName: string;
|
|
35
|
+
projectPath: string;
|
|
36
|
+
gsdVersion: string;
|
|
37
|
+
milestoneId?: string;
|
|
38
|
+
indexRelPath?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function generateHtmlReport(
|
|
42
|
+
data: VisualizerData,
|
|
43
|
+
opts: HtmlReportOptions,
|
|
44
|
+
): string {
|
|
45
|
+
const generated = new Date().toISOString();
|
|
46
|
+
|
|
47
|
+
const sections = [
|
|
48
|
+
buildSummarySection(data, opts, generated),
|
|
49
|
+
buildProgressSection(data),
|
|
50
|
+
buildTimelineSection(data),
|
|
51
|
+
buildDepGraphSection(data),
|
|
52
|
+
buildMetricsSection(data),
|
|
53
|
+
buildHealthSection(data),
|
|
54
|
+
buildChangelogSection(data),
|
|
55
|
+
buildKnowledgeSection(data),
|
|
56
|
+
buildCapturesSection(data),
|
|
57
|
+
buildStatsSection(data),
|
|
58
|
+
buildDiscussionSection(data),
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const milestoneTag = opts.milestoneId
|
|
62
|
+
? ` <span class="sep">/</span> <span class="mono accent">${esc(opts.milestoneId)}</span>`
|
|
63
|
+
: '';
|
|
64
|
+
|
|
65
|
+
const backLink = opts.indexRelPath
|
|
66
|
+
? `<a class="back-link" href="${esc(opts.indexRelPath)}">All Reports</a>`
|
|
67
|
+
: '';
|
|
68
|
+
|
|
69
|
+
return `<!DOCTYPE html>
|
|
70
|
+
<html lang="en">
|
|
71
|
+
<head>
|
|
72
|
+
<meta charset="UTF-8">
|
|
73
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
74
|
+
<title>GSD Report — ${esc(opts.projectName)}${opts.milestoneId ? ` — ${esc(opts.milestoneId)}` : ''}</title>
|
|
75
|
+
<style>${CSS}</style>
|
|
76
|
+
</head>
|
|
77
|
+
<body>
|
|
78
|
+
<header>
|
|
79
|
+
<div class="header-inner">
|
|
80
|
+
<div class="branding">
|
|
81
|
+
<span class="logo">GSD</span>
|
|
82
|
+
<span class="version">v${esc(opts.gsdVersion)}</span>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="header-meta">
|
|
85
|
+
<h1>${esc(opts.projectName)}${milestoneTag}</h1>
|
|
86
|
+
<span class="header-path">${esc(opts.projectPath)}</span>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="header-right">
|
|
89
|
+
${backLink}
|
|
90
|
+
<div class="generated">${formatDateLong(generated)}</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</header>
|
|
94
|
+
<nav class="toc" aria-label="Report sections">
|
|
95
|
+
<ul>
|
|
96
|
+
<li><a href="#summary">Summary</a></li>
|
|
97
|
+
<li><a href="#progress">Progress</a></li>
|
|
98
|
+
<li><a href="#timeline">Timeline</a></li>
|
|
99
|
+
<li><a href="#depgraph">Dependencies</a></li>
|
|
100
|
+
<li><a href="#metrics">Metrics</a></li>
|
|
101
|
+
<li><a href="#health">Health</a></li>
|
|
102
|
+
<li><a href="#changelog">Changelog</a></li>
|
|
103
|
+
<li><a href="#knowledge">Knowledge</a></li>
|
|
104
|
+
<li><a href="#captures">Captures</a></li>
|
|
105
|
+
<li><a href="#stats">Artifacts</a></li>
|
|
106
|
+
<li><a href="#discussion">Planning</a></li>
|
|
107
|
+
</ul>
|
|
108
|
+
</nav>
|
|
109
|
+
<main>
|
|
110
|
+
${sections.join('\n')}
|
|
111
|
+
</main>
|
|
112
|
+
<footer>
|
|
113
|
+
<div class="footer-inner">
|
|
114
|
+
<span>GSD v${esc(opts.gsdVersion)}</span>
|
|
115
|
+
<span class="sep">/</span>
|
|
116
|
+
<span>${esc(opts.projectName)}</span>
|
|
117
|
+
${opts.milestoneId ? `<span class="sep">/</span><span class="mono">${esc(opts.milestoneId)}</span>` : ''}
|
|
118
|
+
<span class="sep">/</span>
|
|
119
|
+
<span>${formatDateLong(generated)}</span>
|
|
120
|
+
</div>
|
|
121
|
+
</footer>
|
|
122
|
+
<script>${JS}</script>
|
|
123
|
+
</body>
|
|
124
|
+
</html>`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── Section: Summary ─────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
function buildSummarySection(
|
|
130
|
+
data: VisualizerData,
|
|
131
|
+
_opts: HtmlReportOptions,
|
|
132
|
+
_generated: string,
|
|
133
|
+
): string {
|
|
134
|
+
const t = data.totals;
|
|
135
|
+
const totalSlices = data.milestones.reduce((s, m) => s + m.slices.length, 0);
|
|
136
|
+
const doneSlices = data.milestones.reduce((s, m) => s + m.slices.filter(sl => sl.done).length, 0);
|
|
137
|
+
const doneMilestones = data.milestones.filter(m => m.status === 'complete').length;
|
|
138
|
+
const activeMilestone = data.milestones.find(m => m.status === 'active');
|
|
139
|
+
const pct = totalSlices > 0 ? Math.round((doneSlices / totalSlices) * 100) : 0;
|
|
140
|
+
|
|
141
|
+
const act = data.agentActivity;
|
|
142
|
+
const kv = [
|
|
143
|
+
kvi('Milestones', `${doneMilestones}/${data.milestones.length}`),
|
|
144
|
+
kvi('Slices', `${doneSlices}/${totalSlices}`),
|
|
145
|
+
kvi('Phase', data.phase),
|
|
146
|
+
t ? kvi('Cost', formatCost(t.cost)) : '',
|
|
147
|
+
t ? kvi('Tokens', formatTokenCount(t.tokens.total)) : '',
|
|
148
|
+
t ? kvi('Duration', formatDuration(t.duration)) : '',
|
|
149
|
+
t ? kvi('Tool calls', String(t.toolCalls)) : '',
|
|
150
|
+
t ? kvi('Units', String(t.units)) : '',
|
|
151
|
+
data.remainingSliceCount > 0 ? kvi('Remaining', String(data.remainingSliceCount)) : '',
|
|
152
|
+
act ? kvi('Rate', `${act.completionRate.toFixed(1)}/hr`) : '',
|
|
153
|
+
].filter(Boolean).join('');
|
|
154
|
+
|
|
155
|
+
const activeInfo = activeMilestone ? (() => {
|
|
156
|
+
const active = activeMilestone.slices.find(s => s.active);
|
|
157
|
+
if (!active) return '';
|
|
158
|
+
return `<div class="active-info">
|
|
159
|
+
Executing <span class="mono">${esc(activeMilestone.id)}/${esc(active.id)}</span> — ${esc(active.title)}
|
|
160
|
+
</div>`;
|
|
161
|
+
})() : '';
|
|
162
|
+
|
|
163
|
+
const activityHtml = act?.active ? `
|
|
164
|
+
<div class="activity-line">
|
|
165
|
+
<span class="dot dot-active"></span>
|
|
166
|
+
<span class="mono">${esc(act.currentUnit?.type ?? '')}</span>
|
|
167
|
+
<span class="mono muted">${esc(act.currentUnit?.id ?? '')}</span>
|
|
168
|
+
<span class="muted">${formatDuration(act.elapsed)} elapsed</span>
|
|
169
|
+
</div>` : '';
|
|
170
|
+
|
|
171
|
+
return section('summary', 'Summary', `
|
|
172
|
+
<div class="kv-grid">${kv}</div>
|
|
173
|
+
<div class="progress-wrap">
|
|
174
|
+
<div class="progress-track"><div class="progress-fill" style="width:${pct}%"></div></div>
|
|
175
|
+
<span class="progress-label">${pct}%</span>
|
|
176
|
+
</div>
|
|
177
|
+
${activeInfo}
|
|
178
|
+
${activityHtml}
|
|
179
|
+
`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ─── Section: Health ──────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
function buildHealthSection(data: VisualizerData): string {
|
|
185
|
+
const h = data.health;
|
|
186
|
+
const t = data.totals;
|
|
187
|
+
|
|
188
|
+
const rows: string[] = [];
|
|
189
|
+
rows.push(hRow('Token profile', h.tokenProfile));
|
|
190
|
+
if (h.budgetCeiling !== undefined) {
|
|
191
|
+
const spent = t?.cost ?? 0;
|
|
192
|
+
const pct = (spent / h.budgetCeiling) * 100;
|
|
193
|
+
const status = pct > 90 ? 'warn' : pct > 75 ? 'caution' : 'ok';
|
|
194
|
+
rows.push(hRow(
|
|
195
|
+
'Budget ceiling',
|
|
196
|
+
`${formatCost(h.budgetCeiling)} (${formatCost(spent)} spent, ${pct.toFixed(0)}% used)`,
|
|
197
|
+
status,
|
|
198
|
+
));
|
|
199
|
+
}
|
|
200
|
+
rows.push(hRow(
|
|
201
|
+
'Truncation rate',
|
|
202
|
+
`${h.truncationRate.toFixed(1)}% per unit (${t?.totalTruncationSections ?? 0} total)`,
|
|
203
|
+
h.truncationRate > 20 ? 'warn' : h.truncationRate > 10 ? 'caution' : 'ok',
|
|
204
|
+
));
|
|
205
|
+
rows.push(hRow(
|
|
206
|
+
'Continue-here rate',
|
|
207
|
+
`${h.continueHereRate.toFixed(1)}% per unit (${t?.continueHereFiredCount ?? 0} total)`,
|
|
208
|
+
h.continueHereRate > 15 ? 'warn' : h.continueHereRate > 8 ? 'caution' : 'ok',
|
|
209
|
+
));
|
|
210
|
+
if (h.tierSavingsLine) rows.push(hRow('Routing savings', h.tierSavingsLine));
|
|
211
|
+
rows.push(hRow('Tool calls', String(h.toolCalls)));
|
|
212
|
+
rows.push(hRow('Messages', `${h.assistantMessages} assistant / ${h.userMessages} user`));
|
|
213
|
+
|
|
214
|
+
const tierRows = h.tierBreakdown.length > 0 ? `
|
|
215
|
+
<h3>Tier breakdown</h3>
|
|
216
|
+
<table class="tbl">
|
|
217
|
+
<thead><tr><th>Tier</th><th>Units</th><th>Cost</th><th>Tokens</th></tr></thead>
|
|
218
|
+
<tbody>
|
|
219
|
+
${h.tierBreakdown.map(tb =>
|
|
220
|
+
`<tr><td class="mono">${esc(tb.tier)}</td>
|
|
221
|
+
<td>${tb.units}</td><td>${formatCost(tb.cost)}</td>
|
|
222
|
+
<td>${formatTokenCount(tb.tokens.total)}</td></tr>`
|
|
223
|
+
).join('')}
|
|
224
|
+
</tbody>
|
|
225
|
+
</table>` : '';
|
|
226
|
+
|
|
227
|
+
return section('health', 'Health', `
|
|
228
|
+
<table class="tbl tbl-kv"><tbody>${rows.join('')}</tbody></table>
|
|
229
|
+
${tierRows}
|
|
230
|
+
`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ─── Section: Progress ────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
function buildProgressSection(data: VisualizerData): string {
|
|
236
|
+
if (data.milestones.length === 0) {
|
|
237
|
+
return section('progress', 'Progress', '<p class="empty">No milestones found.</p>');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const critMS = new Set(data.criticalPath.milestonePath);
|
|
241
|
+
const critSL = new Set(data.criticalPath.slicePath);
|
|
242
|
+
|
|
243
|
+
const msHtml = data.milestones.map(ms => {
|
|
244
|
+
const doneCount = ms.slices.filter(s => s.done).length;
|
|
245
|
+
const onCrit = critMS.has(ms.id);
|
|
246
|
+
const sliceHtml = ms.slices.length > 0
|
|
247
|
+
? ms.slices.map(sl => buildSliceRow(sl, critSL, data)).join('')
|
|
248
|
+
: '<p class="empty indent">No slices in roadmap yet.</p>';
|
|
249
|
+
|
|
250
|
+
return `
|
|
251
|
+
<details class="ms-block" ${ms.status !== 'pending' ? 'open' : ''}>
|
|
252
|
+
<summary class="ms-summary ms-${ms.status}">
|
|
253
|
+
<span class="dot dot-${ms.status}"></span>
|
|
254
|
+
<span class="mono ms-id">${esc(ms.id)}</span>
|
|
255
|
+
<span class="ms-title">${esc(ms.title)}</span>
|
|
256
|
+
<span class="muted">${doneCount}/${ms.slices.length}</span>
|
|
257
|
+
${onCrit ? '<span class="label">critical path</span>' : ''}
|
|
258
|
+
${ms.dependsOn.length > 0 ? `<span class="muted">needs ${ms.dependsOn.map(esc).join(', ')}</span>` : ''}
|
|
259
|
+
</summary>
|
|
260
|
+
<div class="ms-body">${sliceHtml}</div>
|
|
261
|
+
</details>`;
|
|
262
|
+
}).join('');
|
|
263
|
+
|
|
264
|
+
return section('progress', 'Progress', msHtml);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function buildSliceRow(sl: VisualizerSlice, critSL: Set<string>, data: VisualizerData): string {
|
|
268
|
+
const onCrit = critSL.has(sl.id);
|
|
269
|
+
const ver = data.sliceVerifications.find(v => v.sliceId === sl.id);
|
|
270
|
+
const slack = data.criticalPath.sliceSlack.get(sl.id);
|
|
271
|
+
const status = sl.done ? 'complete' : sl.active ? 'active' : 'pending';
|
|
272
|
+
|
|
273
|
+
const taskHtml = sl.tasks.length > 0 ? `
|
|
274
|
+
<ul class="task-list">
|
|
275
|
+
${sl.tasks.map(t => `
|
|
276
|
+
<li class="task-row">
|
|
277
|
+
<span class="dot dot-${t.done ? 'complete' : t.active ? 'active' : 'pending'} dot-sm"></span>
|
|
278
|
+
<span class="mono muted">${esc(t.id)}</span>
|
|
279
|
+
<span class="${t.done ? 'muted' : ''}">${esc(t.title)}</span>
|
|
280
|
+
${t.estimate ? `<span class="muted">${esc(t.estimate)}</span>` : ''}
|
|
281
|
+
</li>`).join('')}
|
|
282
|
+
</ul>` : '';
|
|
283
|
+
|
|
284
|
+
const tags = [
|
|
285
|
+
...(ver?.provides ?? []).map(p => `<span class="tag">provides: ${esc(p)}</span>`),
|
|
286
|
+
...(ver?.requires ?? []).map(r => `<span class="tag">requires: ${esc(r.provides)}</span>`),
|
|
287
|
+
].join('');
|
|
288
|
+
|
|
289
|
+
const keyDecisions = ver?.keyDecisions?.length
|
|
290
|
+
? `<div class="detail-block"><span class="detail-label">Decisions</span><ul>${ver.keyDecisions.map(d => `<li>${esc(d)}</li>`).join('')}</ul></div>`
|
|
291
|
+
: '';
|
|
292
|
+
|
|
293
|
+
const patterns = ver?.patternsEstablished?.length
|
|
294
|
+
? `<div class="detail-block"><span class="detail-label">Patterns</span><ul>${ver.patternsEstablished.map(p => `<li>${esc(p)}</li>`).join('')}</ul></div>`
|
|
295
|
+
: '';
|
|
296
|
+
|
|
297
|
+
const verifBadge = ver?.verificationResult
|
|
298
|
+
? `<div class="verif ${ver.blockerDiscovered ? 'verif-blocker' : ''}">
|
|
299
|
+
${ver.blockerDiscovered ? 'Blocker: ' : ''}${esc(ver.verificationResult)}
|
|
300
|
+
</div>`
|
|
301
|
+
: '';
|
|
302
|
+
|
|
303
|
+
return `
|
|
304
|
+
<details class="sl-block">
|
|
305
|
+
<summary class="sl-summary ${onCrit ? 'sl-crit' : ''}">
|
|
306
|
+
<span class="dot dot-${status} dot-sm"></span>
|
|
307
|
+
<span class="mono muted">${esc(sl.id)}</span>
|
|
308
|
+
<span class="${status === 'active' ? 'accent' : sl.done ? 'muted' : ''}">${esc(sl.title)}</span>
|
|
309
|
+
<span class="risk risk-${(sl.risk || 'unknown').toLowerCase()}">${esc(sl.risk || '?')}</span>
|
|
310
|
+
${sl.depends.length > 0 ? `<span class="muted sl-deps">${sl.depends.map(esc).join(', ')}</span>` : ''}
|
|
311
|
+
${onCrit ? '<span class="label">critical</span>' : ''}
|
|
312
|
+
${slack !== undefined && slack > 0 ? `<span class="muted">+${slack} slack</span>` : ''}
|
|
313
|
+
</summary>
|
|
314
|
+
<div class="sl-detail">
|
|
315
|
+
${tags ? `<div class="tag-row">${tags}</div>` : ''}
|
|
316
|
+
${verifBadge}
|
|
317
|
+
${keyDecisions}
|
|
318
|
+
${patterns}
|
|
319
|
+
${taskHtml}
|
|
320
|
+
</div>
|
|
321
|
+
</details>`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ─── Section: Dependency Graph ────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
function buildDepGraphSection(data: VisualizerData): string {
|
|
327
|
+
const hasSlices = data.milestones.some(ms => ms.slices.length > 0);
|
|
328
|
+
if (!hasSlices) return section('depgraph', 'Dependencies', '<p class="empty">No slices to graph.</p>');
|
|
329
|
+
|
|
330
|
+
const hasDeps = data.milestones.some(ms => ms.slices.some(s => s.depends.length > 0));
|
|
331
|
+
if (!hasDeps) return section('depgraph', 'Dependencies', '<p class="empty">No dependencies defined.</p>');
|
|
332
|
+
|
|
333
|
+
const svgs = data.milestones
|
|
334
|
+
.filter(ms => ms.slices.length > 0)
|
|
335
|
+
.map(ms => buildMilestoneDepSVG(ms, data))
|
|
336
|
+
.filter(Boolean)
|
|
337
|
+
.join('');
|
|
338
|
+
|
|
339
|
+
return section('depgraph', 'Dependencies', svgs);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function buildMilestoneDepSVG(ms: VisualizerMilestone, data: VisualizerData): string {
|
|
343
|
+
const slices = ms.slices;
|
|
344
|
+
if (slices.length === 0) return '';
|
|
345
|
+
|
|
346
|
+
const critSL = new Set(data.criticalPath.slicePath);
|
|
347
|
+
const slMap = new Map(slices.map(s => [s.id, s]));
|
|
348
|
+
|
|
349
|
+
const layerMap = new Map<string, number>();
|
|
350
|
+
const inDeg = new Map<string, number>();
|
|
351
|
+
for (const s of slices) inDeg.set(s.id, 0);
|
|
352
|
+
for (const s of slices) {
|
|
353
|
+
for (const dep of s.depends) {
|
|
354
|
+
if (slMap.has(dep)) inDeg.set(s.id, (inDeg.get(s.id) ?? 0) + 1);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const visited = new Set<string>();
|
|
359
|
+
const q: string[] = [];
|
|
360
|
+
for (const [id, d] of inDeg) {
|
|
361
|
+
if (d === 0) { q.push(id); visited.add(id); layerMap.set(id, 0); }
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
while (q.length > 0) {
|
|
365
|
+
const node = q.shift()!;
|
|
366
|
+
for (const s of slices) {
|
|
367
|
+
if (!s.depends.includes(node)) continue;
|
|
368
|
+
const newDeg = (inDeg.get(s.id) ?? 1) - 1;
|
|
369
|
+
inDeg.set(s.id, newDeg);
|
|
370
|
+
layerMap.set(s.id, Math.max(layerMap.get(s.id) ?? 0, (layerMap.get(node) ?? 0) + 1));
|
|
371
|
+
if (newDeg === 0 && !visited.has(s.id)) { visited.add(s.id); q.push(s.id); }
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
for (const s of slices) if (!layerMap.has(s.id)) layerMap.set(s.id, 0);
|
|
375
|
+
|
|
376
|
+
const maxLayer = Math.max(...[...layerMap.values()]);
|
|
377
|
+
const byLayer = new Map<number, string[]>();
|
|
378
|
+
for (const [id, layer] of layerMap) {
|
|
379
|
+
const arr = byLayer.get(layer) ?? [];
|
|
380
|
+
arr.push(id);
|
|
381
|
+
byLayer.set(layer, arr);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const NW = 130, NH = 40, CGAP = 56, RGAP = 14, PAD = 20;
|
|
385
|
+
let maxRows = 0;
|
|
386
|
+
for (let c = 0; c <= maxLayer; c++) maxRows = Math.max(maxRows, (byLayer.get(c) ?? []).length);
|
|
387
|
+
const totalH = PAD * 2 + maxRows * NH + Math.max(0, maxRows - 1) * RGAP;
|
|
388
|
+
const totalW = PAD * 2 + (maxLayer + 1) * NW + maxLayer * CGAP;
|
|
389
|
+
|
|
390
|
+
const pos = new Map<string, { x: number; y: number }>();
|
|
391
|
+
for (let col = 0; col <= maxLayer; col++) {
|
|
392
|
+
const ids = byLayer.get(col) ?? [];
|
|
393
|
+
const colH = ids.length * NH + Math.max(0, ids.length - 1) * RGAP;
|
|
394
|
+
const startY = (totalH - colH) / 2;
|
|
395
|
+
ids.forEach((id, i) => pos.set(id, { x: PAD + col * (NW + CGAP), y: startY + i * (NH + RGAP) }));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const edges = slices.flatMap(sl => sl.depends.flatMap(dep => {
|
|
399
|
+
if (!pos.has(dep) || !pos.has(sl.id)) return [];
|
|
400
|
+
const f = pos.get(dep)!, t = pos.get(sl.id)!;
|
|
401
|
+
const x1 = f.x + NW, y1 = f.y + NH / 2;
|
|
402
|
+
const x2 = t.x, y2 = t.y + NH / 2;
|
|
403
|
+
const mx = (x1 + x2) / 2;
|
|
404
|
+
const crit = critSL.has(sl.id) && critSL.has(dep);
|
|
405
|
+
return [`<path d="M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}" class="edge${crit ? ' edge-crit' : ''}" marker-end="url(#arr${crit ? '-crit' : ''})"/>`];
|
|
406
|
+
}));
|
|
407
|
+
|
|
408
|
+
const nodes = slices.map(sl => {
|
|
409
|
+
const p = pos.get(sl.id);
|
|
410
|
+
if (!p) return '';
|
|
411
|
+
const crit = critSL.has(sl.id);
|
|
412
|
+
const sc = sl.done ? 'n-done' : sl.active ? 'n-active' : 'n-pending';
|
|
413
|
+
return `<g class="node ${sc}${crit ? ' n-crit' : ''}" transform="translate(${p.x},${p.y})">
|
|
414
|
+
<rect width="${NW}" height="${NH}" rx="4"/>
|
|
415
|
+
<text x="${NW/2}" y="16" class="n-id">${esc(truncStr(sl.id, 18))}</text>
|
|
416
|
+
<text x="${NW/2}" y="30" class="n-title">${esc(truncStr(sl.title, 18))}</text>
|
|
417
|
+
<title>${esc(sl.id)}: ${esc(sl.title)}</title>
|
|
418
|
+
</g>`;
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
const legend = `<div class="dep-legend">
|
|
422
|
+
<span><span class="dot dot-complete dot-sm"></span> done</span>
|
|
423
|
+
<span><span class="dot dot-active dot-sm"></span> active</span>
|
|
424
|
+
<span><span class="dot dot-pending dot-sm"></span> pending</span>
|
|
425
|
+
</div>`;
|
|
426
|
+
|
|
427
|
+
return `
|
|
428
|
+
<div class="dep-block">
|
|
429
|
+
<h3>${esc(ms.id)}: ${esc(ms.title)}</h3>
|
|
430
|
+
${legend}
|
|
431
|
+
<div class="dep-wrap">
|
|
432
|
+
<svg class="dep-svg" viewBox="0 0 ${totalW} ${totalH}" width="${totalW}" height="${totalH}">
|
|
433
|
+
<defs>
|
|
434
|
+
<marker id="arr" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto">
|
|
435
|
+
<path d="M0,0 L0,6 L8,3 z" fill="var(--border-2)"/>
|
|
436
|
+
</marker>
|
|
437
|
+
<marker id="arr-crit" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto">
|
|
438
|
+
<path d="M0,0 L0,6 L8,3 z" fill="var(--accent)"/>
|
|
439
|
+
</marker>
|
|
440
|
+
</defs>
|
|
441
|
+
${edges.join('')}
|
|
442
|
+
${nodes.join('')}
|
|
443
|
+
</svg>
|
|
444
|
+
</div>
|
|
445
|
+
</div>`;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ─── Section: Metrics ─────────────────────────────────────────────────────────
|
|
449
|
+
|
|
450
|
+
function buildMetricsSection(data: VisualizerData): string {
|
|
451
|
+
if (!data.totals) return section('metrics', 'Metrics', '<p class="empty">No metrics data yet.</p>');
|
|
452
|
+
const t = data.totals;
|
|
453
|
+
|
|
454
|
+
const grid = [
|
|
455
|
+
kvi('Total cost', formatCost(t.cost)),
|
|
456
|
+
kvi('Total tokens', formatTokenCount(t.tokens.total)),
|
|
457
|
+
kvi('Input', formatTokenCount(t.tokens.input)),
|
|
458
|
+
kvi('Output', formatTokenCount(t.tokens.output)),
|
|
459
|
+
kvi('Cache read', formatTokenCount(t.tokens.cacheRead)),
|
|
460
|
+
kvi('Cache write', formatTokenCount(t.tokens.cacheWrite)),
|
|
461
|
+
kvi('Duration', formatDuration(t.duration)),
|
|
462
|
+
kvi('Units', String(t.units)),
|
|
463
|
+
kvi('Tool calls', String(t.toolCalls)),
|
|
464
|
+
kvi('Truncations', String(t.totalTruncationSections)),
|
|
465
|
+
].join('');
|
|
466
|
+
|
|
467
|
+
const tokenBreakdown = buildTokenBreakdown(t.tokens);
|
|
468
|
+
|
|
469
|
+
const phaseRow = data.byPhase.length > 0 ? `
|
|
470
|
+
<div class="chart-row">
|
|
471
|
+
${buildBarChart('Cost by phase', data.byPhase.map(p => ({
|
|
472
|
+
label: p.phase, value: p.cost, display: formatCost(p.cost), sub: `${p.units} units`,
|
|
473
|
+
})))}
|
|
474
|
+
${buildBarChart('Tokens by phase', data.byPhase.map(p => ({
|
|
475
|
+
label: p.phase, value: p.tokens.total, display: formatTokenCount(p.tokens.total), sub: formatCost(p.cost),
|
|
476
|
+
})))}
|
|
477
|
+
</div>` : '';
|
|
478
|
+
|
|
479
|
+
const sliceModelRow = (data.bySlice.length > 0 || data.byModel.length > 0) ? `
|
|
480
|
+
<div class="chart-row">
|
|
481
|
+
${data.bySlice.length > 0 ? buildBarChart('Cost by slice', data.bySlice.map(s => ({
|
|
482
|
+
label: s.sliceId, value: s.cost, display: formatCost(s.cost),
|
|
483
|
+
sub: `${s.units} units`,
|
|
484
|
+
}))) : ''}
|
|
485
|
+
${data.byModel.length > 0 ? buildBarChart('Cost by model', data.byModel.map(m => ({
|
|
486
|
+
label: shortModel(m.model), value: m.cost, display: formatCost(m.cost),
|
|
487
|
+
sub: `${m.units} units`,
|
|
488
|
+
}))) : ''}
|
|
489
|
+
</div>` : '';
|
|
490
|
+
|
|
491
|
+
return section('metrics', 'Metrics', `
|
|
492
|
+
<div class="kv-grid">${grid}</div>
|
|
493
|
+
${tokenBreakdown}
|
|
494
|
+
${phaseRow}
|
|
495
|
+
${sliceModelRow}
|
|
496
|
+
`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function buildTokenBreakdown(tokens: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number }): string {
|
|
500
|
+
if (tokens.total === 0) return '';
|
|
501
|
+
const segs = [
|
|
502
|
+
{ label: 'Input', value: tokens.input, cls: 'seg-1' },
|
|
503
|
+
{ label: 'Output', value: tokens.output, cls: 'seg-2' },
|
|
504
|
+
{ label: 'Cache read', value: tokens.cacheRead, cls: 'seg-3' },
|
|
505
|
+
{ label: 'Cache write', value: tokens.cacheWrite, cls: 'seg-4' },
|
|
506
|
+
].filter(s => s.value > 0);
|
|
507
|
+
|
|
508
|
+
const bars = segs.map(s => {
|
|
509
|
+
const pct = (s.value / tokens.total) * 100;
|
|
510
|
+
return `<div class="tseg ${s.cls}" style="width:${pct.toFixed(2)}%" title="${s.label}: ${formatTokenCount(s.value)} (${pct.toFixed(1)}%)"></div>`;
|
|
511
|
+
}).join('');
|
|
512
|
+
|
|
513
|
+
const legend = segs.map(s => {
|
|
514
|
+
const pct = ((s.value / tokens.total) * 100).toFixed(1);
|
|
515
|
+
return `<span class="leg-item"><span class="leg-dot ${s.cls}"></span>${s.label}: ${formatTokenCount(s.value)} (${pct}%)</span>`;
|
|
516
|
+
}).join('');
|
|
517
|
+
|
|
518
|
+
return `
|
|
519
|
+
<div class="token-block">
|
|
520
|
+
<h3>Token breakdown</h3>
|
|
521
|
+
<div class="token-bar">${bars}</div>
|
|
522
|
+
<div class="token-legend">${legend}</div>
|
|
523
|
+
</div>`;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
interface BarEntry { label: string; value: number; display: string; sub?: string; color?: number }
|
|
527
|
+
|
|
528
|
+
const CHART_COLORS = 6;
|
|
529
|
+
|
|
530
|
+
function buildBarChart(title: string, entries: BarEntry[]): string {
|
|
531
|
+
if (entries.length === 0) return '';
|
|
532
|
+
const max = Math.max(...entries.map(e => e.value), 1);
|
|
533
|
+
const rows = entries.map((e, i) => {
|
|
534
|
+
const pct = (e.value / max) * 100;
|
|
535
|
+
const ci = e.color ?? i;
|
|
536
|
+
return `
|
|
537
|
+
<div class="bar-row">
|
|
538
|
+
<div class="bar-lbl">${esc(truncStr(e.label, 22))}</div>
|
|
539
|
+
<div class="bar-track"><div class="bar-fill bar-c${ci % CHART_COLORS}" style="width:${pct.toFixed(1)}%"></div></div>
|
|
540
|
+
<div class="bar-val">${esc(e.display)}</div>
|
|
541
|
+
</div>
|
|
542
|
+
${e.sub ? `<div class="bar-sub">${esc(e.sub)}</div>` : ''}`;
|
|
543
|
+
}).join('');
|
|
544
|
+
return `<div class="chart-block"><h3>${esc(title)}</h3>${rows}</div>`;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ─── Section: Timeline ────────────────────────────────────────────────────────
|
|
548
|
+
|
|
549
|
+
function buildTimelineSection(data: VisualizerData): string {
|
|
550
|
+
if (data.units.length === 0) return section('timeline', 'Timeline', '<p class="empty">No units executed yet.</p>');
|
|
551
|
+
|
|
552
|
+
const sorted = [...data.units].sort((a, b) => a.startedAt - b.startedAt);
|
|
553
|
+
const maxCost = Math.max(...sorted.map(u => u.cost), 0.01);
|
|
554
|
+
|
|
555
|
+
const rows = sorted.map((u, i) => {
|
|
556
|
+
const dur = u.finishedAt > 0 ? formatDuration(u.finishedAt - u.startedAt) : 'running';
|
|
557
|
+
// Cost heatmap: subtle red background for expensive rows
|
|
558
|
+
const intensity = Math.min(u.cost / maxCost, 1);
|
|
559
|
+
const heatStyle = intensity > 0.15 ? ` style="background:rgba(239,68,68,${(intensity * 0.15).toFixed(3)})"` : '';
|
|
560
|
+
return `
|
|
561
|
+
<tr${heatStyle}>
|
|
562
|
+
<td class="muted">${i + 1}</td>
|
|
563
|
+
<td class="mono">${esc(u.type)}</td>
|
|
564
|
+
<td class="mono muted">${esc(u.id)}</td>
|
|
565
|
+
<td>${esc(shortModel(u.model))}</td>
|
|
566
|
+
<td class="muted">${formatDateShort(new Date(u.startedAt).toISOString())}</td>
|
|
567
|
+
<td>${dur}</td>
|
|
568
|
+
<td class="num">${formatCost(u.cost)}</td>
|
|
569
|
+
<td class="num">${formatTokenCount(u.tokens.total)}</td>
|
|
570
|
+
<td class="num">${u.toolCalls}</td>
|
|
571
|
+
<td class="mono">${u.tier ?? ''}</td>
|
|
572
|
+
<td>${u.modelDowngraded ? 'routed' : ''}</td>
|
|
573
|
+
<td class="num">${(u.truncationSections ?? 0) > 0 ? u.truncationSections : ''}</td>
|
|
574
|
+
<td>${u.continueHereFired ? 'yes' : ''}</td>
|
|
575
|
+
</tr>`;
|
|
576
|
+
}).join('');
|
|
577
|
+
|
|
578
|
+
return section('timeline', 'Timeline', `
|
|
579
|
+
<div class="table-scroll">
|
|
580
|
+
<table class="tbl">
|
|
581
|
+
<thead><tr>
|
|
582
|
+
<th>#</th><th>Type</th><th>ID</th><th>Model</th>
|
|
583
|
+
<th>Started</th><th>Duration</th><th>Cost</th>
|
|
584
|
+
<th>Tokens</th><th>Tools</th><th>Tier</th><th>Routed</th><th>Trunc</th><th>CHF</th>
|
|
585
|
+
</tr></thead>
|
|
586
|
+
<tbody>${rows}</tbody>
|
|
587
|
+
</table>
|
|
588
|
+
</div>`);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// ─── Section: Changelog ───────────────────────────────────────────────────────
|
|
592
|
+
|
|
593
|
+
function buildChangelogSection(data: VisualizerData): string {
|
|
594
|
+
if (data.changelog.entries.length === 0) return section('changelog', 'Changelog', '<p class="empty">No completed slices yet.</p>');
|
|
595
|
+
|
|
596
|
+
const entries = data.changelog.entries.map(e => {
|
|
597
|
+
const filesHtml = e.filesModified.length > 0 ? `
|
|
598
|
+
<details class="files-detail">
|
|
599
|
+
<summary class="muted">${e.filesModified.length} file${e.filesModified.length !== 1 ? 's' : ''} modified</summary>
|
|
600
|
+
<ul class="file-list">
|
|
601
|
+
${e.filesModified.map(f => `<li><code>${esc(f.path)}</code>${f.description ? ` — ${esc(f.description)}` : ''}</li>`).join('')}
|
|
602
|
+
</ul>
|
|
603
|
+
</details>` : '';
|
|
604
|
+
|
|
605
|
+
const ver = data.sliceVerifications.find(v => v.sliceId === e.sliceId);
|
|
606
|
+
const decisionsHtml = ver?.keyDecisions?.length ? `
|
|
607
|
+
<div class="detail-block"><span class="detail-label">Decisions</span>
|
|
608
|
+
<ul>${ver.keyDecisions.map(d => `<li>${esc(d)}</li>`).join('')}</ul>
|
|
609
|
+
</div>` : '';
|
|
610
|
+
|
|
611
|
+
return `
|
|
612
|
+
<div class="cl-entry">
|
|
613
|
+
<div class="cl-header">
|
|
614
|
+
<span class="mono muted">${esc(e.milestoneId)}/${esc(e.sliceId)}</span>
|
|
615
|
+
<span class="cl-title">${esc(e.title)}</span>
|
|
616
|
+
${e.completedAt ? `<span class="muted cl-date">${formatDateShort(e.completedAt)}</span>` : ''}
|
|
617
|
+
</div>
|
|
618
|
+
${e.oneLiner ? `<p class="cl-liner">${esc(e.oneLiner)}</p>` : ''}
|
|
619
|
+
${decisionsHtml}
|
|
620
|
+
${filesHtml}
|
|
621
|
+
</div>`;
|
|
622
|
+
}).join('');
|
|
623
|
+
|
|
624
|
+
return section('changelog', `Changelog <span class="count">${data.changelog.entries.length}</span>`, entries);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// ─── Section: Knowledge ───────────────────────────────────────────────────────
|
|
628
|
+
|
|
629
|
+
function buildKnowledgeSection(data: VisualizerData): string {
|
|
630
|
+
const k = data.knowledge;
|
|
631
|
+
if (!k.exists) return section('knowledge', 'Knowledge', '<p class="empty">No KNOWLEDGE.md found.</p>');
|
|
632
|
+
const total = k.rules.length + k.patterns.length + k.lessons.length;
|
|
633
|
+
if (total === 0) return section('knowledge', 'Knowledge', '<p class="empty">KNOWLEDGE.md exists but no entries parsed.</p>');
|
|
634
|
+
|
|
635
|
+
const rulesHtml = k.rules.length > 0 ? `
|
|
636
|
+
<h3>Rules <span class="count">${k.rules.length}</span></h3>
|
|
637
|
+
<table class="tbl">
|
|
638
|
+
<thead><tr><th>ID</th><th>Scope</th><th>Rule</th></tr></thead>
|
|
639
|
+
<tbody>${k.rules.map(r => `<tr><td class="mono">${esc(r.id)}</td><td>${esc(r.scope)}</td><td>${esc(r.content)}</td></tr>`).join('')}</tbody>
|
|
640
|
+
</table>` : '';
|
|
641
|
+
|
|
642
|
+
const patternsHtml = k.patterns.length > 0 ? `
|
|
643
|
+
<h3>Patterns <span class="count">${k.patterns.length}</span></h3>
|
|
644
|
+
<table class="tbl">
|
|
645
|
+
<thead><tr><th>ID</th><th>Pattern</th></tr></thead>
|
|
646
|
+
<tbody>${k.patterns.map(p => `<tr><td class="mono">${esc(p.id)}</td><td>${esc(p.content)}</td></tr>`).join('')}</tbody>
|
|
647
|
+
</table>` : '';
|
|
648
|
+
|
|
649
|
+
const lessonsHtml = k.lessons.length > 0 ? `
|
|
650
|
+
<h3>Lessons <span class="count">${k.lessons.length}</span></h3>
|
|
651
|
+
<table class="tbl">
|
|
652
|
+
<thead><tr><th>ID</th><th>Lesson</th></tr></thead>
|
|
653
|
+
<tbody>${k.lessons.map(l => `<tr><td class="mono">${esc(l.id)}</td><td>${esc(l.content)}</td></tr>`).join('')}</tbody>
|
|
654
|
+
</table>` : '';
|
|
655
|
+
|
|
656
|
+
return section('knowledge', `Knowledge <span class="count">${total}</span>`, `${rulesHtml}${patternsHtml}${lessonsHtml}`);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// ─── Section: Captures ────────────────────────────────────────────────────────
|
|
660
|
+
|
|
661
|
+
function buildCapturesSection(data: VisualizerData): string {
|
|
662
|
+
const c = data.captures;
|
|
663
|
+
if (c.totalCount === 0) return section('captures', 'Captures', '<p class="empty">No captures recorded.</p>');
|
|
664
|
+
|
|
665
|
+
const badge = c.pendingCount > 0
|
|
666
|
+
? `<span class="count count-warn">${c.pendingCount} pending</span>`
|
|
667
|
+
: `<span class="count">all triaged</span>`;
|
|
668
|
+
|
|
669
|
+
const rows = c.entries.map(e => `
|
|
670
|
+
<tr>
|
|
671
|
+
<td class="muted">${formatDateShort(new Date(e.timestamp).toISOString())}</td>
|
|
672
|
+
<td class="mono">${esc(e.status)}</td>
|
|
673
|
+
<td class="mono">${e.classification ?? ''}</td>
|
|
674
|
+
<td>${e.resolution ?? ''}</td>
|
|
675
|
+
<td>${esc(e.text)}</td>
|
|
676
|
+
<td class="muted">${e.rationale ?? ''}</td>
|
|
677
|
+
<td class="muted">${e.resolvedAt ? formatDateShort(e.resolvedAt) : ''}</td>
|
|
678
|
+
<td>${e.executed !== undefined ? (e.executed ? 'yes' : 'no') : ''}</td>
|
|
679
|
+
</tr>`).join('');
|
|
680
|
+
|
|
681
|
+
return section('captures', `Captures ${badge}`, `
|
|
682
|
+
<div class="table-scroll">
|
|
683
|
+
<table class="tbl">
|
|
684
|
+
<thead><tr><th>Captured</th><th>Status</th><th>Class</th><th>Resolution</th><th>Text</th><th>Rationale</th><th>Resolved</th><th>Executed</th></tr></thead>
|
|
685
|
+
<tbody>${rows}</tbody>
|
|
686
|
+
</table>
|
|
687
|
+
</div>`);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// ─── Section: Stats ───────────────────────────────────────────────────────────
|
|
691
|
+
|
|
692
|
+
function buildStatsSection(data: VisualizerData): string {
|
|
693
|
+
const s = data.stats;
|
|
694
|
+
|
|
695
|
+
const missingHtml = s.missingCount > 0 ? `
|
|
696
|
+
<h3>Missing changelogs <span class="count">${s.missingCount}</span></h3>
|
|
697
|
+
<table class="tbl">
|
|
698
|
+
<thead><tr><th>Milestone</th><th>Slice</th><th>Title</th></tr></thead>
|
|
699
|
+
<tbody>
|
|
700
|
+
${s.missingSlices.map(sl => `<tr><td class="mono">${esc(sl.milestoneId)}</td><td class="mono">${esc(sl.sliceId)}</td><td>${esc(sl.title)}</td></tr>`).join('')}
|
|
701
|
+
${s.missingCount > s.missingSlices.length
|
|
702
|
+
? `<tr><td colspan="3" class="muted">and ${s.missingCount - s.missingSlices.length} more</td></tr>`
|
|
703
|
+
: ''}
|
|
704
|
+
</tbody>
|
|
705
|
+
</table>` : '';
|
|
706
|
+
|
|
707
|
+
const updatedHtml = s.updatedCount > 0 ? `
|
|
708
|
+
<h3>Recently completed <span class="count">${s.updatedCount}</span></h3>
|
|
709
|
+
<table class="tbl">
|
|
710
|
+
<thead><tr><th>Milestone</th><th>Slice</th><th>Title</th><th>Completed</th></tr></thead>
|
|
711
|
+
<tbody>${s.updatedSlices.map(sl => `
|
|
712
|
+
<tr><td class="mono">${esc(sl.milestoneId)}</td><td class="mono">${esc(sl.sliceId)}</td><td>${esc(sl.title)}</td><td class="muted">${sl.completedAt ? formatDateShort(sl.completedAt) : ''}</td></tr>`).join('')}
|
|
713
|
+
</tbody>
|
|
714
|
+
</table>` : '';
|
|
715
|
+
|
|
716
|
+
if (!missingHtml && !updatedHtml) {
|
|
717
|
+
return section('stats', 'Artifacts', '<p class="empty">All artifacts accounted for.</p>');
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return section('stats', 'Artifacts', `${missingHtml}${updatedHtml}`);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// ─── Section: Discussion ──────────────────────────────────────────────────────
|
|
724
|
+
|
|
725
|
+
function buildDiscussionSection(data: VisualizerData): string {
|
|
726
|
+
if (data.discussion.length === 0) return section('discussion', 'Planning', '<p class="empty">No milestones.</p>');
|
|
727
|
+
|
|
728
|
+
const rows = data.discussion.map(d => `
|
|
729
|
+
<tr>
|
|
730
|
+
<td class="mono">${esc(d.milestoneId)}</td>
|
|
731
|
+
<td>${esc(d.title)}</td>
|
|
732
|
+
<td class="mono">${d.state}</td>
|
|
733
|
+
<td>${d.hasContext ? 'yes' : ''}</td>
|
|
734
|
+
<td>${d.hasDraft ? 'draft' : ''}</td>
|
|
735
|
+
<td class="muted">${d.lastUpdated ? formatDateShort(d.lastUpdated) : ''}</td>
|
|
736
|
+
</tr>`).join('');
|
|
737
|
+
|
|
738
|
+
return section('discussion', 'Planning', `
|
|
739
|
+
<table class="tbl">
|
|
740
|
+
<thead><tr><th>ID</th><th>Milestone</th><th>State</th><th>Context</th><th>Draft</th><th>Updated</th></tr></thead>
|
|
741
|
+
<tbody>${rows}</tbody>
|
|
742
|
+
</table>`);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// ─── Primitives ────────────────────────────────────────────────────────────────
|
|
746
|
+
|
|
747
|
+
function section(id: string, title: string, body: string): string {
|
|
748
|
+
return `\n<section id="${id}">\n <h2>${title}</h2>\n ${body}\n</section>`;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function kvi(label: string, value: string): string {
|
|
752
|
+
return `<div class="kv"><span class="kv-val">${esc(value)}</span><span class="kv-lbl">${esc(label)}</span></div>`;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function hRow(label: string, value: string, status?: 'ok' | 'caution' | 'warn'): string {
|
|
756
|
+
const cls = status ? ` class="h-${status}"` : '';
|
|
757
|
+
return `<tr${cls}><td>${esc(label)}</td><td>${esc(value)}</td></tr>`;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function shortModel(m: string) { return m.replace(/^claude-/, '').replace(/^anthropic\//, ''); }
|
|
761
|
+
function truncStr(s: string, n: number) { return s.length > n ? s.slice(0, n - 1) + '\u2026' : s; }
|
|
762
|
+
|
|
763
|
+
function formatDateLong(iso: string): string {
|
|
764
|
+
try {
|
|
765
|
+
const d = new Date(iso);
|
|
766
|
+
return d.toLocaleString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit', timeZoneName: 'short' });
|
|
767
|
+
} catch { return iso; }
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function formatDateShort(iso: string): string {
|
|
771
|
+
try {
|
|
772
|
+
const d = new Date(iso);
|
|
773
|
+
return d.toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
774
|
+
} catch { return iso; }
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function esc(s: string | undefined | null): string {
|
|
778
|
+
if (s == null) return '';
|
|
779
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// ─── CSS ───────────────────────────────────────────────────────────────────────
|
|
783
|
+
// Linear-inspired: restrained palette, one accent, no emoji, no gradients.
|
|
784
|
+
|
|
785
|
+
const CSS = `
|
|
786
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
787
|
+
:root{
|
|
788
|
+
--bg-0:#0f1115;--bg-1:#16181d;--bg-2:#1e2028;--bg-3:#272a33;
|
|
789
|
+
--border-1:#2b2e38;--border-2:#3b3f4c;
|
|
790
|
+
--text-0:#ededef;--text-1:#a1a1aa;--text-2:#71717a;
|
|
791
|
+
--accent:#5e6ad2;--accent-subtle:rgba(94,106,210,.12);
|
|
792
|
+
--ok:#22c55e;--ok-subtle:rgba(34,197,94,.12);--warn:#ef4444;--caution:#eab308;
|
|
793
|
+
/* Chart palette — 6 hues for bar charts */
|
|
794
|
+
--c0:#5e6ad2;--c1:#e5796d;--c2:#14b8a6;--c3:#a78bfa;--c4:#f59e0b;--c5:#10b981;
|
|
795
|
+
/* Token breakdown — 4 distinct hues */
|
|
796
|
+
--tk-input:#5e6ad2;--tk-output:#e5796d;--tk-cache-r:#2dd4bf;--tk-cache-w:#64748b;
|
|
797
|
+
--font:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
|
|
798
|
+
--mono:'JetBrains Mono','Fira Code',ui-monospace,SFMono-Regular,monospace;
|
|
799
|
+
}
|
|
800
|
+
html{scroll-behavior:smooth;font-size:13px}
|
|
801
|
+
body{background:var(--bg-0);color:var(--text-0);font-family:var(--font);line-height:1.6;-webkit-font-smoothing:antialiased}
|
|
802
|
+
a{color:var(--accent);text-decoration:none}
|
|
803
|
+
a:hover{text-decoration:underline}
|
|
804
|
+
code{font-family:var(--mono);font-size:12px;background:var(--bg-3);padding:1px 5px;border-radius:3px}
|
|
805
|
+
.mono{font-family:var(--mono);font-size:12px}
|
|
806
|
+
.muted{color:var(--text-2)}
|
|
807
|
+
.accent{color:var(--accent)}
|
|
808
|
+
.sep{color:var(--border-2);margin:0 4px}
|
|
809
|
+
.empty{color:var(--text-2);padding:8px 0;font-size:13px}
|
|
810
|
+
.indent{padding-left:12px}
|
|
811
|
+
.num{font-variant-numeric:tabular-nums;text-align:right}
|
|
812
|
+
|
|
813
|
+
/* Status dots — geometric, no emoji */
|
|
814
|
+
.dot{display:inline-block;width:8px;height:8px;border-radius:50%;flex-shrink:0;vertical-align:middle}
|
|
815
|
+
.dot-sm{width:6px;height:6px}
|
|
816
|
+
.dot-complete{background:var(--ok);opacity:.6}
|
|
817
|
+
.dot-active{background:var(--accent)}
|
|
818
|
+
.dot-pending{background:transparent;border:1.5px solid var(--border-2)}
|
|
819
|
+
|
|
820
|
+
/* Header */
|
|
821
|
+
header{background:var(--bg-1);border-bottom:1px solid var(--border-1);padding:12px 32px;position:sticky;top:0;z-index:200}
|
|
822
|
+
.header-inner{display:flex;align-items:center;gap:16px;max-width:1280px;margin:0 auto}
|
|
823
|
+
.branding{display:flex;align-items:baseline;gap:6px;flex-shrink:0}
|
|
824
|
+
.logo{font-size:18px;font-weight:800;letter-spacing:-.5px;color:var(--text-0)}
|
|
825
|
+
.version{font-size:10px;color:var(--text-2);font-family:var(--mono)}
|
|
826
|
+
.header-meta{flex:1;min-width:0}
|
|
827
|
+
.header-meta h1{font-size:15px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
828
|
+
.header-path{font-size:11px;color:var(--text-2);font-family:var(--mono);display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
829
|
+
.header-right{text-align:right;flex-shrink:0;display:flex;flex-direction:column;align-items:flex-end;gap:4px}
|
|
830
|
+
.generated{font-size:11px;color:var(--text-2)}
|
|
831
|
+
.back-link{font-size:12px;color:var(--text-1)}
|
|
832
|
+
.back-link:hover{color:var(--accent)}
|
|
833
|
+
|
|
834
|
+
/* TOC nav */
|
|
835
|
+
.toc{background:var(--bg-1);border-bottom:1px solid var(--border-1);overflow-x:auto}
|
|
836
|
+
.toc ul{display:flex;list-style:none;max-width:1280px;margin:0 auto;padding:0 32px}
|
|
837
|
+
.toc a{display:inline-block;padding:8px 12px;color:var(--text-2);font-size:12px;font-weight:500;border-bottom:2px solid transparent;transition:color .12s,border-color .12s;white-space:nowrap;text-decoration:none}
|
|
838
|
+
.toc a:hover{color:var(--text-0);border-bottom-color:var(--border-2)}
|
|
839
|
+
.toc a.active{color:var(--text-0);border-bottom-color:var(--accent)}
|
|
840
|
+
|
|
841
|
+
/* Layout */
|
|
842
|
+
main{max-width:1280px;margin:0 auto;padding:32px;display:flex;flex-direction:column;gap:48px}
|
|
843
|
+
section{scroll-margin-top:82px}
|
|
844
|
+
section>h2{font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text-1);margin-bottom:16px;padding-bottom:8px;border-bottom:1px solid var(--border-1);display:flex;align-items:center;gap:8px}
|
|
845
|
+
h3{font-size:13px;font-weight:600;color:var(--text-1);margin:20px 0 8px}
|
|
846
|
+
.count{font-size:11px;font-weight:500;color:var(--text-2);background:var(--bg-3);border-radius:3px;padding:1px 6px}
|
|
847
|
+
.count-warn{color:var(--caution)}
|
|
848
|
+
|
|
849
|
+
/* KV grid (stats/metrics) */
|
|
850
|
+
.kv-grid{display:flex;flex-wrap:wrap;gap:1px;background:var(--border-1);border:1px solid var(--border-1);border-radius:4px;overflow:hidden;margin-bottom:16px}
|
|
851
|
+
.kv{background:var(--bg-1);padding:10px 16px;display:flex;flex-direction:column;gap:2px;min-width:110px;flex:1}
|
|
852
|
+
.kv-val{font-size:18px;font-weight:600;color:var(--text-0);font-variant-numeric:tabular-nums}
|
|
853
|
+
.kv-lbl{font-size:10px;color:var(--text-2);text-transform:uppercase;letter-spacing:.4px}
|
|
854
|
+
|
|
855
|
+
/* Progress bar */
|
|
856
|
+
.progress-wrap{display:flex;align-items:center;gap:10px;margin-bottom:12px}
|
|
857
|
+
.progress-track{flex:1;height:4px;background:var(--bg-3);border-radius:2px;overflow:hidden}
|
|
858
|
+
.progress-fill{height:100%;background:var(--accent);border-radius:2px}
|
|
859
|
+
.progress-label{font-size:12px;font-weight:600;color:var(--text-1);min-width:40px;text-align:right}
|
|
860
|
+
.active-info{font-size:12px;color:var(--text-1);margin-bottom:4px}
|
|
861
|
+
.activity-line{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--text-1);padding:6px 0}
|
|
862
|
+
|
|
863
|
+
/* Tables */
|
|
864
|
+
.tbl{width:100%;border-collapse:collapse;font-size:12px}
|
|
865
|
+
.tbl th{color:var(--text-2);font-weight:500;padding:6px 12px;text-align:left;border-bottom:1px solid var(--border-1);font-size:11px;text-transform:uppercase;letter-spacing:.3px;white-space:nowrap}
|
|
866
|
+
.tbl td{padding:6px 12px;border-bottom:1px solid var(--border-1);vertical-align:top}
|
|
867
|
+
.tbl tr:last-child td{border-bottom:none}
|
|
868
|
+
.tbl tbody tr:hover td{background:var(--accent-subtle)}
|
|
869
|
+
.tbl-kv td:first-child{color:var(--text-2);width:180px}
|
|
870
|
+
.table-scroll{overflow-x:auto;border:1px solid var(--border-1);border-radius:4px}
|
|
871
|
+
.table-scroll .tbl{border:none}
|
|
872
|
+
|
|
873
|
+
/* Health */
|
|
874
|
+
.h-ok td:first-child{color:var(--text-1)}
|
|
875
|
+
.h-caution td{color:var(--caution)}
|
|
876
|
+
.h-warn td{color:var(--warn)}
|
|
877
|
+
|
|
878
|
+
/* Labels */
|
|
879
|
+
.label{font-size:10px;font-weight:500;color:var(--accent);text-transform:uppercase;letter-spacing:.4px}
|
|
880
|
+
.risk{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.3px;flex-shrink:0}
|
|
881
|
+
.risk-low{color:var(--text-2)}
|
|
882
|
+
.risk-medium{color:var(--caution)}
|
|
883
|
+
.risk-high{color:var(--warn)}
|
|
884
|
+
.risk-unknown{color:var(--text-2)}
|
|
885
|
+
|
|
886
|
+
/* Tags */
|
|
887
|
+
.tag-row{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px}
|
|
888
|
+
.tag{font-size:11px;font-family:var(--mono);color:var(--text-2);background:var(--bg-3);border-radius:3px;padding:1px 6px}
|
|
889
|
+
|
|
890
|
+
/* Verification */
|
|
891
|
+
.verif{font-size:12px;color:var(--text-1);padding:4px 0;margin-bottom:6px}
|
|
892
|
+
.verif-blocker{color:var(--warn)}
|
|
893
|
+
|
|
894
|
+
/* Detail blocks */
|
|
895
|
+
.detail-block{font-size:12px;color:var(--text-2);margin-bottom:6px}
|
|
896
|
+
.detail-label{font-weight:600;color:var(--text-1);display:block;margin-bottom:2px}
|
|
897
|
+
.detail-block ul{padding-left:16px;margin-top:2px}
|
|
898
|
+
.detail-block li{margin-bottom:1px}
|
|
899
|
+
|
|
900
|
+
/* Progress tree */
|
|
901
|
+
.ms-block{border:1px solid var(--border-1);border-radius:4px;overflow:hidden;margin-bottom:8px}
|
|
902
|
+
.ms-summary{display:flex;align-items:center;gap:8px;padding:10px 14px;cursor:pointer;list-style:none;background:var(--bg-1);user-select:none;font-size:13px}
|
|
903
|
+
.ms-summary:hover{background:var(--bg-2)}
|
|
904
|
+
.ms-summary::-webkit-details-marker{display:none}
|
|
905
|
+
.ms-id{font-weight:600}
|
|
906
|
+
.ms-title{flex:1;font-weight:500;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
907
|
+
.ms-body{padding:6px 12px 8px 24px;display:flex;flex-direction:column;gap:4px}
|
|
908
|
+
|
|
909
|
+
.sl-block{border:1px solid var(--border-1);border-radius:3px;overflow:hidden}
|
|
910
|
+
.sl-summary{display:flex;align-items:center;gap:6px;padding:6px 10px;cursor:pointer;list-style:none;background:var(--bg-2);font-size:12px;user-select:none}
|
|
911
|
+
.sl-summary:hover{background:var(--bg-3)}
|
|
912
|
+
.sl-summary::-webkit-details-marker{display:none}
|
|
913
|
+
.sl-crit{border-left:2px solid var(--accent)}
|
|
914
|
+
.sl-deps::before{content:'\\2190 ';color:var(--border-2)}
|
|
915
|
+
.sl-detail{padding:8px 12px;background:var(--bg-0);border-top:1px solid var(--border-1)}
|
|
916
|
+
|
|
917
|
+
.task-list{list-style:none;padding:4px 0 0;display:flex;flex-direction:column;gap:2px}
|
|
918
|
+
.task-row{display:flex;align-items:center;gap:6px;font-size:12px;padding:3px 6px;border-radius:2px}
|
|
919
|
+
|
|
920
|
+
/* Dep graph */
|
|
921
|
+
.dep-block{margin-bottom:28px}
|
|
922
|
+
.dep-legend{display:flex;gap:14px;font-size:12px;color:var(--text-2);margin-bottom:8px;align-items:center}
|
|
923
|
+
.dep-legend span{display:flex;align-items:center;gap:4px}
|
|
924
|
+
.dep-wrap{overflow-x:auto;background:var(--bg-1);border:1px solid var(--border-1);border-radius:4px;padding:16px}
|
|
925
|
+
.dep-svg{display:block}
|
|
926
|
+
.edge{fill:none;stroke:var(--border-2);stroke-width:1.5}
|
|
927
|
+
.edge-crit{stroke:var(--accent);stroke-width:2}
|
|
928
|
+
.node rect{fill:var(--bg-2);stroke:var(--border-2);stroke-width:1}
|
|
929
|
+
.n-done rect{fill:var(--ok-subtle);stroke:rgba(34,197,94,.4)}
|
|
930
|
+
.n-active rect{fill:var(--accent-subtle);stroke:var(--accent)}
|
|
931
|
+
.n-crit rect{stroke:var(--accent)!important;stroke-width:1.5!important}
|
|
932
|
+
.n-id{font-family:var(--mono);font-size:10px;fill:var(--text-1);font-weight:600;text-anchor:middle}
|
|
933
|
+
.n-title{font-size:9px;fill:var(--text-2);text-anchor:middle}
|
|
934
|
+
.n-active .n-id{fill:var(--accent)}
|
|
935
|
+
|
|
936
|
+
/* Metrics */
|
|
937
|
+
.token-block{background:var(--bg-1);border:1px solid var(--border-1);border-radius:4px;padding:14px;margin-bottom:16px}
|
|
938
|
+
.token-bar{display:flex;height:16px;border-radius:2px;overflow:hidden;gap:1px;margin-bottom:8px}
|
|
939
|
+
.tseg{height:100%;min-width:2px}
|
|
940
|
+
.seg-1{background:var(--tk-input)}
|
|
941
|
+
.seg-2{background:var(--tk-output)}
|
|
942
|
+
.seg-3{background:var(--tk-cache-r)}
|
|
943
|
+
.seg-4{background:var(--tk-cache-w)}
|
|
944
|
+
.token-legend{display:flex;flex-wrap:wrap;gap:12px}
|
|
945
|
+
.leg-item{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--text-2)}
|
|
946
|
+
.leg-dot{width:8px;height:8px;border-radius:2px;flex-shrink:0}
|
|
947
|
+
.chart-row{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px}
|
|
948
|
+
@media(max-width:860px){.chart-row{grid-template-columns:1fr}}
|
|
949
|
+
.chart-block{background:var(--bg-1);border:1px solid var(--border-1);border-radius:4px;padding:14px}
|
|
950
|
+
.bar-row{display:grid;grid-template-columns:120px 1fr 68px;align-items:center;gap:6px;margin-bottom:2px}
|
|
951
|
+
.bar-lbl{font-size:12px;color:var(--text-2);text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
952
|
+
.bar-track{height:14px;background:var(--bg-3);border-radius:2px;overflow:hidden}
|
|
953
|
+
.bar-fill{height:100%;border-radius:2px;background:var(--c0)}
|
|
954
|
+
.bar-c0{background:var(--c0)}.bar-c1{background:var(--c1)}.bar-c2{background:var(--c2)}
|
|
955
|
+
.bar-c3{background:var(--c3)}.bar-c4{background:var(--c4)}.bar-c5{background:var(--c5)}
|
|
956
|
+
.bar-val{font-size:11px;font-variant-numeric:tabular-nums;color:var(--text-1)}
|
|
957
|
+
.bar-sub{font-size:10px;color:var(--text-2);padding-left:128px;margin-bottom:6px}
|
|
958
|
+
|
|
959
|
+
/* Changelog */
|
|
960
|
+
.cl-entry{border-bottom:1px solid var(--border-1);padding:12px 0}
|
|
961
|
+
.cl-entry:last-child{border-bottom:none}
|
|
962
|
+
.cl-header{display:flex;align-items:center;gap:8px;margin-bottom:4px}
|
|
963
|
+
.cl-title{flex:1;font-weight:500}
|
|
964
|
+
.cl-date{margin-left:auto;white-space:nowrap}
|
|
965
|
+
.cl-liner{font-size:13px;color:var(--text-1);margin-bottom:6px}
|
|
966
|
+
.files-detail summary{font-size:12px;cursor:pointer}
|
|
967
|
+
.file-list{list-style:none;padding-left:10px;margin-top:4px;display:flex;flex-direction:column;gap:2px}
|
|
968
|
+
.file-list li{font-size:12px;color:var(--text-1)}
|
|
969
|
+
|
|
970
|
+
/* Footer */
|
|
971
|
+
footer{border-top:1px solid var(--border-1);padding:20px 32px;margin-top:40px}
|
|
972
|
+
.footer-inner{display:flex;align-items:center;gap:6px;justify-content:center;font-size:11px;color:var(--text-2)}
|
|
973
|
+
|
|
974
|
+
/* Print */
|
|
975
|
+
@media print{
|
|
976
|
+
header,nav.toc{position:static}
|
|
977
|
+
body{background:#fff;color:#1a1a1a}
|
|
978
|
+
:root{--bg-0:#fff;--bg-1:#fafafa;--bg-2:#f5f5f5;--bg-3:#ebebeb;--border-1:#e5e5e5;--border-2:#d4d4d4;--text-0:#1a1a1a;--text-1:#525252;--text-2:#a3a3a3;--accent:#4f46e5;--ok:#16a34a;--ok-subtle:rgba(22,163,74,.08);--c0:#4f46e5;--c1:#dc2626;--c2:#0d9488;--c3:#7c3aed;--c4:#d97706;--c5:#059669;--tk-input:#4f46e5;--tk-output:#dc2626;--tk-cache-r:#0d9488;--tk-cache-w:#64748b}
|
|
979
|
+
section{page-break-inside:avoid}
|
|
980
|
+
.table-scroll{overflow:visible}
|
|
981
|
+
}
|
|
982
|
+
`;
|
|
983
|
+
|
|
984
|
+
// ─── JS ────────────────────────────────────────────────────────────────────────
|
|
985
|
+
|
|
986
|
+
const JS = `
|
|
987
|
+
(function(){
|
|
988
|
+
const sections=document.querySelectorAll('section[id]');
|
|
989
|
+
const links=document.querySelectorAll('.toc a');
|
|
990
|
+
if(!sections.length||!links.length)return;
|
|
991
|
+
const obs=new IntersectionObserver(entries=>{
|
|
992
|
+
for(const e of entries){
|
|
993
|
+
if(!e.isIntersecting)continue;
|
|
994
|
+
for(const l of links)l.classList.remove('active');
|
|
995
|
+
const a=document.querySelector('.toc a[href="#'+e.target.id+'"]');
|
|
996
|
+
if(a)a.classList.add('active');
|
|
997
|
+
}
|
|
998
|
+
},{rootMargin:'-10% 0px -80% 0px',threshold:0});
|
|
999
|
+
for(const s of sections)obs.observe(s);
|
|
1000
|
+
})();
|
|
1001
|
+
`;
|