fix(okr): B1 pekerfil uten lenkeform + B5 kollisjons-guards (kuratert-vern, reservert index.md, kryss-kilde claimed) [skip-docs]
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
7f9c790f43
commit
188b534a3b
2 changed files with 124 additions and 10 deletions
|
|
@ -17,15 +17,27 @@
|
|||
// maa resolvere UNDER bundle-rota (avvis '..'-escape / absolutt-override),
|
||||
// og bundle-rota selv maa ikke vaere den home-kanoniske org-profilen
|
||||
// (~/.claude/okr/org) -- ingestion skriver kun i prosjekt-bundlen.
|
||||
// (5) B5-kollisjons-guards (A2) -- treet selv er skjermet, ikke bare originalene:
|
||||
// - reservert navn: destRel med basename index.md avvises (indeksering
|
||||
// eier index.md; et konsept-slug «index» ville blitt destruert ved
|
||||
// neste generateIndexes).
|
||||
// - kuratert-fil-vern: eksisterende maal-fil UTEN `kilde: innboks` i
|
||||
// frontmatter er haandkuratert -> skriv avvises (aldri stille datatap).
|
||||
// Med `kilde: innboks` er fila ingestion-eid -> re-skriv OK (idempotent
|
||||
// re-ingest av samme drop-zone).
|
||||
// - kryss-kilde-kollisjon: opts.claimed (Map maal -> sourceSlug, delt av
|
||||
// orkestratoren PAA TVERS av per-dokument-kall) avviser at to KILDER
|
||||
// skriver samme destRel i samme kjoering (stille last-wins var B5);
|
||||
// samme kilde kan re-skrive (relasjons-fase 2).
|
||||
//
|
||||
// Reuses: atomisk-skriv-moenster (scripts/write-org-profile.mjs:34-40);
|
||||
// writeFrontmatter (lib/frontmatter.mjs) for peker-frontmatter. Zero npm deps.
|
||||
|
||||
import { writeFileSync, mkdirSync, renameSync, existsSync, realpathSync } from 'node:fs';
|
||||
import { writeFileSync, readFileSync, mkdirSync, renameSync, existsSync, realpathSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
import { writeFrontmatter } from './frontmatter.mjs';
|
||||
import { writeFrontmatter, parseFrontmatter } from './frontmatter.mjs';
|
||||
|
||||
// Den home-kanoniske org-profil-rota. Ingestion skal ALDRI skrive hit (den eies
|
||||
// av write-org-profile.mjs); prosjekt-bundlen er .claude/okr under cwd.
|
||||
|
|
@ -72,6 +84,25 @@ function writeConfined(realBundle, target, data, what) {
|
|||
writeAtomic(target, data);
|
||||
}
|
||||
|
||||
// B5-guards foer skriv (se header (5)). claimed: Map<absolutt maal, sourceSlug>.
|
||||
function guardTarget(target, sourceSlug, claimed, rel) {
|
||||
if (path.basename(target) === 'index.md') {
|
||||
throw new Error(`innboks-write: reservert navn avvist (index.md eies av indekseringen): ${rel}`);
|
||||
}
|
||||
const owner = claimed.get(target);
|
||||
if (owner !== undefined && owner !== sourceSlug) {
|
||||
throw new Error(
|
||||
`innboks-write: kryss-kilde destRel-kollisjon: ${rel} alt skrevet av kilde «${owner}» i denne kjoeringen (naa: «${sourceSlug}»)`,
|
||||
);
|
||||
}
|
||||
if (owner === undefined && existsSync(target)) {
|
||||
const { get } = parseFrontmatter(readFileSync(target, 'utf8'));
|
||||
if (get('kilde') !== 'innboks') {
|
||||
throw new Error(`innboks-write: nekter aa overskrive kuratert (ikke-ingestion) fil: ${rel}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skrevet fil = frontmatter + body verbatim, med garantert trailing newline.
|
||||
function fileContent(concept) {
|
||||
const out = `${concept.frontmatter}${concept.body}`;
|
||||
|
|
@ -88,7 +119,10 @@ function pointerContent(original, link) {
|
|||
title: `Kilde: ${basename}`,
|
||||
kilde: 'innboks',
|
||||
});
|
||||
const body = `Peker til bevart original i drop-zonen (ikke-destruktiv ingestion).\n\n- [${basename}](${link})\n`;
|
||||
// B1: INGEN lenkeform i body -- en md-lenke til en ikke-.md-original feller
|
||||
// strict-gaten (okf-links krever .md). resource: over baerer stien; body
|
||||
// nevner den kun som ren tekst (grep-bar, aldri lenke).
|
||||
const body = `Peker til bevart original i drop-zonen (ikke-destruktiv ingestion): ${link.replace(/^\/+/, '')}\n`;
|
||||
return `${frontmatter}${body}`;
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +131,7 @@ function pointerContent(original, link) {
|
|||
// originals: [{ sourceSlug, path }] -- path = originalens plassering i drop-zonen.
|
||||
// Returnerer { concepts: [skrevne konsept-stier], pointers: [skrevne peker-stier] }
|
||||
// (absolutte stier; pipelinen (Step 10) bruker dette til discard-on-fail rollback).
|
||||
export function writeConcepts(concepts, { bundleRoot, originals = [] } = {}) {
|
||||
export function writeConcepts(concepts, { bundleRoot, originals = [], claimed = new Map() } = {}) {
|
||||
if (!bundleRoot) throw new Error('innboks-write: bundleRoot kreves');
|
||||
const resolvedBundle = path.resolve(bundleRoot);
|
||||
|
||||
|
|
@ -115,7 +149,9 @@ export function writeConcepts(concepts, { bundleRoot, originals = [] } = {}) {
|
|||
throw new Error(`innboks-write: konsept mangler destRel: ${concept.slug ?? '?'}`);
|
||||
}
|
||||
const target = resolveUnderBundle(resolvedBundle, concept.destRel);
|
||||
guardTarget(target, concept.sourceSlug, claimed, concept.destRel);
|
||||
writeConfined(realBundle, target, fileContent(concept), `destinasjons-katalog for ${concept.destRel}`);
|
||||
claimed.set(target, concept.sourceSlug);
|
||||
writtenConcepts.push(target);
|
||||
}
|
||||
|
||||
|
|
@ -140,7 +176,9 @@ export function writeConcepts(concepts, { bundleRoot, originals = [] } = {}) {
|
|||
const level = sibling ? path.dirname(sibling.destRel) : 'dokumenter';
|
||||
const pointerRel = path.join(level, `${original.sourceSlug}.kilde.md`);
|
||||
const pointerTarget = resolveUnderBundle(resolvedBundle, pointerRel);
|
||||
guardTarget(pointerTarget, original.sourceSlug, claimed, pointerRel);
|
||||
writeConfined(realBundle, pointerTarget, pointerContent(original, link), `peker-katalog for ${pointerRel}`);
|
||||
claimed.set(pointerTarget, original.sourceSlug);
|
||||
writtenPointers.push(pointerTarget);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,9 +6,13 @@
|
|||
// - konsept med strategisk-kontekst/-destRel skrives dit, dokumenter/-destRel dit
|
||||
// - skrevet fil = frontmatter (verbatim) + body (verbatim), trailing newline
|
||||
// - originalens sha256 er uendret etter skriv (ikke-destruktiv, SC3)
|
||||
// - peker-fil i konseptets nivaa lenker til originalen i drop-zonen (grep-bar)
|
||||
// - peker-fil i konseptets nivaa baerer original-stien i resource: UTEN noen
|
||||
// lenkeform i body (B1: en body-lenke til ikke-.md-original feller strict)
|
||||
// - mal-sti utenfor bundle-rot avvist (../-escape) og home-org-rot avvist
|
||||
// - ingen .tmp lekker (atomisk temp+renameSync)
|
||||
// - B5-guards: nekter overskriving av kuratert (ikke-ingestion) fil; tillater
|
||||
// idempotent re-skriv av egen kilde:innboks-output; avviser reservert navn
|
||||
// (index.md); avviser kryss-kilde destRel-kollisjon via claimed-registeret
|
||||
// Zero npm deps. Moenster: tests/org-profile-write.test.mjs (mkdtemp + realpath/sha).
|
||||
|
||||
import { test } from 'node:test';
|
||||
|
|
@ -89,7 +93,7 @@ test('writeConcepts: type->nivaa-ruting (strategisk-kontekst + dokumenter)', ()
|
|||
});
|
||||
});
|
||||
|
||||
test('writeConcepts: original uendret (sha256) + peker-fil lenker til drop-zone-original', () => {
|
||||
test('writeConcepts: original uendret (sha256) + peker-fil uten lenkeform (B1)', () => {
|
||||
withBundle(({ bundleRoot, inbox }) => {
|
||||
const tPath = join(inbox, 'tildeling.txt');
|
||||
writeFileSync(tPath, TILDELING);
|
||||
|
|
@ -105,13 +109,85 @@ test('writeConcepts: original uendret (sha256) + peker-fil lenker til drop-zone-
|
|||
assert.equal(sha256(tPath), before, 'original-sha256 uendret etter skriv');
|
||||
assert.ok(existsSync(tPath), 'original finnes fortsatt i drop-zonen');
|
||||
|
||||
// Peker-fil i konseptets nivaa, grep-bar lenke til originalen.
|
||||
// Peker-fil i konseptets nivaa: resource baerer stien, body har INGEN
|
||||
// lenkeform (B1: `- [x.txt](/innboks/x.txt)` er ikke .md -> feller strict).
|
||||
const level = dirname(concepts[0].destRel);
|
||||
const pointer = join(bundleRoot, level, 'tildeling.kilde.md');
|
||||
assert.ok(existsSync(pointer), 'peker-fil skrevet i konseptets nivaa');
|
||||
const pointerBody = readFileSync(pointer, 'utf8');
|
||||
assert.match(pointerBody, /\(\/innboks\/tildeling\.txt\)/, 'peker lenker til original i drop-zonen');
|
||||
assert.match(pointerBody, /^type: Notat$/m, 'peker baerer gyldig OKF-type (vokab)');
|
||||
const pointerContent = readFileSync(pointer, 'utf8');
|
||||
assert.match(pointerContent, /^resource: innboks\/tildeling\.txt$/m, 'resource baerer original-stien');
|
||||
assert.match(pointerContent, /innboks\/tildeling\.txt/, 'original-stien er grep-bar');
|
||||
assert.doesNotMatch(pointerContent, /\]\(/, 'ingen inline/bilde-lenke i pekerfila (B1)');
|
||||
assert.doesNotMatch(pointerContent, /^[ \t]{0,3}\[[^\]]+\]:/m, 'ingen referanse-definisjon i pekerfila (B1)');
|
||||
assert.doesNotMatch(pointerContent, /<a\b|<[a-z][a-z0-9+.-]*:/i, 'ingen HTML-anker/autolink i pekerfila (B1)');
|
||||
assert.match(pointerContent, /^type: Notat$/m, 'peker baerer gyldig OKF-type (vokab)');
|
||||
assert.match(pointerContent, /^kilde: innboks$/m, 'peker baerer kilde:innboks (provenans + B5-eierskap)');
|
||||
});
|
||||
});
|
||||
|
||||
// --- B5 (A2): kollisjons-guards -- treet selv skal vaere uskjermet aldri mer ---
|
||||
|
||||
const CURATED = '---\ntype: Status\ntitle: Kuratert status\n---\nHaandskrevet innhold som ALDRI skal overskrives.\n';
|
||||
|
||||
function concept(slug, destRel, sourceSlug = 'kilde-a') {
|
||||
return {
|
||||
slug,
|
||||
sourceSlug,
|
||||
destRel,
|
||||
frontmatter: '---\ntype: Dokument\nkilde: innboks\n---\n',
|
||||
body: 'innhold',
|
||||
};
|
||||
}
|
||||
|
||||
test('writeConcepts: nekter aa overskrive pre-eksisterende kuratert fil (B5)', () => {
|
||||
withBundle(({ bundleRoot }) => {
|
||||
const curatedPath = join(bundleRoot, 'dokumenter', 'status.md');
|
||||
mkdirSync(dirname(curatedPath), { recursive: true });
|
||||
writeFileSync(curatedPath, CURATED);
|
||||
|
||||
assert.throws(
|
||||
() => writeConcepts([concept('status', 'dokumenter/status.md')], { bundleRoot }),
|
||||
/kuratert|ikke-ingestion|overskriv/i,
|
||||
'skriv over kuratert fil (uten kilde:innboks) skal kaste',
|
||||
);
|
||||
assert.equal(readFileSync(curatedPath, 'utf8'), CURATED, 'kuratert fil er byte-uendret');
|
||||
});
|
||||
});
|
||||
|
||||
test('writeConcepts: tillater idempotent re-skriv av egen kilde:innboks-fil (B5)', () => {
|
||||
withBundle(({ bundleRoot }) => {
|
||||
const c = concept('notat', 'dokumenter/notat.md');
|
||||
writeConcepts([c], { bundleRoot });
|
||||
// Re-ingest (run2): samme fil skrives igjen -- skal IKKE kaste.
|
||||
writeConcepts([c], { bundleRoot });
|
||||
const written = readFileSync(join(bundleRoot, 'dokumenter', 'notat.md'), 'utf8');
|
||||
assert.ok(written.includes('kilde: innboks'), 'ingestion-eierskap staar i skrevet fil');
|
||||
});
|
||||
});
|
||||
|
||||
test('writeConcepts: reservert navn index.md avvises (B5)', () => {
|
||||
withBundle(({ bundleRoot }) => {
|
||||
assert.throws(
|
||||
() => writeConcepts([concept('index', 'dokumenter/index.md')], { bundleRoot }),
|
||||
/reservert|index/i,
|
||||
'destRel med basename index.md skal kaste (indeksering eier index.md)',
|
||||
);
|
||||
assert.ok(!existsSync(join(bundleRoot, 'dokumenter', 'index.md')), 'ingenting skrevet');
|
||||
});
|
||||
});
|
||||
|
||||
test('writeConcepts: kryss-kilde destRel-kollisjon avvises via claimed-register (B5)', () => {
|
||||
withBundle(({ bundleRoot }) => {
|
||||
const claimed = new Map();
|
||||
writeConcepts([concept('status', 'dokumenter/status.md', 'kilde-a')], { bundleRoot, claimed });
|
||||
// Annen kilde, samme destRel -> kollisjon (stille last-wins var B5-datatapet).
|
||||
assert.throws(
|
||||
() => writeConcepts([concept('status', 'dokumenter/status.md', 'kilde-b')], { bundleRoot, claimed }),
|
||||
/kollisjon|kilde/i,
|
||||
'samme destRel fra annen kilde i samme kjoering skal kaste',
|
||||
);
|
||||
// Samme kilde igjen (relasjons-omskriv i fase 2) -> OK.
|
||||
writeConcepts([concept('status', 'dokumenter/status.md', 'kilde-a')], { bundleRoot, claimed });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue