fix(okr): B2 ingestion-kode-hygiene + bump 1.7.1 (patch-lane)
8 hygiene-fikser fra review 2026-07-16 par.4 kode-lista, TDD roed-groenn (suite 138 -> 149): - innboks-split: slugify translittererer ae/oe (datatap-fiks) - innboks-frontmatter: beskrivende feil ved manglende sourceMtime - okf-index: --okf-version bumper eksisterende rot, flagg-tolerant CLI (exit 2 ved manglende verdi), sanitizeEntry strip C1/zero-width/bidi/ Unicode-tag - write-org-profile: circuit-breaker MERGER i stedet for aa overskrive full config (M4); test beviser at eksisterende config overlever - compose-org-profile: intern ----linje trunkerer ikke blokken - coaching-hook: at-risk teller status-markerte tabellrader (M1/m1) - inject-okr-context: topic-guard treffer boeyningsformer (maalene) - frontmatter: BOM/CRLF-toleranse (falsk mangler-type-fiks) Versjonsflater bumpet til 1.7.1 (package/plugin/lock/CLAUDE/README/ SKILL x2/package-shape-test) + CHANGELOG 1.7.1-seksjon (B1+B2). Release-tag + katalog-ref venter paa [G-B] operatoer-go. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
6028ac2f90
commit
482effbad1
25 changed files with 289 additions and 29 deletions
|
|
@ -90,6 +90,31 @@ test('OKR_NOW midtveis fase: mid-coaching', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('at-risk telles per status-RAD (tabell), ikke raaforekomster i prosa (B2/M1)', () => {
|
||||
withWork((work) => {
|
||||
writeConfig(work, '---\nid: "T2-2026"\n---\n');
|
||||
const statusDir = join(work, '.claude', 'okr', 'syklus', 'T2-2026');
|
||||
mkdirSync(statusDir, { recursive: true });
|
||||
// 2 markerte rader + markoer-ord i forklaring OG prosa: kun radene skal telle.
|
||||
writeFileSync(join(statusDir, 'status.md'), [
|
||||
'# Status T2-2026',
|
||||
'',
|
||||
'Merk: "I fare" betyr flat trend; "Blokkert" betyr ingen fremgang.',
|
||||
'',
|
||||
'| KR | Baseline | Maal | Naa | Score | Status |',
|
||||
'|----|----------|------|-----|-------|--------|',
|
||||
'| KR1: Redusere ulykker | 40 | 30 | 35 | 0.50 | I fare |',
|
||||
'| KR2: Oppetid | 10 | 25 | 22 | 0.80 | Paa sporet |',
|
||||
'| KR3: Tilfredshet | 60% | 90% | 65% | 0.17 | Blokkert |',
|
||||
'',
|
||||
'KR1 er i fare fordi trenden er flat. KR3 er blokkert av leverandoer.',
|
||||
'',
|
||||
].join('\n'));
|
||||
const out = runHook(work, '2026-06-20'); // midtveis -> at-risk rapporteres
|
||||
assert.match(out, /OBS: 2 KR er merket/, '2 markerte rader -> teller noeyaktig 2 (ikke 6)');
|
||||
});
|
||||
});
|
||||
|
||||
test('OKR_NOW sen fase: late-coaching', () => {
|
||||
withWork((work) => {
|
||||
writeConfig(work, '---\nid: "T2-2026"\n---\n');
|
||||
|
|
|
|||
|
|
@ -88,6 +88,15 @@ test('OKF fler-linje tags-liste: krasjer ikke + folgende skalar resolver', () =>
|
|||
);
|
||||
});
|
||||
|
||||
test('BOM + CRLF: frontmatter parses (falsk "mangler type"-fiks, B2)', () => {
|
||||
// Windows-produsert fil: UTF-8 BOM foran forste fence + CRLF-linjeskift.
|
||||
const content = '\uFEFF---\r\ntype: Notat\r\ntitle: "X"\r\n---\r\n# body\r\n';
|
||||
const { raw, get } = parseFrontmatter(content);
|
||||
assert.notEqual(raw, null, 'BOM/CRLF skal ikke gi raw null');
|
||||
assert.equal(get('type'), 'Notat');
|
||||
assert.equal(get('title'), 'X');
|
||||
});
|
||||
|
||||
test('ingen frontmatter: raw null, get returnerer null', () => {
|
||||
const { raw, get } = parseFrontmatter('ingen frontmatter her\n');
|
||||
assert.equal(raw, null);
|
||||
|
|
|
|||
|
|
@ -146,6 +146,25 @@ test('projectFrontmatter: kompound-ord i title matcher ikke vokab-term (Statusno
|
|||
);
|
||||
});
|
||||
|
||||
// --- B2 (1.7.1): manglende sourceMtime skal feile beskrivende, ikke RangeError ---
|
||||
|
||||
test('projectFrontmatter: manglende sourceMtime -> beskrivende feil som navngir opsjonen', () => {
|
||||
const concept = { sourceSlug: 'x', title: 'Notat', slug: 'notat', level: 1, body: 'x' };
|
||||
assert.throws(
|
||||
() => projectFrontmatter(concept, { sourcePath: 'innboks/x.txt' }),
|
||||
/sourceMtime/,
|
||||
'feilen skal navngi sourceMtime (ikke en naken RangeError fra toISOString)',
|
||||
);
|
||||
});
|
||||
|
||||
test('projectFrontmatter: ugyldig sourceMtime (Invalid Date) -> samme beskrivende feil', () => {
|
||||
const concept = { sourceSlug: 'x', title: 'Notat', slug: 'notat', level: 1, body: 'x' };
|
||||
assert.throws(
|
||||
() => projectFrontmatter(concept, { sourcePath: 'innboks/x.txt', sourceMtime: new Date('ugyldig') }),
|
||||
/sourceMtime/,
|
||||
);
|
||||
});
|
||||
|
||||
test('projectFrontmatter: helt ord i title matcher fortsatt (Status for KR -> Status)', () => {
|
||||
const concept = { sourceSlug: 's', title: 'Status for KR', slug: 'status-for-kr', level: 1, body: 'x' };
|
||||
const e = projectFrontmatter(concept, { sourcePath: 'innboks/s.txt', sourceMtime: MTIME });
|
||||
|
|
|
|||
|
|
@ -67,6 +67,13 @@ test('splitConcepts: slug-kollisjon -> stabil numerisk disambiguering', () => {
|
|||
assert.equal(concepts[1].slug, 'samme-tittel-2', 'andre forekomst faar -2 suffiks');
|
||||
});
|
||||
|
||||
test('splitConcepts: slug translittererer ae/oe -- norske bokstaver dropper ikke (B2)', () => {
|
||||
// "Økonomi og ærlighet" -- foer B2 ga slugify 'konomi-og-rlighet' (datatap).
|
||||
const md = '# Økonomi og ærlighet\n\nInnhold.';
|
||||
const concepts = splitConcepts(md, { sourceSlug: 'norsk' });
|
||||
assert.equal(concepts[0].slug, 'oekonomi-og-aerlighet', 'OE->oe, ae->ae (translitterert, ikke strippet)');
|
||||
});
|
||||
|
||||
test('splitConcepts: preamble foer foerste heading bevares som ledende konsept', () => {
|
||||
const md = 'Forord uten overskrift.\n\n# Ekte overskrift\n\nKropp.';
|
||||
const concepts = splitConcepts(md, { sourceSlug: 'med-forord' });
|
||||
|
|
|
|||
|
|
@ -312,6 +312,74 @@ test('okf-index: innboks/ + dot-katalog -> ingen egen index.md OG ikke listet i
|
|||
}
|
||||
});
|
||||
|
||||
// --- B2 (1.7.1): okf-index CLI-versjonering + sanitizeEntry-herding ---
|
||||
|
||||
const INDEX_CLI = join(ROOT, 'scripts', 'okf-index.mjs');
|
||||
|
||||
test('okf-index CLI: eksplisitt --okf-version bumper eksisterende rot-index (flagg foer rot)', () => {
|
||||
const dir = tmpRoot();
|
||||
try {
|
||||
buildCleanIngest(dir); // rot-index har allerede okf_version: kb-layout-2026-06
|
||||
execFileSync('node', [INDEX_CLI, '--okf-version', 'kb-layout-2027-01', dir], { encoding: 'utf8' });
|
||||
const root = readFileSync(join(dir, 'index.md'), 'utf8');
|
||||
assert.match(root, /^okf_version: kb-layout-2027-01$/m, 'eksplisitt versjon skal vinne over eksisterende');
|
||||
// Uten flagg bevares den bumpede versjonen (idempotent vedlikehold, som foer).
|
||||
execFileSync('node', [INDEX_CLI, dir], { encoding: 'utf8' });
|
||||
const root2 = readFileSync(join(dir, 'index.md'), 'utf8');
|
||||
assert.match(root2, /^okf_version: kb-layout-2027-01$/m, 'implisitt kjoering bevarer eksisterende versjon');
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('okf-index CLI: --okf-version uten verdi -> bruksfeil exit 2, ingen skriving', () => {
|
||||
const dir = tmpRoot();
|
||||
try {
|
||||
buildCleanIngest(dir);
|
||||
const before = readFileSync(join(dir, 'index.md'), 'utf8');
|
||||
let status = 0;
|
||||
try {
|
||||
execFileSync('node', [INDEX_CLI, dir, '--okf-version'], { encoding: 'utf8', stdio: 'pipe' });
|
||||
} catch (e) {
|
||||
status = e.status;
|
||||
}
|
||||
assert.equal(status, 2, 'manglende flagg-verdi skal gi bruksfeil exit 2');
|
||||
assert.equal(readFileSync(join(dir, 'index.md'), 'utf8'), before, 'rot-index skal vaere uendret');
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('okf-index: RTL/ZWSP/C1/Unicode-tag i title/description strippes fra index-entry', () => {
|
||||
const dir = tmpRoot();
|
||||
try {
|
||||
mkdirSync(join(dir, 'dokumenter'), { recursive: true });
|
||||
writeFileSync(join(dir, 'index.md'), '# Bundle\n\nokf_version: kb-layout-2026-06\n');
|
||||
// RLO (u202E), ZWSP (u200B), C1 NEL (u0085), Unicode tag (U+E0041), ZWJ (u200D)
|
||||
// -- alle som eksplisitte escapes (ASCII-ren testkilde, ingen usynlige bytes).
|
||||
writeFileSync(
|
||||
join(dir, 'dokumenter', 'usynlig.md'),
|
||||
'---\ntype: Notat\ntitle: "Rap\u202Eport\u200B nr\u0085 1\u{E0041}"\n'
|
||||
+ 'description: "Se\u200D vedlegg"\n---\n# U\n',
|
||||
);
|
||||
generateIndexes(dir);
|
||||
const idx = readFileSync(join(dir, 'dokumenter', 'index.md'), 'utf8');
|
||||
const entry = idx.split('\n').find((l) => l.startsWith('* '));
|
||||
assert.ok(entry, 'usynlig.md skal ha en entry');
|
||||
assert.doesNotMatch(
|
||||
entry,
|
||||
/[\u200B-\u200F\u202A-\u202E\u2066-\u2069\u0080-\u009F\uFEFF]|[\u{E0000}-\u{E007F}]/u,
|
||||
'ingen bidi-/zero-width-/C1-/tag-tegn i entry',
|
||||
);
|
||||
assert.match(entry, /Rapport nr 1/, 'synlig tekst bevart etter stripping');
|
||||
assert.match(entry, /Se vedlegg/, 'description-tekst bevart');
|
||||
generateIndexes(dir);
|
||||
assert.equal(readFileSync(join(dir, 'dokumenter', 'index.md'), 'utf8'), idx, 'sanering er idempotent');
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// --- B3 (A1): strict-gaten fanger ALLE standard lenkeformer, ikke bare inline ---
|
||||
|
||||
test('okf-check --strict-ingest: referanse-def/autolink/HTML-anker fanges (B3)', () => {
|
||||
|
|
|
|||
|
|
@ -112,6 +112,18 @@ test('compose: tolererer body som allerede baerer --- fences (samler til ett blo
|
|||
assert.equal(fences.length, 2, 'noeyaktig to frontmatter-fences');
|
||||
});
|
||||
|
||||
test('compose: intern --- i body saneres (trunkerer ikke blokken) (B2)', () => {
|
||||
// En intern fence-linje midt i bodyen ville trunkert blokken den flate
|
||||
// parseren leser -- alt etter fencen (her `ekstra:`) ville forsvunnet.
|
||||
const withInternalFence = `${NESTED_BODY}---\nekstra: "verdi"\n`;
|
||||
const composed = compose(withInternalFence, { OKR_NOW: NOW });
|
||||
const { get } = parseFrontmatter(composed);
|
||||
assert.equal(get('navn'), 'Testdirektoratet', 'navn resolver fortsatt');
|
||||
assert.equal(get('ekstra'), 'verdi', 'innhold ETTER intern fence overlever i samme blokk');
|
||||
const fences = composed.match(/^---$/gm) || [];
|
||||
assert.equal(fences.length, 2, 'noeyaktig to frontmatter-fences');
|
||||
});
|
||||
|
||||
test('compose -> write-org-profile: hjem-profil faar OKF-frontmatter + bevart nestet navn', () => {
|
||||
withDirs((home, work) => {
|
||||
const composed = compose(NESTED_BODY, { OKR_NOW: NOW });
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@
|
|||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { mkdtempSync, writeFileSync, readFileSync, existsSync, realpathSync, rmSync } from 'node:fs';
|
||||
import {
|
||||
mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, realpathSync, rmSync,
|
||||
} from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
|
@ -72,6 +74,31 @@ test('circuit-breaker: uskrivbart hjem -> fallback til prosjektlokal uten error'
|
|||
});
|
||||
});
|
||||
|
||||
test('circuit-breaker M4: eksisterende okr.local.md OVERLEVER fallback (merge, ikke overskriv)', () => {
|
||||
withDirs((home, work) => {
|
||||
writeFileSync(join(home, '.claude'), 'not a directory\n');
|
||||
// Pre-eksisterende FULL config (syklus + onboarding + body) i prosjektlokal fil
|
||||
// -- foer B2 ble denne overskrevet i sin helhet av profil-fallbacken (M4).
|
||||
mkdirSync(join(work, '.claude'), { recursive: true });
|
||||
writeFileSync(
|
||||
join(work, '.claude', 'okr.local.md'),
|
||||
'---\nnavn: "GammelOrg"\nid: "T2-2026"\nonboarding_status: fullfort\n---\nNotater under frontmatter.\n',
|
||||
);
|
||||
const profil = '---\nnavn: "NyOrg"\n---\n';
|
||||
const out = runHelper(work, home, profil);
|
||||
const fallback = join(work, '.claude', 'okr.local.md');
|
||||
assert.equal(realpathSync(out.trim()), realpathSync(fallback), 'stdout rapporterer fallback-sti');
|
||||
const merged = readFileSync(fallback, 'utf8');
|
||||
assert.match(merged, /id: "T2-2026"/, 'syklus-id overlever fallback');
|
||||
assert.match(merged, /onboarding_status: fullfort/, 'onboarding-state overlever fallback');
|
||||
assert.match(merged, /Notater under frontmatter\./, 'body under frontmatter overlever');
|
||||
// Round-trip: hooken resolver NY org (first-match) OG GAMMEL syklus fra samme fil.
|
||||
const injected = runHook(work, home);
|
||||
assert.match(injected, /NyOrg/, 'ny profil er effektiv (first-match foran gammel blokk)');
|
||||
assert.match(injected, /T2-2026/, 'gammel syklus-config resolver fortsatt');
|
||||
});
|
||||
});
|
||||
|
||||
test('round-trip: hjem-skrevet org reflekteres av inject-okr-context', () => {
|
||||
withDirs((home, work) => {
|
||||
const profil = '---\nnavn: "RoundTripOrg"\n---\n';
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// Step 10 (A1): package.json-kontrakten for det bevisste zero-dep-bruddet.
|
||||
// Verifiserer at dep-laget er EXACT-pinnet (ingen ^/~/*), at engines-gulvet er
|
||||
// satt (unpdf krever node >= 22), at pakken er ESM (type: module), at versjonen
|
||||
// er 1.7.0 (direkte maal-versjon, bumpes ikke separat), og at .npmrc slaar av
|
||||
// er 1.7.1 (patch-lane B1+B2), og at .npmrc slaar av
|
||||
// install-scripts (Shai-Hulud / supply-chain). Zero npm deps i selve testen.
|
||||
// Moenster: tests/frontmatter.test.mjs (les fil, assert struktur).
|
||||
|
||||
|
|
@ -24,10 +24,10 @@ function readPkg() {
|
|||
return JSON.parse(readFileSync(PKG, 'utf8'));
|
||||
}
|
||||
|
||||
test('package.json: type module + version 1.7.0 (direkte maal-versjon)', () => {
|
||||
test('package.json: type module + version 1.7.1 (patch-lane)', () => {
|
||||
const pkg = readPkg();
|
||||
assert.equal(pkg.type, 'module');
|
||||
assert.equal(pkg.version, '1.7.0');
|
||||
assert.equal(pkg.version, '1.7.1');
|
||||
});
|
||||
|
||||
test('package.json: engines.node-gulv satt (unpdf krever >= 22)', () => {
|
||||
|
|
|
|||
|
|
@ -62,6 +62,15 @@ test('relevant prompt: injiserer OKR-kontekst', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('boeyningsform: "målene" treffer topic-guarden (B2)', () => {
|
||||
withWork((work) => {
|
||||
makeProjectConfig(work, 'TopicOrg');
|
||||
// Bestemt flertall av maal -- \bm[aa]l\b bommet paa denne foer B2.
|
||||
const out = runHook(work, JSON.stringify({ prompt: 'hvordan ligger vi an mot målene i høst' }));
|
||||
assert.match(out, /OKR-kontekst/, 'boeyningsformen maalene skal injisere kontekst');
|
||||
});
|
||||
});
|
||||
|
||||
test('tomt prompt-felt: bevarer inject-default (tvil -> injiser)', () => {
|
||||
withWork((work) => {
|
||||
makeProjectConfig(work, 'TopicOrg');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue