From 3e3eb18671d8e5aeaa535a9ef3ef701b8161a8fa Mon Sep 17 00:00:00 2001 From: Matthew Valancy Date: Thu, 18 Jun 2026 20:44:52 -0700 Subject: [PATCH] report: hierarchical reference IDs on every section + check (citable tags) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit So a reviewer can say "3.5 looks wrong". buildReport now assigns each sequence a section ref and each case `
.` (presets like a live runner's source.dimension.check are preserved); html.mjs renders a citable §ref badge on each section and a copy-selectable ref chip on each check. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/lib/reporting/aggregate.mjs | 15 +++++++++++++++ tests/lib/reporting/aggregate.test.mjs | 11 +++++++++++ tests/lib/reporting/html.mjs | 4 ++++ 3 files changed, 30 insertions(+) diff --git a/tests/lib/reporting/aggregate.mjs b/tests/lib/reporting/aggregate.mjs index 0c434cc6..d06eb697 100644 --- a/tests/lib/reporting/aggregate.mjs +++ b/tests/lib/reporting/aggregate.mjs @@ -43,7 +43,22 @@ export function sumCounts(sequences) { } /** Build the full report object the reporters consume. Pure; timestamps passed in. */ +/** + * Stable hierarchical reference IDs so a reviewer can cite a specific check + * (e.g. "3.5 looks wrong"). Each sequence gets a section number; each case gets + * `
.`. Pre-set refs (e.g. a deeper source.dimension.check from the + * live runner) are preserved. + */ +export function assignRefs(sequences) { + sequences.forEach((seq, i) => { + seq.ref = seq.ref || String(i + 1); + (seq.cases || []).forEach((c, j) => { c.ref = c.ref || `${seq.ref}.${j + 1}`; }); + }); + return sequences; +} + export function buildReport({ sequences = [], startedAt, finishedAt, target = '', env = {} } = {}) { + assignRefs(sequences); const { totals, byStatus } = sumCounts(sequences); return { schema: 'graphdone.unified-report/1', diff --git a/tests/lib/reporting/aggregate.test.mjs b/tests/lib/reporting/aggregate.test.mjs index 8a3317a8..b31769eb 100644 --- a/tests/lib/reporting/aggregate.test.mjs +++ b/tests/lib/reporting/aggregate.test.mjs @@ -43,6 +43,17 @@ test('sumCounts totals + status tally', () => { assert.equal(byStatus.failed, 1); }); +test('buildReport assigns hierarchical refs (section + section.case), preserving presets', () => { + const r = buildReport({ sequences: [ + { title: 'A', status: 'passed', counts: { passed: 2 }, cases: [{ title: 'x', status: 'passed' }, { title: 'y', status: 'passed' }] }, + { ref: '2.4', title: 'B', status: 'warn', counts: { warned: 1 }, cases: [{ ref: '2.4.1', title: 'z', status: 'warn' }] }, + ] }); + assert.equal(r.sequences[0].ref, '1'); + assert.deepEqual(r.sequences[0].cases.map((c) => c.ref), ['1.1', '1.2']); + assert.equal(r.sequences[1].ref, '2.4'); // preset section ref preserved + assert.equal(r.sequences[1].cases[0].ref, '2.4.1'); // preset case ref preserved +}); + test('buildReport rolls up status + duration', () => { const r = buildReport({ sequences: [{ status: 'passed', counts: { passed: 2 } }, { status: 'warn', counts: { warned: 1 } }], diff --git a/tests/lib/reporting/html.mjs b/tests/lib/reporting/html.mjs index ab440612..7cb91840 100644 --- a/tests/lib/reporting/html.mjs +++ b/tests/lib/reporting/html.mjs @@ -21,6 +21,7 @@ function caseHtml(c) { const err = c.error ? `
${esc(c.error)}
` : ''; return `
+ ${c.ref ? `${esc(c.ref)}` : ''} ${esc(c.title)} ${c.durationMs != null ? `${(c.durationMs / 1000).toFixed(1)}s` : ''} ${err}${atts ? `
${atts}
` : ''} @@ -37,6 +38,7 @@ function seqHtml(seq, i) { return `
${ICON[seq.status] || ''} ${esc(seq.status)} + ${seq.ref ? `§${esc(seq.ref)}` : ''} ${esc(seq.title || seq.id)} ${c.passed || 0}✓ ${c.failed || 0}✗ ${c.warned || 0}⚠ ${c.skipped || 0}⏭ ${seq.durationMs != null ? `${(seq.durationMs / 1000).toFixed(1)}s` : ''} @@ -70,6 +72,8 @@ h1{font-size:22px;margin:0 0 4px;display:flex;align-items:center;gap:10px} .seq summary{display:flex;align-items:center;gap:12px;cursor:pointer;list-style:none;padding:8px 0} .seq summary::-webkit-details-marker{display:none} .badge{padding:2px 10px;border-radius:999px;font-size:11px;font-weight:700;color:#0b1220;text-transform:uppercase} +.sref{background:#1e293b;color:#7dd3fc;border:1px solid #334155;border-radius:5px;padding:1px 7px;font-size:12px;font-weight:700;font-variant-numeric:tabular-nums} +.ref{background:#0b1220;color:#fbbf24;border:1px solid #334155;border-radius:5px;padding:0 6px;font-size:11px;font-weight:700;margin-right:8px;font-variant-numeric:tabular-nums;user-select:all} .sname{font-weight:600;flex:1} .counts{font-variant-numeric:tabular-nums;color:#cbd5e1;font-size:12px} .sdur,.cdur{color:#64748b;font-size:12px}