@kontextmind/kxm
Version:
KXM local-first multi-agent orchestration and operator dashboard
314 lines (287 loc) • 12.4 kB
text/typescript
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));
}