diff --git a/CHANGELOG.md b/CHANGELOG.md index 700263c..ca4b71b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased source - 2026-10-21 + +- Open offline verification reports at the first priority needing review and + jump directly to other severity groups, including without JavaScript. +- Keep complete counts and the existing detail budget; omit empty groups and + use readable status labels. Redaction and verification outcomes are unchanged. + ## 0.3.0 - 2026-10-20 - Review all validated cross-file declarations and inspection limits in offline HTML, text and JSON reports, including CSV summaries, reference metadata and keyed integer allocations. diff --git a/src/reporters/verification-html.ts b/src/reporters/verification-html.ts index 95338e3..726043d 100644 --- a/src/reporters/verification-html.ts +++ b/src/reporters/verification-html.ts @@ -15,12 +15,19 @@ export function renderVerificationHtml(input: VerificationReport): string { const report = redactLocalPaths(input); const issues = report.issues?.length ? report.issues : toIssues(report.findings); const counts = report.summary; + const priorities = [ + { severity: "critical", label: "Critical issues", help: "Resolve these issues before reusing this Skill." }, + { severity: "error", label: "Errors", help: "Fix the failed checks, then verify again." }, + { severity: "warn", label: "Warnings", help: "Review these warnings against your intended use." }, + { severity: "info", label: "Information", help: "Review the recorded checks and notes." }, + ].map(priority => ({ ...priority, issues: issues.filter(issue => issue.finding.severity === priority.severity) })) + .filter(priority => priority.issues.length > 0); + const links = priorities.length ? `` : ""; let remaining = DETAIL_LIMIT; - const groups = (["critical", "error", "warn", "info"] as const).map(severity => { - const all = issues.filter(issue => issue.finding.severity === severity); + const groups = priorities.map(({ severity, label, help, issues: all }, groupIndex) => { const shown = all.slice(0, remaining); remaining -= shown.length; - return `
${severity.toUpperCase()} · ${all.length} issues${shown.map((issue, index) => { + return `
${label} · ${all.length} ${all.length === 1 ? "issue" : "issues"}

${help}

${shown.map((issue, index) => { const f = issue.finding; const resolution = issue.resolutions[0]; const steps = resolution?.kind === "manual" ? resolution.steps.slice(0, 8) : []; @@ -29,14 +36,14 @@ export function renderVerificationHtml(input: VerificationReport): string {

${text(issue.explanation.cause)}

Next step: ${text(resolution?.title ?? f.remediation ?? issue.explanation.impact)}

${steps.length ? `
    ${steps.map(step => `
  1. ${text(step)}
  2. `).join("")}
` : ""}
Issue reference${text(issue.id)}

${text(issue.location?.path ?? "Location not included")}

`; - }).join("")}${all.length > shown.length ? `

Showing ${shown.length} of ${all.length} issues in this group. Use JSON for complete evidence.

` : ""}${all.length ? "" : "

No issues in this group.

"}
`; + }).join("")}${all.length > shown.length ? `

Showing ${shown.length} of ${all.length} issues in this group. Use JSON for complete evidence.

` : ""}
`; }).join(""); const title = report.exitCode === 2 ? "Verification setup required" : counts.fail ? "Changes require attention" : counts.unknown || counts.warn ? "Review incomplete evidence" : "Declared checks passed"; return ` SkillSync · ${title} - +
SkillSync · Offline verification review
-

${title}

Read the cause and next step for each issue. These deterministic checks do not establish runtime safety, authenticated provenance, or approval.

${(["pass", "fail", "warn", "unknown"] as const).map(key => `
${text(counts[key])}${key}
`).join("")}

${text(counts.total)} findings · ${text(issues.length)} deduplicated issues · exit code ${report.exitCode}

-

Issues and next steps

Groups are ordered by severity. At most ${DETAIL_LIMIT} issues are shown. Expand information groups to review successful checks.

${groups}
-

Scope and evidence

${report.targets.length} selected targets. ${text(report.generated_at)}

Local paths and raw evidence contents are excluded. No Skill was executed and no fix was applied by this report.

\n`; +

${title}

These checks read declared metadata. They do not establish runtime safety, authenticated provenance, or approval.

${([{ key: "pass", label: "Passed" }, { key: "fail", label: "Failed" }, { key: "warn", label: "Warnings" }, { key: "unknown", label: "Unverified" }] as const).map(({ key, label }) => `
${text(counts[key])}${label}
`).join("")}

${text(counts.total)} findings · ${text(issues.length)} deduplicated issues · exit code ${report.exitCode}

${links}
+

Issues and next steps

${priorities.length ? `

The first group is open. Choose a priority above to open another group. At most ${DETAIL_LIMIT} issue details are included; use JSON for the full report.

${groups}` : "

No issues were reported. Review the scope and selected targets below.

"}
+

Scope and evidence

${report.targets.length} selected ${report.targets.length === 1 ? "target" : "targets"}. ${text(report.generated_at)}

Local paths and raw evidence contents are excluded. No Skill was executed and no fix was applied by this report.

\n`; } diff --git a/tests/reporters/verification-html.test.ts b/tests/reporters/verification-html.test.ts index d25d6b0..9e7782a 100644 --- a/tests/reporters/verification-html.test.ts +++ b/tests/reporters/verification-html.test.ts @@ -16,3 +16,27 @@ it("escapes metadata, hides raw evidence, and caps details while retaining total expect(html).not.toContain("/private/work"); expect(html).toContain("<local-path>"); }); + +it("links directly to severity groups and opens the first group needing review", () => { + const findings = (["warn", "critical", "error", "info"] as const).map(severity => ({ level: 1 as const, severity, status: "fail" as const, code: `test-${severity}`, skill: "fixture", message: "Review this check", remediation: "Check the saved input", evidence: [] })); + const report: VerificationReport = { schema_version: 1, generated_at: "now", targets: [], findings, issues: toIssues(findings), summary: { total: 4, pass: 0, fail: 4, warn: 0, unknown: 0 }, exitCode: 1 }; + const html = renderVerificationHtml(report); + expect(html).toContain('aria-label="Issue priorities"'); + expect(html).toContain('href="#issues-critical-start"'); + expect(html).toContain('id="issues-critical" open'); + expect(html).not.toContain('id="issues-error" open'); + for (const severity of ["critical", "error", "warn", "info"]) { + expect(html).toContain(`id="issues-${severity}-start"`); + } + expect(html).toContain("Unverified"); + expect(html).not.toContain("CRITICAL ·"); + expect(html).not.toContain("1 issues"); +}); + +it("does not fill an empty report with four empty severity panels", () => { + const report: VerificationReport = { schema_version: 1, generated_at: "now", targets: [], findings: [], issues: [], summary: { total: 0, pass: 0, fail: 0, warn: 0, unknown: 0 }, exitCode: 0 }; + const html = renderVerificationHtml(report); + expect(html).toContain("No issues were reported."); + expect(html).not.toContain('class="issue-group"'); + expect(html).not.toContain('aria-label="Issue priorities"'); +});