workflow-toolkit 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/.agents/skills/ponytail/SKILL.md +111 -0
  2. package/.agents/skills/ponytail-audit/SKILL.md +37 -0
  3. package/.agents/skills/ponytail-debt/SKILL.md +41 -0
  4. package/.agents/skills/ponytail-gain/SKILL.md +47 -0
  5. package/.agents/skills/ponytail-help/SKILL.md +70 -0
  6. package/.agents/skills/ponytail-review/SKILL.md +52 -0
  7. package/.agents/skills/prompt-review/SKILL.md +60 -0
  8. package/.agents/skills/wtk/SKILL.md +64 -0
  9. package/.agents/skills/wtk/references/artifacts.md +82 -0
  10. package/.agents/skills/wtk/references/evidence.md +68 -0
  11. package/.agents/skills/wtk/references/git.md +75 -0
  12. package/.agents/skills/wtk/references/test-contract.md +76 -0
  13. package/.agents/skills/wtk/references/validation.md +127 -0
  14. package/.agents/skills/wtk-config/SKILL.md +102 -0
  15. package/.agents/skills/wtk-config/assets/agents/claude/deep-reviewer.md +40 -0
  16. package/.agents/skills/wtk-config/assets/agents/claude/designer.md +57 -0
  17. package/.agents/skills/wtk-config/assets/agents/claude/explorer.md +41 -0
  18. package/.agents/skills/wtk-config/assets/agents/claude/implementer.md +57 -0
  19. package/.agents/skills/wtk-config/assets/agents/claude/planner.md +68 -0
  20. package/.agents/skills/wtk-config/assets/agents/claude/verifier.md +80 -0
  21. package/.agents/skills/wtk-config/assets/agents/codex/deep-reviewer.toml +41 -0
  22. package/.agents/skills/wtk-config/assets/agents/codex/designer.toml +55 -0
  23. package/.agents/skills/wtk-config/assets/agents/codex/explorer.toml +35 -0
  24. package/.agents/skills/wtk-config/assets/agents/codex/implementer.toml +52 -0
  25. package/.agents/skills/wtk-config/assets/agents/codex/planner.toml +66 -0
  26. package/.agents/skills/wtk-config/assets/agents/codex/verifier.toml +78 -0
  27. package/.agents/skills/wtk-config/assets/agents/cursor/deep-reviewer.md +38 -0
  28. package/.agents/skills/wtk-config/assets/agents/cursor/designer.md +55 -0
  29. package/.agents/skills/wtk-config/assets/agents/cursor/explorer.md +39 -0
  30. package/.agents/skills/wtk-config/assets/agents/cursor/implementer.md +55 -0
  31. package/.agents/skills/wtk-config/assets/agents/cursor/planner.md +66 -0
  32. package/.agents/skills/wtk-config/assets/agents/cursor/verifier.md +79 -0
  33. package/.agents/skills/wtk-config/scripts/ad-index.py +123 -0
  34. package/.agents/skills/wtk-config/scripts/repository_intelligence.py +671 -0
  35. package/.agents/skills/wtk-config/scripts/workflow_config.py +972 -0
  36. package/.agents/skills/wtk-deep-review/SKILL.md +164 -0
  37. package/.agents/skills/wtk-deep-review/assets/PROMPT.md +56 -0
  38. package/.agents/skills/wtk-deep-review/assets/REVIEW_UI.html +1340 -0
  39. package/.agents/skills/wtk-deep-review/assets/findings.schema.json +113 -0
  40. package/.agents/skills/wtk-deep-review/references/context-pack.md +88 -0
  41. package/.agents/skills/wtk-deep-review/references/orchestration.md +164 -0
  42. package/.agents/skills/wtk-deep-review/references/output-contracts.md +149 -0
  43. package/.agents/skills/wtk-deep-review/references/publish-github.md +83 -0
  44. package/.agents/skills/wtk-deep-review/references/state-and-learnings.md +64 -0
  45. package/.agents/skills/wtk-deep-review/references/subagent-runtimes.md +33 -0
  46. package/.agents/skills/wtk-deep-review/references/taxonomy.md +70 -0
  47. package/.agents/skills/wtk-deep-review/scripts/_common.py +371 -0
  48. package/.agents/skills/wtk-deep-review/scripts/build_jobs.py +561 -0
  49. package/.agents/skills/wtk-deep-review/scripts/build_knowledge.py +349 -0
  50. package/.agents/skills/wtk-deep-review/scripts/build_manifest.py +479 -0
  51. package/.agents/skills/wtk-deep-review/scripts/graft_context.py +160 -0
  52. package/.agents/skills/wtk-deep-review/scripts/graphify_context.py +75 -0
  53. package/.agents/skills/wtk-deep-review/scripts/merge_findings.py +313 -0
  54. package/.agents/skills/wtk-deep-review/scripts/render_html.py +240 -0
  55. package/.agents/skills/wtk-deep-review/scripts/render_review.py +282 -0
  56. package/.agents/skills/wtk-deep-review/scripts/run_jobs.py +436 -0
  57. package/.agents/skills/wtk-deep-review/scripts/token_metrics.py +369 -0
  58. package/.agents/skills/wtk-discover/SKILL.md +270 -0
  59. package/.agents/skills/wtk-discover/references/document-format.md +149 -0
  60. package/.agents/skills/wtk-implement/SKILL.md +95 -0
  61. package/.agents/skills/wtk-implement/references/checklist-format.md +116 -0
  62. package/.agents/skills/wtk-implement/references/screens.md +129 -0
  63. package/.agents/skills/wtk-implement/references/test-policy.md +152 -0
  64. package/.agents/skills/wtk-implement/references/verify.md +297 -0
  65. package/.agents/skills/wtk-knowledge-check/SKILL.md +16 -0
  66. package/.agents/skills/wtk-knowledge-check/scripts/check.ts +502 -0
  67. package/.agents/skills/wtk-knowledge-check/scripts/cli.ts +32 -0
  68. package/.agents/skills/wtk-knowledge-check/scripts/frontmatter.ts +44 -0
  69. package/.agents/skills/wtk-lean/.skill-meta.json +6 -0
  70. package/.agents/skills/wtk-lean/NOTICE.md +18 -0
  71. package/.agents/skills/wtk-lean/SKILL.md +137 -0
  72. package/.agents/skills/wtk-lean/references/build.md +168 -0
  73. package/.agents/skills/wtk-lean/references/checks.md +253 -0
  74. package/.agents/skills/wtk-lean/references/memory.md +156 -0
  75. package/.agents/skills/wtk-lean/references/plan.md +514 -0
  76. package/.agents/skills/wtk-lean/references/verify.md +336 -0
  77. package/.agents/skills/wtk-lean/scripts/check_commit.py +121 -0
  78. package/.agents/skills/wtk-lean/scripts/fixtures/checks.md +98 -0
  79. package/.agents/skills/wtk-lean/scripts/fixtures/plan.md +119 -0
  80. package/.agents/skills/wtk-lean/scripts/fixtures/verification.md +41 -0
  81. package/.agents/skills/wtk-lean/scripts/lessons.py +412 -0
  82. package/.agents/skills/wtk-lean/scripts/selftest.py +401 -0
  83. package/.agents/skills/wtk-lean/scripts/validate_checks.py +417 -0
  84. package/.agents/skills/wtk-lean/scripts/validate_plan.py +580 -0
  85. package/.agents/skills/wtk-lean/scripts/validate_verification.py +357 -0
  86. package/.agents/skills/wtk-plan/SKILL.md +102 -0
  87. package/.agents/skills/wtk-plan/references/document-format.md +222 -0
  88. package/.agents/skills/wtk-qa/SKILL.md +14 -0
  89. package/.agents/skills/wtk-qa-execute/SKILL.md +115 -0
  90. package/.agents/skills/wtk-qa-execute/references/fix-loop.md +23 -0
  91. package/.agents/skills/wtk-qa-execute/references/session-protocol.md +25 -0
  92. package/.agents/skills/wtk-qa-plan/SKILL.md +111 -0
  93. package/.agents/skills/wtk-qa-plan/references/profile.md +35 -0
  94. package/.agents/skills/wtk-ship/SKILL.md +49 -0
  95. package/.agents/skills/wtk-ship/remediation.py +170 -0
  96. package/.agents/skills/wtk-ship/scripts/close_feature.py +83 -0
  97. package/.agents/skills/wtk-ship/scripts/review_convergence.py +373 -0
  98. package/.wtk.toml.example +93 -0
  99. package/AGENTS.md +129 -0
  100. package/NOTICE.md +12 -0
  101. package/README.md +440 -0
  102. package/bin/wtk.js +25 -0
  103. package/docs/toolkit/README.md +56 -0
  104. package/docs/toolkit/decisions.md +61 -0
  105. package/docs/toolkit/guidelines/CONTEXT-BUDGET.md +64 -0
  106. package/docs/toolkit/guidelines/DX.md +67 -0
  107. package/docs/toolkit/guidelines/FRONTEND.md +73 -0
  108. package/docs/toolkit/guidelines/KNOWLEDGE-WIKI.md +128 -0
  109. package/docs/toolkit/guidelines/MODELING.md +134 -0
  110. package/docs/toolkit/guidelines/QA-EXECUTION.md +46 -0
  111. package/docs/toolkit/guidelines/QA-SCENARIOS.md +165 -0
  112. package/docs/toolkit/guidelines/REVIEW-ROUNDS.md +160 -0
  113. package/docs/toolkit/guidelines/SECURITY.md +160 -0
  114. package/docs/toolkit/guidelines/UI-UX.md +117 -0
  115. package/docs/toolkit/guidelines/WORKFLOW-MEMORY.md +73 -0
  116. package/docs/toolkit/guidelines.md +50 -0
  117. package/docs/toolkit/loop.md +77 -0
  118. package/docs/toolkit/purpose.md +51 -0
  119. package/docs/toolkit/repository-intelligence.md +53 -0
  120. package/docs/toolkit/reviews.md +77 -0
  121. package/knowledge/AGENTS.md +316 -0
  122. package/knowledge/raw/README.md +17 -0
  123. package/package.json +74 -0
  124. package/scripts/install_security_skills.py +816 -0
  125. package/scripts/installer/engine.js +254 -0
  126. package/scripts/installer/knowledge.js +28 -0
  127. package/scripts/installer/packets.js +77 -0
  128. package/scripts/installer/terminal.js +98 -0
  129. package/scripts/installer/transaction.js +75 -0
  130. package/skills-lock.json +92 -0
  131. package/templates/adoption/agents/core.md +23 -0
  132. package/templates/adoption/agents/quality.md +3 -0
  133. package/templates/adoption/knowledge/wiki/architecture/index.md +3 -0
  134. package/templates/adoption/knowledge/wiki/decisions/index.md +3 -0
  135. package/templates/adoption/knowledge/wiki/design/index.md +3 -0
  136. package/templates/adoption/knowledge/wiki/domain/index.md +3 -0
  137. package/templates/adoption/knowledge/wiki/index.md +3 -0
  138. package/templates/adoption/knowledge/wiki/log.md +3 -0
  139. package/templates/adoption/knowledge/wiki/open-questions/index.md +3 -0
  140. package/templates/adoption/knowledge/wiki/product/index.md +3 -0
  141. package/templates/adoption/knowledge/wiki/research/index.md +3 -0
  142. package/templates/adoption/product/AGENT-CONTEXT.md +25 -0
