/** * 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); }