1#!/usr/bin/env node2// CI gate: regenerate the reference docs in memory and diff against what's3// checked in. Non-zero exit forces contributors to run build-docs.mjs.45import { readFile } from 'node:fs/promises';6import { fileURLToPath } from 'node:url';7import { dirname, join } from 'node:path';8import { scanners } from '../lib/scanners/index.mjs';9import { gates, MAX_CODE_CANDIDATES, GATE_VERSION } from '../lib/gates/index.mjs'
26 for (const [name, content] of Object.entries(expected)) {
27 let actual;
28 try { actual = await readFile(join(REFS, name), 'utf-8'); }
29 catch {
30 console.error(`[check-docs-fresh] ${name} does not exist. Run \`node scripts/build-docs.mjs\`.`);
31 stale = true;
32 continue;
33 }
34 if (actual !== content) {
35 console.error(`[check-docs-fresh] ${name} is stale. Run \`node scripts/build-docs.mjs\` and commit.`);
36 stale = true;
37 }
38 }
39
40 if (stale) process.exit(1);
41 console.error('[check-docs-fresh] OK — generated docs match source');
42}
43
44// MUST stay byte-identical to build-docs.mjs renderers — duplication is the contract.
45function renderScanners() {
46 const sorted = scanners.slice().sort((a, b) => a.metadata.id.localeCompare(b.metadata.id));
47 let out = GENERATED_BANNER + '# Scanner patterns\n\n';
48 out += 'AST/grep-style scanners run in parallel with metric-driven investigation. They find known anti-patterns. Findings on cold-path or unmappable files are dropped unless the scanner declares `trafficIndependent: true`.\n\n';
49 out += `Total scanners: ${sorted.length}.\n\n`;
50 out += '## Patterns\n\n';
51 for (const s of sorted) {
52 const m = s.metadata;
53 out += `### \`${m.id}\` — ${m.title}\n\n`;
54 out += `- **Severity**: ${m.severity}\n`;
55 out += `- **Billing dimension**: ${m.billingDimension}\n`;
56 out += `- **Traffic-independent**: ${m.trafficIndependent ? 'yes (cold-path findings survive the doctrine drop)' : 'no (cold-path findings get dropped)'}\n\n`;
57 out += `**Description.** ${m.description}\n\n`;
58 out += `**Fix.** ${m.fix}\n\n`;
59 if (m.citations?.length) {
60 out += `**Citations:**\n${m.citations.map((c) => `- \`${c}\``).join('\n')}\n\n`;
61 }
62 out += '---\n\n';
63 }
64 return trimTrailingBlankLine(out);
65}
66
67function renderCandidates() {
68 const sorted = gates.slice().sort((a, b) => a.metadata.id.localeCompare(b.metadata.id));
69 let out = GENERATED_BANNER + '# Candidate gates\n\n';
70 out += 'The deterministic threshold expressions that turn observability signals into investigation candidates. Pure JS, no LLM. Thresholds live in `lib/gates/*.mjs`.\n\n';