Skip to content

Commit 4c9fda1

Browse files
feat(report): add vg report --format html with severity summary (#406)
vg report --format html renders the local scan artifact (default .vibgrate/scan_result.json) as one self-contained HTML page: - A summary at the top shows severity counts for vulnerability and security findings, with links to the full tables below. - A missing score renders as n/a; a measured 0 stays 0. - A fix-available count appears when findings carry details.fixedVersions. - The page is deterministic, HTML-escaped and fully local: no network calls and no external assets. html is added to the accepted report formats. Text, JSON and Markdown output are unchanged, and any other --format value still exits 5 with error: unknown --format "<value>" (expected md, text, json, html). DOCS.md, the bundled vg skill text and installer content describe the HTML page. Closes #235
1 parent 814f6f4 commit 4c9fda1

9 files changed

Lines changed: 745 additions & 31 deletions

File tree

‎DOCS.md‎

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -459,46 +459,63 @@ Creates:
459459

460460
### vg report
461461

462-
Render a saved scan artifact. There is no HTML report. The formats are `md`,
463-
`text`, and `json`. `text` is the default. `json` prints the artifact unchanged
464-
and is the only stable machine-readable contract. `sarif` is not a `vg report`
465-
format; write that with `vg scan --format sarif`.
462+
Render a saved scan artifact. The formats are `md`, `text`, `json`, and `html`.
463+
`text` is the default. `json` prints the artifact unchanged and is the only
464+
stable machine-readable contract. `sarif` is not a `vg report` format; write
465+
that with `vg scan --format sarif`.
466466

467467
```bash
468-
vg report [--in <file>] [--format md|text|json]
468+
vg report [--in <file>] [--format md|text|json|html]
469469
```
470470

471-
| Flag | Default | Description |
472-
| ---------- | ---------------------------- | --------------------------------------------------------------------------- |
473-
| `--in` | `.vibgrate/scan_result.json` | Input artifact file |
474-
| `--format` | `text` | `md`, `text`, or `json`. Any other value is a usage error (exit `5`) |
471+
| Flag | Default | Description |
472+
| ---------- | ---------------------------- | -------------------------------------------------------------------------------- |
473+
| `--in` | `.vibgrate/scan_result.json` | Input artifact file |
474+
| `--format` | `text` | `md`, `text`, `json`, or `html`. Any other value is a usage error (exit `5`) |
475475

476476
An unknown value is rejected before the file is read. The process exits `5`,
477477
writes nothing to stdout, and prints one stderr line that names the value and
478478
lists the valid ones:
479479

480480
```text
481-
error: unknown --format "html" (expected md, text, json)
481+
error: unknown --format "sarif" (expected md, text, json, html)
482482
```
483483

484484
The comparison is exact. `HTML`, `sarif`, and `md ` are unknown. Omitting
485485
`--format` stays on `text`.
486486

487+
`md`, `text`, and `json` are unchanged. `html` writes one self-contained page
488+
from that same artifact. It does not call the network and does not load
489+
external files. The page opens with a summary:
490+
491+
- DriftScore and the four component scores. A missing score is `n/a`. A measured `0` stays `0`.
492+
- Counts by severity for vulnerability findings and for security findings. A check that did not run stays "Not scanned" rather than a row of zeros. A check that finished with nothing to report shows zeros.
493+
- When a finding carries `details.fixedVersions`, how many of those findings name a fix.
494+
- Links to the full tables under the summary: vulnerabilities, security findings, and the other findings.
495+
496+
The same artifact always produces the same page. Save it with a shell redirect:
497+
498+
```bash
499+
vg report --format html > report.html
500+
```
501+
487502
#### Report format coverage
488503

489504
`--format md` is `formatMarkdown` (`src/reporting/formatters/markdown.ts`).
490-
`--format text` is `formatText` (`src/reporting/formatters/text.ts`). Both read
491-
the same artifact. A section below is omitted when its guard is false. Lists
492-
follow artifact order except where a sort is named. Colours in the text report
493-
are terminal styling; under `NO_COLOR` the same words are plain. A null score
494-
is `n/a` in both formats. A measured `0` stays `0`.
505+
`--format text` is `formatText` (`src/reporting/formatters/text.ts`).
506+
`--format html` is `formatHtmlReport` (`src/reporting/formatters/html.ts`).
507+
All three read the same artifact. A section below is omitted when its guard is
508+
false. Lists follow artifact order except where a sort is named. Colours in the
509+
text report are terminal styling; under `NO_COLOR` the same words are plain. A
510+
null score is `n/a` in the human formats. A measured `0` stays `0`.
495511

496-
The same artifact, three times:
512+
The same artifact, four formats:
497513

498514
```bash
499515
vg report --in .vibgrate/scan_result.json --format md
500516
vg report --in .vibgrate/scan_result.json --format text
501517
vg report --in .vibgrate/scan_result.json --format json
518+
vg report --in .vibgrate/scan_result.json --format html
502519
```
503520

504521
##### Markdown (`formatMarkdown`)
@@ -566,7 +583,11 @@ When set:
566583
| Architecture Layers | `extended.architecture`. `archetype`, `archetypeConfidence`, `totalClassified`, `unclassified`. **Folders** — the first 12 of `folders`: `path`, `layer`, `confidence`, `fileCount`. **Coverage**, when set — `ratio`, `classified`, `unclassified`, and `bySource` counts sorted by count descending then source name. **Unclassified source** — `unclassifiedFiles.length` when that list is non-empty. **Unclassified by folder** — the first 8 of `unclassifiedFolders`: `count`, `path`. **Layers** with `fileCount` greater than 0: `layer`, `fileCount`, `driftScore`, `riskLevel`. **Boundary violations** — violations whose `rule` does not start with `graph-conflict:`, first 8: `fromFile`, `toFile`, `rule`. **Layer conflicts** — the `graph-conflict:` violations, first 8, same three fields. A capped violation list is marked when `violationsCapped` is set |
567584
| Solution Drift Summary | `solutions` is non-empty. `name`, `projectPaths.length`, and `drift.score` or `n/a` |
568585

569-
`relationshipDiagram` is not rendered. The text formatter leaves that diagram in the JSON artifact.
586+
`relationshipDiagram` is not rendered. Markdown, text, and HTML leave that diagram in the JSON artifact.
587+
588+
##### HTML (`formatHtmlReport`)
589+
590+
`vg report --format html` writes one self-contained page from the same artifact. It does not call the network and does not load external files. The page opens with the summary listed above (DriftScore, severity counts, a fix-available count when a finding carries `details.fixedVersions`, and links to the tables), then the full vulnerability, security, and other-finding tables. A check that did not run stays "Not scanned". A missing score is `n/a`. The same artifact always produces the same page.
570591

571592
##### Differences
572593

@@ -579,10 +600,11 @@ When set:
579600
- DriftScore: Markdown puts the lower-is-better gloss in the opening cell. Text puts the score in **DriftScore Summary** without that gloss.
580601
- VCS: Markdown uses separate VCS, Branch, and Commit rows, and Commit requires `vcs.sha`. Text uses one line and can show `shortSha` without requiring `sha`.
581602
- Runtime wording: Markdown always says `major(s)`. Text says `major` or `majors`.
603+
- HTML is the browser page: the summary above, then the vulnerability, security, and other-finding tables. It does not print Markdown's recommended-standards section or the text inventory.
582604

583605
##### JSON
584606

585-
`--format json` prints `JSON.stringify` of the loaded artifact with a two-space indent. Keys, values, and omissions are unchanged. Parse this when a script needs a field. Markdown and text drop fields, cap lists, and add derived lines; they are not a stable contract.
607+
`--format json` prints `JSON.stringify` of the loaded artifact with a two-space indent. Keys, values, and omissions are unchanged. Parse this when a script needs a field. Markdown, text, and HTML drop fields, cap lists, and add derived lines; they are not a stable contract.
586608

587609
---
588610

‎plugins/claude/vg/skills/vg/SKILL.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: vg
33
description: Query the local code graph (vg) for structure, impact, and navigation instead of grepping/reading many files.
44
---
55

6-
<!-- vg:v4 · managed by `vg install` — auto-refreshed when these instructions evolve; remove this line to opt out -->
6+
<!-- vg:v5 · managed by `vg install` — auto-refreshed when these instructions evolve; remove this line to opt out -->
77

88
# vg — the code map
99

@@ -39,7 +39,7 @@ measured and the tools improved):
3939
- **Upgrade drift:** `vg drift` lists what is outdated across dependencies
4040
(offline; `--online` for currency). `vg scan` scores upgrade drift
4141
(`text`, `json`, `sarif`, or `md`). `vg report` renders a saved scan
42-
artifact as `text` (the default), `md`, or `json`. There is no HTML report.
42+
artifact as `text` (the default), `md`, `json`, or `html` (a self-contained page).
4343
- **Version-correct docs:** `vg lib <name>` returns drift-annotated, version-
4444
specific usage docs for a library — inject these instead of guessing an API.
4545

