01 / 08Foundation26 min lesson + lab

Knowledge That Outlives the Chat

Understand what an LLM Wiki is, what it is not, and why readable files are the foundation of trustworthy AI memory.

By the end

You can distinguish chat history, raw sources, durable knowledge, retrieval, and synthesis, then decide what deserves to become memory.

01

The vanishing-chat problem

A chat can feel like knowledge because the model remembers the current conversation. But a transcript is not a maintained body of truth. Decisions hide between brainstorms, corrections do not reliably update earlier claims, and another agent may see none of it. An LLM Wiki changes the unit of memory: preserve the session as a source, then maintain small named pages that a future person or agent can inspect.

  • Transcripts preserve sequence; wiki pages preserve reusable meaning.
  • A context window is temporary working memory, not durable storage.
  • Knowledge that cannot be inspected or corrected should not be treated as trusted.
02

The Karpathy pattern in plain language

The pattern associated with Andrej Karpathy is deliberately simple: keep raw material, let an LLM maintain a Markdown wiki, give agents instructions for reading and writing it, and expose explicit operations such as ingest, query, and lint. Intelligence sits around portable files rather than trapping the user's knowledge inside one provider or application.

03

Four layers, four responsibilities

Separate raw sources, maintained knowledge, retrieval, and answer synthesis. Sources show what entered the system. Wiki pages show what has been organized. Retrieval selects material relevant to a question. Synthesis explains what that evidence supports. Collapsing the four lets polished prose masquerade as proof and makes failures difficult to diagnose.

  • Source layer: originals, transcripts, images, URLs, and capture receipts.
  • Wiki layer: entities, concepts, decisions, playbooks, and links.
  • Retrieval layer: metadata filters, keyword search, embeddings, and graph traversal.
  • Answer layer: synthesis, citations, confidence, freshness, and gaps.
04

Decide what deserves durable memory

Saving everything is cheap; treating everything as equally authoritative is expensive. Preserve the original automatically, but promote a statement only when it will help future work, has a source, and is correctly scoped. Preferences must name whose preference they are. Decisions need a date and status. Facts that change need a review expectation. Unresolved questions belong in the system too, but should never be disguised as conclusions.

05

Start with an executable, provider-neutral scaffold

The companion starter uses Node.js 20 or newer and TypeScript. fast-glob inventories files, gray-matter parses frontmatter, Zod validates every boundary, tsx runs TypeScript directly, and the canonical vault remains ordinary Markdown. The first implementation is deterministic and provider-neutral: it can ingest, query, lint, evaluate, and test restore without an API key. A later LLM extractor plugs into one typed interface instead of owning storage, policy, or receipts.

Keep the implementation in src, durable content in vault, fixed examples in fixtures, and machine-readable operation evidence in receipts. The CLI dispatches five commands—ingest, query, lint, eval, and restore-test—and every command must return a receipt, use a nonzero exit code for a failed contract, and leave source files untouched unless the user explicitly applies a reviewed change.

  • src/cli.ts owns argument parsing and dispatch; it contains no knowledge policy.
  • src/types.ts owns Zod schemas for sources, pages, answers, findings, and receipts.
  • src/ingest.ts, query.ts, lint.ts, eval.ts, and restore-test.ts each expose one run(args) handler.
  • vault/{Sources,Wiki,Inbox,System,Archive} is portable canonical data; indexes are rebuildable.

Working reference

Commands and patterns

Create the starter runtime and file layout

mkdir llm-wiki-starter && cd llm-wiki-starter
npm init -y
npm install fast-glob gray-matter zod
npm install -D typescript tsx @types/node
npm pkg set type=module
npm pkg set 'scripts.check=tsc --noEmit'
npm pkg set 'scripts.wiki:ingest=tsx src/cli.ts ingest'
npm pkg set 'scripts.wiki:query=tsx src/cli.ts query'
npm pkg set 'scripts.wiki:lint=tsx src/cli.ts lint'
npm pkg set 'scripts.wiki:eval=tsx src/cli.ts eval'
npm pkg set 'scripts.wiki:restore-test=tsx src/cli.ts restore-test'
mkdir -p src vault/{Sources/source-001,Wiki,Inbox,System,Archive} fixtures/{baseline-vault,proposed-vault} receipts

