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
143 lines
6.5 KiB
JavaScript
143 lines
6.5 KiB
JavaScript
// tests/kb-update/test-authority.test.mjs
|
|
// TDD for lag 3 — VERIFISERING-INN. lib/authority.mjs establishes the per-file
|
|
// authority binding: the **Source:** header IS the designated authority for a
|
|
// file's claims (operatør-valg 2026-06-19: header-as-truth). This is what feeds
|
|
// verify-out rule 3 (authority mismatch) — the regression's root cause — and what
|
|
// build-registry uses to populate the per-URL authority_source field.
|
|
//
|
|
// Pure lib (mirrors decisions-io / verify-out / transform): no write-util imports.
|
|
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import {
|
|
resolveAuthority,
|
|
collectDeclaredSources,
|
|
markUrlAuthority,
|
|
authorityCoverage,
|
|
} from '../../scripts/kb-update/lib/authority.mjs';
|
|
import { classifyChange } from '../../scripts/kb-update/lib/verify-out.mjs';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
|
const FILE_WITH_SOURCE =
|
|
'# Tittel\n\n**Last updated:** 2026-06\n**Status:** GA\n' +
|
|
'**Source:** https://learn.microsoft.com/azure/search/hybrid-search-overview\n\n---\n\n## Innhold\n';
|
|
|
|
// --- resolveAuthority: the **Source:** header is the per-file authority ---------
|
|
|
|
test('resolveAuthority returns the declared **Source:** URL', () => {
|
|
assert.equal(
|
|
resolveAuthority(FILE_WITH_SOURCE),
|
|
'https://learn.microsoft.com/azure/search/hybrid-search-overview',
|
|
);
|
|
});
|
|
|
|
test('resolveAuthority returns null when no Source header is present', () => {
|
|
assert.equal(resolveAuthority('# Tittel\n\n**Status:** GA\n\n---\n'), null);
|
|
});
|
|
|
|
test('resolveAuthority rejects a non-URL Source value (no guessed authority)', () => {
|
|
// Verifiseringsplikt: a free-text "source" is not an authority binding.
|
|
assert.equal(resolveAuthority('# T\n\n**Source:** se Microsoft-dokumentasjonen\n\n---\n'), null);
|
|
});
|
|
|
|
test('resolveAuthority accepts a non-Microsoft authority (e.g. EU regulation)', () => {
|
|
// Governance files may anchor on the regulation itself, not a learn.microsoft.com page.
|
|
const c = '# T\n\n**Source:** https://eur-lex.europa.eu/eli/reg/2024/1689/oj\n\n---\n';
|
|
assert.equal(resolveAuthority(c), 'https://eur-lex.europa.eu/eli/reg/2024/1689/oj');
|
|
});
|
|
|
|
// --- collectDeclaredSources: normalized set for registry matching --------------
|
|
|
|
test('collectDeclaredSources returns the normalized MS-Learn sources declared across files', () => {
|
|
const set = collectDeclaredSources([
|
|
FILE_WITH_SOURCE,
|
|
'# B\n\n**Source:** https://learn.microsoft.com/azure/foundry/openai/how-to/batch-blob-storage\n\n---\n',
|
|
'# C — no source\n\n**Status:** GA\n\n---\n',
|
|
'# D — non-MS authority\n\n**Source:** https://eur-lex.europa.eu/x\n\n---\n',
|
|
]);
|
|
assert.ok(set.has('https://learn.microsoft.com/azure/search/hybrid-search-overview'));
|
|
assert.ok(set.has('https://learn.microsoft.com/azure/foundry/openai/how-to/batch-blob-storage'));
|
|
// non-MS authority is not a tracked registry URL → excluded from the matching set
|
|
assert.equal(set.size, 2);
|
|
});
|
|
|
|
test('collectDeclaredSources normalizes (trailing slash / locale) to match registry keys', () => {
|
|
const set = collectDeclaredSources([
|
|
'# T\n\n**Source:** https://learn.microsoft.com/en-us/azure/search/hybrid-search-overview/\n\n---\n',
|
|
]);
|
|
assert.ok(set.has('https://learn.microsoft.com/azure/search/hybrid-search-overview'));
|
|
});
|
|
|
|
// --- markUrlAuthority: a URL entry is an authority iff a file declares it -------
|
|
|
|
test('markUrlAuthority returns the URL when it is a declared authority, else null', () => {
|
|
const declared = new Set(['https://learn.microsoft.com/azure/search/hybrid-search-overview']);
|
|
assert.equal(
|
|
markUrlAuthority('https://learn.microsoft.com/azure/search/hybrid-search-overview', declared),
|
|
'https://learn.microsoft.com/azure/search/hybrid-search-overview',
|
|
);
|
|
assert.equal(markUrlAuthority('https://learn.microsoft.com/azure/other/page', declared), null);
|
|
});
|
|
|
|
// --- authorityCoverage: the coverage metric (criterion: > 0%) ------------------
|
|
|
|
test('authorityCoverage counts entries with a non-null authority_source', () => {
|
|
const registry = {
|
|
urls: {
|
|
'https://learn.microsoft.com/a': { authority_source: 'https://learn.microsoft.com/a' },
|
|
'https://learn.microsoft.com/b': { authority_source: null },
|
|
'https://learn.microsoft.com/c': { authority_source: 'https://learn.microsoft.com/c' },
|
|
},
|
|
};
|
|
const cov = authorityCoverage(registry);
|
|
assert.equal(cov.withAuthority, 2);
|
|
assert.equal(cov.total, 3);
|
|
assert.ok(cov.ratio > 0);
|
|
});
|
|
|
|
// --- END-TO-END: header → authority → verify-out rule 3 (the criterion) --------
|
|
|
|
test('rule 3 fires end-to-end on a real authority: source_url ≠ header authority → flagged', () => {
|
|
// A NON-status change so rules 1 (status-gate) and 2 (refutation) stay silent —
|
|
// this isolates rule 3 and proves the header→authority→mismatch path works.
|
|
const authority = resolveAuthority(FILE_WITH_SOURCE);
|
|
const verdict = classifyChange({
|
|
field: 'description',
|
|
old_value: 'En setning.',
|
|
new_value: 'En annen setning.',
|
|
source_url: 'https://learn.microsoft.com/azure/search/some-other-tangential-page',
|
|
authority_source: authority,
|
|
refutations: [],
|
|
});
|
|
assert.equal(verdict.verdict, 'flagged');
|
|
assert.equal(verdict.status_claim, false); // not a status claim — rule 3 alone flagged it
|
|
assert.ok(verdict.reasons.some((r) => /authority/i.test(r)), `expected an authority reason: ${verdict.reasons}`);
|
|
});
|
|
|
|
test('rule 3 stays silent when source_url matches the header authority', () => {
|
|
const authority = resolveAuthority(FILE_WITH_SOURCE);
|
|
const verdict = classifyChange({
|
|
field: 'description',
|
|
old_value: 'En setning.',
|
|
new_value: 'En annen setning.',
|
|
source_url: authority, // same as authority → no mismatch
|
|
authority_source: authority,
|
|
refutations: [],
|
|
});
|
|
assert.equal(verdict.verdict, 'auto-applied');
|
|
});
|
|
|
|
// --- ARCHITECTURE INVARIANT: authority.mjs imports NO write-utils --------------
|
|
|
|
test('authority.mjs imports NO write-utils (pure, never writes)', () => {
|
|
const src = readFileSync(join(__dirname, '../../scripts/kb-update/lib/authority.mjs'), 'utf8');
|
|
const importLines = src.split('\n').filter((l) => /^\s*import\b/.test(l)).join('\n');
|
|
assert.doesNotMatch(importLines, /\bsaveRegistry\b/);
|
|
assert.doesNotMatch(importLines, /\bsaveDecisions\b/);
|
|
assert.doesNotMatch(importLines, /from\s+'[^']*atomic-write[^']*'/);
|
|
assert.doesNotMatch(importLines, /from\s+'[^']*\/backup\.mjs'/);
|
|
});
|