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>
113 lines
4.3 KiB
JavaScript
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);
|
|
}
|