The OKF bundle: a build output, and its one stable interface

roteiro render okf writes an Open Knowledge Format bundle (v0.2) into okf/ (or --out <dir>): one markdown concept document per graph node, with YAML frontmatter, nested by kind — and by workspace member when rendering a workspace.

OKF is Google Cloud's vendor-neutral specification for the pattern this output has always been: a directory of markdown documents describing concepts, linked to each other. Its only hard requirement is that every concept carries a non-empty type.

It replaced the Obsidian vault in 4.0.0, and Obsidian still reads it

render obsidian is gone. This is not a loss of Obsidian support: a bundle is markdown with YAML frontmatter linked by ordinary markdown links, and Obsidian parses all three — Open folder as vault still works. What changed is that the output targets an open specification with other consumers rather than one application's conventions.

Two differences an Obsidian user will notice:

What the frontmatter carries, and why it is worth reading

Most OKF producers will emit type and little else. Roteiro emits the part the specification treats as optional and important — provenance — because the graph already distinguishes what a person wrote from what a machine derived. OKF turns that into trust tiers (§5.3), which a consumer derives from verified:

the graph's provenancefrontmattertier a consumer derives
authored — ADR and blueprint proseverified: [{ by: human:<id> }]human-reviewed
derived — deterministic extractionverified: [{ by: roteiro/<version> }]machine-confirmed
inferred — heuristic, carries a confidencegenerated: aloneunverified
external-authored / external-derived — imported from a peer's bundlethat peer's own verified: block, re-emitted verbatimwhatever they claimed
external-inferred — imported unverified, or acknowledged rather than trustedgenerated: aloneunverified

Derived is machine-confirmed rather than unverified deliberately: it is reproduced from the AST at a known commit, so a consumer can re-derive it. Inferred gets no verified key at all, because claiming otherwise would launder a guess into a confirmation.

Roteiro reads a bundle too — a peer's, never its own output

roteiro import --from okf <path> imports another repository's bundle as external knowledge. It exists so a cross-repo [[other-repo#thing]] reference resolves to a real concept instead of a placeholder holding only a key.

It does not make the directory below an input. That one is still emptied and rebuilt on every render, and there is no round trip through it. What you import is a bundle some other repository published.

Imported concepts are tagged external-* and can never be mistaken for this repository's own work. By default they arrive at external-inferred whatever the bundle claimed — their information without their confirmation — because a command you ran by hand should not silently adopt a stranger's verified: block as this graph's human-reviewed tier. --trust preserves the tiers they claimed, and re-emits their verifier by name on the next render.

Re-running the command is safe in both directions: it replaces that peer's layer wholesale, so nothing duplicates, and a concept the peer has since deleted is removed rather than left behind as an orphan.

A bundle is read liberally — an unrecognised type is imported, a document that carries no frontmatter or no type is skipped and the reason is printed — but a directory in which nothing at all parsed is refused rather than imported as nothing.

The reader is tested against bundles Roteiro did not write

Reading back one's own output proves a round trip, not interoperability. Since a reader tested only against its own writer will agree with itself about a dialect it also invents, the reader is exercised against two of the four bundles published in the specification's own repository — vendored under crates/rto-render/tests/fixtures/okf-upstream/, provenance and licence recorded alongside them.

That test found four defects the round trip could not, all of them silent. The reader hand-parsed a line-oriented YAML subset shaped like Roteiro's own emitter, and against real third-party markdown it dropped flow mappings (generated: { by: …, at: … }, the form the specification's own examples use), dropped flow sequences, dropped block sequences whose items sit at the parent key's indentation (PyYAML's default), and truncated folded multi-line scalars at their first line. Nothing was skipped and nothing was reported.

The trust consequence was the serious one: all nine concepts of the published acme_retail bundle read as unverified when eight carry a human sign-off, so import --from okf --trust adopted nothing while reporting success. The reader now parses frontmatter with a real YAML parser, and the counts are cross-checked against an independent implementation (below).

You do not have to remember the command

A workspace member that publishes a bundle at its conventional okf/ path is found automatically during the workspace scan, and you are asked about it once:

spoke publishes an OKF bundle, and this graph references it.

bundle:   /home/you/GIT/spoke/okf
asking:   not seen before
contains: 12 concept(s), 1 quarantined, 0 blocked by the content screen [invisible-characters]

[t] trust       import at `external-<their tier>`, keeping what they claimed
[a] acknowledge import at `external-inferred`: their information, not their confirmation
[i] ignore      leave the cross-repo placeholder as it is

