The compiled read model
A dense manifest, section addressing, byte-determinism, and retrieval with no model in the path.
Separate what humans write from what agents read
OKF fuses the two, and that is where it loses on efficiency. Humans write Markdown. A compiler emits
a compact artifact. The OKF source stays intact, so okflint, the visualizer and Obsidian keep
working on the same folder.
A dense manifest that fits in context
Budget 10 to 15 tokens per concept. Then the agent makes one hop: read the manifest, know exactly which three files it needs, fetch them in parallel. No traversal.
Drop YAML for a positional encoding, and declare the keys once in a header.
# tenant: acme
# bundles: ops sales
id bundle kind status grain summary links
orders sales bigquery_table - order_id One row per completed customer order. customers
customers sales bigquery_table deprecated customer_id Registered customers, including churned. orders
rev_net sales metric - - gross - refunds - tax orders paymentsAgainst the YAML equivalent this cuts structural overhead per record by roughly four to five times.
The links column carries the whole graph, so the agent plans traversal without opening anything.
Constraints the compiler enforces: summaries are single-line, contain no tabs, and are truncated at 120 characters by default.
Why those seven columns and no others
id and bundle address. kind and grain are what the model actually needs to decide whether a
concept answers the question. summary is the human sentence, compressed. links is the graph.
status is the only v0.2 trust field in the row, and only when it deviates. A concept with no
status, or one marked current, gets -. deprecated and draft are what an agent has to see
before choosing the concept, which is what earns a cell paid for on every turn. sources,
generated, verified and stale_after are all real, and all cheaper to fetch on demand.
Section-addressable shards
get(id, section) returning one slice costs a fraction of the full document.
An earlier draft invented @schema and @joins markers and assumed an offline LLM pass would
rewrite prose into them. That turned out to be unnecessary. Markdown headings already are the
section markers, and real bundles use them. Every Google sample concept carries # Schema,
# Common query patterns, # Metrics.
The compiler slugs each heading and records its byte range, so section addressing works on unmodified OKF with no model in the build path.
Fenced code blocks are excluded first
A # comment line inside a SQL or shell example is not a heading, and these bundles are full of
them.
Measured on the GA4 sample: fetching the events_ concept whole costs 9,419 bytes, fetching
--section schema costs 5,486. The saving grows with document size, and it costs nothing to have.
Prompt caching is the multiplier
Put the manifest at the front of the prompt. It stays stable across turns, so it hits cache at roughly 10% of the cost.
That makes byte-determinism a hard requirement on the compiler rather than a nicety. Identical input must produce identical output, or every rebuild invalidates the cache and the multiplier disappears.
Concretely:
- rows sorted by bundle, then by id
- link lists sorted
- no timestamp anywhere in the file
- statistics reported to stderr rather than embedded
- sorting by plain code-unit comparison, never
localeCompare, which varies by machine
Rough order of magnitude for 500 concepts:
| Approach | Effective tokens | Round trips |
|---|---|---|
| Naive OKF navigator | ~5.5k | 4 to 5 |
| Compiled manifest, cached | ~1.4k | 2 |
The win comes from replacing model-driven traversal with deterministic selection in one hop, not from TSV being shorter than YAML.
Sorting by bundle is what makes narrowing cheap
Rows arrive grouped by bundle, so one bundle is one contiguous run of lines. The reader hands back a byte range instead of parsing and reassembling. It is exactly as deterministic as sorting by id alone, and it is what makes a large tenant usable.
Scaling break point
The manifest stops fitting somewhere around 2,000 concepts, call it 25k tokens. Past that, shard by domain. A domain-level manifest of 50 lines stays cached, then the domain manifest loads. Still two hops.
Retrieval takes the model out of the loop
BM25 returns top-k, then a deterministic one-hop expansion over the link graph. Zero model calls in the retrieval path. It is the largest single latency win and it is independent of the format.
Search returns manifest rows, never bodies, so the two-hop path holds. Narrow with search, then
fetch only the chosen ids with get.
The searchable text is the manifest row plus the concept body, minus the links column. Link targets are ids, and indexing them would make every concept match its neighbours' names.
Scoring
Textbook BM25 with k1 = 1.2 and b = 0.75, over a per-tenant index rebuilt in memory.
Tokenizing splits on every non-alphanumeric run, so order_id and order id produce the same
tokens and a query written either way matches either form. The query goes through the same
function, which is what keeps that true.
Manifest cells count twice against a word of prose. That weight exists because compiling the
frontmatter away removed the id repetitions that resource and sources URLs used to contribute,
so an id or summary match has to be told apart from an incidental body match.
The value was swept over a 500-concept corpus, measuring hit rate at 8 for queries that name a concept:
| Field weight | Hit rate |
|---|---|
| 1 | 65% |
| 2 | 75% |
| 3+ | no further gain |
| 4+ | starts costing recall on descriptive queries |
Two is the smallest value that captures the gain, which is why it is the one shipped.
Cap the expansion
The first version expanded every link of every hit, which reads fine on a small fixture and falls apart on real data. On the Stack Overflow sample one dataset row links to sixteen tables, so eight hits became twenty-six rows, most of the manifest.
Search that does not narrow is worse than reading the manifest, because it costs a round trip to
learn nothing. Expansion is now capped at k and ranked by how many hits point at a target, which
took --k 3 from 85% of the manifest down to 31%.
The index is not persisted
Measured build times: 2.6 ms for a real bundle, 55 ms at 2,000 concepts, 274 ms at 10,000.
The break point for the manifest is around 2,000 concepts, so persisting the index would save roughly 50 ms once per snapshot in exchange for a serialization format, invalidation logic, and its own collector. It is built in memory and cached by snapshot digest instead, which makes a rebuilt tenant get a fresh index with no invalidation code to get wrong.
Query time is 0.4 ms at 2,000 concepts. There was nothing to buy.
Open questions
- Whether to keep OKF as the literal on-disk source, or accept any Markdown and treat OKF conformance as an export target.
- Whether full BM25F is worth it. The flat field weight above is the cheap version of the same idea, and it recovered the hit rate the compiler had cost. Per-field length normalization would be the real upgrade if id and summary matches start losing to incidental body matches again.
DESIGN.md is behind the code here
It still lists field weighting as undecided. The sweep settled it, and FIELD_WEIGHT = 2 ships
with a test covering it.