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:
Kjell Tore Guttormsen 2026-07-17 04:02:10 +02:00
commit 482effbad1
25 changed files with 289 additions and 29 deletions

View file

@ -49,6 +49,13 @@ try {
// body, so the result is exactly one frontmatter block (see single-block note).
body = body.replace(/^\s*---\s*\r?\n/, '');
body = body.replace(/\r?\n---\s*\r?\n?\s*$/, '\n');
// B2: drop any REMAINING '---'-prefixed line inside the body -- the flat
// parser's block regex stops at the first such line, so an internal fence
// would silently truncate everything below it (see single-block note).
body = body
.split(/\r?\n/)
.filter((line) => !line.startsWith('---'))
.join('\n');
body = body.replace(/\s+$/, '');
const timestamp = process.env.OKR_NOW || new Date().toISOString();

View file

@ -63,8 +63,12 @@ function parseExistingIndex(path) {
function sanitizeEntry(s) {
if (!s) return '';
return String(s)
.replace(/[\x00-\x1f\x7f]/g, ' ') // kontrolltegn -> mellomrom
.replace(/[\x00-\x1f\x7f\u0080-\u009f]/g, ' ') // C0 + C1 kontrolltegn -> mellomrom
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') // noytraliser markdown-lenker (behold tekst)
// B2: fjern usynlige styringstegn -- zero-width (ZWSP/ZWNJ/ZWJ/LRM/RLM),
// bidi-embedding/-override/-isolater (spoofing av leseretning), BOM, og
// Unicode tag-blokken (usynlig smugle-kanal for instruksjonstekst).
.replace(/[\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF]|[\u{E0000}-\u{E007F}]/gu, '')
.replace(/[\])]/g, '') // strip strooe ] ) som brekker round-trip
.replace(/\s+/g, ' ')
.trim()
@ -80,7 +84,9 @@ function entryLine(title, link, desc) {
}
// Generer og skriv index.md for EN katalog (ikke rekursivt). isRoot styrer okf_version.
function writeIndexFor(dir, isRoot, okfVersion) {
// explicitVersion (B2): en eksplisitt oppgitt versjon VINNER over eksisterende
// rot-verdi (bump-mekanisme); ellers bevares eksisterende (idempotent vedlikehold).
function writeIndexFor(dir, isRoot, okfVersion, explicitVersion) {
const existing = parseExistingIndex(join(dir, 'index.md'));
const dirents = readdirSync(dir, { withFileTypes: true });
const subdirs = dirents
@ -97,7 +103,7 @@ function writeIndexFor(dir, isRoot, okfVersion) {
const lines = [`# ${heading}`, ''];
if (isRoot) {
lines.push(`okf_version: ${existing.okfVersion || okfVersion}`, '');
lines.push(`okf_version: ${explicitVersion ? okfVersion : (existing.okfVersion || okfVersion)}`, '');
}
for (const sd of subdirs) {
@ -119,9 +125,10 @@ function writeIndexFor(dir, isRoot, okfVersion) {
// Generer index.md for rot + alle underkataloger, rekursivt.
export function generateIndexes(root, opts = {}) {
const okfVersion = opts.okfVersion || OKF_VERSION;
const explicitVersion = typeof opts.okfVersion === 'string' && opts.okfVersion !== '';
const okfVersion = explicitVersion ? opts.okfVersion : OKF_VERSION;
const walk = (dir, isRoot) => {
writeIndexFor(dir, isRoot, okfVersion);
writeIndexFor(dir, isRoot, okfVersion, explicitVersion);
for (const e of readdirSync(dir, { withFileTypes: true })) {
if (e.isDirectory() && isWalkableDir(e.name)) walk(join(dir, e.name), false);
}
@ -134,13 +141,22 @@ export function generateIndexes(root, opts = {}) {
const isMain = process.argv[1]
&& fileURLToPath(import.meta.url) === process.argv[1];
if (isMain) {
const root = process.argv[2];
if (!root) {
// B2: flagg-tolerant parsing -- `--okf-version <ver>` godtas foer ELLER etter
// rot-argumentet, og manglende flagg-verdi er en bruksfeil (exit 2), ikke krasj.
const usage = () => {
process.stderr.write('Bruk: node okf-index.mjs <bundle-rot> [--okf-version <ver>]\n');
process.exit(2);
};
const args = process.argv.slice(2);
let okfVersion;
const vi = args.indexOf('--okf-version');
if (vi !== -1) {
okfVersion = args[vi + 1];
if (!okfVersion || okfVersion.startsWith('--')) usage();
args.splice(vi, 2);
}
const vi = process.argv.indexOf('--okf-version');
const okfVersion = vi !== -1 ? process.argv[vi + 1] : undefined;
const root = args[0];
if (!root) usage();
generateIndexes(root, { okfVersion });
process.stdout.write(`OKF-index generert for ${root}\n`);
}

View file

@ -12,7 +12,7 @@
// Mirrors the canonical home path defined in
// hooks/scripts/inject-okr-context.mjs:14 (the most-specific-wins read side).
import { readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
import { readFileSync, writeFileSync, mkdirSync, renameSync, existsSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { homedir } from 'node:os';
@ -39,6 +39,22 @@ function writeAtomic(target, data) {
renameSync(tmp, target);
}
// M4 (B2): the fallback target (.claude/okr.local.md) may already carry the
// FULL project config (cycle id, fase, onboarding, Linear). The circuit-breaker
// must never overwrite it -- merge instead: the incoming profile's frontmatter
// lines go FIRST inside ONE block (the flat parser is first-match, so the new
// profile wins per key), the existing frontmatter lines and body follow intact.
function mergeIntoExisting(target, incoming) {
const prior = readFileSync(target, 'utf8');
const incomingInner = (incoming.match(/^---\r?\n([\s\S]*?)\r?\n---/) || [])[1] ?? incoming.trim();
const m = prior.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
if (!m) {
// Existing file without frontmatter: preserve it verbatim as body.
return `---\n${incomingInner}\n---\n${prior}`;
}
return `---\n${incomingInner}\n${m[1]}\n---\n${m[2]}`;
}
try {
writeAtomic(homeTarget, content);
process.stdout.write(homeTarget);
@ -49,7 +65,7 @@ try {
// historikk tree remains cwd-bound regardless; only the profile migrates.
const fallback = join(process.cwd(), '.claude', 'okr.local.md');
try {
writeAtomic(fallback, content);
writeAtomic(fallback, existsSync(fallback) ? mergeIntoExisting(fallback, content) : content);
process.stderr.write(
`notice: kunne ikke skrive hjem-profil (${err.code || err.message}); ` +
`falt tilbake til prosjektlokal ${fallback}\n`,