The answer is recorded per peer, so it is asked once, not every sync. It lapses if the bundle moves, or if it starts carrying a class of screening finding it did not carry when you answered — an ordinary edit does not re-ask you.

You are only asked about peers this repository already references (it holds an extref: placeholder for them), because those are the ones importing would actually help. roteiro import --from okf <path> is still how you read anything else, and running it records your answer too.

When there is nobody to ask — a server, a CI job, a pipe — the bundle is ignored, mentioned once, and nothing is recorded. A graph does not adopt a stranger's concepts because nobody was there to object, and a silent run must never leave behind an answer a person did not give.

A peer's prose is screened before it is stored

Imported text ends up in meta.content, which search results hand to a language model as grounding. So it is screened first, deterministically — no model is asked to judge it:

outcomewhat happens
passstored unchanged
quarantinethe concept is imported; its text is either stripped of invisible characters and hidden regions, or withheld entirely
blockthe concept is not imported at all

Block is reserved for text that is both hidden and directive — instructions to a model inside an HTML comment, behind display:none, or spelled with zero-width characters. A document that merely discusses prompt injection is quarantined, not refused: its concept, kind and relationships still arrive, so the cross-repo placeholder still resolves. What it loses is the prose.

The screen catches invisible codepoints (zero-width, bidi controls, the tag block, C0/C1 controls), content hidden by presentation, and English phrases that read as instructions to a model. It deliberately does not attempt homoglyph detection, decoding of encoded payloads, non-English patterns, or CSS cascade resolution — see crates/rto-graph/src/screen.rs, which says so at length.

OKF validation: what Roteiro borrows, and what it refuses

The obvious next feature is roteiro okf validate — a conformance gate over a stranger's bundle. The question is not whether to have one but how much of it to import, and the answer turned out to be "the model, not the checker".