Creates the runtime, installs the four implementation dependencies, registers every documented package script, and creates every directory referenced later. The next command writes working code rather than empty placeholders.

Copy the complete deterministic starter implementation

cat > tsconfig.json <<'JSON'
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "types": ["node"],
    "noEmit": true
  },
  "include": ["src/**/*.ts"]
}
JSON

cat > .gitignore <<'TXT'
node_modules/
dist/
receipts/*.json
TXT

cat > README.md <<'MD'
# Executable LLM Wiki starter

Canonical sources and maintained pages live under vault/. Every CLI operation prints a JSON receipt and also writes that receipt under receipts/.

## Review an ingest proposal without changing the canonical vault

    npm run wiki:ingest -- --source vault/Sources/source-001 --dry-run --baseline fixtures/baseline-vault --stage fixtures/proposed-vault --json
    git diff --no-index fixtures/baseline-vault fixtures/proposed-vault

The ingest handler rebuilds both fixture snapshots from the current vault/, then writes the candidate page only into fixtures/proposed-vault. A git diff --no-index exit status of 1 means a proposal was found and is expected during review. Both snapshot paths are required, must be distinct directories under fixtures/, and are safe to rebuild. Running --dry-run without the snapshot flags only prints the manifest. To apply a reviewed candidate, run ingest again without --dry-run, --baseline, or --stage; that writes the canonical vault and must happen only after review.
MD

cat > src/types.ts <<'TS'
import { z } from "zod";

export const commandSchema = z.enum(["ingest", "query", "lint", "eval", "restore-test"]);
export type Command = z.infer<typeof commandSchema>;

export const pageFrontmatterSchema = z.object({
  id: z.string().min(1),
  schemaVersion: z.number().int().positive(),
  title: z.string().min(1),
  type: z.enum(["concept", "entity", "decision", "playbook"]),
  status: z.enum(["active", "superseded", "archived"]),
  sourceIds: z.array(z.string()),
  tags: z.array(z.string()).default([]),
  createdAt: z.string().min(1),
  updatedAt: z.string().min(1)
}).passthrough();

export interface Receipt<T = unknown> {
  receiptId: string;
  ok: boolean;
  command: Command;
  startedAt: string;
  finishedAt: string;
  input: unknown;
  output?: T;
  warnings: string[];
  errors: string[];
}

export type Handler = (args: string[]) => Promise<Receipt>;

export function finish<T>(
  command: Command,
  startedAt: string,
  input: unknown,
  output: T | undefined,
  errors: string[] = [],
  warnings: string[] = []
): Receipt<T> {
  return {
    receiptId: command + "-" + startedAt.replace(/[:.]/g, "-"),
    ok: errors.length === 0,
    command,
    startedAt,
    finishedAt: new Date().toISOString(),
    input,
    output,
    warnings,
    errors
  };
}
TS

cat > src/lib.ts <<'TS'
import fg from "fast-glob";
import matter from "gray-matter";
import { readFile } from "node:fs/promises";
import { relative } from "node:path";

export function valueAfter(args: string[], name: string): string | undefined {
  const index = args.indexOf(name);
  return index >= 0 ? args[index + 1] : undefined;
}

export function hasFlag(args: string[], name: string): boolean {
  return args.includes(name);
}

export function slugify(value: string): string {
  return value.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "untitled";
}

export function tokenize(value: string): string[] {
  return [...new Set(value.toLowerCase().match(/[a-z0-9]{3,}/g) ?? [])];
}

export interface IndexedPage {
  path: string;
  relativePath: string;
  data: Record<string, unknown>;
  body: string;
}

export async function readPages(root = "vault"): Promise<IndexedPage[]> {
  const paths = await fg(["Wiki/**/*.md", "Sources/**/*.md", "Sources/**/*.txt"], {
    cwd: root,
    absolute: true,
    onlyFiles: true
  });
  return Promise.all(paths.sort().map(async (path) => {
    const raw = await readFile(path, "utf8");
    const parsed = path.endsWith(".md") ? matter(raw) : { data: {}, content: raw };
    return { path, relativePath: relative(root, path), data: parsed.data, body: parsed.content.trim() };
  }));
}
TS

