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
package/dist/report/comment.js
CHANGED
|
@@ -1,128 +1,317 @@
|
|
|
1
|
+
import { formatUsd, manifestToRunView, maskSecrets, PROOF_LEVELS, proofMeter, SEVERITY_GLYPH, shortSha, STATUS_GLYPH, VERDICT_LABEL, VERDICT_STATUS, } from './viewmodel.js';
|
|
1
2
|
export const SENTINEL = '<!-- argus-reviewer -->';
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
/** Cell semantics mirror the action: flatten newlines, escape pipes, mask secrets, cap length. */
|
|
4
|
+
function cell(s, max = 200) {
|
|
5
|
+
return maskSecrets(String(s ?? '')
|
|
6
|
+
.replace(/\|/g, '\\|')
|
|
7
|
+
.replace(/[\r\n]+/g, ' ')).slice(0, max);
|
|
4
8
|
}
|
|
5
|
-
function
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return
|
|
14
|
-
}
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
9
|
+
function code(s) {
|
|
10
|
+
const t = cell(s);
|
|
11
|
+
const longest = Math.max(0, ...(t.match(/`+/g) ?? []).map((r) => r.length));
|
|
12
|
+
const fence = '`'.repeat(longest + 1);
|
|
13
|
+
const pad = t.startsWith('`') || t.endsWith('`') ? ' ' : '';
|
|
14
|
+
return `${fence}${pad}${t}${pad}${fence}`;
|
|
15
|
+
}
|
|
16
|
+
export function plural(n, one, many = `${one}s`) {
|
|
17
|
+
return `${n} ${n === 1 ? one : many}`;
|
|
18
|
+
}
|
|
19
|
+
function formatDuration(ms) {
|
|
20
|
+
if (ms === undefined || !Number.isFinite(ms) || ms < 0)
|
|
21
|
+
return undefined;
|
|
22
|
+
return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
23
|
+
}
|
|
24
|
+
function statusText(status) {
|
|
25
|
+
return `${STATUS_GLYPH[status]} ${status}`;
|
|
26
|
+
}
|
|
27
|
+
function proofText(level) {
|
|
28
|
+
if (level === null)
|
|
29
|
+
return '';
|
|
30
|
+
if (level === 'none')
|
|
31
|
+
return `${proofMeter(undefined)} none`;
|
|
32
|
+
return `${proofMeter(level)} ${level}`;
|
|
33
|
+
}
|
|
34
|
+
export function findingsOf(cr) {
|
|
35
|
+
return cr !== undefined && Array.isArray(cr.findings) ? cr.findings : [];
|
|
36
|
+
}
|
|
37
|
+
function bestFindingProof(cr) {
|
|
38
|
+
let best = 0;
|
|
39
|
+
for (const f of findingsOf(cr)) {
|
|
40
|
+
best = Math.max(best, PROOF_LEVELS.indexOf(f.evidence?.status ?? ''));
|
|
23
41
|
}
|
|
24
|
-
return
|
|
42
|
+
return PROOF_LEVELS[best] ?? 'suspected';
|
|
43
|
+
}
|
|
44
|
+
export function laneProof(lane, status, cr) {
|
|
45
|
+
if (status === 'skipped')
|
|
46
|
+
return null;
|
|
47
|
+
if (status === 'blocked' || status === 'unavailable')
|
|
48
|
+
return 'none';
|
|
49
|
+
if (status === 'inconclusive' || lane === 'a0')
|
|
50
|
+
return 'suspected';
|
|
51
|
+
if (lane === 'review')
|
|
52
|
+
return bestFindingProof(cr);
|
|
53
|
+
return 'exercised';
|
|
25
54
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
lines.push('| --- | --- |');
|
|
30
|
-
lines.push(`| Vision calls | ${totals.visionCalls} |`);
|
|
31
|
-
const perCall = totals.visionCalls > 0 ? formatUsd(totals.visionCostUsd / totals.visionCalls) : '$0.00';
|
|
32
|
-
lines.push(`| Per-call cost (avg) | ${perCall} |`);
|
|
33
|
-
for (const model of Object.keys(totals.callsByModel).sort()) {
|
|
34
|
-
lines.push(`| Calls (${model}) | ${totals.callsByModel[model]} |`);
|
|
35
|
-
lines.push(`| Spend (${model}) | ${formatUsd(totals.costByModel[model] ?? 0)} |`);
|
|
55
|
+
export function manifestRow(lane, cr) {
|
|
56
|
+
if (!lane.selected) {
|
|
57
|
+
return { lane: lane.lane, status: 'skipped', result: 'not selected', proof: null, spend: '' };
|
|
36
58
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
59
|
+
return {
|
|
60
|
+
lane: lane.lane,
|
|
61
|
+
status: lane.status,
|
|
62
|
+
result: lane.reason ?? lane.summary ?? '',
|
|
63
|
+
proof: laneProof(lane.lane, lane.status, cr),
|
|
64
|
+
spend: lane.usage.metered ? formatUsd(lane.usage.costUsd) : 'unmetered',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function reproducedCount(cr) {
|
|
68
|
+
return typeof cr.provenBlockers === 'number'
|
|
69
|
+
? cr.provenBlockers
|
|
70
|
+
: findingsOf(cr).filter((f) => f.evidence?.status === 'reproduced').length;
|
|
40
71
|
}
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (heals.length === 0) {
|
|
45
|
-
lines.push('No heals this run.');
|
|
72
|
+
function reviewRow(cr) {
|
|
73
|
+
if (cr === undefined) {
|
|
74
|
+
return { lane: 'review', status: 'failed', result: 'no code-review.json', proof: 'none', spend: '' };
|
|
46
75
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
lines.push(`- \`${h.instruction}\` healed with ${h.model ?? 'unknown model'}`);
|
|
50
|
-
}
|
|
76
|
+
if (cr.skipped === true) {
|
|
77
|
+
return { lane: 'review', status: 'skipped', result: cr.summary ?? 'skipped', proof: null, spend: '' };
|
|
51
78
|
}
|
|
52
|
-
|
|
79
|
+
const status = cr.ok === true ? 'passed' : 'failed';
|
|
80
|
+
return {
|
|
81
|
+
lane: 'review',
|
|
82
|
+
status,
|
|
83
|
+
result: `${plural(findingsOf(cr).length, 'finding')}, ${reproducedCount(cr)} reproduced`,
|
|
84
|
+
proof: laneProof('review', status, cr),
|
|
85
|
+
spend: formatUsd(cr.visionCostUsd),
|
|
86
|
+
};
|
|
53
87
|
}
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
for (const t of tests) {
|
|
58
|
-
if (t.asserts.length === 0)
|
|
59
|
-
continue;
|
|
60
|
-
any = true;
|
|
61
|
-
lines.push(`**${t.name}**`);
|
|
62
|
-
for (const a of t.asserts) {
|
|
63
|
-
const icon = a.verdict === 'pass' ? '✅' : '❌';
|
|
64
|
-
lines.push(`- ${icon} *${a.question}* — ${a.reasoning}`);
|
|
65
|
-
}
|
|
66
|
-
lines.push('');
|
|
88
|
+
function flowRow(report) {
|
|
89
|
+
if (report === undefined) {
|
|
90
|
+
return { lane: 'flow', status: 'skipped', result: 'run lane disabled', proof: null, spend: '' };
|
|
67
91
|
}
|
|
68
|
-
|
|
69
|
-
|
|
92
|
+
const heals = report.tests.flatMap((t) => t.healEvents ?? []).length;
|
|
93
|
+
const status = report.ok ? 'passed' : 'failed';
|
|
94
|
+
return {
|
|
95
|
+
lane: 'flow',
|
|
96
|
+
status,
|
|
97
|
+
result: `${report.totals.passed} of ${report.totals.tests} tests passed${heals > 0 ? `, ${heals} healed` : ''}`,
|
|
98
|
+
proof: laneProof('flow', status, undefined),
|
|
99
|
+
spend: formatUsd(report.totals.visionCostUsd),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function laneTable(rows) {
|
|
103
|
+
const lines = ['| Status | Lane | Result | Proof | Spend |', '|---|---|---|---|--:|'];
|
|
104
|
+
for (const r of rows) {
|
|
105
|
+
lines.push(`| ${statusText(r.status)} | ${cell(r.lane)} | ${cell(r.result)} | ${proofText(r.proof)} | ${r.spend} |`);
|
|
70
106
|
}
|
|
71
107
|
return lines;
|
|
72
108
|
}
|
|
73
|
-
function
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
109
|
+
function isVerdict(v) {
|
|
110
|
+
return typeof v === 'string' && Object.hasOwn(VERDICT_LABEL, v);
|
|
111
|
+
}
|
|
112
|
+
/** Headline status and word, shared by the comment and the HTML report. */
|
|
113
|
+
export function verdictOf(ok, aggregate, cr) {
|
|
114
|
+
const verdict = cr !== undefined && cr.skipped !== true && isVerdict(cr.verdict) ? cr.verdict : undefined;
|
|
115
|
+
if (ok !== true) {
|
|
116
|
+
if (verdict === 'needs_changes')
|
|
117
|
+
return { status: 'failed', label: VERDICT_LABEL.needs_changes };
|
|
118
|
+
const s = aggregate !== undefined && aggregate !== 'passed' && aggregate !== 'skipped' ? aggregate : 'failed';
|
|
119
|
+
return { status: s, label: s };
|
|
80
120
|
}
|
|
81
|
-
if (
|
|
82
|
-
|
|
121
|
+
if (verdict !== undefined)
|
|
122
|
+
return { status: VERDICT_STATUS[verdict], label: VERDICT_LABEL[verdict] };
|
|
123
|
+
const s = aggregate ?? 'passed';
|
|
124
|
+
return { status: s, label: s };
|
|
125
|
+
}
|
|
126
|
+
function headline(ok, aggregate, cr) {
|
|
127
|
+
const { status, label } = verdictOf(ok, aggregate, cr);
|
|
128
|
+
return `${STATUS_GLYPH[status]} ${label}`;
|
|
129
|
+
}
|
|
130
|
+
const settled = (s) => s === 'passed' || s === 'skipped';
|
|
131
|
+
const MARKDOWN_LEAD = { strong: (s) => `**${s}**`, code, text: cell };
|
|
132
|
+
/** The verdict line's lead: what the run proved, in one phrase. */
|
|
133
|
+
export function verdictLead(rows, cr, f = MARKDOWN_LEAD) {
|
|
134
|
+
const findings = findingsOf(cr);
|
|
135
|
+
const reviewed = cr !== undefined && cr.skipped !== true;
|
|
136
|
+
if (reviewed) {
|
|
137
|
+
const reproduced = reproducedCount(cr);
|
|
138
|
+
if (reproduced > 0) {
|
|
139
|
+
const files = new Set(findings.filter((x) => x.evidence?.status === 'reproduced').map((x) => x.file));
|
|
140
|
+
const where = files.size === 1 ? ` in ${f.code([...files][0])}` : '';
|
|
141
|
+
return `${f.strong(`${plural(reproduced, 'finding')} reproduced`)}${where}`;
|
|
142
|
+
}
|
|
83
143
|
}
|
|
84
|
-
|
|
144
|
+
const failing = rows.filter((r) => r.lane !== 'review' && !settled(r.status));
|
|
145
|
+
if (failing.length > 0)
|
|
146
|
+
return f.strong(failing.map((r) => `${f.text(r.lane)} ${r.status}`).join(', '));
|
|
147
|
+
if (reviewed && findings.length > 0)
|
|
148
|
+
return f.strong(`${plural(findings.length, 'finding')}, none reproduced`);
|
|
149
|
+
const review = rows.find((r) => r.lane === 'review');
|
|
150
|
+
if (review !== undefined && !settled(review.status))
|
|
151
|
+
return f.strong(`review ${review.status}`);
|
|
152
|
+
if (reviewed)
|
|
153
|
+
return f.strong('No findings');
|
|
154
|
+
return f.strong(rows.some((r) => r.status !== 'skipped') ? 'All selected lanes passed' : 'No lane ran');
|
|
85
155
|
}
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
''
|
|
93
|
-
|
|
156
|
+
function verdictLine(p) {
|
|
157
|
+
const bits = [verdictLead(p.rows, p.cr)];
|
|
158
|
+
const sha = shortSha(p.headSha);
|
|
159
|
+
if (sha !== undefined)
|
|
160
|
+
bits.push(`head ${code(sha)}`);
|
|
161
|
+
if (p.binding?.status === 'mismatch')
|
|
162
|
+
bits.push('head binding mismatch');
|
|
163
|
+
bits.push(formatUsd(p.costUsd));
|
|
164
|
+
const duration = formatDuration(p.durationMs);
|
|
165
|
+
if (duration !== undefined)
|
|
166
|
+
bits.push(duration);
|
|
167
|
+
return bits.join(' · ');
|
|
94
168
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
169
|
+
/** Must match P_FALLBACK_GATE in the action and P_TRUE_POSITIVE_THRESHOLD in src/cli.ts. */
|
|
170
|
+
const P_FALLBACK_GATE = 0.7;
|
|
171
|
+
/** Same fence rule as the action's extractSuggestion: a committable block is non-empty. */
|
|
172
|
+
function hasSuggestion(body) {
|
|
173
|
+
const m = /\r?\n(`{4,})suggestion\r?\n([\s\S]*?)\r?\n\1/.exec(body);
|
|
174
|
+
return m !== null && m[2] !== '';
|
|
175
|
+
}
|
|
176
|
+
const NO_REVIEW_REPORT = 'No code review report was found; check the action logs before merging.';
|
|
177
|
+
const NO_REVIEW_ATTACHED = 'No code review report is attached to this run.';
|
|
178
|
+
function findingsLine(cr, missing = NO_REVIEW_REPORT) {
|
|
179
|
+
if (cr === undefined)
|
|
180
|
+
return missing;
|
|
181
|
+
if (cr.skipped === true)
|
|
182
|
+
return `Code review skipped: ${cell(cr.summary, 300)}`;
|
|
183
|
+
const findings = findingsOf(cr);
|
|
184
|
+
const sev = { bug: 0, risk: 0, nit: 0, q: 0 };
|
|
185
|
+
for (const f of findings) {
|
|
186
|
+
if (f.severity !== undefined && Object.hasOwn(sev, f.severity))
|
|
187
|
+
sev[f.severity] += 1;
|
|
188
|
+
}
|
|
189
|
+
const parts = [
|
|
190
|
+
`${SEVERITY_GLYPH.bug} ${plural(sev.bug, 'bug')}`,
|
|
191
|
+
`${SEVERITY_GLYPH.risk} ${plural(sev.risk, 'risk')}`,
|
|
192
|
+
`${SEVERITY_GLYPH.nit} ${plural(sev.nit, 'nit')}`,
|
|
100
193
|
];
|
|
194
|
+
if (sev.q > 0)
|
|
195
|
+
parts.push(`${SEVERITY_GLYPH.q} ${plural(sev.q, 'question')}`);
|
|
196
|
+
const confident = typeof cr.highConfidenceBlockers === 'number'
|
|
197
|
+
? cr.highConfidenceBlockers
|
|
198
|
+
: findings.filter((f) => typeof f.p === 'number' && f.p >= P_FALLBACK_GATE).length;
|
|
199
|
+
if (confident > 0)
|
|
200
|
+
parts.push(`${confident} high-confidence`);
|
|
201
|
+
const suggestions = Array.isArray(cr.reviewComments)
|
|
202
|
+
? cr.reviewComments.filter((c) => hasSuggestion(c.body ?? '')).length
|
|
203
|
+
: findings.filter((f) => typeof f.suggestion === 'string' && f.suggestion !== '').length;
|
|
204
|
+
if (suggestions > 0)
|
|
205
|
+
parts.push(`${plural(suggestions, 'suggestion')} ready to commit`);
|
|
206
|
+
return parts.join(' · ');
|
|
207
|
+
}
|
|
208
|
+
function footer(meta) {
|
|
209
|
+
const bits = [`Argus ${meta.version}`];
|
|
210
|
+
if (meta.runUrl !== undefined)
|
|
211
|
+
bits.push(`[workflow run and evidence](${meta.runUrl})`);
|
|
212
|
+
if (meta.reportHtml !== undefined)
|
|
213
|
+
bits.push(`report ${code(meta.reportHtml)} in the run artifacts`);
|
|
214
|
+
bits.push('self-hosted, BYOK');
|
|
215
|
+
return `<sub>${bits.join(' · ')}</sub>`;
|
|
101
216
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
217
|
+
function head(status, verdict, rows, summary) {
|
|
218
|
+
return [SENTINEL, `### Argus: ${status}`, '', verdict, '', ...laneTable(rows), '', summary, ''];
|
|
219
|
+
}
|
|
220
|
+
function fold(lines, title, body) {
|
|
221
|
+
if (body.length === 0)
|
|
222
|
+
return;
|
|
223
|
+
lines.push('<details>', `<summary>${title}</summary>`, '', ...body);
|
|
224
|
+
if (body[body.length - 1] !== '')
|
|
225
|
+
lines.push('');
|
|
226
|
+
lines.push('</details>', '');
|
|
227
|
+
}
|
|
228
|
+
const ALL_LANES = ['review', 'flow', 'app', 'a0'];
|
|
229
|
+
export function manifestDuration(m) {
|
|
230
|
+
const ms = Date.parse(m.finishedAt) - Date.parse(m.startedAt);
|
|
231
|
+
return Number.isFinite(ms) ? ms : undefined;
|
|
232
|
+
}
|
|
233
|
+
function headLines(input) {
|
|
234
|
+
const { manifest, codeReview: cr, report } = input;
|
|
235
|
+
const reviewed = cr !== undefined && cr.skipped !== true;
|
|
236
|
+
switch (input.body) {
|
|
237
|
+
case 'missing-key':
|
|
238
|
+
return head(statusText('skipped'), '**Not run:** `OPENROUTER_API_KEY` is not configured, so no lane ran. This status is neutral, not a failure.', ALL_LANES.map((lane) => ({ lane, status: 'skipped', result: 'no API key', proof: null, spend: '' })), 'Fix: add the key as a repository secret, then re-run the workflow: `gh secret set OPENROUTER_API_KEY`');
|
|
239
|
+
case 'no-report':
|
|
240
|
+
return head(statusText('failed'), `**No report:** the run step produced no \`run.json\` under ${code(input.reportDir)}. ` +
|
|
241
|
+
'The commit status fails closed.', [{ lane: 'flow', status: 'failed', result: 'no run.json', proof: 'none', spend: '' }], 'Check the action logs before merging.');
|
|
242
|
+
case 'manifest': {
|
|
243
|
+
if (manifest === undefined)
|
|
244
|
+
throw new Error('a manifest body needs a manifest');
|
|
245
|
+
const m = manifest;
|
|
246
|
+
const rows = manifestToRunView(m).lanes.map((l) => manifestRow(l, cr));
|
|
247
|
+
return head(headline(m.aggregate.ok, m.aggregate.status, cr), verdictLine({
|
|
248
|
+
rows,
|
|
249
|
+
cr,
|
|
250
|
+
headSha: m.identity.intendedHeadSha,
|
|
251
|
+
binding: m.lanes.review.headBinding,
|
|
252
|
+
costUsd: m.aggregate.costUsd,
|
|
253
|
+
durationMs: manifestDuration(m),
|
|
254
|
+
}), rows, findingsLine(cr, NO_REVIEW_ATTACHED));
|
|
255
|
+
}
|
|
256
|
+
case 'full':
|
|
257
|
+
case 'review-only': {
|
|
258
|
+
const flowReport = input.body === 'full' ? report : undefined;
|
|
259
|
+
const rows = manifest !== undefined
|
|
260
|
+
? manifestToRunView(manifest).lanes.map((l) => manifestRow(l, cr))
|
|
261
|
+
: [reviewRow(cr), flowRow(flowReport)];
|
|
262
|
+
const reviewSpend = reviewed ? (cr.visionCostUsd ?? 0) : 0;
|
|
263
|
+
const costUsd = manifest !== undefined
|
|
264
|
+
? manifest.aggregate.costUsd
|
|
265
|
+
: input.body === 'full'
|
|
266
|
+
? (report?.totals.visionCostUsd ?? 0) + reviewSpend
|
|
267
|
+
: reviewSpend;
|
|
268
|
+
const durationMs = manifest !== undefined ? manifestDuration(manifest) : input.body === 'full' ? report?.durationMs : undefined;
|
|
269
|
+
return head(headline(input.ok, manifest?.aggregate.status, cr), verdictLine({
|
|
270
|
+
rows,
|
|
271
|
+
cr,
|
|
272
|
+
headSha: manifest?.identity.intendedHeadSha ?? (reviewed ? cr.headBinding?.intendedSha : undefined),
|
|
273
|
+
binding: manifest?.lanes.review.headBinding ?? (reviewed ? cr.headBinding : undefined),
|
|
274
|
+
costUsd,
|
|
275
|
+
durationMs,
|
|
276
|
+
}), rows, findingsLine(cr));
|
|
277
|
+
}
|
|
109
278
|
}
|
|
110
|
-
|
|
111
|
-
|
|
279
|
+
}
|
|
280
|
+
/** First screen of any sticky body: sentinel through the findings summary. */
|
|
281
|
+
export function renderCommentHead(input) {
|
|
282
|
+
return headLines(input).join('\n');
|
|
283
|
+
}
|
|
284
|
+
export function renderMissingKeyComment(meta) {
|
|
285
|
+
return [...headLines({ body: 'missing-key' }), footer(meta), ''].join('\n');
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Whole manifest-only body (no code-review folds): lane names, status labels,
|
|
289
|
+
* model/cost and head identity come from the shared view-model so the
|
|
290
|
+
* comment agrees with the TUI and dashboard under the contract test.
|
|
291
|
+
*/
|
|
292
|
+
export function renderManifestComment(manifest, meta) {
|
|
293
|
+
const view = manifestToRunView(manifest);
|
|
294
|
+
const lines = headLines({ body: 'manifest', manifest });
|
|
295
|
+
const spend = ['| Lane | Model | Calls | Tokens | Spend |', '|---|---|--:|--:|--:|'];
|
|
296
|
+
for (const lane of view.selectedLanes) {
|
|
297
|
+
const cost = lane.usage.metered ? formatUsd(lane.usage.costUsd) : 'unmetered';
|
|
298
|
+
spend.push(`| ${cell(lane.lane)} | ${cell(lane.model ?? '')} | ${lane.usage.calls} | ${lane.usage.tokens} | ${cost} |`);
|
|
112
299
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
300
|
+
spend.push(`| Total | | ${view.calls} | ${view.tokens} | ${formatUsd(view.costUsd)} |`, '');
|
|
301
|
+
const over = view.selectedLanes.filter((l) => l.budget.exceeded);
|
|
302
|
+
if (over.length > 0)
|
|
303
|
+
spend.push(`**Budget exceeded:** ${over.map((l) => cell(l.lane)).join(', ')}`, '');
|
|
304
|
+
const cache = view.lanes.find((l) => l.lane === 'flow')?.cache;
|
|
305
|
+
if (cache !== undefined) {
|
|
306
|
+
spend.push(`**Fingerprint cache:** ${cache.hits} hit(s) · ${cache.misses} miss(es) · ${cache.heals} heal(s)`, '');
|
|
307
|
+
}
|
|
308
|
+
fold(lines, 'Spend ledger', spend);
|
|
309
|
+
const diagnostics = [];
|
|
310
|
+
if (view.headBinding !== undefined) {
|
|
311
|
+
diagnostics.push(`- Head binding: ${cell(view.headBinding.status)}, ${cell(view.headBinding.detail)}`);
|
|
125
312
|
}
|
|
313
|
+
fold(lines, 'Diagnostics', diagnostics);
|
|
314
|
+
lines.push(footer(meta), '');
|
|
126
315
|
return lines.join('\n');
|
|
127
316
|
}
|
|
128
317
|
/** Map a run report (and optional missing-key flag) to a check-run conclusion. */
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type LaneId, type RunManifest } from './manifest.js';
|
|
2
|
+
/**
|
|
3
|
+
* Offline HTML evidence report (plan U14, R22; DESIGN.md 7.6, A17).
|
|
4
|
+
*
|
|
5
|
+
* One self-contained `report.html` beside `run-manifest.json`: verdict,
|
|
6
|
+
* lanes, findings, flow timeline, heals and spend ledger. Every asset is
|
|
7
|
+
* inline (tokens, WOFF2 subsets as data URIs, the glyph sprite), so the file
|
|
8
|
+
* opens from an artifact zip with no network. Assets come from the generated
|
|
9
|
+
* module, never from assets/ on disk, so a packed install renders it too.
|
|
10
|
+
*
|
|
11
|
+
* Pure: the caller reads the files, this shapes them. Every interpolated
|
|
12
|
+
* string goes through `esc`, which masks secret-shaped tokens and escapes
|
|
13
|
+
* HTML. Inputs are untrusted JSON; a corrupt manifest renders the unreadable
|
|
14
|
+
* state and a missing lane renders unavailable, never a throw.
|
|
15
|
+
*/
|
|
16
|
+
export declare const REPORT_HTML = "report.html";
|
|
17
|
+
export interface ReportHtmlInput {
|
|
18
|
+
/** run-manifest.json file text; undefined when the file is absent. */
|
|
19
|
+
manifestText: string | undefined;
|
|
20
|
+
/** Parsed code-review.json of this run, when the review lane wrote one. */
|
|
21
|
+
codeReview?: unknown;
|
|
22
|
+
/** Parsed run.json of this run, when the flow lane wrote one. */
|
|
23
|
+
run?: unknown;
|
|
24
|
+
/** Argus version shown in the footer. */
|
|
25
|
+
version: string;
|
|
26
|
+
/** Workflow run page (evidence and artifacts). */
|
|
27
|
+
runUrl?: string;
|
|
28
|
+
}
|
|
29
|
+
/** Mask secret-shaped tokens, then escape for HTML text and attributes. */
|
|
30
|
+
export declare function esc(v: unknown): string;
|
|
31
|
+
/** `450ms`, `12.3s`, `4m 05s`, `2h 14m` (DESIGN.md 6.2). */
|
|
32
|
+
export declare function formatDuration(ms: number | undefined): string | undefined;
|
|
33
|
+
type ManifestState = {
|
|
34
|
+
state: 'ok';
|
|
35
|
+
manifest: RunManifest;
|
|
36
|
+
missing: LaneId[];
|
|
37
|
+
} | {
|
|
38
|
+
state: 'missing';
|
|
39
|
+
} | {
|
|
40
|
+
state: 'unreadable';
|
|
41
|
+
detail: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Parse the manifest text, degrading per lane: the run-level fields must
|
|
45
|
+
* pass the shared guard, and each lane that fails `isLaneManifest` is
|
|
46
|
+
* reported missing rather than sinking the whole report.
|
|
47
|
+
*/
|
|
48
|
+
export declare function readManifest(text: string | undefined): ManifestState;
|
|
49
|
+
export declare function renderReportHtml(input: ReportHtmlInput): string;
|
|
50
|
+
export {};
|