Skip to content

Commit 7147969

Browse files
Cobb04codex
andcommitted
fix(reporting): use canonical Session population in HTML
Fixes #117 by deriving the portable HTML hero and Evidence metrics from the reviewed evidence selection, preserving the legacy fallback and distinguishing unavailable data from a real zero population. Validated with focused Node 22/24 renderer suites and npm run check. Co-authored-by: Codex (GPT 5.6 Sol) <codex@openai.com>
1 parent a415fa6 commit 7147969

2 files changed

Lines changed: 122 additions & 8 deletions

File tree

scripts/harness-analysis/renderers/html.mjs

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,29 @@ function formatNumber(value, locale) {
5050
}).format(number(value));
5151
}
5252

53+
function reviewedSessionPopulation(summary, language) {
54+
// Usage efficiency is a separate all-eligible activity census. The visible
55+
// reviewed-Session metric follows the evidence selection used by findings.
56+
const selection = summary?.evidenceBoundary?.manifest?.selection
57+
?? summary?.atAGlance?.coverage?.selection;
58+
const hasCounts = selection
59+
&& Object.hasOwn(selection, "analyzedCount")
60+
&& Object.hasOwn(selection, "eligibleCount");
61+
const analyzed = Number(selection?.analyzedCount);
62+
const eligible = Number(selection?.eligibleCount);
63+
const available = hasCounts
64+
&& Number.isInteger(analyzed)
65+
&& analyzed >= 0
66+
&& Number.isInteger(eligible)
67+
&& eligible >= 0;
68+
return {
69+
value: available
70+
? `${formatNumber(analyzed, language)} / ${formatNumber(eligible, language)}`
71+
: "N/A",
72+
confidence: available ? selection.confidence ?? "" : "",
73+
};
74+
}
75+
5376
function copy(language, en, zh) {
5477
return language === "zh-CN" ? zh : en;
5578
}
@@ -541,14 +564,14 @@ function renderCustomize(summary, language) {
541564
function renderEvidence(summary, language) {
542565
const boundary = summary?.evidenceBoundary ?? {};
543566
const coverage = boundary?.episodeCoverage ?? summary?.atAGlance?.coverage ?? {};
544-
const selection = boundary?.manifest?.selection ?? coverage?.selection ?? {};
567+
const population = reviewedSessionPopulation(summary, language);
545568
const learning = summary?.learningCapture ?? {};
546569
const facts = [
547570
[copy(language, "Evidence mode", "证据模式"), summary?.evidenceMode ?? "—"],
548571
[copy(language, "Task episodes", "任务 Episode"), coverage.episodeCount ?? 0],
549572
[copy(language, "Edited episodes", "含编辑 Episode"), coverage.editedEpisodeCount ?? 0],
550-
[copy(language, "Sampling", "抽样"), `${selection.analyzedCount ?? 0} / ${selection.eligibleCount ?? 0}`],
551-
[copy(language, "Confidence", "可信度"), selection.confidence ?? "—"],
573+
[copy(language, "Sampling", "抽样"), population.value],
574+
[copy(language, "Confidence", "可信度"), population.confidence || "—"],
552575
[copy(language, "Learning state", "学习状态"), learning.state ?? "—"],
553576
];
554577
const gaps = textLines(boundary.sourceGaps);
@@ -570,10 +593,7 @@ function renderHtmlBody(reportData) {
570593
const high = findings.filter((row) => ["Critical", "High"].includes(row.severity)).length;
571594
const medium = findings.filter((row) => row.severity === "Medium").length;
572595
const overview = summary.overview ?? summary.strengths?.[0] ?? copy(language, "Reviewed Harness evidence is ready for inspection.", "已复核的 Harness 证据可供检查。" );
573-
const coverage = summary.atAGlance?.coverage ?? {};
574-
const selection = summary.usageEfficiency?.selection ?? coverage.selection ?? {};
575-
const analyzed = selection.analyzedSessionCount ?? selection.analyzedCount ?? 0;
576-
const eligible = selection.eligibleSessionCount ?? selection.eligibleCount ?? summary.usageActivity?.sessions?.total ?? 0;
596+
const population = reviewedSessionPopulation(summary, language);
577597
const hasUsage = summary.usageActivity !== undefined || summary.usageEfficiency !== undefined;
578598
const effectiveness = loopEffectiveness(dimensions);
579599
const progress = repairProgress(findings);
@@ -590,7 +610,7 @@ function renderHtmlBody(reportData) {
590610
<div class="metrics">
591611
${metric(evidenceScoreLabel(summary, language), `${effectiveness} / 100`, copy(language, "Changes after later task outcomes", "等待后续任务结果后更新"), language)}
592612
${metric(copy(language, "Asset Health / Repair Progress", "资产健康 / 修复进度"), `${progress.score} / 100`, copy(language, `${progress.verified} verified · ${progress.partial} partial · ${progress.pending} pending`, `${progress.verified} 项已验证 · ${progress.partial} 项部分完成 · ${progress.pending} 项待处理`), language)}
593-
${metric(copy(language, "Sessions analyzed", "会话样本"), `${formatNumber(analyzed, language)} / ${formatNumber(eligible, language)}`, selection.confidence ?? "", language)}
613+
${metric(copy(language, "Sessions analyzed", "会话样本"), population.value, population.confidence, language)}
594614
${metric(copy(language, "Findings", "待处理 Finding"), findings.length, `${high} High · ${medium} Medium`, language)}
595615
</div>
596616
${section("fluency", copy(language, "01 · Readiness", "01 · 就绪度"), copy(language, "Five-dimension fluency", "五维流畅度"), renderDimensionGrid(summary, language), copy(language, "Scores and states come from the reviewed source.", "分数与状态来自已复核 source。"), language)}

test/reporting/harness-report-render-cli.test.mjs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1184,6 +1184,68 @@ test("portable publication and restoration failure reports PUBLISH_ROLLBACK_FAIL
11841184
});
11851185
});
11861186

1187+
test("portable HTML Session population stays consistent across generated artifacts", async () => {
1188+
await withTempDir("better-harness-portable-session-population-", async (root) => {
1189+
// Given: reviewed portable data with a canonical 2/2 Session population and
1190+
// no optional usage census or legacy at-a-glance projection.
1191+
const reviewed = projectTaskLoopFindings(reviewedTaskLoopSource(), {
1192+
projectName: "render-source-project",
1193+
direct: true,
1194+
});
1195+
reviewed.summary.evidenceBoundary.manifest.platform = "dsh";
1196+
reviewed.summary.evidenceBoundary.manifest.selection = {
1197+
strategy: "all-eligible",
1198+
eligibleCount: 2,
1199+
analyzedCount: 2,
1200+
confidence: "High",
1201+
};
1202+
reviewed.summary.dimensions = reviewed.summary.dimensions.map((dimension) => {
1203+
const compact = { ...dimension };
1204+
delete compact.subdimensions;
1205+
return compact;
1206+
});
1207+
delete reviewed.summary.atAGlance;
1208+
delete reviewed.summary.usageActivity;
1209+
delete reviewed.summary.usageEfficiency;
1210+
1211+
const findingsPath = path.join(root, "reviewed.findings.json");
1212+
await writeJson(findingsPath, reviewed);
1213+
1214+
// When: every affected portable host routes the same reviewed input through
1215+
// the public renderer.
1216+
for (const host of ["dsh", "codex", "grok", "kimi", "workbuddy"]) {
1217+
const runDir = path.join(root, `run-${host}`);
1218+
const result = runNode([
1219+
renderPath,
1220+
"--findings", findingsPath,
1221+
"--mode", "html",
1222+
"--platform", host,
1223+
"--target", root,
1224+
"--run-dir", runDir,
1225+
"--validate",
1226+
"--json",
1227+
], { cwd: root });
1228+
1229+
// Then: JSON, Markdown, and both visible HTML surfaces use the same
1230+
// canonical reviewed population without a host-specific renderer branch.
1231+
assert.equal(result.status, 0, `${host}: ${result.stderr || result.stdout}`);
1232+
const findings = JSON.parse(readFileSync(path.join(runDir, "findings.json"), "utf8"));
1233+
const markdown = readFileSync(path.join(runDir, "report.md"), "utf8");
1234+
const html = readFileSync(path.join(runDir, "report.html"), "utf8");
1235+
assert.deepEqual(findings.summary.evidenceBoundary.manifest.selection, {
1236+
strategy: "all-eligible",
1237+
eligibleCount: 2,
1238+
analyzedCount: 2,
1239+
confidence: "High",
1240+
}, host);
1241+
assert.match(markdown, /Session selection: all-eligible; 2 sessions analyzed of 2 eligible sessions; High confidence/u, host);
1242+
assert.match(html, /<span>Sessions analyzed<\/span>\s*<strong>2 \/ 2<\/strong>\s*<small>High<\/small>/u, host);
1243+
assert.match(html, /<span>Sampling<\/span><strong>2 \/ 2<\/strong>/u, host);
1244+
assert.match(html, /<span>Confidence<\/span><strong>High<\/strong>/u, host);
1245+
}
1246+
});
1247+
});
1248+
11871249
test("DSH reuses portable HTML report data, target root, and exact artifact contract", async () => {
11881250
await withTempDir("better-harness-dsh-portable-", async (root) => {
11891251
const target = path.join(root, "DSH target with 空格");
@@ -1622,6 +1684,38 @@ test("HTML evidence episode coverage preserves canonical summary facts and legac
16221684
assert.match(legacyHtml, /<span>Edited episodes<\/span><strong>5<\/strong>/u);
16231685
});
16241686

1687+
test("portable HTML Session population distinguishes valid zero from unavailable data", () => {
1688+
const fixture = sampleFindings();
1689+
const reportData = {
1690+
...fixture,
1691+
language: "en",
1692+
target: { name: "render-fixture", path: "/tmp/render-fixture" },
1693+
};
1694+
1695+
const zeroHtml = renderHtml({
1696+
...reportData,
1697+
summary: {
1698+
...reportData.summary,
1699+
evidenceBoundary: {
1700+
manifest: {
1701+
selection: {
1702+
strategy: "all-eligible",
1703+
eligibleCount: 0,
1704+
analyzedCount: 0,
1705+
confidence: "Low",
1706+
},
1707+
},
1708+
},
1709+
},
1710+
});
1711+
const unavailableHtml = renderHtml(reportData);
1712+
1713+
assert.match(zeroHtml, /<span>Sessions analyzed<\/span>\s*<strong>0 \/ 0<\/strong>\s*<small>Low<\/small>/u);
1714+
assert.match(zeroHtml, /<span>Sampling<\/span><strong>0 \/ 0<\/strong>/u);
1715+
assert.match(unavailableHtml, /<span>Sessions analyzed<\/span>\s*<strong>N\/A<\/strong>/u);
1716+
assert.match(unavailableHtml, /<span>Sampling<\/span><strong>N\/A<\/strong>/u);
1717+
});
1718+
16251719
test("HTML CJK phrase breaking emits bounded deterministic markup without changing report data", () => {
16261720
// Given: reviewed phrases plus escaping, long-token, path, URL, and mixed-Latin boundaries.
16271721
const fixture = sampleFindings();

0 commit comments

Comments
 (0)