cat > src/ingest.ts <<'TS'
import fg from "fast-glob";
import matter from "gray-matter";
import { cp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
import { basename, dirname, join, resolve, sep } from "node:path";
import { finish, type Receipt } from "./types.js";
import { hasFlag, slugify, valueAfter } from "./lib.js";

async function resolveSource(input: string): Promise<string> {
  const info = await stat(input);
  if (info.isFile()) return input;
  const files = await fg(["**/*.md", "**/*.txt"], { cwd: input, absolute: true, onlyFiles: true });
  if (files.length === 0) throw new Error("No Markdown or text source found under " + input);
  return files.sort()[0];
}

function fixtureDestination(input: string, flag: string): string {
  const fixtureRoot = resolve("fixtures");
  const destination = resolve(input);
  if (destination === fixtureRoot || !destination.startsWith(fixtureRoot + sep)) {
    throw new Error(flag + " must name a directory inside fixtures/");
  }
  return destination;
}

async function stageProposal(
  baselineArg: string,
  proposalArg: string,
  relativeTarget: string,
  markdown: string,
  action: "created" | "updated" | "unchanged"
): Promise<{ baseline: string; proposal: string; target: string; canonicalVaultChanged: false }> {
  const baselineRoot = fixtureDestination(baselineArg, "--baseline");
  const proposalRoot = fixtureDestination(proposalArg, "--stage");
  if (
    baselineRoot === proposalRoot ||
    baselineRoot.startsWith(proposalRoot + sep) ||
    proposalRoot.startsWith(baselineRoot + sep)
  ) {
    throw new Error("--baseline and --stage must be distinct, non-nested fixture directories");
  }
  await rm(baselineRoot, { recursive: true, force: true });
  await rm(proposalRoot, { recursive: true, force: true });
  await cp("vault", baselineRoot, { recursive: true });
  await cp("vault", proposalRoot, { recursive: true });
  const stagedTarget = join(proposalRoot, relativeTarget);
  if (action !== "unchanged") {
    await mkdir(dirname(stagedTarget), { recursive: true });
    await writeFile(stagedTarget, markdown, "utf8");
  }
  return {
    baseline: baselineArg,
    proposal: proposalArg,
    target: join(proposalArg, relativeTarget),
    canonicalVaultChanged: false
  };
}

export async function run(args: string[]): Promise<Receipt> {
  const startedAt = new Date().toISOString();
  const sourceArg = valueAfter(args, "--source");
  const dryRun = hasFlag(args, "--dry-run");
  const stageArg = valueAfter(args, "--stage");
  const baselineArg = valueAfter(args, "--baseline");
  try {
    if (!sourceArg) throw new Error("Missing --source <file-or-directory>");
    if (Boolean(stageArg) !== Boolean(baselineArg)) {
      throw new Error("Use --baseline <fixtures/path> and --stage <fixtures/path> together");
    }
    if (stageArg && !dryRun) throw new Error("Proposal staging requires --dry-run");
    const sourceFile = await resolveSource(sourceArg);
    const raw = await readFile(sourceFile, "utf8");
    const source: { data: Record<string, unknown>; content: string } = sourceFile.endsWith(".md")
      ? matter(raw)
      : { data: {}, content: raw };
    const heading = source.content.match(/^#\s+(.+)$/m)?.[1]?.trim();
    const title = heading || String(source.data.title || basename(sourceFile).replace(/\.[^.]+$/, ""));
    const slug = slugify(title);
    const target = join("vault", "Wiki", slug + ".md");
    const now = new Date().toISOString();
    const sourceId = basename(dirname(sourceFile));
    let createdAt = now;
    let previous: string | undefined;
    try {
      previous = await readFile(target, "utf8");
      createdAt = String(matter(previous).data.createdAt || now);
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
    }
    const markdown = matter.stringify(source.content.trim() + "\n", {
      id: "wiki-" + slug,
      schemaVersion: 1,
      title,
      type: "concept",
      status: "active",
      sourceIds: [sourceId],
      tags: ["ingested"],
      createdAt,
      updatedAt: previous ? String(matter(previous).data.updatedAt || now) : now
    });
    const action = previous === markdown ? "unchanged" : previous ? "updated" : "created";
    const relativeTarget = join("Wiki", slug + ".md");
    const staging = stageArg && baselineArg
      ? await stageProposal(baselineArg, stageArg, relativeTarget, markdown, action)
      : undefined;
    const manifest = {
      sourceId,
      sourceFile,
      target,
      action,
      created: action === "created" ? [target] : [],
      updated: action === "updated" ? [target] : [],
      unchanged: action === "unchanged" ? [target] : [],
      rejected: [],
      warnings: [],
      dryRun,
      staging
    };
    if (!dryRun && action !== "unchanged") {
      await mkdir(dirname(target), { recursive: true });
      await writeFile(target, markdown, "utf8");
    }
    return finish("ingest", startedAt, { source: sourceArg, dryRun, baseline: baselineArg, stage: stageArg }, manifest);
  } catch (error) {
    return finish("ingest", startedAt, { source: sourceArg, dryRun, baseline: baselineArg, stage: stageArg }, undefined, [(error as Error).message]);
  }
}
TS

cat > src/query.ts <<'TS'
import { finish, type Receipt } from "./types.js";
import { readPages, tokenize, valueAfter } from "./lib.js";

export interface QueryResult {
  question: string;
  answer: string;
  citations: Array<{ id: string; title: string; sourceId?: string; location: string; score: number }>;
  retrievalPath: "lexical";
  confidence: "high" | "medium" | "low";
  freshness: { checkedAt: string; newestSourceAt: string | null; staleSourceIds: string[] };
  gaps: Array<{ type: string; message: string }>;
  nextVerificationStep: string;
}

export async function queryVault(question: string, root = "vault"): Promise<QueryResult> {
  const terms = tokenize(question);
  const pages = await readPages(root);
  const ranked = pages.map((page) => {
    const title = String(page.data.title || page.relativePath);
    const haystack = (title + " " + page.body).toLowerCase();
    const score = terms.reduce((sum, term) => sum + (haystack.split(term).length - 1), 0);
    return { page, title, score };
  }).filter((row) => row.score > 0).sort((a, b) => b.score - a.score || a.page.relativePath.localeCompare(b.page.relativePath)).slice(0, 5);
  if (ranked.length === 0) {
    return {
      question,
      answer: "I do not know from the current vault.",
      citations: [],
      retrievalPath: "lexical",
      confidence: "low",
      freshness: { checkedAt: new Date().toISOString(), newestSourceAt: null, staleSourceIds: [] },
      gaps: [{ type: "no-results", message: "No canonical source contains the important question terms." }],
      nextVerificationStep: "Capture or identify a source that contains the missing terms."
    };
  }
  const citations = ranked.map((row, index) => ({
    id: "citation-" + (index + 1),
    title: row.title,
    sourceId: Array.isArray(row.page.data.sourceIds) ? String(row.page.data.sourceIds[0] || "") : undefined,
    location: row.page.relativePath,
    score: row.score
  }));
  const excerpt = ranked[0].page.body.replace(/\s+/g, " ").slice(0, 320);
  return {
    question,
    answer: excerpt || "The matching page has no body text.",
    citations,
    retrievalPath: "lexical",
    confidence: ranked[0].score >= 3 ? "high" : "medium",
    freshness: { checkedAt: new Date().toISOString(), newestSourceAt: null, staleSourceIds: [] },
    gaps: [],
    nextVerificationStep: "Open the top citation and verify the excerpt in context."
  };
}

export async function run(args: string[]): Promise<Receipt> {
  const startedAt = new Date().toISOString();
  const question = valueAfter(args, "--question") || "";
  try {
    if (!question.trim()) throw new Error("Missing --question <text>");
    return finish("query", startedAt, { question }, await queryVault(question));
  } catch (error) {
    return finish("query", startedAt, { question }, undefined, [(error as Error).message]);
  }
}
TS

cat > src/lint.ts <<'TS'
import fg from "fast-glob";
import matter from "gray-matter";
import { readFile, writeFile } from "node:fs/promises";
import { relative } from "node:path";
import { finish, pageFrontmatterSchema, type Receipt } from "./types.js";
import { hasFlag } from "./lib.js";

interface Finding {
  severity: "error" | "warning";
  page: string;
  message: string;
  evidence: string;
  proposedAction: string;
  fixed?: boolean;
}

export async function run(args: string[]): Promise<Receipt> {
  const startedAt = new Date().toISOString();
  try {
    const paths = await fg("Wiki/**/*.md", { cwd: "vault", absolute: true, onlyFiles: true });
    const sourcePaths = await fg("Sources/**/*", { cwd: "vault", onlyFiles: true });
    const sourceIds = new Set(sourcePaths.map((path) => path.split(/[\\/]/)[1]).filter(Boolean));
    const entries = await Promise.all(paths.sort().map(async (path) => {
      const raw = await readFile(path, "utf8");
      return { path, page: relative("vault", path), raw, parsed: matter(raw) };
    }));
    const knownTargets = new Set(entries.map((entry) => entry.page.replace(/\.md$/i, "").toLowerCase()));
    const inbound = new Map<string, number>();
    const findings: Finding[] = [];
    const ids = new Map<string, string>();
    const fix = hasFlag(args, "--fix");
    let fixed = 0;
    for (const entry of entries) {
      const { path, page, parsed, raw } = entry;
      const result = pageFrontmatterSchema.safeParse(parsed.data);
      if (!result.success) {
        findings.push({
          severity: "error",
          page,
          message: result.error.issues.map((issue) => issue.path.join(".") + ": " + issue.message).join("; "),
          evidence: "frontmatter",
          proposedAction: "Add or repair the required frontmatter fields."
        });
        continue;
      }
      const prior = ids.get(result.data.id);
      if (prior) findings.push({ severity: "error", page, message: "Duplicate id also used by " + prior, evidence: result.data.id, proposedAction: "Assign one page a new stable id and update its inbound links." });
      ids.set(result.data.id, page);
      if (result.data.sourceIds.length === 0) {
        findings.push({ severity: "warning", page, message: "No sourceIds are attached.", evidence: "sourceIds: []", proposedAction: "Attach a preserved source or label the page as an unsupported working note." });
      }
      for (const sourceId of result.data.sourceIds) {
        if (!sourceIds.has(sourceId)) findings.push({ severity: "error", page, message: "Unknown sourceId " + sourceId, evidence: sourceId, proposedAction: "Restore the source packet or correct the source ID." });
      }
      const reviewAfter = parsed.data.reviewAfter;
      if (typeof reviewAfter === "string" && Number.isFinite(Date.parse(reviewAfter)) && Date.parse(reviewAfter) < Date.now()) {
        findings.push({ severity: "warning", page, message: "Review date has passed.", evidence: reviewAfter, proposedAction: "Review the claim against current sources and set the next review date." });
      }
      for (const match of parsed.content.matchAll(/\[\[([^\]]+)\]\]/g)) {
        const rawTarget = match[1].split("|")[0].split("#")[0].trim().replace(/\.md$/i, "");
        const target = (rawTarget.includes("/") ? rawTarget : "Wiki/" + rawTarget).toLowerCase();
        if (knownTargets.has(target)) {
          inbound.set(target, (inbound.get(target) ?? 0) + 1);
        } else {
          findings.push({ severity: "error", page, message: "Broken wikilink " + match[0], evidence: match[0], proposedAction: "Create the target, correct the slug, or remove the unsupported link." });
        }
      }
      if (!raw.endsWith("\n")) {
        if (fix) {
          await writeFile(path, raw + "\n", "utf8");
          fixed += 1;
        }
        findings.push({ severity: "warning", page, message: "File has no final newline.", evidence: "end-of-file", proposedAction: "Append one newline.", fixed: fix });
      }
    }
    for (const entry of entries) {
      const target = entry.page.replace(/\.md$/i, "").toLowerCase();
      if (!/(^|\/)home$|(^|\/)index$/.test(target) && (inbound.get(target) ?? 0) === 0) {
        findings.push({ severity: "warning", page: entry.page, message: "Page is orphaned.", evidence: "zero inbound wikilinks", proposedAction: "Link it from a canonical page or archive it." });
      }
    }
    const errors = findings.filter((finding) => finding.severity === "error");
    return finish("lint", startedAt, { args, fix }, { pages: paths.length, sources: sourceIds.size, fixed, findings }, errors.map((finding) => finding.page + ": " + finding.message));
  } catch (error) {
    return finish("lint", startedAt, { args }, undefined, [(error as Error).message]);
  }
}
TS

cat > src/eval.ts <<'TS'
import { readFile } from "node:fs/promises";
import { z } from "zod";
import { finish, type Receipt } from "./types.js";
import { valueAfter } from "./lib.js";
import { queryVault } from "./query.js";

const suiteSchema = z.array(z.object({ question: z.string().min(1), expectCitation: z.string().min(1) }));

export async function run(args: string[]): Promise<Receipt> {
  const startedAt = new Date().toISOString();
  const fixture = valueAfter(args, "--fixture") || "fixtures/questions.json";
  try {
    const cases = suiteSchema.parse(JSON.parse(await readFile(fixture, "utf8")));
    const results = [];
    for (const item of cases) {
      const answer = await queryVault(item.question);
      const passed = answer.citations.some((citation) => citation.location === item.expectCitation);
      results.push({ ...item, passed, citations: answer.citations.map((citation) => citation.location) });
    }
    const failed = results.filter((result) => !result.passed);
    return finish("eval", startedAt, { fixture }, { total: results.length, passed: results.length - failed.length, results }, failed.map((result) => "Missing expected citation for: " + result.question));
  } catch (error) {
    return finish("eval", startedAt, { fixture }, undefined, [(error as Error).message]);
  }
}
TS

cat > src/restore-test.ts <<'TS'
import fg from "fast-glob";
import { cp, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { finish, type Receipt } from "./types.js";
import { queryVault } from "./query.js";

export async function run(args: string[]): Promise<Receipt> {
  const startedAt = new Date().toISOString();
  let temporary = "";
  try {
    temporary = await mkdtemp(join(tmpdir(), "llm-wiki-restore-"));
    const restoredRoot = join(temporary, "vault");
    await cp("vault", restoredRoot, { recursive: true });
    const files = await fg("**/*.md", { cwd: restoredRoot, onlyFiles: true });
    const answer = await queryVault("What does the starter source explain?", restoredRoot);
    const valid = files.length > 0 && answer.citations.length > 0;
    return finish("restore-test", startedAt, { args }, { restoredFiles: files.length, citedQueryPassed: answer.citations.length > 0 }, valid ? [] : ["Restored vault did not produce a cited query."]);
  } catch (error) {
    return finish("restore-test", startedAt, { args }, undefined, [(error as Error).message]);
  } finally {
    if (temporary) await rm(temporary, { recursive: true, force: true });
  }
}
TS

cat > src/cli.ts <<'TS'
import { mkdir, writeFile } from "node:fs/promises";
import { commandSchema, finish, type Command, type Handler, type Receipt } from "./types.js";
import { run as ingest } from "./ingest.js";
import { run as query } from "./query.js";
import { run as lint } from "./lint.js";
import { run as evaluate } from "./eval.js";
import { run as restoreTest } from "./restore-test.js";

const handlers: Record<Command, Handler> = { ingest, query, lint, eval: evaluate, "restore-test": restoreTest };

async function main(): Promise<void> {
  const parsed = commandSchema.safeParse(process.argv[2]);
  if (!parsed.success) {
    console.error("Usage: tsx src/cli.ts <ingest|query|lint|eval|restore-test> [options]");
    process.exitCode = 2;
    return;
  }
  let receipt: Receipt;
  try {
    receipt = await handlers[parsed.data](process.argv.slice(3));
  } catch (error) {
    receipt = finish(parsed.data, new Date().toISOString(), { args: process.argv.slice(3) }, undefined, [(error as Error).message]);
  }
  await mkdir("receipts", { recursive: true });
  const stamp = receipt.finishedAt.replace(/[:.]/g, "-");
  await writeFile("receipts/" + stamp + "-" + receipt.command + ".json", JSON.stringify(receipt, null, 2) + "\n", "utf8");
  console.log(JSON.stringify(receipt, null, 2));
  process.exitCode = receipt.ok ? 0 : 1;
}

await main();
TS

cat > vault/Sources/source-001/source.md <<'MD'
---
title: Starter source
---
# Starter source

The starter source explains that canonical knowledge stays in readable Markdown and every operation emits a JSON receipt.
MD

cat > vault/Wiki/home.md <<'MD'
---
id: wiki-home
schemaVersion: 1
title: Home
type: concept
status: active
sourceIds:
  - source-001
tags:
  - index
createdAt: "2026-01-01T00:00:00.000Z"
updatedAt: "2026-01-01T00:00:00.000Z"
---
# Home

The starter source explains that canonical knowledge stays in readable Markdown and every operation emits a JSON receipt.
MD

cat > fixtures/questions.json <<'JSON'
[
  {
    "question": "What does the starter source explain?",
    "expectCitation": "Wiki/home.md"
  }
]
JSON

touch fixtures/baseline-vault/.gitkeep fixtures/proposed-vault/.gitkeep receipts/.gitkeep

This is the copyable implementation home for every package script: typed dispatch, deterministic ingest, review-only proposal snapshots, lexical query, schema lint, fixed-question evaluation, clean-directory restore, fixtures, and JSON receipts. The README documents the exact staging contract. It uses no model or API key.

Run every starter command and create the Git baseline

npm run check
npm run wiki:ingest -- --source vault/Sources/source-001 --dry-run --json
npm run wiki:query -- --question 'What does the starter source explain?' --json
npm run wiki:lint -- --format json
npm run wiki:eval
npm run wiki:restore-test
git init -b main
git add .
git -c user.name='LLM Wiki Builder' -c user.email='wiki-builder@example.invalid' commit -m 'chore: initialize executable wiki starter'

Type-checks the code, executes all five real handlers, writes their receipts, initializes the main branch, and creates the baseline commit required by later branch and diff exercises. The one-command Git identity applies only to this commit and does not change global settings.

When the happy path breaks

Failure modes

01

Symptom

A folder of chat exports is described as a knowledge base.

Likely cause

Storage and curation were treated as the same job.

Recovery

Keep transcripts as sources and create separate maintained pages for decisions, entities, and reusable knowledge.

02

Symptom

The AI sounds certain but nobody can find support for its answer.

Likely cause

Synthesis was allowed without a citation contract.

Recovery

Require every answer to identify supporting sources or explicitly state that evidence is missing.

03

Symptom

The plan starts with a vector database and no real questions.

Likely cause

Infrastructure was chosen before the knowledge workflow.

Recovery

Start with five real questions, their sources, and the pages a human needs. Add retrieval infrastructure only after the file model works.

Hands-on lab

Turn one conversation into durable knowledge

Separate an original conversation from the useful knowledge it contains.

  1. 01Choose a real conversation with one decision, one useful fact, and one unresolved question.
  2. 02Save the untouched transcript under Sources with a stable source ID.
  3. 03Create three Markdown pages: a decision, an entity or concept, and an open question.
  4. 04Link each page to the source and label any inference as an inference.
  5. 05Ask a fresh session to answer using only those pages, then record what remains unknown.

Deliverable: A four-file mini-wiki and a short reflection on what became clearer after separating source from knowledge.

Reusable artifact

Starter implementation map

A reference map for the working bootstrap above; these signatures explain the contract, while the copyable source files are the executable implementation.

type Command = 'ingest' | 'query' | 'lint' | 'eval' | 'restore-test'
type Receipt<T> = { ok: boolean; command: Command; startedAt: string; finishedAt: string; input: unknown; output?: T; warnings: string[]; errors: string[] }
type Handler = (args: string[]) => Promise<Receipt<unknown>>
src/cli.ts: validate process.argv[2], load the matching Handler, print one JSON receipt, and set process.exitCode = 1 when ok is false
src/types.ts: export Zod schemas and inferred TypeScript types; never maintain two independent definitions
src/ingest.ts | query.ts | lint.ts | eval.ts | restore-test.ts: each exports run(args: string[]): Promise<Receipt<unknown>>
Canonical roots: vault/Sources, vault/Wiki, vault/Inbox, vault/System, vault/Archive
Proposal roots: --baseline fixtures/baseline-vault and --stage fixtures/proposed-vault; staging rebuilds both snapshots but never writes vault/Wiki
Test roots: fixtures; operation evidence: receipts; derived indexes: rebuildable and disposable
Extractor extension: type Extractor = (source: SourceRecord) => Promise<CandidateEntry[]>; deterministic extraction remains the no-key baseline
Exit contract: 0 only when validation and verification pass; nonzero for invalid input, unsafe writes, failed evaluation, or failed restore

Before you move on

Receipt checklist

  • Untouched source transcript with a stable source ID.
  • Three maintained pages linking to the source.
  • A test answer produced without the original conversation.
  • A written gap showing what the mini-wiki cannot answer.

Primary sources

Continue with the source

Home
Book
Blog
Calugaru Labs
GitHub
LinkedIn
Email
Theme