@uniqbit/mate-core 0.18.5 → 1.0.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/claude-plugin/hooks/companion-instructions.mjs +9 -0
  2. package/claude-plugin/hooks/hooks.json +12 -2
  3. package/package.json +10 -2
  4. package/src/brand/art.generated.ts +69 -0
  5. package/src/brand/index.ts +12 -0
  6. package/src/brand/palette.ts +19 -0
  7. package/src/brand/svg.generated.ts +8 -0
  8. package/src/brand/terminal.ts +83 -0
  9. package/src/brand/types.ts +33 -0
  10. package/src/cli/commands/artifact/artifact.ts +8 -0
  11. package/src/cli/commands/artifact/delivery/archive.ts +117 -0
  12. package/src/cli/commands/artifact/delivery/branch-flags.ts +55 -0
  13. package/src/cli/commands/artifact/delivery/environment.ts +104 -0
  14. package/src/cli/commands/artifact/delivery/link.ts +154 -0
  15. package/src/cli/commands/artifact/delivery/output.ts +58 -0
  16. package/src/cli/commands/artifact/finish/command.ts +33 -3
  17. package/src/cli/commands/artifact/finish/engine.ts +47 -3
  18. package/src/cli/commands/artifact/finish/finisher.ts +10 -0
  19. package/src/cli/commands/artifact/finish/openspec.ts +177 -138
  20. package/src/cli/commands/artifact/finish/secret-scan.ts +43 -0
  21. package/src/cli/commands/artifact/pending/command.ts +31 -3
  22. package/src/cli/commands/artifact/pending/discovery.ts +21 -57
  23. package/src/cli/commands/cap/graphify.ts +52 -3
  24. package/src/cli/commands/cap/index-cmd.ts +8 -1
  25. package/src/cli/commands/cap/openspec.ts +13 -25
  26. package/src/cli/commands/companion/open.ts +56 -39
  27. package/src/cli/commands/companion/update.ts +75 -42
  28. package/src/cli/commands/doctor.ts +176 -3
  29. package/src/cli/commands/plugin/install.ts +9 -4
  30. package/src/cli/commands/plugin/plugin.ts +3 -2
  31. package/src/cli/commands/report/adapter.ts +11 -2
  32. package/src/cli/commands/report/contract.ts +21 -0
  33. package/src/cli/commands/report/highlight.ts +34 -4
  34. package/src/cli/commands/report/markdown.ts +110 -0
  35. package/src/cli/commands/report/palette.ts +118 -0
  36. package/src/cli/commands/report/renderer.ts +456 -153
  37. package/src/cli/commands/report/types.ts +16 -0
  38. package/src/cli/commands/setup.ts +7 -15
  39. package/src/cli/commands/studio/client-asset.ts +16 -0
  40. package/src/cli/commands/studio/openspec-cli.ts +10 -58
  41. package/src/cli/commands/studio/routes.ts +2 -0
  42. package/src/cli/commands/studio/selection.ts +19 -1
  43. package/src/cli/commands/studio/server.ts +42 -1
  44. package/src/cli/commands/studio/views/client.ts +6 -616
  45. package/src/cli/commands/studio/views/dashboard/changes.tsx +5 -10
  46. package/src/cli/commands/studio/views/document.tsx +137 -69
  47. package/src/cli/commands/studio/views/markdown.tsx +250 -0
  48. package/src/cli/commands/studio/views/palette.ts +134 -0
  49. package/src/cli/commands/studio/views/reports/index.tsx +2 -7
  50. package/src/cli/commands/studio/views/skills/index.tsx +43 -62
  51. package/src/cli/commands/studio/views/specs/index.tsx +7 -15
  52. package/src/cli/commands/studio/views/studio-client/actions.ts +51 -0
  53. package/src/cli/commands/studio/views/studio-client/companion.ts +13 -0
  54. package/src/cli/commands/studio/views/studio-client/index.ts +32 -0
  55. package/src/cli/commands/studio/views/studio-client/keys.ts +3 -0
  56. package/src/cli/commands/studio/views/studio-client/main.ts +3 -0
  57. package/src/cli/commands/studio/views/studio-client/page.ts +146 -0
  58. package/src/cli/commands/studio/views/studio-client/panels.ts +57 -0
  59. package/src/cli/commands/studio/views/studio-client/reports.ts +122 -0
  60. package/src/cli/commands/studio/views/studio-client/theme.ts +63 -0
  61. package/src/cli/commands/studio/views/studio-client/vault-editor.ts +166 -0
  62. package/src/cli/commands/studio/views/studio-client/vault-pane.ts +202 -0
  63. package/src/cli/commands/studio/views/studio-client/vault-tree.ts +258 -0
  64. package/src/cli/commands/studio/views/studio-client.generated.ts +2 -0
  65. package/src/cli/commands/studio/views/styles.ts +217 -206
  66. package/src/cli/commands/studio/views/vault/state.ts +3 -5
  67. package/src/cli/commands/studio/views/workflow/delivery-path.tsx +370 -0
  68. package/src/cli/commands/studio/views/workflow/graph.ts +220 -0
  69. package/src/cli/commands/studio/views/workflow/index.tsx +2 -3
  70. package/src/cli/commands/studio/views/workflow/steps.ts +228 -287
  71. package/src/cli/commands/wrap.ts +5 -13
  72. package/src/cli/main.ts +4 -1
  73. package/src/cli/parse-flags.ts +25 -7
  74. package/src/cli/setup-selector.tsx +38 -162
  75. package/src/cli/usage.ts +4 -0
  76. package/src/hooks/companion-instructions.ts +190 -0
  77. package/src/hooks/hook-io.ts +63 -0
  78. package/src/hooks/session-banner.ts +21 -29
  79. package/src/hooks/session-guidance.ts +9 -17
  80. package/src/hooks/session-prepare.ts +115 -28
  81. package/src/hooks/validate-artifact-path.ts +82 -24
  82. package/src/index.ts +4 -0
  83. package/src/lib/change-layout.ts +154 -0
  84. package/src/lib/components/wizard-header.tsx +3 -1
  85. package/src/lib/context-mode-package.ts +24 -11
  86. package/src/lib/delivery/archive-change.ts +154 -0
  87. package/src/lib/delivery/branches.ts +108 -0
  88. package/src/lib/delivery/environment.ts +29 -0
  89. package/src/lib/delivery/forge.ts +279 -0
  90. package/src/lib/delivery/overlap.ts +202 -0
  91. package/src/lib/delivery/record-delivery.ts +196 -0
  92. package/src/lib/delivery/record.ts +343 -0
  93. package/src/lib/delivery/refresh.ts +174 -0
  94. package/src/lib/delivery/repository-access.ts +97 -0
  95. package/src/lib/delivery/stages.ts +111 -0
  96. package/src/lib/delivery/token.ts +58 -0
  97. package/src/lib/frontmatter.ts +18 -0
  98. package/src/lib/opencode-plugin-package.ts +5 -5
  99. package/src/lib/openspec-cli.ts +116 -0
  100. package/src/lib/openspec-scopes.ts +86 -0
  101. package/src/lib/orchestrator/adapters/claude-args.ts +50 -0
  102. package/src/lib/orchestrator/adapters/claude.ts +48 -40
  103. package/src/lib/orchestrator/adapters/opencode.ts +2 -0
  104. package/src/lib/orchestrator/editor.ts +9 -9
  105. package/src/lib/orchestrator/openspec-store.ts +14 -54
  106. package/src/lib/orchestrator/projection-claude-commands.ts +36 -0
  107. package/src/lib/orchestrator/projection-claude-links.ts +159 -0
  108. package/src/lib/orchestrator/projection-claude-skills.ts +19 -137
  109. package/src/lib/orchestrator/projection-entries.ts +45 -14
  110. package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
  111. package/src/lib/orchestrator/projection-types.ts +3 -0
  112. package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
  113. package/src/lib/orchestrator/session-prepare.ts +45 -5
  114. package/src/lib/orchestrator/setup-compatibilities.ts +18 -75
  115. package/src/lib/orchestrator/types.ts +2 -2
  116. package/src/lib/orchestrator/wrap-pass.ts +22 -1
  117. package/src/lib/package-paths.ts +8 -0
  118. package/src/opencode/companion-hooks.ts +64 -5
  119. package/src/opencode/companion-policy.ts +37 -18
  120. package/src/opencode/projected-guidance.ts +4 -1
  121. package/src/opencode/session-prepare.ts +2 -1
  122. package/src/opencode/tui.tsx +4 -2
  123. package/src/runtime/artifact-refusal.ts +63 -0
  124. package/src/runtime/claude-delivery-plan.ts +89 -7
  125. package/src/runtime/commit-attribution-guard.ts +181 -0
  126. package/src/runtime/companion-guidance.ts +15 -3
  127. package/src/runtime/companion-instructions.ts +340 -0
  128. package/src/runtime/error-message.ts +4 -0
  129. package/src/runtime/index.ts +5 -0
  130. package/src/runtime/managed-block.ts +117 -0
  131. package/src/runtime/markdown-scan.ts +266 -0
  132. package/src/runtime/policy.ts +15 -1
  133. package/src/runtime/projected-flag.ts +7 -0
  134. package/src/runtime/projected-guidance.ts +1 -0
  135. package/src/runtime/runtime-document-manifest.ts +62 -11
  136. package/src/runtime/secret-guard.ts +148 -0
  137. package/src/runtime/secret-patterns.ts +164 -0
  138. package/src/runtime/secret-scan.ts +90 -0
  139. package/src/runtime/session-kind.ts +13 -0
  140. package/src/runtime/shell-heredoc.ts +81 -0
  141. package/src/runtime/wrap-state.ts +36 -2
  142. package/src/runtime/write-atomic.ts +64 -0
  143. package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +8 -6
  144. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +3 -5
  145. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +7 -9
  146. package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
  147. package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +10 -12
  148. package/src/templates/mate-skills/THIRD-PARTY-NOTICES.md +40 -0
  149. package/src/templates/mate-skills/agents/mate-apply-change/SKILL.md +76 -0
  150. package/src/templates/mate-skills/agents/mate-apply-change/references/openspec-apply-change.md +198 -0
  151. package/src/templates/mate-skills/agents/mate-archive-change/SKILL.md +59 -0
  152. package/src/templates/mate-skills/{claude/mate-openspec-backfill → agents/mate-backfill-spec}/SKILL.md +4 -4
  153. package/src/templates/mate-skills/agents/mate-clarify-intent/SKILL.md +95 -0
  154. package/src/templates/mate-skills/agents/mate-explore-change/SKILL.md +50 -0
  155. package/src/templates/mate-skills/agents/mate-explore-change/references/openspec-explore.md +348 -0
  156. package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +3 -3
  157. package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +5 -5
  158. package/src/templates/mate-skills/agents/mate-propose-change/SKILL.md +103 -0
  159. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-propose.md +167 -0
  160. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-update-change.md +101 -0
  161. package/src/templates/mate-skills/{claude/mate-artifact-publish → agents/mate-publish-artifact}/SKILL.md +91 -23
  162. package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  163. package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +74 -16
  164. package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +74 -68
  165. package/src/templates/mate-skills/claude/mate-apply-change/SKILL.md +76 -0
  166. package/src/templates/mate-skills/claude/mate-apply-change/references/openspec-apply-change.md +198 -0
  167. package/src/templates/mate-skills/claude/mate-archive-change/SKILL.md +59 -0
  168. package/src/templates/mate-skills/{agents/mate-openspec-backfill → claude/mate-backfill-spec}/SKILL.md +4 -4
  169. package/src/templates/mate-skills/claude/mate-clarify-intent/SKILL.md +95 -0
  170. package/src/templates/mate-skills/claude/mate-explore-change/SKILL.md +50 -0
  171. package/src/templates/mate-skills/claude/mate-explore-change/references/openspec-explore.md +348 -0
  172. package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +3 -3
  173. package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +5 -5
  174. package/src/templates/mate-skills/claude/mate-propose-change/SKILL.md +103 -0
  175. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-propose.md +167 -0
  176. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-update-change.md +101 -0
  177. package/src/templates/mate-skills/{agents/mate-artifact-publish → claude/mate-publish-artifact}/SKILL.md +91 -23
  178. package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  179. package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +74 -16
  180. package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +74 -68
  181. package/src/templates/mate-skills/openspec-references.ts +140 -0
  182. package/src/templates/root/TEMPLATE_AGENTS.md +2 -2
  183. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3727 -320
  184. package/src/tools/setup/capabilities/context-mode.ts +25 -5
  185. package/src/tools/setup/capabilities/graphify-gate.ts +31 -0
  186. package/src/tools/setup/capabilities/graphify-secrets.ts +255 -0
  187. package/src/tools/setup/capabilities/graphify.ts +12 -0
  188. package/src/tools/setup/capabilities/openspec.ts +151 -270
  189. package/src/tools/setup/capabilities/tokensave.ts +36 -3
  190. package/src/tools/setup/context-services.ts +9 -17
  191. package/src/tools/setup/dynamic-plugins/install.ts +24 -28
  192. package/src/tools/setup/engine.ts +97 -21
  193. package/src/tools/setup/managed-output.ts +51 -0
  194. package/src/tools/setup/mate.ts +86 -11
  195. package/src/tools/setup/plugin.ts +35 -0
  196. package/src/tools/setup/plugins/guidance.ts +7 -4
  197. package/src/tools/setup/providers/agent-file-sections.ts +4 -2
  198. package/src/tools/setup/providers/claude-attribution.ts +65 -0
  199. package/src/tools/setup/providers/claude-plugin-folders.ts +625 -0
  200. package/src/tools/setup/providers/claude-plugin-hooks.ts +39 -19
  201. package/src/tools/setup/providers/claude.ts +284 -8
  202. package/src/tools/setup/providers/opencode.ts +180 -1
  203. package/src/tools/setup/utils.ts +14 -4
  204. package/src/tools/setup.ts +126 -10
  205. package/wrappers/bin/graphify +52 -4
  206. package/wrappers/bin/openspec +33 -9
  207. package/wrappers/lib/graphify-gate.mjs +8 -0
  208. package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
  209. package/src/tools/setup/runtime-documents.ts +0 -291