@@ -0,0 +1,1340 @@
1
+ <!doctype html>
2
+ <!-- wtk-deep-review fixed report UI. Hydrated by scripts/render_html.py, which
3
+ swaps the data placeholder inside the application/json script tag (that
4
+ token must appear exactly once, in that tag only). Edit the UI here,
5
+ nowhere else — models never author report HTML. Self-contained: no
6
+ network, works via file://. -->
7
+ <html lang="en">
8
+ <head>
9
+ <meta charset="utf-8">
10
+ <meta name="viewport" content="width=device-width, initial-scale=1">
11
+ <meta name="color-scheme" content="dark light">
12
+ <title>Deep Review</title>
13
+ <style>
14
+ :root {
15
+ color-scheme: dark;
16
+ --bg: oklch(0.155 0 0);
17
+ --surface: oklch(0.205 0 0);
18
+ --surface-2: oklch(0.245 0 0);
19
+ --ink: oklch(0.965 0 0);
20
+ --ink-2: oklch(0.78 0 0);
21
+ --ink-3: oklch(0.63 0 0);
22
+ --line: oklch(0.29 0 0);
23
+ --line-strong: oklch(0.37 0 0);
24
+ --accent: oklch(0.68 0.14 46);
25
+ --accent-text: oklch(0.74 0.13 48);
26
+ --accent-tint: oklch(0.68 0.14 46 / 0.12);
27
+ --sev-critical: #d03b3b;
28
+ --sev-major: #ec835a;
29
+ --sev-minor: #fab219;
30
+ --sev-trivial: oklch(0.64 0.04 250);
31
+ --sev-critical-text: #e66767;
32
+ --sev-major-text: #ee9268;
33
+ --sev-minor-text: #e7ad34;
34
+ --sev-trivial-text: oklch(0.72 0.04 250);
35
+ --good: #0ca30c;
36
+ --good-text: #2fbf2f;
37
+ --dot-ring: rgb(255 255 255 / 0.14);
38
+ --code-bg: oklch(0.185 0 0);
39
+ --focus: oklch(0.74 0.13 48);
40
+ --z-sticky: 10;
41
+ --z-toast: 30;
42
+ }
43
+ [data-theme="light"] {
44
+ color-scheme: light;
45
+ --bg: oklch(1 0 0);
46
+ --surface: oklch(0.972 0 0);
47
+ --surface-2: oklch(0.947 0 0);
48
+ --ink: oklch(0.165 0 0);
49
+ --ink-2: oklch(0.42 0 0);
50
+ --ink-3: oklch(0.53 0 0);
51
+ --line: oklch(0.905 0 0);
52
+ --line-strong: oklch(0.83 0 0);
53
+ --accent: oklch(0.568 0.149 46);
54
+ --accent-text: oklch(0.49 0.14 46);
55
+ --accent-tint: oklch(0.568 0.149 46 / 0.10);
56
+ --sev-critical-text: #b32a2a;
57
+ --sev-major-text: #a84f24;
58
+ --sev-minor-text: #7d5a00;
59
+ --sev-trivial-text: oklch(0.46 0.035 250);
60
+ --good-text: #006300;
61
+ --dot-ring: rgb(0 0 0 / 0.14);
62
+ --code-bg: oklch(0.965 0 0);
63
+ --focus: oklch(0.49 0.14 46);
64
+ }
65
+
66
+ * { box-sizing: border-box; }
67
+ html { scrollbar-gutter: stable; }
68
+ body {
69
+ margin: 0;
70
+ background: var(--bg);
71
+ color: var(--ink);
72
+ font: 400 14px/1.55 system-ui, -apple-system, "Segoe UI", sans-serif;
73
+ -webkit-font-smoothing: antialiased;
74
+ text-rendering: optimizeLegibility;
75
+ }
76
+ code, pre, .mono {
77
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
78
+ }
79
+ button {
80
+ font: inherit;
81
+ color: inherit;
82
+ background: none;
83
+ border: 0;
84
+ padding: 0;
85
+ cursor: pointer;
86
+ }
87
+ a { color: var(--accent-text); text-decoration: none; }
88
+ a:hover { text-decoration: underline; }
89
+ :focus-visible {
90
+ outline: 2px solid var(--focus);
91
+ outline-offset: 2px;
92
+ border-radius: 4px;
93
+ }
94
+ ::selection { background: var(--accent-tint); }
95
+
96
+ .wrap { max-width: 1160px; margin: 0 auto; padding: 0 24px; }
97
+
98
+ /* ---------- topbar ---------- */
99
+ .topbar {
100
+ position: sticky;
101
+ top: 0;
102
+ z-index: var(--z-sticky);
103
+ background: var(--bg);
104
+ border-bottom: 1px solid var(--line);
105
+ }
106
+ .topbar-in {
107
+ display: flex;
108
+ align-items: center;
109
+ gap: 12px;
110
+ min-height: 52px;
111
+ padding-top: 8px;
112
+ padding-bottom: 8px;
113
+ }
114
+ .brand { display: flex; align-items: baseline; gap: 10px; min-width: 0; flex: 1; }
115
+ .brand h1 { font-size: 15px; font-weight: 650; margin: 0; letter-spacing: -0.01em; white-space: nowrap; }
116
+ .brand .target {
117
+ font-size: 12.5px;
118
+ color: var(--ink-2);
119
+ overflow: hidden;
120
+ text-overflow: ellipsis;
121
+ white-space: nowrap;
122
+ }
123
+ .round-chip {
124
+ font-size: 11.5px;
125
+ font-weight: 600;
126
+ color: var(--ink-2);
127
+ border: 1px solid var(--line-strong);
128
+ border-radius: 999px;
129
+ padding: 2px 9px;
130
+ white-space: nowrap;
131
+ }
132
+ .topbar-tools { display: flex; align-items: center; gap: 8px; }
133
+ .updated {
134
+ font-size: 11.5px;
135
+ color: var(--ink-3);
136
+ font-variant-numeric: tabular-nums;
137
+ white-space: nowrap;
138
+ }
139
+ .icon-btn {
140
+ display: inline-flex;
141
+ align-items: center;
142
+ gap: 6px;
143
+ padding: 5px 9px;
144
+ border: 1px solid var(--line);
145
+ border-radius: 8px;
146
+ font-size: 12px;
147
+ color: var(--ink-2);
148
+ background: var(--bg);
149
+ transition: background-color 150ms ease-out, border-color 150ms ease-out, color 150ms ease-out;
150
+ }
151
+ .icon-btn:hover { background: var(--surface); color: var(--ink); }
152
+ .icon-btn[aria-pressed="true"] {
153
+ border-color: var(--accent);
154
+ color: var(--accent-text);
155
+ background: var(--accent-tint);
156
+ }
157
+ .icon-btn svg { width: 14px; height: 14px; flex: none; }
158
+
159
+ /* ---------- verdict banner ---------- */
160
+ .banner {
161
+ margin-top: 20px;
162
+ border: 1px solid color-mix(in oklab, var(--v-color) 38%, var(--line));
163
+ background: color-mix(in oklab, var(--v-color) 9%, var(--bg));
164
+ border-radius: 12px;
165
+ padding: 16px 18px;
166
+ display: flex;
167
+ gap: 14px;
168
+ align-items: flex-start;
169
+ }
170
+ .banner .v-icon {
171
+ width: 30px; height: 30px; flex: none;
172
+ display: grid; place-items: center;
173
+ border-radius: 8px;
174
+ background: color-mix(in oklab, var(--v-color) 20%, var(--bg));
175
+ color: var(--v-text);
176
+ }
177
+ .banner .v-icon svg { width: 17px; height: 17px; }
178
+ .banner h2 {
179
+ margin: 0;
180
+ font-size: 16px;
181
+ font-weight: 700;
182
+ letter-spacing: 0.01em;
183
+ color: var(--v-text);
184
+ }
185
+ .banner .v-rationale { margin: 2px 0 0; font-size: 13.5px; color: var(--ink-2); max-width: 72ch; }
186
+ .banner .v-meta {
187
+ margin-top: 8px;
188
+ display: flex;
189
+ flex-wrap: wrap;
190
+ gap: 6px 16px;
191
+ font-size: 12px;
192
+ color: var(--ink-3);
193
+ }
194
+ .banner .v-meta code { font-size: 11.5px; }
195
+
196
+ /* ---------- KPI tiles ---------- */
197
+ .tiles {
198
+ display: grid;
199
+ grid-template-columns: repeat(auto-fit, minmax(128px, 1fr));
200
+ gap: 10px;
201
+ margin-top: 14px;
202
+ }
203
+ .tile {
204
+ text-align: left;
205
+ border: 1px solid var(--line);
206
+ border-radius: 10px;
207
+ padding: 11px 13px 9px;
208
+ background: var(--bg);
209
+ transition: background-color 150ms ease-out, border-color 150ms ease-out;
210
+ }
211
+ .tile:hover { background: var(--surface); }
212
+ .tile[aria-pressed="true"] {
213
+ border-color: var(--accent);
214
+ background: var(--accent-tint);
215
+ }
216
+ .tile .t-label {
217
+ display: flex;
218
+ align-items: center;
219
+ gap: 7px;
220
+ font-size: 11.5px;
221
+ font-weight: 600;
222
+ letter-spacing: 0.02em;
223
+ color: var(--ink-2);
224
+ }
225
+ .tile .t-value {
226
+ font-size: 26px;
227
+ font-weight: 700;
228
+ letter-spacing: -0.02em;
229
+ margin-top: 2px;
230
+ color: var(--ink);
231
+ }
232
+ .tile .t-sub { font-size: 11px; color: var(--ink-3); margin-top: -2px; }
233
+ .tile.is-zero .t-value { color: var(--ink-3); font-weight: 600; }
234
+ .tile.static { cursor: default; }
235
+ .tile.static:hover { background: var(--bg); }
236
+
237
+ .dot {
238
+ width: 8px; height: 8px;
239
+ border-radius: 999px;
240
+ flex: none;
241
+ background: var(--d, var(--ink-3));
242
+ box-shadow: inset 0 0 0 1px var(--dot-ring);
243
+ }
244
+
245
+ /* ---------- toolbar ---------- */
246
+ .toolbar {
247
+ position: sticky;
248
+ top: 52px;
249
+ z-index: var(--z-sticky);
250
+ background: var(--bg);
251
+ padding: 12px 0 10px;
252
+ border-bottom: 1px solid var(--line);
253
+ display: flex;
254
+ flex-wrap: wrap;
255
+ gap: 8px;
256
+ align-items: center;
257
+ }
258
+ .search {
259
+ position: relative;
260
+ flex: 1 1 220px;
261
+ max-width: 340px;
262
+ }
263
+ .search > svg {
264
+ position: absolute; left: 9px; top: 50%;
265
+ translate: 0 -50%;
266
+ width: 14px; height: 14px;
267
+ color: var(--ink-3);
268
+ pointer-events: none;
269
+ }
270
+ .search input {
271
+ width: 100%;
272
+ padding: 6px 26px 6px 29px;
273
+ font: inherit;
274
+ font-size: 13px;
275
+ color: var(--ink);
276
+ background: var(--surface);
277
+ border: 1px solid var(--line);
278
+ border-radius: 8px;
279
+ transition: border-color 150ms ease-out;
280
+ }
281
+ .search input::placeholder { color: var(--ink-3); }
282
+ .search input:focus { outline: none; border-color: var(--accent); }
283
+ .search .clear-q {
284
+ position: absolute; right: 4px; top: 50%;
285
+ translate: 0 -50%;
286
+ width: 20px; height: 20px;
287
+ display: none;
288
+ place-items: center;
289
+ color: var(--ink-3);
290
+ border-radius: 6px;
291
+ }
292
+ .search .clear-q:hover { color: var(--ink); background: var(--surface-2); }
293
+ .search .clear-q svg { width: 12px; height: 12px; }
294
+ .search.has-q .clear-q { display: grid; }
295
+
296
+ .chip {
297
+ display: inline-flex;
298
+ align-items: center;
299
+ gap: 6px;
300
+ padding: 4px 11px;
301
+ border: 1px solid var(--line);
302
+ border-radius: 999px;
303
+ font-size: 12px;
304
+ font-weight: 500;
305
+ color: var(--ink-2);
306
+ background: var(--bg);
307
+ transition: background-color 150ms ease-out, border-color 150ms ease-out, color 150ms ease-out;
308
+ }
309
+ .chip:hover { background: var(--surface); color: var(--ink); }
310
+ .chip[aria-pressed="true"] {
311
+ background: var(--ink);
312
+ border-color: var(--ink);
313
+ color: var(--bg);
314
+ }
315
+ .chip .count { font-size: 11px; color: inherit; opacity: 0.65; font-variant-numeric: tabular-nums; }
316
+ .toolbar .sep { width: 1px; height: 18px; background: var(--line); margin: 0 2px; }
317
+ .select-wrap { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--ink-3); }
318
+ .select-wrap select {
319
+ font: inherit;
320
+ font-size: 12px;
321
+ color: var(--ink-2);
322
+ background: var(--bg);
323
+ border: 1px solid var(--line);
324
+ border-radius: 8px;
325
+ padding: 4px 6px;
326
+ }
327
+ .toolbar .spacer { flex: 1; }
328
+
329
+ .result-line {
330
+ display: flex;
331
+ align-items: center;
332
+ gap: 10px;
333
+ font-size: 12px;
334
+ color: var(--ink-3);
335
+ margin: 12px 0 4px;
336
+ }
337
+ .result-line .link-btn { color: var(--accent-text); font-size: 12px; }
338
+ .result-line .link-btn:hover { text-decoration: underline; }
339
+
340
+ /* ---------- findings ---------- */
341
+ .file-group { margin-top: 18px; }
342
+ .file-head {
343
+ position: sticky;
344
+ top: 105px;
345
+ z-index: calc(var(--z-sticky) - 1);
346
+ background: var(--bg);
347
+ display: flex;
348
+ align-items: center;
349
+ gap: 10px;
350
+ padding: 6px 0;
351
+ border-bottom: 1px solid var(--line);
352
+ }
353
+ .file-head .path {
354
+ font-size: 12.5px;
355
+ font-weight: 600;
356
+ color: var(--ink);
357
+ word-break: break-all;
358
+ }
359
+ .file-head .file-dots { display: flex; align-items: center; gap: 5px; margin-left: auto; flex: none; }
360
+ .file-head .file-dots .n { font-size: 11px; color: var(--ink-3); font-variant-numeric: tabular-nums; margin-right: 2px; }
361
+
362
+ .finding {
363
+ border: 1px solid var(--line);
364
+ border-radius: 12px;
365
+ margin-top: 10px;
366
+ background: var(--bg);
367
+ transition: border-color 150ms ease-out;
368
+ }
369
+ .finding[open] { border-color: var(--line-strong); }
370
+ .finding summary {
371
+ list-style: none;
372
+ display: flex;
373
+ align-items: center;
374
+ gap: 10px;
375
+ padding: 10px 14px;
376
+ cursor: pointer;
377
+ border-radius: 12px;
378
+ transition: background-color 150ms ease-out;
379
+ }
380
+ .finding summary::-webkit-details-marker { display: none; }
381
+ .finding summary:hover { background: var(--surface); }
382
+ .finding[open] summary { border-radius: 12px 12px 0 0; }
383
+ .sev-tag {
384
+ display: inline-flex;
385
+ align-items: center;
386
+ gap: 6px;
387
+ font-size: 11px;
388
+ font-weight: 650;
389
+ letter-spacing: 0.03em;
390
+ text-transform: uppercase;
391
+ color: var(--sev-text);
392
+ flex: none;
393
+ width: 74px;
394
+ }
395
+ .f-title {
396
+ font-size: 13.5px;
397
+ font-weight: 600;
398
+ color: var(--ink);
399
+ flex: 1;
400
+ min-width: 0;
401
+ }
402
+ .f-flags { display: inline-flex; gap: 5px; flex: none; }
403
+ .flag {
404
+ font-size: 10.5px;
405
+ font-weight: 600;
406
+ color: var(--ink-3);
407
+ border: 1px solid var(--line);
408
+ border-radius: 5px;
409
+ padding: 1px 6px;
410
+ white-space: nowrap;
411
+ }
412
+ .flag.qw { color: var(--accent-text); border-color: color-mix(in oklab, var(--accent) 45%, var(--line)); }
413
+ .f-anchor, .f-anchor-body {
414
+ font-size: 11.5px;
415
+ color: var(--ink-3);
416
+ flex: none;
417
+ font-variant-numeric: tabular-nums;
418
+ }
419
+ .anchor-row { display: flex; gap: 8px; align-items: center; }
420
+ .chev { flex: none; color: var(--ink-3); transition: rotate 150ms ease-out; }
421
+ .chev svg { width: 14px; height: 14px; display: block; }
422
+ .finding[open] .chev { rotate: 90deg; }
423
+
424
+ .f-body { padding: 4px 14px 14px 98px; }
425
+ .f-body > * + * { margin-top: 12px; }
426
+ .prose { font-size: 13.5px; color: var(--ink-2); max-width: 74ch; }
427
+ .prose p { margin: 0 0 8px; }
428
+ .prose p:last-child { margin-bottom: 0; }
429
+ .prose code {
430
+ font-size: 12px;
431
+ background: var(--code-bg);
432
+ border: 1px solid var(--line);
433
+ border-radius: 5px;
434
+ padding: 1px 5px;
435
+ }
436
+ .prose pre {
437
+ background: var(--code-bg);
438
+ border: 1px solid var(--line);
439
+ border-radius: 8px;
440
+ padding: 10px 12px;
441
+ overflow-x: auto;
442
+ font-size: 12px;
443
+ line-height: 1.5;
444
+ }
445
+ .prose pre code { background: none; border: 0; padding: 0; }
446
+ .prose ul, .prose ol { margin: 0 0 8px; padding-left: 20px; }
447
+ .prose li { margin: 2px 0; }
448
+ .prose h1, .prose h2, .prose h3, .prose h4 {
449
+ font-size: 13px;
450
+ font-weight: 650;
451
+ color: var(--ink);
452
+ margin: 14px 0 6px;
453
+ text-wrap: balance;
454
+ }
455
+ .prose table { border-collapse: collapse; font-size: 12.5px; margin: 8px 0; width: 100%; }
456
+ .prose th, .prose td {
457
+ border: 1px solid var(--line);
458
+ padding: 5px 9px;
459
+ text-align: left;
460
+ vertical-align: top;
461
+ }
462
+ .prose th { background: var(--surface); font-weight: 600; color: var(--ink); }
463
+ .prose blockquote {
464
+ margin: 8px 0;
465
+ padding: 2px 12px;
466
+ border: 1px solid var(--line);
467
+ border-radius: 8px;
468
+ color: var(--ink-3);
469
+ background: var(--surface);
470
+ }
471
+ .prose hr { border: 0; border-top: 1px solid var(--line); margin: 12px 0; }
472
+
473
+ .kv { font-size: 12px; color: var(--ink-3); }
474
+ .kv .k { font-weight: 600; color: var(--ink-2); }
475
+ .evidence { font-size: 12px; color: var(--ink-3); padding-left: 0; margin: 4px 0 0; list-style: none; }
476
+ .evidence li { margin: 3px 0; padding-left: 16px; position: relative; }
477
+ .evidence li::before {
478
+ content: "";
479
+ position: absolute; left: 2px; top: 7px;
480
+ width: 5px; height: 5px;
481
+ border-radius: 999px;
482
+ background: var(--good);
483
+ }
484
+
485
+ .codebox { border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
486
+ .codebox-head {
487
+ display: flex;
488
+ align-items: center;
489
+ gap: 8px;
490
+ padding: 6px 10px;
491
+ background: var(--surface);
492
+ border-bottom: 1px solid var(--line);
493
+ font-size: 11.5px;
494
+ font-weight: 600;
495
+ color: var(--ink-2);
496
+ }
497
+ .codebox-head .note { font-weight: 400; color: var(--ink-3); }
498
+ .codebox-head .copy-btn { margin-left: auto; }
499
+ .codebox pre {
500
+ margin: 0;
501
+ padding: 10px 12px;
502
+ overflow-x: auto;
503
+ font-size: 12px;
504
+ line-height: 1.55;
505
+ background: var(--code-bg);
506
+ max-height: 420px;
507
+ overflow-y: auto;
508
+ }
509
+ .copy-btn {
510
+ display: inline-flex;
511
+ align-items: center;
512
+ gap: 5px;
513
+ font-size: 11px;
514
+ font-weight: 600;
515
+ color: var(--ink-3);
516
+ padding: 3px 8px;
517
+ border: 1px solid var(--line);
518
+ border-radius: 6px;
519
+ background: var(--bg);
520
+ transition: color 150ms ease-out, border-color 150ms ease-out;
521
+ }
522
+ .copy-btn:hover { color: var(--ink); border-color: var(--line-strong); }
523
+ .copy-btn.copied { color: var(--good-text); border-color: color-mix(in oklab, var(--good) 45%, var(--line)); }
524
+ .copy-btn svg { width: 12px; height: 12px; }
525
+
526
+ .f-meta {
527
+ display: flex;
528
+ flex-wrap: wrap;
529
+ gap: 4px 16px;
530
+ font-size: 11px;
531
+ color: var(--ink-3);
532
+ border-top: 1px solid var(--line);
533
+ padding-top: 10px;
534
+ }
535
+
536
+ /* ---------- secondary sections ---------- */
537
+ .section { margin-top: 30px; }
538
+ .section > h3 {
539
+ font-size: 13px;
540
+ font-weight: 650;
541
+ margin: 0 0 4px;
542
+ display: flex;
543
+ align-items: center;
544
+ gap: 8px;
545
+ color: var(--ink);
546
+ }
547
+ .section > h3 .n { font-size: 12px; font-weight: 600; color: var(--ink-3); }
548
+ .section .hint { font-size: 12px; color: var(--ink-3); margin: 0 0 10px; }
549
+ .plain-list { list-style: none; margin: 0; padding: 0; border: 1px solid var(--line); border-radius: 12px; }
550
+ .plain-list:empty { display: none; }
551
+ .plain-list li {
552
+ display: flex;
553
+ align-items: baseline;
554
+ gap: 10px;
555
+ padding: 8px 14px;
556
+ font-size: 12.5px;
557
+ border-top: 1px solid var(--line);
558
+ }
559
+ .plain-list li:first-child { border-top: 0; }
560
+ .plain-list .li-icon { flex: none; align-self: center; display: grid; place-items: center; width: 16px; height: 16px; }
561
+ .plain-list .li-icon svg { width: 14px; height: 14px; }
562
+ .plain-list .li-title { color: var(--ink-2); min-width: 0; }
563
+ .plain-list .li-file { font-size: 11.5px; color: var(--ink-3); margin-left: auto; flex: none; }
564
+ .plain-list .li-round { font-size: 11px; color: var(--ink-3); flex: none; width: 52px; text-align: right; }
565
+ .muted-details > summary {
566
+ cursor: pointer;
567
+ font-size: 12.5px;
568
+ color: var(--ink-2);
569
+ padding: 6px 0;
570
+ list-style: none;
571
+ display: inline-flex;
572
+ align-items: center;
573
+ gap: 7px;
574
+ }
575
+ .muted-details > summary::-webkit-details-marker { display: none; }
576
+ .muted-details > summary:hover { color: var(--ink); }
577
+ .muted-details > summary .chev { rotate: 0deg; }
578
+ .muted-details[open] > summary .chev { rotate: 90deg; }
579
+
580
+ /* ---------- timeline ---------- */
581
+ .timeline {
582
+ display: flex;
583
+ gap: 0;
584
+ overflow-x: auto;
585
+ padding: 6px 0 10px;
586
+ }
587
+ .round-node { display: flex; align-items: flex-start; flex: none; }
588
+ .round-card {
589
+ border: 1px solid var(--line);
590
+ border-radius: 10px;
591
+ padding: 9px 13px;
592
+ min-width: 150px;
593
+ background: var(--bg);
594
+ }
595
+ .round-node.current .round-card { border-color: var(--accent); background: var(--accent-tint); }
596
+ .round-card .r-top {
597
+ display: flex;
598
+ align-items: center;
599
+ gap: 7px;
600
+ font-size: 12px;
601
+ font-weight: 650;
602
+ }
603
+ .round-card .r-verdict { font-size: 10.5px; font-weight: 700; letter-spacing: 0.04em; color: var(--rv-text); }
604
+ .round-card .r-date { font-size: 11px; color: var(--ink-3); margin-top: 3px; font-variant-numeric: tabular-nums; }
605
+ .round-card .r-stats { font-size: 11px; color: var(--ink-2); margin-top: 3px; font-variant-numeric: tabular-nums; }
606
+ .round-link {
607
+ flex: none;
608
+ width: 26px;
609
+ height: 1px;
610
+ background: var(--line-strong);
611
+ margin-top: 26px;
612
+ }
613
+
614
+ /* ---------- walkthrough ---------- */
615
+ .walkthrough {
616
+ border: 1px solid var(--line);
617
+ border-radius: 12px;
618
+ margin-top: 14px;
619
+ }
620
+ .walkthrough > summary {
621
+ list-style: none;
622
+ cursor: pointer;
623
+ display: flex;
624
+ align-items: center;
625
+ gap: 9px;
626
+ padding: 11px 14px;
627
+ font-size: 13px;
628
+ font-weight: 600;
629
+ border-radius: 12px;
630
+ transition: background-color 150ms ease-out;
631
+ }
632
+ .walkthrough > summary::-webkit-details-marker { display: none; }
633
+ .walkthrough > summary:hover { background: var(--surface); }
634
+ .walkthrough > summary .chev { margin-left: auto; }
635
+ .walkthrough[open] > summary { border-bottom: 1px solid var(--line); border-radius: 12px 12px 0 0; }
636
+ .walkthrough .prose { padding: 14px 16px; max-width: none; }
637
+ .walkthrough .prose > :first-child { margin-top: 0; }
638
+
639
+ /* ---------- empty / states ---------- */
640
+ .empty {
641
+ border: 1px dashed var(--line-strong);
642
+ border-radius: 12px;
643
+ padding: 34px 20px;
644
+ text-align: center;
645
+ margin-top: 16px;
646
+ }
647
+ .empty .e-icon { display: inline-grid; place-items: center; width: 34px; height: 34px; border-radius: 999px; background: var(--surface); color: var(--ink-3); margin-bottom: 10px; }
648
+ .empty .e-icon svg { width: 17px; height: 17px; }
649
+ .empty.good .e-icon { background: color-mix(in oklab, var(--good) 15%, var(--bg)); color: var(--good-text); }
650
+ .empty h3 { margin: 0; font-size: 14px; font-weight: 650; }
651
+ .empty p { margin: 4px 0 0; font-size: 12.5px; color: var(--ink-3); }
652
+ .empty .link-btn { color: var(--accent-text); font-weight: 600; }
653
+
654
+ .hydrate-error {
655
+ max-width: 560px;
656
+ margin: 15vh auto;
657
+ padding: 0 24px;
658
+ text-align: center;
659
+ }
660
+ .hydrate-error h1 { font-size: 17px; }
661
+ .hydrate-error p { color: var(--ink-2); font-size: 13.5px; }
662
+ .hydrate-error code { background: var(--surface); border: 1px solid var(--line); border-radius: 5px; padding: 1px 6px; font-size: 12.5px; }
663
+
664
+ footer.wrap {
665
+ margin-top: 44px;
666
+ padding-top: 14px;
667
+ padding-bottom: 34px;
668
+ border-top: 1px solid var(--line);
669
+ font-size: 11.5px;
670
+ color: var(--ink-3);
671
+ display: flex;
672
+ flex-wrap: wrap;
673
+ gap: 4px 18px;
674
+ }
675
+
676
+ #toast {
677
+ position: fixed;
678
+ bottom: 22px;
679
+ left: 50%;
680
+ translate: -50% 0;
681
+ z-index: var(--z-toast);
682
+ background: var(--ink);
683
+ color: var(--bg);
684
+ font-size: 12.5px;
685
+ font-weight: 600;
686
+ padding: 7px 14px;
687
+ border-radius: 999px;
688
+ opacity: 0;
689
+ pointer-events: none;
690
+ transition: opacity 180ms ease-out;
691
+ }
692
+ #toast.show { opacity: 1; }
693
+
694
+ @media (max-width: 720px) {
695
+ .wrap { padding: 0 14px; }
696
+ .topbar-in { flex-wrap: wrap; }
697
+ .updated { display: none; }
698
+ .sev-tag { width: auto; }
699
+ .finding summary { flex-wrap: wrap; }
700
+ .f-body { padding-left: 14px; }
701
+ .f-anchor { display: none; }
702
+ .toolbar { top: 0; position: static; }
703
+ .file-head { position: static; }
704
+ .result-line { flex-wrap: wrap; }
705
+ }
706
+ @media (prefers-reduced-motion: reduce) {
707
+ *, *::before, *::after { transition: none !important; animation: none !important; }
708
+ }
709
+ @media print {
710
+ .topbar-tools, .toolbar, .result-line .link-btn, .copy-btn, #toast { display: none !important; }
711
+ .topbar, .toolbar, .file-head { position: static; }
712
+ .finding { break-inside: avoid; }
713
+ body { font-size: 12px; }
714
+ }
715
+ </style>
716
+ </head>
717
+ <body>
718
+ <div id="app"></div>
719
+ <div id="toast" role="status" aria-live="polite"></div>
720
+ <noscript><p style="padding:24px">This report needs JavaScript (all local — no network access).</p></noscript>
721
+ <script id="dr-data" type="application/json">__DEEP_REVIEW_DATA__</script>
722
+ <script>
723
+ "use strict";
724
+ (() => {
725
+ /* ---------- data ---------- */
726
+ let DATA = null;
727
+ try { DATA = JSON.parse(document.getElementById("dr-data").textContent); } catch (_) { /* placeholder */ }
728
+ const app = document.getElementById("app");
729
+ if (!DATA || !DATA.schema_version) {
730
+ document.title = "Deep Review — template";
731
+ app.innerHTML = `<div class="hydrate-error"><h1>Template not hydrated</h1>
732
+ <p>This file is the fixed wtk-deep-review UI template. Generate the report with</p>
733
+ <p><code>python3 &lt;skill-dir&gt;/scripts/render_html.py --out &lt;out&gt;</code></p></div>`;
734
+ return;
735
+ }
736
+
737
+ /* ---------- constants ---------- */
738
+ const SEV = ["critical", "major", "minor", "trivial"];
739
+ const SEV_RANK = { critical: 3, major: 2, minor: 1, trivial: 0 };
740
+ const SEV_LABEL = { critical: "Critical", major: "Major", minor: "Minor", trivial: "Trivial" };
741
+ const CAT_LABEL = { "potential-issue": "Potential issue", refactor: "Refactor", nitpick: "Nitpick" };
742
+ const VERDICT = {
743
+ SHIP: { color: "var(--good)", text: "var(--good-text)", icon: "check", label: "SHIP" },
744
+ FIX_BEFORE_SHIP: { color: "var(--sev-major)", text: "var(--sev-major-text)", icon: "alert", label: "FIX BEFORE SHIP" },
745
+ REWORK: { color: "var(--sev-critical)", text: "var(--sev-critical-text)", icon: "block", label: "REWORK" },
746
+ PENDING: { color: "var(--ink-3)", text: "var(--ink-2)", icon: "clock", label: "ROUND IN PROGRESS" },
747
+ };
748
+ const ICONS = {
749
+ check: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 8.5l3.5 3.5 7.5-8"/></svg>',
750
+ alert: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M8 1.8 15 14H1z"/><path d="M8 6.2v3.4"/><circle cx="8" cy="11.9" r="0.4" fill="currentColor"/></svg>',
751
+ block: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="8" cy="8" r="6.2"/><path d="M3.8 3.8l8.4 8.4"/></svg>',
752
+ clock: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><circle cx="8" cy="8" r="6.2"/><path d="M8 4.5V8l2.4 1.6"/></svg>',
753
+ chev: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3.5 10.5 8 6 12.5"/></svg>',
754
+ search: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><circle cx="7" cy="7" r="4.6"/><path d="M10.6 10.6 14 14"/></svg>',
755
+ copy: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"><rect x="5.5" y="5.5" width="8" height="8" rx="1.5"/><path d="M10.5 5.5v-2a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2"/></svg>',
756
+ x: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>',
757
+ sun: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="8" cy="8" r="3.2"/><path d="M8 1.2v1.8M8 13v1.8M1.2 8H3M13 8h1.8M3.2 3.2l1.3 1.3M11.5 11.5l1.3 1.3M12.8 3.2l-1.3 1.3M4.5 11.5l-1.3 1.3"/></svg>',
758
+ moon: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"><path d="M13.5 9.8A6 6 0 0 1 6.2 2.5a6 6 0 1 0 7.3 7.3z"/></svg>',
759
+ refresh: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M13.6 6.6A6 6 0 0 0 2.9 4.9M2.4 9.4a6 6 0 0 0 10.7 1.7"/><path d="M13.8 2.5v4.1h-4.1M2.2 13.5V9.4h4.1"/></svg>',
760
+ dismiss: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><circle cx="8" cy="8" r="6.2"/><path d="M5 8h6"/></svg>',
761
+ dup: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 8a5.5 5.5 0 0 1 9.4-3.9L13.5 5.7"/><path d="M13.5 2v3.7H9.8"/><path d="M13.5 8a5.5 5.5 0 0 1-9.4 3.9L2.5 10.3"/><path d="M2.5 14v-3.7h3.7"/></svg>',
762
+ };
763
+
764
+ /* ---------- utils ---------- */
765
+ const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
766
+ const shortSha = (s) => (s || "").slice(0, 7);
767
+ const fmtDate = (iso) => {
768
+ if (!iso) return "";
769
+ const d = new Date(iso);
770
+ return isNaN(d) ? iso : d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
771
+ };
772
+ const anchorOf = (f) => {
773
+ const end = f.end_line && f.end_line !== f.line ? `-${f.end_line}` : "";
774
+ return `${f.file}:${f.line}${end}`;
775
+ };
776
+ /* ---------- tiny markdown renderer (escape-first) ---------- */
777
+ function inlineMd(s) {
778
+ let out = esc(s);
779
+ out = out.replace(/&lt;br\s*\/?&gt;/gi, "<br>");
780
+ out = out.replace(/`([^`]+)`/g, (_, c) => `<code>${c}</code>`);
781
+ out = out.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
782
+ out = out.replace(/(^|[\s(])_([^_]+)_(?=[\s).,;:!?]|$)/g, "$1<em>$2</em>");
783
+ out = out.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '<a href="$2" rel="noopener" target="_blank">$1</a>');
784
+ return out;
785
+ }
786
+ function renderMd(src) {
787
+ if (!src) return "";
788
+ const lines = String(src).replace(/\r\n/g, "\n").split("\n");
789
+ const out = [];
790
+ let i = 0;
791
+ const isTableRow = (l) => /^\s*\|.*\|\s*$/.test(l);
792
+ while (i < lines.length) {
793
+ const line = lines[i];
794
+ if (/^\s*```/.test(line)) {
795
+ const buf = [];
796
+ i++;
797
+ while (i < lines.length && !/^\s*```/.test(lines[i])) buf.push(lines[i++]);
798
+ i++;
799
+ out.push(`<pre><code>${esc(buf.join("\n"))}</code></pre>`);
800
+ continue;
801
+ }
802
+ if (/^\s*<!--/.test(line) && /-->\s*$/.test(line)) { i++; continue; }
803
+ const h = line.match(/^(#{1,6})\s+(.*)$/);
804
+ if (h) { out.push(`<h${Math.min(h[1].length, 4)}>${inlineMd(h[2])}</h${Math.min(h[1].length, 4)}>`); i++; continue; }
805
+ if (/^\s*(-{3,}|\*{3,})\s*$/.test(line)) { out.push("<hr>"); i++; continue; }
806
+ if (isTableRow(line) && isTableRow(lines[i + 1] || "") && /^[\s|:-]+$/.test(lines[i + 1])) {
807
+ const cells = (l) => l.trim().replace(/^\||\|$/g, "").split("|").map((c) => c.trim());
808
+ const head = cells(line);
809
+ i += 2;
810
+ const rows = [];
811
+ while (i < lines.length && isTableRow(lines[i])) rows.push(cells(lines[i++]));
812
+ out.push(`<table><thead><tr>${head.map((c) => `<th>${inlineMd(c)}</th>`).join("")}</tr></thead><tbody>${
813
+ rows.map((r) => `<tr>${r.map((c) => `<td>${inlineMd(c)}</td>`).join("")}</tr>`).join("")}</tbody></table>`);
814
+ continue;
815
+ }
816
+ if (/^\s*>/.test(line)) {
817
+ const buf = [];
818
+ while (i < lines.length && /^\s*>/.test(lines[i])) buf.push(lines[i++].replace(/^\s*>\s?/, ""));
819
+ out.push(`<blockquote>${renderMd(buf.join("\n"))}</blockquote>`);
820
+ continue;
821
+ }
822
+ const li = line.match(/^\s*([-*]|\d+\.)\s+/);
823
+ if (li) {
824
+ const ordered = /\d+\./.test(li[1]);
825
+ const buf = [];
826
+ while (i < lines.length) {
827
+ const m = lines[i].match(/^\s*([-*]|\d+\.)\s+(.*)$/);
828
+ if (!m) break;
829
+ buf.push(`<li>${inlineMd(m[2])}</li>`);
830
+ i++;
831
+ }
832
+ out.push(ordered ? `<ol>${buf.join("")}</ol>` : `<ul>${buf.join("")}</ul>`);
833
+ continue;
834
+ }
835
+ if (line.trim() === "") { i++; continue; }
836
+ const buf = [];
837
+ while (i < lines.length && lines[i].trim() !== "" && !/^\s*(#|```|>|\||[-*]\s|\d+\.\s)/.test(lines[i])) buf.push(lines[i++]);
838
+ if (buf.length) out.push(`<p>${inlineMd(buf.join(" "))}</p>`);
839
+ else i++;
840
+ }
841
+ return out.join("\n");
842
+ }
843
+
844
+ /* ---------- persisted UI state ---------- */
845
+ const LS_KEY = `wtk-deep-review:${DATA.target}`;
846
+ const defaults = { q: "", sev: [], cat: [], kind: [], status: [], scope: [], qw: false, sort: "severity", theme: "system", refresh: true, open: {}, closed: {} };
847
+ let ui = { ...defaults };
848
+ try { ui = { ...defaults, ...JSON.parse(localStorage.getItem(LS_KEY) || "{}") }; } catch (_) {}
849
+ const save = () => { try { localStorage.setItem(LS_KEY, JSON.stringify(ui)); } catch (_) {} };
850
+
851
+ /* ---------- theme ---------- */
852
+ const sysDark = matchMedia("(prefers-color-scheme: dark)");
853
+ function applyTheme() {
854
+ const mode = ui.theme === "system" ? (sysDark.matches ? "dark" : "light") : ui.theme;
855
+ document.documentElement.dataset.theme = mode;
856
+ return mode;
857
+ }
858
+ sysDark.addEventListener("change", () => { if (ui.theme === "system") applyTheme(); });
859
+ applyTheme();
860
+
861
+ /* ---------- derived data ---------- */
862
+ const open = (DATA.findings || []).filter((f) => f.round_status === "new" || f.round_status === "duplicate");
863
+ const openBySev = Object.fromEntries(SEV.map((s) => [s, open.filter((f) => f.severity === s).length]));
864
+ const advisories = open.filter((f) => f.result_kind === "advisory");
865
+ const aiPrompts = open.filter((f) => f.ai_prompt);
866
+ document.title = `Deep Review — ${DATA.target} · round ${DATA.round}`;
867
+
868
+ /* ---------- filtering ---------- */
869
+ function visibleFindings() {
870
+ const q = ui.q.trim().toLowerCase();
871
+ return open.filter((f) => {
872
+ if (ui.sev.length && !ui.sev.includes(f.severity)) return false;
873
+ if (ui.cat.length && !ui.cat.includes(f.category)) return false;
874
+ if (ui.kind.length && !ui.kind.includes(f.result_kind)) return false;
875
+ if (ui.status.length) {
876
+ const st = f.round_status === "duplicate" ? "recurring" : "new";
877
+ if (!ui.status.includes(st)) return false;
878
+ }
879
+ if (ui.scope.length) {
880
+ const sc = f.in_diff ? "in-diff" : "outside";
881
+ if (!ui.scope.includes(sc)) return false;
882
+ }
883
+ if (ui.qw && !f.quick_win) return false;
884
+ if (q) {
885
+ const hay = `${f.title}\n${f.body}\n${f.file}\n${f.fingerprint}\n${(f.rules || []).map((r) => r.id).join(" ")}`.toLowerCase();
886
+ if (!hay.includes(q)) return false;
887
+ }
888
+ return true;
889
+ });
890
+ }
891
+ const filtersActive = () =>
892
+ ui.q.trim() !== "" || ui.sev.length || ui.cat.length || ui.kind.length || ui.status.length || ui.scope.length || ui.qw;
893
+
894
+ /* ---------- render helpers ---------- */
895
+ const sevDot = (s) => `<span class="dot" style="--d: var(--sev-${s})"></span>`;
896
+ const sevTag = (s) => `<span class="sev-tag" style="--sev-text: var(--sev-${s}-text)">${sevDot(s)}${SEV_LABEL[s]}</span>`;
897
+
898
+ function copyBtn(payload, label = "Copy") {
899
+ return `<button type="button" class="copy-btn" data-copy="${esc(encodeURIComponent(payload))}">${ICONS.copy}<span>${esc(label)}</span></button>`;
900
+ }
901
+
902
+ function findingCard(f) {
903
+ const fp = f.fingerprint;
904
+ const openByDefault = (f.severity === "critical" || f.severity === "major");
905
+ const isOpen = ui.open[fp] ? true : ui.closed[fp] ? false : openByDefault;
906
+ const flags = [
907
+ f.quick_win ? '<span class="flag qw">&#9889; quick win</span>' : "",
908
+ f.round_status === "duplicate" ? `<span class="flag">recurring &middot; R${f.first_round ?? "?"}</span>` : "",
909
+ !f.in_diff ? '<span class="flag">outside diff</span>' : "",
910
+ f.result_kind === "advisory" ? '<span class="flag">advisory · no verdict impact</span>' : "",
911
+ ].join("");
912
+ const rules = (f.rules || []).map((r) =>
913
+ `<p class="kv"><span class="k">Guideline ${esc(r.id)}</span> (<code>${esc(r.source)}</code>): &ldquo;${esc(r.guideline)}&rdquo;</p>`).join("");
914
+ const evidence = (f.evidence || []).length
915
+ ? `<div><p class="kv"><span class="k">Evidence trail</span></p><ul class="evidence">${f.evidence.map((e) => `<li class="mono">${esc(e)}</li>`).join("")}</ul></div>` : "";
916
+ const also = (f.also_applies || []).length
917
+ ? `<p class="kv"><span class="k">Also applies to</span> <span class="mono">${f.also_applies.map(esc).join(", ")}</span></p>` : "";
918
+ const suggestion = f.suggestion
919
+ ? `<div class="codebox"><div class="codebox-head"><span>&#128221; Committable suggestion</span><span class="note">replaces lines ${esc(String(f.line))}&ndash;${esc(String(f.end_line || f.line))} &mdash; review before committing</span>${copyBtn(f.suggestion)}</div><pre><code>${esc(f.suggestion)}</code></pre></div>` : "";
920
+ const prompt = f.ai_prompt
921
+ ? `<div class="codebox"><div class="codebox-head"><span>&#129302; Prompt for AI agents</span>${copyBtn(f.ai_prompt, "Copy prompt")}</div><pre><code>${esc(f.ai_prompt)}</code></pre></div>` : "";
922
+ const repair = (f.repair_plan || []).length
923
+ ? `<details class="repair-plan"><summary>&#128736; Repair plan</summary><ol>${f.repair_plan.map((step) => `<li>${esc(step.replace(/^\d+\.\s*/, ""))}</li>`).join("")}</ol></details>` : "";
924
+ const meta = [
925
+ `<span>${esc(CAT_LABEL[f.category] || f.category)}</span>`,
926
+ `<span>${esc(f.result_kind || "defect")}</span>`,
927
+ `<span>fingerprint <span class="mono">${esc(fp)}</span></span>`,
928
+ f.source_jobs?.length ? `<span>reported by ${f.source_jobs.map(esc).join(", ")}</span>` : "",
929
+ f.hunk ? `<span>hunk <span class="mono">${esc(f.hunk)}</span></span>` : "",
930
+ ].filter(Boolean).join("");
931
+ return `<details class="finding" data-fp="${esc(fp)}" ${isOpen ? "open" : ""}>
932
+ <summary>
933
+ ${sevTag(f.severity)}
934
+ <span class="f-title">${esc(f.title)}</span>
935
+ <span class="f-flags">${flags}</span>
936
+ <span class="f-anchor mono">${esc(anchorOf(f))}</span>
937
+ <span class="chev">${ICONS.chev}</span>
938
+ </summary>
939
+ <div class="f-body">
940
+ <div class="anchor-row">
941
+ <span class="f-anchor-body mono">${esc(anchorOf(f))}</span>
942
+ ${copyBtn(anchorOf(f), "Copy anchor")}
943
+ </div>
944
+ <div class="prose">${renderMd(f.body)}</div>
945
+ ${also}${rules}${evidence}${suggestion}${repair}${prompt}
946
+ <div class="f-meta">${meta}</div>
947
+ </div>
948
+ </details>`;
949
+ }
950
+
951
+ function fileGroups(list) {
952
+ const groups = new Map();
953
+ for (const f of list) {
954
+ if (!groups.has(f.file)) groups.set(f.file, []);
955
+ groups.get(f.file).push(f);
956
+ }
957
+ let ordered = [...groups.entries()];
958
+ if (ui.sort === "file") ordered.sort((a, b) => a[0].localeCompare(b[0]));
959
+ else ordered.sort((a, b) =>
960
+ Math.max(...b[1].map((f) => SEV_RANK[f.severity])) - Math.max(...a[1].map((f) => SEV_RANK[f.severity])) ||
961
+ a[0].localeCompare(b[0]));
962
+ return ordered.map(([path, fs]) => {
963
+ fs.sort((a, b) => (ui.sort === "file" ? a.line - b.line : SEV_RANK[b.severity] - SEV_RANK[a.severity] || a.line - b.line));
964
+ const dots = SEV.map((s) => {
965
+ const n = fs.filter((f) => f.severity === s).length;
966
+ return n ? `<span class="n">${n}</span><span class="dot" style="--d: var(--sev-${s})" title="${n} ${SEV_LABEL[s]}"></span>` : "";
967
+ }).join("");
968
+ return `<section class="file-group">
969
+ <div class="file-head"><span class="path mono">${esc(path)}</span><span class="file-dots">${dots}</span></div>
970
+ ${fs.map(findingCard).join("")}
971
+ </section>`;
972
+ }).join("");
973
+ }
974
+
975
+ /* ---------- sections ---------- */
976
+ function bannerHtml() {
977
+ const v = VERDICT[DATA.verdict] || VERDICT.PENDING;
978
+ const rationale = DATA.verdict === "PENDING"
979
+ ? "Findings are merged; the verdict lands when render_review.py completes this round."
980
+ : esc(DATA.verdict_rationale || "");
981
+ const pr = DATA.pr ? `<span>PR <a href="${esc(DATA.pr.url)}" target="_blank" rel="noopener">#${esc(String(DATA.pr.number))} ${esc(DATA.pr.title)}</a></span>` : "";
982
+ const c = DATA.counts || {};
983
+ return `<section class="banner" style="--v-color:${v.color}; --v-text:${v.text}" aria-label="verdict">
984
+ <span class="v-icon">${ICONS[v.icon]}</span>
985
+ <div>
986
+ <h2>${v.label}</h2>
987
+ ${rationale ? `<p class="v-rationale">${rationale}</p>` : ""}
988
+ <div class="v-meta">
989
+ ${pr}
990
+ <span><span class="mono">${esc(shortSha(DATA.base))}&hellip;${esc(shortSha(DATA.head))}</span></span>
991
+ <span>${c.selected ?? 0} files reviewed &middot; ${c.ignored ?? 0} ignored &middot; ${c.skipped ?? 0} skipped</span>
992
+ <span>assertive review</span>
993
+ </div>
994
+ </div>
995
+ </section>`;
996
+ }
997
+
998
+ function tilesHtml() {
999
+ const res = (DATA.resolved || []).length;
1000
+ const tiles = SEV.map((s) => `
1001
+ <button type="button" class="tile ${openBySev[s] ? "" : "is-zero"}" data-sev="${s}" aria-pressed="${ui.sev.includes(s)}">
1002
+ <span class="t-label"><span class="dot" style="--d: var(--sev-${s})"></span>${SEV_LABEL[s]}</span>
1003
+ <span class="t-value">${openBySev[s]}</span>
1004
+ </button>`).join("");
1005
+ return `<div class="tiles" role="group" aria-label="open findings by severity — click to filter">
1006
+ ${tiles}
1007
+ <div class="tile static ${res ? "" : "is-zero"}">
1008
+ <span class="t-label"><span class="dot" style="--d: var(--good)"></span>Resolved</span>
1009
+ <span class="t-value">${res}</span>
1010
+ <span class="t-sub">since last round</span>
1011
+ </div>
1012
+ <div class="tile static ${advisories.length ? "" : "is-zero"}">
1013
+ <span class="t-label"><span class="dot"></span>Advisories</span>
1014
+ <span class="t-value">${advisories.length}</span>
1015
+ <span class="t-sub">of ${open.length} open</span>
1016
+ </div>
1017
+ </div>`;
1018
+ }
1019
+
1020
+ function chip(group, val, label, count, pressed) {
1021
+ return `<button type="button" class="chip" data-group="${group}" data-val="${val}" aria-pressed="${pressed}">${label}${count != null ? ` <span class="count">${count}</span>` : ""}</button>`;
1022
+ }
1023
+
1024
+ function toolbarHtml() {
1025
+ const catCounts = Object.fromEntries(["potential-issue", "refactor", "nitpick"].map((c) => [c, open.filter((f) => f.category === c).length]));
1026
+ const kindCounts = Object.fromEntries(["defect", "advisory"].map((kind) => [kind, open.filter((f) => f.result_kind === kind).length]));
1027
+ const recurring = open.filter((f) => f.round_status === "duplicate").length;
1028
+ const outside = open.filter((f) => !f.in_diff).length;
1029
+ const qwCount = open.filter((f) => f.quick_win).length;
1030
+ return `<div class="toolbar" role="search">
1031
+ <div class="search ${ui.q ? "has-q" : ""}">
1032
+ ${ICONS.search}
1033
+ <input id="q" type="search" placeholder="Filter findings… ( / )" value="${esc(ui.q)}" aria-label="Filter findings">
1034
+ <button type="button" class="clear-q" aria-label="Clear search">${ICONS.x}</button>
1035
+ </div>
1036
+ ${chip("kind", "defect", "Defects", kindCounts.defect, ui.kind.includes("defect"))}
1037
+ ${chip("kind", "advisory", "Advisories", kindCounts.advisory, ui.kind.includes("advisory"))}
1038
+ <span class="sep"></span>
1039
+ ${chip("cat", "potential-issue", "Issues", catCounts["potential-issue"], ui.cat.includes("potential-issue"))}
1040
+ ${chip("cat", "refactor", "Refactors", catCounts.refactor, ui.cat.includes("refactor"))}
1041
+ ${chip("cat", "nitpick", "Nitpicks", catCounts.nitpick, ui.cat.includes("nitpick"))}
1042
+ <span class="sep"></span>
1043
+ ${chip("status", "new", "New", open.length - recurring, ui.status.includes("new"))}
1044
+ ${chip("status", "recurring", "Recurring", recurring, ui.status.includes("recurring"))}
1045
+ ${outside ? chip("scope", "outside", "Outside diff", outside, ui.scope.includes("outside")) : ""}
1046
+ ${qwCount ? `<button type="button" class="chip" data-group="qw" aria-pressed="${ui.qw}">&#9889; Quick wins <span class="count">${qwCount}</span></button>` : ""}
1047
+ <span class="spacer"></span>
1048
+ <label class="select-wrap">Sort
1049
+ <select id="sort" aria-label="Sort findings">
1050
+ <option value="severity" ${ui.sort === "severity" ? "selected" : ""}>severity</option>
1051
+ <option value="file" ${ui.sort === "file" ? "selected" : ""}>file</option>
1052
+ </select>
1053
+ </label>
1054
+ ${aiPrompts.length ? `<button type="button" class="icon-btn" id="copy-all" title="Copy every open Critical/Major AI fix prompt">${ICONS.copy}Fix prompts <span class="count">${aiPrompts.length}</span></button>` : ""}
1055
+ </div>`;
1056
+ }
1057
+
1058
+ function findingsHtml() {
1059
+ const vis = visibleFindings();
1060
+ const active = filtersActive();
1061
+ let body;
1062
+ if (!open.length) {
1063
+ const res = (DATA.resolved || []).length;
1064
+ body = DATA.verdict === "SHIP"
1065
+ ? `<div class="empty good"><span class="e-icon">${ICONS.check}</span><h3>No open findings</h3><p>${res ? `${res} finding${res === 1 ? "" : "s"} resolved since the last round. ` : ""}This round is clean — ship it.</p></div>`
1066
+ : `<div class="empty"><span class="e-icon">${ICONS.clock}</span><h3>No findings yet</h3><p>Reviewer outputs are merged as the round progresses.</p></div>`;
1067
+ } else if (!vis.length) {
1068
+ body = `<div class="empty"><span class="e-icon">${ICONS.search}</span><h3>No findings match</h3><p><button type="button" class="link-btn" id="clear-filters-empty">Clear filters</button> to see all ${open.length} open findings.</p></div>`;
1069
+ } else {
1070
+ const defects = vis.filter((f) => f.result_kind === "defect");
1071
+ const advice = vis.filter((f) => f.result_kind === "advisory");
1072
+ body = `${defects.length ? `<section class="section"><h3>Defects <span class="n">${defects.length}</span></h3>${fileGroups(defects)}</section>` : ""}
1073
+ ${advice.length ? `<section class="section"><h3>Advisories <span class="n">${advice.length}</span></h3><p class="hint">Actionable maintainability and project-rule guidance; excluded from the verdict.</p>${fileGroups(advice)}</section>` : ""}`;
1074
+ }
1075
+ const line = open.length
1076
+ ? `<div class="result-line"><span>Showing ${vis.length} of ${open.length} open findings</span>${active ? `<button type="button" class="link-btn" id="clear-filters">Clear filters</button>` : ""}
1077
+ <span class="spacer" style="flex:1"></span>
1078
+ <button type="button" class="link-btn" data-toggle-all="open">Expand all</button>
1079
+ <button type="button" class="link-btn" data-toggle-all="close">Collapse all</button></div>`
1080
+ : "";
1081
+ return `${line}<div id="findings">${body}</div>`;
1082
+ }
1083
+
1084
+ function listItem(icon, iconColor, title, file, line, roundTxt) {
1085
+ return `<li><span class="li-icon" style="color:${iconColor}">${icon}</span>
1086
+ <span class="li-title">${esc(title)}</span>
1087
+ <span class="li-file mono">${esc(file)}${line ? `:${line}` : ""}</span>
1088
+ <span class="li-round">${esc(roundTxt)}</span></li>`;
1089
+ }
1090
+
1091
+ function secondaryHtml() {
1092
+ const parts = [];
1093
+ const stats = DATA.review_stats || {};
1094
+ const coverage = stats.coverage || {};
1095
+ const laneCoverage = coverage.lanes || {};
1096
+ parts.push(`<section class="section"><h3>Review observability</h3>
1097
+ <p class="hint">${stats.candidates ?? 0} candidates investigated · ${stats.reported ?? 0} reported before deduplication · ${stats.suppressed ?? 0} suppressions recorded.</p>
1098
+ <p class="hint">${coverage.selected_hunk_lines ?? 0} selected hunk lines · defect coverage ${laneCoverage.defect?.complete ? "complete" : "incomplete"}.</p>
1099
+ </section>`);
1100
+ const suppressions = DATA.suppressions || [];
1101
+ if (suppressions.length) {
1102
+ parts.push(`<section class="section"><details class="muted-details"><summary><span class="chev">${ICONS.chev}</span>Suppressed candidates <span class="n">${suppressions.length}</span></summary>
1103
+ <ul class="plain-list" style="margin-top:8px">${suppressions.map((row) => listItem(ICONS.dismiss, "var(--ink-3)", `${row.candidate} — ${row.reason}`, row.file, row.line, row.source_job)).join("")}</ul></details></section>`);
1104
+ }
1105
+ const unrev = DATA.unreviewed_open || [];
1106
+ if (unrev.length) {
1107
+ parts.push(`<section class="section"><h3>Still open — not re-reviewed <span class="n">${unrev.length}</span></h3>
1108
+ <p class="hint">Raised in an earlier round; their files were carried or skipped this round, so their status is unchanged.</p>
1109
+ <ul class="plain-list">${unrev.map((r) => listItem(ICONS.dup, "var(--ink-3)", r.title, r.file, null, `R${r.round ?? "?"}`)).join("")}</ul></section>`);
1110
+ }
1111
+ const res = DATA.resolved || [];
1112
+ if (res.length) {
1113
+ parts.push(`<section class="section"><h3>Resolved since last round <span class="n">${res.length}</span></h3>
1114
+ <ul class="plain-list">${res.map((r) => listItem(ICONS.check, "var(--good-text)", r.title, r.file, null, `R${r.round ?? "?"}`)).join("")}</ul></section>`);
1115
+ }
1116
+ const dis = DATA.dismissed || [];
1117
+ if (dis.length) {
1118
+ parts.push(`<section class="section"><details class="muted-details"><summary><span class="chev">${ICONS.chev}</span>Dismissed — never re-raised <span class="n">${dis.length}</span></summary>
1119
+ <ul class="plain-list" style="margin-top:8px">${dis.map((r) => listItem(ICONS.dismiss, "var(--ink-3)", r.title, r.file, null, `R${r.round ?? "?"}`)).join("")}</ul></details></section>`);
1120
+ }
1121
+ return parts.join("");
1122
+ }
1123
+
1124
+ function timelineHtml() {
1125
+ const rounds = DATA.rounds || [];
1126
+ if (!rounds.length) return "";
1127
+ const nodes = rounds.map((r, i) => {
1128
+ const v = VERDICT[r.verdict] || VERDICT.PENDING;
1129
+ const stats = r.stats
1130
+ ? `<div class="r-stats">${r.stats.open ?? "?"} open &middot; ${r.stats.critical ?? 0}&#8202;C ${r.stats.major ?? 0}&#8202;M &middot; ${r.stats.advisories ?? 0} advisories</div>` : "";
1131
+ return `${i ? '<span class="round-link" aria-hidden="true"></span>' : ""}
1132
+ <div class="round-node ${r.current ? "current" : ""}">
1133
+ <div class="round-card" style="--rv-text:${v.text}">
1134
+ <div class="r-top"><span class="dot" style="--d:${v.color}"></span>Round ${r.n} <span class="r-verdict">${r.verdict === "PENDING" ? "IN PROGRESS" : esc(String(r.verdict || "").replace(/_/g, " "))}</span></div>
1135
+ <div class="r-date">${esc(fmtDate(r.reviewed_at))} &middot; <span class="mono">${esc(shortSha(r.base))}&hellip;${esc(shortSha(r.head))}</span></div>
1136
+ ${stats}
1137
+ </div>
1138
+ </div>`;
1139
+ }).join("");
1140
+ return `<section class="section"><h3>Rounds</h3><div class="timeline">${nodes}</div></section>`;
1141
+ }
1142
+
1143
+ function walkthroughHtml() {
1144
+ if (!DATA.walkthrough) return "";
1145
+ return `<details class="walkthrough" ${ui.open["__walkthrough"] ? "open" : ""} id="walkthrough">
1146
+ <summary>Walkthrough &amp; changes<span class="chev">${ICONS.chev}</span></summary>
1147
+ <div class="prose">${renderMd(DATA.walkthrough)}</div>
1148
+ </details>`;
1149
+ }
1150
+
1151
+ /* ---------- shell ---------- */
1152
+ function render() {
1153
+ app.innerHTML = `
1154
+ <header class="topbar"><div class="wrap topbar-in">
1155
+ <div class="brand">
1156
+ <h1>Deep Review</h1>
1157
+ <span class="target mono" title="${esc(DATA.target)}">${esc(DATA.target)}</span>
1158
+ <span class="round-chip">round ${DATA.round}</span>
1159
+ </div>
1160
+ <div class="topbar-tools">
1161
+ <span class="updated" id="updated" title="${esc(DATA.generated_at)}"></span>
1162
+ <button type="button" class="icon-btn" id="auto-refresh" aria-pressed="${ui.refresh}" title="Reload when a new round lands">${ICONS.refresh}<span>Auto</span></button>
1163
+ <button type="button" class="icon-btn" id="theme" title="Toggle theme (t)"></button>
1164
+ </div>
1165
+ </div></header>
1166
+ <main class="wrap">
1167
+ ${bannerHtml()}
1168
+ ${tilesHtml()}
1169
+ ${walkthroughHtml()}
1170
+ ${open.length ? toolbarHtml() : ""}
1171
+ ${findingsHtml()}
1172
+ ${secondaryHtml()}
1173
+ ${timelineHtml()}
1174
+ </main>
1175
+ <footer class="wrap">
1176
+ <span>wtk-deep-review &middot; generated ${esc(fmtDate(DATA.generated_at))}</span>
1177
+ <span>artifacts: <span class="mono">${esc(DATA.out_dir || "")}</span></span>
1178
+ <span class="mono">review.md &middot; findings.json &middot; state.json</span>
1179
+ </footer>`;
1180
+ syncThemeBtn();
1181
+ tickUpdated();
1182
+ }
1183
+
1184
+ function syncThemeBtn() {
1185
+ const btn = document.getElementById("theme");
1186
+ if (!btn) return;
1187
+ const mode = applyTheme();
1188
+ btn.innerHTML = (mode === "dark" ? ICONS.moon : ICONS.sun) + `<span>${ui.theme === "system" ? "Auto" : mode === "dark" ? "Dark" : "Light"}</span>`;
1189
+ }
1190
+
1191
+ /* ---------- auto refresh ---------- */
1192
+ const genMs = Date.parse(DATA.generated_at || "") || Date.now();
1193
+ function tickUpdated() {
1194
+ const el = document.getElementById("updated");
1195
+ if (!el) return;
1196
+ const s = Math.max(0, Math.round((Date.now() - genMs) / 1000));
1197
+ el.textContent = s < 5 ? "just updated" : s < 60 ? `updated ${s}s ago` : s < 3600 ? `updated ${Math.round(s / 60)}m ago` : `updated ${fmtDate(DATA.generated_at)}`;
1198
+ }
1199
+ setInterval(tickUpdated, 1000);
1200
+ setInterval(() => {
1201
+ if (!ui.refresh || document.hidden) return;
1202
+ const ae = document.activeElement;
1203
+ if (ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.tagName === "SELECT")) return;
1204
+ if (String(getSelection() || "").length) return;
1205
+ location.reload();
1206
+ }, 10000);
1207
+
1208
+ /* ---------- events (delegated) ---------- */
1209
+ let toastTimer;
1210
+ function toast(msg) {
1211
+ const t = document.getElementById("toast");
1212
+ t.textContent = msg;
1213
+ t.classList.add("show");
1214
+ clearTimeout(toastTimer);
1215
+ toastTimer = setTimeout(() => t.classList.remove("show"), 1400);
1216
+ }
1217
+ async function copyText(text, btn) {
1218
+ let ok = false;
1219
+ try { await navigator.clipboard.writeText(text); ok = true; } catch (_) {
1220
+ const ta = document.createElement("textarea");
1221
+ ta.value = text;
1222
+ ta.style.position = "fixed";
1223
+ ta.style.opacity = "0";
1224
+ document.body.appendChild(ta);
1225
+ ta.select();
1226
+ try { ok = document.execCommand("copy"); } catch (_) {}
1227
+ ta.remove();
1228
+ }
1229
+ if (ok && btn) {
1230
+ btn.classList.add("copied");
1231
+ setTimeout(() => btn.classList.remove("copied"), 1200);
1232
+ }
1233
+ toast(ok ? "Copied" : "Copy failed — select manually");
1234
+ }
1235
+ const toggleIn = (arr, val) => { const i = arr.indexOf(val); i >= 0 ? arr.splice(i, 1) : arr.push(val); };
1236
+
1237
+ document.addEventListener("click", (e) => {
1238
+ const copyEl = e.target.closest("[data-copy]");
1239
+ if (copyEl) { e.preventDefault(); copyText(decodeURIComponent(copyEl.dataset.copy), copyEl); return; }
1240
+ const tile = e.target.closest(".tile[data-sev]");
1241
+ if (tile) { toggleIn(ui.sev, tile.dataset.sev); save(); render(); return; }
1242
+ const chipEl = e.target.closest(".chip[data-group]");
1243
+ if (chipEl) {
1244
+ const g = chipEl.dataset.group;
1245
+ if (g === "qw") ui.qw = !ui.qw;
1246
+ else toggleIn(ui[g], chipEl.dataset.val);
1247
+ save(); render(); return;
1248
+ }
1249
+ if (e.target.closest("#clear-filters") || e.target.closest("#clear-filters-empty")) {
1250
+ Object.assign(ui, { q: "", sev: [], cat: [], kind: [], status: [], scope: [], qw: false });
1251
+ save(); render(); return;
1252
+ }
1253
+ const tog = e.target.closest("[data-toggle-all]");
1254
+ if (tog) {
1255
+ const openAll = tog.dataset.toggleAll === "open";
1256
+ document.querySelectorAll(".finding").forEach((d) => {
1257
+ d.open = openAll;
1258
+ const fp = d.dataset.fp;
1259
+ if (openAll) { ui.open[fp] = 1; delete ui.closed[fp]; }
1260
+ else { ui.closed[fp] = 1; delete ui.open[fp]; }
1261
+ });
1262
+ save(); return;
1263
+ }
1264
+ if (e.target.closest("#copy-all")) {
1265
+ const text = aiPrompts.map((f) => f.ai_prompt).join("\n\n---\n\n");
1266
+ copyText(text, e.target.closest("#copy-all"));
1267
+ return;
1268
+ }
1269
+ if (e.target.closest("#auto-refresh")) {
1270
+ ui.refresh = !ui.refresh;
1271
+ save();
1272
+ document.getElementById("auto-refresh").setAttribute("aria-pressed", String(ui.refresh));
1273
+ toast(ui.refresh ? "Auto-reload on" : "Auto-reload off");
1274
+ return;
1275
+ }
1276
+ if (e.target.closest("#theme")) {
1277
+ ui.theme = ui.theme === "system" ? "dark" : ui.theme === "dark" ? "light" : "system";
1278
+ save(); syncThemeBtn(); return;
1279
+ }
1280
+ if (e.target.closest(".search .clear-q")) {
1281
+ ui.q = ""; save(); render();
1282
+ document.getElementById("q")?.focus();
1283
+ return;
1284
+ }
1285
+ });
1286
+
1287
+ document.addEventListener("toggle", (e) => {
1288
+ const d = e.target;
1289
+ if (d.classList?.contains("finding")) {
1290
+ const fp = d.dataset.fp;
1291
+ if (d.open) { ui.open[fp] = 1; delete ui.closed[fp]; }
1292
+ else { ui.closed[fp] = 1; delete ui.open[fp]; }
1293
+ save();
1294
+ }
1295
+ if (d.id === "walkthrough") {
1296
+ if (d.open) ui.open["__walkthrough"] = 1; else delete ui.open["__walkthrough"];
1297
+ save();
1298
+ }
1299
+ }, true);
1300
+
1301
+ let qTimer;
1302
+ document.addEventListener("input", (e) => {
1303
+ if (e.target.id === "q") {
1304
+ clearTimeout(qTimer);
1305
+ const val = e.target.value;
1306
+ qTimer = setTimeout(() => {
1307
+ ui.q = val;
1308
+ save();
1309
+ render();
1310
+ const inp = document.getElementById("q");
1311
+ if (inp) {
1312
+ inp.focus();
1313
+ inp.setSelectionRange(inp.value.length, inp.value.length);
1314
+ }
1315
+ }, 180);
1316
+ }
1317
+ });
1318
+ document.addEventListener("change", (e) => {
1319
+ if (e.target.id === "sort") { ui.sort = e.target.value; save(); render(); }
1320
+ });
1321
+
1322
+ document.addEventListener("keydown", (e) => {
1323
+ if (e.key === "/" && !/INPUT|TEXTAREA|SELECT/.test(document.activeElement?.tagName || "")) {
1324
+ e.preventDefault();
1325
+ document.getElementById("q")?.focus();
1326
+ }
1327
+ if (e.key === "Escape" && document.activeElement?.id === "q") {
1328
+ document.activeElement.blur();
1329
+ }
1330
+ if (e.key === "t" && !/INPUT|TEXTAREA|SELECT/.test(document.activeElement?.tagName || "")) {
1331
+ ui.theme = ui.theme === "system" ? "dark" : ui.theme === "dark" ? "light" : "system";
1332
+ save(); syncThemeBtn();
1333
+ }
1334
+ });
1335
+
1336
+ render();
1337
+ })();
1338
+ </script>
1339
+ </body>
1340
+ </html>