‎skills/vg/SKILL.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: vg
33
description: Query the local code graph (vg) for structure, impact, and navigation instead of grepping/reading many files.
44
---
55

6-
<!-- vg:v4 · managed by `vg install` — auto-refreshed when these instructions evolve; remove this line to opt out -->
6+
<!-- vg:v5 · managed by `vg install` — auto-refreshed when these instructions evolve; remove this line to opt out -->
77

88
# vg — the code map
99

@@ -39,7 +39,7 @@ measured and the tools improved):
3939
- **Upgrade drift:** `vg drift` lists what is outdated across dependencies
4040
(offline; `--online` for currency). `vg scan` scores upgrade drift
4141
(`text`, `json`, `sarif`, or `md`). `vg report` renders a saved scan
42-
artifact as `text` (the default), `md`, or `json`. There is no HTML report.
42+
artifact as `text` (the default), `md`, `json`, or `html` (a self-contained page).
4343
- **Version-correct docs:** `vg lib <name>` returns drift-annotated, version-
4444
specific usage docs for a library — inject these instead of guessing an API.
4545

‎src/install/content.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ export const NUDGE_END = '<!-- vg:end -->';
2222
* v3 = library-docs workflow resolves by package name only, and an
2323
* `entityCount` of 0 means read `node_modules` rather than retry;
2424
* v4 = `vg report` formats are text (default), md, and json. There is no
25-
* HTML report, and sarif belongs to `vg scan`.
25+
* HTML report, and sarif belongs to `vg scan`;
26+
* v5 = `vg report` also accepts `html` (a self-contained page).
2627
*/
27-
export const INSTALL_CONTENT_VERSION = 4;
28+
export const INSTALL_CONTENT_VERSION = 5;
2829