@@ -1,13 +1,19 @@
1
1
  import { reportDataToDocument } from "./adapter";
2
2
  import { validateReportDocument } from "./contract";
3
3
  import { highlightInline, highlightStyleBlock } from "./highlight";
4
+ import { escapeHTML, renderMarkdownSubset } from "./markdown";
5
+ import { DARK_PALETTE, LIGHT_PALETTE, paletteCss, type ReportPalette } from "./palette";
4
6
  import type {
5
7
  ReportData,
6
8
  ReportDiagramSection,
7
9
  ReportDiffSection,
8
10
  ReportDocument,
9
11
  ReportKeyValue,
12
+ ReportMetric,
10
13
  ReportSection,
14
+ ReportStatus,
15
+ ReportStatusTone,
16
+ ReportTextSection,
11
17
  ReportValue,
12
18
  } from "./types";
13
19
 
@@ -24,11 +30,20 @@ const MERMAID_RUNTIME_ID = "mermaid-runtime";
24
30
 
25
31
  const CODE_BLOCK_SELECTOR = "pre.report-code";
26
32
 
33
+ /** A report with more sections than this carries an index linking to each one. */
34
+ const INDEX_THRESHOLD = 4;
35
+
27
36
  const carriesMermaid = (sections: ReportSection[]): boolean =>
