feat(ms-ai-architect): Spor 3 Port 2 — create-time-guard (born-verified contract) [skip-docs]

Type-aware create-guard for reference-filer (Spor 3 Port 1/Port 2):

- kb-headers.mjs: parseTypeHeader/parseVerifiedHeader/parseVerifiedByHeader
  (samme top-500-byte bold-label-skann som parseSourceHeader).
- transform.mjs: buildKbHeader er type-aware (reference krever
  source+verified+verified_by; non-reference kaster pa MS-Learn-source) +
  emitterer Type/Verified/Verified by; validateKbFile type-aware;
  stampVerifiedMeta = fodt-verifisert-gate (stempler KUN ved bestatt
  judge-verdikt, ellers kaster).
- validate-kb-file.mjs (ny): kjorbar gate generatorer kaller for commit
  (exit != 0 ved kontraktbrudd).
- generate-skills.md + kb-update.md + transform-prompt.md: wiret til
  fodt-verifisert kontrakt (header-felt + judge-steg + create-guard-gate).

Suite 586/586 (33 nye + 1 invariant). Plugin-validering 239/0/0.

Utenfor scope (flagget): generate-skills.sh legacy (sonnet/Cosmo/no-source),
Cosmo-persona i generatorene (S-Cosmo), korpus-migrering av 306 filer (Spor 1).
This commit is contained in:
Kjell Tore Guttormsen 2026-06-29 10:36:21 +02:00
commit 5a0ba1fc9e
10 changed files with 621 additions and 54 deletions

View file

