Lukker de fire release-relevante MAJOR-funnene fra /trekreview (S33). Alle TDD
roed -> groenn, ingen fiks landet uten en test som feilet foerst. Suite 163 -> 167.
R1 (af16d5e4) hooks/scripts/coaching-hook.mjs:83 -- LEVENDE REGRESJON innfoert av
1.8.0. Denne releasen skrev status-malen (commands/sporing.md:86-88) om til den
kanoniske skalaen On Track/At Risk/Off Track, mens hooken fortsatt talte kun
/i fare|blokkert/i over tabellrader. En status generert under 1.8.0 ga derfor
atRiskCount = 0 og SessionStart-nudgen sluttet stille aa utloese seg. Hooken
teller naa de kanoniske etikettene; de to norske er BEHOLDT som bakover-
kompatibilitet for status-filer skrevet foer 1.8.0. Nudge-teksten bruker samme
kanoniske vokabular. Ny testcase mates av malen slik den faktisk genereres i dag;
den eksisterende casen beholder gammelt vokabular og daekker legacy-stien.
R2 (7ec575be) F-i-omskrivingen ga hver kommando en Kontekstbevissthet-blokk som
INSTRUERER Glob, men allowed-tools ble kun utvidet i kaskade.md. Glob lagt til i
export, gap, governance, innfoering, kvalitet, moeter, skriv, sporing. Ny vakt-
case (k): nevner BODY verktoeyet, maa frontmatter deklarere det (13 kommandoer
instruerer Glob; alle 13 dekket). Case (i) grepper kun etter fjernede fraser og
kunne ikke fange dette.
R3 (5e61ae0d) tests/package-shape.test.mjs asserterte KUN package.json -- som er
private:true og dermed den ene flaten som aldri shipper. En delvis bump ville
shippet groenn. Ny versjonssync-case dekker .claude-plugin/plugin.json, README-
badgen og begge SKILL.md, med forventet verdi UTLEDET fra package.json (ett sted
aa endre ved neste bump). Mutasjonsbevist: hver av de fire flatene tilbakestilt
til 1.7.1 en om gangen -> casen roed i alle fire tilfeller.
R4 (ccff16e1 + 231c53fc) Tre parallelle confidence-etikettsett overlevde F-c:
fremdriftssporer-agent.md:68 ("Paa sporet / I fare / Blokkert"), :98
("Confidence: [Hoey/Medium/Lav]" -- en annen akse: stoerrelse, ikke sannsynlighet)
og SKILL.md:48 ("blocked"; kanonisk er "off track"). Alle tre erstattet med
referanse til kanon (okr-framework.md:389-392). sporing.md og agenten den
delegerer til svarer naa i samme vokabular. Vakt-case (a)/(b) skanner naa samme
sett som (d) allerede brukte (+ agents/ + SKILL.md), samlet i canonScan(). Ny
case (a2) fanger etikettsett skrevet som bullet eller mal-linje -- tabell-
signaturen alene fanget dem ikke, og det var nettopp formen driften overlevde i.
Divergens gjenkjennes STRUKTURELT (skraastrek-enumerasjon av >= 2 etiketter, med
>= 1 ikke-kanonisk), saa loepende prosa som "For KR i fare" og "Blokkert av
eksterne faktorer" ikke gir falske positive. Verifisert: roed-listen var noeyaktig
de 3 kjente linjene, ingen andre.
CHANGELOG/README: vakt-antall 13 -> 15 cases, suite 149 -> 167, og de fire
fiksene lagt inn under [1.8.0] (Added + Fixed). Ingen versjonsbump.
GJENSTAAR fra reviewet (3 MAJOR, IKKE i denne bolgen): 607313e3 F-d-kadens-
moensteret kan ikke feile paa den kanoniske kadens-tabellen; 029ef814
freshen-references.md:24 baerer fortsatt en foreldet kontekstblokk og vaktens
exclude-kommentar kaller fila feilaktig "allerede-fikset" (den er utenfor
1.8.0-scope by design, jf. ba91fc2 -- kommentaren er usann, ikke ekskluderingen);
7579d59c "Outcome" er utestbar i 10-dims-casen pga. substring-containment.
Verify: node --test --test-reporter=tap tests/*.test.mjs -> 167 pass / 0 fail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016pUfkQ1YyH75z9y6RBaBHG
317 lines
15 KiB
JavaScript
317 lines
15 KiB
JavaScript
// canon-consistency.test.mjs
|
|
// 1.8.0 "En kanon": deterministisk konsistensvakt som laaser F-a..F-i-konsolideringen
|
|
// (review-2026-07-16.md §3). Offline, zero-dep, node:test. Hver case sporer til ett
|
|
// review-funn og blir GROENN naar dens SISTE bidragende fil refererer kanon; RED-baseline
|
|
// etableres foer konsumentene refererer kanon (TDD Iron Law).
|
|
//
|
|
// Norsk markdown-prosa matches via \uXXXX-escapes (test-kilde holdes ASCII-ren; unngaar
|
|
// bash 3.2 set -u multibyte-krasj). Kollekter-til-array -> assert.deepEqual([], msg) som B1.
|
|
// Moenster: tests/reference-integrity.test.mjs + tests/package-shape.test.mjs
|
|
// Kilde: review-2026-07-16.md §3 (F-a..F-i, fil:linje)
|
|
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
const REF = 'skills/okr-offentlig-sektor/references';
|
|
|
|
// --- delte parse-hjelpere (hver med egen sanity-case, jf. B1 refs.length >= 10) ---
|
|
|
|
function readDoc(rel) {
|
|
return readFileSync(join(ROOT, rel), 'utf8');
|
|
}
|
|
|
|
function mdFiles(dir) {
|
|
return readdirSync(join(ROOT, dir))
|
|
.filter((n) => n.endsWith('.md'))
|
|
.sort()
|
|
.map((n) => join(dir, n));
|
|
}
|
|
|
|
const SKILL = 'skills/okr-offentlig-sektor/SKILL.md';
|
|
|
|
// R4 (review.md 231c53fc): kanon-skanningen skal dekke ALLE konsumentflater --
|
|
// references/, commands/, agents/ OG SKILL.md (som ligger ETT nivaa over references/).
|
|
// Case (d) brukte allerede dette settet; (a)/(b) var smalere enn sine egne case-navn
|
|
// lovet, saa etikett-drift i agents/ + SKILL.md var usynlig. Ett felles sett, ett sted.
|
|
function canonScan() {
|
|
return [...mdFiles(REF), ...mdFiles('commands'), ...mdFiles('agents'), SKILL];
|
|
}
|
|
|
|
// Kontig. pipe-tabell-blokker: hver blokk = sammenhengende linjer som (trimmet) starter med '|'.
|
|
function mdTables(body) {
|
|
const tables = [];
|
|
let cur = [];
|
|
for (const line of body.split('\n')) {
|
|
if (line.trim().startsWith('|')) {
|
|
cur.push(line);
|
|
} else if (cur.length) {
|
|
tables.push(cur);
|
|
cur = [];
|
|
}
|
|
}
|
|
if (cur.length) tables.push(cur);
|
|
return tables;
|
|
}
|
|
|
|
function linesMatching(body, re) {
|
|
return body.split('\n').filter((l) => re.test(l));
|
|
}
|
|
|
|
function headingsOf(body, re) {
|
|
return body
|
|
.split('\n')
|
|
.filter((l) => re.test(l))
|
|
.map((l) => l.replace(/^#+\s+/, '').trim());
|
|
}
|
|
|
|
// Confidence-tabell-signatur: en pipe-tabell hvis label-vokabular baerer alle tre nivaaer.
|
|
// ALDRI match paa tokenet "50%" eller ordet "confidence" (unngaar falsk-positiv paa
|
|
// score-aritmetikk / prosa).
|
|
function isConfidenceTable(tableLines) {
|
|
const txt = tableLines.join('\n');
|
|
return /on track/i.test(txt) && /at risk/i.test(txt) && /off track/i.test(txt);
|
|
}
|
|
|
|
// --- F-c: divergerende etikettSETT (R4 / review.md ccff16e1) ---
|
|
// Et parallelt etikettsett gjenkjennes STRUKTURELT som en skraastrek-enumerasjon av
|
|
// statusetiketter -- ikke som loepende prosa. "For KR i fare" og "Blokkert av eksterne
|
|
// faktorer" er legitim prosa; det doktrinen forbyr er den parallelle SKALAEN
|
|
// ("Paa sporet / I fare / Blokkert"). Kanoniske etiketter: okr-framework.md:389-392.
|
|
const CANON_LABELS = ['on track', 'at risk', 'off track'];
|
|
const DIVERGENT_LABELS = ['p\u00e5 sporet', 'i fare', 'blokkert', 'blocked'];
|
|
// Magnitude-aksen (Hoey/Medium/Lav) er en ANNEN akse enn sannsynlighet. Kun flagget naar
|
|
// linjen faktisk snakker om confidence -- en prioritetsskala med samme ord er legitim.
|
|
const MAGNITUDE_SCALE = /(h\u00f8y|medium|lav)\s*\/\s*(h\u00f8y|medium|lav)/i;
|
|
|
|
// Segmenter mellom skraastreker; et segment "er" en etikett naar det (etter stripping av
|
|
// listemarkoer, utheving, klammer og emoji-hale) starter/slutter paa etiketten.
|
|
function labelSegments(line) {
|
|
return line
|
|
.split('/')
|
|
.map((s) => s.replace(/[*_`[\]()]/g, '').replace(/^[\s\-+]*(?:\d+\.)?\s*/, '').trim().toLowerCase())
|
|
.map((s) => {
|
|
const all = [...CANON_LABELS, ...DIVERGENT_LABELS];
|
|
return all.find((l) => s === l || s.startsWith(`${l} `) || s.endsWith(` ${l}`)) ?? null;
|
|
});
|
|
}
|
|
|
|
// Returnerer de divergerende etikettene i en enumerasjon (>= 2 etikett-segmenter), ellers [].
|
|
function divergentEnumeration(line) {
|
|
const hits = labelSegments(line).filter(Boolean);
|
|
if (hits.length < 2) return [];
|
|
return hits.filter((h) => DIVERGENT_LABELS.includes(h));
|
|
}
|
|
|
|
// ==================== parser-sanity (jf. B1) ====================
|
|
|
|
test('parser-sanity: headingsOf finner 10 rubrikk-dimensjoner', () => {
|
|
const dims = headingsOf(readDoc(`${REF}/okr-quality-rubrics.md`), /^### /);
|
|
assert.equal(dims.length, 10, `forventet 10 dims, fant ${dims.length}`);
|
|
});
|
|
|
|
test('parser-sanity: mdTables finner confidence-tabellen i okr-framework.md', () => {
|
|
const conf = mdTables(readDoc(`${REF}/okr-framework.md`)).filter(isConfidenceTable);
|
|
assert.ok(conf.length >= 1, `forventet minst 1 confidence-tabell i framework, fant ${conf.length}`);
|
|
});
|
|
|
|
test('parser-sanity: linesMatching finner check-in-linjer i okr-framework.md', () => {
|
|
const hits = linesMatching(readDoc(`${REF}/okr-framework.md`), /check-?in/i);
|
|
assert.ok(hits.length >= 2, `forventet >= 2 check-in-linjer, fant ${hits.length}`);
|
|
});
|
|
|
|
// ==================== F-a..F-i konsistensvakt ====================
|
|
|
|
// (a) F-c confidence EN gang. RED til calculator-dedup (Step 6).
|
|
// R4: skanne-settet utvidet fra references/+commands/ til canonScan() (som case (d)).
|
|
test('(a) F-c: noeyaktig EN confidence-tabell, kanonisk i okr-framework.md', () => {
|
|
const found = [];
|
|
for (const f of canonScan()) {
|
|
if (mdTables(readDoc(f)).some(isConfidenceTable)) found.push(f);
|
|
}
|
|
assert.deepEqual(
|
|
found,
|
|
[`${REF}/okr-framework.md`],
|
|
`confidence-tabell skal finnes noeyaktig EN gang (okr-framework.md); fant: ${found.join(', ')}`,
|
|
);
|
|
});
|
|
|
|
// (a2) F-c divergerende etikettSETT (R4 / review.md ccff16e1). En tabell-signatur alene
|
|
// fanger ikke parallelle skalaer skrevet som bullet/mal-linje -- det var nettopp formen
|
|
// driften overlevde i (agents/ + SKILL.md).
|
|
test('(a2) F-c: ingen parallelle confidence-etikettsett utenfor kanon', () => {
|
|
const violations = [];
|
|
for (const f of canonScan()) {
|
|
const body = readDoc(f);
|
|
body.split('\n').forEach((line, i) => {
|
|
const divergent = divergentEnumeration(line);
|
|
if (divergent.length > 0) {
|
|
violations.push(`${f}:${i + 1}: divergerende etikettsett (${divergent.join(', ')}): ${line.trim()}`);
|
|
}
|
|
if (/confidence/i.test(line) && MAGNITUDE_SCALE.test(line)) {
|
|
violations.push(`${f}:${i + 1}: magnitude-skala paa confidence-aksen: ${line.trim()}`);
|
|
}
|
|
});
|
|
}
|
|
assert.deepEqual(
|
|
violations,
|
|
[],
|
|
`kanonisk sett = On Track / At Risk / Off Track (okr-framework.md:389-392):\n${violations.join('\n')}`,
|
|
);
|
|
});
|
|
|
|
// (b) F-d kadens strukturell (kadens x publikum). RED til kadens-konsumenter (Step 11-12).
|
|
// Team er default; publikum-markoer ledelse/ledergruppe hever til ledelses-review.
|
|
test('(b) F-d: ingen team-check-in maanedlig, ingen ledelsesreview ukentlig', () => {
|
|
const scan = canonScan(); // R4: utvidet fra references/+commands/
|
|
// Kadens-adjektiv DIREKTE foran check-in (ev. via okr/team-kvalifikator) = team-maanedlig.
|
|
// Strukturell: fanger IKKE "maanedlig 30-min review" (korrekt dobbeltrytme i implementation.md)
|
|
// eller "Maanedlig status | Oboard check-ins" (governance-tabell) — kun kadens->check-in-binding.
|
|
const teamMonthly = /(m\u00e5nedlig|m\u00e5nedlige)\s+(okr\s+|team[-\s])?check-?ins?/i;
|
|
const ledWeekly = /(ukentlig|ukentlige)[^\n]*\b(ledelse|ledergruppe|ledelses)\b/i;
|
|
const violations = [];
|
|
for (const f of scan) {
|
|
const body = readDoc(f);
|
|
for (const l of linesMatching(body, teamMonthly)) violations.push(`${f}: ${l.trim()}`);
|
|
for (const l of linesMatching(body, ledWeekly)) {
|
|
if (/review/i.test(l)) violations.push(`${f}: ${l.trim()}`);
|
|
}
|
|
}
|
|
assert.deepEqual(violations, [], `kadens-motsigelse (kadens x publikum):\n${violations.join('\n')}`);
|
|
});
|
|
|
|
// (c) F-e scoreband == rubrics. RED til kvalitet/kvalitetssjekker refererer rubrics:5 (Step 10).
|
|
// 8-10-baand ELLER "Score 8-10"-kolonne = eget scoreband/inline-rubrikk (rubrics-toppbaand = 9-10).
|
|
test('(c) F-e: kvalitet + kvalitetssjekker baerer ingen egen 8-10 scoreband/inline-rubrikk', () => {
|
|
const files = ['commands/kvalitet.md', 'agents/kvalitetssjekker-agent.md'];
|
|
const hits = [];
|
|
for (const f of files) {
|
|
for (const l of linesMatching(readDoc(f), /8-10/)) hits.push(`${f}: ${l.trim()}`);
|
|
}
|
|
assert.deepEqual(hits, [], `eget scoreband/inline-rubrikk (8-10) skal referere okr-quality-rubrics.md:5:\n${hits.join('\n')}`);
|
|
});
|
|
|
|
// (d) F-g antipattern-antall drift-laas. GROENN i dag (B1 lukket 19->20). Aldri hardkod 20.
|
|
test('(d) F-g: prosa-omtaler av antipattern-antall == dynamisk telling (drift-laas)', () => {
|
|
const anti = readDoc(`${REF}/okr-antipatterns.md`);
|
|
const antipatternCount = linesMatching(anti, /^### \d+\.\d+/).length;
|
|
const categoryCount = linesMatching(anti, /^## \d+\./).length;
|
|
assert.ok(antipatternCount > 0, `parser-sanity: fant ${antipatternCount} antipatterns`);
|
|
assert.ok(categoryCount > 0, `parser-sanity: fant ${categoryCount} kategorier`);
|
|
const scan = canonScan();
|
|
const drift = [];
|
|
for (const f of scan) {
|
|
for (const m of readDoc(f).matchAll(/(\d+)\s+antipatterns\b/gi)) {
|
|
if (Number(m[1]) !== antipatternCount) drift.push(`${f}: "${m[0].trim()}" != ${antipatternCount}`);
|
|
}
|
|
}
|
|
assert.deepEqual(drift, [], `antipattern-antall drift (telt = ${antipatternCount}):\n${drift.join('\n')}`);
|
|
});
|
|
|
|
// (e) F-g kategori-navn. RED til Step 7 (analyse/trendanalytiker bruker de 5 ekte navnene).
|
|
test('(e) F-g: analyse + trendanalytiker uten oppdiktede antipattern-kategorier', () => {
|
|
const files = ['commands/analyse.md', 'agents/trendanalytiker-agent.md'];
|
|
const invented = /Ambisjonsbalanse|Organisatoriske|Offentlig sektor-spesifikke|Offentlig-spesifikke/;
|
|
const hits = [];
|
|
for (const f of files) {
|
|
for (const l of linesMatching(readDoc(f), invented)) hits.push(`${f}: ${l.trim()}`);
|
|
}
|
|
assert.deepEqual(hits, [], `oppdiktede kategorier (ekte: Formulering/Prosess/Kultur/Struktur/Ledelse):\n${hits.join('\n')}`);
|
|
});
|
|
|
|
// (f) F-h binaer/milepael. RED til Step 10. antipatterns = 0 binaer (laas); kvalitet uten Ja/Nei-bullet.
|
|
test('(f) F-h: antipatterns 0 binaer-omtaler (laas) + kvalitet uten Ja/Nei-binaerbullet', () => {
|
|
const antiBinaer = linesMatching(readDoc(`${REF}/okr-antipatterns.md`), /bin\u00e6r/i);
|
|
const kvalBinaer = linesMatching(readDoc('commands/kvalitet.md'), /Ja\/Nei|Bin\u00e6re KR/);
|
|
const problems = [
|
|
...antiBinaer.map((l) => `okr-antipatterns.md (skal ha 0 binaer): ${l.trim()}`),
|
|
...kvalBinaer.map((l) => `kvalitet.md antipattern-bullet: ${l.trim()}`),
|
|
];
|
|
assert.deepEqual(problems, [], `binaer/milepael-inkonsistens:\n${problems.join('\n')}`);
|
|
});
|
|
|
|
// (g) F-e agent 10 dims. RED til Step 10. Utled dimensjonsnavnene, ikke hardkod.
|
|
test('(g) F-e: kvalitetssjekker-agent daekker alle 10 rubrikk-dimensjoner', () => {
|
|
const dims = headingsOf(readDoc(`${REF}/okr-quality-rubrics.md`), /^### /);
|
|
assert.equal(dims.length, 10, `parser-sanity: forventet 10 dims, fant ${dims.length}`);
|
|
const agent = readDoc('agents/kvalitetssjekker-agent.md');
|
|
const missing = dims.filter((d) => !agent.includes(d));
|
|
assert.deepEqual(missing, [], `kvalitetssjekker-agent mangler rubrikk-dims: ${missing.join(', ')}`);
|
|
});
|
|
|
|
// (h) F-a fjerning. RED til Step 7. Score->modenhet-avledning borte; sandbagging-trendsignal til stede.
|
|
test('(h) F-a: ingen score->modenhet-avledning; sandbagging-trendsignal til stede', () => {
|
|
const problems = [];
|
|
for (const f of ['commands/analyse.md', 'agents/trendanalytiker-agent.md']) {
|
|
const body = readDoc(f);
|
|
// Avlednings-stigen identifiseres av de unike etikettene Utforsker + Skalering (ikke
|
|
// "modenhet"-prosa, som selvrapportfeltet beholder).
|
|
if (/Utforsker/i.test(body) && /Skalering/i.test(body)) {
|
|
problems.push(`${f}: score->modenhet-avledning til stede (Utforsker+Skalering)`);
|
|
}
|
|
}
|
|
const trend = readDoc('agents/trendanalytiker-agent.md');
|
|
if (!(/0\.85/.test(trend) && /sandbagging/i.test(trend))) {
|
|
problems.push('trendanalytiker-agent.md: mangler sandbagging-trendsignal (>0.85 over 2+ sykluser)');
|
|
}
|
|
assert.deepEqual(problems, [], `F-a auto-regel I:\n${problems.join('\n')}`);
|
|
});
|
|
|
|
// (i) F-i kontekst. RED til Step 13. Spesifikke fjernede-betingelse-fraser, ALDRI bar "er listet".
|
|
// Ekskluderer allerede-fiksede analyse.md + freshen-references.md (plan §3 case (i)).
|
|
test('(i) F-i: ingen foreldede kontekst-injeksjons-fraser i commands/', () => {
|
|
const phrases = /injiseres automatisk via hook|Hvis relevante filer er listet|aktive OKR-filer er listet|listet i system-kontekst/;
|
|
const exclude = new Set(['commands/analyse.md', 'commands/freshen-references.md']);
|
|
const hits = [];
|
|
for (const f of mdFiles('commands')) {
|
|
if (exclude.has(f)) continue;
|
|
for (const l of linesMatching(readDoc(f), phrases)) hits.push(`${f}: ${l.trim()}`);
|
|
}
|
|
assert.deepEqual(hits, [], `foreldet post-1.6.0 kontekst-blokk (bruk analyse.md:13-20-moenster):\n${hits.join('\n')}`);
|
|
});
|
|
|
|
// (j) score-grenser doc-invariant (ingen kode beregner score). RED til Step 6. To distinkte asserts.
|
|
test('(j) score-grenser: okr-calculator dokumenterer kapp [0,1.0] OG div-paa-null-regel', () => {
|
|
const calc = readDoc(`${REF}/okr-calculator.md`);
|
|
const problems = [];
|
|
const hasCap = /kapp/i.test(calc) || /\[0,?\s*1[.,]0\]/.test(calc) || /maksimalt\s+1[.,]0/i.test(calc);
|
|
const hasDivNull =
|
|
/udefinert/i.test(calc) || /Target\s*==\s*Baseline/i.test(calc) || /0 m\u00e5lbare/i.test(calc);
|
|
if (!hasCap) problems.push('mangler kapp-regel [0, 1.0] (over-/underoppnaaelse)');
|
|
if (!hasDivNull) problems.push('mangler div-paa-null-regel (Target==Baseline / 0 maalbare KR -> udefinert)');
|
|
assert.deepEqual(problems, [], `score-grenser doc-invariant:\n${problems.join('\n')}`);
|
|
});
|
|
|
|
// (k) R2 (review.md 7ec575be): F-i-omskrivingen ga hver kommando en Kontekstbevissthet-blokk
|
|
// som INSTRUERER `Glob`, men allowed-tools ble ikke utvidet tilsvarende -- et direktiv
|
|
// kommandoen ikke kan utfoere. Strukturell invariant: nevner BODY verktoeyet, maa
|
|
// frontmatter deklarere det. Case (i) grepper kun etter fjernede fraser og fanger ikke dette.
|
|
function frontmatterAndBody(rel) {
|
|
const raw = readDoc(rel);
|
|
const m = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(raw);
|
|
return m ? { fm: m[1], body: m[2] } : { fm: '', body: raw };
|
|
}
|
|
|
|
test('(k) R2: kommandoer som instruerer Glob deklarerer Glob i allowed-tools', () => {
|
|
const missing = [];
|
|
let instructing = 0;
|
|
for (const f of mdFiles('commands')) {
|
|
const { fm, body } = frontmatterAndBody(f);
|
|
if (!/\bGlob\b/.test(body)) continue;
|
|
instructing += 1;
|
|
const declared = /^allowed-tools:\s*(.+)$/m.exec(fm);
|
|
const tools = (declared ? declared[1] : '').split(',').map((t) => t.trim());
|
|
if (!tools.includes('Glob')) {
|
|
missing.push(`${f}: allowed-tools = ${declared ? declared[1].trim() : '(mangler)'}`);
|
|
}
|
|
}
|
|
assert.ok(instructing >= 10, `parser-sanity: fant ${instructing} Glob-instruerende kommandoer`);
|
|
assert.deepEqual(
|
|
missing,
|
|
[],
|
|
`Glob instruert i body uten dekning i allowed-tools:\n${missing.join('\n')}`,
|
|
);
|
|
});
|