config-audit/scanners/lib/best-practices-register.mjs
Kjell Tore Guttormsen 55f83a3c99 feat(knowledge): best-practices register foundation — v5.7 Fase 1 Chunk 1
Add knowledge/best-practices.json: a machine-readable, provenance-stamped, schema-validated best-practices register — the source of truth for the upcoming v5.7 optimization lens (CA-OPT). 13 seed entries migrated from the v5.5 V-rows (loading-model + compaction facts) and the Anthropic 'Steering Claude Code' blog (mechanism-fit rules); each entry carries source.url + verified date + confidence. Only confirmed claims are user-facing (Verifiseringsplikt).

scanners/lib/best-practices-register.mjs: zero-dependency loader + validator (loadRegister/validateRegister/getEntry, native JSON.parse — not YAML, since the repo is zero-dep and yaml-parser.mjs can't parse arrays-of-objects). tests/lib/best-practices-register.test.mjs: 22 tests (schema, provenance integrity, negative cases, lookup).

Byte-stable: no scanner consumes the register yet (Chunk 2), so all scanner output is unchanged. Suite 1023->1045, self-audit A/A, readmeCheck passed. Full design: docs/v5.7-optimization-lens-plan.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:37:47 +02:00

113 lines
4.3 KiB
JavaScript

/**
* best-practices-register — loader + schema validator for the machine-readable
* best-practices register (knowledge/best-practices.json).
*
* The register is the SOURCE OF TRUTH for the v5.7 optimization lens (CA-OPT). Each entry
* is provenance-stamped (source.url + source.verified) and carries a confidence; only
* CONFIRMED claims are surfaced user-facing (Verifiseringsplikt). Zero-dependency: native
* JSON, validated by hand here. See docs/v5.7-optimization-lens-plan.md.
*/
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { SEVERITY } from './severity.mjs';
/** Absolute path to the bundled register. */
export const REGISTER_PATH = fileURLToPath(
new URL('../../knowledge/best-practices.json', import.meta.url)
);
/** Confidence levels. Only `confirmed` is consumed user-facing by the lens. */
export const CONFIDENCE_LEVELS = Object.freeze(['confirmed', 'inferred', 'unverified']);
const VALID_SEVERITIES = new Set(Object.keys(SEVERITY));
const ID_RE = /^BP-[A-Z]+-\d{3}$/;
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
/**
* Load + parse a register file (defaults to the bundled one). Throws on missing file or
* invalid JSON — callers that want graceful handling should try/catch.
* @param {string} [path]
* @returns {{version:number, entries:object[]}}
*/
export function loadRegister(path = REGISTER_PATH) {
return JSON.parse(readFileSync(path, 'utf8'));
}
/**
* Validate a parsed register against the schema. Never throws — returns a result so the
* caller (and tests) can inspect every problem at once.
* @param {unknown} data
* @returns {{valid:boolean, errors:string[]}}
*/
export function validateRegister(data) {
const errors = [];
if (!data || typeof data !== 'object' || Array.isArray(data)) {
return { valid: false, errors: ['register must be an object'] };
}
if (typeof data.version !== 'number') errors.push('version must be a number');
if (!Array.isArray(data.entries)) {
errors.push('entries must be an array');
return { valid: false, errors };
}
const seen = new Set();
data.entries.forEach((e, i) => {
const at = `entry[${i}]${e && typeof e === 'object' && e.id ? ` (${e.id})` : ''}`;
if (!e || typeof e !== 'object' || Array.isArray(e)) {
errors.push(`${at}: must be an object`);
return;
}
// id — required, BP-TOPIC-NNN, unique
if (typeof e.id !== 'string' || !ID_RE.test(e.id)) {
errors.push(`${at}: id must match BP-TOPIC-NNN`);
} else if (seen.has(e.id)) {
errors.push(`${at}: duplicate id`);
} else {
seen.add(e.id);
}
// claim — required, non-empty
if (typeof e.claim !== 'string' || e.claim.trim() === '') {
errors.push(`${at}: claim is required`);
}
// confidence — required, enum
if (!CONFIDENCE_LEVELS.includes(e.confidence)) {
errors.push(`${at}: confidence must be one of ${CONFIDENCE_LEVELS.join('|')}`);
}
// source — required object with url + verified date (provenance)
if (!e.source || typeof e.source !== 'object' || Array.isArray(e.source)) {
errors.push(`${at}: source is required`);
} else {
if (typeof e.source.url !== 'string' || e.source.url.trim() === '') {
errors.push(`${at}: source.url is required`);
}
if (typeof e.source.verified !== 'string' || !DATE_RE.test(e.source.verified)) {
errors.push(`${at}: source.verified must be a YYYY-MM-DD date`);
}
}
// optional fields — typed only when present
if (e.severity !== undefined && !VALID_SEVERITIES.has(e.severity)) {
errors.push(`${at}: severity must be one of ${[...VALID_SEVERITIES].join('|')}`);
}
for (const key of ['mechanism', 'appliesTo', 'recommendation', 'category']) {
if (e[key] !== undefined && typeof e[key] !== 'string') {
errors.push(`${at}: ${key} must be a string`);
}
}
if (e.lensCheck !== undefined && e.lensCheck !== null && typeof e.lensCheck !== 'string') {
errors.push(`${at}: lensCheck must be a string or null`);
}
});
return { valid: errors.length === 0, errors };
}
/**
* Look up an entry by id.
* @param {{entries:object[]}} register
* @param {string} id
* @returns {object|undefined}
*/
export function getEntry(register, id) {
if (!register || !Array.isArray(register.entries)) return undefined;
return register.entries.find((e) => e.id === id);
}