Lag 3 etablerer per-fil autoritetsbinding som mater verify-out regel 3 (autoritets-mismatch — regresjonens rotårsak, inert til nå pga 0% dekning). Designvalg (operatør 2026-06-19): HEADER-AS-TRUTH — en fils **Source:**-header ER den utpekte autoriteten. Gjetter ALDRI fra siterte URLer (251/303 filer siterer 4-10 → gjetning ville fått regel 3 til å bomme + 303 operatør-beslutninger = primær-fallgruven). - lib/authority.mjs (ren lib, SKRIVER ALDRI; kun read-only kb-headers+url-normalize): resolveAuthority (validert http(s); fri tekst avvist; ikke-MS-autoritet som EU-forordning tillatt), collectDeclaredSources (normalisert Set), markUrlAuthority, authorityCoverage. - build-registry: recomputer authority_source per URL-entry fra headere (ikke carry-over) + dekningslogg. - commands/kb-update.md §4 c1: authority_source=resolveAuthority(fil) → lag-5 c2. TDD: 11 tester FØR kode inkl. import-invariant + ende-til-ende regel 3 (source_url ≠ header-autoritet → flagged). Tester: validate 239 · kb-update 122 (+11) · kb-eval 15 · kb-integrity 115/115. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01REiKFhP4w6xGXXqWKpPCJJ
80 lines
3.5 KiB
JavaScript
80 lines
3.5 KiB
JavaScript
// 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<string>} 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<string>} 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 };
|
||
}
|