argus-reviewer-e2e 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -71
- package/action/action.yml +134 -10
- package/action/approval-review.mjs +13 -3
- package/action/bootstrap.mjs +4 -0
- package/action/emit-review.mjs +16 -0
- package/action/runtime.mjs +32 -0
- package/action/sticky-comment.cjs +1260 -479
- package/dist/cli.d.ts +103 -7
- package/dist/cli.js +1208 -186
- package/dist/config.d.ts +96 -11
- package/dist/config.js +102 -4
- package/dist/detect.d.ts +29 -2
- package/dist/detect.js +98 -7
- package/dist/driver/browser.d.ts +32 -0
- package/dist/driver/browser.js +56 -1
- package/dist/driver/target.d.ts +4 -1
- package/dist/driver/target.js +27 -6
- package/dist/engine/actions.d.ts +5 -0
- package/dist/engine/actions.js +8 -0
- package/dist/engine/explore.d.ts +78 -0
- package/dist/engine/explore.js +373 -0
- package/dist/engine/loop.d.ts +2 -2
- package/dist/engine/loop.js +8 -8
- package/dist/engine/prompts.d.ts +28 -1
- package/dist/engine/prompts.js +88 -0
- package/dist/evidence/ci.d.ts +13 -1
- package/dist/evidence/ci.js +38 -3
- package/dist/evidence/gate.d.ts +8 -0
- package/dist/evidence/gate.js +1 -1
- package/dist/evidence/link.js +1 -1
- package/dist/executor/a0.d.ts +114 -1
- package/dist/executor/a0.js +216 -4
- package/dist/fsutil.d.ts +3 -2
- package/dist/fsutil.js +7 -4
- package/dist/journal/schema.d.ts +1 -1
- package/dist/log.d.ts +2 -1
- package/dist/log.js +10 -2
- package/dist/mention.d.ts +45 -0
- package/dist/mention.js +107 -0
- package/dist/pipeline/app.d.ts +126 -0
- package/dist/pipeline/app.js +250 -0
- package/dist/pipeline/budget.d.ts +1 -0
- package/dist/pipeline/budget.js +1 -1
- package/dist/pipeline/verify.d.ts +20 -3
- package/dist/pipeline/verify.js +189 -35
- package/dist/probe/persist.d.ts +68 -0
- package/dist/probe/persist.js +184 -0
- package/dist/probe/queue.d.ts +12 -0
- package/dist/probe/queue.js +10 -2
- package/dist/report/brand-assets.generated.d.ts +9 -0
- package/dist/report/brand-assets.generated.js +8 -0
- package/dist/report/comment.d.ts +99 -6
- package/dist/report/comment.js +292 -103
- package/dist/report/html.d.ts +50 -0
- package/dist/report/html.js +879 -0
- package/dist/report/manifest.d.ts +29 -0
- package/dist/report/manifest.js +37 -0
- package/dist/report/run.d.ts +54 -1
- package/dist/report/run.js +34 -9
- package/dist/report/viewmodel.d.ts +91 -0
- package/dist/report/viewmodel.js +241 -0
- package/dist/review/adjudicate.d.ts +6 -6
- package/dist/review/adjudicate.js +2 -2
- package/dist/review/inline.d.ts +44 -0
- package/dist/review/inline.js +95 -0
- package/dist/review/packs.d.ts +21 -0
- package/dist/review/packs.js +47 -0
- package/dist/review/scope.d.ts +16 -0
- package/dist/review/scope.js +74 -0
- package/dist/review/secrets.d.ts +10 -10
- package/dist/review/secrets.js +7 -7
- package/dist/review/testfiles.d.ts +18 -0
- package/dist/review/testfiles.js +26 -0
- package/dist/review/triage.d.ts +1 -1
- package/dist/review/triage.js +10 -10
- package/dist/review/validate.d.ts +41 -0
- package/dist/review/validate.js +76 -0
- package/dist/ui/errors.d.ts +54 -0
- package/dist/ui/errors.js +236 -0
- package/dist/ui/style.d.ts +34 -0
- package/dist/ui/style.js +48 -0
- package/dist/ui/summary.d.ts +38 -0
- package/dist/ui/summary.js +101 -0
- package/dist/vision/cost.d.ts +1 -1
- package/dist/vision/decisions.d.ts +9 -3
- package/dist/vision/decisions.js +31 -21
- package/dist/vision/openrouter.d.ts +4 -0
- package/dist/vision/openrouter.js +30 -4
- package/package.json +11 -2
|
@@ -0,0 +1,879 @@
|
|
|
1
|
+
import { EMPTY_STATES, FONT_FACES_CSS, MARK_SVG, SPRITE_SYMBOLS, TOKENS_CSS, } from './brand-assets.generated.js';
|
|
2
|
+
import { findingsOf, laneProof, manifestDuration, manifestRow, plural, verdictLead, verdictOf, } from './comment.js';
|
|
3
|
+
import { emptyLane, LANE_IDS } from './manifest.js';
|
|
4
|
+
import { formatUsd, isLaneManifest, isRunManifest, laneView, maskSecrets, PROOF_LEVELS, SEVERITIES, SEVERITY_LABEL, shortSha, STATUS_GLYPH, } from './viewmodel.js';
|
|
5
|
+
/**
|
|
6
|
+
* Offline HTML evidence report (plan U14, R22; DESIGN.md 7.6, A17).
|
|
7
|
+
*
|
|
8
|
+
* One self-contained `report.html` beside `run-manifest.json`: verdict,
|
|
9
|
+
* lanes, findings, flow timeline, heals and spend ledger. Every asset is
|
|
10
|
+
* inline (tokens, WOFF2 subsets as data URIs, the glyph sprite), so the file
|
|
11
|
+
* opens from an artifact zip with no network. Assets come from the generated
|
|
12
|
+
* module, never from assets/ on disk, so a packed install renders it too.
|
|
13
|
+
*
|
|
14
|
+
* Pure: the caller reads the files, this shapes them. Every interpolated
|
|
15
|
+
* string goes through `esc`, which masks secret-shaped tokens and escapes
|
|
16
|
+
* HTML. Inputs are untrusted JSON; a corrupt manifest renders the unreadable
|
|
17
|
+
* state and a missing lane renders unavailable, never a throw.
|
|
18
|
+
*/
|
|
19
|
+
export const REPORT_HTML = 'report.html';
|
|
20
|
+
const REPO_URL = 'https://github.com/duketopceo/Argus';
|
|
21
|
+
const isObj = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
22
|
+
const arr = (v) => (Array.isArray(v) ? v.filter(isObj) : []);
|
|
23
|
+
const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
24
|
+
const str = (v) => typeof v === 'string' && v !== '' ? v : undefined;
|
|
25
|
+
// ---- Escaping and formatting ------------------------------------------------
|
|
26
|
+
/** Mask secret-shaped tokens, then escape for HTML text and attributes. */
|
|
27
|
+
export function esc(v) {
|
|
28
|
+
return maskSecrets(String(v ?? ''))
|
|
29
|
+
.replace(/&/g, '&')
|
|
30
|
+
.replace(/</g, '<')
|
|
31
|
+
.replace(/>/g, '>')
|
|
32
|
+
.replace(/"/g, '"')
|
|
33
|
+
.replace(/'/g, ''');
|
|
34
|
+
}
|
|
35
|
+
/** Totals: 4 decimals above a cent, else 6 so small spend stays visible. */
|
|
36
|
+
const usdTotal = (n) => ((n ?? 0) >= 0.01 ? `$${(n ?? 0).toFixed(4)}` : formatUsd(n));
|
|
37
|
+
/** Budget caps read as set: `$1.00`. */
|
|
38
|
+
const usdCap = (n) => `$${n.toFixed(2)}`;
|
|
39
|
+
/** `450ms`, `12.3s`, `4m 05s`, `2h 14m` (DESIGN.md 6.2). */
|
|
40
|
+
export function formatDuration(ms) {
|
|
41
|
+
if (ms === undefined || !Number.isFinite(ms) || ms < 0)
|
|
42
|
+
return undefined;
|
|
43
|
+
if (ms < 1000)
|
|
44
|
+
return `${Math.round(ms)}ms`;
|
|
45
|
+
if (ms < 60_000)
|
|
46
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
47
|
+
const s = Math.floor(ms / 1000);
|
|
48
|
+
if (s < 3600)
|
|
49
|
+
return `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, '0')}s`;
|
|
50
|
+
return `${Math.floor(s / 3600)}h ${String(Math.floor((s % 3600) / 60)).padStart(2, '0')}m`;
|
|
51
|
+
}
|
|
52
|
+
const SHA = /^[0-9a-f]{7,40}$/i;
|
|
53
|
+
const REPO = /^[\w.-]+\/[\w.-]+$/;
|
|
54
|
+
const SAFE_URL = /^https:\/\/[^\s"'<>`]+$/;
|
|
55
|
+
// ---- Markup atoms -----------------------------------------------------------
|
|
56
|
+
function glyph(id, cls = '') {
|
|
57
|
+
return `<svg class="g${cls ? ` ${cls}` : ''}" aria-hidden="true" focusable="false"><use href="#${id}"/></svg>`;
|
|
58
|
+
}
|
|
59
|
+
const TONE = {
|
|
60
|
+
passed: 'passed',
|
|
61
|
+
failed: 'failed',
|
|
62
|
+
inconclusive: 'caution',
|
|
63
|
+
blocked: 'ink',
|
|
64
|
+
unavailable: 'muted',
|
|
65
|
+
skipped: 'muted',
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* The SVG glyph draws the status; the §6.7 text glyph rides along clipped
|
|
69
|
+
* (and hidden from assistive tech), so copied or text-only output keeps the
|
|
70
|
+
* same `◌ unavailable` vocabulary as the comment and terminal.
|
|
71
|
+
*/
|
|
72
|
+
function status(s, word = s) {
|
|
73
|
+
return `<span class="st t-${TONE[s]}">${glyph(`status-${s}`)}<span class="tg" aria-hidden="true">${STATUS_GLYPH[s]} </span><span>${esc(word)}</span></span>`;
|
|
74
|
+
}
|
|
75
|
+
function proof(level) {
|
|
76
|
+
if (level === null)
|
|
77
|
+
return '';
|
|
78
|
+
const n = level === 'none' ? 0 : PROOF_LEVELS.indexOf(level) + 1;
|
|
79
|
+
return `<span class="proof p${n}">${glyph(`proof-${n}`, 'meter')}<span>${level}</span></span>`;
|
|
80
|
+
}
|
|
81
|
+
const SEVERITY_TONE = { bug: 'failed', risk: 'caution', nit: 'muted', q: 'muted' };
|
|
82
|
+
const SEVERITY_GLYPH_ID = {
|
|
83
|
+
bug: 'severity-bug',
|
|
84
|
+
risk: 'severity-risk',
|
|
85
|
+
nit: 'severity-nit',
|
|
86
|
+
q: 'severity-q',
|
|
87
|
+
};
|
|
88
|
+
const isSeverity = (v) => typeof v === 'string' && SEVERITIES.includes(v);
|
|
89
|
+
function severity(s) {
|
|
90
|
+
return `<span class="st t-${SEVERITY_TONE[s]}">${glyph(SEVERITY_GLYPH_ID[s])}<span>${SEVERITY_LABEL[s]}</span></span>`;
|
|
91
|
+
}
|
|
92
|
+
function copyButton(value, label) {
|
|
93
|
+
return (`<button type="button" class="control copy" data-copy="${esc(value)}" aria-label="${esc(label)}">` +
|
|
94
|
+
`${glyph('copy', 'i-copy')}${glyph('check', 'i-check')}</button>`);
|
|
95
|
+
}
|
|
96
|
+
function emptyState(name, sentence, command) {
|
|
97
|
+
const art = EMPTY_STATES[name];
|
|
98
|
+
const pic = art === undefined ? '' : `<div class="art art-light">${art.light}</div><div class="art art-dark">${art.dark}</div>`;
|
|
99
|
+
const cmd = command === undefined
|
|
100
|
+
? ''
|
|
101
|
+
: `<p class="cmd"><code>${esc(command)}</code>${copyButton(command, `Copy command: ${command}`)}</p>`;
|
|
102
|
+
return `<div class="empty" aria-hidden="false">${pic}<p>${sentence}</p>${cmd}</div>`;
|
|
103
|
+
}
|
|
104
|
+
function dl(rows) {
|
|
105
|
+
const items = rows
|
|
106
|
+
.filter((r) => r[1] !== undefined && r[1] !== '')
|
|
107
|
+
.map(([k, v]) => `<div><dt>${k}</dt><dd>${v}</dd></div>`);
|
|
108
|
+
return items.length === 0 ? '' : `<dl class="kv">${items.join('')}</dl>`;
|
|
109
|
+
}
|
|
110
|
+
const HTML_LEAD = {
|
|
111
|
+
strong: (s) => `<strong>${s}</strong>`,
|
|
112
|
+
code: (s) => `<code>${esc(s)}</code>`,
|
|
113
|
+
text: esc,
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Parse the manifest text, degrading per lane: the run-level fields must
|
|
117
|
+
* pass the shared guard, and each lane that fails `isLaneManifest` is
|
|
118
|
+
* reported missing rather than sinking the whole report.
|
|
119
|
+
*/
|
|
120
|
+
export function readManifest(text) {
|
|
121
|
+
if (text === undefined)
|
|
122
|
+
return { state: 'missing' };
|
|
123
|
+
let value;
|
|
124
|
+
try {
|
|
125
|
+
value = JSON.parse(text);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return { state: 'unreadable', detail: 'did not parse' };
|
|
129
|
+
}
|
|
130
|
+
if (!isObj(value))
|
|
131
|
+
return { state: 'unreadable', detail: 'failed validation' };
|
|
132
|
+
const lanesIn = isObj(value.lanes) ? value.lanes : {};
|
|
133
|
+
const missing = LANE_IDS.filter((id) => !isLaneManifest(lanesIn[id], id));
|
|
134
|
+
const lanes = Object.fromEntries(LANE_IDS.map((id) => [id, missing.includes(id) ? emptyLane(id, false) : lanesIn[id]]));
|
|
135
|
+
// Run-level fields go through the whole-manifest guard with the lanes it
|
|
136
|
+
// already vetted, so the two checks cannot drift apart.
|
|
137
|
+
const candidate = { ...value, lanes };
|
|
138
|
+
if (!isRunManifest(candidate)) {
|
|
139
|
+
return { state: 'unreadable', detail: 'failed validation' };
|
|
140
|
+
}
|
|
141
|
+
return { state: 'ok', manifest: candidate, missing };
|
|
142
|
+
}
|
|
143
|
+
const MISSING_ROW = (lane) => ({
|
|
144
|
+
lane,
|
|
145
|
+
status: 'unavailable',
|
|
146
|
+
result: 'evidence missing',
|
|
147
|
+
proof: 'none',
|
|
148
|
+
spend: '',
|
|
149
|
+
});
|
|
150
|
+
function laneRowOf(c, id) {
|
|
151
|
+
return c.rows.find((r) => r.lane === id);
|
|
152
|
+
}
|
|
153
|
+
function verdictSection(c) {
|
|
154
|
+
const m = c.manifest;
|
|
155
|
+
let v;
|
|
156
|
+
if (m !== undefined) {
|
|
157
|
+
v = verdictOf(m.aggregate.ok, m.aggregate.status, c.cr);
|
|
158
|
+
// A lane record the file lost is missing evidence: never a clean pass.
|
|
159
|
+
if (c.missing.length > 0 && v.status === 'passed')
|
|
160
|
+
v = { status: 'inconclusive', label: 'inconclusive' };
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
v = { status: 'unavailable', label: c.ms.state === 'missing' ? 'no manifest' : 'manifest unreadable' };
|
|
164
|
+
}
|
|
165
|
+
const lead = m !== undefined || c.cr !== undefined ? verdictLead(c.rows, c.cr, HTML_LEAD) : '';
|
|
166
|
+
const levels = c.rows.map((r) => r.proof);
|
|
167
|
+
const best = levels.reduce((acc, p) => {
|
|
168
|
+
const rank = (x) => (x === null ? -1 : x === 'none' ? 0 : PROOF_LEVELS.indexOf(x) + 1);
|
|
169
|
+
return rank(p) > rank(acc) ? p : acc;
|
|
170
|
+
}, null);
|
|
171
|
+
const binding = m?.lanes.review.headBinding ?? (isObj(c.crRaw?.headBinding) ? c.crRaw.headBinding : undefined);
|
|
172
|
+
const bindingWord = binding?.status === 'not_applicable' ? 'fixture' : typeof binding?.status === 'string' ? binding.status : undefined;
|
|
173
|
+
const headCell = c.head === undefined
|
|
174
|
+
? undefined
|
|
175
|
+
: `<code>${esc(shortSha(c.head))}</code>${SHA.test(c.head) ? copyButton(c.head, 'Copy head commit') : ''}` +
|
|
176
|
+
(bindingWord !== undefined
|
|
177
|
+
? ` <span class="bind b-${bindingWord === 'mismatch' ? 'caution' : 'plain'}">${esc(bindingWord)}</span>`
|
|
178
|
+
: '');
|
|
179
|
+
let spend;
|
|
180
|
+
if (m !== undefined) {
|
|
181
|
+
const selected = LANE_IDS.map((id) => m.lanes[id]).filter((l) => l.selected && l.usage.metered);
|
|
182
|
+
const caps = selected.map((l) => num(l.budget.limitUsd));
|
|
183
|
+
const known = caps.filter((x) => x !== undefined);
|
|
184
|
+
// A cap is shown only when every metered lane has one; a partial sum would understate it.
|
|
185
|
+
const cap = known.length > 0 && known.length === caps.length ? known.reduce((a, b) => a + b, 0) : undefined;
|
|
186
|
+
spend = `<span class="fig">${usdTotal(m.aggregate.costUsd)}</span>${cap !== undefined ? ` <span class="of">of ${usdCap(cap)}</span>` : ''}`;
|
|
187
|
+
}
|
|
188
|
+
else if (c.cr !== undefined && num(c.crRaw?.visionCostUsd) !== undefined) {
|
|
189
|
+
spend = `<span class="fig">${usdTotal(num(c.crRaw?.visionCostUsd))}</span>`;
|
|
190
|
+
}
|
|
191
|
+
const duration = m !== undefined ? formatDuration(manifestDuration(m)) : undefined;
|
|
192
|
+
const where = c.repo !== undefined
|
|
193
|
+
? `${esc(c.repo)}${m?.identity.pr !== undefined ? ` <span class="dim">#${esc(m.identity.pr)}</span>` : ''}`
|
|
194
|
+
: undefined;
|
|
195
|
+
return `<section id="verdict" class="verdict" aria-labelledby="verdict-h">
|
|
196
|
+
<h1 id="verdict-h" class="headline t-${TONE[v.status]}">${glyph(`status-${v.status}`, 'big')}<span>${esc(v.label)}</span></h1>
|
|
197
|
+
${lead !== '' ? `<p class="lead">${lead}</p>` : ''}
|
|
198
|
+
${dl([
|
|
199
|
+
['Proof', best === null ? undefined : proof(best)],
|
|
200
|
+
['Head', headCell],
|
|
201
|
+
['Spend', spend],
|
|
202
|
+
['Duration', duration === undefined ? undefined : `<span class="data">${duration}</span>`],
|
|
203
|
+
['Repository', where],
|
|
204
|
+
])}
|
|
205
|
+
${binding?.status === 'mismatch' ? `<p class="banner t-caution">${glyph('status-inconclusive')}<span>Head binding mismatch: ${esc(binding.detail)}</span></p>` : ''}
|
|
206
|
+
</section>`;
|
|
207
|
+
}
|
|
208
|
+
/** One wrapping line of lane facts under the lane row. */
|
|
209
|
+
function laneDetails(view) {
|
|
210
|
+
const u = view.usage;
|
|
211
|
+
const duration = formatDuration(view.durationMs);
|
|
212
|
+
const bits = [
|
|
213
|
+
str(view.model) !== undefined ? `<code>${esc(view.model)}</code>` : undefined,
|
|
214
|
+
duration !== undefined ? `<span class="data">${duration}</span>` : undefined,
|
|
215
|
+
view.status !== 'skipped' ? `<span class="data">${plural(u.calls, 'call')}, ${u.tokens.toLocaleString('en-US')} tokens</span>` : undefined,
|
|
216
|
+
str(view.reportPath) !== undefined ? `<code>${esc(view.reportPath)}</code>` : undefined,
|
|
217
|
+
].filter((b) => b !== undefined);
|
|
218
|
+
const notes = [
|
|
219
|
+
view.summary !== undefined && view.reason !== undefined && view.summary !== view.reason ? esc(view.summary) : undefined,
|
|
220
|
+
// A matching binding is already in the header; only a problem earns a line here.
|
|
221
|
+
view.headBinding !== undefined && view.headBinding.status !== 'match' && view.headBinding.status !== 'not_applicable'
|
|
222
|
+
? `Head ${esc(view.headBinding.status)}: ${esc(view.headBinding.detail)}`
|
|
223
|
+
: undefined,
|
|
224
|
+
].filter((b) => b !== undefined);
|
|
225
|
+
return ((bits.length > 0 ? `<p class="lane-facts">${bits.map((b) => `<span>${b}</span>`).join('')}</p>` : '') +
|
|
226
|
+
notes.map((n) => `<p class="lane-note">${n}</p>`).join(''));
|
|
227
|
+
}
|
|
228
|
+
/** Escaped text with `backtick` spans set as code, as the model writes them. */
|
|
229
|
+
function prose(v) {
|
|
230
|
+
return esc(v).replace(/`([^`\n]+)`/g, '<code>$1</code>');
|
|
231
|
+
}
|
|
232
|
+
function lanesSection(c) {
|
|
233
|
+
if (c.manifest === undefined) {
|
|
234
|
+
const sentence = c.ms.state === 'unreadable'
|
|
235
|
+
? `<strong>Manifest unreadable:</strong> <code>run-manifest.json</code> ${esc(c.ms.detail)}, so lane results are unknown. Re-run verify to write a fresh one.`
|
|
236
|
+
: '<strong>No manifest:</strong> this run wrote no <code>run-manifest.json</code>, so lane results are unknown. Run verify to produce one.';
|
|
237
|
+
return `<section id="lanes" aria-labelledby="lanes-h"><h2 id="lanes-h">Lanes</h2>${emptyState('manifest-unreadable', sentence, 'argus-reviewer verify')}</section>`;
|
|
238
|
+
}
|
|
239
|
+
const items = LANE_IDS.map((id) => {
|
|
240
|
+
const row = laneRowOf(c, id) ?? MISSING_ROW(id);
|
|
241
|
+
const view = c.views[id];
|
|
242
|
+
const resultText = row.status === 'unavailable' && row.result === 'evidence missing'
|
|
243
|
+
? `<span class="t-muted">evidence missing</span>`
|
|
244
|
+
: esc(row.result);
|
|
245
|
+
return `<li class="lane${view?.selected === false ? ' off' : ''}" id="lane-${id}">
|
|
246
|
+
<div class="lane-head">${status(row.status)}<span class="lane-name">${glyph(`lane-${id}`, 'lg')}${id}</span><span class="lane-result">${resultText}</span><span class="lane-proof">${proof(row.proof)}</span><span class="lane-spend data">${esc(row.spend)}</span></div>
|
|
247
|
+
${view !== undefined && view.selected ? laneDetails(view) : ''}
|
|
248
|
+
</li>`;
|
|
249
|
+
});
|
|
250
|
+
return `<section id="lanes" aria-labelledby="lanes-h"><h2 id="lanes-h">Lanes</h2><ul class="lanes">${items.join('\n')}</ul></section>`;
|
|
251
|
+
}
|
|
252
|
+
function fileLink(c, file, line) {
|
|
253
|
+
const label = `${file}${line !== undefined ? `:${line}` : ''}`;
|
|
254
|
+
if (c.repo !== undefined && REPO.test(c.repo) && c.head !== undefined && SHA.test(c.head) && !/[\s"'<>`]/.test(file) && !file.includes('..')) {
|
|
255
|
+
const href = `https://github.com/${c.repo}/blob/${c.head}/${file.split('/').map(encodeURIComponent).join('/')}${line !== undefined ? `#L${line}` : ''}`;
|
|
256
|
+
return `<a class="path" href="${esc(href)}"><code>${esc(label)}</code></a>`;
|
|
257
|
+
}
|
|
258
|
+
return `<code class="path">${esc(label)}</code>`;
|
|
259
|
+
}
|
|
260
|
+
function findingsSection(c) {
|
|
261
|
+
const head = (n) => `<h2 id="findings-h">Findings${n !== undefined ? ` <span class="count">${n}</span>` : ''}</h2>`;
|
|
262
|
+
if (c.cr === undefined) {
|
|
263
|
+
const review = c.manifest?.lanes.review;
|
|
264
|
+
const sentence = c.manifest === undefined || review?.selected === true
|
|
265
|
+
? 'No code review report is attached to this run.'
|
|
266
|
+
: 'Code review did not run in this verify, so there are no findings to show.';
|
|
267
|
+
return `<section id="findings" aria-labelledby="findings-h">${head()}<p class="note">${sentence}</p></section>`;
|
|
268
|
+
}
|
|
269
|
+
if (c.cr.skipped === true) {
|
|
270
|
+
return `<section id="findings" aria-labelledby="findings-h">${head()}<p class="note">Code review skipped: ${esc(c.cr.summary ?? 'no reason given')}</p></section>`;
|
|
271
|
+
}
|
|
272
|
+
const findings = arr(findingsOf(c.cr));
|
|
273
|
+
const rank = (f) => (isSeverity(f.severity) ? SEVERITIES.indexOf(f.severity) : SEVERITIES.length);
|
|
274
|
+
const proofRank = (f) => PROOF_LEVELS.indexOf(f.evidence?.status);
|
|
275
|
+
const sorted = [...findings].sort((a, b) => rank(a) - rank(b) || proofRank(b) - proofRank(a));
|
|
276
|
+
const counts = SEVERITIES.map((s) => [s, findings.filter((f) => f.severity === s).length]);
|
|
277
|
+
const summary = counts
|
|
278
|
+
.filter(([s, n]) => n > 0 || s !== 'q')
|
|
279
|
+
.map(([s, n]) => `<span class="st t-${SEVERITY_TONE[s]}">${glyph(SEVERITY_GLYPH_ID[s])}<span>${plural(n, SEVERITY_LABEL[s], s === 'q' ? 'questions' : undefined)}</span></span>`)
|
|
280
|
+
.join('');
|
|
281
|
+
if (findings.length === 0) {
|
|
282
|
+
return `<section id="findings" aria-labelledby="findings-h">${head(0)}<p class="note">No findings: the review read the diff and raised nothing.</p></section>`;
|
|
283
|
+
}
|
|
284
|
+
const items = sorted.map((f, i) => {
|
|
285
|
+
const sev = isSeverity(f.severity) ? severity(f.severity) : `<span class="st t-muted">${esc(f.severity ?? 'finding')}</span>`;
|
|
286
|
+
const file = str(f.file);
|
|
287
|
+
const line = num(f.line);
|
|
288
|
+
const ev = f.evidence;
|
|
289
|
+
const level = typeof ev?.status === 'string' && PROOF_LEVELS.includes(ev.status) ? ev.status : 'suspected';
|
|
290
|
+
const evidenceNote = ev !== undefined && (str(ev.detail) !== undefined || (typeof ev.status === 'string' && level !== ev.status))
|
|
291
|
+
? `<p class="evidence">Evidence${typeof ev.status === 'string' && ev.status !== level ? ` (${esc(ev.status)})` : ''}: ${esc(ev.detail ?? '')}</p>`
|
|
292
|
+
: '';
|
|
293
|
+
const p = num(f.p);
|
|
294
|
+
const suggestion = str(f.suggestion);
|
|
295
|
+
const meta = [
|
|
296
|
+
str(f.category) !== undefined ? esc(f.category) : undefined,
|
|
297
|
+
p !== undefined ? `confidence <span class="data">${p.toFixed(2)}</span>` : undefined,
|
|
298
|
+
].filter(Boolean);
|
|
299
|
+
return `<li class="finding" id="finding-${i + 1}">
|
|
300
|
+
<div class="finding-head">${sev}${file !== undefined ? `<span class="loc">${fileLink(c, file, line)}${copyButton(`${file}${line !== undefined ? `:${line}` : ''}`, `Copy path ${file}`)}</span>` : ''}<span class="finding-proof">${proof(level)}</span></div>
|
|
301
|
+
<p class="msg">${prose(f.message ?? '')}</p>
|
|
302
|
+
${meta.length > 0 ? `<p class="meta">${meta.join(' · ')}</p>` : ''}
|
|
303
|
+
${evidenceNote}
|
|
304
|
+
${suggestion !== undefined ? `<details class="suggest"><summary class="control-ish">Suggested change</summary><pre class="diff">${suggestion.split('\n').map((l) => `<span class="add">+ ${esc(l)}</span>`).join('\n')}</pre></details>` : ''}
|
|
305
|
+
</li>`;
|
|
306
|
+
});
|
|
307
|
+
return `<section id="findings" aria-labelledby="findings-h">${head(findings.length)}<p class="sev">${summary}</p><ol class="findings">${items.join('\n')}</ol></section>`;
|
|
308
|
+
}
|
|
309
|
+
function stepsList(steps) {
|
|
310
|
+
if (steps.length === 0)
|
|
311
|
+
return '';
|
|
312
|
+
const items = steps.map((s) => {
|
|
313
|
+
const ok = s.ok === true;
|
|
314
|
+
const chips = [
|
|
315
|
+
s.healed === true ? '<span class="chip t-caution">healed</span>' : '',
|
|
316
|
+
str(s.model) !== undefined ? `<code class="dim">${esc(s.model)}</code>` : '',
|
|
317
|
+
].join('');
|
|
318
|
+
return `<li class="step">${glyph(ok ? 'status-passed' : 'status-failed', ok ? 'node t-passed' : 'node t-failed')}<div><p>${esc(s.instruction ?? '')}${chips !== '' ? ` ${chips}` : ''}</p>${str(s.action) !== undefined ? `<p class="act"><code>${esc(s.action)}</code></p>` : ''}${str(s.reason) !== undefined ? `<p class="why">${esc(s.reason)}</p>` : ''}</div></li>`;
|
|
319
|
+
});
|
|
320
|
+
return `<ol class="timeline" aria-label="Steps">${items.join('')}</ol>`;
|
|
321
|
+
}
|
|
322
|
+
function assertsList(asserts) {
|
|
323
|
+
if (asserts.length === 0)
|
|
324
|
+
return '';
|
|
325
|
+
const items = asserts.map((a) => {
|
|
326
|
+
const pass = a.verdict === 'pass';
|
|
327
|
+
return `<li class="step">${glyph(pass ? 'status-passed' : 'status-failed', pass ? 'node t-passed' : 'node t-failed')}<div><p>Check: ${esc(a.question ?? '')}${a.cached === true ? ' <span class="chip">cached</span>' : ''}</p>${str(a.reasoning) !== undefined ? `<p class="why">${esc(a.reasoning)}</p>` : ''}</div></li>`;
|
|
328
|
+
});
|
|
329
|
+
return `<ol class="timeline" aria-label="Checks">${items.join('')}</ol>`;
|
|
330
|
+
}
|
|
331
|
+
function flowSection(c) {
|
|
332
|
+
const h = '<h2 id="flow-h">Flow</h2>';
|
|
333
|
+
const wrap = (body) => `<section id="flow" aria-labelledby="flow-h">${h}${body}</section>`;
|
|
334
|
+
if (c.missing.includes('flow')) {
|
|
335
|
+
return wrap(`<p class="lane-line">${status('unavailable', 'unavailable: evidence missing')}</p><p class="note">The manifest has no readable flow lane record, so this run's flow result is unknown.</p>`);
|
|
336
|
+
}
|
|
337
|
+
const lane = c.manifest?.lanes.flow;
|
|
338
|
+
if (lane !== undefined && !lane.selected) {
|
|
339
|
+
return wrap(`<p class="note">${status('skipped')} Not selected. Run <code>argus-reviewer verify --flow</code> to replay recorded flows.</p>`);
|
|
340
|
+
}
|
|
341
|
+
const head = lane !== undefined ? `<p class="lane-line">${status(lane.status)}<span>${esc(lane.reason ?? lane.summary ?? '')}</span></p>` : '';
|
|
342
|
+
if (c.run === undefined) {
|
|
343
|
+
return wrap(`${head}<p class="note">Step timeline unavailable: no <code>run.json</code> from the flow lane is attached to this run.</p>`);
|
|
344
|
+
}
|
|
345
|
+
const t = c.run.totals ?? {};
|
|
346
|
+
const tests = arr(c.run.tests);
|
|
347
|
+
const cache = lane?.cache;
|
|
348
|
+
const nTests = num(t.tests);
|
|
349
|
+
const nCalls = num(t.visionCalls);
|
|
350
|
+
const facts = [
|
|
351
|
+
nTests !== undefined ? `${num(t.passed) ?? 0} of ${plural(nTests, 'test')} passed` : undefined,
|
|
352
|
+
nCalls !== undefined ? plural(nCalls, 'vision call') : undefined,
|
|
353
|
+
cache !== undefined ? `cache ${cache.hits} hit${cache.hits === 1 ? '' : 's'}, ${cache.misses} miss${cache.misses === 1 ? '' : 'es'}, ${plural(cache.heals, 'heal')}` : undefined,
|
|
354
|
+
formatDuration(num(c.run.durationMs)),
|
|
355
|
+
].filter(Boolean);
|
|
356
|
+
const items = tests.map((test, i) => {
|
|
357
|
+
const ok = test.ok === true;
|
|
358
|
+
const steps = arr(test.steps);
|
|
359
|
+
const asserts = arr(test.asserts);
|
|
360
|
+
const video = str(test.videoPath);
|
|
361
|
+
const dur = formatDuration(num(test.durationMs));
|
|
362
|
+
const body = stepsList(steps) + assertsList(asserts);
|
|
363
|
+
return `<li class="test" id="test-${i + 1}">
|
|
364
|
+
<div class="test-head">${status(ok ? 'passed' : 'failed')}<span class="test-name">${esc(test.name ?? `test ${i + 1}`)}</span><code class="dim">${esc(test.file ?? '')}</code>${dur !== undefined ? `<span class="data dim">${dur}</span>` : ''}</div>
|
|
365
|
+
${str(test.failureMessage) !== undefined ? `<p class="why t-failed">${esc(test.failureMessage)}</p>` : ''}
|
|
366
|
+
${body !== '' ? body : '<p class="note">No step records: every step replayed from the cache or the test had none.</p>'}
|
|
367
|
+
${video !== undefined ? `<p class="meta">Video in the artifact: <code>${esc(video)}</code></p>` : ''}
|
|
368
|
+
</li>`;
|
|
369
|
+
});
|
|
370
|
+
return wrap(`${head}${facts.length > 0 ? `<p class="meta data">${facts.join(' · ')}</p>` : ''}${items.length > 0 ? `<ol class="tests">${items.join('\n')}</ol>` : '<p class="note">The flow lane ran no tests.</p>'}`);
|
|
371
|
+
}
|
|
372
|
+
function healsSection(c) {
|
|
373
|
+
const h = (n) => `<h2 id="heals-h">Heals${n !== undefined ? ` <span class="count">${n}</span>` : ''}</h2>`;
|
|
374
|
+
const wrap = (n, body) => `<section id="heals" aria-labelledby="heals-h">${h(n)}${body}</section>`;
|
|
375
|
+
if (c.run === undefined) {
|
|
376
|
+
return wrap(undefined, emptyState('nothing-to-heal', 'Heals come from the flow lane, and it left no record in this run.', 'argus-reviewer verify --flow'));
|
|
377
|
+
}
|
|
378
|
+
const heals = arr(c.run.tests).flatMap((t) => arr(t.healEvents).map((e) => ({ test: t.name, ...e })));
|
|
379
|
+
if (heals.length === 0) {
|
|
380
|
+
return wrap(0, emptyState('nothing-to-heal', 'Nothing to heal: every cached target matched.', 'argus-reviewer cache list'));
|
|
381
|
+
}
|
|
382
|
+
const items = heals.map((e) => `<li class="heal">${glyph('status-inconclusive', 't-caution')}<div><p><strong>${esc(e.instruction ?? '')}</strong></p><p class="meta">in ${esc(e.test ?? 'a test')}${str(e.model) !== undefined ? ` · re-grounded by <code>${esc(e.model)}</code>` : ''}</p></div></li>`);
|
|
383
|
+
return wrap(heals.length, `<p class="note">A heal means a cached target stopped matching and the model found it again. Review each before merging: the cache now points at the new target.</p><ul class="heals">${items.join('')}</ul>`);
|
|
384
|
+
}
|
|
385
|
+
/** 20 ticks, no filled track; the text always says the numbers (DESIGN.md 6.8). */
|
|
386
|
+
function tally(spent, limit, exceeded) {
|
|
387
|
+
if (limit === undefined || limit <= 0)
|
|
388
|
+
return `<span class="data">${formatUsd(spent)}</span> <span class="dim">no cap</span>`;
|
|
389
|
+
const ratio = spent / limit;
|
|
390
|
+
const tone = exceeded || ratio > 1 ? 'failed' : ratio >= 0.8 ? 'caution' : 'plain';
|
|
391
|
+
const filled = Math.min(20, Math.round(ratio * 20));
|
|
392
|
+
const ticks = Array.from({ length: 20 }, (_, i) => `<i${i < filled ? ' class="on"' : ''}></i>`).join('');
|
|
393
|
+
return `<span class="tally t-${tone}"><span class="ticks" aria-hidden="true">${ticks}</span><span class="data">spent ${formatUsd(spent)} of ${usdCap(limit)}</span>${exceeded ? ' <strong class="t-failed">exceeded</strong>' : ''}</span>`;
|
|
394
|
+
}
|
|
395
|
+
function ledgerRows(c) {
|
|
396
|
+
const m = c.manifest;
|
|
397
|
+
if (m === undefined)
|
|
398
|
+
return [];
|
|
399
|
+
return LANE_IDS.filter((id) => !c.missing.includes(id) && m.lanes[id].selected).map((id) => {
|
|
400
|
+
const l = m.lanes[id];
|
|
401
|
+
const metered = l.usage.metered;
|
|
402
|
+
return {
|
|
403
|
+
lane: id,
|
|
404
|
+
model: l.model ?? l.usage.model ?? '',
|
|
405
|
+
calls: l.usage.calls,
|
|
406
|
+
tokens: l.usage.tokens,
|
|
407
|
+
spend: metered ? formatUsd(l.usage.costUsd) : 'unmetered',
|
|
408
|
+
budget: metered ? tally(l.usage.costUsd, l.budget.limitUsd ?? undefined, l.budget.exceeded) : '<span class="dim">unmetered</span>',
|
|
409
|
+
exceeded: l.budget.exceeded === true,
|
|
410
|
+
};
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
function byModel(c) {
|
|
414
|
+
const rows = new Map();
|
|
415
|
+
const add = (model, calls, cost) => {
|
|
416
|
+
const r = rows.get(model) ?? [0, 0];
|
|
417
|
+
rows.set(model, [r[0] + calls, r[1] + cost]);
|
|
418
|
+
};
|
|
419
|
+
for (const call of arr(c.crRaw?.calls))
|
|
420
|
+
add(String(call.model ?? 'unknown'), 1, num(call.costUsd) ?? 0);
|
|
421
|
+
const t = c.run?.totals;
|
|
422
|
+
if (isObj(t?.callsByModel)) {
|
|
423
|
+
for (const [model, calls] of Object.entries(t.callsByModel)) {
|
|
424
|
+
add(model, num(calls) ?? 0, isObj(t.costByModel) ? (num(t.costByModel[model]) ?? 0) : 0);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return [...rows.entries()].map(([k, [n, cost]]) => [k, n, cost]).sort((a, b) => b[2] - a[2] || a[0].localeCompare(b[0]));
|
|
428
|
+
}
|
|
429
|
+
function ledgerSection(c) {
|
|
430
|
+
const h = '<h2 id="ledger-h">Spend ledger</h2>';
|
|
431
|
+
const m = c.manifest;
|
|
432
|
+
if (m === undefined) {
|
|
433
|
+
return `<section id="ledger" aria-labelledby="ledger-h">${h}<p class="note">Spend is recorded in the manifest, which this run does not have in readable form.</p></section>`;
|
|
434
|
+
}
|
|
435
|
+
const rows = ledgerRows(c);
|
|
436
|
+
const body = rows
|
|
437
|
+
.map((r) => `<tr><th scope="row">${glyph(`lane-${r.lane}`, 'lg')}${r.lane}</th><td><code>${esc(r.model)}</code></td><td class="num">${r.calls}</td><td class="num">${r.tokens.toLocaleString('en-US')}</td><td class="num">${r.spend}</td><td>${r.budget}</td></tr>`)
|
|
438
|
+
.join('');
|
|
439
|
+
const models = byModel(c);
|
|
440
|
+
const over = rows.filter((r) => r.exceeded);
|
|
441
|
+
return `<section id="ledger" aria-labelledby="ledger-h">${h}
|
|
442
|
+
<div class="scroll"><table class="ledger"><thead><tr><th scope="col">Lane</th><th scope="col">Model</th><th scope="col" class="num">Calls</th><th scope="col" class="num">Tokens</th><th scope="col" class="num">Spend</th><th scope="col">Budget</th></tr></thead>
|
|
443
|
+
<tbody>${body}</tbody>
|
|
444
|
+
<tfoot><tr><th scope="row">Total</th><td></td><td class="num">${m.aggregate.calls}</td><td class="num">${m.aggregate.tokens.toLocaleString('en-US')}</td><td class="num">${usdTotal(m.aggregate.costUsd)}</td><td></td></tr></tfoot></table></div>
|
|
445
|
+
${over.length > 0 ? `<p class="banner t-failed">${glyph('status-failed')}<span>Budget exceeded: ${over.map((r) => r.lane).join(', ')}</span></p>` : ''}
|
|
446
|
+
${models.length > 0
|
|
447
|
+
? `<h3>By model</h3><div class="scroll"><table class="ledger"><thead><tr><th scope="col">Model</th><th scope="col" class="num">Calls</th><th scope="col" class="num">Spend</th></tr></thead><tbody>${models
|
|
448
|
+
.map(([k, n, cost]) => `<tr><td><code>${esc(k)}</code></td><td class="num">${n}</td><td class="num">${formatUsd(cost)}</td></tr>`)
|
|
449
|
+
.join('')}</tbody></table></div>`
|
|
450
|
+
: ''}
|
|
451
|
+
</section>`;
|
|
452
|
+
}
|
|
453
|
+
function rail(c) {
|
|
454
|
+
const links = [
|
|
455
|
+
['verdict', 'Verdict'],
|
|
456
|
+
['lanes', 'Lanes'],
|
|
457
|
+
['findings', 'Findings'],
|
|
458
|
+
['flow', 'Flow'],
|
|
459
|
+
['heals', 'Heals'],
|
|
460
|
+
['ledger', 'Spend ledger'],
|
|
461
|
+
];
|
|
462
|
+
const m = c.manifest;
|
|
463
|
+
const spend = m !== undefined
|
|
464
|
+
? `<div class="rail-spend"><p class="cap">Total spend</p><p class="fig">${usdTotal(m.aggregate.costUsd)}</p><p class="dim data">${plural(m.aggregate.calls, 'call')}</p></div>`
|
|
465
|
+
: '';
|
|
466
|
+
return `<aside class="rail control" aria-label="Report navigation"><nav class="control" aria-label="Sections"><ul>${links
|
|
467
|
+
.map(([id, label]) => `<li><a href="#${id}">${label}</a></li>`)
|
|
468
|
+
.join('')}</ul></nav>${spend}</aside>`;
|
|
469
|
+
}
|
|
470
|
+
// ---- Styles -----------------------------------------------------------------
|
|
471
|
+
/** Light color tokens, re-applied for print so paper is always light. */
|
|
472
|
+
const LIGHT_COLORS = (/:root\s*\{([^}]*)\}/.exec(TOKENS_CSS)?.[1] ?? '')
|
|
473
|
+
.split('\n')
|
|
474
|
+
.filter((l) => l.includes('--argus-color-'))
|
|
475
|
+
.map((l) => l.replace(/\/\*.*\*\//, '').trim())
|
|
476
|
+
.join('\n ');
|
|
477
|
+
const CSS = `
|
|
478
|
+
:root {
|
|
479
|
+
--canvas: var(--argus-color-canvas);
|
|
480
|
+
--surface: var(--argus-color-surface);
|
|
481
|
+
--sunk: var(--argus-color-surface-sunk);
|
|
482
|
+
--hairline: var(--argus-color-hairline);
|
|
483
|
+
--control: var(--argus-color-control-border);
|
|
484
|
+
--ink: var(--argus-color-ink);
|
|
485
|
+
--ink-2: var(--argus-color-ink-2);
|
|
486
|
+
--ink-3: var(--argus-color-ink-3);
|
|
487
|
+
--accent: var(--argus-color-accent);
|
|
488
|
+
--passed: var(--argus-color-passed);
|
|
489
|
+
--failed: var(--argus-color-failed);
|
|
490
|
+
--caution: var(--argus-color-caution);
|
|
491
|
+
--passed-tint: var(--argus-color-passed-tint);
|
|
492
|
+
--failed-tint: var(--argus-color-failed-tint);
|
|
493
|
+
--caution-tint: var(--argus-color-caution-tint);
|
|
494
|
+
--sans: var(--argus-font-sans);
|
|
495
|
+
--mono: var(--argus-font-mono);
|
|
496
|
+
color-scheme: light;
|
|
497
|
+
}
|
|
498
|
+
@media (prefers-color-scheme: dark) {
|
|
499
|
+
:root { color-scheme: dark; }
|
|
500
|
+
.art-light { display: none; }
|
|
501
|
+
}
|
|
502
|
+
@media (prefers-color-scheme: light) {
|
|
503
|
+
.art-dark { display: none; }
|
|
504
|
+
}
|
|
505
|
+
* { box-sizing: border-box; }
|
|
506
|
+
html { -webkit-text-size-adjust: 100%; }
|
|
507
|
+
body {
|
|
508
|
+
margin: 0;
|
|
509
|
+
background: var(--canvas);
|
|
510
|
+
color: var(--ink);
|
|
511
|
+
font: 400 14px/21px var(--sans);
|
|
512
|
+
-webkit-font-smoothing: antialiased;
|
|
513
|
+
overflow-x: hidden;
|
|
514
|
+
}
|
|
515
|
+
h1, h2, h3, p, dl, dd, ol, ul, figure { margin: 0; }
|
|
516
|
+
ol, ul { padding: 0; list-style: none; }
|
|
517
|
+
code, .data, .num, .fig, pre {
|
|
518
|
+
font-family: var(--mono);
|
|
519
|
+
font-stretch: 87.5%;
|
|
520
|
+
font-weight: 450;
|
|
521
|
+
font-feature-settings: 'tnum', 'zero';
|
|
522
|
+
/* Code is quoted, never typeset: <= must not become a ligature. */
|
|
523
|
+
font-variant-ligatures: none;
|
|
524
|
+
}
|
|
525
|
+
code, .data { font-size: 13px; line-height: 19px; }
|
|
526
|
+
code { overflow-wrap: anywhere; }
|
|
527
|
+
a { color: var(--accent); text-underline-offset: 2px; }
|
|
528
|
+
a:focus-visible, button:focus-visible, summary:focus-visible {
|
|
529
|
+
outline: 2px solid var(--accent);
|
|
530
|
+
outline-offset: 2px;
|
|
531
|
+
border-radius: var(--argus-radius-xs);
|
|
532
|
+
}
|
|
533
|
+
.dim { color: var(--ink-3); }
|
|
534
|
+
.t-passed { color: var(--passed); }
|
|
535
|
+
.t-failed { color: var(--failed); }
|
|
536
|
+
.t-caution { color: var(--caution); }
|
|
537
|
+
.t-ink { color: var(--ink); }
|
|
538
|
+
.t-muted { color: var(--ink-3); }
|
|
539
|
+
.t-plain { color: var(--ink-2); }
|
|
540
|
+
.tg { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: pre; }
|
|
541
|
+
.g { width: 1em; height: 1em; flex: none; vertical-align: -0.125em; }
|
|
542
|
+
.g.lg { width: 16px; height: 16px; color: var(--ink-3); margin-right: 6px; }
|
|
543
|
+
.skip {
|
|
544
|
+
position: absolute; left: 16px; top: -48px; padding: 4px 8px; background: var(--surface);
|
|
545
|
+
border: 1px solid var(--control); border-radius: var(--argus-radius-sm); z-index: 2;
|
|
546
|
+
}
|
|
547
|
+
.skip:focus { top: 8px; }
|
|
548
|
+
|
|
549
|
+
.page {
|
|
550
|
+
display: grid;
|
|
551
|
+
grid-template-columns: minmax(0, 880px);
|
|
552
|
+
justify-content: center;
|
|
553
|
+
column-gap: 48px;
|
|
554
|
+
padding: 0 16px;
|
|
555
|
+
}
|
|
556
|
+
.masthead, .foot { grid-column: 1 / -1; }
|
|
557
|
+
.masthead {
|
|
558
|
+
display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px;
|
|
559
|
+
justify-content: space-between;
|
|
560
|
+
padding: 24px 0 16px;
|
|
561
|
+
border-bottom: 1px solid var(--hairline);
|
|
562
|
+
}
|
|
563
|
+
.brand { display: flex; align-items: center; gap: 8px; font: 600 15px/22px var(--sans); }
|
|
564
|
+
.brand svg { width: 24px; height: 24px; color: var(--accent); }
|
|
565
|
+
.brand .dim { font-weight: 400; }
|
|
566
|
+
.runid { display: flex; align-items: center; gap: 4px; color: var(--ink-3); min-width: 0; }
|
|
567
|
+
.runid code { font-size: 12px; line-height: 16px; }
|
|
568
|
+
main { min-width: 0; }
|
|
569
|
+
section { padding: 32px 0; border-bottom: 1px solid var(--hairline); }
|
|
570
|
+
section:last-child { border-bottom: 0; }
|
|
571
|
+
h2 { font: 600 15px/22px var(--sans); margin-bottom: 16px; display: flex; align-items: baseline; gap: 8px; }
|
|
572
|
+
h3 { font: 600 13px/19px var(--sans); color: var(--ink-2); margin: 24px 0 8px; }
|
|
573
|
+
.count { font: 450 13px/19px var(--mono); font-stretch: 87.5%; color: var(--ink-3); }
|
|
574
|
+
.note { color: var(--ink-2); }
|
|
575
|
+
.note .st { margin-right: 8px; }
|
|
576
|
+
.meta { color: var(--ink-3); font-size: 13px; line-height: 19px; }
|
|
577
|
+
|
|
578
|
+
.verdict { padding-top: 32px; }
|
|
579
|
+
.headline { display: flex; align-items: center; gap: 12px; font: 650 36px/40px var(--sans); letter-spacing: -0.015em; }
|
|
580
|
+
.headline span { color: var(--ink); }
|
|
581
|
+
.headline .big { width: 32px; height: 32px; }
|
|
582
|
+
.lead { margin-top: 12px; font: 400 17px/26px var(--sans); color: var(--ink-2); text-wrap: pretty; }
|
|
583
|
+
.lead strong { color: var(--ink); font-weight: 600; }
|
|
584
|
+
.verdict .kv { margin-top: 24px; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
|
|
585
|
+
.kv { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px 24px; }
|
|
586
|
+
.kv > div { min-width: 0; }
|
|
587
|
+
.kv dt { font: 500 12px/16px var(--sans); color: var(--ink-3); margin-bottom: 2px; }
|
|
588
|
+
.kv dd { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; min-width: 0; color: var(--ink-2); }
|
|
589
|
+
.verdict .kv dd { color: var(--ink); }
|
|
590
|
+
.fig { font-size: 20px; line-height: 28px; font-weight: 500; font-stretch: 100%; }
|
|
591
|
+
.of { color: var(--ink-3); font: 450 13px/19px var(--mono); font-stretch: 87.5%; }
|
|
592
|
+
.bind { font: 500 12px/16px var(--sans); padding: 0 6px; border-radius: var(--argus-radius-xs); border: 1px solid var(--hairline); color: var(--ink-2); }
|
|
593
|
+
.bind.b-caution { color: var(--caution); border-color: currentColor; }
|
|
594
|
+
.banner {
|
|
595
|
+
display: flex; gap: 8px; align-items: baseline; margin-top: 16px; padding: 8px 12px;
|
|
596
|
+
border: 1px solid currentColor; border-radius: var(--argus-radius-sm);
|
|
597
|
+
}
|
|
598
|
+
.banner span { color: var(--ink); }
|
|
599
|
+
|
|
600
|
+
.st { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
|
|
601
|
+
.st > span { color: var(--ink); }
|
|
602
|
+
.t-muted.st > span { color: var(--ink-3); }
|
|
603
|
+
.proof { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; color: var(--ink-2); font-size: 13px; }
|
|
604
|
+
.proof .meter { width: 24px; height: 24px; margin: -4px 0; color: var(--ink-2); }
|
|
605
|
+
.proof.p4 .meter { color: var(--ink); }
|
|
606
|
+
.proof.p0 .meter { color: var(--ink-3); }
|
|
607
|
+
|
|
608
|
+
.lanes > li { padding: 12px 0; border-top: 1px solid var(--hairline); }
|
|
609
|
+
.lanes > li:first-child { border-top: 0; padding-top: 0; }
|
|
610
|
+
.lane-head {
|
|
611
|
+
display: grid; align-items: center; column-gap: 16px; row-gap: 4px;
|
|
612
|
+
grid-template-columns: 128px 72px minmax(0, 1fr) auto 96px;
|
|
613
|
+
}
|
|
614
|
+
.lane-name { display: inline-flex; align-items: center; font-weight: 600; }
|
|
615
|
+
.lane-result { color: var(--ink-2); min-width: 0; overflow-wrap: anywhere; }
|
|
616
|
+
.lane-spend { text-align: right; color: var(--ink-2); }
|
|
617
|
+
.lane.off .lane-name, .lane.off .lane-result { color: var(--ink-3); }
|
|
618
|
+
.lane-facts, .lane-note { margin: 4px 0 0 144px; color: var(--ink-3); font-size: 13px; line-height: 19px; }
|
|
619
|
+
.lane-facts { display: flex; flex-wrap: wrap; gap: 0 16px; }
|
|
620
|
+
.lane-facts code, .lane-facts .data { color: var(--ink-2); font-size: 12px; line-height: 19px; }
|
|
621
|
+
.lane-note { color: var(--ink-2); }
|
|
622
|
+
|
|
623
|
+
.sev { display: flex; flex-wrap: wrap; gap: 4px 16px; margin-bottom: 16px; }
|
|
624
|
+
.findings > li { padding: 16px 0; border-top: 1px solid var(--hairline); }
|
|
625
|
+
.finding-head { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; }
|
|
626
|
+
.finding-head .st { min-width: 72px; }
|
|
627
|
+
.loc { display: inline-flex; align-items: center; gap: 2px; min-width: 0; }
|
|
628
|
+
.finding-proof { margin-left: auto; }
|
|
629
|
+
.msg code { background: var(--sunk); padding: 0 4px; border-radius: var(--argus-radius-xs); }
|
|
630
|
+
.msg { margin-top: 8px; color: var(--ink); max-width: 72ch; text-wrap: pretty; }
|
|
631
|
+
.finding .meta, .evidence { margin-top: 4px; }
|
|
632
|
+
.evidence { color: var(--ink-2); font-size: 13px; line-height: 19px; }
|
|
633
|
+
.suggest { margin-top: 12px; }
|
|
634
|
+
.suggest summary {
|
|
635
|
+
cursor: pointer; display: inline-flex; align-items: center; min-height: 24px;
|
|
636
|
+
color: var(--accent); font-weight: 500;
|
|
637
|
+
}
|
|
638
|
+
.diff {
|
|
639
|
+
margin: 8px 0 0; padding: 12px; overflow-x: auto; background: var(--sunk);
|
|
640
|
+
border-radius: var(--argus-radius-md); font-size: 12px; line-height: 18px;
|
|
641
|
+
}
|
|
642
|
+
.diff .add { color: var(--passed); display: block; white-space: pre; }
|
|
643
|
+
|
|
644
|
+
.lane-line { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline; margin-bottom: 4px; }
|
|
645
|
+
.lane-line > span:last-child { color: var(--ink-2); }
|
|
646
|
+
.tests { margin-top: 16px; }
|
|
647
|
+
.tests > li { padding: 16px 0; border-top: 1px solid var(--hairline); }
|
|
648
|
+
.test-head { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; }
|
|
649
|
+
.test-name { font-weight: 600; }
|
|
650
|
+
.timeline { margin: 12px 0 0 7px; border-left: 1px solid var(--hairline); }
|
|
651
|
+
.timeline .step { position: relative; display: flex; gap: 12px; padding: 0 0 12px 0; margin-left: -8px; }
|
|
652
|
+
.timeline .step:last-child { padding-bottom: 0; }
|
|
653
|
+
.timeline .node { width: 16px; height: 16px; margin-top: 2px; background: var(--canvas); border-radius: 50%; }
|
|
654
|
+
.timeline .act code { color: var(--ink-2); font-size: 12px; line-height: 16px; }
|
|
655
|
+
.why { color: var(--ink-2); font-size: 13px; line-height: 19px; }
|
|
656
|
+
.step p .chip + code, .step p code.dim { margin-left: 6px; }
|
|
657
|
+
.chip {
|
|
658
|
+
display: inline-block; font: 500 12px/16px var(--sans); padding: 0 6px; border: 1px solid currentColor;
|
|
659
|
+
border-radius: var(--argus-radius-xs); margin-left: 4px; color: var(--ink-3);
|
|
660
|
+
}
|
|
661
|
+
.chip.t-caution { color: var(--caution); }
|
|
662
|
+
|
|
663
|
+
.heals > li { display: flex; gap: 12px; padding: 12px 0; border-top: 1px solid var(--hairline); }
|
|
664
|
+
.heals .g { margin-top: 3px; }
|
|
665
|
+
.heals > li:first-child { margin-top: 12px; }
|
|
666
|
+
|
|
667
|
+
.empty { display: grid; justify-items: start; gap: 12px; padding: 8px 0; color: var(--ink-2); }
|
|
668
|
+
.empty .art svg { width: 160px; height: 120px; display: block; }
|
|
669
|
+
.cmd { display: inline-flex; align-items: center; gap: 4px; padding: 2px 4px 2px 10px; background: var(--sunk); border-radius: var(--argus-radius-sm); }
|
|
670
|
+
|
|
671
|
+
.scroll { overflow-x: auto; }
|
|
672
|
+
.ledger { width: 100%; border-collapse: collapse; font-size: 13px; line-height: 19px; }
|
|
673
|
+
.ledger th, .ledger td { text-align: left; padding: 6px 12px 6px 0; border-bottom: 1px solid var(--hairline); white-space: nowrap; vertical-align: middle; }
|
|
674
|
+
.ledger thead th { font: 500 12px/16px var(--sans); color: var(--ink-3); }
|
|
675
|
+
.ledger tbody th { font-weight: 600; }
|
|
676
|
+
.ledger tfoot th, .ledger tfoot td { border-bottom: 0; font-weight: 600; }
|
|
677
|
+
.ledger .num { text-align: right; }
|
|
678
|
+
.ledger code { font-size: 12px; line-height: 16px; }
|
|
679
|
+
.tally { display: inline-flex; align-items: center; gap: 8px; }
|
|
680
|
+
.ticks { display: inline-flex; gap: 2px; }
|
|
681
|
+
.ticks i { width: 3px; height: 10px; border-radius: 1px; box-shadow: inset 0 0 0 1px var(--hairline); }
|
|
682
|
+
.ticks i.on { background: currentColor; box-shadow: none; }
|
|
683
|
+
.tally .data { color: var(--ink-2); font-size: 12px; line-height: 16px; }
|
|
684
|
+
|
|
685
|
+
button.copy {
|
|
686
|
+
display: none; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0;
|
|
687
|
+
border: 0; background: none; color: var(--ink-3); border-radius: var(--argus-radius-sm); cursor: pointer;
|
|
688
|
+
}
|
|
689
|
+
.js button.copy { display: inline-flex; }
|
|
690
|
+
button.copy:hover { color: var(--ink); background: var(--sunk); }
|
|
691
|
+
button.copy .g { width: 16px; height: 16px; }
|
|
692
|
+
button.copy .i-check, button.copy[data-done] .i-copy { display: none; }
|
|
693
|
+
button.copy[data-done] .i-check { display: block; color: var(--passed); }
|
|
694
|
+
|
|
695
|
+
.rail { display: none; }
|
|
696
|
+
.foot {
|
|
697
|
+
display: flex; flex-wrap: wrap; gap: 4px 12px; padding: 24px 0 48px; border-top: 1px solid var(--hairline);
|
|
698
|
+
color: var(--ink-3); font-size: 12px; line-height: 16px;
|
|
699
|
+
}
|
|
700
|
+
.foot a { color: var(--ink-2); }
|
|
701
|
+
|
|
702
|
+
@media (min-width: 1280px) {
|
|
703
|
+
.page { grid-template-columns: minmax(0, 880px) 200px; }
|
|
704
|
+
.rail { display: block; }
|
|
705
|
+
.rail > * { position: sticky; top: 24px; }
|
|
706
|
+
.rail { padding-top: 32px; }
|
|
707
|
+
.rail nav ul { display: grid; gap: 4px; }
|
|
708
|
+
.rail nav a { color: var(--ink-2); text-decoration: none; display: block; padding: 2px 0; }
|
|
709
|
+
.rail nav a:hover { color: var(--accent); }
|
|
710
|
+
.rail-spend { margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--hairline); top: 200px; }
|
|
711
|
+
.cap { font: 500 12px/16px var(--sans); color: var(--ink-3); }
|
|
712
|
+
}
|
|
713
|
+
@media (max-width: 640px) {
|
|
714
|
+
.headline { font-size: 28px; line-height: 32px; }
|
|
715
|
+
.headline .big { width: 26px; height: 26px; }
|
|
716
|
+
.lead { font-size: 15px; line-height: 22px; }
|
|
717
|
+
.lane-head { grid-template-columns: auto minmax(0, 1fr) auto; }
|
|
718
|
+
.lane-head .st { grid-column: 1; }
|
|
719
|
+
.lane-name { grid-column: 2; }
|
|
720
|
+
.lane-spend { grid-column: 3; }
|
|
721
|
+
.lane-result { grid-column: 1 / -1; }
|
|
722
|
+
.lane-proof { grid-column: 1 / -1; }
|
|
723
|
+
.lane-facts, .lane-note { margin-left: 0; }
|
|
724
|
+
.lane-spend { grid-row: 1; }
|
|
725
|
+
.lane-head .st, .lane-name { grid-row: 1; }
|
|
726
|
+
.finding-proof { margin-left: 0; }
|
|
727
|
+
}
|
|
728
|
+
@media print {
|
|
729
|
+
:root {
|
|
730
|
+
${LIGHT_COLORS}
|
|
731
|
+
color-scheme: light;
|
|
732
|
+
}
|
|
733
|
+
.control, .skip, button.copy {
|
|
734
|
+
display: none !important;
|
|
735
|
+
}
|
|
736
|
+
.art-dark {
|
|
737
|
+
display: none !important;
|
|
738
|
+
}
|
|
739
|
+
.art-light {
|
|
740
|
+
display: block !important;
|
|
741
|
+
}
|
|
742
|
+
body {
|
|
743
|
+
background: #fff;
|
|
744
|
+
font-size: 11pt;
|
|
745
|
+
}
|
|
746
|
+
.page {
|
|
747
|
+
display: block;
|
|
748
|
+
padding: 0;
|
|
749
|
+
}
|
|
750
|
+
section {
|
|
751
|
+
padding: 16px 0;
|
|
752
|
+
}
|
|
753
|
+
li, .kv, .banner, table {
|
|
754
|
+
break-inside: avoid;
|
|
755
|
+
}
|
|
756
|
+
h2, h3 {
|
|
757
|
+
break-after: avoid;
|
|
758
|
+
}
|
|
759
|
+
.scroll {
|
|
760
|
+
overflow: visible;
|
|
761
|
+
}
|
|
762
|
+
.ledger td {
|
|
763
|
+
white-space: normal;
|
|
764
|
+
}
|
|
765
|
+
.ticks {
|
|
766
|
+
display: none;
|
|
767
|
+
}
|
|
768
|
+
details::details-content {
|
|
769
|
+
content-visibility: visible;
|
|
770
|
+
display: block;
|
|
771
|
+
}
|
|
772
|
+
.suggest summary {
|
|
773
|
+
color: var(--ink-2);
|
|
774
|
+
}
|
|
775
|
+
a {
|
|
776
|
+
color: inherit;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
`;
|
|
780
|
+
/** Copy buttons only: the report reads fully without script. */
|
|
781
|
+
const SCRIPT = `document.documentElement.classList.add('js');
|
|
782
|
+
document.addEventListener('click', function (e) {
|
|
783
|
+
var b = e.target.closest && e.target.closest('button[data-copy]');
|
|
784
|
+
if (!b || !navigator.clipboard) return;
|
|
785
|
+
navigator.clipboard.writeText(b.getAttribute('data-copy')).then(function () {
|
|
786
|
+
b.setAttribute('data-done', '');
|
|
787
|
+
setTimeout(function () { b.removeAttribute('data-done'); }, 1200);
|
|
788
|
+
});
|
|
789
|
+
});`;
|
|
790
|
+
// ---- Document ---------------------------------------------------------------
|
|
791
|
+
export function renderReportHtml(input) {
|
|
792
|
+
const ms = readManifest(input.manifestText);
|
|
793
|
+
const manifest = ms.state === 'ok' ? ms.manifest : undefined;
|
|
794
|
+
const missing = ms.state === 'ok' ? ms.missing : [];
|
|
795
|
+
const crRaw = isObj(input.codeReview) ? input.codeReview : undefined;
|
|
796
|
+
const cr = crRaw;
|
|
797
|
+
const run = isObj(input.run) ? input.run : undefined;
|
|
798
|
+
const views = {};
|
|
799
|
+
const rows = [];
|
|
800
|
+
if (manifest !== undefined) {
|
|
801
|
+
for (const id of LANE_IDS) {
|
|
802
|
+
if (missing.includes(id)) {
|
|
803
|
+
rows.push(MISSING_ROW(id));
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
const view = laneView(manifest.lanes[id]);
|
|
807
|
+
views[id] = view;
|
|
808
|
+
rows.push(manifestRow(view, cr));
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
else if (cr !== undefined && cr.skipped !== true) {
|
|
812
|
+
rows.push({ lane: 'review', status: cr.ok === true ? 'passed' : 'failed', result: '', proof: laneProof('review', cr.ok === true ? 'passed' : 'failed', cr), spend: '' });
|
|
813
|
+
}
|
|
814
|
+
const crHead = isObj(crRaw?.headBinding) ? str(crRaw.headBinding.intendedSha) : undefined;
|
|
815
|
+
const c = {
|
|
816
|
+
ms,
|
|
817
|
+
manifest,
|
|
818
|
+
missing,
|
|
819
|
+
views,
|
|
820
|
+
rows,
|
|
821
|
+
cr,
|
|
822
|
+
crRaw,
|
|
823
|
+
run,
|
|
824
|
+
repo: manifest?.identity.repo,
|
|
825
|
+
head: manifest?.identity.intendedHeadSha ?? manifest?.identity.checkoutSha ?? crHead,
|
|
826
|
+
};
|
|
827
|
+
const runId = manifest?.runId;
|
|
828
|
+
const titleBits = ['Argus evidence report'];
|
|
829
|
+
if (runId !== undefined)
|
|
830
|
+
titleBits.push(runId);
|
|
831
|
+
const runLink = input.runUrl !== undefined && SAFE_URL.test(input.runUrl)
|
|
832
|
+
? `<a href="${esc(input.runUrl)}">workflow run and artifacts</a>`
|
|
833
|
+
: undefined;
|
|
834
|
+
const started = manifest !== undefined ? Date.parse(manifest.startedAt) : NaN;
|
|
835
|
+
return `<!doctype html>
|
|
836
|
+
<html lang="en">
|
|
837
|
+
<head>
|
|
838
|
+
<meta charset="utf-8">
|
|
839
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
840
|
+
<meta name="color-scheme" content="light dark">
|
|
841
|
+
<meta name="referrer" content="no-referrer">
|
|
842
|
+
<title>${esc(titleBits.join(': '))}</title>
|
|
843
|
+
<style>
|
|
844
|
+
${TOKENS_CSS}
|
|
845
|
+
${FONT_FACES_CSS}
|
|
846
|
+
${CSS}
|
|
847
|
+
</style>
|
|
848
|
+
</head>
|
|
849
|
+
<body>
|
|
850
|
+
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">${SPRITE_SYMBOLS}</svg>
|
|
851
|
+
<a class="skip control" href="#main">Skip to report</a>
|
|
852
|
+
<div class="page">
|
|
853
|
+
<header class="masthead">
|
|
854
|
+
<p class="brand">${MARK_SVG.replace('<svg', '<svg aria-hidden="true" focusable="false"')}<span>Argus</span><span class="dim">evidence report</span></p>
|
|
855
|
+
${runId !== undefined ? `<p class="runid"><span>run</span> <code>${esc(runId)}</code>${copyButton(runId, 'Copy run id')}${Number.isFinite(started) ? ` <span>· ${esc(new Date(started).toISOString().slice(0, 16).replace('T', ' '))} UTC</span>` : ''}</p>` : ''}
|
|
856
|
+
</header>
|
|
857
|
+
<main id="main">
|
|
858
|
+
${verdictSection(c)}
|
|
859
|
+
${lanesSection(c)}
|
|
860
|
+
${findingsSection(c)}
|
|
861
|
+
${flowSection(c)}
|
|
862
|
+
${healsSection(c)}
|
|
863
|
+
${ledgerSection(c)}
|
|
864
|
+
</main>
|
|
865
|
+
${rail(c)}
|
|
866
|
+
<footer class="foot">
|
|
867
|
+
<span>Argus ${esc(input.version)}</span>
|
|
868
|
+
${runLink !== undefined ? `<span>${runLink}</span>` : ''}
|
|
869
|
+
<span><a href="${REPO_URL}">github.com/duketopceo/Argus</a></span>
|
|
870
|
+
<span>Self-contained: this file makes no network requests.</span>
|
|
871
|
+
</footer>
|
|
872
|
+
</div>
|
|
873
|
+
<script>
|
|
874
|
+
${SCRIPT}
|
|
875
|
+
</script>
|
|
876
|
+
</body>
|
|
877
|
+
</html>
|
|
878
|
+
`;
|
|
879
|
+
}
|