UNPKG

@kontextmind/kxm

Version:

KXM local-first multi-agent orchestration and operator dashboard

314 lines (287 loc) • 12.4 kB
import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { redactSecrets } from "./redact.ts"; import type { ContextItem } from "./context.ts"; import type { StateContradiction } from "./state.ts"; /** * Compiled Karpathy-style knowledge wiki (v0.5, issue #35). * * The wiki is a compiled, human-readable view — never the authoritative * database. Every generated claim links back to the context item / journal * entry / state record that produced it. Open contradictions remain visible; * compilation never resolves them. Generation is deterministic so diffs are * reviewable and tests are reproducible. */ export const WIKI_ROOT = ".kxm/knowledge/wiki"; export interface WikiSourcePool { project: string; /** State items for the project (all lifecycle states). */ stateItems: ContextItem[]; /** Journal- and store-derived knowledge/evidence/episode items. */ contextItems: ContextItem[]; /** Contradictions detected by the temporal state layer. */ contradictions: StateContradiction[]; /** Open contradiction journal item IDs (unresolved journal contradictions). */ openContradictionItemIds: string[]; /** Injected clock for reproducible builds. */ compiledAt: string; } export interface CompiledWiki { /** Repo-relative path → file content, sorted by path for stable diffs. */ pages: Map<string, string>; index: string; /** Metadata-only compile audit: what was rendered from where. */ audit: { project: string; pages: string[]; stateItems: number; contextItems: number; contradictions: number; compiledAt: string; }; } const SECTION_BY_KIND: Record<string, string> = { knowledge: "architecture", decision: "decisions", episode: "incidents", skill: "patterns", }; function wikiSectionFor(item: ContextItem): string { if (SECTION_BY_KIND[item.kind]) return SECTION_BY_KIND[item.kind]!; if (item.provenance.sourceRef?.startsWith("journal:")) { // Journal evidence items are incident-shaped learning. return "incidents"; } return "patterns"; } function claimLine(item: ContextItem): string { const refs = [`\`${item.id}\``]; if (item.provenance.sourceRef) refs.push(`source: \`${item.provenance.sourceRef}\``); const lineage = item.provenance.derivedFrom ?? []; if (lineage.length > 0) refs.push(`derived from: ${lineage.map((id) => `\`${id}\``).join(", ")}`); const authority = item.authority === "policy" || item.authority === "instruction" ? ` (${item.authority})` : ""; return `- ${redactSecrets(item.summary)}${authority} — ${refs.join(" · ")}`; } function page(title: string, heading: string, claims: ContextItem[], footer: string): string { const lines = [ `# ${title}`, "", heading, "", ...(claims.length > 0 ? claims.map(claimLine) : ["_No reviewed records yet._"]), "", footer, ]; return `${lines.join("\n")}\n`; } const GENERATED_FOOTER = "<!-- generated by kxm context wiki-compile; the wiki is a compiled view, not the authoritative database -->"; /** Compile the wiki deterministically. Same inputs always produce the same * bytes (only the index records `compiledAt`). */ export function compileKnowledgeWiki(pool: WikiSourcePool): CompiledWiki { const pages = new Map<string, string>(); const live = pool.contextItems.filter( (item) => item.status !== "superseded" && item.status !== "rejected", ); const superseded = pool.contextItems.filter((item) => item.status === "superseded"); // Group live items by wiki section, then by stable summary-derived slug. const sections = new Map<string, ContextItem[]>(); for (const item of live) { const section = wikiSectionFor(item); const bucket = sections.get(section) ?? []; bucket.push(item); sections.set(section, bucket); } const indexLines: string[] = [ `# ${pool.project} knowledge wiki`, "", "Compiled synthesis of durable journal evidence, temporal state, and governed records. Every claim links back to its evidence; open contradictions are listed, never silently resolved.", "", ]; for (const section of [...sections.keys()].sort()) { const items = sections.get(section)!.sort((left, right) => left.id.localeCompare(right.id)); const path = `${WIKI_ROOT}/${section}/${pool.project}.md`; pages.set( path, page( `${pool.project} — ${section}`, `Claims below are compiled from reviewed records. Each line links the record ID and source.`, items, GENERATED_FOOTER, ), ); indexLines.push(`- [${section}](${section}/${pool.project}.md) — ${items.length} claim(s)`); } // Temporal state: rendered from the authoritative state layer, preserving // current/superseded distinctions. Superseded state is explicitly labeled. const currentState = pool.stateItems.filter((item) => item.status === "current"); const supersededState = pool.stateItems.filter((item) => item.status === "superseded"); const statePath = `${WIKI_ROOT}/architecture/${pool.project}-state.md`; const stateLines = [ `# ${pool.project} — temporal state`, "", "Rendered from the authoritative state layer. Current values are live; superseded values are kept visible with their validity window and successor link.", "", "## Current", "", ...(currentState.length > 0 ? currentState .sort((left, right) => (left.stateKey ?? "").localeCompare(right.stateKey ?? "")) .map((item) => claimLine(item)) : ["_No current state records._"]), "", "## Superseded", "", ...(supersededState.length > 0 ? supersededState .sort((left, right) => (left.stateKey ?? "").localeCompare(right.stateKey ?? "")) .map((item) => claimLine(item)) : ["_No superseded state records._"]), "", GENERATED_FOOTER, ]; pages.set(statePath, `${stateLines.join("\n")}\n`); indexLines.push(`- [temporal state](${pool.project}-state.md) — ${currentState.length} current, ${supersededState.length} superseded`); // Decisions page mirrors decision-shaped knowledge records explicitly. const decisions = live.filter((item) => item.kind === "knowledge" && item.provenance.sourceRef?.startsWith("journal:") === false); if (decisions.length > 0) { pages.set( `${WIKI_ROOT}/decisions/${pool.project}.md`, page( `${pool.project} — decisions`, "Decision records with their evidence links.", decisions.sort((left, right) => left.id.localeCompare(right.id)), GENERATED_FOOTER, ), ); indexLines.push(`- [decisions (tracked records)](decisions/${pool.project}.md) — ${decisions.length}`); } // Contradictions: compiled visibility, zero silent resolution. const contradictionLines: string[] = [ `# ${pool.project} — open contradictions`, "", "These remain unresolved by compilation. Resolution happens through evidence-backed state promotion or journal decisions, never through re-generating this page.", "", ]; let contradictionCount = 0; for (const contradiction of [...pool.contradictions].sort((left, right) => left.stateKey.localeCompare(right.stateKey))) { contradictionCount += 1; contradictionLines.push( `## \`${contradiction.stateKey}\``, "", `- Competing current records: ${contradiction.competingCurrentIds.map((id) => `\`${id}\``).join(", ") || "none"}`, `- Competing proposals: ${contradiction.competingProposalIds.map((id) => `\`${id}\``).join(", ") || "none"}`, "", ); } for (const id of [...pool.openContradictionItemIds].sort()) { contradictionCount += 1; contradictionLines.push(`- Unresolved journal contradiction: \`${id}\``); } if (contradictionCount === 0) contradictionLines.push("_No open contradictions._"); contradictionLines.push("", GENERATED_FOOTER); pages.set(`${WIKI_ROOT}/contradictions/${pool.project}.md`, `${contradictionLines.join("\n")}\n`); indexLines.push(`- [contradictions](contradictions/${pool.project}.md) — ${contradictionCount} open`); // Superseded knowledge is retained in history so current/superseded // distinctions survive rebuilds. if (superseded.length > 0) { pages.set( `${WIKI_ROOT}/incidents/${pool.project}-history.md`, page( `${pool.project} — superseded knowledge history`, "Superseded records retained for learning. Never presented as current truth.", superseded.sort((left, right) => left.id.localeCompare(right.id)), GENERATED_FOOTER, ), ); indexLines.push(`- [superseded history](incidents/${pool.project}-history.md) — ${superseded.length}`); } indexLines.push("", GENERATED_FOOTER); const index = `${indexLines.join("\n")}\n`; pages.set(`${WIKI_ROOT}/index.md`, index); return { pages, index, audit: { project: pool.project, pages: [...pages.keys()].sort(), stateItems: pool.stateItems.length, contextItems: pool.contextItems.length, contradictions: contradictionCount, compiledAt: pool.compiledAt, }, }; } /** Write compiled pages to disk. Deterministic content keeps diffs bounded * and reviewable; only `index.md` embeds the compile timestamp. */ export function writeCompiledWiki(root: string, wiki: CompiledWiki): string[] { const written: string[] = []; for (const [relativePath, content] of [...wiki.pages].sort(([left], [right]) => left.localeCompare(right))) { const absolute = join(root, relativePath); mkdirSync(absolute.slice(0, absolute.lastIndexOf("/")), { recursive: true }); writeFileSync(absolute, content); written.push(relativePath); } return written; } export interface WikiLintIssue { severity: "error" | "warning"; rule: "broken_ref" | "orphan_page" | "stale_state_link" | "unresolved_contradiction"; path: string; message: string; } /** Lint a compiled wiki: broken evidence refs, orphan pages, stale state * links, and unresolved contradictions. Wiki content stays reviewable; lint * failures block release gates, not runtime work. */ export function lintKnowledgeWiki( pages: Map<string, string>, pool: Pick<WikiSourcePool, "project" | "stateItems" | "contextItems" | "contradictions" | "openContradictionItemIds">, ): WikiLintIssue[] { const issues: WikiLintIssue[] = []; const knownIds = new Set<string>([ ...pool.stateItems.map((item) => item.id), ...pool.contextItems.map((item) => item.id), ]); const index = pages.get(`${WIKI_ROOT}/index.md`); for (const [path, content] of pages) { if (path.endsWith("index.md")) continue; // Broken refs: backticked record IDs that the pool does not know. for (const match of content.matchAll(/`((?:ctx|journal)_[A-Za-z0-9_]+)`/g)) { const id = match[1]!; if (!knownIds.has(id)) { issues.push({ severity: "error", rule: "broken_ref", path, message: `references unknown record ${id}` }); } } // Orphan pages: exist but are not linked from the index. if (index && !index.includes(path.split("/").pop()!)) { issues.push({ severity: "warning", rule: "orphan_page", path, message: "not linked from index.md" }); } // Stale state links: pages claiming "Current" for a superseded state key. for (const item of pool.stateItems) { if (item.status !== "superseded" || !item.stateKey) continue; const claimPattern = new RegExp(`Current[\\s\\S]{0,400}\`${item.id}\``, "u"); if (claimPattern.test(content)) { issues.push({ severity: "error", rule: "stale_state_link", path, message: `renders superseded state item ${item.id} (key ${item.stateKey}) as current`, }); } } } // Unresolved contradictions must stay visible: an index without the // contradictions entry while contradictions exist is a lint error. const hasContradictions = pool.contradictions.length > 0 || pool.openContradictionItemIds.length > 0; if (hasContradictions && index && !index.includes("contradictions")) { issues.push({ severity: "error", rule: "unresolved_contradiction", path: `${WIKI_ROOT}/index.md`, message: "open contradictions exist but the index does not surface them", }); } return issues.sort((left, right) => left.path.localeCompare(right.path) || left.message.localeCompare(right.message)); }