// authority.mjs — Lag 3: VERIFISERING-INN. Establishes the per-file authority // binding that feeds verify-out rule 3 (authority mismatch — the regression's // root cause) and build-registry's per-URL authority_source field. // // Design (operatør-valg 2026-06-19): HEADER-AS-TRUTH. A file's **Source:** header // IS the designated authority for that file's claims — full stop. We never GUESS // an authority from a file's many cited URLs (251/303 files cite 4–10 URLs, so a // derivation would be a guess, and a wrong authority makes rule 3 misfire). Coverage // grows as files adopt the header (lag 4 writes it for every regenerated file). // // Pure lib (mirrors decisions-io / verify-out / transform): the only imports are // read-only sibling parsers (kb-headers, url-normalize). It NEVER writes. import { parseSourceHeader } from './kb-headers.mjs'; import { normalizeUrl } from './url-normalize.mjs'; const RE_HTTP_URL = /^https?:\/\/\S+$/i; /** * The designated authority for a file's claims = its declared **Source:** header, * IF that value is a real http(s) URL. A free-text "source" is not an authority * binding (verifiseringsplikt: no guessed authority). The URL is returned as * declared (not normalized) — it may legitimately be a non-Microsoft authority * (e.g. an EU regulation for a governance file); verify-out normalizes when it * compares. Returns null when absent or not a URL. * @param {string} content — full reference-file content (header scanned) * @returns {string|null} */ export function resolveAuthority(content) { const src = parseSourceHeader(content); if (!src) return null; return RE_HTTP_URL.test(src.trim()) ? src.trim() : null; } /** * Collect the set of declared authority sources across many files, normalized to * match registry keys. Only learn.microsoft.com authorities survive normalization * (normalizeUrl returns null otherwise) — which is correct, since the registry * only tracks Microsoft Learn URLs. A non-MS authority is a valid per-file binding * but is simply not a tracked registry entry to mark. * @param {string[]} contents — reference-file contents * @returns {Set} normalized declared-source URLs */ export function collectDeclaredSources(contents) { const set = new Set(); for (const c of contents || []) { const a = resolveAuthority(c); if (!a) continue; const n = normalizeUrl(a); if (n) set.add(n); } return set; } /** * The authority_source value for a registry URL entry: the URL itself when some * file declares it as its **Source:** (i.e. this URL is a designated authority), * else null. The registry is URL-keyed, so storing the URL is type-consistent with * the change.authority_source field rule 3 consumes; null keeps the field honest * where no file vouches for the URL. * @param {string} normUrl — a normalized registry URL key * @param {Set} declaredSet — output of collectDeclaredSources * @returns {string|null} */ export function markUrlAuthority(normUrl, declaredSet) { return declaredSet && declaredSet.has(normUrl) ? normUrl : null; } /** * Coverage metric for the criterion ("authority_source-dekning > 0%"). * @param {{urls: object}} registry * @returns {{withAuthority: number, total: number, ratio: number}} */ export function authorityCoverage(registry) { const urls = (registry && registry.urls) || {}; const entries = Object.values(urls); const withAuthority = entries.filter((e) => e && e.authority_source).length; const total = entries.length; return { withAuthority, total, ratio: total ? withAuthority / total : 0 }; }