W4G1/okf is a pure-Rust OKF v0.2 toolkit on crates.io (Apache-2.0, compatible with this project's MIT OR Apache-2.0). Its okf-validator crate does strict conformance checking with a severity split, okf lint adds hygiene rules, and the okf CLI also offers trust, info, links, graph, computations, diff and fmt. It is current, and it is substantially more complete than anything worth writing here as a side quest. Notably okf-core, the model and parser underneath it, has zero dependencies.

Two other ecosystem tools were read and are not suitable as references: okflint is a generic engine that validates against a manifest the producer writes, so it answers "does this match the rules I declared" rather than "does this conform to OKF v0.2" — and it cannot run on a third-party bundle at all without one being authored first. okf-schema ships no canonical OKF schema; every schema lives inside the bundle being checked and is the producer's own.

So the standing position is:

The comparison, against okf-core / okf-validator 0.2.6 (2026-08-27) on 2026-09-01, over every bundle published in the specification's repository at commit ad30107:

BundleConceptsokf trustRoteiro's readerResult
acme_retail98 human-reviewed, 1 unverified8 external-authored, 1 external-inferredagree
ga499 unverified9 external-inferredagree
stackoverflow2626 unverified26 external-inferredagree
crypto_bitcoin99 unverified9 external-inferredagree

Nothing was skipped by either side in any bundle. Roteiro's own render okf output for this repository also validates clean: 0 conformance errors across 9,029 concepts, judged by a validator that has never seen our output — a stronger statement than every_emitted_bundle_is_conformant can make, since that test encodes our own reading of §11.

What removing the oracle costs, and what covers it. A check that runs once proves the code was right that day. Two of the four bundles are therefore vendored as fixtures, so the suite permanently exercises a foreign bundle — the thing phase 1 never did, and the source of every defect this found.

The limit, stated so it is known rather than rediscovered: the fixtures cannot catch a divergence in a YAML shape that no vendored bundle contains. That is not hypothetical. stackoverflow writes tags: stackoverflow, posts, deprecated — a bare comma-separated string where §4.1 asks for a list — in seven documents, and that bundle is not vendored. The oracle is how that shape was found at all; it is now pinned by an_off_spec_shape_is_read_where_a_real_producer_writes_one, but the next such shape has no tripwire and would be found the same way or not at all.

So the standing advice is to re-run the comparison whenever the reader's parsing changes, or a new bundle is imported in anger. It is one command (cargo install okf), and okf::read's module documentation records exactly what to run and what the answer was last time. That is discipline, not automation, and naming it as such is the point.

What the four published bundles say about the spec

All four upstream bundles (ga4, acme_retail, crypto_bitcoin, stackoverflow) are conformant — confirmed independently by okf-validator and by okflint's core stage. Two disagreements surfaced and are worth raising upstream rather than encoding here:

The commands, and which of them gate

Everything under roteiro okf reads. None of it writes to the graph, and none of it rewrites a bundle: roteiro render okf is the only writer, and roteiro import --from okf is the only path by which a peer's content enters the graph — with the consent gate above.

CommandAnswersGates?
okf infoWhat is this bundle — size, tiers, staleness, links, computations, and the files it carries that are not conceptsnever
okf validateDoes it conform to OKF v0.2on any error
okf lintIs it hygienic — L1–L12, plus our R1never
okf trustWhat does it claim about itself, and has any of it expired--check, on staleness
okf linksDo its internal links name something the bundle contains — a concept, an asset, or a reserved file--check, on a target the bundle does not contain at all
okf syntaxDoes its fenced code parseon any error
okf computationsWhat Attested Computations does it declare (§10)--check, on an incomplete contract
okf diffWhat changed between two bundlesnever

Start with info; it composes the others' reports rather than deriving anything of its own, so it cannot disagree with the command that reports a number in detail.

Every report is escaped, and a shown id may not be the literal id

These are the commands an operator runs to decide whether to trust a bundle, which makes them the worst place for that bundle to choose what the terminal shows. A concept id, a path, a title, a status, an actor's name and a parser's quotation of the bundle's own bytes are all text somebody else wrote, and U+202E RIGHT-TO-LEFT OVERRIDE reverses everything after it while U+200B and the tag block occupy no width at all. added X and removed X reading as each other's opposite is a wrong answer, not a mangled one.

So every bundle-derived field on every okf report goes through rto_graph::screen::escape_for_diagnostic — the same allowlist #865 established, stated as a property rather than as a list of characters. A character reaches you unchanged when it is an ordinary space, or Unicode says it puts a mark on the page and does not call it a Default_Ignorable_Code_Point — with one character carved out of that in each direction:

Everything else follows the rule with no exceptions. A CJK path and an NFD-decomposed accent (which is what macOS hands out) are untouched. An emoji's base character is too, but its joiners and selectors are not — and they are caught by different halves of the rule, which is worth keeping straight: U+200D ZERO WIDTH JOINER is Cf, an Other category, so it is not ink at all, while U+FE0F VARIATION SELECTOR-16 is Mn, an ink category, caught only because Unicode marks it Default_Ignorable_Code_Point. So a heart-plus-VS16 shows as the heart followed by a visible \u{fe0f}. That cost is accepted rather than overlooked — passing an invisible character through silently is the bug this exists to close. Counts, severities, lint codes and the words of the report itself are this workspace's own and are not escaped. A trust tier is ours on okf trust, where ConceptTrust::tier is one of §5.3's three fixed tokens — but not on okf diff, where TrustMove carries the two tiers as String, so they are escaped there. Same word, two provenances: the field's type is what decides, not its name.

Three consequences worth knowing before they surprise you:

When any of that happened, the report says so on its last line and points at --json, which is not escaped and carries the exact bytes.

That promise was broken once and is worth knowing about, because the shape recurs. Issue #778: links --check gated on "resolves to a concept", so a link to a diagram sitting in the bundle was reported broken and failed CI, while info listed that same diagram under other files in the same run. A gate that cries wolf gets switched off — and on a real bundle 17 of 17 reported breakages existed on disk, with the one genuinely dead link indistinguishable among them. All three commands now ask whether the bundle contains the target, and only a target it does not contain at all fails the gate.

Two rules hold across all of them. --json selects a format and never changes what is reported or whether the command gates — settled on main by a bug where it did both. And the bare command reports without gating: reporting is what you want reading a stranger's bundle, gating is what you want in CI over your own, and a command that only did one would be wrong half the time.

A bundle is not only markdown, and the report says what else is in it

okf-core resolves a frontmatter path to any file — is_file(), no extension filter — and §10's computation: names one. So a conformant bundle can cite a document nothing here reads, and until ADR-0024 every report described only the part it could see: you read "0 violations", concluded the source was trustworthy, and the PDFs were never in scope of the thing you read.

okf info therefore inventories them, and prints the answer either way:

$ roteiro okf info okf/
  other files: 2 (4.0 KiB), not markdown and not screened:
      docs/policy.pdf (4.0 KiB)
      img/logo.svg (6 B)

Nothing is opened. The path, the size and the extension come from the directory entry, so this adds no parser and no attack surface of its own — and a symlinked directory is never recursed into, because this walks a directory somebody else controls.

A directory that will not open is reported, and reported even when nothing else was found:

  other files: none — every file in this bundle is markdown
  warning: 1 entry could not be inspected, so the inventory above is incomplete:
      locked

"None" and "the walk could not finish" are different answers, and printing the first when the second is true would be the same silence-taken-for-absence this inventory exists to remove.

The same line rides the consent prompt, which is the moment a person decides whether to trust a source: "screened clean" is a claim about the concepts, and it has to be visible that the screen's verdict did not cover everything in front of them.

Serving one is a separate decision. The viewer (okf-viewer feature, ADR-0022) is reached through roteiro explorer or roteiro serve at /okf, and roteiro explorer run inside a bundle directory serves that bundle even where there is no repository. Since ADR-0022 v1.4 it is also reachable from anywhere, including from inside a repository, as --scope bundle <PATH> on either server — one bundle, no graph and no explorer app beside it. That closed the hole the working-directory entry left: explorer discovers a repository before it looks for a bundle, so standing in one made the bundle mode unreachable. Its /f/ route types a file from a closed allow-list under a default-src 'none'; sandbox policy with nosniff, and now sets Content-Disposition: attachment for anything outside the image allow-list. A bundle does not get to choose how its bytes are presented, any more than it chooses what they are. Images are excluded because the viewer embeds them with <img>.

Staleness is asked as of a day you name

okf trust and okf info take --today YYYY-MM-DD. §5.4's rule is now >= stale_after, so the answer moves on its own — which makes the host clock an input, and an undeclared one.

$ roteiro okf trust okf/ --today 2026-12-31
  human-reviewed 8, machine-confirmed 0, unverified 1
  stale 7 (as of 2026-12-31)
  human-reviewed     metrics/revenue — verified by human:alice [STALE since 2026-12-31T00:00:00Z]

Without it the host's UTC date is used and still printed, so a captured summary says what it was true of. A malformed value is refused rather than quietly replaced by today's date: the flag exists to make a run reproducible, and a typo that restored the clock would leave a pipeline green and meaningless.

The combination worth looking for is the one above — human-reviewed and stale. The tier alone reads as reassurance, and it is a claim about when somebody last looked, not about whether it is still true.

One place Roteiro guarantees more than the specification asks

§11 says consumers MUST NOT reject a bundle for broken cross-links. Roteiro treats a broken authored link as drift and fails roteiro check over it. Both are right — the specification is telling consumers to be liberal, and Roteiro is a producer that promises more than it must. A Roteiro bundle should not contain a broken link.

The bundle is deleted and rebuilt on every render

The output directory is emptied first. Not merged, not updated in place — removed, then recreated:

roteiro render okf --out okf     # rm -rf okf, then write it

Nothing you put inside survives. Not a note you added there, not a folder you organised.

This is deliberate and will not change. A bundle is a build output of the graph, regenerated over itself, so a concept for a symbol you have since renamed does not linger for ever. The alternative — merging into whatever is already there — would mean the bundle accumulating concepts for code that no longer exists, which is worse than losing a file you should not have kept there.

Keep your own notes outside it and link in. A sibling folder works; so does anywhere your editor indexes alongside it.

Names are derived from keys, and collisions are settled once

A concept's filename is a slug of its node key. Where two keys slug identically within one directory, the later one carries a short digest of its key — the first in key order keeps the bare name, so an unchanged graph renders byte-identically.

Long keys are truncated at 200 bytes, and a truncated name always carries the digest: truncation can create a collision that the full keys did not have, by merging two long keys that share a prefix.

This matters more than it sounds. The Obsidian vault this replaced wrote one flat directory, and on macOS and Windows two names differing only in case are one file — so it was once 104 notes short of the count it printed, silently. Nesting by kind makes most of that structural, and the renderer asserts that the number of concepts it reports equals the number of files it wrote.

Workspace bundles

render okf -w <name> renders one bundle spanning a workspace's member repositories, nested per member:

okf/
  index.md
  app/
    files/…  decisions/…  symbols/…
  deploy/
    files/…  symbols/…

Nesting is what keeps two members' concepts apart. Node keys are repository-relative, so every repository's README.md is the same key — file:README.md — and a flat layout would have one overwrite the other. The vault solved this by qualifying keys as <project>::<key> and hashing every filename; directories make it structural instead.

Each member is read with its own configuration: [ingest] toggles and [debt] ignore come from that repository's roteiro.toml, not from wherever the command was run.

Bare render okf renders the current project alone, with sections at the bundle root — deliberately not "the workspace containing this repo". A bare render silently becoming a multi-repo one would move every concept a directory deeper and break every link into the bundle with no error.