From 7cfcd4dea831f73551121024c10f8c7b2f58d023 Mon Sep 17 00:00:00 2001 From: elhoim Date: Tue, 28 Jul 2026 07:15:06 +0000 Subject: [PATCH 1/3] feat(knowledge): regenerate _schema.md on the doc-integrity pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GenerateKnowledgeSchemaDoc.ts is currently dead code — nothing invokes it. Two consequences: _schema.md does not exist on a fresh install, and where it does exist it drifts from KnowledgeSchema.ts, which is the declared source of truth. That drift is not theoretical. On the archive this was found on, _schema.md declared kb-v3 while describing 0.9% of the notes it governed, and the gap went unnoticed because nothing regenerated the doc. Wire the generator into DocIntegrity so the doc is rebuilt from the schema on every integrity pass. Fire-and-forget, non-fatal on failure, and run as a subprocess rather than imported because the generator executes main() on import. --- LifeOS/install/hooks/DocIntegrity.hook.ts | 7 +++++++ .../hooks/handlers/RebuildKnowledgeSchema.ts | 21 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 LifeOS/install/hooks/handlers/RebuildKnowledgeSchema.ts diff --git a/LifeOS/install/hooks/DocIntegrity.hook.ts b/LifeOS/install/hooks/DocIntegrity.hook.ts index 154235fd46..8c94126211 100755 --- a/LifeOS/install/hooks/DocIntegrity.hook.ts +++ b/LifeOS/install/hooks/DocIntegrity.hook.ts @@ -19,6 +19,7 @@ import { readHookInput, parseTranscriptFromInput } from './lib/hook-io'; import { handleDocCrossRefIntegrity } from './handlers/DocCrossRefIntegrity'; import { handleRebuildArchSummary } from './handlers/RebuildArchSummary'; import { handleMemoryDirIntegrity } from './handlers/MemoryDirIntegrity'; +import { handleRebuildKnowledgeSchema } from './handlers/RebuildKnowledgeSchema'; async function main() { const input = await readHookInput(); @@ -59,6 +60,12 @@ async function main() { console.error('[DocIntegrity] Memory-dir handler failed:', err); } + try { + await handleRebuildKnowledgeSchema(); + } catch (err) { + console.error('[DocIntegrity] Knowledge-schema regen failed:', err); + } + process.exit(0); } diff --git a/LifeOS/install/hooks/handlers/RebuildKnowledgeSchema.ts b/LifeOS/install/hooks/handlers/RebuildKnowledgeSchema.ts new file mode 100644 index 0000000000..2beee6d878 --- /dev/null +++ b/LifeOS/install/hooks/handlers/RebuildKnowledgeSchema.ts @@ -0,0 +1,21 @@ +/** + * RebuildKnowledgeSchema — regenerate `MEMORY/KNOWLEDGE/_schema.md` from + * `KnowledgeSchema.ts` (the pure-data source of truth) on the doc-integrity + * pass, so the schema doc never drifts from the schema and exists on every + * install. Fire-and-forget; a failure is non-fatal (logged by the caller). + * + * Runs the standalone generator as a subprocess rather than importing it, + * because `GenerateKnowledgeSchemaDoc.ts` executes `main()` on import. + */ +import { spawn } from "child_process"; +import { join } from "path"; +import { getLifeosDir } from "../lib/paths"; + +export async function handleRebuildKnowledgeSchema(): Promise { + const generator = join(getLifeosDir(), "TOOLS", "GenerateKnowledgeSchemaDoc.ts"); + await new Promise((resolve) => { + const child = spawn("bun", [generator], { stdio: "ignore" }); + child.on("close", () => resolve()); + child.on("error", () => resolve()); + }); +} From 4d1faf88bf5ca85edba2825dcf5787ef6158e6f0 Mon Sep 17 00:00:00 2001 From: elhoim Date: Tue, 28 Jul 2026 07:22:44 +0000 Subject: [PATCH 2/3] feat(knowledge): report note conformance on the doc-integrity pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RebuildKnowledgeSchema (#1685) keeps _schema.md honest against KnowledgeSchema.ts, so the DOC can no longer drift from the CODE. Nothing checks whether the NOTES conform to either. That gap is how an archive reached 0.9% conformance without a single warning: a bulk import wrote 1,744 notes off-schema, later writers copied their frontmatter and spread the dialect, and KnowledgeLint — the one tool that could have said so — is invoked by nothing in the tree. It only ever runs when a human remembers to type it. Add a read-only conformance check alongside the regen. It calls validate() from KnowledgeSchema directly rather than shelling out, reports a percentage plus the top violation classes, names the repair command (MigrateKnowledge.ts), and emits a typed event for Pulse. Reports only. Never edits, never blocks — non-conformance is a soft warning, matching MemoryDirIntegrity's contract. Verified on a 1,801-note archive: reports 100.0% clean, and a deliberately downgraded note is caught as '1x convention — not kb-v3'. Stacks on #1685. --- LifeOS/install/hooks/DocIntegrity.hook.ts | 7 ++ .../hooks/handlers/KnowledgeConformance.ts | 119 ++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 LifeOS/install/hooks/handlers/KnowledgeConformance.ts diff --git a/LifeOS/install/hooks/DocIntegrity.hook.ts b/LifeOS/install/hooks/DocIntegrity.hook.ts index 8c94126211..5b6a43e672 100755 --- a/LifeOS/install/hooks/DocIntegrity.hook.ts +++ b/LifeOS/install/hooks/DocIntegrity.hook.ts @@ -20,6 +20,7 @@ import { handleDocCrossRefIntegrity } from './handlers/DocCrossRefIntegrity'; import { handleRebuildArchSummary } from './handlers/RebuildArchSummary'; import { handleMemoryDirIntegrity } from './handlers/MemoryDirIntegrity'; import { handleRebuildKnowledgeSchema } from './handlers/RebuildKnowledgeSchema'; +import { handleKnowledgeConformance } from './handlers/KnowledgeConformance'; async function main() { const input = await readHookInput(); @@ -66,6 +67,12 @@ async function main() { console.error('[DocIntegrity] Knowledge-schema regen failed:', err); } + try { + await handleKnowledgeConformance(); + } catch (err) { + console.error('[DocIntegrity] Knowledge-conformance check failed:', err); + } + process.exit(0); } diff --git a/LifeOS/install/hooks/handlers/KnowledgeConformance.ts b/LifeOS/install/hooks/handlers/KnowledgeConformance.ts new file mode 100644 index 0000000000..ca300c32c8 --- /dev/null +++ b/LifeOS/install/hooks/handlers/KnowledgeConformance.ts @@ -0,0 +1,119 @@ +#!/usr/bin/env bun +/** + * KnowledgeConformance.ts — Knowledge Archive schema conformance checker + * + * PURPOSE: + * RebuildKnowledgeSchema keeps `_schema.md` honest against `KnowledgeSchema.ts`, + * so the DOC can no longer drift from the CODE. Nothing, however, checks whether + * the NOTES conform to either. That gap is how an archive reached 0.9% + * conformance without a single warning: a bulk import wrote 1,744 notes off + * schema, later writers copied their frontmatter, and the only tool that could + * have said so (KnowledgeLint) was never invoked by anything. + * + * This closes the detection half. It reports; it never edits or blocks. + * + * TRIGGER: Stop hook (called from DocIntegrity.hook.ts) + * + * READS: + * MEMORY/KNOWLEDGE/{People,Companies,Ideas,Research,Blogs,Books}/*.md + * + * WRITES: + * stderr (audit log with [KnowledgeConformance] tag) + * STATE/events.jsonl (typed event: doc.integrity.knowledge_conformance) + * + * SIDE EFFECTS: + * None — read-only check. Non-conformance is a soft warning. Never blocks. + */ + +import { readdirSync, readFileSync, existsSync, appendFileSync, mkdirSync } from 'fs'; +import { join } from 'path'; +import { getLifeosDir } from '../lib/paths'; +import { parseNote, validate, slugFromPath, DIR_TO_TYPE, type CanonicalType } from '../../LIFEOS/TOOLS/KnowledgeSchema'; + +const TAG = '[KnowledgeConformance]'; +const LIFEOS_DIR = getLifeosDir(); +const MEMORY_DIR = join(LIFEOS_DIR, 'MEMORY'); +const KNOWLEDGE_DIR = join(MEMORY_DIR, 'KNOWLEDGE'); +const EVENTS_FILE = join(MEMORY_DIR, 'STATE', 'events.jsonl'); +const DIRS = ['People', 'Companies', 'Ideas', 'Research', 'Blogs', 'Books'] as const; + +function emitEvent(payload: Record): void { + try { + mkdirSync(join(MEMORY_DIR, 'STATE'), { recursive: true }); + appendFileSync(EVENTS_FILE, JSON.stringify({ timestamp: new Date().toISOString(), ...payload }) + '\n', 'utf-8'); + } catch { + /* non-fatal */ + } +} + +export async function handleKnowledgeConformance(): Promise { + const startTime = Date.now(); + if (!existsSync(KNOWLEDGE_DIR)) return; + + let total = 0; + let conformant = 0; + const byDir: Record = {}; + const violationCounts: Record = {}; + + for (const dir of DIRS) { + const dirPath = join(KNOWLEDGE_DIR, dir); + if (!existsSync(dirPath)) continue; + const dirType = DIR_TO_TYPE[dir] as CanonicalType; + byDir[dir] = { n: 0, ok: 0 }; + + for (const file of readdirSync(dirPath)) { + if (!file.endsWith('.md') || file.startsWith('_')) continue; + const path = join(dirPath, file); + total++; + byDir[dir].n++; + try { + const violations = validate(parseNote(readFileSync(path, 'utf-8')), slugFromPath(path), dirType); + if (violations.length === 0) { + conformant++; + byDir[dir].ok++; + } else { + for (const v of violations) { + const key = `${v.key} — ${v.problem}`; + violationCounts[key] = (violationCounts[key] || 0) + 1; + } + } + } catch { + violationCounts['unparseable frontmatter'] = (violationCounts['unparseable frontmatter'] || 0) + 1; + } + } + } + + const nonConformant = total - conformant; + const pct = total > 0 ? ((conformant / total) * 100).toFixed(1) : '0.0'; + + emitEvent({ + event: 'doc.integrity.knowledge_conformance', + total, + conformant, + non_conformant: nonConformant, + conformance_pct: Number(pct), + per_dir: byDir, + top_violations: Object.entries(violationCounts) + .sort((a, b) => b[1] - a[1]) + .slice(0, 5) + .map(([problem, count]) => ({ problem, count })), + ok: nonConformant === 0, + }); + + const elapsed = Date.now() - startTime; + if (nonConformant > 0) { + console.error(`${TAG} ${nonConformant}/${total} notes off-schema (${pct}% conformant). Repair: bun LIFEOS/TOOLS/MigrateKnowledge.ts`); + for (const [problem, count] of Object.entries(violationCounts).sort((a, b) => b[1] - a[1]).slice(0, 3)) { + console.error(`${TAG} ${count}× ${problem}`); + } + } + console.error(`${TAG} === Check complete (${elapsed}ms, ${pct}% conformant) ===`); +} + +// Allow running standalone for verification. +if (import.meta.main) { + handleKnowledgeConformance().catch((err) => { + console.error(`${TAG} Fatal:`, err); + process.exit(1); + }); +} From 100c7364401841aaba4edc1affbbfe1886886363 Mon Sep 17 00:00:00 2001 From: elhoim Date: Tue, 28 Jul 2026 07:26:20 +0000 Subject: [PATCH 3/3] feat(knowledge): warn at write time when a note lands off-schema MemorySystem.renderInitialNote is the sanctioned way a note is born on the schema, but nothing prevents a writer from bypassing it. A bulk import did exactly that and wrote 1,744 notes off-schema; every writer afterwards read a neighbouring note, copied its frontmatter, and spread the legacy dialect further. The convention was documented and unenforced. #1686 reports the damage on the Stop pass. This reports it at the write, while the author is still in the loop and the fix is one edit instead of a migration. Advisory only. Emits additionalContext naming the violations and pointing at renderInitialNote; never blocks, because the note is already on disk by PostToolUse and a hard gate on note-writing would be worse than the drift. Silent for every path outside MEMORY/KNOWLEDGE//*.md, including _index.md and _schema.md. Verified: silent on a conformant note, silent on _schema.md, _index.md and a settings.json path, and on a note downgraded to kb-v2 it emits the violation plus the repair route. Stacks on #1686. --- .../install/hooks/KnowledgeWriteGuard.hook.ts | 90 +++++++++++++++++++ LifeOS/install/hooks/hooks.json | 10 +++ 2 files changed, 100 insertions(+) create mode 100644 LifeOS/install/hooks/KnowledgeWriteGuard.hook.ts diff --git a/LifeOS/install/hooks/KnowledgeWriteGuard.hook.ts b/LifeOS/install/hooks/KnowledgeWriteGuard.hook.ts new file mode 100644 index 0000000000..851dc1b433 --- /dev/null +++ b/LifeOS/install/hooks/KnowledgeWriteGuard.hook.ts @@ -0,0 +1,90 @@ +#!/usr/bin/env bun +/** + * KnowledgeWriteGuard.hook.ts — schema feedback at the moment a note is written. + * + * PURPOSE: + * `MemorySystem.renderInitialNote` is the sanctioned way a Knowledge note is + * born on the schema, but nothing prevents a writer from bypassing it. A bulk + * import did exactly that and wrote 1,744 notes off-schema; every later writer + * then read a neighbouring note, copied its frontmatter, and spread the legacy + * dialect further. The convention was documented and unenforced. + * + * KnowledgeConformance reports the damage on the Stop pass. This reports it at + * the write, while the author is still in the loop and the fix is one edit + * rather than a migration. + * + * TRIGGER: PostToolUse, matcher Write|Edit|MultiEdit + * + * CONTRACT: + * Advisory only. Emits `additionalContext` naming the violations and the + * canonical writer. NEVER blocks — a hard gate on note-writing would be worse + * than the drift it prevents, and the note is already on disk by PostToolUse. + * Silent for every path outside MEMORY/KNOWLEDGE//*.md. + */ + +import { readFileSync, existsSync } from 'fs'; +import { join, sep } from 'path'; +import { getLifeosDir } from './lib/paths'; +import { parseNote, validate, slugFromPath, DIR_TO_TYPE, type CanonicalType } from '../LIFEOS/TOOLS/KnowledgeSchema'; + +const KNOWLEDGE_DIR = join(getLifeosDir(), 'MEMORY', 'KNOWLEDGE'); + +function readStdin(): Promise { + return new Promise((resolve) => { + let data = ''; + const timer = setTimeout(() => resolve(data), 2000); + process.stdin.on('data', (c) => { data += c.toString(); }); + process.stdin.on('end', () => { clearTimeout(timer); resolve(data); }); + process.stdin.on('error', () => { clearTimeout(timer); resolve(data); }); + }); +} + +/** The note's directory decides its type; anything outside a typed dir is not a note. */ +function knowledgeDirOf(path: string): string | null { + if (!path.endsWith('.md')) return null; + if (!path.startsWith(KNOWLEDGE_DIR + sep)) return null; + const rest = path.slice(KNOWLEDGE_DIR.length + 1).split(sep); + if (rest.length !== 2) return null; // must be KNOWLEDGE//.md + if (rest[1].startsWith('_')) return null; // _index.md / _schema.md are not notes + return DIR_TO_TYPE[rest[0]] ? rest[0] : null; +} + +(async () => { + const raw = await readStdin(); + if (!raw.trim()) process.exit(0); + + let input: Record; + try { input = JSON.parse(raw); } catch { process.exit(0); } + + const path: string = input?.tool_input?.file_path ?? ''; + const dir = path ? knowledgeDirOf(path) : null; + if (!dir || !existsSync(path)) process.exit(0); + + let violations: { key: string; problem: string }[] = []; + try { + violations = validate( + parseNote(readFileSync(path, 'utf-8')), + slugFromPath(path), + DIR_TO_TYPE[dir] as CanonicalType, + ); + } catch { + violations = [{ key: 'frontmatter', problem: 'could not be parsed' }]; + } + if (violations.length === 0) process.exit(0); + + const lines = violations.slice(0, 6).map((v) => ` - \`${v.key}\` — ${v.problem}`); + const more = violations.length > 6 ? `\n …and ${violations.length - 6} more` : ''; + + console.log(JSON.stringify({ + hookSpecificOutput: { + hookEventName: input.hook_event_name || 'PostToolUse', + additionalContext: + `⚠ Knowledge note written off-schema — \`${path.split(sep).slice(-2).join('/')}\`\n` + + lines.join('\n') + more + + `\n\nNotes are born on the schema via \`MemorySystem.renderInitialNote\`. ` + + `Do not hand-write frontmatter by copying a sibling — that is how a legacy dialect spreads. ` + + `Fix this note now, or run \`bun LIFEOS/TOOLS/MigrateKnowledge.ts\` (dry-run by default) for a bulk repair.`, + }, + })); + process.exit(0); +})().catch(() => process.exit(0)); diff --git a/LifeOS/install/hooks/hooks.json b/LifeOS/install/hooks/hooks.json index ff8b570a12..b1389ff35d 100644 --- a/LifeOS/install/hooks/hooks.json +++ b/LifeOS/install/hooks/hooks.json @@ -212,6 +212,16 @@ "timeout": 8 } ] + }, + { + "matcher": "Write|Edit|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "$HOME/.claude/hooks/KnowledgeWriteGuard.hook.ts", + "timeout": 5 + } + ] } ], "PostToolUseFailure": [