@ -15,12 +15,38 @@
// validateKbFile uses the SAME source detection as build-registry — one source of
// truth for "what counts as a Source header" (the top-500-byte region).
import { parseSourceHeader } from './kb-headers.mjs';
import {
parseSourceHeader,
parseTypeHeader,
parseVerifiedHeader,
parseVerifiedByHeader,
} from './kb-headers.mjs';
import { getCategorySkill } from './taxonomy.mjs';
// Mandatory header fields. Status is obligatory per spec ("status-felt obligatorisk");
// Source is what lets lag 3 backfill authority_source and lag-5 rule 3 eventually fire.
const REQUIRED_META = ['title', 'status', 'category', 'source', 'lastUpdated'];
// Base mandatory header fields for EVERY file type. Status is obligatory per spec
// ("status-felt obligatorisk").
const REQUIRED_BASE_META = ['title', 'status', 'category', 'lastUpdated'];
// Spor 3 Port 1 — the frontmatter-contract `type`. Only `reference` files are in the
// correctness scope (they carry a Microsoft source we verify against). Template /
// methodology / regulatory are out of scope and carry NO Microsoft source.
const VALID_TYPES = ['reference', 'template', 'methodology', 'regulatory'];
const DEFAULT_TYPE = 'reference';
// Extra fields a `reference` file MUST carry to be "born verified" (Spor 3 Port 2):
// `source` lets lag-5 authority checks fire; `verified`/`verified_by` record that the
// content cleared the judge before it shipped. A reference file without these is the
// drift-introducing failure mode the create-guard exists to stop.
const REQUIRED_REFERENCE_META = ['source', 'verified', 'verified_by'];
// The current GATE-PASS claim judge (judge-claim-prompt-v2). stampVerifiedMeta records
// `verified_by: judge-v<JUDGE_VERSION>` unless the caller overrides or marks `human`.
const JUDGE_VERSION = 2;
// A Microsoft Learn / docs authority URL — a non-reference file may NOT carry one.
const MS_LEARN_RE = /\b(?:learn|docs)\.microsoft\.com/i;
const DATE_RE = /^\d{4}-\d{2}(?:-\d{2})?$/;
// A reference file longer than this many lines should carry a table of contents
// (best-practices §Structure). MIRRORS eval.mjs N4_LARGE_FILE_LINES — kept in sync
@ -60,33 +86,84 @@ function slugify(heading) {
}
/**
* Build the deterministic KB-file header block. Status and Source live IN the
* header (top 500 bytes) so kb-headers.parseSourceHeader and the "Last updated:"
* scanner can see them the old bottom-of-file source section was invisible to
* those scanners, which is why authority_source coverage was 0%.
* Build the deterministic KB-file header block. Status, Source and the Port 1 contract
* fields (Type / Verified / Verified by) live IN the header (top 500 bytes) so
* kb-headers.* and the "Last updated:" scanner can see them the old bottom-of-file
* source section was invisible to those scanners, which is why authority_source
* coverage was 0%.
*
* @param {{title: string, status: string, category: string, source: string,
* lastUpdated: string}} meta
* Type-aware create-guard (Spor 3 Port 2): a `reference` file MUST carry
* source + verified + verified_by (it is "born verified"); a non-reference file
* (template/methodology/regulatory) is out of correctness scope and may NOT carry a
* Microsoft Learn source. Either violation is a hard throw the create-time gate.
*
* @param {{title: string, status: string, category: string, lastUpdated: string,
* type?: string, source?: string, verified?: string, verified_by?: string}} meta
* @returns {string} the header block, ending with the `---` rule + trailing newline
* @throws if any mandatory field is missing/empty
* @throws if the type is unknown, a mandatory field is missing, or a non-reference
* file carries a Microsoft Learn source
*/
export function buildKbHeader(meta) {
const m = meta ?? {};
const missing = REQUIRED_META.filter((k) => {
const type = m.type ?? DEFAULT_TYPE;
if (!VALID_TYPES.includes(type)) {
throw new Error(`buildKbHeader: invalid type '${type}' (expected ${VALID_TYPES.join('|')})`);
}
const isReference = type === 'reference';
const empty = (k) => {
const v = m[k];
return v === undefined || v === null || String(v).trim() === '';
});
};
const required = isReference
? [...REQUIRED_BASE_META, ...REQUIRED_REFERENCE_META]
: REQUIRED_BASE_META;
const missing = required.filter(empty);
if (missing.length) {
throw new Error(`buildKbHeader: missing mandatory field(s): ${missing.join(', ')}`);
}
return (
if (!isReference && !empty('source') && MS_LEARN_RE.test(String(m.source))) {
throw new Error(
`buildKbHeader: a ${type} file may not carry a Microsoft Learn source (out of correctness scope)`,
);
}
let header =
`# ${m.title}\n\n` +
`**Last updated:** ${m.lastUpdated}\n` +
`**Status:** ${m.status}\n` +
`**Category:** ${m.category}\n` +
`**Source:** ${m.source}\n\n` +
`---\n`
);
`**Type:** ${type}\n`;
if (!empty('source')) header += `**Source:** ${m.source}\n`;
if (!empty('verified')) header += `**Verified:** ${m.verified}\n`;
if (!empty('verified_by')) header += `**Verified by:** ${m.verified_by}\n`;
return `${header}\n---\n`;
}
/**
* Born-verified gate (Spor 3 Port 2): stamp `verified`/`verified_by` onto a reference
* meta IFF the judge verdict passed otherwise REFUSE (throw). This encodes
* "nytt/regenerert innhold kjøres gjennom judgen FØR commit født verifisert; ellers
* ingen fil". transform.mjs never calls the judge itself (pure lib); the command runs
* the v2 claim judge, aggregates per-claim results into `{pass}`, and calls this. A
* failing verdict means no file is born.
*
* @param {object} baseMeta the reference meta (title/status/category/source/lastUpdated)
* @param {{pass: boolean, judgeVersion?: number, by?: 'human'}} verdict
* @param {string} today YYYY-MM or YYYY-MM-DD, supplied by the caller (lib stays pure)
* @returns {object} baseMeta with type='reference' + verified + verified_by set
* @throws if the verdict is not passing, or today is not a valid date
*/
export function stampVerifiedMeta(baseMeta, verdict, today) {
if (!verdict || verdict.pass !== true) {
throw new Error('stampVerifiedMeta: refusing to stamp verified — judge verdict not passing');
}
if (!DATE_RE.test(String(today ?? ''))) {
throw new Error(`stampVerifiedMeta: invalid today date '${today}' (expected YYYY-MM[-DD])`);
}
const verifiedBy =
verdict.by === 'human'
? 'human'
: `judge-v${Number.isInteger(verdict.judgeVersion) ? verdict.judgeVersion : JUDGE_VERSION}`;
return { ...baseMeta, type: 'reference', verified: today, verified_by: verifiedBy };
}
/**
@ -181,10 +258,16 @@ const RE_STATUS = /\*\*Status:\*\*\s*\S/i;
/**
* Validate that a candidate KB file carries the mandatory contract: a title, a
* "Last updated:" header (so it is never dateless never the 'unverified' bucket),
* a Status field, a Source header in the scannable region, and for a large file
* (>100 lines, per N4) a table of contents. The TOC clause is the write-path
* regression gate: a regenerated large file that lost its TOC fails here and the
* caller re-prompts, so KB-update can never silently strip a TOC back out.
* a Status field, and for a large file (>100 lines, per N4) a table of contents.
* The TOC clause is the write-path regression gate: a regenerated large file that lost
* its TOC fails here and the caller re-prompts, so KB-update can never silently strip a
* TOC back out.
*
* Type-aware (Spor 3 Port 2): a `reference` file MUST additionally carry source +
* verified + verified_by (born-verified). A file with no `**Type:**` header is treated
* as a reference the create path always stamps Type, and the legacy corpus (no Type)
* is migrated under Spor 1, where this same contract is the target. Non-reference files
* (template/methodology/regulatory) are out of correctness scope and need no source.
*
* @param {string} content full file content
* @returns {{valid: boolean, missing: string[]}}
@ -195,7 +278,12 @@ export function validateKbFile(content) {
if (!RE_TITLE.test(s)) missing.push('title');
if (!RE_LAST_UPDATED.test(s)) missing.push('last_updated');
if (!RE_STATUS.test(s)) missing.push('status');
if (parseSourceHeader(s) === null) missing.push('source');
const type = parseTypeHeader(s) ?? DEFAULT_TYPE;
if (type === 'reference') {
if (parseSourceHeader(s) === null) missing.push('source');
if (parseVerifiedHeader(s) === null) missing.push('verified');
if (parseVerifiedByHeader(s) === null) missing.push('verified_by');
}
if (isLargeContent(s) && !hasToc(s)) missing.push('toc');
return { valid: missing.length === 0, missing };
}