# Roadmap

This roadmap tracks delivery against the plan in [`docs/specification.md`](docs/specification.md#roadmap). Each phase builds strictly on the previous one's output — a feature is only listed once, in the phase where it first ships, and later phases assume everything in earlier phases already works.

## Status at a glance

| Phase | Status |
|---|---|
| Phase 1 — Foundations | ✅ Complete |
| Phase 2 — Depth & Integration | ✅ Complete (11/11) |
| Phase 3 — Search, Interop & Intelligence | ✅ Complete (15/15) |
| Improvement Plan Delivery (competitive gap-closing) | ✅ Complete (7/7) |
| Improvement Plan (AI-native platform maturity, community feedback) | ✅ Complete (11/11 shipped) |
| Improvement Plan (provenance depth, graph diff, MCP tool-selection) | ✅ Shipped (6/6) |
| Phase 4 — Ecosystem | ⬜ Not started |

---

## Phase 1 — Foundations ✅

- [x] `okf-cli` skeleton (`init`, `scan`, `generate`, `validate`, `search`)
- [x] Repository scanning: recursive walk, `.gitignore` handling, git-aware indexing, single-package workspace support
- [x] Tree-sitter parsing and core symbol extraction (packages, modules, types, functions) for the initial language set: **Rust, Python, TypeScript/JavaScript, Go**
- [x] Direct relationship extraction (imports, call graph) for the initial language set
- [x] OKF bundle generation (`okf-generator`): markdown + YAML frontmatter, cross-linked via markdown links
- [x] Validator (`okf-validator`): schema conformance, frontmatter validity, link integrity, orphan detection, duplicate-identity checks
- [x] Basic search (`okf-search`): by symbol, package, module, type, and tag (no relationship queries yet)
- [x] Packaged as a standalone CLI binary with a cross-platform release workflow

Verified by dogfooding: running `okf-rs generate .` on this repository itself produces a full, deterministic OKF bundle (identical output across repeated runs) that `okf-rs validate` reports as clean, with a real resolved call graph across `self.method()`, `Type::method()`, and bare-identifier calls.

**Known limitations carried into Phase 2 by design, not oversight:**
- ~~Call-graph resolution only resolves a call when its callee name is *unambiguous* project-wide (exactly one function/method with that name).~~ — resolved in Phase 2 for ambiguous calls too, via optional LSP-backed disambiguation (`okf-rs generate --lsp`, see below); the unambiguous-name-only algorithm remains the always-on default.
- ~~Single-package projects only~~ — resolved in Phase 2 by multi-package workspace/monorepo aggregation (see below).
- No schema-version field is emitted yet, so the validator's schema-version conformance check (named in the spec) has nothing to check against until that's introduced.

---

## Phase 2 — Depth & Integration ✅

- [x] Graph queries (`okf-graph`): cross-module links, ownership, public API surface, and cycle/dependency analysis (Tarjan's SCC), plus shortest call-path — exposed via `okf-rs graph {callers,callees,cycles,api,modules,path}`
- [x] Relationship-aware queries backed by `okf-graph` — delivered as the `okf-rs graph` subcommand family above rather than folded into `okf-rs search` itself, since relationship queries (traversal, cycles) are a different shape than free-text search
- [x] Bundle diffing (`okf-rs diff <ref-a> <ref-b> [path]`) — compares two git refs' concepts (added/removed/changed) using a non-destructive `git worktree` checkout of each ref, so it never touches the caller's working tree
- [x] Agent entry-point generation: `CLAUDE.md`, `AGENTS.md`, `.github/copilot-instructions.md`, written/updated by `okf-rs init` (skip with `--no-agent-files`); idempotent via marker comments, so pre-existing content in those files is preserved and only the okf-rs section is replaced on re-run
- [x] LSP integration (`okf-lsp`) to enrich/disambiguate symbols beyond what Tree-sitter alone can resolve. A minimal, synchronous LSP client (spawns a language server, does the `initialize`/`initialized` handshake, `textDocument/didOpen`, and `textDocument/definition` over stdio LSP framing) plugs into `okf-analyzer`'s existing call resolution: a call whose callee name matches more than one candidate project-wide — previously always left unresolved — is now, when `okf-rs generate --lsp` is passed, resolved by asking that call site's real language server which definition it actually resolves to, and accepting the answer only if it lands inside one of the already-known candidates' own locations (never a guess). Entirely opt-in and additive: plain `okf-rs generate` (no `--lsp`) is byte-for-byte unaffected, and a language with no available server (or a server that errors/returns nothing) simply falls back to the exact same "leave unresolved" behavior as before
- [x] Incremental indexing: content-hash-based re-analysis of only changed files — `okf-rs generate` persists a `.okf-cache.json` at the project root, keyed by each file's path and content hash (not mtime, which git checkouts/CI don't preserve reliably); a file whose hash still matches skips the tree-sitter parse entirely and reuses its cached extraction, while the project-wide call-graph resolution step still runs in full every time (it's cheap, and it's what keeps a changed file's *callers* correctly updated). `--no-cache` bypasses the cache entirely (rule out a stale cache, or verify output determinism independent of cache state)
- [x] Extended language coverage: Java, C#, PHP, Kotlin, C/C++, Swift — all six shipped, each with its own real visibility rule rather than one heuristic reused everywhere: **Java**/**C#** require an explicit `public` modifier (classes/structs/interfaces/enums, methods/constructors, a single call-capture pattern per language covering bare/`this.`/object-receiver/static calls uniformly); **PHP**/**Kotlin** are public *by default*, opposite polarity — only an explicit `private`/`protected`(/`internal` for Kotlin) narrows it, matching what these languages actually do; **C++** is section-based (`public:`/`private:`/`protected:` labels apply to every member until the next label, with `class` defaulting private and `struct` public); **Swift** requires an explicit `public`/`open` (its unmarked default, `internal`, is genuinely not public, unlike Kotlin/PHP). Kotlin/Swift also share one `class_declaration` node kind across class/struct/enum/object, distinguished by shape (a literal keyword child, or body-node kind) rather than by node kind — the same shape Rust's `function_item` already has for functions-vs-methods
- [x] Multi-package workspace and monorepo aggregation — `okf_core::Project::load` now discovers *every* manifest file anywhere under the project root (not just at the root itself), so a Cargo/npm/monorepo workspace with several member packages is aggregated into one bundle instead of only recognizing the root. `okf-analyzer` emits one `Package` concept per discovered manifest with a name (a workspace-root "virtual manifest" with no `[package]`/name of its own — the common Cargo-workspace shape — correctly produces none), and links each `Module` concept to its owning package via a `MemberOf` relationship, resolved by longest-prefix directory match so a nested member package wins over an ancestor workspace-root package that's also, itself, named. `okf-generator` renders a `Package`'s `# Contains` section from the reverse of that relationship (a `Module`'s own `# Member of` section was already free, via the existing generic relationship rendering), and `okf-graph` gained `owning_package()` alongside `owning_module()`. Single-package projects are entirely unaffected — same ids, same output
- [x] MCP server (`okf-mcp`) exposing symbol, call-graph, and API-surface queries — a stdio JSON-RPC 2.0 server implementing MCP's `initialize`/`tools/list`/`tools/call`, wrapping `okf-search` and `okf-graph` (via `okf_parser::read_bundle`) as the `search`, `graph_callers`, `graph_callees`, `graph_api`, `graph_cycles`, `graph_modules`, and `graph_path` tools
- [x] Basic documentation generation (Markdown, HTML) templated directly from the bundle — no LLM required. `okf-docs` renders two genuinely different shapes straight from `Concept` data, with no markdown-parsing or HTML-templating dependency: a browsable static HTML site (one page per concept plus a root and a per-kind index, cross-linked with relative links, HTML-escaped signatures/descriptions) and a single consolidated Markdown document (a table of contents followed by one `##`/`###` section per kind/concept). Exposed as `okf-rs docs [bundle] [--project] [--output] [--format html|markdown]` (default `html` into `docs/`, `markdown` into `docs.md`), reading a previously generated bundle the same way `search`/`validate`/`graph` do
- [x] Continuous indexing in local development (`okf-watch`) — `okf-rs watch` regenerates the bundle once immediately, then again each time a burst of filesystem activity (recursive, via `notify`) settles for a debounce period (`--debounce-ms`, default 300ms), reusing the exact same `.okf-cache.json` incremental cache `generate` does. Reports a regenerate only when something actually changed (a file was reparsed, or the concept id set differs from the last reported run) — a wakeup from unrelated churn under an ignored path (e.g. `target/` mid-`cargo build`) still costs a cheap `.gitignore`-aware re-scan and a harmless, byte-identical bundle rewrite, but isn't reported, so watch mode doesn't spam the terminal

Verified by dogfooding: `okf-rs graph api .` on this repository lists 71 public concepts, `okf-rs graph cycles .` correctly finds none, `okf-rs graph modules .` shows real cross-crate dependency edges, and `okf-rs diff <commit> <commit> .` against this repo's own history correctly reports added/changed functions using non-destructive worktrees (verified the working tree and branch were untouched afterward, including on an invalid-ref error path).

Relationships (`Calls`/`CalledBy`/`Imports`/...) are now serialized into each concept's `relationships` frontmatter field (target ids grouped by kind), alongside the existing human-readable "# Calls" / "# Called by" markdown body sections. `okf_parser::read_bundle` reverses `okf-generator`'s writer to reconstruct the full relationship-rich concept model from a bundle on disk, so `okf-rs graph` now queries a previously generated bundle directly — the same way `okf-rs search` and `okf-rs validate` do — instead of re-analyzing the project from source; run `okf-rs generate` first (and after any source change, to keep the bundle current).

`okf-mcp` reuses that same bundle-reading path: each tool call re-reads the bundle fresh via `okf_parser::read_bundle`/`okf_search::SearchIndex::build`, so a running server always reflects the latest `generate` without needing a restart. Verified end-to-end with a hand-fed `initialize` → `notifications/initialized` → `tools/list` → `tools/call` sequence over stdio against this repo's own bundle, and with unit tests covering the JSON-RPC dispatch (unknown methods, notifications never getting a response) and each tool (missing bundle, unknown concept id, missing arguments).

Verified by dogfooding: on this repository (22 source files), a cold `okf-rs generate .` reports "22 files parsed, 0 reused from cache"; re-running it immediately after with no source changes reports "0 files parsed, 22 reused from cache"; and the resulting bundle is byte-for-byte identical across the cold run, the warm (cached) run, and a `--no-cache` run (`diff -rq` on all three trees reports no differences) — confirming the cache changes performance, never output.

Java support verified end-to-end against a small standalone sample (a `package`-scoped class with a public/private method pair): `okf-rs scan` recognizes `.java` files, `okf-rs generate` extracts the class and both methods with a resolved `Calls` edge between them, `okf-rs validate` reports the bundle clean, `okf-rs search` finds the method by name, and `okf-rs graph callers` correctly reports the caller from the bundle's serialized relationships. C#, PHP, Kotlin, C++, and Swift support all verified the same way against an equivalent small sample in each language. While dogfooding the C++ sample, found and fixed a pre-existing, language-independent `okf-validator` false positive (below): a `Module` concept spanning its own sole top-level member (common for a minimal single-class file) was wrongly flagged as a duplicate resource.

`okf-rs watch` verified against a scratch project: the startup (baseline) run reports immediately; appending a function to a watched file reports a regenerate with the new concept count and `reparsed > 0`; a bare `touch` of the same file (mtime bumped, content byte-identical) triggers a wakeup but is correctly silent, since the content hash is unchanged. `okf-watch`'s own test suite drives the real filesystem watcher (not a mock) end-to-end, additionally covering that deleting a file is reported even though nothing needs reparsing (caught via the concept-id-set check, not the reparse count) and that a burst of rapid edits coalesces into a single regenerate.

Multi-package aggregation verified by dogfooding on this repository itself — a real Cargo workspace with 11 member crates and a virtual (no `[package]`) workspace-root manifest: `okf-rs scan .` reports "12 packages detected" (the 11 members plus the virtual root), `okf-rs generate .` emits exactly 11 `Package` concepts (correctly skipping the nameless virtual root), each crate's `Package` page's `# Contains` section correctly lists only that crate's own modules (verified for `okf-core`, which has two: `src` and `src/config`), and `okf-rs graph api` still reports zero `Package` concepts in the public API surface (unaffected by there now being 11 of them instead of 1). Also verified with a synthetic two-member workspace that a module's `MemberOf` relationship points at the correct sibling package, never the other one.

Documentation generation verified by dogfooding on this repository's own bundle (394 concepts): `okf-rs docs --output docs` produced 394 concept pages plus 5 index pages; a link-integrity sweep of all 2,944 internal `href`s in the generated site confirmed every one resolves to a file that actually exists; signatures containing real Rust generics from this codebase (e.g. `Vec<Concept>`, `Result<Vec<Concept>>`) render correctly HTML-escaped (`Vec&lt;Concept&gt;`) with no unescaped angle brackets or markdown syntax leaking into the HTML; and a `Package` concept's page (e.g. `okf-docs` itself) correctly lists its member module under `# Contains`. `okf-rs docs --format markdown` produced a single 4,233-line `docs.md` with a working table of contents and per-concept `**Calls:**`/`**Called by:**` relationship lines.

LSP disambiguation verified two ways. First, end-to-end against real `rust-analyzer` and `pyright-langserver` processes (not mocks): a synthetic two-module Rust project with two `fn run()` definitions and a bare `run()` call whose enclosing module only ever sees one of them — genuinely ambiguous by name alone, genuinely unambiguous by real Rust scoping — has `okf-analyzer`'s `analyze_with_cache_lsp(..., use_lsp: true)` correctly resolve the call to the one `run` actually in scope, both with `rust-analyzer` and with the equivalent Python fixture against `pyright-langserver`. Second, by dogfooding on this repository itself: after this session's own tree-sitter dedup work left 11 identically-named `extract(source, path) -> Result<FileExtraction>` functions (one per language module) all dispatched from one dynamic-by-name-only-looking `match` arm in `okf_tree_sitter::extract_source`, a plain `okf-rs generate .` correctly leaves every one of those 11 dispatch calls (and every language module's own test functions that call their local `extract`) unresolved ("doesn't call anything"/no `Calls` edge) — exactly the documented, name-only-ambiguous baseline — while `okf-rs generate --lsp .` (~15s against this 11-crate workspace, mostly `rust-analyzer` indexing time) correctly resolves every single one to its actual language-specific `extract` function, confirmed via `okf-rs graph callees`/`graph callers` in both directions, with `okf-rs validate` and `okf-rs graph cycles` both still reporting clean afterward.

Hardened following an internal code review of the LSP integration: `okf-lsp`'s response reader now runs on a background thread feeding an `mpsc` channel, so waiting for a reply times out after 30s instead of blocking forever on a server that never answers; `which()` locates a language server on Windows via `PATHEXT`-derived suffixes (`.EXE`/`.CMD`/`.BAT`/`.COM`), not just the bare command name; `file://` URIs are percent-encoded/decoded so a project path containing spaces or non-ASCII characters still round-trips correctly against a real server's own returned URIs; `okf-analyzer`'s per-language retry budget (needed while a freshly started server is still indexing) is now paid at most once per language — tracked via a `warmed_up` flag — instead of on every ambiguous call, so a call the server genuinely can't resolve costs one quick attempt rather than repeating the full ~10s retry storm; and Kotlin/Swift's `call_site` is now computed from the callee identifier itself rather than the whole call/receiver expression, so a member call like `this.foo()` points the LSP query at `foo`, not `this`.

**Known limitations:**
- LSP server support (`okf_lsp::server_command`) is wired up and verified end-to-end for **Rust** (`rust-analyzer`) and **Python** (`pyright-langserver`) only; **Go** (`gopls`) and **TypeScript**/**JavaScript** (`typescript-language-server`) have config entries using the same generic client, but weren't exercised against a live server in this environment — `okf_lsp::is_available` correctly reports `false` and disambiguation is silently skipped for them (and every other language) until a server config is added and verified. `--lsp` is `okf-rs generate`-only for now; `okf-rs watch` intentionally doesn't offer it, since respawning a language server and waiting for it to re-index on every debounced regenerate would make continuous watching impractically slow rather than "an optional bit slower."
- **`--lsp` output is not guaranteed byte-identical across environments, and `validate --ci` cannot yet tell that apart from a real regression.** The determinism guarantee verified above (Phase 1 dogfooding: identical output across repeated runs) holds for the default, tree-sitter-only path, whose only inputs are the source text itself. `--lsp` resolution is a different contract: it asks a real language server which definition an ambiguous call resolves to, and that answer can depend on the server's index state — what's been built, cached, or warmed up in the environment it's running in, not just the source tree. Concretely (raised by external review, August 2026): a bundle generated with `--lsp` on a developer's machine, then regenerated with `generate --no-cache && validate --ci` on a cold CI runner, can disagree on some `Calls` edges with **zero source change**, and today `validate --ci` reports that as a hard failure indistinguishable from an actual regression. Edge provenance has since shipped (every relationship now records `resolved_by`/`confidence` — see "Record edge provenance" in the [AI-native platform maturity improvement plan](#improvement-plan--ai-native-platform-maturity-community-feedback)), so the raw data needed to tell "the edge set changed" apart from "the same edges, a different resolver" now exists in the bundle — but `validate --ci`/`diff` don't make that distinction yet; they still report any content difference, `resolved_by` alone included, as one undifferentiated mismatch. Until that's wired up, the practical mitigation stays procedural: generate the bundle committed to the repo (and the one `--ci` validates) with the *same* `--lsp`/no-`--lsp` flags, in the *same kind* of environment, every time — never compare an `--lsp`-generated bundle against a `--no-cache`-regenerated one from a different machine or a colder runner. `okf-rs generate --check-determinism` (see the improvement plan's "Strengthen deterministic-build guarantees" item, also shipped) is the tool to actually check this within one environment — two independent `--lsp` runs on the same machine should still agree; it doesn't by itself solve the cross-environment case above, which needs the same procedural discipline either way.
- `okf-docs`'s HTML output has no client-side search or syntax highlighting — it's plain cross-linked HTML, not a generated-site-generator-style experience; finding something still means starting from an index page or an external `grep`/`okf-rs search`.
- Public/private (`is_public`) detection is tailored per language rather than one heuristic reused everywhere, but is still a simplification in places: Rust/Java/C#/Swift require an explicit `pub`/`public`/`open`; Go uses real capitalization; PHP/Kotlin default to public unless explicitly restricted; C++ is section-based with `class`/`struct` defaults; Python/JavaScript/TypeScript fall back to a leading-underscore naming-convention heuristic, since those languages have no real access-modifier concept to key on, pending real `export`/access-modifier tracking. C#'s `internal`/`protected internal`/`private protected` are all folded into "private" for now, since `is_public` is a boolean.
- Every language's per-file `Module` concept follows the same convention (one module per source file), not any language's actual package/namespace/module-spans-multiple-files model; a `package`/`namespace` declaration doesn't yet merge same-package/namespace files into one module concept. This matters most for Java, C#, PHP, Kotlin, and Swift, where that's the common real-world organization.
- C++'s out-of-class method definitions (`ClassName::method() { ... }`, the common header/source split) default to private, since the access-specifier section that actually governs them lives in the class body — often a different file entirely — that this extractor isn't looking at in the same pass.
- Kotlin's and C++'s primary/secondary constructors aren't extracted as `Method` concepts (Kotlin's have no `name` field to key one on in that grammar; scope was kept tight given four languages landed in one pass) — regular methods are unaffected in both.
- The incremental cache speeds up `okf-rs generate` only; `okf-rs diff` still re-analyzes both git-worktree checkouts from scratch (a shared, path-relocatable cache across worktrees is possible future work, since the cache key is a repo-relative path + content hash, not an absolute path).
- `okf-rs watch` watches the project root recursively at the OS level (via `notify`), not `.gitignore`-filtered at the watch itself — only the subsequent re-scan is `.gitignore`-aware. A very large, actively churning ignored directory (e.g. a `target/` mid-build) still triggers a debounce cycle and a cheap re-scan per burst, even though nothing gets reported; skipping the watch subscription for ignored paths entirely is possible future work.
- Multi-package discovery finds every manifest file in the tree rather than resolving a workspace's declared member list (e.g. Cargo's `[workspace] members = [...]` globs, npm's `"workspaces"` field) — in practice these produce the same result (every real member has its own manifest file, which is what's actually discovered), but a workspace member declared without ever having been given its own manifest wouldn't be found this way. A manifest under a vendored/generated directory (`node_modules`, `vendor`, `target`, `dist`, `build`) is excluded by a small hardcoded list as a backstop, on top of the `.gitignore`-based exclusion the scan already does for everything.
- A `Module`'s owning package is resolved purely by directory-prefix containment against each `Package`'s manifest location, not by actually parsing which source paths a package's manifest declares as its own (e.g. Cargo's `include`/`exclude`, npm's `"files"`) — accurate for the overwhelmingly common "package = its own directory tree" shape, but not for a manifest that deliberately claims files outside (or excludes files inside) its own directory.
- Tree-sitter extraction is `#[cfg(...)]`-blind: it parses raw source text without evaluating conditional-compilation attributes, so two differently-cfg-gated definitions with the *same name* in the same file (e.g. `#[cfg(windows)] fn f() {}` next to `#[cfg(not(windows))] fn f() {}`) are both extracted as concepts sharing one id, which `okf-generator` correctly refuses to write as a silent duplicate-overwrite. Found while dogfooding this repo's own `--lsp` work; the fix is to keep one definition per name per file (a single function with an internal runtime branch, e.g. `cfg!(windows)`, works fine) rather than parallel `#[cfg]`-gated siblings.

## Phase 3 — Search, Interop & Intelligence

Ordered deterministic/offline work first (no new runtime dependency, no external service, nothing optional), then interop, then the genuinely optional AI-dependent items last — each later item builds on a stable knowledge graph the earlier ones establish, and none of them are required for the ones before to be useful.

- [x] Full-text search engine: ranked, relevance-scored search (via [Tantivy](https://github.com/quickwit-oss/tantivy)) layered onto today's exact/substring `okf-search`
- [x] Stable, versioned public library API: `okf-graph`/`okf-query` promoted from an internal implementation detail of `okf-cli`/`okf-mcp` to a documented, semver'd crate other Rust tools — including `okf-server` in Phase 4 — can embed directly
- [x] DITA ⇄ OKF converter: export an OKF bundle to DITA (as before), plus *import* existing DITA XML as a source
- [x] Optional AI enrichment: function summaries, module descriptions — pluggable via any OpenAI-compatible endpoint ([Ollama](https://ollama.com)/LM Studio/LocalAI, [Crustly](https://github.com/jyjeanne/crustly), or a cloud provider), never a hard dependency on one vendor. Architecture explanations, usage examples, and glossary entries remain future work on the same infrastructure — see the note below
- [x] Architecture extraction: architectural layers, domain boundaries, design patterns
- [x] REST endpoint, database model, and event-flow detection
- [x] PDF export
- [ ] Validator/graph quality tooling — extends `okf-validator`/`okf-graph` rather than opening new crates, since the parser and internal concept graph already exist:
  - [x] Validate relationship targets, not just markdown body links: a `Calls`/`CalledBy`/`Imports`/`MemberOf` frontmatter target that doesn't resolve to a real concept is now a validator error (external targets, prefixed `external/`, are exempt), instead of disappearing silently in `okf-graph` (`Graph::get` returned `None`, filtered out)
  - [x] Isolated-concept detection in the relationship graph itself (zero `Calls`/`CalledBy` edges in or out), via `Graph::isolated_concepts()` — distinct from the existing `index.md`-reachability orphan check, which only catches concepts unreachable via markdown links. Exposed as `okf-rs graph isolated` and the `graph_isolated` MCP tool
  - [x] Redundant link detection: a file linking to the same resolved target more than once is now a validator warning
  - [x] Required-document presence check: a missing root `index.md` is now its own explicit validator error, instead of manifesting only as a wave of "orphaned" warnings on every other concept
  - [x] Inconsistent-relationship detection: bundle-wide `Calls`/`CalledBy` symmetry (the only relationship pair `okf-analyzer` ever emits as two sides of one edge — every other kind has no reverse kind to be asymmetric against) is now a validator warning, via a two-pass cross-file `id -> outgoing`/`id -> incoming` view rather than the existing per-file checks
  - [x] Knowledge-base coverage metric: `okf-rs coverage` reports the proportion of concepts with a description, with at least one tag, and participating in the call graph (excludes `Module`/`Package`, matching `isolated_concepts()`'s own exclusion) — computed entirely from the bundle already on disk, no re-scan of the source project
  - [x] `okf-rs graph stats`: node count, per-kind breakdown, relationship edge counts per kind (not a single total, to sidestep whether a resolved `Calls`/`CalledBy` pair is one edge or two), and connected components of the `Calls`/`CalledBy` graph via `Graph::connected_components()` — deliberately omits a "depth" metric, since the call graph isn't guaranteed acyclic (`cycles()` already finds real ones) and no single definition of "depth" survives that
  - [x] AI-suggested missing links between semantically close concepts, building on the optional AI enrichment above

Verified by dogfooding: an initial `okf-rs generate .` (474 concepts) followed by `okf-rs validate` on this repository reported 0 errors and 584 redundant-link warnings — genuine repeated bullets in generated `# Calls` sections, from a symbol being called more than once within one function body, since `okf-generator` emitted one relationship per call site rather than deduplicated per target. Fixed at the source (see below): `okf-rs generate .` (476 concepts, after adding the two new relationship-target/redundant-link tests themselves) followed by `okf-rs validate` now reports "no issues found" — 0 errors, 0 warnings. `okf-rs graph isolated .` correctly lists real never-called/never-calling concepts (e.g. top-level `Struct`/`Enum` types, which never appear as `Calls`/`CalledBy` targets).

Verified by dogfooding again after the symmetry/coverage/stats additions: on this repository's own 527-concept bundle, `okf-rs validate` still reports "no issues found" (the new symmetry check found no asymmetric `Calls`/`CalledBy` pairs, as expected on a purely `okf-rs generate`d bundle); `okf-rs coverage` reports 74% (356/477) of non-`Module`/`Package` concepts participate in the call graph; `okf-rs graph stats` reports 663 `Calls` edges, 663 `Called by` edges (symmetric, as expected), and 3 connected components — one large 352-concept cluster plus two small 2-concept ones, alongside 121 isolated concepts.

`okf-generator` now deduplicates relationship targets by `(kind, target)` before rendering — both in the `relationships` frontmatter field and in the human-readable `# Calls`/`# Called by`/... body sections — via a shared `unique_by_kind_and_target` helper, so a concept calling the same target from multiple call sites in its body is listed once, not once per call site. This is the fix the redundant-link warning above pointed at, rather than a separate roadmap item: the validator's new check was correctly surfacing real duplication already present in generated bundles, and the actual bug was in the generator, not something for hand-edited bundles to work around.

`okf-search::FullTextIndex` adds ranked, relevance-scored search on top of the exact/substring `SearchIndex` above, via an in-memory Tantivy index rebuilt fresh from `okf_parser::read_bundle` on every call (no persistent index file to keep in sync with the bundle, matching how `okf-graph`/`okf-query` already read a bundle back off disk). It searches title, type, description, signature, and tags — not just title/type/tags like the exact index — and a lightweight identifier splitter (`split_identifier_words`) inserts word boundaries at `snake_case`/`camelCase`/`PascalCase` transitions so a query for `verifyToken` matches a `snake_case`-named concept and vice versa; Tantivy's own tokenizer already splits on `_`/`-` for free. Exposed as `okf-rs search <query> --ranked` and the `search_ranked` MCP tool, alongside (not replacing) the existing exact/substring `search`/`search` tool, so both stay independently available and independently tested. Wired through `okf-query::search_ranked`, the same shared query layer `okf-cli` and `okf-mcp` already share for every other operation.

Verified by dogfooding: on this repository's own 555-concept bundle, `okf-rs search "ranks results by relevance" --ranked` correctly surfaces `ranks_exact_title_match_first` and other concepts whose *doc comment* (captured as each concept's `description`) contains that phrase, none of which the exact/substring `search` finds at all ("No matches for `ranks results by relevance`"), since it never looks past title/type/tags. `okf-rs search buildBundle --ranked` correctly ranks `SearchIndex::build`, `FullTextIndex::build`, and `Graph::build` above unrelated concepts, confirming the `camelCase`-to-`snake_case` boundary matching works across real crate boundaries, not just the unit tests' synthetic fixtures. `okf-rs validate` still reports "no issues found" afterward — this is a read-only query feature, and doesn't touch bundle generation.

`okf-graph` and `okf-query` are now formally the "public library API" the spec names as what Phase 4's `okf-server` embeds directly instead of shelling out to the `okf-rs` binary. Concretely: both crates gained `#![deny(missing_docs)]` (a doc-coverage gate enforced at compile time, not just a convention — the two small gaps this surfaced, `Graph` itself and `Graph::build`/`get`, are now documented) and a crate-level `# Stability` section spelling out that they're versioned with the rest of the single-version workspace, so a breaking change to either is a breaking change for the whole release, never a silent one. Each crate's doc comment also gained a runnable `# Example` (a `cargo test --doc` doctest, not just prose) showing a downstream consumer embedding it directly: `okf-graph`'s builds a `Graph` from hand-built `Concept`s and queries `callees`/`callers`/`cycles`; `okf-query`'s builds a tiny bundle on disk and calls the crate's own new typed API.

That new typed API is the other half of this item: `okf-query`'s `search`/`graph_callers`/... were already just thin wrappers over `okf_graph::Graph`/`okf_search::SearchIndex`, which already return borrowed, structured data (`Vec<&Concept>`, `Vec<SearchHit>`) — genuinely embeddable as-is, nothing to promote there. The two operations that were *not* already available as structured data anywhere — `coverage` and `graph_stats`, which compute real aggregates rather than relaying a `Graph` query — gained `coverage_report`/`graph_stats_report`, returning a `CoverageReport`/`GraphStatsReport` struct (`#[derive(Serialize)]`, so JSON-embeddable for a future `okf-server` REST/GraphQL response) instead of the pre-formatted text `coverage`/`graph_stats` still return for `okf-cli`/`okf-mcp`. Both text functions are now thin `.to_string()` wrappers over the typed ones — same underlying computation, one code path, not two to keep in sync — verified by every pre-existing `coverage`/`graph_stats` text-output test still passing unchanged (byte-for-byte identical output), plus new tests asserting directly on the typed structs' fields and round-tripping them through `serde_json::to_value`.

The DITA ⇄ OKF converter (export + import) was deliberately skipped over for now — deferred, not abandoned, at explicit request — so Phase 3 work continued with the next item below rather than blocking on it.

New crate `okf-enrich` adds optional AI enrichment: `okf-rs generate --enrich` fills in a missing `description` for `Function`/`Method`/`Module`/`Package` concepts via any OpenAI-compatible `chat/completions` endpoint — `--enrich-base-url`/`--enrich-model`/`--enrich-api-key`, or the matching `OKF_ENRICH_*` environment variables, so it works unmodified against Ollama, LM Studio, LocalAI, Crustly, or a cloud provider without a hard dependency on any one of them (`okf-enrich`'s only network dependency is `ureq`, a small blocking HTTP client — no async runtime pulled into the rest of the otherwise-synchronous workspace). A description a concept already has — human-written, or from an earlier `--enrich` run — is never re-queried or overwritten: `enrich_missing_descriptions` takes the freshly re-analyzed concepts *and* the bundle previously written to the same output path (read via `okf_parser::read_bundle` before `write_bundle` overwrites it), and only calls the endpoint for a concept whose id has no non-empty description in either. This matters because `generate` re-derives every concept from source on every run — nothing about `description` survives that on its own (tree-sitter extraction never sets it; it's `None` from every language extractor today) — so without this reuse step, every single `--enrich` run would re-query the endpoint for the entire bundle, not just what's new. The description is written into the same existing `description` frontmatter field every other bundle-consuming tool already reads — zero OKF schema changes.

Verified two ways, neither requiring a real LLM (none is reachable from this environment): `okf-enrich`'s own unit tests drive a hand-rolled OpenAI-compatible mock HTTP server (a background-thread `TcpListener` speaking just enough HTTP/1.1 to satisfy `ureq`) covering a successful completion, an unreachable endpoint, and every branch of the reuse logic (generated vs. reused vs. skipped-as-already-described vs. skipped-as-wrong-kind) in isolation. End-to-end, a new `okf-cli` e2e test points `okf-rs generate --enrich` at that same kind of mock server against a two-concept synthetic project (one `Module`, one `Function`): the first run reports "Enriched 2 concepts (2 generated, 0 reused)" and the mock server sees exactly 2 requests, with `description: a generated description` landing in the written bundle's frontmatter; a second run against the same output reports "(0 generated, 2 reused)" with the mock server's request count still at 2 — confirming the reuse path actually skips the network call, not just that it returns the same text — and `okf-rs validate` reports the resulting bundle clean. `--enrich` without `--enrich-base-url`/`--enrich-model` (or their env vars) fails fast with a clear error naming both, rather than attempting a request against an unconfigured client.

**Known limitations, scoped deliberately to keep this shippable in one pass:** only `Function`/`Method`/`Module`/`Package` are enriched — types (`Struct`/`Enum`/`Class`/`Trait`/`Interface`) aren't yet, though `is_enrichable` is a one-line change away from adding them. The roadmap's other three AI-enrichment facets — architecture explanations, usage examples, and glossary entries — aren't implemented: each needs a place in the OKF bundle format to live (a new concept kind, a new bundle-root artifact, or a new body section) that `description`'s already-existing frontmatter field didn't need, which is a real design question left for a follow-up rather than a quick addition to this pass.

New crate `okf-arch` adds deterministic architecture extraction — no AI dependency, layered onto `okf-graph` the same way the earlier Phase 3 items layered onto `okf-search`:

- **Layers** (`okf_arch::layers`, `okf-rs graph layers`): each package's depth in the package-level dependency graph — aggregated from `Graph::module_dependencies()` via each module's `Graph::owning_package()` (a new small, documented addition to `okf-graph`'s public API: `Graph::of_kind()`, enumerating every concept of a kind, needed to list every `Package` regardless of whether it appears in any edge). Layer 0 is a package with no dependency on any other package in the bundle; a package's layer is one more than the deepest package it depends on. Packages in a dependency cycle collapse into the same layer via a self-contained Tarjan SCC implementation (a twin of `okf-graph`'s own private one, over a plain package-id adjacency map instead of `Calls`/`CalledBy` edges) — there's no well-defined "shorter/longer path" between two packages that depend on each other.
- **Domains** (`okf_arch::domains`, `okf-rs graph domains`): connected components of the *undirected* package dependency graph — which packages actually collaborate, directly or transitively. Deliberately keeps a dependency-free package as its own singleton domain, unlike `Graph::connected_components`'s analogous singleton-dropping at concept granularity, since "this package is its own isolated domain" is a complete answer here, not noise to hide.
- **Design patterns** (`okf_arch::detect_patterns`, `okf-rs graph patterns`): structural/naming heuristics for four patterns — Builder (`*Builder`-named type with a `build` method), Singleton (a type with an `instance`/`get_instance`/`shared`/`singleton`-named method), Factory (a `create_*`/`make_*`-named function/method, or a method on a `*Factory`-named type — deliberately *not* `new_*`, since `new` is an idiomatic Rust constructor name and would otherwise flag nearly every Rust type in existence), and Visitor (a type with two or more `visit_*`-named methods). Every detector correlates a method to its owning type via `Concept::id` (stripped of its kind-directory prefix, e.g. `structs/`/`functions/`) rather than `Concept::qualified_name`, since that field's separator isn't stable across a fresh analysis (`.`) and a bundle read back off disk (`::`, `read_bundle`'s best-effort reconstruction) — the id, built once by `Concept::make_id`, always uses `/` regardless of which path a caller's concepts came from. Explicitly a structural signal to review, not a semantic guarantee a match really implements the pattern's intent — documented as such in the crate docs, the CLI help text, and the MCP tool description.

All three are exposed via `okf-rs graph {layers,domains,patterns}` and the `graph_layers`/`graph_domains`/`graph_patterns` MCP tools, wired through `okf-query` the same way as every other graph query — and, since `okf_arch::layers`/`domains`/`detect_patterns` already return typed, `Serialize`-derived structs (`PackageLayer`/`Domain`/`DetectedPattern`), there was no need to repeat the `coverage`/`graph_stats` `*_report()` pattern from the previous item: `okf-query`'s job here is purely CLI/MCP text formatting over data that's already structured.

Verified two ways. First, entirely synthetic fixtures free of real-world noise, covering the algorithm itself: a two-package `core`/`app` dependency (`app` at layer 1, `core` at 0, and the pair reported as one domain); a mutual two-package dependency cycle (both packages collapse to layer 0); an isolated single-package project (layer 0, its own singleton domain); two packages with no dependency between them (two separate singleton domains, not one); and, for patterns, direct hits and near-miss non-matches for all four detectors (a `*Builder` type *without* a `build` method isn't flagged; a single `visit_*` method isn't enough to flag Visitor; a `new`-named constructor isn't flagged as Factory).

Second, dogfooded against this repository's own 651-concept, 17-package bundle — with an important, honestly-documented finding: `okf-rs graph layers`/`domains` on this repo's real bundle reports nearly every package collapsed into one big layer-0 domain, which traced back not to a bug in `okf-arch` but to pre-existing noise already present in `Graph::module_dependencies()` itself: several crates' `#[cfg(test)] mod tests` define same-named helper functions (`write`, `concept`, ...), and the analyzer's project-wide, unambiguous-name call resolution occasionally resolves a bare call in one crate's test module to another crate's same-named test helper — producing a spurious cross-package edge (confirmed by tracing one such edge, `okf-analyzer -> okf-cli`, to test functions like `does_not_resolve_ambiguous_call_names` rather than any production code path). `--lsp` would very likely resolve this correctly (real scope-aware resolution, not name-only), but `rust-analyzer` isn't installed in this environment to confirm firsthand. `okf-rs graph patterns` on the same bundle wasn't affected by this at all (name/structure heuristics don't depend on the call graph) and correctly found exactly the three legitimate `create_*`/`make_*`-named Factory matches in this codebase (`make_concept` in `okf-analyzer`/`okf-tree-sitter`, `Concept::make_id`), none of which are false positives.

**Known limitation:** `layers`/`domains` inherit whatever noise already exists in `Graph::module_dependencies()` — including test-code call edges, which a project with same-named test helpers across multiple packages (like this one) can turn into spurious cross-package dependencies. Filtering test code out of the call graph would need a way to tell a test concept from a production one, which the schema doesn't track today; `--lsp`'s real scope-aware resolution is the most likely fix already available, once verified against an environment where `rust-analyzer` is actually installed.

`okf-arch` gained a third analysis, `detect_features` (`okf-rs graph features`, the `graph_features` MCP tool): REST endpoints, database models, and event-flow participants, via the same structural/naming-heuristic approach `detect_patterns` already established — a public method on a `*Controller`-named type (REST endpoint, ASP.NET MVC/Web API/Spring MVC/NestJS naming convention), a `*Model`/`*Entity`/`*Record`-named type (database model), and an `emit_*`/`publish_*`/`subscribe_*`/`on_*`/`*_handler`/`*_event`-named function or method (event-flow participant). The method-to-owning-type correlation logic (`path_after_kind_dir`/`owner_path`/`is_type_kind`/`group_methods_by_owner`) moved out of `patterns.rs` into a new shared `ownership` module so both detectors reuse it rather than duplicating it — a pure refactor, no behavior change, confirmed by every pre-existing `patterns` test still passing unchanged.

**Known limitation, and it's a real one, not a subtle one:** every detector here works from a concept's name/kind alone. Decorator/annotation/attribute source text — Spring's `@RestController`/`@GetMapping`, Flask's `@app.route(...)`, JPA's `@Entity`, ASP.NET's `[HttpGet]` — isn't captured anywhere in the `Concept` model today; extracting it would need a schema change (a new field) and per-language extractor work across all 11 supported languages, well beyond what this pass could responsibly take on alongside everything else already shipped in this session. What's here instead is deliberately the same kind of naming-convention signal `detect_patterns` already uses, not a framework-aware one: a codebase that doesn't name its REST controllers `*Controller`, its models `*Model`/`*Entity`/`*Record`, or its event functions with one of the naming conventions above — or that relies on decorators alone to mark a route or an entity — won't show up here at all. This is the honest, current shape of the feature; decorator-aware detection is real future work, not a rounding error.

Verified two ways. First, synthetic fixtures for each detector and its near-miss: a public method on a `*Controller` type is flagged, but the same method on a non-`*Controller`-named type isn't; each of the three model-suffix conventions (`*Model`/`*Entity`/`*Record`) is independently detected, and a same-named type without any suffix match isn't; `emit_*`/`on_*`/`*_handler`/`*_event`-named functions are all detected with the right per-convention evidence text, and an unrelated name isn't. Second, end-to-end against a small synthetic Rust project (`UserModel`, `UserController::get_user`, `emit_user_created`) via the real `okf-rs` binary: `okf-rs generate .` followed by `okf-rs graph features` correctly reports all three — `RestEndpoint functions/src/UserController/get_user`, `DatabaseModel classes/src/UserModel`, `EventFlow functions/src/emit_user_created` — each with its evidence line, and `okf-rs validate` reports the resulting bundle clean. Dogfooded against this repository's own bundle too: `okf-rs graph features` correctly reports nothing found, since `okf-rs` itself is a static-analysis CLI with no REST controllers, ORM models, or event-emitting code — the clean-negative case, not a false positive to worry about.

`okf-docs` gained `--format pdf` (`okf-rs docs --format pdf`, defaulting to `docs.pdf`): a single, paginated A4 PDF with one section per concept grouped by kind, mirroring `--format markdown`'s consolidated content but as one file meant to be printed, archived, or shared as-is — with a PDF outline/bookmark per concept (the PDF-native navigation panel every reader already shows) standing in for a printed table of contents with page numbers, which would need a second measurement pass over the whole document to get right.

Built directly on `printpdf`'s low-level page/layer/text primitives rather than a higher-level PDF layout crate, and deliberately on `printpdf 0.7` rather than the current `0.12`: the newer major version pulls in a full CSS-like layout engine (`azul-layout`, `taffy`, `rust-fontconfig`) for its high-level API, drastically heavier than a single-file, hand-templated export needs — the same "no heavy dependency for something hand-rollable" judgment call `okf-docs`'s existing HTML/Markdown renderers and `okf-enrich`'s `ureq`-over-`reqwest`+`tokio` choice already made. `printpdf 0.7` adds only 9 lightweight crates to the tree, no async runtime, and (checked directly, not assumed) still produces a `--release` (LTO) binary that builds successfully and grows by about 1&nbsp;MB. Uses `printpdf`'s built-in Helvetica/Helvetica-Bold/Courier fonts — part of the PDF spec's standard 14, present in every conformant reader — instead of embedding a font file, keeping both the dependency footprint and binary size down.

**Known limitation:** `printpdf`'s built-in fonts don't expose glyph-width metrics, so line-wrapping estimates each character's width as a fixed fraction of the font size rather than measuring real glyph widths — a deliberately simple heuristic (documented in the module docs, tuned to wrap a little early rather than overflow the page) appropriate for a readable, utilitarian export, not point-perfect typesetting.

Verified three ways. Unit tests cover the word-wrapping helper directly (wraps within the character budget, a single word longer than the limit is hard-split rather than looping or overflowing, empty text produces one empty line rather than zero) and `generate_pdf` itself (a well-formed `%PDF-` header on a single concept, and — the pagination path specifically — 200 concepts rendered without error, forcing many page breaks). End-to-end, `okf-rs docs --format pdf` was run against this repository's own 670-concept bundle: 496&nbsp;KB, 76 pages. Rather than trust the file size and header alone, the output was independently parsed with `lopdf` (a separate PDF library from the one that wrote it) and its text extracted from every page: 141,337 characters of real, readable content came back, including literal strings from this bundle (`"Documentation"`, `"Functions"`, `"read_bundle"`, `"Rust Function"`) — confirming the PDF isn't just structurally valid but actually contains the right content, not empty or corrupted pages. A new `okf-cli` e2e test (`docs --format pdf`) checks the same `%PDF-` header and a non-trivial file size through the real compiled binary, alongside the pre-existing HTML/Markdown format checks in the same test.

The last item in "Validator/graph quality tooling" — AI-suggested missing links — lands as `okf_enrich::suggest_missing_links` (`okf-rs suggest-links`), closing out that whole sub-list. It builds on the optional AI enrichment above exactly as the roadmap names it, rather than opening a new AI integration: candidate generation is free and deterministic (full-text search — `okf_search::FullTextIndex`, the same ranking `okf-rs search --ranked` uses, given a new `build_from_concepts` constructor so it can index concepts already in memory instead of re-reading the bundle off disk a second time), and only candidates that pass three cheap, local filters — not the concept itself, not a `Module`/`Package` on either side, and no existing relationship edge already connecting them in *either* direction — are sent to the configured endpoint for a single yes/no judgment. This keeps the network cost bounded and predictable: at most `--candidates` (default 3) endpoint calls per concept that has a description, never an O(n²) sweep of every possible pair, which would be both too slow and too expensive against a real LLM for any bundle bigger than a handful of concepts. Suggestions are advisory only — nothing is written back into the bundle, the same posture `okf-rs validate`'s warnings already have.

Verified two ways, again without a real LLM (none reachable from this environment): `okf-enrich`'s own tests, against the mock endpoint pattern already established for `enrich_missing_descriptions`, cover a confirmed suggestion, a `NO` response producing none, an already-linked pair never being sent to the endpoint at all (checked via the mock server's own request count, not just the output), `Module`/`Package` concepts never participating on either side, a description-less concept never being used as a search source, and — the trickiest case — a mutually-similar pair (`a_token_thing`/`b_token_thing`, each finding the other as a candidate) being judged exactly once, not twice, confirming the unordered-pair dedup actually works and isn't just coincidentally not double-counting. End-to-end, a small synthetic Rust project (`verify_token`, `decode_jwt`, `format_currency`, none calling each other) was run through the real binary against a mock endpoint that discriminates on prompt content rather than replying with one fixed string: `generate --enrich` first (populating descriptions), then `suggest-links` — which correctly reported `decode_jwt <-> verify_token: verify_token likely calls decode_jwt to validate the token's claims`, and did *not* suggest anything involving `format_currency`, the one function genuinely unrelated to the other two. A new `okf-cli` e2e test covers the same `generate --enrich` → `suggest-links` pipeline against a simpler mock (always confirms), asserting the endpoint was actually queried and the suggestion's reason text made it into the CLI's output.

**Known limitation:** like the design-pattern/REST-endpoint detectors, candidate quality is only as good as the full-text search behind it — a concept with a thin or generic description won't surface good candidates for the endpoint to judge in the first place, and this only ever considers concepts that already have a description (from `--enrich` or written by hand), so an entirely undocumented bundle has nothing for it to suggest links between until `--enrich` (or a human) fills in descriptions first.

The DITA ⇄ OKF converter, paused earlier in this phase, was picked back up and completed, closing out Phase 3 (15/15). New crate `okf-dita` exports a bundle's concepts as DITA topics (`okf-rs docs --format dita`, the "DITA (planned)" documentation-generation format the original spec named alongside Markdown/HTML/PDF) and imports an existing DITA corpus back into the bundle (`okf-rs generate --dita <path>`) as `Document` concepts — the roadmap's own framing of "bring an existing DITA corpus into the same bundle/CLI/MCP/graph queries every other source already works with," verified concretely below, not just asserted.

Getting a `Document` concept to genuinely participate meant two small, compiler-guided schema additions rather than bolting DITA on as a special case: `ConceptKind::Document` and `Language::Dita`, so an imported topic's frontmatter type reads `DITA Document` the same shape every code concept's `<Language> <Kind>` type already has. Both are closed enums matched exhaustively in a few places (`ConceptKind::as_str`/`bundle_dir`/`parse`, `Language::display_name`); the compiler pointed at every one of them, including a genuinely unreachable arm in `okf-tree-sitter`'s language-dispatch `match` (unreachable because `Language::from_extension` deliberately never maps `.dita` to `Language::Dita` — DITA import is a separate path from source-code extraction, never joining `okf_core::Project::load`'s file scan). `Document` joins `Module`/`Package` in `Graph::public_api`/`Graph::isolated_concepts`/`coverage`'s call-graph-eligible denominator — structural the same way, in the sense that a documentation topic was never going to carry a `Calls` edge or count as API surface, so treating that as a quality signal would be noise, not a finding.

Export uses the DITA spec's generic `<topic>` doctype (not a specialization like `<reference>`/`<concept>`, which carry their own child-ordering validity rules a generic topic doesn't need to satisfy) via the same hand-templated-string approach `okf-docs`'s HTML/Markdown/PDF renderers already use — no XML-writing dependency needed for output. Import uses [`roxmltree`](https://github.com/RazrFalcon/roxmltree) (zero dependencies of its own) — non-validating and read-only, matching what parsing a topic to pull out a title/shortdesc/id actually needs. A topic that isn't well-formed XML is skipped with a warning rather than failing the whole `generate --dita` run, since a real corpus can be large and one bad file shouldn't block the rest.

Verified three ways, and the third one caught a real bug. Unit tests cover both directions independently against hand-written fixtures (title/shortdesc/id extraction, a missing title falling back to the filename, a missing id falling back to a path-derived name, mixed inline markup like `<b>` inside a `<shortdesc>` collapsing to its plain text, a single input file working the same as a directory, one malformed file not blocking the rest of a batch import). Then a dedicated round-trip test — export a concept to DITA, immediately re-import that exact output — caught the real bug: `roxmltree` rejects any `<!DOCTYPE ...>` declaration by default, and `export_dita`'s own topic files (correctly) declare one (`<!DOCTYPE topic PUBLIC "-//OASIS//DTD DITA Topic//EN" "topic.dtd">`, standard for real DITA content), so the *exact output this crate itself produces* failed to re-import until `ParsingOptions::allow_dtd` was explicitly enabled — confirmed safe to enable since `roxmltree` never resolves an external DTD subset over the network or filesystem, it only tolerates the declaration's syntax. A hand-written fixture alone, doctype-free for convenience, would never have caught this — it takes both halves of a converter agreeing on the same real-world shape, not just each passing in isolation.

Finally, dogfooded end-to-end against this repository's own bundle: `okf-rs docs --format dita` on this repo's 715-concept bundle produced 715 topic files plus a `root.ditamap`; feeding that whole exported corpus into `okf-rs generate --dita <path>` against a fresh synthetic one-function project imported all 715 back as `Document` concepts with zero skips (after the doctype fix — before it, every single one was skipped, which is exactly what surfaced the bug above outside the unit tests too). The resulting 717-concept mixed code+docs bundle validated clean; `okf-rs coverage` correctly reported call-graph participation as 1/1 (excluding all 715 `Document`s and the one `Module`, not diluting the denominator); `okf-rs graph api`/`graph isolated` correctly listed only the one real function, never a documentation topic; and both `okf-rs search Domain` and `okf-rs search --ranked "package dependency graph"` correctly found and ranked `Document` concepts imported from this repo's own DITA export — proving imported documentation and extracted code genuinely share one search index, one graph, one bundle, not two parallel systems glued together at the CLI.

## Improvement Plan Delivery — Competitive Gap-Closing

[`docs/improvement-plan.md`](docs/improvement-plan.md) compares `okf-rs` against two adjacent codebase-knowledge-graph tools (CodeGraph, code-review-graph) and scores a set of candidate improvements GO / conditional-GO / no-go. All five items scored **GO**, plus both items scored **CONDITIONAL GO** (gated on determinism and on sequencing after change-impact analysis shipped first, respectively), are now delivered:

- [x] Parallel extraction (`okf-analyzer`): per-file read+tree-sitter-parse now runs across a `rayon` thread pool, merged back in deterministic file order — makes the "Parallel" design principle real rather than aspirational, with no change to output.
- [x] Composite `explore` query: `okf-rs explore <query>` / the `explore` MCP tool bundles a concept's signature, description, callers, callees, blast radius, public-API membership, and cycle membership into one response, instead of composing several separate `search`/`graph_*` calls.
- [x] Change-impact analysis: a new `Graph::transitive_callers` primitive (the "blast radius" building block) plus `okf_analyzer::impact()` — scoring every added/removed/changed concept between two git refs by transitive-caller count, public-API membership, and cycle membership — and `okf-rs impact <ref-a> <ref-b>`.
- [x] GraphML + Obsidian documentation export: `okf-rs docs --format graphml` (for Gephi/yEd/any GraphML tool) and `--format obsidian` (a vault of `[[wikilink]]`-cross-linked notes), alongside the existing HTML/Markdown/PDF/DITA formats.
- [x] Embedding-based semantic search: `EnrichClient::embed()` against any OpenAI-compatible `/embeddings` endpoint, cosine-ranked in `okf-enrich`, exposed as `okf-rs search --semantic` and the `search_semantic` MCP tool.
- [x] Modularity-based community detection (`okf-arch`): a deterministic greedy modularity-optimization algorithm (Clauset-Newman-Moore agglomerative merging) over weighted package-dependency edges, alongside the existing connected-components `domains()` — `okf-rs graph communities` / `graph_communities` MCP tool.
- [x] PR review automation: `okf-rs review <ref-a> <ref-b>` renders `impact()` as a Markdown, sticky-comment-ready report (`--fail-on-risk <N>` for optional CI gating), plus [`.github/workflows/pr-review.yml`](.github/workflows/pr-review.yml), a ready-to-use GitHub Action.

Verified by dogfooding: two `--no-cache` runs of the now-parallelized extraction produce byte-identical bundles on this repository's own ~850-concept source tree, confirming parallelism changed performance, not output. `okf-rs graph communities` demonstrates the exact gap the improvement plan named: on this repo's own 18-crate dependency graph, `graph domains` (plain connected components) collapses everything into one component, while `graph communities` splits it into three meaningful clusters — and a hand-built "two dense triangles joined by one thin bridge" test confirms the algorithm can split what connected components structurally cannot. `okf-rs impact`/`okf-rs review` were run against real commit ranges from this repository's own history (e.g. the Phase 3 architecture-extraction commits), correctly computing blast radius and rendering a well-formed Markdown report with a sticky-comment marker; a dedicated end-to-end test confirms `--fail-on-risk` gates correctly at, but not below, its configured threshold. `okf-rs search --semantic` and the `explore` composite query were both verified end-to-end via mock-endpoint e2e tests through the real compiled binary, not just unit tests.

A follow-up code review of this delivery (five independent finder passes, plus a numerical verification pass) surfaced 15 real findings — mostly duplicated worktree-checkout/edge-resolution logic and a couple of avoidable inefficiencies (`explore` building a full-text index even for exact-id lookups, redundant vector-norm recomputation) — and one incorrect finding worth recording as a negative result: a claimed modularity-formula bug in the community-detection merge loop, checked by directly computing modularity on 2000 random graphs rather than taken on faith. The shipped formula turned out to be a positive constant multiple of the mathematically exact value, which provably preserves both the merge-decision sign and the candidate ranking the algorithm actually uses it for (zero mismatches across every trial), while the reviewer's proposed "fix" would have introduced real mismatches in a meaningful fraction of cases — so the code was left as shipped. All 15 real findings are fixed; a separate, genuine bug a later review pass found — the PR review GitHub Action diffing against `github.sha`, which for `pull_request` events is GitHub's ephemeral test-merge commit rather than the PR's actual head commit — is also fixed, using `github.event.pull_request.head.sha` instead.

---

## Improvement Plan — AI-Native Platform Maturity (Community Feedback)

Sequenced ahead of Phase 4 — Ecosystem below: this plan makes the graph itself more trustworthy and cheaper for an agent to consume, which is higher-leverage right now than `okf-server`/multi-repo serving, and several Phase 4 items (notably the LSP server) are more valuable once determinism and provenance here are solid.

External review feedback (recorded verbatim in [`docs/feedback/2026-08-community-roadmap-review.md`](docs/feedback/2026-08-community-roadmap-review.md)) on the MCP tool surface, determinism guarantees, and provenance/confidence tracking, distilled into a status-tracked plan below. It doesn't replace [`docs/improvement-plan.md`](docs/improvement-plan.md) (the competitive gap analysis against CodeGraph/code-review-graph, already delivered above) — it's the next layer once that gap is closed.

**Vision:** `okf-rs` should be **deterministic, auditable, Git-native, AI-friendly, CI-ready, and language-independent** — not just another code graph generator, but a transparent, deterministic, Git-native knowledge graph platform for AI coding agents, developer tooling, and CI/CD.

### High priority

- [x] ✅ **Optimize the MCP API** — the 13 single-relation `graph_*` tools (`graph_callers`, `graph_callees`, `graph_api`, `graph_cycles`, `graph_modules`, `graph_isolated`, `graph_stats`, `graph_path`, `graph_layers`, `graph_domains`, `graph_communities`, `graph_patterns`, `graph_features`) are replaced by one consolidated `graph` tool taking a `relation` enum (`callers`/`callees`/`path`/`api`/`cycles`/`modules`/`isolated`/`stats`/`layers`/`domains`/`communities`/`patterns`/`features`) plus the id/from/to arguments the chosen relation needs — exactly the `explore(relation=...)` shape this item proposed, as a sibling to (not a replacement of) the existing per-concept `explore` composite tool, which already covers the "bundle several facts about one concept" case this doesn't. `search`/`search_ranked`/`search_semantic`/`coverage` are untouched — the proliferation this item named was specifically the `graph_*` family. **Breaking change to the MCP tool surface** (pre-1.0, `okf-mcp` version 0.3.0): the old tool names are gone from both `tools/list` and `tools/call`, not kept as hidden aliases, since the whole point is shrinking what's registered — an agent/config calling `graph_callers` directly now gets a clear `unknown tool` error rather than silently costing nothing extra.
- [x] 🚧 **Measure session-level MCP performance** — `okf-mcp --benchmark [project]` reports the fixed schema-registration cost (`tools/list`'s byte/token size, paid every session whether any tool is used or not), samples up to 5 `Function`/`Method` concepts that have at least one caller, and for each computes a "naive" grep-and-read cost (every project file, `.gitignore`-aware, containing an unqualified substring match on the name) against the real `graph` tool's response size — then reports the average tokens saved per structural query and the resulting break-even point (how many structural queries recoup the registration cost). All computed locally from the bundle and project source already on disk, no LLM call or tokenizer dependency (tokens estimated via the same ~4-chars-per-token rule of thumb the README's hand-picked example already uses). **Not done:** a cost comparison against RAG-based retrieval — that needs a real vector-store/chunking/re-ranking baseline to be a fair number rather than a fabricated one, so it's reported as an explicit, undone gap in the tool's own output rather than a made-up figure.
- [x] 🚧 **Strengthen deterministic-build guarantees** — `okf-rs generate --check-determinism` runs analysis twice, independently (always bypassing the incremental cache both times, regardless of `--no-cache` — a fresh `AnalysisCache::default()` each run, since determinism-checking should never depend on cache state), renders each to its own throwaway directory (cleaned up automatically, real `--output`/`.okf-cache.json` untouched), and diffs the two byte-for-byte, reporting every differing file and exiting non-zero if they disagree. Works with `--lsp` too — that's the actual point: the tree-sitter-only path is expected to always pass, while `--lsp` is exactly where the local-vs-cold-CI-runner divergence documented in [Phase 2's known limitations](#phase-2--depth--integration-) could show up, and this is the tool to actually catch it instead of discovering it the hard way in CI. Rejects `--enrich` up front with a clear error, rather than reporting a live endpoint's inherent non-determinism as a false positive unrelated to what this flag checks. **Still open** (the "still open" half of this item): a single, consolidated documented statement of the determinism guarantee — today it's this flag plus scattered per-phase "Known limitations" notes, not one canonical reference — and improved reproducibility of `--lsp` mode itself (this flag *detects* non-determinism, it doesn't make `--lsp` more reproducible).
- [x] ✅ **Record edge provenance** — every `relationships.<kind>` entry now carries `resolved_by`: `tree-sitter` for Tree-sitter's own unambiguous name match, or the resolving language server's binary name (`rust-analyzer`, `pyright-langserver`, ...) for an `--lsp`-disambiguated edge — e.g. `{target: functions/decode_jwt, resolved_by: tree-sitter, confidence: exact}` instead of a bare target id. Closes exactly the gap named: `okf-rs generate --lsp` no longer silently upgrades a call from unresolved to resolved with no record of which resolver decided it.
- [x] ✅ **Confidence levels** — delivered together with provenance above (same schema change, same `Relationship` fields): `confidence` is `exact` (Tree-sitter, syntax-derived) or `semantic` (LSP-resolved). The roadmap's other two named values, `inferred` (an AI-suggested edge — `okf_enrich::suggest_missing_links` stays advisory-only, never written back into a bundle) and `unresolved` (a call site with no resolvable target isn't represented as an edge at all today, so there's no `Relationship` for it to carry that confidence on) aren't modeled: adding an enum variant nothing produces would be exactly the kind of unpopulated field this project avoids elsewhere.

Verified by dogfooding, both the unit-test suite and the real binary. `cargo test -p okf-mcp` (19 tests, all passing) covers every relation dispatching to the right underlying `okf-query` function, an unknown relation producing a clear error naming every valid one, and the schema's `relation` enum listing all 13 values with `relation` as the only required property. End-to-end against this repository's own bundle (622 functions, 86 methods, 213 public concepts): a hand-fed `initialize` → `tools/list` → `tools/call` sequence over stdio against the real compiled `okf-mcp` binary confirms `tools/list` now reports 6 tools (`search`, `explore`, `search_semantic`, `search_ranked`, `coverage`, `graph`) instead of 18; `graph` with `relation: "callers", id: "functions/crates/okf-cli/src/cmd_generate"` correctly returns `functions/crates/okf-cli/src/run — Rust Function`, matching the README's own worked example; `relation: "bogus"` returns the clear enumerated error rather than a silent no-op; and calling the old `graph_callers` tool name directly now correctly returns `unknown tool` rather than silently working, confirming the removal is real, not just hidden from `tools/list`. Measuring the actual schema `tools/list` returns (not just the tool count) found a real, if modest, byte reduction alongside the 18→6 tool-count drop: 6,238 bytes (~1,560 tokens) before, 4,863 bytes (~1,215 tokens) after — the relation descriptions were folded into the one `graph` tool's own description rather than dropped, which is why the byte reduction (~22%) is smaller than the tool-count reduction (~67%); the point of this item was fewer *decision points* and less *registration* overhead, not literally the smallest possible schema.

`okf-mcp --benchmark`'s own test suite (7 new tests, in `benchmark.rs`) covers the naive-cost walker finding exactly the one file with a real call site and correctly skipping a `.gitignore`d one, concept sampling correctly excluding a never-called function and respecting the sample-size limit, an empty bundle reporting "no sampled queries" instead of dividing by zero, and the rendered report containing both the headline numbers and the RAG-comparison caveat. Dogfooded against this repository's own freshly generated bundle (622 functions, 86 methods): `okf-mcp . --benchmark` reports `tools/list` at 6 tools / 4,716 bytes (~1,179 tokens), and on its 5 sampled concepts (all from `okf-analyzer`, since sampling is deterministically ordered by concept id) an average saving of ~39,892 tokens per structural query — pushing break-even down to "~1 structural query," i.e. registering this server pays for itself almost immediately on this codebase. That average is inflated by construction on two of the five samples (`insert`/`save` are common enough method names that the naive project-wide substring match picks up 12 and 3 unrelated files respectively) — the report says so in its own output rather than presenting the number as clean, and a rarer symbol name (e.g. this repository's own `cmd_generate`, used as the README's hand-picked example) gives a much tighter, still-clearly-positive comparison. The RAG cost comparison the roadmap item also names is deliberately not computed — the report says exactly that, rather than fabricating a number without a real retrieval baseline to measure against.

`okf-rs generate --check-determinism` is verified two ways. Unit tests (`determinism_tests` in `okf-cli`) cover the directory-diffing logic directly: byte-identical trees report no diffs, differing content is reported with the right relative path, a file present on only one side is reported as such (distinctly for "run 1 only" vs. "run 2 only"), nested subdirectories are walked correctly, and the throwaway scratch directory is actually removed on drop. End-to-end, two new `okf-cli` e2e tests run the real compiled binary: `--check-determinism` on a small synthetic Rust project correctly reports "Deterministic" with the right concept count, and confirms it never writes to `knowledge/` at all (this is a read-only diagnostic, not a generate variant); `--check-determinism --enrich` together fail immediately with a clear error naming both flags, before any analysis or network call. Dogfooded against this repository's own source tree via the real binary: `okf-rs generate . --check-determinism` reports "Deterministic... (885 concepts)" — confirming the tree-sitter-only path holds the guarantee it claims, on the same codebase (and now the same code path) every other Phase 1–3 dogfooding pass already exercised.

Edge provenance and confidence levels are one schema change (`Relationship` gained `resolved_by`/`confidence` fields, rendered as `{target, resolved_by, confidence}` instead of a bare target-id string), threaded through every crate that touches a relationship: `okf-analyzer`'s call-resolution loop now carries a `ResolvedEdge` (not a bare `(caller, callee)` pair) from the point a call is resolved — either by Tree-sitter's unambiguous match, or by `okf-lsp`'s real language server — all the way to the `Relationship`s pushed onto both concepts, so an LSP-resolved edge's real resolver name and `Confidence::Semantic` survive intact instead of being flattened partway through; `okf-generator` renders the richer shape; `okf_parser::read_bundle` and `okf-validator` (which re-parses frontmatter YAML independently of `read_bundle`, so both needed the same fix) accept *either* shape — a bare string or the new mapping — so every bundle ever generated stays valid without regenerating. `okf-validator` also gained a new check, `check_relationship_provenance`: a populated `confidence` that isn't `exact`/`semantic`, or an explicitly empty `resolved_by`, is now a validator error.

Verified two ways. Unit tests across every touched crate (`okf-parser`: `Confidence::as_str`/`parse`/`Display` round-trip, `Relationship::new`'s defaults, three new `read_bundle` tests for the object-shaped entry, a missing/invalid-provenance fallback, and a no-target entry being skipped; `okf-generator`: the round-trip test now asserts a real non-default provenance — `rust-analyzer`/`Confidence::Semantic`, as `--lsp` would produce — survives write-then-read-back, not just the tree-sitter defaults every other fixture happens to use; `okf-analyzer`: `resolves_unambiguous_calls_and_detects_package` asserts `resolved_by: tree-sitter`/`Confidence::Exact` on a real resolved edge, and — genuinely exercised, not skipped, since `rust-analyzer` is installed in this environment — `resolves_an_ambiguous_call_via_a_real_rust_analyzer_when_scoping_disambiguates_it` asserts `resolved_by: rust-analyzer`/`Confidence::Semantic` on an edge only the real language server could resolve; `okf-validator`: four new tests covering a valid object-shaped entry, an unrecognized `confidence` value, an empty `resolved_by`, and a bare-string entry staying valid with nothing to flag). Dogfooded end-to-end against this repository's own bundle (906 concepts): `okf-rs generate .` renders the new shape (e.g. `resolved_by: tree-sitter` / `confidence: exact` on a real edge, confirmed by inspecting the written file directly), `okf-rs validate` reports "no issues found," and `okf-rs generate . --check-determinism` still reports "Deterministic" — confirming the schema change didn't introduce non-determinism of its own. `okf-mcp`'s `graph` tool (relation `callers`) was re-checked against the new bundle shape and returns the same answer as before the change, unaffected, since query rendering only ever read `target`/`target_display`. The full workspace (`cargo test --workspace`, `cargo clippy --workspace --all-targets -D warnings`, `cargo fmt --check --all`) stayed clean throughout — including every doctest, one of which (`okf-graph`'s own `# Example`) needed updating for the new required `Relationship` fields, caught by the compiler exactly the way the DITA work's own schema changes were.

### Medium priority

- [x] ✅ **Better highlight Git-native Markdown as a differentiator** — README's "Why okf-rs?" section (the first thing a new visitor reads) now leads with this instead of listing it as one bullet among several: a concrete before/after `# Calls` diff snippet showing what a wrong edge actually looks like in a pull request, and the reasoning it's illustrating — "being wrong in public," visible to a reviewer who's never run `okf-rs`, is the safety property that matters for a knowledge layer AI agents act on, not just a format preference — directly reusing the framing external review (August 2026) praised as "the point I think is right and undersold." The "Open" bullet is renamed "Git-native" to name the property instead of a generic virtue, "Deterministic" now points at `generate --check-determinism` as the thing that lets a reader verify the claim instead of taking it on faith, and the closing paragraph's `docs/improvement-plan.md` link is expanded in place to state the CodeGraph/code-review-graph comparison's actual conclusion (database-plus-query-layer vs. the artifact itself being the knowledge base) rather than leaving a reader to click through for it.
- [x] ✅ **CI validation mode** — `okf-rs validate --project . --ci` already shipped (orphan-warnings-as-errors gate — every other warning class stays advisory even under `--ci`, per spec §7/§11 and [issue #23](https://github.com/jyjeanne/okf-rs/issues/23); exercised in `okf-cli`'s e2e suite) and, together with [`.github/workflows/pr-review.yml`](.github/workflows/pr-review.yml) from the Improvement Plan Delivery above, covers "perfect for GitHub Actions." Provenance-metadata validation shipped alongside edge provenance above: a populated `confidence` that isn't `exact`/`semantic`, or an explicitly empty `resolved_by`, is a validator error (`check_relationship_provenance`). The last named gap — "graphs are up-to-date" / "no unexpected changes" — is now `okf-rs generate --check-fresh`: re-analyzes the project fresh (bypassing the incremental cache), renders it to a throwaway directory, and diffs that against the bundle already on disk at `--output`, reporting "Up to date" or listing every stale file and exiting non-zero. Read-only (never touches `--output`/`.okf-cache.json`), fails clearly if no bundle exists yet rather than reporting everything as stale, and — the same reasoning `--check-determinism --enrich` already established — rejects `--enrich`, since comparing against a freshly analyzed *unenriched* bundle would flag every AI-written description as spurious drift rather than real staleness.
- [x] ✅ **Separate syntax vs. semantic relationship kind** — delivered as part of confidence levels above rather than as a second field: `confidence: exact` *is* "syntax-derived" (Tree-sitter) and `confidence: semantic` *is* "LSP-resolved," the exact distinction this item asked for. A dedicated `kind: syntax`/`kind: semantic` field alongside `confidence` would carry the same information twice — simplified to one field once it became clear they'd always move together (nothing produces a "semantic, but not LSP-resolved" edge, or vice versa). `okf-rs graph`/`search` filtering by it is not yet built — the field exists in every generated bundle, but no query surfaces a `--confidence` filter yet.

`okf-rs generate --check-fresh` reuses `--check-determinism`'s own `ScratchDir`/`diff_dirs` machinery (`diff_dirs` generalized to take two labels instead of a hardcoded "run 1"/"run 2", since this diffs the committed bundle against a fresh regenerate — genuinely different things, not two interchangeable runs) rather than duplicating it. Verified two ways. Unit tests cover the relabeled `diff_dirs` directly, including a dedicated test confirming the labels it's given actually show up in the output, not a leftover hardcoded string. End-to-end, five new `okf-cli` e2e tests run the real compiled binary: right after a `generate`, `--check-fresh` reports "Up to date"; after editing source *without* regenerating, it reports "Stale", names the specific new concept file that's missing, and — checked directly — leaves the committed bundle byte-for-byte untouched (read-only, not a silent regenerate); with no bundle at all yet, it fails with a clear message pointing at `generate` rather than reporting every possible file as stale; `--check-fresh --enrich` and `--check-determinism --check-fresh` together are both rejected up front with clear errors. Dogfooded against this repository's own 913-concept bundle: `--check-fresh` reports "Up to date" right after `generate`; adding a throwaway public function to `okf-core` without regenerating makes it correctly report "Stale: 4 file(s)..." — the new function's own concept file, that directory's `functions/index.md`, the owning module's page, and the bundle root `index.md` (all genuinely changed) — and reverting the source change makes it report "Up to date" again, with the bundle on disk never touched at any point in the process (confirmed via `git status`).

### Long-term

- [x] ✅ **Real-world benchmarks beyond self-hosting** — a first pass, not the full six-language sweep the item names: `okf-rs` run end-to-end (`generate`, `validate`, `generate --check-determinism`, `okf-mcp --benchmark`) against three real external projects cloned fresh, one per language: [ripgrep](https://github.com/BurntSushi/ripgrep) (Rust, 110 files/56K LOC), [requests](https://github.com/psf/requests) (Python, 37 files/12K LOC), [cobra](https://github.com/spf13/cobra) (Go, 36 files/17K LOC). Java, TypeScript, and C# remain for a follow-up pass. **Not measured this pass** (honestly scoped, not fabricated): memory usage (no `/usr/bin/time -v` in this environment) and AI task-completion time / comparison against RAG (both need a real agent-task harness and a RAG baseline neither of which exist yet — the same gap `okf-mcp --benchmark`'s own "Not measured here" note already names for the session-level item above).
- [x] ✅ **Explainability** — `okf-rs graph explain <from> <to>` (and the MCP `graph` tool's `explain` relation) renders exactly the shape this item names: the relation kind between two concepts as an arrow diagram, plus a human-readable reason built directly on edge provenance (`okf_parser::Relationship::reason`) — "Resolved via Tree-sitter's unambiguous, project-wide name match..." for a `tree-sitter`/`exact` edge, or "Resolved by asking rust-analyzer which definition this call site actually resolves to..." for an `--lsp`-resolved one. Checks both concepts' own relationship lists (a kind like `Calls`/`CalledBy` is only ever recorded on one side), rendering whichever direction is actually on record rather than forcing a canonical one; falls back to explaining the shortest call *path* hop-by-hop when there's no single direct relationship.
- [x] ✅ **Continued AI-agent optimization** — the "incremental/lazy graph loading" half of this item, applied to *serving* a bundle rather than generating it: `okf-mcp` now caches parsed concepts per bundle path across the lifetime of one server process, keyed on a cheap, content-free fingerprint (file count, total bytes, latest modification time across the same `.md` files `read_bundle` itself walks) rather than any TTL or manual invalidation. A cache hit skips the walk-and-parse entirely; any `generate` run between two calls changes the fingerprint and forces a fresh parse, so a session never needs restarting to see the latest bundle — the same freshness guarantee the pre-cache design made, kept intentionally rather than traded away for speed. Scoped to `okf-mcp` specifically, not `okf-query` generally: `okf-cli` is a fresh process per invocation with nothing to amortize, so every `okf-query` function keeps its original `bundle: &Path`-taking form unchanged, gaining a `*_from_concepts` sibling that operates on already-loaded concepts for `okf-mcp`'s cache to call into. `search`/`search_ranked` are not cached — they build their own exact/BM25 index straight from the bundle path, a separate mechanism from the concept cache. Token-efficient serialization, context-aware extraction, and hybrid graph+RAG workflows remain open — this item ships the one piece that was concretely actionable without a much larger design (a graph+RAG hybrid, in particular, needs a real vector-store integration this project doesn't have).

Real-world benchmarking found and fixed two genuine bugs neither self-hosting nor synthetic test fixtures had ever surfaced — the actual value this item exists for, not just a numbers exercise. **Both found on the very first run against ripgrep**, `okf-rs`'s largest sample (110 files):

1. **Validator false positive on mutual-call pairs.** Two functions that call each other (`HasAvailableSubCommands`/`IsAvailableCommand` in cobra; `RequestsCookieJar.set`/`create_cookie` in requests) legitimately link to the same target twice on one concept page — once under `# Calls`, once under `# Called by` — which `okf-validator`'s redundant-link check flagged as a warning, since it tracked "already linked" per whole file rather than per section. Fixed by making the check section-aware (`split_into_sections`, breaking at each top-level `# Heading`): a repeat *within* one section (a real duplicate) still warns; a repeat *across* two different relation-kind sections (two distinct facts that happen to share a target) doesn't. Before the fix: ripgrep reported 5 such warnings, requests 2, cobra 2 — 9 false positives across the three repos, on a codebase (`okf-rs` itself) that had produced zero of them in every prior dogfooding pass.
2. **A concept literally named `index` collides with the bundle's own reserved `index.md` navigation filename.** Ripgrep's `impl std::ops::Index<Match> for [u8] { fn index(...) }` produced a concept file at a path — `.../index.md` — that `okf-generator` also uses for every kind-directory's plain navigational listing (`functions/index.md`, `modules/index.md`, ...). `okf-validator` correctly rejected it ("only the bundle-root index.md may carry frontmatter"): 6 such errors on ripgrep alone. Worse than the false positive above — by the same construction, a bare top-level function or module literally named `index` (extremely common for a web framework's entry-point handler, e.g. Flask/Django) would collide *directly* with a kind-root's own `functions/index.md`/`modules/index.md`, and since `write_bundle` writes concept files before kind-directory index pages, the concept would be **silently overwritten with no error at all** — confirmed by reading `write_bundle`'s write order, though not directly reproduced (every extractor tried always prefixes a qualified name with its containing module, so a truly bare top-level `index` id didn't occur in the three sample repos). Fixed in `Concept::disambiguate_ids` (already the established, tested mechanism for id collisions — see Phase 1's `#[cfg(...)]`-blind duplicate-definition handling): any concept whose id ends in `/index` (case-insensitively) is now bumped to `.../index-2` before anything is written, reusing the exact same "-2", "-3", ... suffix logic a genuine duplicate id already gets.

Both fixes are covered by dedicated unit tests (`okf-validator`: a mutual-call pair not flagged, a genuine within-section duplicate still flagged, `split_into_sections` tested directly; `okf-parser`: five `disambiguate_tests` covering the bump itself, case-insensitivity, chaining past the reserved slot when two concepts are both literally named `index`, and independence across directories) and re-verified against all three real repos with the fix applied: `okf-rs validate` now reports "no issues found" on ripgrep, requests, and cobra alike, and `generate --check-determinism` still reports deterministic on all three (confirming neither fix introduced its own non-determinism).

Headline numbers (all three, `okf-mcp --benchmark` default 5-sample): ripgrep — 3,494 concepts, 18 MB bundle, 1.13s cold `generate`; requests — 845 concepts, 0.34–0.75s; cobra — 647 concepts, 0.30–0.48s (a range across runs in this shared environment, not a precision claim). MCP registration cost held steady at 6 tools / ~1,275 tokens across all three (bundle-independent, as expected); average tokens saved per sampled structural query ranged from ~3,100 (ripgrep, where two of five samples were common method names like `insert`/`has_command` — `--benchmark`'s own documented inflation caveat) up to ~21,000 (cobra) — break-even stayed at "~1 structural query" on every project sampled, consistent with the self-hosted numbers reported for the `--benchmark` item above.

`okf-mcp`'s bundle cache (`cache.rs`, new module) is a fingerprint-keyed `HashMap<PathBuf, (Fingerprint, Arc<Vec<Concept>>)>` behind a `Mutex`, matching the server's already-single-threaded-per-request stdio loop. Every `okf-query` function that consumes concepts (`explore`, every `graph_*` relation, `coverage`, `graph_stats`, `search_semantic`, `suggest_links` — 20 functions in total, including the two report-struct variants, `coverage_report`/`graph_stats_report`) gained a `*_from_concepts` sibling that runs the identical query against an already-loaded `&[Concept]` slice; the original `bundle: &Path`-taking functions now just call `load_concepts` once and delegate, so `okf-cli`'s behavior and every existing test for it are unchanged. Verified two ways. Unit tests: `okf-query` has a parity test asserting every `*_from_concepts` function produces byte-identical output to its path-based counterpart given the same concepts (across two bundle fixtures — one plain, one with packages/layers/domains — plus dedicated parity tests for the two functions needing a live `EnrichClient`); `okf-mcp`'s `cache.rs` has 7 tests covering a cache hit skipping the parse entirely (asserted via an internal load counter, not just matching output), a new concept file, an edited file's content, and a deleted file each correctly invalidating the cache, an unrelated non-`.md` file *not* invalidating it, and a missing bundle erroring clearly without ever getting cached; `tools.rs` adds an end-to-end test confirming a mixed `coverage`/`graph`/`explore` sequence through the real `call` dispatch function only parses once. Dogfooded end-to-end against this repository's own bundle (979 concepts) using the real compiled binary, comparing the pre-cache and post-cache builds (via `git stash`, matching the technique used to verify the two real-world-benchmarking bug fixes above) on an identical 110-call session over stdio — 10 rounds of `explore`/`graph callers`/`graph callees` against 3 real concept ids, plus a `coverage` and a `graph stats` call each round, the kind of back-and-forth an agent actually does while investigating one area of code: **~42ms/call before the cache, ~5ms/call after — roughly 8.4x fewer wall-clock milliseconds per call**, averaged over 5 runs each way to smooth out shared-environment noise. This is a modest bundle (979 concepts); the gap widens with bundle size, since what's being skipped on a cache hit is the walk-and-parse cost `read_bundle` pays per file, which grows with the bundle, while the fingerprint check that replaces it stays a cheap stat-only walk regardless. The freshness guarantee was checked directly, not just asserted: editing a concept's `description` and re-querying through the cache (`cache.rs`'s `editing_a_concept_files_content_invalidates_the_cache` test) picks up the new content on the very next call, with no server restart.

A follow-up code review of the cache change (multiple independent finder passes, run at medium/high effort) surfaced and fixed three issues, none of which changed the headline result above. **Argument-validation precedence**: `explore` and every `graph` relation had started validating their own arguments (`query`/`id`/`from`/`to`) *after* calling into the cache, so a malformed request against a project that also hadn't been `generate`d yet got a "no bundle found" error instead of the argument-specific one, masking which of the two problems was the real one — fixed by validating each relation's own arguments before touching the cache, with tests pairing a bad/missing argument against a nonexistent bundle path to lock the precedence in. **Duplicated filter logic**: the cache's fingerprint hand-duplicated `read_bundle`'s file filter (`.md` extension, exclude `index.md`) instead of sharing one definition — a future edit to either copy alone could have silently desynced them, with the cache then never invalidating on a file `read_bundle` actually parses; extracted into `okf_parser::is_concept_file`, now the one definition both use. **A misleading comment**: `benchmark.rs` claimed its `BundleCache` was "throwaway" with "nothing to amortize" within a run, when it's actually shared across the whole sampled-concepts loop — corrected to say so plainly, noting it doesn't change the benchmark's own reported numbers either way (those measure response size, not call latency). Full workspace `cargo test`/`clippy -D warnings`/`fmt --check` stayed clean throughout.

### Research topics

Exploratory, not yet scheduled: deterministic semantic analysis, incremental graph updates, graph compression, provenance tracking (concrete form TBD by the provenance item above), multi-language project federation, graph federation across repositories, AI-native graph formats, and knowledge-graph diff algorithms (`okf-rs diff` already exists at the concept level — this is the open question of what a genuinely *graph-structural* diff, not just an added/removed/changed concept list, would look like).

### Guiding principles for this plan

Every item above should improve at least one of: determinism, transparency, auditability, AI efficiency, developer experience, CI/CD integration, or Git friendliness — the same test the rest of this roadmap has applied since Phase 1.

**Not done here, deliberately:** the reviewer's suggestion to convert this into a GitHub Projects roadmap with milestones and linked issues is a good one, but it's a repository/process change (creating GitHub milestones and issues) rather than a documentation change, so it's left for an explicit follow-up request rather than done as a side effect of recording this feedback.

---

## Improvement Plan — Provenance Depth, Graph Diff & MCP Tool-Selection

The same external reviewer's *second* round of feedback (recorded verbatim in
[`docs/feedback/2026-08-provenance-graph-diff-review.md`](docs/feedback/2026-08-provenance-graph-diff-review.md)),
distilled into a codebase-grounded, phased plan in
[`docs/improvement-plan-provenance-diff.md`](docs/improvement-plan-provenance-diff.md) after a gap
check against the AI-native platform maturity plan above found most of that review's phases 1-3
and 9 already shipped. Only the plan's six genuinely new phases (A-F) are tracked here.

- [x] ✅ **Phase A — Resolver version.** `Relationship` gains an optional `resolver_version` field,
  populated from the LSP `initialize` handshake's `serverInfo.version` (`okf_lsp::LspClient::server_version`)
  and threaded through `okf-analyzer`'s `ResolvedEdge` the same path `resolved_by`/`confidence`
  already travel. Rendered as `resolver_version: 1.88.0` alongside `resolved_by`/`confidence` in
  frontmatter, omitted entirely for `tree-sitter` edges or a server that didn't report one — the
  same optional-field, backward-compatible shape those two fields already established.
  `okf_validator::check_relationship_provenance` gained a new warning (not an error, matching the
  rest of that check's severity choices): a `resolver_version` set without a real `resolved_by`
  alongside it (i.e. on a `tree-sitter` edge) is flagged, since tree-sitter has no version to
  record. **Deliberately not built**, per the plan's own reasoning: a closed `ProvenanceOrigin`
  enum (`TreeSitter`/`Lsp`/`Manual`/`Derived`) — `resolved_by` is already a free-form string (a
  hand-edited bundle can carry `resolved_by: hand-edited` today), and this project already
  rejected adding confidence-enum variants nothing produces; a new enum with unproduced variants
  would repeat that mistake.
- [x] ✅ **Phase B — Provenance-aware graph diff.** `okf_analyzer::diff` gains a new
  `RelationshipChangeKind` (`Unchanged`/`SourceChange`/`ResolverChange`/`ConfidenceChange`/
  `ProvenanceChange`) and a `diff_relationships(before, after)` helper, pairing a concept's
  relationships by `(kind, target)` and classifying each pair by which provenance fields moved.
  `ChangedConcept` gains a `relationship_changes` field carrying the non-`Unchanged` pairs — purely
  additive; `Added`/`Removed`/`Changed` and every existing `impact()`/`review()`/`explain()`
  consumer are untouched. Closes a real bug the plan's own gap analysis named, in both directions:
  previously, `diff`'s (kind, target)-only equality check meant a **resolver-version-only change
  was invisible to `diff` entirely** (the concept never even entered `report.changed`), while a
  genuine target rewire that also happened to change resolver/version was at risk of being
  misread as "just the resolver" if a classifier weren't careful to check the target first. Fixed
  a fifth case the plan's own review caught before this shipped: `resolved_by` **and** `confidence`
  changing *together* on an unchanged target (the shape a same-named function added elsewhere in
  the project produces — a previously-unambiguous call becoming ambiguous, newly needing `--lsp`)
  fits neither `ResolverChange` nor `ConfidenceChange` alone, so it's its own `ProvenanceChange`
  variant rather than being silently absorbed into one of the other two.
- [x] ✅ **Phase C — `okf-rs diff --ci` policy.** A new `--ci` flag classifies a `DiffReport` via
  `okf_analyzer::ci_summary` into source/resolver/confidence counts and renders them as
  `❌`/`⚠️`/`ℹ️` lines plus a deterministic exit code, evaluated against a new `okf.toml` `[diff]`
  section (`okf_core::config::DiffPolicy`: `resolver_changes` defaults to `"warn"`,
  `confidence_changes` defaults to `"ignore"`; either can be set to `"warn"`/`"fail"`/`"ignore"`).
  Source-level changes (added/removed concepts, a changed signature, or a
  `RelationshipChangeKind::SourceChange` pair) are never configurable — always both reported and
  failure-worthy, mirroring `validate --ci`'s "warnings are real problems by default" posture.
  `ProvenanceChange` pairs are folded into `resolver_changes`, not a fourth policy knob of their
  own. Caught and fixed one real gap while implementing the plan's own "counting unit" spec: a
  pure signature-only change (no relationship difference at all) wasn't counted by the plan's
  original formula, which would have let a CI run silently pass a PR that only changed a
  function's signature — `ci_summary` now counts a `Changed` concept's `before_signature !=
  after_signature` as a source change in its own right, independent of `relationship_changes`.
- [x] ✅ **Phase D — Artifact-level reproducibility metadata.** The bundle-root `index.md`'s existing
  `okf_version` frontmatter (already the one place OKF permits frontmatter on an `index.md`) gains
  `generator_name`/`generator_version` (always populated — `"okf-rs"`/the crate's own compile-time
  version) and `source_revision` (the commit `HEAD` pointed to when `generate` ran — new
  `okf_core::git::head_revision`, best-effort, `None` outside a git repo). Deliberately **no
  `generated_at` timestamp** — a new `manifest.md` file was this plan's first draft and is
  deliberately *not* what shipped (see the plan doc's own correction): every other `.md` file in a
  bundle is required to carry a recognized concept `type:`, which metadata has none of, so a
  separate file would be a validator error under this project's own tooling. Extending the
  existing, already-permitted root frontmatter avoids that entirely. Caught and fixed a real
  regression risk while implementing: naively embedding `source_revision` would make `generate
  --check-fresh` report false staleness whenever `HEAD` moves for a reason that never touches the
  analyzed source tree (a README edit, say) — `source_revision` describes *which commit produced
  this artifact*, not *whether the analyzed content matches*, and conflating the two would break
  `--check-fresh`'s own contract. Fixed by having `okf-cli`'s `diff_dirs` normalize
  (`strip_source_revision_line`) that one line out of the root `index.md` comparison
  `--check-determinism`/`--check-fresh` both use, before diffing — the artifact still records its
  real provenance; the *staleness check* just doesn't conflate that with content drift.
- [x] ✅ **Phase E — Specialized-vs-consolidated MCP tool-*selection* benchmark.** Shipped in two
  parts, exactly as the plan's own "CONDITIONAL GO" verdict scoped it.

  **The offline harness** (`okf_mcp::tool_selection_benchmark`) is the fully offline, model-free
  half the plan's own review called for building first: a fixed 14-question set (one per `graph`
  relation), each with a known-correct answer verified directly against a real fixture bundle
  through the actual `tools::call` dispatch — no model involved, matching every other benchmark in
  this codebase's offline-and-deterministic posture. The pre-0.3.0 specialized-tool-name mapping
  (`graph_<relation>`) is reconstructed for comparison, with the one documented exception
  (`explain`, which postdates the consolidation and has no specialized-era counterpart) scored
  against the consolidated design only, never a dropped or invented one.

  **The live-endpoint runner** (`okf_mcp::tool_selection_live`, behind `okf-mcp
  --benchmark-tool-selection`) actually calls a model now that the plan's deliberate
  "who owns interpreting a non-deterministic result, which endpoint" decision has been made:
  an OpenAI-compatible endpoint configured entirely through environment variables —
  `OKF_BENCHMARK_MODEL_BASE_URL`/`OKF_BENCHMARK_MODEL`/`OKF_BENCHMARK_MODEL_API_KEY` — mirroring
  `--enrich`'s existing `OKF_ENRICH_*` pattern, but a deliberately **separate** set of variables so
  the benchmarked model and the enrichment model can be configured independently. Not a reuse of
  `okf_enrich::EnrichClient`: that client's `complete()` sends plain system+user chat completion
  with no `tools`/`tool_choice`, insufficient for a benchmark that has to measure which tool schema
  a model actually picks — this module has its own small `ToolCallingClient` speaking the
  OpenAI-compatible function-calling wire format (`tools: [{type: "function", function: {...}}]`,
  reading back `choices[0].message.tool_calls`). Runs both designs for real: the consolidated
  design against the live `graph` tool's own schema (`tools::list()`, not a hand-copied
  approximation), the specialized design against 13 reconstructed `graph_<relation>` schemas whose
  descriptions are lifted from the consolidated tool's own per-relation bullet points (so a
  specialized-design loss can't be an artifact of a worse description) — reusing the harness's own
  `questions()`/`scores_correctly()`/`specialized_tool_name()` rather than duplicating them.
  Reports tool-selection accuracy, final-answer accuracy, total tokens, and total latency per
  design, plus a per-question `[WRONG]`/`[ERROR]` breakdown, with the model name, sample size, and
  an explicit "not a universal claim about LLMs in general" caveat stated up front. Single-shot
  only (`tool_choice: "auto"`, no multi-turn retry loop) — a deliberate scope cut from the plan's
  original "records whether it needed a follow-up/retry" sketch: a retry loop needs its own scoring
  policy (does a correct-on-retry count as correct?) the plan never specified, so a well-defined
  single-shot measurement shipped instead of an underspecified multi-shot one. Never run by `cargo
  test --workspace`, matching every other live-network path in this codebase; this module's own
  tests cover the HTTP request/response parsing layer against a hand-rolled mock server (the same
  pattern `okf_enrich::test_support` established), not real model behavior.
- [x] ✅ **Phase F — `tests/fixtures/` golden dataset.** The reviewer's proposed layout, exactly:
  `provenance/{tree-sitter,lsp,mixed}.md` (real bundle concept files covering Phase A's three
  provenance shapes), `diff/{unchanged,added-edge,removed-edge,resolver-change,semantic-change}/
  {before,after}/` (real bundle directories for Phase B's five worked scenarios), and
  `mcp/{consolidated,specialized}/` (Phase E's 14-question set and reconstructed pre-0.3.0
  tool-name mapping, exported as portable JSON — the one piece of data that genuinely couldn't be
  used outside Rust before this). Every fixture is exercised by a real test, not left to rot:
  `okf-parser` round-trips the provenance fixtures through the real `read_bundle` path;
  `okf-analyzer` round-trips every diff scenario through the real `read_bundle` → `diff` →
  `ci_summary` pipeline (not the in-memory `Concept` literals every other test in that file
  builds by hand — a genuinely different code path, and the first test in this repo to diff two
  *real bundle directories on disk* rather than two in-memory concept lists); a dedicated guard
  test greps the analyzer test file for every `diff/` subdirectory's name, so an unreferenced
  fixture fails the build instead of silently rotting; and `okf-mcp` checks the exported JSON
  against the live Rust question set on every run, so the two can never drift apart. Most of Phase
  B's own classification scenarios deliberately stayed as inline `Concept`/`Relationship` struct
  literals in Rust test code rather than being force-relocated here — this phase only moved what's
  genuinely more natural as data than code (real fixture bundles, portable JSON), matching its own
  stated scope rather than relocating everything for its own sake.

Verified by dogfooding. Unit tests cover the new field end-to-end: `okf-lsp` parses `serverInfo.version`
from a real `initialize` response and — genuinely exercised in this environment, not skipped —
`LspClient::server_version()` returns `Some(..)` against a real `rust-analyzer`; `okf-parser`
round-trips `resolver_version` through `read_bundle` (present, absent, and the pre-existing
bare-string/missing-field fallback paths all still resolve to `None`); `okf-generator`'s frontmatter
round-trip test now asserts a real `resolver_version` (not just `resolved_by`/`confidence`) survives
write-then-read-back; `okf-validator` covers both the new warning and a resolver_version alongside a
real resolver staying unflagged. End-to-end against this repository's own source via the real
compiled binary: `okf-rs generate . --lsp` (992 concepts) rendered `resolver_version: 1.94.1
(e408947 2026-03-25)` — the real installed `rust-analyzer`'s own reported version string, parentheses
and all — on 444 relationship entries; `okf-rs validate` reported "no issues found" against that
bundle (confirming the new warning doesn't misfire on real `--lsp` output, where every versioned
edge does name a real resolver); and, on the plain tree-sitter-only path (no `--lsp`, where this
field is never populated), `okf-rs generate . --check-determinism` still reported "Deterministic"
(992 concepts) — confirming this schema addition doesn't regress the determinism guarantee it
deliberately doesn't touch.

Phase B verified by dogfooding. `cargo test -p okf-analyzer` covers the plan's six worked
scenarios directly (added edge, removed edge, a same-resolver target rewire, a resolver-version
bump, a resolver change that also rewires the target, and the combined resolver+confidence case),
plus order-independence (`diff_relationships` pairs by key, not position, so a reordered-but-
identical relationship list still reports empty) and a signature-only change correctly reporting
an empty `relationship_changes`. One new test asserts the bug-fix directly, not just the
classification: a concept whose only difference between two snapshots is a `resolver_version` bump
now correctly appears in `report.changed` at all (`report.changed.len() == 1`), which it did not
before this phase — the previous (kind, target)-only equality check had no way to see it. The full
workspace suite (`cargo test --workspace`, `cargo clippy --workspace --all-targets -D warnings`,
`cargo fmt --check --all`) stayed clean throughout, including every pre-existing `okf-cli` e2e test
for `diff`/`impact`/`review` against the real compiled binary — confirming the new field is
additive, not a behavior change, for every existing consumer.

Phase C verified two ways. `render_ci_report` — the pure rendering/policy-evaluation function
`cmd_diff_ci` is a thin wrapper over, the same split `render_review_markdown`/`cmd_review` already
established — is covered by 12 unit tests reproducing the plan's full exit-code table (no changes,
metadata-only, resolver-only at both `"warn"` and `"fail"`, added/removed/rewired source edges,
mixed source+resolver, and a dedicated test confirming a `ProvenanceChange`-derived count follows
`resolver_changes`' policy rather than `confidence_changes`') entirely with synthetic `CiSummary`
data — deliberately so, since a genuine resolver-*version* difference needs two different
installed `rust-analyzer` binaries, which neither this environment nor CI can guarantee, and
`okf-rs diff`'s underlying two-ref analysis doesn't run `--lsp` at all regardless (a pre-existing
limitation this phase didn't introduce or attempt to fix). End-to-end, three new `okf-cli` e2e
tests run the real compiled binary: a source-level change (an added/removed function) fails `--ci`
with `❌ SOURCE CHANGES: 2` and `exit code: 1`; a ref diffed against itself exits `0` with `No
changes between...`; and a project with a hand-written `okf.toml` `[diff]` section still loads and
runs correctly (confirming the config-loading path doesn't error just because the file exists),
though it can't exercise the resolver/confidence *gating* itself for the same `--lsp` reason.
Dogfooded against this repository's own real git history via the real binary: `okf-rs diff
717b445 97474f6 . --ci` (the release-v0.4.0 commit against this plan's own Phase A+B commit)
correctly reports `❌ SOURCE CHANGES: 57` and exits `1`; the same ref against itself reports `No
changes...` and exits `0`; and the pre-existing non-`--ci` `okf-rs diff` output is confirmed
byte-for-byte unaffected by this phase. Full workspace `cargo test`/`clippy -D warnings`/`fmt
--check` stayed clean throughout.

Phase D verified two ways. Unit tests cover the new `okf_core::git` module directly (`None`
outside a git repository, the real `HEAD` SHA inside one, `None` in a freshly initialized repo
with no commits yet, and — the deliberate non-behavior — the SHA is still returned when the
working tree is dirty, not withheld); `okf-generator`'s root-index tests cover
`generator_name`/`generator_version` always being present, `source_revision` omitted entirely
when `None` (never rendered as `source_revision: null`), and rendered correctly when given;
`okf-cli`'s `strip_source_revision_line`/`diff_dirs` tests cover the normalization directly — two
root `index.md` files differing *only* in `source_revision` report no diff, while a real content
difference in the same file (e.g. a changed "Contains N" count) still gets caught, confirming the
normalization strips exactly one line, not the file's ability to be diffed at all. End-to-end
against real git repositories via the compiled binary: `okf-rs generate` records the actual
current `HEAD` SHA in the bundle root (`generate` inside a fresh repo at a known commit, comparing
the bundle's `source_revision` against `git rev-parse HEAD` run independently); and the regression
test this phase's own design doc flagged as the real risk — commit the source, generate, commit
the bundle, then commit an unrelated file (`README.md`) that never touches `src/` at all, moving
`HEAD` one commit past what the committed bundle recorded — confirms `generate --check-fresh`
still reports "Up to date," not a false "Stale." Dogfooded against this repository's own source
via the real binary: `okf-rs generate .` (1062 concepts) rendered `source_revision:
"5989f246c4c50e13bc4bd0a6769c04816e773e43"` — exactly this repository's real `HEAD` at generation
time, confirmed by an independent `git rev-parse HEAD`; `okf-rs validate` reported "no issues
found"; and both `generate --check-determinism` and `generate --check-fresh` (run immediately
after) reported clean, confirming this phase doesn't regress either guarantee on a real, non-toy
codebase. Full workspace `cargo test`/`clippy -D warnings`/`fmt --check` stayed clean throughout
(one pre-existing, environment-timing-dependent real-`rust-analyzer` test flaked and passed on
retry in isolation, unrelated to this phase — the same flake already noted on this PR's CI).

Phase E's harness verified by dogfooding — through the real MCP tool dispatch, not a shortcut
around it. `every_question_s_expected_answer_is_correct_against_the_fixture` calls
`tools::call("graph", ...)` — the same function the real `okf-mcp` stdio server dispatches every
`tools/call` request through — for all 14 questions against the fixture bundle, and passed on the
first run: every one of the fixture's deliberately-varied shapes (a real cross-package call for
`callers`/`callees`/`path`/`explain`/`api`/`modules`/`layers`/`domains`/`communities`, a genuine
two-function call cycle for `cycles`, a call-free concept for `isolated`, a `*Builder`/
`*Controller`-shaped pair for `patterns`/`features`) produced exactly the claimed answer. A
second test cross-checks the question set's 14 relations against the `graph` tool's own live
schema (`tools::list()`), not a hardcoded copy of it, so the question set can't silently drift out
of sync with what `graph` actually exposes if a relation is ever added or renamed. Full workspace
`cargo test`/`clippy -D warnings`/`fmt --check` stayed clean throughout.

Phase F verified by dogfooding — and by design, its own tests are the dogfooding: this phase
exists specifically to make the fixtures a real, disk-resident artifact rather than data baked
into test source, so "does a real reader parse these correctly" is the actual claim under test,
not a metaphor. `golden_provenance_fixtures_round_trip_through_read_bundle` and
`golden_diff_fixtures_classify_correctly_through_read_bundle` both passed on the first run against
the real fixture files (not adjusted after the fact to match whatever the code happened to
produce) — confirming the hand-authored YAML frontmatter in `tests/fixtures/` is byte-for-byte
compatible with what `okf-generator` itself would have written, for every provenance shape and
diff scenario named. `every_diff_fixture_subdirectory_is_referenced_by_a_test` was verified to
actually catch drift, not just pass vacuously: temporarily adding an unreferenced sixth
subdirectory under `tests/fixtures/diff/` during development made it fail with the expected
message, then removing it made it pass again. `exported_json_fixtures_match_the_rust_question_set`
closes the loop on Phase E's own "a form a non-Rust benchmark script could also load" goal — the
question set now genuinely exists outside Rust, checked against the source of truth on every test
run rather than trusted to stay in sync by hand. Full workspace `cargo test`/
`clippy -D warnings`/`fmt --check` stayed clean throughout.

Phase E's live-endpoint runner verified by dogfooding against a **real, separate-process** HTTP
server — not just the in-process Rust mock its own unit tests use — proving the whole chain (CLI
flag parsing → env-var config → real socket HTTP request → OpenAI-compatible function-calling
response parsing → dispatch through the real `graph` tool → scoring → report rendering) actually
works end to end, three ways: (1) `okf-mcp --benchmark-tool-selection` with no env vars set exits
with a clear error naming `OKF_BENCHMARK_MODEL_BASE_URL`, not a panic or a generic failure; (2)
against a standalone Python mock server (a genuinely separate OS process, reachable only over
`127.0.0.1`) that answers every question correctly for whichever schema it's offered, both designs
scored perfectly — 14/14 (100%) consolidated, 13/13 (100%) specialized; (3) against a second mock
server deliberately wrong on the `callers` question and silent (no tool call at all) on `stats`,
the report correctly dropped to 12/14 (86%) consolidated and 11/13 (85%) specialized, with an exact
`[WRONG] "Who calls bar?": expected \`callers\`, got Some("callees")` line and an exact `[ERROR]
"...concept-kind and relationship stats?": model ... did not call any tool` line — confirming the
scoring and error-reporting paths, not just the happy path, hold up against a real socket. Full
workspace `cargo build`/`cargo test --workspace --no-fail-fast`/`clippy --all-targets -D warnings`/
`fmt --check` stayed clean throughout (33 test-result blocks, all passed, zero failures).

A follow-up code review of that live-endpoint runner caught and fixed three real issues before
merge, none of them requiring a design change: (1) `ResponseMessage.tool_calls` now deserializes
as `Option<Vec<ToolCall>>` instead of a bare `Vec` with `#[serde(default)]` — that annotation only
covers a *missing* key, and several OpenAI-compatible endpoints reply with an explicit JSON `null`
for `tool_calls` instead, which previously failed deserialization with a generic "malformed
response" error rather than this module's own clear "did not call any tool" diagnostic; a
regression test locks this in, and it was re-verified against a real standalone mock server
replying with a literal `"tool_calls":null` — the CLI now reports the intended per-question
`[ERROR]` line instead. (2) `tool_selection_live::run` is now fallible
(`-> Result<LiveReport>`) instead of panicking through `fixture_bundle()`'s `.unwrap()`s on I/O
failure — `tool_selection_benchmark` now exposes both the original panicking `fixture_bundle()`
(kept for the many test call sites, where a hard setup panic is the accepted idiom already used
throughout this codebase's tests) and a fallible `fixture_bundle_try()` that `run` uses instead,
propagated with `?` through `main()`'s existing `anyhow` error path — an unwritable or full temp
directory now surfaces as a normal error message rather than crashing the process. (3) the
"always answers correctly" end-to-end test no longer reimplements the mock server's raw
Content-Length/body-read loop inline; `start_mock_server` is now a thin wrapper over a new
`start_mock_server_with(respond)` taking a per-request response callback, and that test reuses it.
Full workspace verification repeated after both this fix and a rebase onto three unrelated PRs
that merged to `main` in the meantime — clean throughout, zero failures.

---

## Phase 4 — Ecosystem

Both items below build directly on Phase 3's stable `okf-graph`/`okf-query` library API rather than re-implementing graph access.

- [ ] `okf-server`: REST + GraphQL API over the knowledge graph — multi-repository, organization-wide serving
- [ ] `okf-rs` as an LSP server: hover, go-to-definition, and find-references backed by the OKF bundle, reachable from any LSP-capable editor through one server implementation rather than a bespoke plugin per IDE; dedicated VS Code/JetBrains extensions remain useful afterward as thin wrappers around it
- [ ] Visualization: interactive graph explorer over `okf-server`
- [ ] Continuous/distributed indexing at organization scale (beyond the local `okf-watch` from Phase 2)

The improvement plan's remaining conditional-GO/no-go items — bundled local embedding runtime, persisted query cache, static SVG export, multi-repo/daemon serving — stay deliberately out of scope; see [`docs/improvement-plan.md`](docs/improvement-plan.md#6-costbenefit-study-and-gonogo) for why each was scored that way.

---

See [`docs/specification.md`](docs/specification.md) for the full project specification, including the OKF bundle format, architecture, and design principles this roadmap implements against.