28
37
  sections.some((section) => section.type === "diagram" && section.mermaid !== undefined);
29
38
 
30
- const carriesVisual = (sections: ReportSection[]): boolean =>
31
- sections.some((section) => section.type === "diagram" || section.type === "diff");
39
+ /** Diagrams, diffs, and Markdown text can all carry highlighted code. */
40
+ const carriesCode = (sections: ReportSection[]): boolean =>
41
+ sections.some(
42
+ (section) =>
43
+ section.type === "diagram" ||
44
+ section.type === "diff" ||
45
+ (section.type === "text" && section.format === "markdown"),
46
+ );
32
47
 
33
48
  type RenderInput = ReportDocument | ReportData;
34
49
 
@@ -45,6 +60,70 @@ function valueToString(value: ReportValue): string {
45
60
  return String(value);
46
61
  }
47
62
 
63
+ const TONE_WORDS: Record<Exclude<ReportStatusTone, "neutral">, readonly string[]> = {
64
+ ok: [
65
+ "ok",
66
+ "pass",
67
+ "passed",
68
+ "passing",
69
+ "success",
70
+ "succeeded",
71
+ "done",
72
+ "complete",
73
+ "completed",
74
+ "healthy",
75
+ "ready",
76
+ "up",
77
+ "yes",
78
+ ],
79
+ warn: [
80
+ "warn",
81
+ "warning",
82
+ "stale",
83
+ "degraded",
84
+ "pending",
85
+ "partial",
86
+ "skipped",
87
+ "outdated",
88
+ "deprecated",
89
+ ],
90
+ error: [
91
+ "error",
92
+ "fail",
93
+ "failed",
94
+ "failing",
95
+ "failure",
96
+ "missing",
97
+ "broken",
98
+ "down",
99
+ "invalid",
100
+ "blocked",
101
+ "critical",
102
+ "no",
103
+ ],
104
+ info: ["info", "new", "changed", "running", "in progress", "queued", "note"],
105
+ };
106
+
107
+ /** The explicit tone when present, else the tone of a well-known status word, else neutral. */
108
+ export function inferTone(item: ReportStatus): ReportStatusTone {
109
+ if (item.tone !== undefined) return item.tone;
110
+ const word = item.status.trim().toLowerCase();
111
+ for (const [tone, words] of Object.entries(TONE_WORDS)) {
112
+ if (words.includes(word)) return tone as ReportStatusTone;
113
+ }
114
+ return "neutral";
115
+ }
116
+
117
+ /** Period, generation time, context, then metadata, as one line of parts. */
118
+ function headerParts(data: ReportDocument): string[] {
119
+ return [
120
+ ...(data.period ? [data.period] : []),
121
+ `Generated ${data.generatedAt}`,
122
+ ...(data.context ? [data.context] : []),
123
+ ...data.metadata.map((item) => `${item.label}: ${valueToString(item.value)}`),
124
+ ];
125
+ }
126
+
48
127
  function renderTable(headers: string[], rows: string[][]): string {
49
128
  if (rows.length === 0) return `| ${headers.join(" | ")} |`;
50
129
 
@@ -57,17 +136,6 @@ function renderTable(headers: string[], rows: string[][]): string {
57
136
  return [renderRow(headers), divider, ...rows.map(renderRow)].join("\n");
58
137
  }
59
138
 
60
- function renderKeyValueTable(rows: ReportKeyValue[]): string {
61
- return renderTable(
62
- ["Field", "Value"],
63
- rows.map((row) => [row.label, valueToString(row.value)]),
64
- );
65
- }
66
-
67
- function printSection(title: string, body: string): string {
68
- return `${title}\n\n${body}\n`;
69
- }
70
-
71
139
  function emptyTableMessage(title: string): string {
72
140
  const normalized = title.toLowerCase();
73
141
  return normalized === "spending" || normalized === "savings"
@@ -75,16 +143,34 @@ function emptyTableMessage(title: string): string {
75
143
  : "No data available.";
76
144
  }
77
145
 
146
+ const withDetail = (value: string, detail: string | undefined): string =>
147
+ detail ? `${value} — ${detail}` : value;
148
+
149
+ function markdownList(lines: string[]): string {
150
+ return lines.length ? lines.map((line) => `- ${line}`).join("\n") : "No data available.";
151
+ }
152
+
153
+ function markdownKeyValues(items: ReportKeyValue[]): string {
154
+ return markdownList(items.map((item) => `**${item.label}:** ${valueToString(item.value)}`));
155
+ }
156
+
157
+ function markdownMetrics(items: ReportMetric[]): string {
158
+ return markdownList(
159
+ items.map((item) => `**${item.label}:** ${withDetail(valueToString(item.value), item.detail)}`),
160
+ );
161
+ }
162
+
163
+ function withCaption(body: string, caption: string | undefined): string {
164
+ return caption ? `${body}\n\n${caption}` : body;
165
+ }
166
+
78
167
  function sectionMarkdown(section: ReportSection): string {
79
168
  switch (section.type) {
80
169
  case "metadata":
81
170
  case "key-value":
82
- return renderKeyValueTable(section.items);
171
+ return markdownKeyValues(section.items);
83
172
  case "metrics":
84
- return renderTable(
85
- ["Metric", "Value"],
86
- section.items.map((item) => [item.label, valueToString(item.value)]),
87
- );
173
+ return markdownMetrics(section.items);
88
174
  case "table":
89
175
  return section.rows.length
90
176
  ? renderTable(
@@ -93,23 +179,24 @@ function sectionMarkdown(section: ReportSection): string {
93
179
  )
94
180
  : emptyTableMessage(section.title);
95
181
  case "statuses":
96
- return section.items.length
97
- ? renderTable(
98
- ["Status", "Value"],
99
- section.items.map((item) => [
100
- item.label,
101
- item.detail ? `${item.status}: ${item.detail}` : item.status,
102
- ]),
103
- )
104
- : "No data available.";
182
+ return markdownList(
183
+ section.items.map(
184
+ (item) =>
185
+ `[${inferTone(item)}] **${item.label}:** ${withDetail(item.status, item.detail)}`,
186
+ ),
187
+ );
105
188
  case "text":
106
189
  return section.content;
107
190
  case "diagram":
108
- return section.mermaid === undefined
109
- ? fence("", section.text)
110
- : fence("mermaid", section.mermaid);
191
+ return withCaption(
192
+ section.mermaid === undefined ? fence("", section.text) : fence("mermaid", section.mermaid),
193
+ section.caption,
194
+ );
111
195
  case "diff":
112
- return section.patch.trim() ? fence("diff", section.patch) : "No data available.";
196
+ return withCaption(
197
+ section.patch.trim() ? fence("diff", section.patch) : "No data available.",
198
+ section.caption,
199
+ );
113
200
  }
114
201
  }
115
202
 
@@ -119,25 +206,12 @@ function fence(language: string, content: string): string {
119
206
 
120
207
  export function renderMarkdown(input: RenderInput): string {
121
208
  const data = normalize(input);
122
- const sections: string[] = [`# ${data.title}\n`];
123
- const metadata = [...data.metadata];
124
- if (data.period) metadata.unshift({ label: "Period", value: data.period });
125
- if (data.context) metadata.push({ label: "Context", value: data.context });
126
- sections.push(renderKeyValueTable(metadata));
127
- sections.push(
128
- "\n" +
129
- printSection(
130
- "## Summary",
131
- renderTable(
132
- ["Metric", "Value"],
133
- data.summary.map((item) => [item.label, valueToString(item.value)]),
134
- ),
135
- ),
136
- );
209
+ const parts: string[] = [`# ${data.title}`, headerParts(data).join(" · ")];
210
+ if (data.summary.length) parts.push(`## Summary\n\n${markdownMetrics(data.summary)}`);
137
211
  for (const section of data.sections) {
138
- sections.push(printSection(`## ${section.title}`, sectionMarkdown(section)));
212
+ parts.push(`## ${section.title}\n\n${sectionMarkdown(section)}`);
139
213
  }
140
- return sections.join("\n");
214
+ return `${parts.join("\n\n")}\n`;
141
215
  }
142
216
 
143
217
  export function renderJSON(input: RenderInput): string {
@@ -145,44 +219,86 @@ export function renderJSON(input: RenderInput): string {
145
219
  return JSON.stringify(normalize(input), null, 2);
146
220
  }
147
221
 
148
- function escapeHTML(value: string): string {
149
- return value.replace(/[&<>"']/g, (character) => {
150
- switch (character) {
151
- case "&":
152
- return "&amp;";
153
- case "<":
154
- return "&lt;";
155
- case ">":
156
- return "&gt;";
157
- case '"':
158
- return "&quot;";
159
- default:
160
- return "&#39;";
161
- }
162
- });
222
+ function emptyHTML(message: string): string {
223
+ return `<p class="empty">${escapeHTML(message)}</p>`;
163
224
  }
164
225
 
165
- function renderHTMLTable(headers: string[], rows: string[][]): string {
166
- const headerHTML = headers.map((header) => `<th scope="col">${escapeHTML(header)}</th>`).join("");
167
- const rowsHTML = rows.length
168
- ? rows
169
- .map((row) => `<tr>${row.map((value) => `<td>${escapeHTML(value)}</td>`).join("")}</tr>`)
170
- .join("")
171
- : "";
172
- return `<div class="table-wrap"><table><thead><tr>${headerHTML}</tr></thead><tbody>${rowsHTML}</tbody></table></div>`;
226
+ /** Every non-empty cell is a number, or a numeric-looking string such as `$12.50` or `1.2M`. */
227
+ const NUMERIC_TEXT = /^[-+]?[$€£]?\d[\d,.]*\s?[KMBT%]?(?:\/[a-z]+)?$/i;
228
+ const PLACEHOLDER_TEXT = new Set(["", "N/A", "n/a", "-", "—"]);
229
+
230
+ function isNumericColumn(rows: ReportValue[][], column: number): boolean {
231
+ const cells = rows
232
+ .map((row) => row[column])
233
+ .filter(
234
+ (cell) => cell !== null && !(typeof cell === "string" && PLACEHOLDER_TEXT.has(cell.trim())),
235
+ );
236
+ if (cells.length === 0) return false;
237
+ return (
238
+ cells.every((cell) => typeof cell === "number") ||
239
+ cells.every((cell) => typeof cell === "string" && NUMERIC_TEXT.test(cell.trim()))
240
+ );
173
241
  }
174
242
 
175
- function emptyHTML(message: string): string {
176
- return `<p class="empty">${escapeHTML(message)}</p>`;
243
+ function renderHTMLTable(columns: string[], rows: ReportValue[][]): string {
244
+ const numeric = columns.map((_, index) => isNumericColumn(rows, index));
245
+ const numClass = (index: number): string => (numeric[index] ? ' class="num"' : "");
246
+ const headerHTML = columns
247
+ .map((column, index) => `<th scope="col"${numClass(index)}>${escapeHTML(column)}</th>`)
248
+ .join("");
249
+ const rowsHTML = rows
250
+ .map(
251
+ (row) =>
252
+ `<tr>${row.map((value, index) => `<td${numClass(index)}>${escapeHTML(valueToString(value))}</td>`).join("")}</tr>`,
253
+ )
254
+ .join("");
255
+ return `<div class="table-wrap"><table><thead><tr>${headerHTML}</tr></thead><tbody>${rowsHTML}</tbody></table></div>`;
177
256
  }
178
257
 
179
258
  function renderHTMLKeyValues(items: ReportKeyValue[]): string {
180
- return items.length
181
- ? renderHTMLTable(
182
- ["Field", "Value"],
183
- items.map((item) => [item.label, valueToString(item.value)]),
184
- )
185
- : emptyHTML("No metadata available.");
259
+ if (items.length === 0) return emptyHTML("No metadata available.");
260
+ const rows = items
261
+ .map(
262
+ (item) =>
263
+ `<div><dt>${escapeHTML(item.label)}</dt><dd>${escapeHTML(valueToString(item.value))}</dd></div>`,
264
+ )
265
+ .join("");
266
+ return `<dl class="report-kv">${rows}</dl>`;
267
+ }
268
+
269
+ function renderHTMLTiles(items: ReportMetric[]): string {
270
+ const tiles = items
271
+ .map((item) => {
272
+ const detail = item.detail ? `<p class="tile-detail">${escapeHTML(item.detail)}</p>` : "";
273
+ return `<div class="tile"><p class="tile-value">${escapeHTML(valueToString(item.value))}</p><p class="tile-label">${escapeHTML(item.label)}</p>${detail}</div>`;
274
+ })
275
+ .join("");
276
+ return `<div class="report-tiles">${tiles}</div>`;
277
+ }
278
+
279
+ function renderHTMLStatuses(items: ReportStatus[]): string {
280
+ const rows = items
281
+ .map((item) => {
282
+ const detail = item.detail
283
+ ? `<span class="status-detail">${escapeHTML(item.detail)}</span>`
284
+ : "";
285
+ return `<li class="status" data-tone="${inferTone(item)}"><span class="tone-marker" aria-hidden="true"></span><span class="status-label">${escapeHTML(item.label)}</span><span class="status-value">${escapeHTML(item.status)}</span>${detail}</li>`;
286
+ })
287
+ .join("");
288
+ return `<ul class="report-statuses">${rows}</ul>`;
289
+ }
290
+
291
+ function renderHTMLText(section: ReportTextSection): string {
292
+ if (!section.content) return emptyHTML("No text available.");
293
+ return section.format === "markdown"
294
+ ? `<div class="report-text report-markdown">${renderMarkdownSubset(section.content)}</div>`
295
+ : `<p class="report-text">${escapeHTML(section.content).replaceAll("\n", "<br>")}</p>`;
296
+ }
297
+
298
+ function renderHTMLCaption(caption: string | undefined): string {
299
+ return caption
300
+ ? `<div class="report-caption report-markdown">${renderMarkdownSubset(caption)}</div>`
301
+ : "";
186
302
  }
187
303
 
188
304
  type DiffLineKind = "add" | "del" | "context" | "hunk";
@@ -302,11 +418,27 @@ function renderHTMLDiff(section: ReportDiffSection): string {
302
418
  .join("");
303
419
  }
304
420
 
421
+ /** A `classDef`, `style`, or `linkStyle` line that paints with a fixed colour. */
422
+ const FIXED_COLOUR_LINE = /^\s*(?:classDef|style|linkStyle)\s.*\b(?:fill|stroke|color)\s*:/;
423
+
424
+ /**
425
+ * The dark copy drops fixed colours: a light fill authored for a white page would
426
+ * otherwise sit under the dark scheme's light text and hide every label.
427
+ */
428
+ function darkMermaidSource(source: string): string {
429
+ return source
430
+ .split("\n")
431
+ .filter((line) => !FIXED_COLOUR_LINE.test(line))
432
+ .join("\n");
433
+ }
434
+
305
435
  function renderHTMLDiagram(section: ReportDiagramSection): string {
306
436
  if (section.mermaid === undefined) {
307
437
  return `<pre class="report-code report-diagram-text">${escapeHTML(section.text)}</pre>`;
308
438
  }
309
- return `<div class="report-diagram"><pre class="mermaid">${escapeHTML(section.mermaid)}</pre></div>`;
439
+ const dark = darkMermaidSource(section.mermaid);
440
+ const darkAttribute = dark === section.mermaid ? "" : ` data-dark-source="${escapeHTML(dark)}"`;
441
+ return `<div class="report-diagram"><pre class="mermaid"${darkAttribute}>${escapeHTML(section.mermaid)}</pre></div>`;
310
442
  }
311
443
 
312
444
  function renderHTMLSectionBody(section: ReportSection): string {
@@ -316,36 +448,22 @@ function renderHTMLSectionBody(section: ReportSection): string {
316
448
  return renderHTMLKeyValues(section.items);
317
449
  case "metrics":
318
450
  return section.items.length
319
- ? renderHTMLTable(
320
- ["Metric", "Value"],
321
- section.items.map((item) => [item.label, valueToString(item.value)]),
322
- )
451
+ ? renderHTMLTiles(section.items)
323
452
  : emptyHTML("No metrics available.");
324
453
  case "table":
325
454
  return section.rows.length
326
- ? renderHTMLTable(
327
- section.columns,
328
- section.rows.map((row) => row.map(valueToString)),
329
- )
455
+ ? renderHTMLTable(section.columns, section.rows)
330
456
  : emptyHTML(emptyTableMessage(section.title));
331
457
  case "statuses":
332
458
  return section.items.length
333
- ? renderHTMLTable(
334
- ["Status", "Value"],
335
- section.items.map((item) => [
336
- item.label,
337
- item.detail ? `${item.status}: ${item.detail}` : item.status,
338
- ]),
339
- )
459
+ ? renderHTMLStatuses(section.items)
340
460
  : emptyHTML("No status data available.");
341
461
  case "text":
342
- return section.content
343
- ? `<p class="report-text">${escapeHTML(section.content).replaceAll("\n", "<br>")}</p>`
344
- : emptyHTML("No text available.");
462
+ return renderHTMLText(section);
345
463
  case "diagram":
346
- return renderHTMLDiagram(section);
464
+ return renderHTMLDiagram(section) + renderHTMLCaption(section.caption);
347
465
  case "diff":
348
- return renderHTMLDiff(section);
466
+ return renderHTMLDiff(section) + renderHTMLCaption(section.caption);
349
467
  }
350
468
  }
351
469
 
@@ -353,28 +471,136 @@ function renderHTMLSection(section: ReportSection): string {
353
471
  return `<section id="${escapeHTML(section.id)}"><h2>${escapeHTML(section.title)}</h2>${renderHTMLSectionBody(section)}</section>`;
354
472
  }
355
473
 
474
+ function renderHTMLIndex(sections: ReportSection[]): string {
475
+ if (sections.length <= INDEX_THRESHOLD) return "";
476
+ const links = sections
477
+ .map(
478
+ (section) => `<li><a href="#${escapeHTML(section.id)}">${escapeHTML(section.title)}</a></li>`,
479
+ )
480
+ .join("");
481
+ return `\n <nav class="report-index interactive-only" aria-label="Sections"><ol>${links}</ol></nav>`;
482
+ }
483
+
484
+ /** The kit's symbol: the Ink bar follows the text colour, the three blocks the accent. */
485
+ const MATE_SYMBOL = `<svg class="report-symbol" viewBox="0 0 256 256" width="28" height="28" aria-hidden="true" focusable="false"><path fill="currentColor" d="M36 16H144V240H36A20 20 0 0 1 16 220V36A20 20 0 0 1 36 16Z"/><path class="report-symbol-accent" d="M160 16H220A20 20 0 0 1 240 36V80H160Z M160 96H240V160H160Z M160 176H240V220A20 20 0 0 1 220 240H160Z"/></svg>`;
486
+
487
+ const PRINT_ICON = `<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9V3h12v6"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><path d="M6 14h12v7H6z"/></svg>`;
488
+
489
+ const PRINT_LABEL = "Print / Save as PDF";
490
+
491
+ /** Mermaid theme variables for one report palette; notes stay neutral, not mermaid yellow. */
492
+ function mermaidThemeVariables(palette: ReportPalette, darkMode: boolean): Record<string, unknown> {
493
+ return {
494
+ darkMode,
495
+ background: palette.bg,
496
+ primaryColor: palette.surface,
497
+ primaryTextColor: palette.ink,
498
+ primaryBorderColor: palette.accent,
499
+ secondaryColor: palette.hunkBg,
500
+ tertiaryColor: palette.bg,
501
+ lineColor: palette.muted,
502
+ textColor: palette.ink,
503
+ noteBkgColor: palette.hunkBg,
504
+ noteBorderColor: palette.rule,
505
+ noteTextColor: palette.ink,
506
+ fontFamily: "system-ui, -apple-system, Segoe UI, sans-serif",
507
+ fontSize: "14px",
508
+ };
509
+ }
510
+
511
+ const NODE_SHAPES = ["rect", "polygon", "circle", "ellipse", "path"];
512
+
513
+ /** Node classes an author can apply with `class` or `:::` instead of fixing a colour. */
514
+ function mermaidClassCss(palette: ReportPalette): string {
515
+ const classes: [name: string, fill: string, stroke: string][] = [
516
+ ["added", palette.addBg, palette.ok],
517
+ ["changed", palette.hunkBg, palette.info],
518
+ ["removed", palette.delBg, palette.error],
519
+ ];
520
+ return classes
521
+ .map(([name, fill, stroke]) => {
522
+ const selector = NODE_SHAPES.map((shape) => `.node.${name} ${shape}`).join(", ");
523
+ return `${selector} { fill: ${fill} !important; stroke: ${stroke} !important; }`;
524
+ })
525
+ .join(" ");
526
+ }
527
+
528
+ /**
529
+ * Draws every diagram twice, in the light and the dark palette, one diagram at a time.
530
+ * CSS then shows the copy for the active scheme and always the light copy in print, so
531
+ * neither a scheme switch nor printing has to wait for a redraw. A diagram that fails
532
+ * to render keeps showing its escaped source.
533
+ */
534
+ const MERMAID_INIT = `(() => {
535
+ const mermaid = window.mermaid;
536
+ if (!mermaid) return;
537
+ const variants = ${JSON.stringify([
538
+ {
539
+ scheme: "light",
540
+ themeVariables: mermaidThemeVariables(LIGHT_PALETTE, false),
541
+ themeCSS: mermaidClassCss(LIGHT_PALETTE),
542
+ },
543
+ {
544
+ scheme: "dark",
545
+ themeVariables: mermaidThemeVariables(DARK_PALETTE, true),
546
+ themeCSS: mermaidClassCss(DARK_PALETTE),
547
+ },
548
+ ])};
549
+ const draw = async (sources, id) => {
550
+ const drawn = [];
551
+ for (const { scheme, themeVariables, themeCSS } of variants) {
552
+ mermaid.initialize({ startOnLoad: false, securityLevel: "strict", theme: "base", themeVariables, themeCSS });
553
+ const { svg } = await mermaid.render(id + "-" + scheme, sources[scheme]);
554
+ const figure = document.createElement("div");
555
+ figure.className = "diagram-" + scheme;
556
+ figure.innerHTML = svg;
557
+ drawn.push(figure);
558
+ }
559
+ return drawn;
560
+ };
561
+ (async () => {
562
+ const nodes = [...document.querySelectorAll(".report-diagram pre.mermaid")];
563
+ for (const [index, node] of nodes.entries()) {
564
+ const id = "report-diagram-" + index;
565
+ try {
566
+ const sources = { light: node.textContent, dark: node.dataset.darkSource ?? node.textContent };
567
+ node.after(...(await draw(sources, id)));
568
+ node.hidden = true;
569
+ } catch {
570
+ for (const scheme of ["light", "dark"]) document.getElementById("d" + id + "-" + scheme)?.remove();
571
+ }
572
+ }
573
+ })();
574
+ })();`;
575
+
576
+ /**
577
+ * Runs before the styles apply: `?scheme=light` or `?scheme=dark` pins the colour scheme
578
+ * through `data-scheme` on `<html>`; any other value leaves the browser preference in charge.
579
+ */
580
+ const SCHEME_SCRIPT = `(function () {
581
+ var scheme = new URLSearchParams(location.search).get("scheme");
582
+ if (scheme === "light" || scheme === "dark") document.documentElement.dataset.scheme = scheme;
583
+ })();`;
584
+
356
585
  export function renderHTML(input: RenderInput): string {
357
586
  const data = normalize(input);
358
587
  const legacy = isReportDocument(input) ? undefined : input;
359
- const metadata = [...data.metadata];
360
- if (data.period) metadata.unshift({ label: "Period", value: data.period });
361
- if (data.context) metadata.push({ label: "Context", value: data.context });
362
- const summary: ReportMetricsSection = {
363
- id: "summary",
364
- title: "Summary",
365
- type: "metrics",
366
- items: data.summary,
367
- };
368
588
 
369
589
  const repositoryAttributes = legacy
370
590
  ? ` data-working-repository="${escapeHTML(legacy.workingRepoPath)}" data-companion-repository="${escapeHTML(legacy.companionRepoPath)}"`
371
591
  : "";
372
592
 
373
- const visualStyles = carriesVisual(data.sections)
593
+ const meta = headerParts(data)
594
+ .map((part) => `<span>${escapeHTML(part)}</span>`)
595
+ .join("");
596
+ const summary = data.summary.length
597
+ ? `\n <section id="summary" class="report-summary" aria-label="Summary">${renderHTMLTiles(data.summary)}</section>`
598
+ : "";
599
+ const codeStyles = carriesCode(data.sections)
374
600
  ? `\n <style>${highlightStyleBlock(CODE_BLOCK_SELECTOR)}${VISUAL_SECTION_CSS}</style>`
375
601
  : "";
376
602
  const diagramRuntime = carriesMermaid(data.sections)
377
- ? `\n <script id="${MERMAID_RUNTIME_ID}" src="${MERMAID_RUNTIME_URL}" integrity="${MERMAID_RUNTIME_INTEGRITY}" crossorigin="anonymous"></script>\n <script>window.mermaid?.initialize({ startOnLoad: true, securityLevel: "strict" });</script>`
603
+ ? `\n <script id="${MERMAID_RUNTIME_ID}" src="${MERMAID_RUNTIME_URL}" integrity="${MERMAID_RUNTIME_INTEGRITY}" crossorigin="anonymous"></script>\n <script>${MERMAID_INIT}</script>`
378
604
  : "";
379
605
 
380
606
  return `<!doctype html>
@@ -382,67 +608,144 @@ export function renderHTML(input: RenderInput): string {
382
608
  <head>
383
609
  <meta charset="utf-8">
384
610
  <meta name="viewport" content="width=device-width, initial-scale=1">
611
+ <meta name="color-scheme" content="light dark">
612
+ <script id="report-scheme">${SCHEME_SCRIPT}</script>
385
613
  <title>${escapeHTML(data.title)}</title>
386
614
  <style>
387
- :root { color-scheme: light; font-family: system-ui, sans-serif; background: #f4f6f8; color: #18212b; }
388
- body { margin: 0; }
389
- .report { max-width: 1120px; margin: 0 auto; padding: 32px 20px 56px; }
390
- header, section { background: #fff; border: 1px solid #dce2e8; border-radius: 14px; margin-bottom: 20px; padding: 24px; box-shadow: 0 8px 24px rgb(24 33 43 / 6%); }
391
- h1, h2 { margin-top: 0; }
392
- h1 { margin-bottom: 8px; }
393
- h2 { font-size: 1.15rem; }
394
- .eyebrow { color: #536273; font-size: .8rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
395
- .print-control { background: #18212b; border: 0; border-radius: 8px; color: #fff; cursor: pointer; padding: 10px 14px; }
396
- .table-wrap { overflow-x: auto; }
397
- table { border-collapse: collapse; min-width: 100%; }
398
- th, td { border-bottom: 1px solid #e8edf1; padding: 10px 12px; text-align: left; vertical-align: top; }
399
- th { color: #536273; font-size: .8rem; text-transform: uppercase; }
400
- .empty { color: #536273; margin: 0; }
401
- @media (max-width: 640px) { .report { padding: 16px 12px 32px; } header, section { padding: 16px; } }
402
- @media print {
403
- :root, body { background: #fff; color: #000; }
404
- .report { max-width: none; padding: 0; }
405
- header, section { border: 0; border-radius: 0; box-shadow: none; margin-bottom: 16px; padding: 0; }
406
- .print-control, .interactive-only { display: none !important; }
407
- .table-wrap { overflow: visible; }
408
- table { width: 100%; }
409
- th, td { color: #000; }
410
- }
411
- </style>${visualStyles}
615
+ ${paletteCss()}
616
+ ${REPORT_CSS}
617
+ </style>${codeStyles}
412
618
  </head>
413
619
  <body>
414
620
  <main class="report" data-report-version="${data.version}"${repositoryAttributes}>
415
- <header>
416
- <p class="eyebrow">Mate report</p>
417
- <h1>${escapeHTML(data.title)}</h1>
418
- ${renderHTMLKeyValues(metadata)}
419
- <button class="print-control interactive-only" type="button" onclick="window.print()">Print / Save as PDF</button>
420
- </header>
421
- ${renderHTMLSection(summary)}
621
+ <header class="report-header">
622
+ <div class="report-heading">
623
+ ${MATE_SYMBOL}
624
+ <h1>${escapeHTML(data.title)}</h1>
625
+ <button class="print-control interactive-only" type="button" aria-label="${PRINT_LABEL}" title="${PRINT_LABEL}" onclick="window.print()">${PRINT_ICON}</button>
626
+ </div>
627
+ <p class="report-meta">${meta}</p>
628
+ </header>${summary}${renderHTMLIndex(data.sections)}
422
629
  ${data.sections.map(renderHTMLSection).join("\n ")}
423
630
  </main>${diagramRuntime}
424
631
  </body>
425
632
  </html>`;
426
633
  }
427
634
 
635
+ const MONO = "ui-monospace, SFMono-Regular, Menlo, monospace";
636
+
637
+ const REPORT_CSS = `
638
+ html { background: var(--bg); color: var(--ink); font-family: system-ui, -apple-system, "Segoe UI", sans-serif; line-height: 1.5; }
639
+ body { margin: 0; }
640
+ a { color: var(--accent); }
641
+ .report { max-width: 1040px; margin: 0 auto; padding: 40px 24px 64px; }
642
+ .report-heading { align-items: center; display: flex; gap: 12px; }
643
+ .report-symbol { color: var(--ink); flex: none; }
644
+ .report-symbol-accent { fill: var(--accent); }
645
+ h1 { flex: 1; font-size: 1.6rem; font-weight: 650; letter-spacing: -.01em; line-height: 1.2; margin: 0; }
646
+ .report-meta { color: var(--muted); font-size: .85rem; margin: 8px 0 0 40px; }
647
+ .report-meta span + span::before { content: "·"; padding: 0 .5em; }
648
+ .print-control { align-items: center; background: transparent; border: 0; border-radius: 8px; color: var(--muted); cursor: pointer; display: inline-flex; flex: none; height: 36px; justify-content: center; padding: 0; width: 36px; }
649
+ .print-control:hover, .print-control:focus-visible { color: var(--accent); }
650
+ .print-control:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
651
+ section { margin-top: 40px; }
652
+ h2 { border-bottom: 1px solid var(--rule); color: var(--muted); font-size: .75rem; font-variant: small-caps; font-weight: 650; letter-spacing: .08em; margin: 0 0 16px; padding-bottom: 8px; text-transform: uppercase; }
653
+ .report-tiles { display: grid; gap: 24px; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
654
+ .tile { border-top: 1px solid var(--rule); padding-top: 12px; }
655
+ .tile p { margin: 0; }
656
+ .tile-value { font-size: 1.6rem; font-variant-numeric: tabular-nums; font-weight: 650; line-height: 1.2; }
657
+ .tile-label { color: var(--muted); font-size: .85rem; margin-top: 4px !important; }
658
+ .tile-detail { color: var(--muted); font-size: .8rem; }
659
+ .report-index ol { display: flex; flex-wrap: wrap; gap: 4px 20px; list-style: none; margin: 32px 0 0; padding: 0; }
660
+ .report-index a { font-size: .85rem; text-decoration: none; }
661
+ .report-index a:hover { text-decoration: underline; }
662
+ .report-kv { display: grid; gap: 8px 24px; grid-template-columns: minmax(140px, max-content) 1fr; margin: 0; }
663
+ .report-kv div { display: contents; }
664
+ .report-kv dt { color: var(--muted); }
665
+ .report-kv dd { margin: 0; overflow-wrap: anywhere; }
666
+ .report-statuses { list-style: none; margin: 0; padding: 0; }
667
+ .status { align-items: baseline; border-bottom: 1px solid var(--rule); display: flex; flex-wrap: wrap; gap: 4px 12px; padding: 8px 0; }
668
+ .status:last-child { border-bottom: 0; }
669
+ .status-label { font-weight: 600; }
670
+ .status-value { color: var(--tone); margin-left: auto; text-align: right; }
671
+ .status-detail { color: var(--muted); flex-basis: 100%; font-size: .85rem; padding-left: 22px; }
672
+ .tone-marker { align-self: center; background: var(--tone); display: inline-block; flex: none; height: 10px; width: 10px; }
673
+ .status[data-tone="ok"] { --tone: var(--ok); }
674
+ .status[data-tone="ok"] .tone-marker { border-radius: 50%; }
675
+ .status[data-tone="neutral"] { --tone: var(--neutral); }
676
+ .status[data-tone="neutral"] .tone-marker { background: transparent; border: 2px solid var(--tone); border-radius: 50%; box-sizing: border-box; }
677
+ .status[data-tone="warn"] { --tone: var(--warn); }
678
+ .status[data-tone="warn"] .tone-marker { clip-path: polygon(50% 0, 100% 100%, 0 100%); }
679
+ .status[data-tone="error"] { --tone: var(--error); }
680
+ .status[data-tone="info"] { --tone: var(--info); }
681
+ .status[data-tone="info"] .tone-marker { transform: rotate(45deg) scale(.85); }
682
+ .table-wrap { overflow-x: auto; }
683
+ table { border-collapse: collapse; min-width: 100%; }
684
+ th, td { border-bottom: 1px solid var(--rule); padding: 8px 12px; text-align: left; vertical-align: top; }
685
+ th:first-child, td:first-child { padding-left: 0; }
686
+ th:last-child, td:last-child { padding-right: 0; }
687
+ th { color: var(--muted); font-size: .75rem; font-weight: 650; letter-spacing: .04em; text-transform: uppercase; }
688
+ tbody tr:last-child td { border-bottom: 0; }
689
+ .num { font-variant-numeric: tabular-nums; text-align: right; }
690
+ .report-text { margin: 0; max-width: 72ch; }
691
+ .report-markdown > :first-child { margin-top: 0; }
692
+ .report-markdown > :last-child { margin-bottom: 0; }
693
+ .report-markdown h3, .report-markdown h4, .report-markdown h5, .report-markdown h6 { font-size: 1rem; margin: 20px 0 8px; }
694
+ .report-markdown code { background: var(--surface); border-radius: 4px; font-family: ${MONO}; font-size: .88em; padding: .1em .3em; }
695
+ .report-markdown pre code { background: none; padding: 0; }
696
+ .report-caption { color: var(--muted); font-size: .9rem; margin-top: 12px; max-width: 72ch; }
697
+ .empty { color: var(--muted); margin: 0; }
698
+ @media (max-width: 640px) {
699
+ .report { padding: 24px 16px 40px; }
700
+ .report-meta { margin-left: 0; }
701
+ .report-kv { grid-template-columns: 1fr; gap: 2px; }
702
+ .report-kv dd { margin-bottom: 8px; }
703
+ }
704
+ @media print {
705
+ .report { max-width: none; padding: 0; }
706
+ section { margin-top: 24px; }
707
+ .report-header, h2, .diff-file { break-after: avoid-page; }
708
+ .report-markdown :is(h3, h4, h5, h6) { break-after: avoid-page; }
709
+ .report-markdown :is(ul, ol) { break-inside: avoid-page; }
710
+ .report-caption { break-before: avoid-page; }
711
+ tr, .tile, .status, .report-kv dt, .report-kv dd, .report-markdown li, .report-diagram { break-inside: avoid-page; }
712
+ .report-kv dt { break-after: avoid-page; }
713
+ thead { display: table-header-group; }
714
+ p, li { orphans: 3; widows: 3; }
715
+ .print-control, .interactive-only, .report-index { display: none !important; }
716
+ .table-wrap { overflow: visible; }
717
+ table { width: 100%; }
718
+ .tone-marker { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
719
+ }`;
720
+
428
721
  const VISUAL_SECTION_CSS = `
429
- pre.report-code { background: #f7f9fb; border: 1px solid #e8edf1; border-radius: 10px; margin: 0; overflow-x: auto; padding: 14px 16px; }
722
+ pre.report-code { background: var(--surface); border: 1px solid var(--rule); border-radius: 8px; font-family: ${MONO}; font-size: .82rem; margin: 0; overflow-x: auto; padding: 14px 16px; }
723
+ .report-markdown pre.report-code { margin: 12px 0; }
430
724
  .report-diagram { max-width: 100%; overflow-x: auto; }
431
725
  .report-diagram svg { height: auto; max-width: 100%; }
432
- .report-diagram .mermaid { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .82rem; margin: 0; white-space: pre; }
726
+ .report-diagram svg * { filter: none !important; }
727
+ .report-diagram .diagram-dark { display: none; }
728
+ @media screen and (prefers-color-scheme: dark) {
729
+ :root:not([data-scheme="light"]) .report-diagram .diagram-light { display: none; }
730
+ :root:not([data-scheme="light"]) .report-diagram .diagram-dark { display: block; }
731
+ }
732
+ :root[data-scheme="dark"] .report-diagram .diagram-light { display: none; }
733
+ :root[data-scheme="dark"] .report-diagram .diagram-dark { display: block; }
734
+ .report-diagram .mermaid { font-family: ${MONO}; font-size: .82rem; margin: 0; white-space: pre; }
433
735
  .diff-file-group + .diff-file-group { margin-top: 18px; }
434
- .diff-file { color: #536273; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .8rem; font-weight: 700; margin: 0 0 8px; }
435
- .report-diff { border: 1px solid #e8edf1; border-radius: 10px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .82rem; overflow-x: auto; padding: 6px 0; }
736
+ .diff-file { color: var(--muted); font-family: ${MONO}; font-size: .8rem; font-weight: 650; margin: 0 0 8px; }
737
+ .report-diff { border: 1px solid var(--rule); border-radius: 8px; font-family: ${MONO}; font-size: .82rem; overflow-x: auto; padding: 6px 0; }
436
738
  .diff-line { -webkit-print-color-adjust: exact; min-width: max-content; padding: 0 12px; print-color-adjust: exact; white-space: pre; }
437
739
  .diff-glyph { display: inline-block; user-select: none; width: 1ch; }
438
- .diff-add { background: #e6ffec; }
439
- .diff-del { background: #ffebe9; }
440
- .diff-hunk { background: #eef2f6; color: #536273; }
740
+ .diff-add { background: var(--add-bg); }
741
+ .diff-del { background: var(--del-bg); }
742
+ .diff-hunk { background: var(--hunk-bg); color: var(--muted); }
441
743
  @media print {
442
744
  pre.report-code, .report-diff { border-color: #999; }
745
+ .report-diagram { background: #fff; }
746
+ .report-diagram svg { max-height: 90vh; }
747
+ .report-diagram .diagram-light { display: block !important; }
748
+ .report-diagram .diagram-dark { display: none !important; }
443
749
  .report-diagram, .report-diff, pre.report-code { overflow: visible; }
444
- .diff-line { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
445
750
  .diff-hunk { color: #333; }
446
751
  }`;
447
-
448
- type ReportMetricsSection = Extract<ReportSection, { type: "metrics" }>;