{"slug": "okf-graph-wiki-agent-maintained-knowledge-graph-for-context-retrieval", "title": "OKF Graph Wiki — Agent-Maintained Knowledge Graph for Context Retrieval", "summary": "A developer has built an agent-maintained knowledge graph wiki using the Open Knowledge Format (OKF), extending Andrej Karpathy's LLM Wiki pattern. The system compiles typed subject-predicate-object triples into a SQLite index for fast, deterministic retrieval, prioritizing context assembly for LLM queries over human browsing. The project includes a TypeScript/SQLite reference implementation and an implementation spec.", "body_md": "I've been running [Andrej Karpathy's LLM Wiki](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f)\npattern for several months — reading sources, compiling them into a\ncompounding, git-diffable wiki instead of re-deriving everything from\nscratch every session — and I'm a genuine convert. But two things kept\nnagging at me: **query salience** (finding the right page reliably, not\njust something plausible) and **token economy** (not re-reading half the\nwiki to answer one question).\n\nThe missing piece turned out to be the\n[Open Knowledge Format](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md)\n(OKF): structured YAML frontmatter an agent can use to record the graph\nit's already implicitly building in prose, as typed\nsubject–predicate–object triples rather than untyped markdown links. To\nkeep retrieval fast and deterministic — not another LLM call on every\nquestion — that frontmatter compiles into a disposable SQLite index.\nWhat follows specs that redesign for someone to actually build — an\n**implementation spec**, not a blog post, where every section states\nwhat to build rather than just the idea. It ships with a working\nTypeScript/SQLite reference implementation (Appendix) and pitfalls\nvalidated by building and running it, not guessed at (§10).\n\nThe original gist frames the wiki as a human-browsable, compounding\nartifact: an agent reads sources, updates markdown pages, a person reads\nthe results in Obsidian. That's still true here, but it is **not the\nprimary purpose of this version**. The primary purpose is: **given a\nquestion posed to an LLM, retrieve and assemble the right slice of the\ngraph into that LLM's context window.** Human browsing is a secondary,\nfree side benefit of keeping everything as linked markdown. Every design\ndecision below is made in service of the retrieval goal first.\n\nConcretely, that reframing changed three things versus the original gist: the file format is now OKF (structured provenance/trust/lifecycle fields, not just prose), relationships are typed subject-predicate-object triples in frontmatter (not just untyped markdown links), and there is a retrieval pipeline spec (section 6) that did not exist in the original idea at all.\n\n- Maintain a persistent, compounding knowledge base as a directory of plain markdown files, agent-written and agent-maintained.\n- Represent facts as typed, sourced, subject-predicate-object triples that a query engine can traverse, not just prose an LLM re-reads from scratch each time.\n- At question time, retrieve a bounded, relevance-ranked, trust-ranked set of facts and background prose that fits a context budget — this is the deliverable, not an afterthought.\n- Stay diffable, portable, and toolable: git-diffable markdown as the\nonly source of truth; every other artifact (SQLite index,\n`timeline.md`\n\n,`index.md`\n\nentries) is a regenerable build product, never hand-edited.\n\n**Not a general agent-memory system.** This is not session memory, preference tracking, or conversational recall — see §9.3 for why an existing tool in that space (`mnemon-dev/mnemon`\n\n) was evaluated and rejected as the storage engine.**Not a fixed ontology.** No upfront RDF/OWL schema. Both OKF's`type`\n\nand this spec's predicate vocabulary are open and grow by convention, documented as they're coined (§8).**Not a graph database.** SQLite with JSON1 functions is sufficient at the scale this pattern is meant for (hundreds to low thousands of concepts); this spec does not call for Neo4j or similar.\n\nConsidered a fourth layer, `router.md`\n\n(a hand-curated dispatch table\nfrom question domain to subdirectory), and dropped it: `context_for()`\n\n(§6) queries the whole bundle in one hybrid-retrieval pass and doesn't\nbenefit from a coarse pre-filter, and the one audience that would have\nneeded file-based dispatch — a human browsing the vault in Obsidian —\nalready has direct SQL access to `wiki.db`\n\n, either via Datasette or a\nObsidian SQLite-query plugin (SQLite Explorer, SQLite DB, and similar all\nsupport embedding live query results in a note). The only scenario where\na router file would still earn its keep is an agent given pure\nfilesystem read access with no shell or SQL tool at all; not designed\nfor by default.\n\n```\nbundle/\n  CLAUDE.md / AGENTS.md   # schema doc: conventions, predicate vocabulary, ingest rules\n  wiki.db                 # SQLite index, regenerated from frontmatter, gitignored or snapshotted\n  log.md                  # OKF-native edit history (when the wiki changed)\n  timeline.md             # world-event chronology (when things happened), regenerated\n  wiki-trigger-keywords.txt  # concept titles + tags, regenerated — optional harness-hook pre-filter (§12)\n  entities/\n    index.md              # OKF-native per-directory catalog\n    <concept>.md           # OKF concept files, one per entity/topic\n  computations/\n    ...                    # optional: OKF Attested Computation concepts, if the bundle needs sanctioned metrics\n  references/\n    ...                    # OKF convention: mirrored external material, executor/attester code\n```\n\n| File | Role | Maintained how |\n|---|---|---|\n`index.md` |\nPer-directory catalog (OKF §8): concept list with one-line descriptions, for progressive disclosure. | Regenerable from concept frontmatter; may also be hand-edited. |\n| Concept files | The atomic units of knowledge: frontmatter + prose body. Source of truth. |\nAgent-written during ingest (§7.1). |\n`timeline.md` |\nChronological ledger of world events (as opposed to `log.md` , which is edit history), built from every date-qualified `relations` entry across the bundle. |\nFully regenerated, never hand-edited. |\n`wiki.db` |\nSQLite index over all concept frontmatter, the substrate the retrieval pipeline (§6) queries. | Fully regenerated on every ingest. |\n`wiki-trigger-keywords.txt` |\nFlat, deduped list of every concept's `title` and `tags` — a deterministic pre-filter an optional harness hook can grep against, to nudge querying the wiki without paying `context_for()` 's cost on every turn. Not wired to anything by default; see §12. |\nFully regenerated on every Reindex. |\n\nEvery concept is one markdown file: YAML frontmatter + prose body, per\n[OKF v0.2](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md).\nConcept identity is its bundle-relative path with `.md`\n\nremoved (OKF §2)\n— no separate ID scheme needed.\n\nThe original gist names the content a wiki should hold: \"Summaries,\nentity pages, concept pages, comparisons, an overview, a synthesis.\"\nThis spec keeps that taxonomy — it does not collapse into one generic\nfile kind. It maps onto OKF's open `type`\n\nfield (§4.2) plus the derived\ntimeline (§3):\n\n| Page kind | `type` value |\nCarries `relations` ? |\nWhat it's for |\n|---|---|---|---|\n| Entity page | `Person` , `Organization` , `Product` , `Place` , `Country` , or `Entity` as a fallback |\nYes — the primary source of typed triples | A specific named person, organization, product, place, country (§4.5 worked example). Use the most specific value that fits — a person's file gets `type: Person` , not generic `Entity` ; fall back to `Entity` only when none of the starter set applies. |\n| Concept page | `Concept` |\nOptional — abstract topics rarely assert their own triples, but link and synthesize several entities' relations in prose | An idea, a trend, a comparison, a synthesis, an overview — the original gist's \"comparisons, an overview, a synthesis.\" |\n| World event chronology | (not a page type) |\nn/a — derived view, not authored content | `timeline.md` (§3), built from every date-qualified `relations` entry across Entity and Concept pages. Not its own `type` — deliberately: it stays a regenerated aggregate, never hand-authored, per the source-of-truth rule in §1. |\n\nConcept pages are where a synthesized answer worth keeping gets filed back into the wiki (§7.2) — this is the mechanism that keeps the bundle compounding beyond raw source ingest, not just entity extraction.\n\n| Field | OKF section | Required? | Purpose |\n|---|---|---|---|\n`type` |\n§4.1 | Required |\ne.g. `Person` , `Entity` , `Playbook` , `Metric` . Open vocabulary — see §8. |\n`title` , `description` , `tags` |\n§4.1 | Recommended | Display, seed-matching, and index generation. |\n`sources` |\n§5.1 | Optional | Provenance: what this concept was derived from. Each `relations` entry cites into this by `id` . |\n`generated` |\n§5.2 | Recommended | `{ by, at }` — who/what wrote the current content, actor convention (§7 of OKF). |\n`verified` |\n§5.2–5.3 | Optional | Drives the trust tier used in retrieval ranking (§6.4): no `verified` → unverified; non-`human:` actor → machine-confirmed; any `human:` actor → human-reviewed. |\n`status` , `stale_after` |\n§5.4–5.5 | Optional | Lifecycle. `stale_after` is checked at retrieval time (§6.4), not just for display. |\n`superseded_by` , `supersedes` |\nextension, not in OKF core |\nOptional | Concept-file-level supersession — a whole concept file explicitly replaced by another. See §7.7. Distinct from `stale_after` (time-based expiry, no replacement recorded) and from a per-fact contradiction (§7.3's `contradiction` check, which flags but does not resolve). |\n`relations` |\nextension, not in OKF core |\nOptional | Typed triples. See §4.3. Valid per OKF §4.1 (\"producers MAY include any additional keys... consumers MUST NOT reject documents with unrecognized fields\") — this bundle stays OKF v0.2 conformant. |\n\n**Provenance convention for locally-mirrored sources.** When a `sources[]`\n\nentry's material was saved locally into `references/`\n\nduring ingest\n(§7.1), record the bundle-relative path there as `resource`\n\n— e.g.\n`resource: references/tc-profile-2026.html`\n\n— rather than just the\noriginal external URL. A `sources[]`\n\nentry whose `resource`\n\nis *not* a\n`references/...`\n\npath (a bare external URL, or the field absent\nentirely) marks a concept as not traceable to a locally mirrored\ndocument — the signal §7.6's Rebuilding from references operation uses\nto distinguish an ingest-derived concept file from a hand-assembled one\n(a filed-back Concept page built from discussion and general web search,\nfor instance) without needing a separate flag.\n\n```\nrelations:\n  - { predicate: invested_in, object: /entities/openai.md, date: \"2019-03-01\", source: tc-profile-2026 }\n  - { predicate: married_to, object: /entities/laura-arrillaga-andreessen.md }\n  - { predicate: born_on, object: \"1971-07-16\" }\n```\n\n- The concept file itself is the implicit\n**subject**— no separate subject field, identity is the file path (OKF §2). `predicate`\n\n: free-text string. Open vocabulary, governed by §8.`object`\n\n: either a bundle-relative path beginning with`/`\n\n(an entity-to-entity edge — the target should resolve to another concept, but per OKF §6.1 consumers MUST tolerate a broken target, since it may represent not-yet-written knowledge) or a literal value (an entity-to-value fact, e.g. a birth date).`date`\n\n(optional): an ISO 8601 date,**the field this spec uses for temporal qualification of a fact — do not name this field** See §10.1 for why.`on`\n\n.`source`\n\n(optional): joins to a`sources[].id`\n\non the same concept, reusing OKF's existing per-claim attribution mechanism (§5.1) rather than inventing a second citation scheme.\n\nProse links in the body (`[OpenAI](/entities/openai.md)`\n\n) still work as\nOKF intends — untyped, human-narrative traversal. `relations`\n\nis the\nmachine layer for typed graph queries; it does not replace prose links,\nit sits alongside them.\n\n```\n---\ntype: Person\ntitle: Antony Blinken\ndescription: United States Secretary of State (2021-2025).\ntags: [person, united-states, foreign-policy]\nrelations:\n  - { predicate: secretary_of, object: /entities/us-department-of-state.md, date: \"2021-01-26\", source: state-dept-bio }\n  - { predicate: represents, object: /entities/united-states.md, source: state-dept-bio }\ngenerated: { by: \"reference_agent/claude-sonnet-5\", at: \"2026-08-09T10:00:00Z\" }\nverified: { by: \"human:reviewer\", at: \"2026-08-09T11:00:00Z\" }\nsources:\n  - { id: state-dept-bio, resource: \"https://2021-2025.state.gov/biographies/antony-j-blinken/\", title: \"U.S. Department of State — Antony J. Blinken\" }\n---\n\n# Summary\n\nAntony Blinken served as [the United States](/entities/united-states.md)'s\nSecretary of State from January 2021, heading the\n[Department of State](/entities/us-department-of-state.md).[^state-dept-bio]\n\n[^state-dept-bio]: U.S. Department of State — Antony J. Blinken\n```\n\nNo `relations`\n\nblock required; it links to and synthesizes existing\nEntity pages instead of asserting its own triples. This is what a filed-\nback query answer (§7.2) typically looks like.\n\n```\n---\ntype: Concept\ntitle: The U.S. Department of State under Antony Blinken\ndescription: Synthesis of continuity in U.S. foreign-policy leadership during Antony Blinken's tenure as Secretary of State.\ntags: [synthesis, united-states, foreign-policy]\ngenerated: { by: \"reference_agent/claude-sonnet-5\", at: \"2026-08-09T12:00:00Z\" }\n---\n\n# Synthesis\n\nFrom January 2021, [Antony Blinken](/entities/antony-blinken.md)\nrepresented [the United States](/entities/united-states.md) as Secretary\nof State — see [the timeline](/timeline.md#2021-01-26) for when the\nappointment began relative to other tracked events.\n```\n\n**Do not build a bespoke database engine.** Load every concept file's\nfrontmatter into a `concepts`\n\ntable with a small loader — the reference\nimplementation's `build-index.ts`\n\ndoes this in well under a hundred\nlines using the `yaml`\n\nnpm package for parsing and `bun:sqlite`\n\nfor the\ndatabase; any TypeScript/JS equivalent works the same way.\n\nList-valued fields (`relations`\n\n, `sources`\n\n, `tags`\n\n) land as JSON text\ncolumns automatically — query them with SQLite's built-in `json_each`\n\n/\n`json_extract`\n\n, no separate `relations`\n\ntable or ETL step required. The\nauto-generated `_path`\n\ncolumn doubles as concept identity, matching\nOKF's own file-path-is-identity rule for free.\n\nAdd on top of the base `concepts`\n\ntable:\n\n: an FTS5 virtual table over`concepts_fts`\n\n`title`\n\n,`description`\n\n,`tags`\n\n, and body text, for BM25 keyword search (§6.1).:`embeddings`\n\n`(path TEXT PRIMARY KEY, vector TEXT)`\n\n— one row per concept, vector as a JSON float array, produced by whichever embedding backend is configured (§6.1, §9.2).\n\nRebuild `wiki.db`\n\n(and `concepts_fts`\n\n/`embeddings`\n\n) as the last step of\nevery ingest (§7.1). It is a cache, never a second source of truth.\n\nAn existing SQLite-backed, four-graph (temporal/entity/causal/semantic) agent memory tool was evaluated and rejected as the storage layer for this bundle. Recorded here so the implementing agent doesn't re-derive or re-litigate this:\n\n- Its SQLite DB\n**is** the source of truth — no per-node markdown files exist to keep git-diffable, which inverts this spec's core principle (§1, §3). - Its\n`insights`\n\nschema is a flat memory-snippet model with no room for OKF's`type`\n\n/`sources`\n\n/`verified`\n\n/`status`\n\n/`stale_after`\n\nfields. - Its edges are inferred heuristically (embedding similarity thresholds, keyword regex, temporal proximity) from a fixed four-type enum, not deliberately asserted, sourced, open-vocabulary predicates.\n- It has importance-decay and auto-pruning by design, which works against the \"nothing gets silently dropped\" compounding-wiki goal.\n\nThis is the actual deliverable. Given a question, produce a context block for the LLM answering it.\n\nThree signals, fused with **Reciprocal Rank Fusion** (`score = Σ 1/(k + rank + 1)`\n\n,\n`k ≈ 60`\n\n), exact-title match weighted 2x by being included in the fusion\ntwice:\n\n**Exact title match**— the concept's full`title`\n\nappears in the question at a whole-word/phrase boundary, not as a bare substring. Boundary-checked is load-bearing: a concept titled \"AI\" must not seed on the word \"explain\", nor \"Go\" on \"ago\". Cheap, high precision, and*necessary*: on a cross-linked wiki, a concept is routinely mentioned on other concepts' pages, so term-frequency methods alone cannot distinguish \"the question is about X\" from \"X is merely mentioned here\" (see §10.3 — this is a validated finding, not a hypothetical).**FTS5 BM25** over`concepts_fts`\n\n— keyword recall for partial or non-exact mentions. Used for*ranking*candidates as-is; used for*confidence*only after a word-coverage check, not on raw presence alone — validated, not hypothetical (§10.4).**Vector cosine similarity** over`embeddings`\n\n— semantic recall for paraphrased questions with no lexical overlap.**Embedding backend:**(`nomic-embed-text`\n\nvia a local Ollama endpoint`localhost:11434`\n\n), with`search_document:`\n\n/`search_query:`\n\nprefixing per its convention — decided, not a placeholder. See §9.2 for why a TF-IDF stand-in was used during prototyping and is not carried forward.\n\nLightweight keyword classification into `WHY | WHEN | ENTITY | GENERAL`\n\n(extend as needed). Drives predicate weighting in §6.3 — a pattern worth\nkeeping from Mnemon's intent-adaptive traversal even though its storage\nengine was rejected (§5.1): a WHY question should weight causally-loaded\npredicates higher, a WHEN question should weight the date-qualified\nsubset higher, and so on.\n\nBFS from the seed concept(s), hop limit 2 by default. Score every traversed relation:\n\n```\nscore = predicate_weight(intent, predicate) * hop_decay^hop * trust_multiplier(subject_trust_tier)\n```\n\n`predicate_weight`\n\n: a small hand-maintained table per intent (grows with the predicate vocabulary, §8).`hop_decay`\n\n: constant < 1 (e.g. 0.6) — relevance decays with distance from the seed.`trust_multiplier`\n\n:`{human-reviewed: 1.0, machine-confirmed: 0.8, unverified: 0.6}`\n\n, derived from`verified`\n\nper OKF §5.3.\n\nDo not return the whole neighborhood — this is what makes the retrieval bounded rather than a graph dump.\n\nSort scored triples descending, take the top N by context budget.\nExclude or down-rank facts where `today >= stale_after`\n\n(OKF §5.5).\nTrust tier is a first-class ranking input here, not just display\nmetadata — when more candidates exist than the budget allows, prefer\nhuman-reviewed, non-stale facts over unverified ones at equal predicate\nweight. A concept file with `status: superseded`\n\ngets its own filtering\npass, distinct from `stale_after`\n\n's — see §7.7.\n\nRender for the LLM prompt as compact fact lines plus prose background from the seed concepts' bodies, not raw JSON:\n\n```\n- Andreessen Horowitz invested_in OpenAI, as of 2021-04-01 (trust: human-reviewed, source: a16z-portfolio)\n```\n\nTriples answer *that* a fact holds; the seed concepts' prose bodies are\nincluded for facts that need more than a triple to convey correctly\n(why, not just that).\n\n- Agent reads a new source, extracts facts, discusses with the user\n(or proceeds unsupervised, per the bundle's own policy in\n`CLAUDE.md`\n\n/`AGENTS.md`\n\n). - Creates or updates the relevant concept file(s): body prose,\n`relations`\n\n,`sources`\n\n,`generated`\n\n. A single source may touch several concepts. **Hard rule — assert both directions of a relation in the same pass.** When Antony Blinken's concept file gets`{ predicate: secretary_of, object: /entities/us-department-of-state.md }`\n\n, the Department's concept file gets the matching`{ predicate: secretary, object: /entities/antony-blinken.md }`\n\nin the same ingest step. Chosen over computing inverses at query time from a predicate-inverse lookup table: it keeps the query layer (§6) simple, at the cost of ingest needing to touch both concepts. §7.3's asymmetric-relations lint check exists specifically to catch a violation of this rule.- Regenerates\n`wiki.db`\n\n(§5) and`timeline.md`\n\n(§3) unconditionally — every ingest, no exceptions, regardless of how small the change — and any`index.md`\n\nentries affected. - Appends an entry to\n`log.md`\n\n(OKF §9). **Never** hand-edits`timeline.md`\n\n,`wiki.db`\n\n, or auto-generated`index.md`\n\nentries directly — they are step-4 outputs, not inputs.\n\nThe `context_for(question)`\n\npipeline, §6. This is the hot path and\nshould be fast — it's a handful of indexed SQLite queries, not a\nfull-bundle scan.\n\nA synthesized answer worth keeping — a comparison, an analysis, a\nconnection the retrieval surfaced — should be filed back as a new\nConcept page (§4.1) rather than left in chat history, the same\ncompounding behavior the original gist specified (\"good answers can be\nfiled back into the wiki as new pages... your explorations compound in\nthe knowledge base just like ingested sources do\"). Filing back runs the\nsame steps as §7.1: write the page, regenerate `wiki.db`\n\n/`timeline.md`\n\n,\nupdate `index.md`\n\n, log it.\n\nPeriodic health-check pass over `wiki.db`\n\n, expressible as SQL:\n\n- Orphan concepts: no inbound\n`relations`\n\nand no inbound prose links. - Broken relation targets:\n`object`\n\npaths that don't resolve to an existing concept (tolerate per OKF §6.1, but surface for review). - Stale concepts:\n`stale_after`\n\nin the past. - Predicate drift: near-duplicate predicates that should be merged\n(\n`invested_in`\n\nvs`invested-in`\n\nvs`backs`\n\n) — prune during lint, don't let the vocabulary fork. - Contradictions: conflicting\n`relations`\n\nasserted about the same subject/predicate pair from different sources. Exempts a small, hand-maintained set of predicates that are inherently multi-valued (`has_part`\n\n— a subject can genuinely have several parts at once), so the check only fires on predicates expected to hold one object. - Asymmetric relations: a\n`relations`\n\nentry with no corresponding reverse entry on the object concept — see the hard rule in §7.1; this is the check that catches a violation of it.\n\nLint detects; it does not itself rewrite files. A predicate-synonym finding, for example, gets fixed by a §7.4 Reindex-triggering bulk edit or a normal ingest-style edit, not by the lint pass itself.\n\nA full rebuild of `wiki.db`\n\n(§5), `timeline.md`\n\n(§3), and\n`wiki-trigger-keywords.txt`\n\n(§3, §12 — every concept's `title`\n\nand\n`tags`\n\n, deduped) from every concept file, independent of any single\ncontent change. Idempotent — running it twice with no intervening edits\nproduces the same output. Needed for:\n\n**Embedding backend changes**(§9.2): switching the embedding model (e.g. upgrading`nomic-embed-text`\n\nto a newer version) requires re-embedding every concept, not just recently-touched ones.**Recovery**:`wiki.db`\n\nis a disposable cache (§5); if it's lost or corrupted, Reindex is how it comes back, with no dependency on ingest history.**Bulk migrations**: executing a predicate rename or OKF-version upgrade that lint (§7.3) flagged, across every affected file, then rebuilding the index in one pass rather than file-by-file ingest edits.\n\nIngest, Query, and Lint have no feedback loop on retrieval *quality* —\nonly on data integrity. This matters specifically because `context_for()`\n\nfeeds an LLM automatically rather than a human reading the wiki directly,\nso a ranking regression can go unnoticed. A commenter on the original\nKarpathy gist measured this failure mode directly: a compiled wiki\nanswered *confidently* even for questions from a domain the wiki had\nnever covered, with no signal anywhere that the answer wasn't grounded —\nevery query returned a full set of \"relevant\" hits regardless.\n\nMaintain a fixed regression set of questions with expected seed concepts\nand/or expected facts, split between in-bundle and deliberately\nout-of-bundle questions. Periodically (or on every Reindex) run\n`context_for()`\n\nagainst the set and check: does the right concept still\nseed first (catches the exact-title/BM25/vector-ranking regressions from\n§10.3), does an out-of-bundle question return a clear \"no confident\nmatch\" rather than a plausible-looking wrong answer, and has trust-tier\nranking (§6.4) held. This is the retrieval-quality analog of §7.3's\ndata-integrity checks — both are lint in spirit, but this one needs a\nlabeled question set to run against, not just a schema/frontmatter scan.\n\nA recovery/refresh operation, distinct from Ingest (§7.1) and Reindex\n(§7.4): discards concept files that were produced by ingesting a source\nnow sitting in `references/`\n\n, and reconstructs them by re-running Ingest\nagainst those same references — e.g. after fixing a bad extraction,\nadopting a new `type`\n\nconvention (§8), or wanting a later ingest\nprocess's quality applied to older pages. Unlike Reindex, this **does**\ntouch concept files, deliberately — the goal is regenerating them, not\njust the derived `wiki.db`\n\n/`timeline.md`\n\ncache.\n\n**Scope, using the §4.2 provenance convention.** A concept file is\n*ingest-derived* — and therefore in scope for replacement — only if its\n`sources`\n\nfrontmatter cites at least one `references/...`\n\n-rooted\n`resource`\n\n. A concept file with no `sources`\n\nblock, or whose sources are\nall external URLs never mirrored locally, is out of scope by\nconstruction: that is the shape of a filed-back Concept page (§4.1, §7.2)\nbuilt from discussion and general web search rather than a raw ingest of\na saved document, and this operation must never touch it.\n\nProcedure:\n\n- Identify candidates by scanning every concept's\n`sources`\n\nfor a`references/...`\n\n-rooted`resource`\n\n— deterministic, run through the CLI (`rebuild-candidates`\n\n, reads from`wiki.db`\n\n) rather than a raw shell scan, same rationale as the rest of §7's operations. - Confirm scope with the user before deleting anything — an\ningest-derived file may since have accumulated a\n`verified: {by: \"human:...\"}`\n\nstamp or hand edits worth preserving; a passage worth keeping that isn't grounded in a reference should be moved into its own Concept page first, since Concept pages stay out of scope here by design. - Delete the confirmed concept files.\n`references/`\n\nitself,`log.md`\n\n, and every out-of-scope concept file are untouched. - Re-run Ingest (§7.1) over the same\n`references/`\n\nmaterial, one source at a time — this is what actually reconstructs the concept files, now under the bundle's current conventions (§8). - Reindex (§7.4) once every deleted concept file has been recreated.\n- Run Lint (§7.3), then manually confirm prose links inside preserved\nConcept pages still resolve — Lint's broken-target check covers typed\n`relations`\n\nonly, not markdown prose links, so a concept recreated under a slightly different path won't necessarily surface there. - Append one\n`log.md`\n\nentry for the whole rebuild, not one per re-ingested reference — this is a single deliberate operation, not N separate ingests.\n\n**Lint (extends §7.3).** One check, using the same provenance scan as\nstep 1 above, run in the opposite direction: **unreviewed-reference** — a\nfile under `references/`\n\nthat no concept's `sources[].resource`\n\ncites\nanywhere in the bundle. This is a deterministic set difference\n(`references/`\n\ndirectory listing minus every cited `resource`\n\npath), not\na freshness or mtime check — there is no ingestion-tracking state\nanywhere in this design (§9.3: regenerate, never persist-and-drift), so\nthis is the closest thing to \"what's new in `references/`\n\n\" the pipeline\noffers. It is a prompt to look, not a verdict: a reference can\nlegitimately have zero citing concepts because it was reviewed and\ndeliberately never promoted to a concept page — out of the bundle's\ndomain, or mirrored for context only. Don't treat every finding as\nmissed work; check the reference before assuming it needs ingesting.\n\nA **concept-file-level** mechanism for recording that one whole concept\nfile has been replaced by another — not a per-fact confidence score with\ncontinuous decay (that's out of scope; see the note at the end of this\nsection). Distinct from two things already in this spec that look\nsimilar but do less:\n\n`stale_after`\n\n(§4.2, §6.4) marks a fact as expired by*date*, with no record of what, if anything, replaced it.- Lint's\n`contradiction`\n\ncheck (§7.3)*detects*that two relations conflict; it does not resolve which one is current or link them.\n\nSupersession closes that gap for the concept-file granularity: an explicit, dated, bidirectional pointer from an outdated concept file to the one that replaced it, with the old file preserved (not deleted) and excluded from default retrieval rather than silently left ambiguous.\n\n**When to use it.** Only when the old concept file remains independently\nmeaningful as a historical record — a former officeholder, a superseded\norg structure, a deprecated architecture decision. If the old content was\nsimply *wrong* (a bad extraction, a typo, a source that turned out to be\ninaccurate), fix it in place via a normal Ingest edit (§7.1) — no\nsupersession chain. Rule of thumb: if \"what did we believe before\n`<date>`\n\n?\" is a legitimate question about this concept, use supersession;\nif it isn't, just correct the file.\n\n**Frontmatter fields** (extension, like `relations`\n\n— not OKF core):\n\n```\n# on the outdated concept file (entities/george-hw-bush-presidency.md)\nstatus: superseded\nsuperseded_by: /entities/bill-clinton-presidency.md\nsuperseded_at: \"1993-01-20\"   # optional; defaults to the successor's generated.at if absent\n# on the successor concept file (entities/bill-clinton-presidency.md)\nsupersedes: [/entities/george-hw-bush-presidency.md]\n```\n\n`superseded_by`\n\n: bundle-relative path (with`.md`\n\n, like a`relations`\n\n`object`\n\n— §4.3) to the one concept file that replaces this one. A concept has at most one`superseded_by`\n\nat a time.`supersedes`\n\n: array — a successor may replace more than one prior concept file (e.g. a merge).- Setting\n`superseded_by`\n\nwithout also setting`status: superseded`\n\nis invalid —`status`\n\nis what §6.4's default filtering and §7.3's lint actually key off;`superseded_by`\n\nsupplies the destination.\n\n**Hard rule — assert both directions in the same edit**, exactly the\ndiscipline §7.1 requires for `relations`\n\n: when A gets `superseded_by: B`\n\n,\nB gets `supersedes: [..., A]`\n\nin the same pass. §7.7's\n`asymmetric-supersession`\n\nlint check below exists specifically to catch\na violation of this rule, mirroring §7.3's existing\n`asymmetric-relation`\n\ncheck.\n\n**Chains.** Supersession may chain (A → B → C — e.g. George H. W. Bush\n→ Bill Clinton → George W. Bush) — no automatic mid-chain\nresolution is attempted; a query-time chain walk (below) always resolves\nto the end of the chain. Bound the walk (e.g. 5 hops) to survive an\naccidentally-introduced cycle without hanging — the `supersession-cycle`\n\nlint check below is what's supposed to catch that case before it ever\nreaches query time.\n\n**Retrieval integration (extends §6.4 and §6.3):**\n\n- A concept file with\n`status: superseded`\n\nis excluded from seed matching by default (§6.1) — exact-title, BM25, and vector search must not surface it as a top-level seed. Instead, resolve transparently to the end of its`superseded_by`\n\nchain, the concept-file analogue of how`stale_after`\n\nalready excludes a fact rather than presenting it as current. - This is exclude-from-seeding, not delete-from-graph: a superseded\nconcept remains a valid\n`relations`\n\n`object`\n\non other, still-active concepts (e.g.`{ predicate: preceded, object: /entities/george-hw-bush-presidency.md }`\n\n) and must resolve normally, not be flagged as`broken-target`\n\n. - During bounded graph expansion (§6.3), a hop that lands on a superseded\nconcept is down-weighted, not excluded outright — a\n`SUPERSEDED_MULTIPLIER`\n\n(e.g.`0.15`\n\n) alongside the existing`trust_multiplier`\n\nin the scoring formula, preserving \"deprioritized, not deleted\" rather than adding a second binary include/exclude gate next to`stale_after`\n\n's. - An explicitly retrospective question (\"what was true before\n`<date>`\n\n?\", \"who was President before Bill Clinton?\") is exactly what a superseded concept still needs to answer — treat this as a new case for §6.2's intent detection (a`HISTORICAL`\n\nbucket, or a modifier on`WHEN`\n\n) that lifts the default seeding exclusion for that query only, rather than ever deleting the content. - Serialization (§6.5): a fact drawn from a superseded concept renders\nwith an explicit marker so the answering LLM can never present it as\ncurrent without the reader being able to tell:\n\n```\n- George H. W. Bush inaugurated President, as of 1989-01-20 (superseded by /entities/bill-clinton-presidency.md as of 1993-01-20)\n```\n\n**Lint (extends §7.3)** — two new checks, same \"detects, does not\nrewrite\" discipline as the rest of §7.3:\n\n**dangling-supersession**:`superseded_by`\n\ndoesn't resolve to an existing concept file. Parallel to`broken-target`\n\n, scoped to this field.**asymmetric-supersession**:`superseded_by`\n\nset on A with no matching`supersedes`\n\nentry on the target, or vice versa. Parallel to`asymmetric-relation`\n\n; catches a violation of the hard rule above.**supersession-cycle**: walking`superseded_by`\n\nfrom any concept revisits a concept already in the chain. Catches an accidental loop before it breaks the bounded query-time walk above.\n\n** entities/index.md.** A superseded concept's entry is not removed —\nit's still real prior content, worth being able to find by browsing —\nbut gets annotated with the successor path, generated the same\nnon-hand-edited way as every other index entry (§3, §7.1 step 5).\n\n**Interaction with §7.6.** Orthogonal: being `status: superseded`\n\ndoes\nnot exempt a concept file from Rebuilding from references — scope there\nis decided purely by the provenance convention (§4.2), independent of\nlifecycle status.\n\n**Non-goal, explicitly.** This is a discrete, binary predecessor→successor\npointer at the concept-file granularity — not the broader \"continuous\nconfidence score with time-decay and reinforcement\" some wiki-pattern\nwriteups propose at the individual-fact level. That's a materially\ndifferent, heavier mechanism (per-fact provenance counting, a decay\nfunction, a reinforcement event); nothing here should be read as a first\nstep toward it without a separate design pass.\n\nBoth `type`\n\n(OKF-native) and `predicate`\n\n(this spec's extension) are\n**open vocabularies**, not fixed enums — consistent with OKF's own\n\"producers pick descriptive values, consumers tolerate unknown ones\"\nstance (§4.2). `Concept`\n\n, plus the Entity-page starter set — `Person`\n\n,\n`Organization`\n\n, `Product`\n\n, `Place`\n\n, `Country`\n\n— are the `type`\n\nvalues every bundle\nshould expect to use from day one (§4.1); use the specific value whenever\na page clearly fits one — a person's file gets `type: Person`\n\n, not\ngeneric `Entity`\n\n— and reach for plain `Entity`\n\nonly as a fallback when\nnone of the starter set applies. Everything else (`Playbook`\n\n, `Metric`\n\n,\n`Attested Computation`\n\n, or a bundle-specific type) is genuinely open and\ngrows as needed. Document predicates as they're\ncoined during ingest in `CLAUDE.md`\n\n/`AGENTS.md`\n\n, prune synonyms during\nlint (§7.3). Do not pre-design an ontology before there's a bundle to\nobserve.\n\nSurfaced as open questions during design, since closed. Recorded with\nrationale rather than deleted, so the implementing agent knows *why*,\nnot just *what*:\n\n`founded`\n\n/ `founded_by`\n\n, `invested_in`\n\n/ `backed_by`\n\n: **hard ingest\nrule, both directions asserted in the same pass** (§7.1 step 3), enforced\nby the asymmetric-relations lint check (§7.3). Rejected alternative:\ncomputing inverses automatically from a predicate-inverse lookup table at\nquery time, which would need only one direction hand-asserted. Chosen\nagainst, because it pushes complexity into the query layer (§6) for\nevery read, in exchange for saving one extra frontmatter edit at ingest\n— the wrong trade given queries vastly outnumber ingests over the\nbundle's life.\n\nDecided (§6.1): `nomic-embed-text`\n\nvia a local Ollama endpoint\n(`localhost:11434`\n\n) — the same convention Mnemon uses, evaluated and\nkept even though Mnemon's storage engine was not (§5.1). During early\nprototyping, a TF-IDF vector stood in for the *mechanism*\n(RRF fusion, ranking) because the sandbox used to build the first\nprototype couldn't reach Hugging Face Hub to pull model weights — an\nenvironment constraint, not a design choice. TF-IDF was never carried\nforward: it's corpus-relative (needs refitting as the bundle grows,\nunlike static per-document embeddings) and, being pure term-frequency,\nwon't generalize to paraphrased questions the way dense embeddings\nshould. The finished reference implementation calls Ollama directly for\nevery embedding (`ollama_embed`\n\n, wired at both index-build and\nquery time) — there is no TF-IDF fallback in the production path.\n\n`wiki.db`\n\nand `timeline.md`\n\nare regenerated on every ingest,\nunconditionally (§7.1 step 4) — never hand-edited, never treated as a\nsecond source of truth. This is the property the rest of the design\nleans on (§1, §3): concept frontmatter is authoritative, everything else\nis a rebuildable cache. Dropping `router.md`\n\n(§3) removed the one file in\nthe bundle that didn't follow this rule.\n\nThese were hit while prototyping this exact design and cost real debugging time — treat as hard rules, not suggestions.\n\nYAML 1.1 parses bare `on`\n\n/ `off`\n\n/ `yes`\n\n/ `no`\n\nas booleans (the\n\"Norway problem\"). `on: 2009-07-06`\n\nsilently becomes key `true`\n\n. Use\n`date`\n\n(§4.2).\n\nAn unquoted `2026-08-09`\n\n-style scalar risks being auto-parsed into a\nnative date object under some YAML parsers' default schema, and naive\ndownstream serialization can then produce a broken, non-ISO string\ninstead of clean ISO text — silently breaking OKF's own ISO-8601\nrequirement for `generated.at`\n\n(§5.2). The reference implementation's\nfrontmatter reader (`lib/frontmatter.ts`\n\n) uses the `yaml`\n\nnpm package's\nYAML 1.2 core schema, which does not have this failure mode — verified\ndirectly, not assumed. Quote every date/datetime frontmatter value as a\nstring regardless of which parser is in use anyway — it's the portable\nconvention for any other tooling (a YAML 1.1 parser, an editor plugin)\nthat might read this bundle, and it costs nothing.\n\nValidated directly: on a 4-concept test bundle, both FTS5 BM25 and a TF-IDF vector both ranked the wrong entity first for \"Who is Marc Andreessen?\" — because a related concept's page happened to mention \"Marc Andreessen\" in its own body text. Term-frequency and even embedding similarity alone cannot reliably distinguish \"this page is about X\" from \"this page merely references X\" on a cross-linked wiki, which is the normal case, not an edge case. §6.1's exact-title signal exists specifically to fix this and must not be dropped as a \"redundant\" simplification later.\n\nAlso validated directly: a naive substring check for this signal is\nwrong, not just imprecise — a concept titled \"AI\" seeded on any question\ncontaining \"explain\" (`expl-AI-n`\n\n), and \"Go\" seeded on \"ago\". Match at a\nwhole-word/phrase boundary (Unicode letter/number boundaries, not ASCII\n`\\b`\n\n, so this also holds for non-English titles), never plain\n`.includes()`\n\n.\n\nValidated directly against a 66-concept real-world bundle (Northlight, Aug\n2026), not a synthetic one: \"How do I fix a flat bicycle tire?\" was\ngated as confident because FTS5 `MATCH`\n\nreturned a nonempty result set —\nthe original rule — and its top-ranked bm25() result outscored a\ngenuinely relevant question's top-ranked result by raw score magnitude.\nThe match was a coincidence: \"flat\" hit the target concept's\n\"apartment\" sense inside an unrelated housing-policy essay,\n\"fix\" is a generic word, and neither \"bicycle\" nor \"tire\" appeared\nanywhere in that document at all — 2 of 4 query words genuinely present,\ncovering half the question, yet it out-scored a real match by bm25()'s\nraw number.\n\nThe mechanism: bm25()'s IDF term rewards rarity. A document containing\none coincidentally rare query word scores *higher* than a document\ncovering several common ones, regardless of how much of the question it\nactually accounts for — this is not a tunable edge case, it is what IDF\ndoes by design, so no threshold on the raw score fixes it.\n\nMeasured across this same bundle: every genuinely relevant question\ntested had **100%** of its extracted content words present in its\ncorrect top-ranked match; every deliberately off-domain natural-English\nquestion tested had **≤50%**. Gate BM25 confidence on word *coverage* of\nthe top match — the fraction of the question's content words that\nactually appear in it, checked independently of bm25()'s score — not on\nscore magnitude and not on mere nonemptiness. `>0.5`\n\n(strictly more than\nhalf) separates the two clusters with margin on the cases tested; retune\nper-bundle via `evaluate.json`\n\n(§7.5) the same way the vector threshold\nis retuned. This does not change which candidates get *ranked* by BM25\n— only whether a BM25-only match is trusted enough to answer at all.\n\nEverything in §1–§9 targets the scale this pattern is meant for — hundreds to low thousands of concepts (§2's non-goal: this is deliberately not a graph database). Nothing in this section should be built now. It's recorded so the trigger conditions are explicit if the bundle ever outgrows that range, rather than discovered as a production incident.\n\nDocument count alone is not the right scaling variable to watch — it's a proxy that tracks some components and not others.\n\n| Component | Degrades along | Not along | Rough threshold | Fix |\n|---|---|---|---|---|\n| FTS5 keyword search | — | doc count | Scales to millions; no revisit needed. | — |\n`relations` JSON column |\nEdge count (concepts × avg relations/concept) — JSON1 has no index, so `json_each` is scanned per query. |\nDoc count in isolation | Tens of thousands of edges, once §6.3's multi-hop expansion starts self-joining over an unindexed view. | §11.2 |\n`embeddings` table |\nVector row count — the reference implementation's cosine-similarity scorer is a brute-force in-process loop over every row today. | Doc count in the abstract, though it tracks 1:1 (one vector per concept) | Low thousands of vectors, where per-query linear scan starts adding real latency. | §11.3 |\n| Graph traversal | Edge density and required traversal depth. | Doc count | Only if requirements shift to unbounded-depth or whole-graph algorithms — not reached by §6.3's bounded 1–2 hop retrieval. | §11.4 |\n\n**When**: multi-hop queries (§6.3) measurably slow down doing self-joins\nover `json_each`\n\n— in practice, tens of thousands of edges (a few\nthousand concepts at typical relation density).\n\n**What**: a build step folded into Reindex (§7.4) that flattens every\nconcept's `relations`\n\narray into rows of a normalized, indexed table:\n\n```\ncreate table edges (\n  subject_path   text not null,\n  predicate      text not null,\n  object_path    text,   -- null if the object is a literal, not a concept\n  object_literal text,   -- populated when the object is not a concept path\n  date           text,\n  source_id      text\n);\ncreate index idx_edges_subject on edges(subject_path);\ncreate index idx_edges_object  on edges(object_path);\n```\n\nConcept frontmatter's `relations`\n\nfield (§4.3) is unchanged — this is a\nquery-layer materialization, not a file-format change. `context_for()`\n\n's\nexpansion query swaps `json_each`\n\n/`json_extract`\n\ncalls for plain indexed\njoins against `edges`\n\n; same logic, faster execution.\n\n**When**: cosine-similarity retrieval (§6.1) adds noticeable per-query\nlatency — roughly low thousands of concepts, since the current scorer\nis an unindexed linear scan over every embedding row, run in-process\non each query.\n\n**What**: replace the plain `embeddings(path, vector)`\n\ntable with a\n`sqlite-vec`\n\n`vec0`\n\nvirtual table\nand query via SQL KNN instead of an in-process loop. It's real and actively\nmaintained — a single dependency-free C extension — but still pre-v1\n(0.1.10-alpha at time of writing): pin a version and expect this\nmigration to need revisiting before it hits 1.0. Embedding generation\nitself (Ollama + `nomic-embed-text`\n\n, §9.2) is unaffected; only storage\nand query of the resulting vectors change.\n\n**Not triggered by concept count.** Triggered by either:\n\n- Edge density growing far past a sparse wiki-style graph, dense enough that indexed-table joins (§11.2) stop being fast even bounded to 1–2 hops.\n- A requirement change: if\n`context_for()`\n\never needs unbounded-depth traversal or whole-graph algorithms (shortest path across the entire bundle, centrality, community detection) rather than the bounded, budget-constrained expansion specified in §6.3 — a different query shape that SQL joins express poorly regardless of indexing, where Neo4j/Cypher-class engines earn their keep.\n\nIf neither condition holds, §11.2's indexed edge table keeps SQLite adequate indefinitely. §6's retrieval design was built to never need more than a bounded hop count in the first place, which is exactly the case a relational engine handles well.\n\n§7.2 assumes the calling agent judges when a question is in-bundle and\ninvokes `context_for()`\n\ndeliberately. That judgment can silently fail to\nfire in practice — a full stretch of clearly in-domain conversation can\npass with zero automatic queries, because nothing forces the check. This\nsection specs an optional mitigation, external to this bundle's own CLI:\na harness-level hook (Claude Code's `settings.json`\n\nhooks —\n`UserPromptSubmit`\n\n) that nudges the agent to query when the incoming\nmessage plausibly touches the bundle. Not installed by\n`/add-llm-okf-graph-wiki-vb`\n\nby default — a per-group opt-in, since it\nadds a script and a `settings.json`\n\nentry the operator should choose to\nadd, not something every bundle needs.\n\nThe hook's only job is a cheap, deterministic yes/no on whether the\nincoming message is in-bundle. It must not run full `context_for()`\n\n(§6)\nitself — that pipeline (FTS + vector embedding + bounded graph expansion)\nis comparatively expensive, and would be paid on every turn regardless of\ntopic if the hook triggered it directly, including the majority of turns\nthat have nothing to do with the bundle's domain. On a match, the hook\nshould only emit a short nudge string reminding the agent to consider\nrunning `query`\n\n— the agent still pays for `context_for()`\n\nonly if and\nwhen it actually acts on the nudge.\n\n`wiki-trigger-keywords.txt`\n\n(§3, §7.4) is the input: every concept's\n`title`\n\nand `tags`\n\n, deduped and regenerated on every Reindex — same\n\"cache, never hand-edited\" treatment as `wiki.db`\n\n/`timeline.md`\n\n(§9.3).\nDeliberately narrower than a full free-text scan: proper nouns and\nspecific tags have a much lower false-positive rate than generic topic\nwords matched against arbitrary prose (validated directly — see §12.4).\n\nA plain shell script registered as an additional entry under the\nharness's `hooks.UserPromptSubmit`\n\narray (alongside any hook already\nregistered there, not replacing it):\n\n- Read the harness's stdin JSON payload, extract the prompt text\n(\n`.prompt`\n\n, falling back to other field names in case the harness's schema differs — fail silent-open, i.e. no nudge, on a schema mismatch rather than erroring). - Loop\n`grep -qiFw`\n\n(fixed-string, whole-word, case-insensitive) over each non-comment, non-blank line of`wiki-trigger-keywords.txt`\n\n. - On any match, print one line naming the matched concept(s) and\nsuggesting the\n`query`\n\nop. Exit 0 either way — this hook must never block the turn.\n\nThe keywords file lives inside the bundle (§3); the hook script itself\nlives in the group's own workspace (`/workspace/agent/hooks/`\n\nby\nconvention), not under the shared, read-only skill directories\n(`/app/skills/...`\n\n) — those are mounted read-only to the agent and can't\nhost a script that needs per-group editing.\n\nConcretely, the registration:\n\n```\n{\n  \"hooks\": {\n    \"UserPromptSubmit\": [\n      { \"hooks\": [{ \"type\": \"command\", \"command\": \"<existing-hook-if-any>\" }] },\n      {\n        \"hooks\": [\n          {\n            \"type\": \"command\",\n            \"command\": \"/workspace/agent/hooks/wiki-trigger.sh\",\n            \"timeout\": 5\n          }\n        ]\n      }\n    ]\n  }\n}\n```\n\nA short `timeout`\n\nmatters: this hook must never meaningfully delay a\nturn — if it hangs, the harness should cut it off well before it becomes\nnoticeable.\n\nThree were hit building this pattern, worth carrying forward as hard rules rather than rediscovering:\n\n**Whole-word matching, not substring.** A trigger list sourced from`tags`\n\n(§7.4) routinely includes short slugs (`ai`\n\n,`ft`\n\n, and similar). Plain substring`grep -F`\n\nmatches those inside unrelated words (`ai`\n\ninside \"again,\"`ft`\n\ninside \"often\"), producing near-constant false positives. Use`-w`\n\n(`grep -qiFw`\n\n).**Harness-injected wrapper text can collide with keywords.** Some harnesses prepend boilerplate to every prompt (e.g. a context/metadata tag carrying the install timezone) before the hook ever sees it. A keyword that happens to match a token inside that boilerplate (a country or city name embedded in a timezone string, for instance) fires on every single turn regardless of what the user actually typed. Strip known wrapper markup from the extracted prompt text before matching.The reference container doesn't install it by default — a script that assumes it's on`jq`\n\nis not guaranteed present in the agent image.`PATH`\n\nfails silently (empty prompt extracted, hook effectively always a no-op) rather than erroring loudly. Either add`jq`\n\nvia the bundle's own`install_packages`\n\nself-mod path (§ container skill's self-mod docs) before relying on it, or parse the stdin JSON with something already guaranteed available (e.g. a one-line`node -e`\n\nfallback) so the hook degrades to \"no nudge\" only on a genuine schema mismatch, not on a missing binary.\n\nThe gate itself is free relative to `context_for()`\n\n— a loop of `grep`\n\ncalls over a short keyword list, a few milliseconds, no LLM or embedding\ncall. Token cost only appears on an actual match, and even then is just\nthe fixed reminder string (no retrieved facts injected).\n\nA finished TypeScript implementation (runs on Bun) demonstrates every mechanism above end-to-end, one file per concern:\n\n`build-index.ts`\n\n— reindex: walks`entities/`\n\n/`computations/`\n\n, rebuilds`concepts`\n\n,`concepts_fts`\n\n(FTS5), and`embeddings`\n\nfrom scratch (§5, §7.1 step 4).`context-for.ts`\n\n—`context_for(question)`\n\n: hybrid seed matching (exact title + FTS5 + vector cosine, fused), bounded graph expansion, trust- and budget-aware filtering (§6).`lib/ollama-embed.ts`\n\n— real`nomic-embed-text`\n\nembeddings via a local Ollama endpoint, plus the cosine-similarity scorer (§9.2). No TF-IDF or other placeholder in this path.`lib/rrf.ts`\n\n— reciprocal rank fusion across the three seed signals.`lib/graph-score.ts`\n\n— bounded multi-hop expansion and predicate-aware scoring (§6.2–§6.3).`lib/frontmatter.ts`\n\n— the YAML 1.2 core-schema frontmatter reader/writer (see §10.2's note on the YAML 1.1 date-coercion pitfall it sidesteps).`lint.ts`\n\n,`evaluate.ts`\n\n,`timeline.ts`\n\n,`keywords.ts`\n\n— the remaining CLI operations (§7.3–§7.4);`keywords.ts`\n\nregenerates`wiki-trigger-keywords.txt`\n\n(§12's optional hook input) from every concept's`title`\n\nand`tags`\n\n, skipping non-string tag values (frontmatter documents`tags`\n\nas strings, but a bare unquoted number in YAML — e.g. a year written as`2009`\n\ninstead of`\"2009\"`\n\n— parses as a JSON number; caught in practice, not hypothetical, and also the right call independent of the parse-safety angle, since a bare number is a high-false-positive trigger keyword regardless of type).\n\nEach `lib/`\n\nmodule has unit tests alongside it. Treat this as a\ndirect, runnable reference for shape and query patterns in any\nlanguage/runtime — nothing in it is a placeholder standing in for a\nnot-yet-built mechanism.", "url": "https://wpnews.pro/news/okf-graph-wiki-agent-maintained-knowledge-graph-for-context-retrieval", "canonical_source": "https://gist.github.com/VivianBalakrishnan/83d1ea5f929d0ae51bca7fe25129b0d7", "published_at": "2026-08-14 17:01:15+00:00", "updated_at": "2026-08-15 07:12:19.513610+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "ai-agents", "developer-tools", "ai-infrastructure"], "entities": ["Andrej Karpathy", "Open Knowledge Format", "Google Cloud Platform", "SQLite", "TypeScript", "Obsidian", "Datasette", "mnemon-dev/mnemon"], "alternates": {"html": "https://wpnews.pro/news/okf-graph-wiki-agent-maintained-knowledge-graph-for-context-retrieval", "markdown": "https://wpnews.pro/news/okf-graph-wiki-agent-maintained-knowledge-graph-for-context-retrieval.md", "text": "https://wpnews.pro/news/okf-graph-wiki-agent-maintained-knowledge-graph-for-context-retrieval.txt", "jsonld": "https://wpnews.pro/news/okf-graph-wiki-agent-maintained-knowledge-graph-for-context-retrieval.jsonld"}}