fix(okr): gate-herding B2 scoped strict + B3 alle lenkeformer + M2 realpath-confinement + M3 deriveType basename/ord-grense
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a807ee2b79
commit
7f9c790f43
6 changed files with 222 additions and 18 deletions
|
|
@ -10,6 +10,8 @@
|
|||
// jf. Pass-2 ordering-fiks (Revisions #21). Serialiserer via writeFrontmatter
|
||||
// (additiv array-gren fra Step 3). Zero npm dependencies.
|
||||
|
||||
import path from 'node:path';
|
||||
|
||||
import { writeFrontmatter } from './frontmatter.mjs';
|
||||
import { snapType, snapTags, routeLevel, TYPE_VOCAB, TAGS_VOCAB } from './okf-vocab.mjs';
|
||||
|
||||
|
|
@ -18,11 +20,21 @@ const DESCRIPTION_MAX = 240;
|
|||
// Vokab sortert lengst-foerst saa "Overordnede OKR" matcher foer "OKR".
|
||||
const TYPE_BY_LENGTH = [...TYPE_VOCAB].sort((a, b) => b.length - a.length);
|
||||
|
||||
// Regel-utledning: foerste vokab-term som forekommer i title+sti -> kanonisk type.
|
||||
// M3 (A1): vokab-term matcher kun som HELT ord (ikke substring -- kompound som
|
||||
// «Statusnotat» skal ikke snappe til Status). Ord-grense = ikke-bokstav/-siffer
|
||||
// paa begge sider (\b haandterer ikke ae/oe/aa -- derfor \p{L}\p{N}-lookaround).
|
||||
function termMatches(hay, term) {
|
||||
const esc = term.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
return new RegExp(`(?<![\\p{L}\\p{N}])${esc}(?![\\p{L}\\p{N}])`, 'u').test(hay);
|
||||
}
|
||||
|
||||
// Regel-utledning: foerste vokab-term (lengst foerst) som forekommer som helt
|
||||
// ord i title + BASENAME av kilden -> kanonisk type. Full sti deltar ALDRI
|
||||
// (M3: et /okr/-katalogsegment skal ikke forgifte routeLevel-rutingen).
|
||||
function deriveType(title, sourcePath) {
|
||||
const hay = `${title} ${sourcePath}`.toLowerCase();
|
||||
const hay = `${title} ${path.basename(String(sourcePath))}`.toLowerCase();
|
||||
for (const t of TYPE_BY_LENGTH) {
|
||||
if (hay.includes(t.toLowerCase())) return snapType(t);
|
||||
if (termMatches(hay, t)) return snapType(t);
|
||||
}
|
||||
return snapType('');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
// 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 } from 'node:fs';
|
||||
import { writeFileSync, mkdirSync, renameSync, existsSync, realpathSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
|
|
@ -52,6 +52,26 @@ function resolveUnderBundle(resolvedBundle, rel) {
|
|||
return resolved;
|
||||
}
|
||||
|
||||
// M2 (A1): den leksikalske sjekken over slipper symlinks -- en symlinket
|
||||
// katalog/original INNE i bundlen kan peke UT av den. realpathSync paa den
|
||||
// faktiske noden (destinasjons-parent etter mkdir / original foer peker-skriv)
|
||||
// maa ogsaa lande under bundle-rotas realpath, ellers avvises skrivet.
|
||||
function assertRealUnderBundle(realBundle, p, what) {
|
||||
const real = realpathSync(p);
|
||||
if (real !== realBundle && !real.startsWith(realBundle + path.sep)) {
|
||||
throw new Error(`innboks-write: ${what} resolverer utenfor bundle-rot (symlink-escape avvist): ${p}`);
|
||||
}
|
||||
return real;
|
||||
}
|
||||
|
||||
// Destinasjons-parent opprettes, realpath-sjekkes, DERETTER skrives det atomisk.
|
||||
function writeConfined(realBundle, target, data, what) {
|
||||
const dir = path.dirname(target);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
assertRealUnderBundle(realBundle, dir, what);
|
||||
writeAtomic(target, data);
|
||||
}
|
||||
|
||||
// Skrevet fil = frontmatter + body verbatim, med garantert trailing newline.
|
||||
function fileContent(concept) {
|
||||
const out = `${concept.frontmatter}${concept.body}`;
|
||||
|
|
@ -86,13 +106,16 @@ export function writeConcepts(concepts, { bundleRoot, originals = [] } = {}) {
|
|||
throw new Error(`innboks-write: skriv til home-org-rot avvist: ${bundleRoot}`);
|
||||
}
|
||||
|
||||
// Bundle-rotas realpath er ankeret for alle M2-symlink-sjekker under.
|
||||
const realBundle = realpathSync(resolvedBundle);
|
||||
|
||||
const writtenConcepts = [];
|
||||
for (const concept of concepts) {
|
||||
if (!concept.destRel) {
|
||||
throw new Error(`innboks-write: konsept mangler destRel: ${concept.slug ?? '?'}`);
|
||||
}
|
||||
const target = resolveUnderBundle(resolvedBundle, concept.destRel);
|
||||
writeAtomic(target, fileContent(concept));
|
||||
writeConfined(realBundle, target, fileContent(concept), `destinasjons-katalog for ${concept.destRel}`);
|
||||
writtenConcepts.push(target);
|
||||
}
|
||||
|
||||
|
|
@ -107,6 +130,9 @@ export function writeConcepts(concepts, { bundleRoot, originals = [] } = {}) {
|
|||
if (relToBundle === '' || relToBundle.startsWith('..') || path.isAbsolute(relToBundle)) {
|
||||
throw new Error(`innboks-write: original utenfor bundle-rot: ${original.path}`);
|
||||
}
|
||||
// M2: en symlink-original i drop-zonen passerer den leksikalske sjekken
|
||||
// over, men realpathen kan peke UT av bundlen -> avvis.
|
||||
assertRealUnderBundle(realBundle, resolvedOriginal, `original ${original.path}`);
|
||||
const link = `/${relToBundle.split(path.sep).join('/')}`;
|
||||
|
||||
// Nivaa = nivaaet til foerste konsept fra samme kilde; default dokumenter/.
|
||||
|
|
@ -114,7 +140,7 @@ 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);
|
||||
writeAtomic(pointerTarget, pointerContent(original, link));
|
||||
writeConfined(realBundle, pointerTarget, pointerContent(original, link), `peker-katalog for ${pointerRel}`);
|
||||
writtenPointers.push(pointerTarget);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue