Files
Charles Wiltgen 21990414b2 refactor(scripts): generate audit-area surfaces from one registry
scripts/audit-areas.json is now the single source for /axiom:audit
areas. Three surfaces are generated from it into marker-delimited
regions, leaving the surrounding hand-authored prose untouched:

  commands/audit.md   argument: list   (hash markers, inside frontmatter)
  commands/audit.md   body table       (model-facing "Detects" text)
  docs/commands/utility/audit.md       (human-facing prose)

The registry keeps `detects` and `docsDetects` as separate fields
rather than reconciling them. They differ for 22 of 32 areas because
one is routing text the model reads and the other is prose a person
reads — the docs-are-human / source-is-for-LLM split, not drift.

The sidebar is deliberately NOT generated: each of its groups
interleaves audit entries with unrelated commands (Build holds
audit-build beside fix-build, optimize-build, resolve-deps), so the
audit rows are not a spliceable region and generating the groups would
delete the others. It stays hand-maintained and is now checked directly
against the registry, which is what the old docs-vs-sidebar grouped
parity was approximating.

Retires validateParity and validateGroupedParity: with three surfaces
generated from one source they cannot disagree by construction, so the
set-parity and grouped-parity comparisons are replaced by one staleness
check — the same generate-and-diff pattern already used for the inlined
auditors (12d-bis) and the Codex variant (12f). validateRegistry gates
the source itself (duplicate areas, unknown groups, empty fields, a `|`
that would break a table cell, a docPath the sidebar parser could not
read the area back out of).

Also collapses the three arbitrary orderings that had accumulated
across the surfaces into one: group order, then alphabetical within
group. Row order in a for-LLM table carries no meaning, and docs and
sidebar already used exactly this order.

Verified no data was lost: every body-table row and every docs
description is byte-identical to the previous committed content
compared order-independently; only ordering changed. Mutation-tested by
adding an area to the registry alone — all three generated regions plus
the sidebar check reported it. 126 unit tests pass.
2026-07-25 11:09:25 -07:00

152 lines
4.4 KiB
TypeScript

/**
* Generate the /axiom:audit area surfaces from scripts/audit-areas.json.
*
* npm run build:audit-areas # write
* npm run build:audit-areas -- --check # verify only, non-zero on drift
*
* Three regions, each delimited by BEGIN/END markers so the surrounding
* hand-authored prose is untouched:
*
* commands/audit.md `argument:` line (hash markers, in frontmatter)
* commands/audit.md body table (html markers)
* docs/commands/utility/audit.md grouped tables (html markers)
*
* The docs sidebar is NOT generated: each of its groups interleaves audit
* entries with unrelated commands, so the audit rows are not a spliceable
* region. It stays hand-maintained and is checked against the registry by
* validateSidebarAgainstRegistry in pre-deploy.
*
* Generated output is COMMITTED — same convention as axiom-codex/ and the
* inlined auditors. pre-deploy.ts runs this in --check mode and fails the
* build if a committed file has drifted from the registry.
*
* Rendering logic lives in scripts/audit-areas.ts (I/O free, unit tested).
* This file is the disk layer.
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
MARKERS,
renderArgumentList,
renderBodyTable,
renderDocsTables,
spliceRegion,
validateRegistry,
type AuditRegistry,
} from "./audit-areas.ts";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const PLUGIN = path.join(root, ".claude-plugin/plugins/axiom");
const check = process.argv.includes("--check");
const registryPath = path.join(root, "scripts/audit-areas.json");
const registry: AuditRegistry = JSON.parse(fs.readFileSync(registryPath, "utf8"));
const registryErrors = validateRegistry(registry);
if (registryErrors.length > 0) {
console.error("audit-areas.json is invalid:");
for (const e of registryErrors) console.error(`${e}`);
process.exit(1);
}
interface Target {
file: string;
marker: string;
style: "html" | "line" | "hash";
render: () => string;
label: string;
}
const targets: Target[] = [
{
file: path.join(PLUGIN, "commands/audit.md"),
marker: MARKERS.argument,
style: "hash",
render: () => renderArgumentList(registry),
label: "commands/audit.md argument: line",
},
{
file: path.join(PLUGIN, "commands/audit.md"),
marker: MARKERS.bodyTable,
style: "html",
render: () => renderBodyTable(registry),
label: "commands/audit.md body table",
},
{
file: path.join(root, "docs/commands/utility/audit.md"),
marker: MARKERS.docsTable,
style: "html",
render: () => renderDocsTables(registry),
label: "docs/commands/utility/audit.md grouped tables",
},
];
let written = 0;
let unchanged = 0;
const drift: string[] = [];
const errors: string[] = [];
// Group by file so two regions in one file compose rather than clobber.
const byFile = new Map<string, Target[]>();
for (const t of targets) {
const list = byFile.get(t.file) ?? [];
list.push(t);
byFile.set(t.file, list);
}
for (const [file, fileTargets] of byFile) {
if (!fs.existsSync(file)) {
errors.push(`${path.relative(root, file)} not found`);
continue;
}
const original = fs.readFileSync(file, "utf8");
let content = original;
for (const t of fileTargets) {
const next = spliceRegion(content, t.marker, t.render(), t.style);
if (next === null) {
errors.push(
`${t.label}: markers ${t.marker}_BEGIN / ${t.marker}_END not found or malformed`,
);
continue;
}
content = next;
}
if (content === original) {
unchanged += fileTargets.length;
continue;
}
if (check) {
drift.push(path.relative(root, file));
} else {
fs.writeFileSync(file, content);
written += fileTargets.length;
}
}
if (errors.length > 0) {
for (const e of errors) console.error(`${e}`);
process.exit(1);
}
if (check) {
if (drift.length > 0) {
console.error(
`audit-area surfaces are stale relative to scripts/audit-areas.json: ${drift.join(", ")}`,
);
console.error(" Run: npm run build:audit-areas");
process.exit(1);
}
console.log(
`audit areas: ${registry.areas.length} areas across ${registry.groupOrder.length} groups; all 3 surfaces up to date`,
);
} else {
console.log(
`audit areas: ${registry.areas.length} areas across ${registry.groupOrder.length} groups; ${written} region(s) written, ${unchanged} unchanged`,
);
}