2930
const VERSION_MARKER_RE = /<!--\s*vg:v(\d+)\b[^>]*-->/;
3031

@@ -94,7 +95,7 @@ measured and the tools improved):
9495
- **Upgrade drift:** \`vg drift\` lists what is outdated across dependencies
9596
(offline; \`--online\` for currency). \`vg scan\` scores upgrade drift
9697
(\`text\`, \`json\`, \`sarif\`, or \`md\`). \`vg report\` renders a saved scan
97-
artifact as \`text\` (the default), \`md\`, or \`json\`. There is no HTML report.
98+
artifact as \`text\` (the default), \`md\`, \`json\`, or \`html\` (a self-contained page).
9899
- **Version-correct docs:** \`vg lib <name>\` returns drift-annotated, version-
99100
specific usage docs for a library — inject these instead of guessing an API.
100101

‎src/reporting/commands/report.test.ts‎

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as os from 'node:os';
44
import * as path from 'node:path';
55
import { main } from '../../cli.js';
66
import { ExitCode } from '../../util/exit.js';
7+
import { REPORT_FORMATS, reportCommand } from './report.js';
78

89
const SENTINEL = 'report-format-sentinel';
910

@@ -91,14 +92,45 @@ async function runCli(args: string[]): Promise<{ code: number; stderr: string; s
9192
}
9293

9394
describe('vg report --format', () => {
95+
it('names html in help and in the accepted list', () => {
96+
const option = reportCommand.options.find((item) => item.long === '--format');
97+
expect(option?.description).toBe('Output format (md|text|json|html)');
98+
expect(REPORT_FORMATS).toEqual(['md', 'text', 'json', 'html']);
99+
});
100+
101+
it('renders html from the local artifact and leaves text, json, and md alone', async () => {
102+
const file = writeArtifact();
103+
const html = await runCli(['report', '--in', file, '--format', 'html']);
104+
expect(html.code).toBe(0);
105+
expect(html.stdout).toContain('<!doctype html>');
106+
expect(html.stdout).toContain('id="summary"');
107+
expect(html.stdout).toContain('id="drift-score">1');
108+
expect(html.stderr).toBe('');
109+
110+
const text = await runCli(['report', '--in', file]);
111+
expect(text.code).toBe(0);
112+
expect(text.stdout).toContain('Vibgrate Drift Report');
113+
expect(text.stdout).not.toContain('<!doctype');
114+
115+
const md = await runCli(['report', '--in', file, '--format', 'md']);
116+
expect(md.code).toBe(0);
117+
expect(md.stdout).toContain('# Vibgrate Drift Report');
118+
expect(md.stdout).not.toContain('<!doctype');
119+
120+
const json = await runCli(['report', '--in', file, '--format', 'json']);
121+
expect(json.code).toBe(0);
122+
expect(JSON.parse(json.stdout)).toEqual(artifact);
123+
expect(json.stdout).not.toContain('<!doctype');
124+
});
125+
94126
it('rejects an unknown format with exit 5, the value, the valid list, and empty stdout', async () => {
95127
const file = writeArtifact();
96-
for (const format of ['html', 'HTML', 'sarif']) {
128+
for (const format of ['HTML', 'sarif', 'html ']) {
97129
const result = await runCli(['report', '--in', file, '--format', format]);
98130
expect(result.code).toBe(ExitCode.USAGE_ERROR);
99131
expect(result.stdout).toBe('');
100132
expect(result.stderr).toBe(
101-
`error: unknown --format ${JSON.stringify(format)} (expected md, text, json)\n`,
133+
`error: unknown --format ${JSON.stringify(format)} (expected md, text, json, html)\n`,
102134
);
103135
expect(result.stderr).not.toContain(SENTINEL);
104136
}

‎src/reporting/commands/report.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import { usageError } from '../../util/exit.js';
55
import { pathExists, readJsonFile } from '../utils/fs.js';
66
import { formatText } from '../formatters/text.js';
77
import { formatMarkdown } from '../formatters/markdown.js';
8+
import { formatHtmlReport } from '../formatters/html.js';
89
import type { ReportFormat, ScanArtifact } from '../types.js';
910

10-
/** Accepted by `vg report --format`. `text` is the commander default. */
11-
const REPORT_FORMATS = ['md', 'text', 'json'] as const satisfies readonly ReportFormat[];
11+
/** Formats `vg report` accepts. `text` is the default. */
12+
export const REPORT_FORMATS = ['md', 'text', 'json', 'html'] as const satisfies readonly ReportFormat[];
1213

1314
function assertReportFormat(format: string): asserts format is ReportFormat {
1415
if ((REPORT_FORMATS as readonly string[]).includes(format)) return;
@@ -43,5 +44,8 @@ export const reportCommand = new Command('report')
4344
case 'text':
4445
console.log(formatText(artifact));
4546
break;
47+
case 'html':
48+
console.log(formatHtmlReport(artifact));
49+
break;
4650
}
4751
});

0 commit comments

Comments
 (0)