fix(okr): gate-herding B2 scoped strict + B3 alle lenkeformer + M2 realpath-confinement + M3 deriveType basename/ord-grense

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-07-16 20:12:35 +02:00
commit 7f9c790f43
6 changed files with 222 additions and 18 deletions

View file

@ -12,7 +12,7 @@
// Zero npm dependencies (node:-builtins).
import { readdirSync, readFileSync, existsSync } from 'node:fs';
import { join, relative } from 'node:path';
import { join, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseFrontmatter } from '../lib/frontmatter.mjs';
import { TYPE_VOCAB } from '../lib/okf-vocab.mjs';
@ -20,9 +20,15 @@ import { resolveBundleLink } from '../lib/okf-links.mjs';
const RECOMMENDED = ['resource', 'title', 'description', 'timestamp'];
// Markdown-lenker [tekst](maal) -- brukt av --strict-ingest til aa validere at
// hvert lenke-maal er en trygg, on-disk bundle-root-relativ .md (anti-RAG-poison).
// Lenke-deteksjon for --strict-ingest: hvert lenke-maal maa vaere en trygg,
// on-disk bundle-root-relativ .md (anti-RAG-poison). B3 (A1): ALLE fire
// standard lenkeformer fanges -- inline, referanse-definisjon, autolink og
// raa HTML-anker -- ikke bare inline (de tre siste lakk foer A1).
const MD_LINK_RE = /\[[^\]]*\]\(([^)]+)\)/g;
const REF_DEF_RE = /^[ \t]{0,3}\[[^\]]+\]:[ \t]*(\S+)/gm;
const AUTOLINK_RE = /<([a-z][a-z0-9+.-]*:[^<>\s]*)>/gi;
const HTML_HREF_RE = /<a\b[^>]*\bhref\s*=\s*["']?([^"'\s>]+)/gi;
const LINK_FORMS = [MD_LINK_RE, REF_DEF_RE, AUTOLINK_RE, HTML_HREF_RE];
// En title som selv baerer en markdown-lenke er en injeksjons-vektor -> avvises.
const TITLE_LINK_RE = /\[[^\]]*\]\([^)]*\)/;
@ -60,8 +66,14 @@ function rootOkfVersion(root) {
// strictIngest (default AV): paa skrivestien handheves det lukkede vokabularet
// + lenke-allow-listen. Lese-siden (default) forblir tolerant (exit 0/1 kun paa
// manglende type) -- denne porten er aktiv KUN ved ingestion (--strict-ingest).
export function checkBundle(root, { strictIngest = false } = {}) {
const concepts = walkConcepts(root);
// files (B2, A1): orkestratoren scoper strict til KUN kjoeringens skrevne filer
// (writeConcepts-returens {concepts, pointers}) -- ALDRI hele roten, som ville
// felt legitimt haandkuratert innhold (lovlig out-of-vocab/eksterne lenker paa
// lese-siden). Uten files: full-root-walk som foer (CLI/lese-siden).
export function checkBundle(root, { strictIngest = false, files } = {}) {
const concepts = Array.isArray(files)
? files.map((f) => resolve(root, f))
: walkConcepts(root);
const missingType = [];
const warnings = [];
const strictErrors = [];
@ -84,12 +96,15 @@ export function checkBundle(root, { strictIngest = false } = {}) {
if (title && TITLE_LINK_RE.test(title)) {
strictErrors.push(`${rel}: lenke-baerende title «${title}»`);
}
// 3. hver markdown-lenke maa resolvere til en trygg, on-disk bundle-fil.
for (const m of raw.matchAll(MD_LINK_RE)) {
const target = m[1];
const resolved = resolveBundleLink(target, root);
if (!resolved) strictErrors.push(`${rel}: utrygg lenke ${target}`);
else if (!existsSync(resolved)) strictErrors.push(`${rel}: dangling lenke ${target}`);
// 3. hvert lenke-maal (alle fire lenkeformer, B3) maa resolvere til en
// trygg, on-disk bundle-fil.
for (const re of LINK_FORMS) {
for (const m of raw.matchAll(re)) {
const target = m[1];
const resolved = resolveBundleLink(target, root);
if (!resolved) strictErrors.push(`${rel}: utrygg lenke ${target}`);
else if (!existsSync(resolved)) strictErrors.push(`${rel}: dangling lenke ${target}`);
}
}
}
for (const field of RECOMMENDED) {