feat(okr): deterministisk frontmatter-projeksjon + vokab-snap + mtime-timestamp (SC idempotens) [skip-docs]
This commit is contained in:
parent
136093da38
commit
d011fb1922
2 changed files with 199 additions and 0 deletions
124
tests/innboks-frontmatter.test.mjs
Normal file
124
tests/innboks-frontmatter.test.mjs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
// innboks-frontmatter.test.mjs
|
||||
// Step 5 (SC idempotens): deterministisk frontmatter-projeksjon. projectFrontmatter
|
||||
// utleder type/tags regel-basert (INGEN LLM) og snapper mot lukket vokab; resource =
|
||||
// original relativ sti; description = foerste ikke-tomme avsnitt; timestamp = ISO av
|
||||
// original-mtime (IKKE veggklokke); kilde:innboks; og fester concept.destRel =
|
||||
// routeLevel(type)/slug.md FOER relasjons-steget. Asserterer paa parseFrontmatter().
|
||||
// get() (aldri substring for verdier). Moenster: tests/oppsett-okf-write.test.mjs:93.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parseFrontmatter } from '../lib/frontmatter.mjs';
|
||||
import { TYPE_VOCAB, routeLevel } from '../lib/okf-vocab.mjs';
|
||||
import { splitConcepts } from '../lib/innboks-split.mjs';
|
||||
import { projectFrontmatter } from '../lib/innboks-frontmatter.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const FIX = join(HERE, 'fixtures', 'inbox-converted');
|
||||
const dokA = readFileSync(join(FIX, 'dok-a.md'), 'utf8');
|
||||
|
||||
const MTIME = new Date('2026-03-15T08:30:00.000Z');
|
||||
const OPTS = { sourcePath: 'innboks/dok-a.txt', sourceMtime: MTIME };
|
||||
|
||||
// Frontmatter-noekler i rekkefolge (kun linjestart-noekler; list-elementer hoppes over).
|
||||
function fmKeyOrder(fm) {
|
||||
const { raw } = parseFrontmatter(fm);
|
||||
return (raw || '')
|
||||
.split('\n')
|
||||
.map((l) => {
|
||||
const m = l.match(/^([a-z_]+):/);
|
||||
return m ? m[1] : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
test('projectFrontmatter: type snappes til lukket vokab; keyword i title -> kanonisk type', () => {
|
||||
const [c1] = splitConcepts(dokA, { sourceSlug: 'dok-a' });
|
||||
const e = projectFrontmatter(c1, OPTS);
|
||||
const { get } = parseFrontmatter(e.frontmatter);
|
||||
assert.ok(TYPE_VOCAB.includes(get('type')), 'type i lukket vokab');
|
||||
assert.equal(get('type'), 'Tildelingsbrev', 'keyword "Tildelingsbrev" i title -> kanonisk type');
|
||||
});
|
||||
|
||||
test('projectFrontmatter: ukjent emne -> safe default Dokument (negativ vokab-sjekk)', () => {
|
||||
const concept = {
|
||||
sourceSlug: 'x',
|
||||
title: 'Helt annerledes emne',
|
||||
slug: 'helt-annerledes-emne',
|
||||
level: 1,
|
||||
body: 'Innhold uten vokab-noekkelord.',
|
||||
};
|
||||
const e = projectFrontmatter(concept, { sourcePath: 'innboks/x.txt', sourceMtime: MTIME });
|
||||
assert.equal(parseFrontmatter(e.frontmatter).get('type'), 'Dokument');
|
||||
assert.doesNotMatch(
|
||||
e.frontmatter,
|
||||
/^type: (Tildelingsbrev|Virksomhetsplan|OKR)$/m,
|
||||
'ikke feil-snappet til en foeringstype',
|
||||
);
|
||||
});
|
||||
|
||||
test('projectFrontmatter: resource = original relativ sti (kanonisk navn, ikke "source")', () => {
|
||||
const [c1] = splitConcepts(dokA, { sourceSlug: 'dok-a' });
|
||||
const e = projectFrontmatter(c1, OPTS);
|
||||
const { get } = parseFrontmatter(e.frontmatter);
|
||||
assert.equal(get('resource'), 'innboks/dok-a.txt');
|
||||
assert.equal(get('source'), null, 'noekkelen heter resource, ikke source');
|
||||
});
|
||||
|
||||
test('projectFrontmatter: description deterministisk (2 kjoeringer byte-identiske, foerste avsnitt)', () => {
|
||||
const [c1] = splitConcepts(dokA, { sourceSlug: 'dok-a' });
|
||||
const e1 = projectFrontmatter(c1, OPTS);
|
||||
const e2 = projectFrontmatter(c1, OPTS);
|
||||
assert.equal(e1.frontmatter, e2.frontmatter, 'byte-identisk projeksjon');
|
||||
assert.match(parseFrontmatter(e1.frontmatter).get('description'), /Tildelingsbrevet gir overordnede/);
|
||||
});
|
||||
|
||||
test('projectFrontmatter: lang body -> description trunkeres deterministisk', () => {
|
||||
const longBody = 'A'.repeat(500);
|
||||
const concept = { sourceSlug: 'lang', title: 'Lang', slug: 'lang', level: 1, body: longBody };
|
||||
const e = projectFrontmatter(concept, { sourcePath: 'innboks/lang.txt', sourceMtime: MTIME });
|
||||
const desc = parseFrontmatter(e.frontmatter).get('description');
|
||||
assert.ok(desc.length <= 244, 'trunkert til <= 240 + ellipsis');
|
||||
assert.match(desc, /\.\.\.$/, 'ellipsis-markoer ved trunkering');
|
||||
});
|
||||
|
||||
test('projectFrontmatter: timestamp = ISO av sourceMtime (idempotent paa tvers av veggklokke)', () => {
|
||||
const [c1] = splitConcepts(dokA, { sourceSlug: 'dok-a' });
|
||||
const e = projectFrontmatter(c1, OPTS);
|
||||
assert.equal(parseFrontmatter(e.frontmatter).get('timestamp'), MTIME.toISOString());
|
||||
});
|
||||
|
||||
test('projectFrontmatter: kilde:innboks provenans-markoer alltid satt', () => {
|
||||
const [c1] = splitConcepts(dokA, { sourceSlug: 'dok-a' });
|
||||
const e = projectFrontmatter(c1, OPTS);
|
||||
assert.equal(parseFrontmatter(e.frontmatter).get('kilde'), 'innboks');
|
||||
assert.equal(e.kilde, 'innboks', 'kilde ogsaa paa konsept-objektet');
|
||||
});
|
||||
|
||||
test('projectFrontmatter: concept.destRel satt + konsistent med routeLevel(type)', () => {
|
||||
const [c1] = splitConcepts(dokA, { sourceSlug: 'dok-a' });
|
||||
const e = projectFrontmatter(c1, OPTS);
|
||||
assert.ok(e.destRel, 'destRel satt FOER relasjons-steget');
|
||||
assert.equal(e.destRel, `${routeLevel(e.type)}/${c1.slug}.md`);
|
||||
assert.equal(e.destRel, 'strategisk-kontekst/tildelingsbrev-2026.md', 'Tildelingsbrev -> strategisk-kontekst');
|
||||
});
|
||||
|
||||
test('projectFrontmatter: kanonisk noekkel-rekkefolge (type/resource/title/desc/tags/timestamp/kilde)', () => {
|
||||
const [c1] = splitConcepts(dokA, { sourceSlug: 'dok-a' });
|
||||
const e = projectFrontmatter(c1, OPTS);
|
||||
const order = fmKeyOrder(e.frontmatter);
|
||||
const canon = ['type', 'resource', 'title', 'description', 'tags', 'timestamp', 'kilde'];
|
||||
assert.deepEqual(order, canon.filter((k) => order.includes(k)), 'present keys i kanonisk rekkefolge');
|
||||
assert.equal(order[0], 'type', 'type foerst');
|
||||
assert.equal(order[order.length - 1], 'kilde', 'kilde sist');
|
||||
});
|
||||
|
||||
test('projectFrontmatter: tags er multi-linje liste (writeFrontmatter array-gren) + i vokab', () => {
|
||||
const [c1] = splitConcepts(dokA, { sourceSlug: 'dok-a' });
|
||||
const e = projectFrontmatter(c1, OPTS);
|
||||
assert.match(e.frontmatter, /^tags:$/m, 'multi-linje tags-noekkel');
|
||||
assert.match(e.frontmatter, /^ {2}- Tildelingsbrev$/m, 'tag-element i vokab');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue