okf 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.okf/capabilities/agent-skill.md +112 -0
  3. data/.okf/capabilities/bundles-manager.md +144 -0
  4. data/.okf/capabilities/graph-server.md +678 -0
  5. data/.okf/capabilities/index.md +26 -0
  6. data/.okf/capabilities/library-api.md +82 -0
  7. data/.okf/capabilities/linter.md +83 -0
  8. data/.okf/capabilities/read-views.md +228 -0
  9. data/.okf/capabilities/render.md +66 -0
  10. data/.okf/capabilities/search.md +297 -0
  11. data/.okf/capabilities/validator.md +60 -0
  12. data/.okf/cli.md +214 -0
  13. data/.okf/design/browser-tests.md +211 -0
  14. data/.okf/design/core-shell-split.md +73 -0
  15. data/.okf/design/index.md +17 -0
  16. data/.okf/design/integration-first.md +140 -0
  17. data/.okf/design/packaging.md +65 -0
  18. data/.okf/design/ruby-floor.md +53 -0
  19. data/.okf/design/runtime-dependencies.md +82 -0
  20. data/.okf/design/search-engines.md +154 -0
  21. data/.okf/design/server-trust-boundary.md +139 -0
  22. data/.okf/index.md +40 -0
  23. data/.okf/log.md +724 -0
  24. data/.okf/model/bundle.md +47 -0
  25. data/.okf/model/concept.md +75 -0
  26. data/.okf/model/graph.md +59 -0
  27. data/.okf/model/index.md +9 -0
  28. data/.okf/model/skeleton.md +76 -0
  29. data/.okf/overview.md +87 -0
  30. data/.okf/registry.md +432 -0
  31. data/.okf/structure/format-layer.md +59 -0
  32. data/.okf/structure/index.md +22 -0
  33. data/.okf/structure/search.md +53 -0
  34. data/.okf/structure/the-analysers.md +60 -0
  35. data/.okf/structure/the-cli.md +99 -0
  36. data/.okf/structure/the-disk-shell.md +76 -0
  37. data/.okf/structure/the-model.md +81 -0
  38. data/.okf/structure/the-server.md +74 -0
  39. data/.okf/structure/the-skill.md +52 -0
  40. data/.okf/testing/adding-a-verb.md +76 -0
  41. data/.okf/testing/index.md +12 -0
  42. data/.okf/testing/the-harness.md +45 -0
  43. data/CHANGELOG.md +161 -16
  44. data/README.md +226 -17
  45. data/lib/okf/cli/command.rb +8 -3
  46. data/lib/okf/cli/registry.rb +306 -47
  47. data/lib/okf/cli.rb +1 -1
  48. data/lib/okf/registry.rb +448 -22
  49. data/lib/okf/render/graph/template.html.erb +6 -2
  50. data/lib/okf/server/hub.rb +6 -2
  51. data/lib/okf/skill/reference/cli/registry.md +49 -6
  52. data/lib/okf/skill/reference/cli.md +1 -1
  53. data/lib/okf/version.rb +1 -1
  54. metadata +46 -5
data/.okf/log.md ADDED
@@ -0,0 +1,724 @@
1
+ # Update Log
2
+
3
+ ## 2026-08-22
4
+
5
+ * **The registry can now take a row out of another registry file, and the
6
+ project-local file has a shorter name.** [`registry
7
+ import`](/registry.md#import-the-copy-that-owns-what-it-takes) is the opposite
8
+ trade from `link`: a link holds a live pointer to a whole file and the other
9
+ side keeps owning what it lends, while an import copies chosen references and
10
+ hands over ownership. Both are needed because they answer different questions —
11
+ "compose that repository's curation" against "I want *that one bundle*, here,
12
+ as mine". Without it the second had one answer: read the path out of `okf
13
+ registry list -g` and retype it into `registry set`, laundering through the
14
+ clipboard information already on screen.
15
+
16
+ Three decisions did the work. The source is `--from`, not a second meaning for
17
+ `-g`, because `-g` names the registry acted *on* across all ten sibling
18
+ subcommands and a verb that names two files should name the second rather than
19
+ invert the first. Slugs are preserved and a collision **refuses**, which is
20
+ where import and link deliberately disagree: a linked name was never chosen
21
+ here so `link_slug` mints around it, an imported one was typed so the "never
22
+ substitute a name you chose" rule applies in full. And a group brings the
23
+ groups nested inside it, because recreating a name here that resolved to a
24
+ larger set there is that same substitution with nothing on screen to reveal it.
25
+ Everything is validated before anything is written, so an import lands whole or
26
+ leaves the file byte-for-byte alone — a refusal that already moved three of four
27
+ rows is not a refusal.
28
+
29
+ * **`.okf.json` is the project-local registry's name; `.okf-registry.json` is
30
+ still discovered.** The file is committed, so retiring the old name outright
31
+ would break every repository carrying one to save eight characters. Both are
32
+ checked *per directory* on the way up rather than one name swept to the root
33
+ and then the other, or a legacy file at a repo root would beat a `.okf.json`
34
+ two levels down and "the nearest one wins" would quietly mean something else.
35
+ The deprecation note is the `registry` umbrella's alone — the one verb whose
36
+ subject is a registry file, and the one nobody runs in a loop or pipes into
37
+ something else, which is exactly what `lint` and `search` are.
38
+
39
+ ## 2026-08-21
40
+
41
+ * **The [registry](/registry.md) composes other registry files, and its umbrella
42
+ grew a `-g`.** A repository that curates its own bundles — this one commits a
43
+ `.okf-registry.json` naming five — could not lend that curation to `~/.okf`
44
+ without registering every row again by hand and re-syncing forever. `okf
45
+ registry link <name> <file>` points the global registry at the file instead:
46
+ its bundles resolve here at read time under their own slugs, `@<name>` is the
47
+ set, and nothing is copied, so the target goes on owning its rows. Two
48
+ restrictions carry the whole design. Only the *global* registry follows links,
49
+ which makes depth one structural rather than enforced — a linked file's own
50
+ links are never read, so no chain forms and there is no cycle to detect. And a
51
+ link is read-only: `rename`, `del`, `default`, `set --as` and `group` all
52
+ refuse a slug it owns, from the `⚙ Bundles` panel as from the terminal, because
53
+ the guard lives in the model rather than in the [CLI](/cli.md).
54
+
55
+ A linked name is minted around a collision rather than refused (`central` →
56
+ `onm-central`), which is the "implicit is forgiving" rule reaching a name this
57
+ registry did not choose — and it is the design's one *computed* slug, so
58
+ `registry list` prints the moved row with the name it carries in its source
59
+ file. `-g`/`--global` is the smaller half: `OKF_NO_DISCOVERY=1` already forced
60
+ the global registry, but a lever reachable only through an env var is one most
61
+ people never find. It is the `registry` umbrella's alone — the one verb whose
62
+ subject *is* a registry file — so [the env-var rule](/cli.md#one-lever-not-two)
63
+ keeps its scope rather than losing its edge.
64
+
65
+ ## 2026-08-19
66
+
67
+ * **This bundle became okf's own, and holds everything about okf.** It began the
68
+ same day as a file-level map of `lib/**` and nothing more, because the
69
+ repository bundle was still the gem's and a second copy of a catalogue is
70
+ worse than none. That reason expired when the repository bundle became the
71
+ *ecosystem's*: the format, the pure model, the seven capabilities, the CLI,
72
+ the registry and the seven design constraints all moved here, where they are
73
+ about one gem and can say so.
74
+
75
+ What stayed at the root is what is not okf's: the OKF format itself, which
76
+ four gems and any future non-Ruby implementation speak, and the layout,
77
+ governance and vision of the repository. A concept cannot link out of its own
78
+ bundle — `Path.normalize_relative!` refuses every `..` — so the references
79
+ that used to cross now name the other bundle in prose, as `@okf-eco format/…`.
80
+ Every one was accounted for rather than dropped.
81
+
82
+ * **`structure/` is the half a test holds to the tree.** `AGENTS.md` carried a
83
+ hand-maintained Map of `lib/**` and nothing checked it: a file could arrive,
84
+ move or leave and the Map would keep reading plausibly. [Structure](/structure/)
85
+ owns it now, eight concepts over the fifty files, and
86
+ `test/unit/bundle_catalog_test.rb` fails on a file no concept names, a concept
87
+ naming a file that is gone, or a verb table out of step with
88
+ `OKF::CLI.builtins`.
89
+
90
+ ## 2026-08-17
91
+
92
+ * **Update**: **the skill's CLI reference is an index and seven leaves, and the
93
+ skill gained a third channel** — [agent skill](capabilities/agent-skill.md).
94
+ A question about one verb used to load all of `reference/cli.md`: 46,234 bytes
95
+ for anything becomes 9,084 for a routing question and 19,533 for the heaviest
96
+ leaf, and cross-file citations now name a `rule:` marker instead of a section
97
+ anchor, so a key survives the next move. The same tree is generated into
98
+ `skills/` as well as into the plugin, which is what lets a generic installer
99
+ (`npx skills add serradura/okf`) reach it with no gem and no Claude Code;
100
+ one task writes every copy, and `rake skill:verify` fails the build on drift.
101
+
102
+ ## 2026-08-14
103
+
104
+ * **Addition**: the **§6.3 references inventory**, on every surface at once —
105
+ `okf references` on the [CLI](capabilities/read-views.md), pure
106
+ `Bundle::References` in [the model](model/bundle.md), and a `references`
107
+ tool on the MCP server (`@okf-mcp design/the-tool-set`). It is the one lens
108
+ that sees a bundle's non-markdown files (a `.py` attester, a `.sql`
109
+ computation) with the concepts citing each, and it names §6.2's bare-path
110
+ trap with its leading-slash fix instead of leaving it to documentation.
111
+ * **Fix**: a spec-compliance pass hardened the checkers, each fix stated
112
+ where it lives: the [validator](capabilities/validator.md) refuses a
113
+ calendar-invalid log date the digit shape used to admit; the
114
+ [linter](capabilities/linter.md) grows `log_order` (its first log-side
115
+ check) and reads both §7 identity fields in `unprefixed_actor`; an
116
+ uppercase `mailto:` no longer resolves as a bundle path; and §10.3's inline
117
+ computation is proven by the fence, not the heading over it. Two behaviors
118
+ the pass confirmed as deliberate are now documented and pinned rather than
119
+ silent: hidden files are outside the bundle, and a frontmatter `id`
120
+ [renames the concept, not its home](model/concept.md).
121
+ * **Addition**: okf-mcp (`@okf-mcp design/the-tool-set`) reaches **fourteen
122
+ read-only tools** — `references`, `tags` (with the `by` curation view),
123
+ `types` and `stats` join — plus a pinnable `lint` clock (`today`),
124
+ `fields`/`except` projections on search and catalog, and `graph`'s traffic
125
+ `cut`. The rollups behind the new views were extracted kernel-side first
126
+ (`Bundle#stats`, `#tag_groups` — [one home](model/bundle.md)), so both
127
+ shells consume the same counting rules; `files` is deliberately not a tool,
128
+ and the refusal is recorded in the concept.
129
+
130
+
131
+ ## 2026-08-13
132
+
133
+ * **OKF v0.2 is the target**: the gem reads, validates, lints, serves and
134
+ teaches the published v0.2 (`@okf-eco format/okf-0-2`) — one reading rule on
135
+ [Concept](model/concept.md) carrying §13.1's two fallbacks inline (no version
136
+ hierarchy), shape warnings for every §5/§10 family on the
137
+ [validator](capabilities/validator.md) (machine-readable, `check:`/`source:`),
138
+ eight [linter](capabilities/linter.md) categories with pinned severities, an
139
+ explicit clock, and the two Migration findings that tell a v0.1 bundle what
140
+ to change without ever failing it. The surfaces speak it too:
141
+ [trust/status columns and filters](capabilities/read-views.md) on the CLI,
142
+ [source text in search](capabilities/search.md) where the body used to hold
143
+ it, and the trust line as the [graph page](capabilities/graph-server.md)'s
144
+ third channel. This bundle migrated in the same change — `generated` replaces
145
+ `timestamp`, `sources` (`@okf-eco format/citations`) replace the `# Citations`
146
+ sections — and the lessons live in the concepts each is about.
147
+
148
+
149
+ ## 2026-08-07
150
+
151
+ * **Release**: **okf-mcp 1.0.0** shipped — the MCP server (`@okf-mcp design/the-tool-set`),
152
+ a fourth surface beside the [CLI](cli.md), the
153
+ [graph server](capabilities/graph-server.md) and the
154
+ [library API](capabilities/library-api.md): any MCP-capable host reads these
155
+ bundles through ten read-only tools, concepts as resources, and the two
156
+ consuming prompts, all on the [registry's](registry.md) identity. What it is
157
+ and every constraint that shaped it live in the concept, and the pre-release
158
+ reviews that hardened it are recorded there as the rules they taught — not the
159
+ rounds they took. It rides **okf 1.13.0**, whose gemspec floor now names the
160
+ kernel release shipping every API the shell calls (`Bundle#directories`).
161
+ * **Fix**: okf 1.13.0 closes a read-containment gap in the kernel. A bundle file
162
+ that was a symlink pointing outside the root was followed on read — the guard
163
+ was lexical (`File.expand_path` resolves a name, not a link), so a concept or
164
+ an `index.md` symlinked out of the bundle served its target verbatim, over the
165
+ [graph server](capabilities/graph-server.md), `okf render`, and — the reason it
166
+ finally mattered — okf-mcp (`@okf-mcp design/the-tool-set`)'s `--http`. Every read
167
+ now resolves the real path and refuses a target outside the root; the rule and
168
+ its boundary are in [the server trust boundary](design/server-trust-boundary.md).
169
+
170
+
171
+ ## 2026-07-24
172
+
173
+ * **Change**: the repository became a **monorepo**, and this bundle acquired a subject it did not have before — the repository itself, alongside the gem it has always described. (Only that: [the index](index.md) and [overview](overview.md) still read as the gem's, correctly, because the gem is still all there is to document. The reframing comes when a sibling does.) The layout (`@okf-eco decisions/monorepo-layout`) is the new concept: one directory per gem named for the gem it ships (`okf/` is the baseline all-in-one; `okf-mcp/`, `okf-tui/`, `okf-sqlite3/` land beside it), so a directory, its release-tag prefix, its CI job and its `require` path are one word rather than four mappings. Everything that is not a gem stays at the root — `plugin/` and `.claude-plugin/` because `marketplace.json` publishes `./plugin`, this bundle because it covers the project, and the `Dockerfile` because its build context *must* be the root: the gemspec derives `spec.files` from `git ls-files`, which needs the `.git` only the root has. Every `resource:` and citation here moved down a level with the code.
174
+ * **Note**: moving a gem down one level is mechanical; what is not is that **four mechanisms around it resolved paths from the repository root, and three of them failed without saying so**. `spec.files` needed nothing — `git ls-files` with `chdir:` returns paths relative to where it runs, so the gemspec sees its own tree and its reject list *shrank* from fourteen prefixes to six, the eight removed having been rejecting paths that are no longer under the gem. `.gitignore` broke where it was anchored — sixteen of its nineteen entries carry a leading `/`, and all sixteen stopped matching at once, so the first test run would have staged a coverage report; the unanchored `*.gem` and `Gemfile.lock` kept working, which is what made the breakage partial and easy to miss. SimpleCov failed in the direction that looks like success: its root defaults to the working directory, so the plugin's curation hook — a repo-level file this suite tests — fell out of the report and line coverage read **98.63% against 98.47%**, the percentage rising while the thing measured got smaller. Only `.dockerignore` fails loudly, and it is the one carrying a real invariant: whatever it drops from under the gem must also be in the gemspec's reject list, because `git ls-files` reads the *index* and an excluded path is still listed in `spec.files` — so `gem build` fails on a file that is not in the context. The generalizable half: **a path resolved from an implicit root is a dependency on where you are standing**, and the ones that degrade quietly are worse than the ones that crash.
175
+ * **Note**: the gem must distribute `LICENSE.txt` and `NOTICE`, and `git ls-files` from the gem directory cannot see the root's copies. **A symlink builds a gem that either refuses to install or installs broken, depending on whose RubyGems does it.** `gem build` does not resolve the link — it writes a symlink into the package tar, warns (`LICENSE.txt is a symlink, which is not supported on all platforms`) and succeeds. RubyGems **>= 3.2** then refuses to extract one pointing outside the gem (`Gem::Package::SymlinkError`); RubyGems **< 3.2** has no guard, and measured on Ruby 2.7 / RubyGems 3.1.6 — inside this gem's supported range — `gem install` exits **0** and lays down a dangling `LICENSE.txt`. The older half is the worse one, against the intuition that an old installer is merely stricter or looser: there the gem installs cleanly and simply carries no licence. They are duplicated real files now, with `okf/test/unit/packaging_test.rb` asserting both that neither is a symlink and that each is byte-identical to the root's — the assertion being what makes a duplicate safe rather than merely conventional. Found by building the thing and installing it instead of reasoning about it, which is the same lesson the recall probes taught from the other end.
176
+ * **Sync**: caught the bundle up with **project-local registries** — the
177
+ [registry](registry.md) now has two homes, and which one answers is decided by
178
+ where you stand: `okf registry init` drops a `.okf-registry.json` that okf
179
+ discovers by walking up from the working directory and uses in place of the
180
+ global `$OKF_HOME` one while you are inside its tree (the file's presence is the
181
+ whole state, the nearest one on the path wins, and `OKF_NO_DISCOVERY=1` forces
182
+ the global one for a fixed-cwd caller). A bundle inside the tree is stored
183
+ **relative** to the file, so a committed registry travels with the repo — a
184
+ checkout elsewhere, or a container mounting it, resolves the same bundles
185
+ unchanged — while paths still read back absolute everywhere the CLI reports
186
+ them. The [CLI](cli.md) records the grown subcommand list (`init` through
187
+ `group`/`ungroup`, correcting a groups-era omission) and that `@slug` now
188
+ resolves through a discovered local registry before falling back to `$OKF_HOME`.
189
+ * **Sync**: the derived `area` field is renamed **`top_dir`** — the
190
+ first-path-segment rollup the [read views](capabilities/read-views.md) and
191
+ [search facade](design/search-engines.md) carry. "Area" was never the OKF
192
+ spec's word (the earlier `dir` rename already established that); the rollup now
193
+ names itself in the spec's vocabulary, the `dir` at the top level. The `--json`
194
+ keys move with it (`catalog`/`search` rows carry `top_dir`, `stats` emits
195
+ `top_dirs`/`by_top_dir`, `graph --hubs` emits `top_dir`/`by_top_dir`). The
196
+ deprecated `--area`/`--by area` *input* flags are untouched — they still warn
197
+ and map to `--dir`/`--by dir`, and now source the renamed field internally.
198
+
199
+
200
+ ## 2026-07-23
201
+
202
+ * **Addition**: the [registry](registry.md) grows **groups** — a slug that names
203
+ a set of bundles (members are bundle or group slugs, so groups nest) and
204
+ resolves recursively to its bundle leaves. `okf registry group backend @orders
205
+ @billing` / `ungroup` manage them; `@backend` then stands in for the set
206
+ wherever the two set-taking verbs run — [`search`](capabilities/search.md)
207
+ merges the members into one ranking, [`server`](capabilities/graph-server.md)
208
+ mounts each. Every single-bundle verb refuses a `@group` with exit 2, the same
209
+ second-bundle rule, and `@all` is unchanged (a group is a named subset of the
210
+ bundles it already covers). Groups live in their own list so the
211
+ first-is-default rule and the `File.directory?` guards never meet a pathless
212
+ entry; one namespace spans both, so a `rename` cascades the new name across
213
+ member lists and a `del` cascade-drops the slug, deleting any group it empties.
214
+ * **Addition**: [read-views](capabilities/read-views.md) gains `graph
215
+ --traffic`, and [agent-skill](capabilities/agent-skill.md)'s refine playbook
216
+ gains it as a third structural read. The playbook's step 2 measured concepts
217
+ twice — tag locality and the hub origin test — while step 3 decides about
218
+ *directories* ("does this directory prune?", "concern or container?"), and
219
+ nothing measured at that grain. `--traffic` collapses concepts into their
220
+ directory and the links between two directories into one weighted arc, so each
221
+ row carries internal/out/in traffic and a cohesion ratio: cohesion versus
222
+ coupling, applied to a knowledge tree. On a 47-concept bundle it read three
223
+ findings off one screen — a directory with 50 outbound links and 4 inbound
224
+ behaving like a projection rather than a container, two directories at 0%
225
+ cohesion holding inert reference material, and a central `decisions/` reading
226
+ low for the reason the playbook already predicts, which is centrality rather
227
+ than mis-homing. The cut is fitted to the bundle rather than fixed: at a fixed
228
+ weight of 3, ten bundles ranged from 2 arcs to 136.
229
+ * **Update**: the graph page draws links in three amounts rather than always
230
+ all of them — every link, the *spine* (each concept's strongest, about one per
231
+ concept, and chosen so it touches every linked concept), or none, with a
232
+ selected concept's own links always shown in full. A dense bundle opens on its
233
+ spine by default rather than greeting the reader with the thicket: the trigger
234
+ is undirected degree, the measure the bundle above reads at 9.7, above a floor
235
+ set between that and a tree's ~2, so a browsable graph is left on every link.
236
+ `okf server --map` / `okf render --map` override that to no links with the
237
+ directories boxed. A dense bundle is unreadable because of its arrows, not its
238
+ dots: 227 links over 47 concepts at an average degree of 9.7, three quarters of
239
+ them crossing a directory boundary.
240
+ * **Maintenance**: the bundle catches up with both. [The model](model/) gains a
241
+ fourth member, [skeleton](model/skeleton.md) — the graph reduced to directories,
242
+ arcs and per-link cuts, which [`--traffic`](capabilities/read-views.md) and the
243
+ page's link layer both read. [graph-server](capabilities/graph-server.md) gains
244
+ the link layer, the dense-opens-on-spine default and `--map`;
245
+ [render](capabilities/render.md) gains the baked `--map` flag.
246
+
247
+
248
+ ## 2026-07-22
249
+
250
+ * **Correction**: [browser-tests](design/browser-tests.md) drops the CI job. The
251
+ concept argued a non-blocking check was worth its noise because a red job still
252
+ makes a regression visible; the measurement went the other way — 5 failures in
253
+ its last 7 runs while the Ruby matrix stayed green, nearly all of them the CDN
254
+ the page boots against rather than the page. The same sentence that justified
255
+ the job ("a check that cries wolf gets muted within a month") had come to
256
+ describe it. It also cost what the argument never priced: a ✗ on the
257
+ repository's front page reads as a broken gem, not as a slow jsdelivr. The
258
+ suite is a local obligation now, enforced by nothing, and the note records what
259
+ restoring it would take first — caching `vendor/` between runs so a cold runner
260
+ stops reaching for the CDN at all.
261
+ * **Update**: [graph-server](capabilities/graph-server.md) names a bundle by its
262
+ slug. Every row that offered a choice between bundles — the ⌘K switcher, the
263
+ Bundles panel, the hub's own `/b/` page — led with `Folder.label`, the derived
264
+ `parent/dir` string, and put the slug in muted grey beside it. That is the
265
+ address in the name's place: a bundle is addressed by `@okf-gem` and
266
+ `/b/okf-gem/`, and the folder is where it happens to sit. In a registry of
267
+ projects the label is also `…/.okf` on nearly every line, so the loudest column
268
+ repeated the one word that tells no two bundles apart. `Folder.label` now reads
269
+ a `.okf` directory as its parent (`repo/.okf` → `repo`), which fixes `okf
270
+ registry list` and the default server title too; the rows carry `@slug` as the
271
+ name, and the folder only where it is not the name repeated.
272
+ * **Correction**: the rail says **Index** while the root map is the open file.
273
+ Index is a shortcut into Files, so the two share one `data-view` — and
274
+ `activeRail()` read only that, which lit Files on the one screen a reader
275
+ reached by asking for Index. The open file is what tells them apart, so it is
276
+ what the rail reads.
277
+ * **Update**: [read-views](capabilities/read-views.md) records that `dirs` and
278
+ `stats` answer about the *same* directories. Both read `Bundle#directory_index`
279
+ now; grouping the catalog instead knew only the directories that happen to hold
280
+ a concept, so the two verbs disagreed about how big a bundle was and — worse —
281
+ `by_dir` omitted directories `--dir` answers about. A directory holding nothing
282
+ directly reports the zero it holds, which is what makes `by_dir.keys` a
283
+ complete list of what `--dir` can name. The same paragraph now covers `--area`
284
+ refusing `--dir` as well as `--depth`: one reason wearing two shapes, since the
285
+ deprecated flag is exact and both of the others select a range.
286
+ * **Update**: [graph-server](capabilities/graph-server.md) and
287
+ [library-api](capabilities/library-api.md) record who names the search
288
+ endpoint. The route answers on every app; *advertising* it is the caller's,
289
+ because the page resolves it against the reader's URL and only the host knows
290
+ its own prefix. A default of `"search"` — which this bundle briefly described —
291
+ would have pointed an app mounted at `/knowledge` back at its host's root. The
292
+ correction is worth keeping visible: the bug was invisible from inside the app,
293
+ and only appears where the gem is a library rather than a command.
294
+ * **Correction**: the `/search` cap and engine live on
295
+ [graph-server](capabilities/graph-server.md)'s `App` alone. `Hub` kept its own
296
+ copies after the payload moved, so raising the cap in the obvious place would
297
+ have changed nothing — a constant duplicated with its reasoning intact is the
298
+ kind that drifts quietly.
299
+ * **Update**: [search](capabilities/search.md) and
300
+ [search-engines](design/search-engines.md) record the prepared corpus. The
301
+ server was rebuilding the whole index on every request — 1.45 s per search on a
302
+ 414-concept bundle, flat across repeats, because each one threw away what the
303
+ last had built. This bundle had already predicted it: the build is ~95% of the
304
+ index path's cost, and a long-lived server is exactly the case that amortizes
305
+ it where a one-shot CLI cannot. `Search.prepare` holds a corpus and
306
+ `Search.with` queries it, so a search is 0.016–0.052 s and the build lands at
307
+ boot. The engine opts in by exposing `prepare`; the scan declares none and is
308
+ handed none, so the seam cost no engine anything. The trade is staleness — a
309
+ corpus is a snapshot — and the hub drops its own on any registry write, which
310
+ is the bug this nearly shipped with rather than a precaution.
311
+ * **Update**: [graph-server](capabilities/graph-server.md) gains `/search` on the
312
+ single-bundle app and loses `f`. The route was hub-only because it was
313
+ conceived as the cross-bundle one, which left the mode most readers meet first
314
+ with a palette that could not find anything; one bundle is a legal one-element
315
+ set, so the assumption was the only obstacle. What followed is the interesting
316
+ half: a row with **no slug** is a shape three places had never seen, and each
317
+ read a missing slug as a *foreign* bundle — a `../undefined/` 404 on every
318
+ result, an "undefined" chip, and a full page reload to reach a node already on
319
+ screen. One absent field, three wrong answers, none of them where the change
320
+ was made.
321
+ * **Update**: [graph-server](capabilities/graph-server.md) records three canvas
322
+ behaviours a big bundle turns from cosmetic into structural — the force layouts
323
+ settling once instead of rendering every tick of the simulation, a cluster box
324
+ becoming scenery rather than the largest drag target on the page, and no form
325
+ control under 16px where iOS Safari would zoom the view and not zoom back.
326
+ * **Update**: [read-views](capabilities/read-views.md) records three ways `--dir`
327
+ could answer wrongly and exit 0: an ancestor chain handed back case-folded
328
+ (so every ancestor of a capitalised directory vanished from the chain the flag
329
+ exists to draw), a trailing slash refused (while the human views *print* one, so
330
+ pasting a row back returned nothing), and `--area` with `--depth` unioning
331
+ rather than narrowing. The shape they share is worth more than any of them:
332
+ each was a rule written against one spelling of a directory and exercised only
333
+ in that spelling.
334
+ * **Update**: [agent-skill](capabilities/agent-skill.md) converges on one first
335
+ move. The skill named three different ones across seven places, which is the
336
+ deliberation cost an agent pays on every retrieval — and the lookup table added
337
+ to remove that cost duplicated the reference, contradicted its own file, and
338
+ quoted measurements from a bundle no reader can open. Reverted, and the
339
+ disagreement fixed by subtraction instead: `okf dirs` first, everywhere.
340
+
341
+
342
+ ## 2026-07-21
343
+
344
+ * **Update**: [read-views](capabilities/read-views.md) gains `--depth` and the
345
+ `dirs` subtree count. Both came out of running the CLI against a bundle an
346
+ order of magnitude bigger than this one, which is the only way the gap shows:
347
+ every read view had a `--dir` and none had a way to ask for a *level*, so the
348
+ §6 map — one section per directory — was hundreds of KB with no lever but
349
+ naming each branch by hand. The subtree count is the half that is easy to miss:
350
+ depth alone truncates a deep tree into a column of zeroes, because direct
351
+ counts are honest and an intermediate directory holds nothing of its own. Two
352
+ numbers per row, the second defined as what `--dir` on that row returns, so the
353
+ view and the flag cannot drift apart.
354
+ * **Update**: [graph-server](capabilities/graph-server.md) records cluster mode
355
+ nesting. The same rename reaches the page: a cluster is a directory, the boxes
356
+ nest as the directories do to a depth the reader picks, and the filter chips
357
+ list every dir rather than first segments. Two bugs came out of it, both of the
358
+ shape this bundle keeps noting — a rule written for one level, exercised only
359
+ at one level. The empty-box pass read a compound's *children*, so an
360
+ intermediate box holding nothing but sub-boxes always read empty and took its
361
+ whole branch off the canvas; and fcose, handed a nested graph whose nodes went
362
+ `display:none` mid-animation, threw on a label it could no longer measure. The
363
+ second one is the more interesting: it was only reachable by typing during the
364
+ tiling, which is exactly the *other order* the phantom-box bug taught this
365
+ bundle to look for, one release ago, in this same function.
366
+ * **Update**: [read-views](capabilities/read-views.md), [search](capabilities/search.md)
367
+ and [graph](model/graph.md) now say `--dir` where they said `--area`, and
368
+ read-views gains the `dirs` verb. The rename is not cosmetic: "area" appears
369
+ nowhere in the OKF spec (`grep -ci area SPEC.md` → 0) — it was this gem's own
370
+ word for a concept id's *first path segment*, and that projection threw away
371
+ every level below it. The spec's word for grouping is *directories*, the value
372
+ the catalog already carried on every row, so `dir` becomes the only machine
373
+ word (full path, `.` at root, `(root)` for humans) and "cluster" stays prose
374
+ for what a dir groups. `--area` and `tags --by area` keep their old exact
375
+ behavior and warn, for one release.
376
+ * **Correction**: a maintain pass against `okf/CHANGELOG.md` found the drift running
377
+ the *other* way — the bundle was current and the changelog was not. Every
378
+ concept touched by this branch's server/page work had its body updated in the
379
+ same commit as the code (`bundles-manager`, `graph-server`,
380
+ `server-trust-boundary`, `registry` all speak `--read-only`, and a search for
381
+ `--allow-manage`/`--allow-edit` returns nothing), but `[Unreleased]` carried
382
+ none of it: the registry's browser surface and its four write gates,
383
+ cross-bundle search from the hub, the topbar box's count and bridge, the 404
384
+ rebuilt as a directory, the touch preview card, and additive type chips were
385
+ all shipped and undocumented. Written now, documenting the **net end state
386
+ only** — the flag is `--read-only` with management as the default, and the
387
+ intermediate `--allow-edit` → `--allow-manage` renames are deliberately not
388
+ named, because neither ever appeared in a release and a changelog that
389
+ describes states no user ever saw is a changelog nobody can use.
390
+ * **Update**: [browser-tests](design/browser-tests.md) gained the vendor cache's
391
+ bypass (`OKF_NO_VENDOR_CACHE=1`). The cache itself was already documented down
392
+ to the measurement that refuted its own rationale; the one lever a reader needs
393
+ to check the template's pins against the real CDN was the part missing.
394
+ * **Addition**: the skill gained a **`refine` verb** and the CLI its two
395
+ evidence views. `playbooks/refine.md` is the third authoring boundary —
396
+ `curate` keeps the structure sound, `maintain` keeps the content true,
397
+ `refine` changes where knowledge lives: the directory
398
+ tree as a lossy projection of the link graph, cohesion over balance, concerns
399
+ as tags never containers, free levers (heading sectioning, tag curation,
400
+ extraction) before file moves, and a propose-don't-apply contract (a report
401
+ plus a frozen execution prompt). The evidence is mechanical now: `okf tags
402
+ --by` rows carry each tag's `count/total` so locality (domain vs concern)
403
+ reads per row, and `okf graph --hubs` ranks inbound links grouped by source
404
+ area — the hub origin test. The [read views](capabilities/read-views.md) and
405
+ [agent skill](capabilities/agent-skill.md) concepts record both; the design
406
+ came from evaluating a field report on a 50-concept production bundle whose
407
+ restructuring had to be hand-derived.
408
+ * **Change**: the hub's 404 stopped leading with the apology. It is a directory
409
+ reached by a wrong turn, not an error page, so the **asked path is now the
410
+ heading** — mono, 27px, where a dropped slash reads as a shape — and "not
411
+ found" is the eyebrow above it. A reader arrives already knowing they are
412
+ lost; the URL bar told them. The near miss became a **row** instead of a
413
+ sentence, wearing the same anatomy as the list under it and already lit, with
414
+ `⏎` wired to it before a character is typed. Rows gained the folder, which is
415
+ the fact that actually distinguishes bundles on a real server (`site/.okf`,
416
+ `minifts/.okf`, `okf-core/.okf` are three titles that read alike). And colour
417
+ went back to marking exceptions only: a healthy row draws no verdict edge,
418
+ because six rules saying "nothing to report" is a page where the one that
419
+ matters cannot be found by looking.
420
+ * **Fix**: the 404's ↑↓ keys are gone, and moving through the list is Tab's job
421
+ again. The hand-rolled cursor was a second focus model living beside the real
422
+ one — it lit rows the browser did not consider focused, it was invisible to a
423
+ screen reader, and keeping the two in step is what left the near miss and a
424
+ list row highlighted at once, which made ↑↓ read as doing nothing at all. Every
425
+ row is an `<a href>`, a filtered-out row is `display:none` and leaves the tab
426
+ order on its own, and Shift-Tab goes back: all of it free, none of it ours to
427
+ maintain. What is left is one mark meaning "⏎ opens this", which never moves
428
+ and stands down the moment the caret leaves the box — past that point `⏎`
429
+ belongs to whatever Tab focused. `/` reaches the box from anywhere, the same
430
+ key the graph page binds, so a reader who tabbed into the list and changed
431
+ their mind does not have to tab back out of it.
432
+ * **Feature**: the 404's box escalates the way the graph page's does, through the
433
+ **same component**. A bundle list cannot answer "where is the thing about
434
+ decay?" — but the hub can, since `/search` reads inside every bundle it hosts.
435
+ So a query matching no *bundle* drops the graph page's own bridge panel under
436
+ the box — `No bundle matches "…"`, then `Search every bundle ⏎` and
437
+ `Clear esc` — rather than saying no twice or inventing a second dialect of one
438
+ idea two pages apart. The hits land under the list, each opening its concept in
439
+ place (`/b/<slug>/?select=<id>`).
440
+ * **Fix**: the 404's guess only ever looked at the slug the router parsed, so it
441
+ was silent on the commonest typo there is. `/bokf-tui/` is `/b/okf-tui/` minus
442
+ one slash, and the router — which only looks *under* the mount — hands back no
443
+ slug at all. The guess now falls back to the path's own first segment, whole
444
+ first so a bundle really named `borders` beats `orders` reached by eating the
445
+ mount letter. The dropped separator is named outright only on evidence with no
446
+ second reading; short of that the page teaches the URL shape rather than
447
+ guessing at the mistake.
448
+ * **Fix**: the graph's Filters panel had three chip groups and two grammars.
449
+ Areas and tags were additive — nothing selected means everything, a click
450
+ narrows, a second click undoes — while **types were subtractive**: every type
451
+ showed until you clicked one *away*. Same chip component, same panel, opposite
452
+ meaning, and the catalog's and tags' own type chips one view over were already
453
+ additive, so the odd one out was odd twice. Types now select like everything
454
+ else (`hiddenTypes` → `activeTypes`), which also means two types compound into
455
+ a union the way two tags do — something the old model could not express at all.
456
+ The change is a net deletion: `.chip.off` had no other user, and `chipRow`'s
457
+ per-group state-class argument was there only to tell types apart from the
458
+ rest. `typeFocused` collapses into the same shape as `tagFocused`, taking its
459
+ one-type-bundle special case with it.
460
+ * **Change**: the `/b/` manager's registry forms are gone, and the page stayed.
461
+ For a stretch both surfaces carried the same four verbs — the forms here and
462
+ the graph page's ⚙ Bundles panel — which is two implementations of one
463
+ contract and the shape that drifts. The panel wins because managing a set is
464
+ something you do while reading it, not on a detour to a page you had to know
465
+ existed. `/b/` keeps the jobs only it can do: the list, the redirect target
466
+ when no bundle is named, the way back from a 404, and the empty state a hub
467
+ with no bundles has no graph page to show. It now carries no forms, no script
468
+ and no token — a page with nothing to post has no business holding the
469
+ credential — and the four `POST /registry/<verb>` routes answer JSON to one
470
+ caller instead of two shapes to two.
471
+ * **Note**: `--allow-manage` is now **`--read-only`**, and the axis flipped. The
472
+ old flag read as the on switch and was not one — a loopback bind was already
473
+ writable, and all `--allow-manage` did was widen that to a bind nobody should
474
+ widen it to. So the opt-in is gone outright: a non-loopback bind is refused
475
+ with no flag that opens it, because the registry is a per-user file and the
476
+ machine that owns it is the machine that manages it. What is left is the way
477
+ *out*, named for the word the hub's own refusal already used. A flag that
478
+ cannot be misread as permission beats one that has to be read carefully.
479
+ * **Fix**: on a touch screen, tapping a concept destroyed the graph. At `≤768px`
480
+ the inspector is `grid-template-columns:0 1fr`, so a tap measured `#stage` at
481
+ **0 px wide** — the graph was not covered, it was gone, and exploring became
482
+ open → read → close → tap the next dot. A **preview card** now rises at the
483
+ bottom edge instead, carrying the concept's head over a graph that keeps every
484
+ pixel and stays live; drag it up for the neighbourhood and the body, tap a row
485
+ and it swaps in place while the camera walks. Two subtractions are the point:
486
+ the 0.26 s entrance is gone outright — the card takes exactly one transform
487
+ value for its whole life on screen — and a miss on bare canvas no longer
488
+ dismisses it, because the misses are constant at that size and each one made
489
+ the next dot replay the entrance. Together those two were the slideshow. The
490
+ camera aims at the visible band rather than the canvas centre, or the selected
491
+ node parks under the card describing it. The branch is wider than the chrome's
492
+ (`≤768px` **or** `≤1024px` portrait): a portrait tablet has the same bug and
493
+ wants the same gesture, while landscape at that size keeps the inspector.
494
+ Folder and index taps fill the card too — they used to write into an invisible
495
+ `#side-body`, so tree and cluster modes were silently dead on touch.
496
+ * **Feature**: the graph page stops hiding what it can do. The topbar box said
497
+ "search concepts…", *filtered* the current view, emptied the graph in silence
498
+ when nothing matched, and never mentioned the ⌘K palette that searches every
499
+ bundle. It now carries the chord as a chip, a live `7/8` count — so an empty
500
+ result is a number that reached zero rather than a view that went blank — and,
501
+ on zero, a panel naming the bundle and the query with the way on: `⏎` hands
502
+ the query to the palette prefilled and already searching. The escalation is
503
+ the TUI's own, arriving four surfaces late. It will fire rarely, and that is
504
+ the design working: the box's index reaches full bodies wherever the page
505
+ holds them, so most real words match *something* locally.
506
+ * **Feature**: the registry moved onto the page. A ⚙ in the rail opens a
507
+ **Bundles** slide-over — every registered bundle with its size, its health as
508
+ a word, the default marked and the one being read marked differently, and a
509
+ `⋯` per row carrying Make default, Rename… and Remove…. It reads a new
510
+ `GET /bundles` (the [manager](capabilities/bundles-manager.md)'s own rows, as
511
+ JSON) on every open rather than baking the list in, because the hub re-reads
512
+ the registry per request and a boot snapshot goes stale silently. The four
513
+ `POST` verbs gained a JSON rendering for it; every gate, status and sentence
514
+ is unchanged, and asking for JSON is not a way around any of them. No **Add**:
515
+ a browser cannot hand over a filesystem path, and registering is the agent's
516
+ act — the footer says so rather than leaving the absence to be noticed.
517
+ * **Fix**: a slide-over parked at `translateX(100%)` **still occupies layout**,
518
+ and `#views` did not clip — the Filters panel escapes this only because
519
+ `#stage` does. The closed panel widened the document by its own 340px, and
520
+ writing the spec found the half a prototype could not: it does the same *while
521
+ sliding*, so fixing only the closed state still flashes a horizontal scrollbar
522
+ on every open. `hidden` while closed plus `overflow:hidden` on `#views` settle
523
+ both, and the spec samples `scrollWidth` across the whole animation — measured
524
+ after it lands, the scrollbar has already gone. The hazard was latent for any
525
+ panel added outside `#stage`, so the clip is the fix that matters.
526
+ * **Fix**: the hub's 404 was a centred card in a chrome that existed nowhere
527
+ else in the product — the page a reader reaches by being wrong was also the
528
+ page telling them they had left. It is now the app shell with nothing to show:
529
+ the same rail, mark, theme toggle, topbar and row anatomy, plus the asked path
530
+ as a chip, a did-you-mean, and a filterable list. The guess is Levenshtein
531
+ with a shared-prefix shortcut, which is the part that earns its keep —
532
+ truncation is the commonest way a slug comes out wrong, and plain edit distance
533
+ scores `ord` three edits from `orders`. It is rendered in Ruby, not from an
534
+ inlined payload: this is where someone lands when something has already gone
535
+ wrong, and a page that needs JavaScript to say what happened has picked the
536
+ worst possible moment to need it.
537
+ * **Note**: "workspace" is retired. It appeared only in the server layer and in
538
+ this bundle, never in anything a user types — `registry` is the CLI verb
539
+ family, `$OKF_HOME`, `OKF::Registry`; **Bundles** is what the surface has
540
+ always called itself, in the TUI's view and in `/b/`'s own `<h1>`. It is also
541
+ the only word true in both modes, since the hub serves ephemeral sets with no
542
+ registry at all: a panel titled "Registry" would be lying half the time.
543
+ * **Feature**: the server reaches what the TUI reaches, through a browser. Three
544
+ pieces, shipped in order. **Cross-bundle search**: the hub answers
545
+ `GET /search?q=`, `Search.across` over every hosted bundle on one shared index,
546
+ and the ⌘K palette gains a **Concepts** group that fetches as you type. The
547
+ group comes last because it is the only one that arrives asynchronously, and a
548
+ group landing above the cursor moves the row under the reader's fingers between
549
+ the keystroke and the Enter. The engine is *named* `:index` rather than left to
550
+ route off `fuzzy: true` — that worked only because nothing else declares the
551
+ capability, and it is also the right engine here for a reason the CLI's default
552
+ does not share: a long-lived server amortizes an index build over every
553
+ keystroke, and the browser's own MiniSearch is a port of it, so a palette hit
554
+ and an in-page search rank alike. **The [bundles
555
+ manager](capabilities/bundles-manager.md)**: `/b/` went from a bare list to
556
+ the browser counterpart of the TUI's bundles view — size, health verdict,
557
+ default marker, and the entries the hub *cannot* host shown muted rather than
558
+ omitted, because leaving them off answers "where did my bundle go?" with
559
+ silence. **Registry writes**: four `POST` routes behind three gates
560
+ (loopback-or-`--allow-manage`, a registry to write to, same-origin plus a
561
+ per-boot token), each rebuilding the hub's served set from disk — a write that
562
+ leaves the running server on the old set is a lie the next click believes.
563
+ * **Note**: the manager page carries no script, deliberately. Rename and Remove
564
+ are `<details>` disclosures, Add is a text field for an absolute path — a
565
+ browser cannot hand over a filesystem path at all (the File System Access API
566
+ yields an opaque handle, and is Chromium-only), so there was never a picker to
567
+ choose over typing. The first browser run of the new specs paid for itself: an
568
+ HTML `pattern` whose character class is a valid Ruby regexp and invalid under
569
+ the `v` flag a browser compiles it with, throwing on every keystroke where no
570
+ integration assertion could see it.
571
+ * **Sync**: the browser suite now serves the page's CDN libraries from a local
572
+ read-through cache, and [browser tests](design/browser-tests.md) records both
573
+ what that buys and what it does not. It is keyed on the request URL rather
574
+ than a manifest of the versions the template pins, because a manifest is a
575
+ second copy of those pins that can drift into serving a library the page no
576
+ longer loads — the one failure a cache is most likely to hide. A warm run
577
+ touches no network (proven by making the fetch path throw and watching 64
578
+ cases still pass), but `vendor/` is build output, so CI still starts cold and
579
+ the `continue-on-error` rationale stands until the workflow restores it.
580
+
581
+
582
+ ## 2026-07-20
583
+
584
+ * **Fix**: the graph no longer collapses on return from another view — and the
585
+ cause was misdiagnosed for months. The [browser suite](design/browser-tests.md)
586
+ held it open as a `test.fixme` on the theory it was a resize race (setView's rAF
587
+ firing at 0×0, the ResizeObserver's 240ms debounce). Tracing it with the
588
+ browser tools — trapping every zoom change, then `cy.animate`'s caller — showed
589
+ the one animation that ran was a *fit*, not a resize: `fitGraph` reads the
590
+ container's own width, and the one-shot boot fit (`setTimeout(fitGraph, 400)`
591
+ after load) fires on whatever view is up by then. Leave the graph inside that
592
+ window and it fits a hidden 0×0 canvas, `(w-2*pad)/bb.w` goes negative, and the
593
+ zoom clamps to minZoom — staying there on return. Fixed by guarding `fitGraph`
594
+ to skip a zero-size canvas (the template already guarded the `?view=` deep-link
595
+ start for this exact reason, just not the navigate-away case). The `fixme` is
596
+ now a normal `views.spec.js` test that fires the hidden fit by hand and asserts
597
+ the zoom is untouched — deterministic in both modes, red before the guard, green
598
+ after. The lesson: the old repro's load-sensitivity (deterministic alone, flaky
599
+ under parallel workers) was not noise to route around with a `fixme` — it was
600
+ the symptom of a timer racing boot. Under load, boot ran past 400ms and the fit
601
+ landed while the graph was still visible, so it fit correctly and the bug
602
+ "vanished." Reading the actual animation, not the plausible mechanism, found it.
603
+ * **Addition**: [browser tests](design/browser-tests.md) — the graph page is now
604
+ driven in real Chromium, every spec in both render modes, with any thrown error
605
+ failing the run. Two findings worth more than the suite itself. The page has a
606
+ real bug: dwell ~300ms on another view, return to Graph, and the graph redraws
607
+ at a tenth of its size; both resize paths run and neither is sufficient. And
608
+ the first version of the test for it **could not fail** — it read `cy.width()`,
609
+ which reports the live container while the render is collapsed, so it passed
610
+ with every resize path deleted. A test that cannot fail is worse than none,
611
+ because it is counted; mutation-check a new spec or it is not proven.
612
+
613
+
614
+ ## 2026-07-19
615
+
616
+ * **Change**: `okf skill <a> <b>` **installed into `<a>`, ignored `<b>` and
617
+ exited 0.** Every `<dir>` verb refuses a trailing argument through
618
+ `positional_dir`, but `skill` takes a *destination*, not a bundle, so it read
619
+ as outside the rule and hand-rolled its own `argv.shift` — the one verb with a
620
+ positional that never met the guard. It goes through the shared `positional` +
621
+ `no_extras?` pair now and exits 2 before writing anything, and [cli](cli.md)'s
622
+ exit-code section states the rule as being about the **positional** rather
623
+ than about a second *bundle*, since the narrow phrasing is what made the verb
624
+ look exempt. Predates the registry work — a straight carry-over from the
625
+ monolith, found by reviewing the moved code rather than the diff.
626
+ * **Update**: the **shipped skill** learned that the verb list is open. Its
627
+ [cli reference](https://github.com/serradura/okf/blob/main/gems/okf/lib/okf/skill/reference/cli.md)
628
+ and `SKILL.md`'s verb row now say an installed extension adds verbs of its
629
+ own, so a verb `okf help` shows and the reference does not document reads as
630
+ **normal rather than a documentation error**. Worth doing because the skill is
631
+ how an agent learns this surface, and an agent that treats an unknown verb as
632
+ a doc bug will go looking for the bug. `rake plugin:sync` run, so the
633
+ generated copy does not drift.
634
+ * **Change**: the CLI became a **registry**, and with it an extension point — the new extension points (`@okf-eco design/extension-points`) concept, with [cli](cli.md)'s dispatch section rewritten around it. `lib/okf/cli.rb` was 1,794 lines and a 15-arm `case`; the verbs now live one per file under `lib/okf/cli/`, each a `Command` subclass registering itself at load, with the shared surface (refs, flags, the JSON emitters, the printers) on a base class. Behaviour is unchanged and the suite says so: every existing test passed untouched. Any gem shipping `okf/plugin.rb` on its load path can now add a verb — `okf-tui` is the first, answering `okf tui` — with **no edit to this gem and no list of known addons**, which a test enforces by grepping `cli.rb` for their names. `Search.register` set the idiom and this copies it exactly: append-only, idempotent by id, duck type checked at registration, so an addon cannot displace a built-in.
635
+ * **Note**: discovery is **lazy**, and the arithmetic is the one that made the scan the default engine. `Gem.find_latest_files` costs ~11ms on the 2.4 floor — small, and still not worth paying on a run that only wanted `okf lint`, so a built-in resolves and dispatches without scanning at all. Only an unknown verb and `okf help` pay. An unplanned consequence, worth keeping: an addon claiming a built-in's verb is not merely refused, it is never loaded, because running that verb never triggers the scan.
636
+ * **Note**: discovery is a **code-execution** decision, so the trust boundary is
637
+ argued in extension-points (`@okf-eco design/extension-points`) rather than assumed.
638
+ The usual principle — Ruby trusts `gem install`, not `require` — is not the
639
+ whole truth: it holds for native extensions, which run `extconf.rb` at install,
640
+ and **not** for pure-Ruby gems, which execute nothing until something requires
641
+ them. So a convention loader does escalate, narrowly, for a pure-Ruby gem that
642
+ is installed but never required. Bounded two ways: under Bundler the scan is
643
+ bundle-scoped (the Gemfile is already an allowlist, measured — a sibling
644
+ okf-tui checkout absent from the Gemfile is not found), and only `okf-*` gems
645
+ are loaded, so a transitive dependency shipping `okf/plugin.rb` is discovered,
646
+ skipped and reported. Resolving the owning gem's name reads `full_gem_path`
647
+ and never loads the file; a test pins that. An `$OKF_HOME` allowlist was
648
+ **considered and rejected as disproportionate** — the window it closes is one
649
+ the user opened with `gem install`, and it would cost the property that makes
650
+ the seam worth having.
651
+ * **Note**: **Thor was rejected, and the floor decided it** — Thor 1.3+ requires Ruby >= 2.6 against this gem's [2.4](design/ruby-floor.md), and only the EOL 1.2.2 accepts it, which is the pin-an-old-line-for-everyone mistake already recorded. It would also be a fourth [runtime dependency](design/runtime-dependencies.md) buying little: what was needed was a registry, not an option-parsing DSL, and `optparse` plus this gem's help/exit-code/stream-injection contract were already in hand. What kamal's Thor layout *did* supply is structure — a base class holding the shared surface, one file per command, privacy as the command boundary. Ideas are free.
652
+ * **Correction**: the plugin test seam was **wrong in a way only a real gem could show**. `reset_plugins!` cleared the registry but left the file in `$LOADED_FEATURES`, and `require` is idempotent — so the next scan found the plugin, required it, got `false`, and registered nothing; the verb was gone until the process restarted. Every test hid it by writing its plugin to a fresh `Dir.mktmpdir`, so the path differed on every load and `require` always ran. It surfaced the moment the seam was pointed at `okf-tui`, whose `lib/` path does not move. The lesson is narrower than "test with real things" and worth stating: **a fixture that varies where the real thing is fixed can hide a bug in exactly the dimension it varies.**
653
+ * **Correction**: [search](capabilities/search.md) listed **prefix matching** among what the index buys, and it buys nothing. A substring match already reaches every prefix — `dedup` finds `deduplication` under either engine, while `duplication` and `uplicat` find it under the scan alone — so `prefix` is what a token index needs to *catch up* to raw text, not a capability it adds on top. The claim was written the same day the scan became the default, into the very section arguing when to reach for the index, and it survived because "prefix matching" reads like a feature the alternative lacks. It was caught only by building the [skill](capabilities/agent-skill.md)'s engine-selection table and running each row instead of restating it: a table forces a claim per cell, and a cell is small enough to test. The advantages are now stated as a closed list of **three** — relevance ranking, typo tolerance, page parity — because a numbered list resists growing by plausible-sounding items in a way an open one does not.
654
+ * **Change**: the default search engine became the **scan**, and the index became opt-in (`--engine index`, or `--fuzzy`, which routes there). [search](capabilities/search.md) and [the engine contract](design/search-engines.md) both invert around it. The argument is a lifecycle one the earlier benchmark had already named but not acted on: a CLI process builds an index, asks one question and exits, so the build amortizes over nothing — measured end to end, **3.00 s against 0.24 s at 1,000 concepts**, 0.83 s against 0.18 s at 250, with the build ~95% of the index path at every size. Recall settled it rather than speed: raw-text matching has no tokenizer, so it has none of the tokenizer's holes, and the code-span loss recorded below stops being something a plain `okf search` pays. What the index buys — BM25+ ranking, prefix matching, `--fuzzy`, and rank parity with the browser page — is now reached by asking for it, which makes that parity **conditional** where the constraint in `AGENTS.md` had stated it flatly. The change costs one constant: capability routing already picks the first available engine offering what the query requires, so `--fuzzy` finds the index by itself and `-e` stops moving anything, since the default now offers `:regexp` outright.
655
+
656
+
657
+ ## 2026-07-18
658
+
659
+ * **Change**: `okf search` gained **`--engine NAME`**, and with it the answer to a question the capability flags structurally could not ask. A flag routes by what a query *requires* — `-e` needs `:regexp`, `--fuzzy` needs `:fuzzy` — but raw-text matching requires nothing, so nothing selected it. `--engine scan` names it directly: pre-index behaviour, phrases and infixes and dotted identifiers and code spans all matching, paying the coarser ranking for it. The design decision underneath is that the engine chooses **where** to match (tokens or raw text) and `-e` chooses **how** (literal or pattern) — previously the scan compiled every term as a regexp, invisible while `-e` was its only door, but wrong the moment `--engine scan` opened another: `7.2.0` would have matched `7x2y0` and `review (pending` would have been exit 2. Terms are escaped now unless `--regexp` says otherwise. A named engine that cannot do what was also asked is refused rather than falling back — falling back answers a different question than the one posed — and the refusal names an engine that can. This also makes the capability router's error path reachable from the CLI for the first time; it was previously dead code awaiting an addon.
660
+ * **Correction**: [search](capabilities/search.md) missed the **largest** recall loss of the index swap. A backtick is Unicode `Sk`, not `\p{P}`, so MiniSearch's tokenizer never splits it off and a word inside a code span indexes as one glued token: `` `minifts` `` is not found by the query `minifts`. In this bundle that is **409 distinct tokens over 1,013 occurrences**, and `okf search .okf minifts` answers 2 where the scan answers 5. The original swap notes recorded exactly one loss — the infix `ustomer` — because the analysis reasoned about the tokenizer instead of running queries against the corpus. The lesson generalizes past this bug: the conformance suite asserts that engines agree with each other *structurally* and cannot detect that both are missing a third of the matches. The tests that found real defects here — the losses pinning, the tokenizer spike — were the ones that ran real queries against real content.
661
+ * **Correction**: BM25 ranking does not contain the index's precision loss — it can invert it. Ranking normalizes by field length, so a short body dense in `7`, `2` and `0` outscores the concept that actually says `7.2.0`: on this bundle `okf search .okf 7.2.0` ranks [the Ruby floor](design/ruby-floor.md), a page full of `2.4` and `3.x`, **above** [the graph server](capabilities/graph-server.md), the one concept naming the version. So the loss is not merely extra rows below a correct top hit — the true hit can be buried, which is what makes `-e` (raw text, no tokenizer) the reason the tradeoff is acceptable at all rather than a nicety.
662
+ * **Change**: search became a facade over **N engines** chosen by capability, replacing the `regexp ? scan : index` ternary that could not hold a third. [The engine contract](design/search-engines.md) is the new concept: the facade owns everything that defines a result (documents, the row and its key order, the snippet, the sort), and an engine answers only which documents match, how well, and where. Selection is by what the query *needs* — `-e` requires `:regexp`, `--fuzzy` requires `:fuzzy` — so there is no `--engine` flag and no second vocabulary to keep consistent with the first. Routing is silent by decision: no note, no header change, no JSON key, because someone who typed `-e` does not need to be told what `-e` does on every run. The consequence is that `okf search --help` became load-bearing rather than decorative — it is now the only surface where the scan's existence is discoverable — and an integration test pins both the attribution and the silence, since a well-meaning `note: using the scan engine` is exactly the kind of thing that drifts back in.
663
+ * **Correction**: the oracle rule the roadmap carried for addon search — backends must return "the same match set, modulo ranking order" as the kernel — is retired. It was a rule about one implementation wearing two hats, and the moment two engines legitimately disagree (a phrase, an infix, a dotted identifier) it makes one of them a bug by definition. Replaced by a shared **conformance suite**: what every engine must do regardless of engine, plus capability-gated blocks for what only some can. A registered engine with no conformance class is a failing test.
664
+ * **Sync**: the CLI's search stopped being a linear scan and became a full-text index, so the browser and the CLI now run **one engine**. [`minifts`](design/runtime-dependencies.md) — the pure-Ruby port of the MiniSearch build the page already loads — is the gem's third runtime dependency, admitted because it costs the footprint nothing the first two were chosen to protect: no native extension, no dependency subtree, the same Ruby 2.4 floor (verified on 2.4.10), and it is precisely what defers SQLite + FTS5. [search](capabilities/search.md) records what changed underneath: terms are **tokens** matched whole or by prefix rather than substrings, ranking is BM25+ with the old field weights riding as boost, `--fuzzy` opts into the browser's typo tolerance, and `-e` stays a linear scan because a pattern is the one query an inverted index cannot answer — so `-e --fuzzy` is a usage error rather than a silently dropped flag. The row still carries which fields hit, read off the index's own per-term record: ranking improved and nothing was traded for it. The [gap the sync below opened](capabilities/graph-server.md) — CLI substring versus browser fuzzy — is closed from the CLI side, one release after it was named.
665
+ * **Correction**: the cross-bundle merge's justification died in the swap, silently. [search](capabilities/search.md) had argued that merging rankings was "legitimate because scores are absolute term weights, **not per-bundle normalized**" — and BM25 is nothing *but* per-corpus normalization, weighing every term by how rare it is. Searching each bundle separately and interleaving the results would have produced a list that looked sorted and compared nothing, with no error, no failing test, and no visible symptom beyond rows in a slightly wrong order. The fix is structural rather than a caveat: the searched bundles are indexed as **one corpus**, so the ranking is comparable by construction, and the observable proof is now a test — the same concept scores *lower* merged than alone, because the term got commoner. The lesson generalizes past this bug: a claim can be load-bearing for code it never names. That sentence was the merge's whole warrant, it lived in a *capability* doc while the change was to an *engine*, and swapping the engine falsified the warrant without touching the merge.
666
+ * **Correction**: the benchmark that motivated the swap measured the wrong lifecycle, and the bundle now says so where a maintainer will meet it. `minifts` sustains ~44–56× the scan's query throughput — true, and the right number for a browser or a server holding an index across many queries. `okf search` is a process that builds an index, asks **one** question, and exits, so it pays the build and amortizes it over nothing: 55 ms against the scan's 2.4 ms on this bundle, 2.2 s against 103 ms at a thousand concepts. Invisible at the size real bundles are today, a 20× regression at scale. Recorded in [search](capabilities/search.md) as the current ceiling with the cached prebuilt index named as what collects the throughput, because the honest version of "we made search 50× faster" is that we made it 50× faster *per query* and the CLI does not yet run enough queries to notice. A benchmark's unit of work has to match the caller's lifecycle, or it measures something real that nobody experiences.
667
+ * **Sync**: the graph page's search box became a full-text index, and the interaction bugs it exposed were fixed. [MiniSearch](capabilities/graph-server.md) — lazy-loaded, pinned to the `7.2.0` build the Ruby `minisearch` port tracks so a Ruby-built index and the browser's rank identically — now backs the graph, catalog, files and Indexes views: ranked, multi-term, prefix and typo-tolerant over title, id, type, tags and *description*, plus bodies wherever the page already holds them, which is why [`okf render`](capabilities/render.md)'s baked file searches bodies offline while the live server's index stays metadata-only until a backend body index exists. The graph could not be searched by a leaf's description at all before. It also splits the bundle's search story in two on purpose, which [search](capabilities/search.md) now records: the CLI stays deterministic substring so a row can say which field hit, the browser goes fuzzy because a human scanning a graph wants the near miss, and the shared build is the road back to one engine. The bugs were all *filter-shaped*. Cluster mode draws a box per area and never re-applied the active filter when you clustered, so entering cluster mode with a search on left phantom empty rectangles — and the tell was that type and tag filters, which are applied *while already clustered*, never did it. The file tree force-expanded every folder whenever a search was active, so its fold clicks were dead. And a dense graph leaves almost no empty canvas to click, so deselecting got a key. The lesson is what the phantom boxes taught: the filter pass was correct, and provable — the defect lived in the *other order*, where clustering built the boxes after the filter had already run, and only one of the two orders had ever been exercised.
668
+ * **Sync**: the graph renderer moved out of the server, and five concepts caught up. `OKF::Server::Graph` became [`OKF::Render::Graph`](capabilities/render.md) — the view layer paired with the pure [graph model](model/graph.md) — and the static bake left the Rack app: what was `App#render_static` is now `Render::Graph.static`/`.payload`, so [`okf render`](capabilities/render.md) no longer instantiates a server to write a file. The [graph server](capabilities/graph-server.md) keeps only the HTTP concern, its `GET /` delegating to the renderer, and both draw from one [`OKF::Bundle::Folder`](capabilities/library-api.md) — the `log_entries` builder moved there too — so the baked payload and the live endpoints derive from the same source and cannot drift. The drift the move left behind was pure address-rot: [render](capabilities/render.md)'s `resource` and citation, the [trust boundary](design/server-trust-boundary.md)'s template path, the [library API](capabilities/library-api.md)'s `render_static` surface, the graph server's own citation still crediting the app with a method it no longer holds, and the [core/shell split](design/core-shell-split.md)'s shell diagram, which had no renderer node though `boundary_test`'s denylist had reserved `Render::` all along. Behavior did not change — the render output is byte-identical, which is why nothing mechanical could have caught this: a moved file leaves every sentence reading true while its citations point where the code used to be.
669
+
670
+
671
+ ## 2026-07-17
672
+
673
+ * **Creation**: [static render](capabilities/render.md) became its own capability — the seventh — extracted from the [graph server](capabilities/graph-server.md) it had lived inside as a section. The signal that it wanted its own file was that the rest of the bundle already reached for it: the [trust boundary](design/server-trust-boundary.md) linked `okf render` by name, [integration first](design/integration-first.md) told its `render -o` exit-code story — and a heading others cite on its own is a concept wearing a section's clothes. It answers a question the live server does not: not "can I explore it" but "can I ship it where nothing runs," the same page with `EMBED` swapping live endpoints for an inlined payload, one template and no second renderer to keep in sync. The section that held it is now a pointer, so the shared page-and-template story stays in one place while render owns the export-specific half — the weight it trades for needing no server, and the flags baked in because a static file cannot be told them later. The [overview](overview.md), both indexes, and the [CLI](cli.md)'s verb table read seven now. The lesson is the atomic-concept one from the other side: a capability documented only as a subsection of its sibling is reachable by whoever already knows to look inside the sibling, and by no one else.
674
+ * **Sync**: the user-facing name for one registered bundle became a single token, `@slug`, and the bundle followed the README and [skill](capabilities/agent-skill.md) in adopting it. Every command banner and the `okf help` map now read `<dir|@slug>` in one spelling, with a note under the map defining the token, where the grammar used to set `slug` beside `@ref` and explain the pair once — in prose at the foot of `okf help`, past where a reader who already knows the verb ever looks; [cli.md](cli.md), the [registry](registry.md), and [search](capabilities/search.md) drop `@ref` for the single-bundle form to match. What stays is the tell: "ref" survives only where flattening it to `@slug` would be false — `@all` is still *a ref, not a flag*, and the one `resolve_ref` seam resolves slug, bare `@`, and `@all` alike. Retiring a piece of jargon is not erasing the mechanism it overnamed: the umbrella was real, and only its use as the everyday word for one bundle was the confusion.
675
+ * **Sync**: [`--help`](cli.md) stopped being the one command surface the injected streams could not reach. It had been OptionParser's officious handler, which prints to the process's own stdout and ends in `exit` — so the first test to ask any command for help took the whole runner down with it, mid-run and green, which is why in this suite no test ever had. Each parser now writes its banner to the injected `out:` and throws `:help` back to `run`, which returns the caught status like every other path, and the [integration base](design/integration-first.md) flunks with the offending argv if any path calls `exit` instead. The [CLI](cli.md) concept already claimed the whole surface was driven in tests; `--help` was the clause that made it not quite true, and closing it is the difference between a property asserted and one that merely holds until someone tests the exception.
676
+ * **Correction**: the [registry](registry.md) normalized a slug on the two paths that write one and not on the third that reads one, and that single asymmetry paid out three ways. A hand-typed `"slug": "My Docs"` listed while `@my-docs`, `rename`, and `default` all missed it — the verbs that could repair the entry were the ones that could not see it, the same dead end the reserved `all` row had, one step wider. It was also the [graph server](capabilities/graph-server.md)'s XSS trigger: slugs reach the bundle switcher's HTML, its JS escape covered `& < >` but not quotes, and an un-normalized read was the only way a quote could ever reach a slug. And `registry del ./notes` fell through the same normalization to delete an entry pointing somewhere else entirely, reporting success. One rule missing from one path, three unrelated-looking bugs — worth remembering when the next one looks local. (The escape was hardened anyway: a page whose safety rests on a guarantee three layers away is not one you can reason about where you read it.)
677
+ * **Correction**: the reserved-slug fix recorded below overcorrected within a day. Refusing a registry file whose row claimed `all` treated an unusable *name* as a malformed *file* — so one legacy row took every healthy entry down with it, and `registry del`/`rename`, the two verbs that could fix it, died on the very read they needed to survive. Hand-editing JSON was the only way out of a state the gem itself could produce: every release before the reservation slugged a directory named `all/` exactly that. The [registry](registry.md) now mints around it on read, as it already does when registering `all/` — the entry answers to `all-2`. Two lessons, and the second is the one worth keeping. A guard is only as good as its failure mode: this one made the disease (one unnameable entry) better and the cure (an unreadable registry) catastrophic. And a reservation is a *migration*, not just a rule — the moment a name becomes illegal, some file already on disk is holding it, and the rule has to say what happens to that file. This one said "reject", which is the one answer that cannot be applied to a file you have already shipped.
678
+ * **Correction**: §9's best-effort read had a hole where the errno gets in. `bundle.unparseable` tolerates a file whose *frontmatter* will not parse, but a file that will not **open** threw `Errno::EACCES` straight out of `Bundle::Reader` — and the read is the one path every verb shares, so one locked file broke `lint`, `validate`, `catalog`, `server`, and `registry set` alike, as a backtrace under exit 1 (which the [CLI](cli.md)'s contract spends for "non-conformant bundle", a claim nothing had established). "One bad file never breaks the rest" was a promise the [model](model/bundle.md) made and one layer did not keep. It joins the same bucket now, reported under §9.1 with the file and the errno. The shape is worth naming: the tolerance was written against the failure that was *interesting* (malformed authoring) and not the one that was *boring* (a file mode), and boring failures are exactly what a shared read path meets in the wild. Found while chasing something else — the [hub](capabilities/graph-server.md) dropped a bundle the listing starred, and the reason the hub dropped it was this.
679
+ * **Sync**: [integration first](design/integration-first.md) gained the rule that orders the work — a change earns a *red* integration test before it earns a patch, and the failure has to be read rather than merely seen, since one that fails on a missing fixture proves nothing about the bug. The two corrections below were fixed that way and the concept cites the first: the red run printed both halves of the disagreement at once, `/` redirecting to one bundle while the listing starred another. Written afterwards a test can only certify the code it was read off; written alongside, a bug and its test come to agree with each other — the same "green suite certifies a bug" the concept already warned about, reached from the other side. Recorded in AGENTS.md too, which is what `.claude/CLAUDE.md` loads.
680
+ * **Correction**: making the default a *position* left two derivations of it, and they disagreed. [`registry list`](cli.md) computed the star from raw order while the [hub](capabilities/graph-server.md) computed it from the bundles it could actually load — so a registry whose first entry had vanished starred `gone (missing)` while `/` redirected to the next bundle. Both readings were defensible and one had to be wrong: the star means "the bundle a bare `okf server` opens", so it must skip exactly what the hub skips, and the [registry](registry.md)'s default is now the first entry *still on disk*. Its mirror fell out of the same rule — `registry default <slug>` must refuse a vanished directory, or the move would answer with a slug the user never typed. The mechanism is worth more than the bug: the [old design stored the default](registry.md), so both readers read one field and could not drift; deriving it made the field free and the *agreement* the thing to maintain. A derived value with two derivations is a foreign key wearing a disguise.
681
+ * **Sync**: three simplifications landed before the registry ever shipped, and the bundle records the design rather than the removals. `$OKF_HOME` is the [CLI](cli.md)'s single lever on which registry a verb reads — the `--home` flag it replaces had to be remembered on the three verbs that offered it and forgotten on the eleven that did not, to name a location the env var already named. The [registry](registry.md)'s default became a **position**: the first entry is the default and `registry default <slug>` moves it there, because a stored slug is a foreign key into the same list it lives in, and every operation owed it referential integrity — carry it through a rename, re-point it after `add --as`, clear it on a remove, fall back when it dangled anyway. Position owes nothing, and a dangling default is now unrepresentable rather than handled. And "every registered bundle" became the ref `@all` instead of a `--all` flag: the flag *reinterpreted the positionals* (`search .okf home` read `.okf` as the bundle, `search --all .okf` as a term), so [search](capabilities/search.md)'s diagnostics existed only to explain the flip. As a ref there is one grammar — slot 1 is always a bundle identity — and both diagnostics are gone. `all` is reserved as a slug, which is the registry's own "may invent a name, never substitute one you chose" rule reaching one name further.
682
+ * **Sync**: the gem's testing rule became a design constraint, so the bundle records it — a new [integration first](design/integration-first.md) concept: the CLI is the product, so the suite that drives it end to end outranks the unit tests; the folders under `test/integration/cli/` are the three ways a user names a bundle (by path, by ref, several at once) with one file per command *and* subcommand; `rake test:integration` measures the layer alone because the full suite's number flatters (unit tests reach code no user can), which turns coverage into a map — a hole in `cli.rb` is a hole, a gap in `bundle/writer.rb` is the [library API](capabilities/library-api.md)'s to prove; and fixtures follow common closure, one group's living under that group. It carries its own argument: `rooted` and `mentions` exist because a branch no fixture can reach is a branch nobody has ever proven.
683
+ * **Sync**: [`graph`](capabilities/read-views.md) was the last view that named no bundle — a bare pair of counts over a bare `nodes`/`edges` payload, by path or by ref alike. It carries the same identity head as every other view now, so the [read views](capabilities/read-views.md) concept can state the rule without an exception hiding under it.
684
+ * **Sync**: the [CLI](cli.md)'s exit-code table gains the subtle member — a *second* bundle is a usage error, because only `search` merges and only `server` mounts several; reading the first and dropping the rest answered confidently about a bundle nobody asked about. A bad `-o` path joins it: exit 2, not a backtrace.
685
+ * **Sync**: every bundle-scoped output now names its bundle in the identity the caller used — the [CLI](cli.md) records the two-keys-one-meaning rule (`bundle` is always a directory, `slug` always a registry slug), the `@handbook (/path)` human header, and why a path-named bundle carries no slug (inventing one implies a registration that does not exist; looking one up would cost a registry read on every plain-dir run). [Cross-bundle search](capabilities/search.md)'s head became `bundles: [{ slug, dir }]` so a row resolves to `<dir>/<id>.md` without a second lookup, and its rows carry `slug` — the key that used to be `bundle`, which meant a *directory* in single-bundle mode and a *slug* here. One key, two meanings, chosen by invocation form: the shape most likely to make an agent confabulate the bundle a concept came from.
686
+ * **Sync**: `--home` grew a coherent rule — it steers @refs wherever a verb offers it (`registry`, `server`, and now `search`), and `$OKF_HOME` backs every verb that does not — replacing the earlier "--home never steers a ref". [`search`](capabilities/search.md) also records that `--all` takes no directory: one passed anyway would demote to a search term and answer a confident "no matches" for a bundle nobody searched.
687
+
688
+
689
+ ## 2026-07-16
690
+
691
+ * **Correction**: the [graph server](capabilities/graph-server.md)'s responsive section claimed a `≤1024px` breakpoint and justified it as a statement about touch input — "tablets included… the tablet line rather than the phone one". The code's breakpoint is `≤768px` (phones and portrait tablets), so the rationale was not merely off by a number but backwards: a landscape tablet crosses `769px` and gets the *desktop* layout. The number came from a superseded commit message instead of the template — the exact drift this bundle exists to prevent. Rewritten against the CSS: the breakpoint tracks the width available to the chrome, not a device class, which is why rotation re-evaluates it.
692
+ * **Sync**: `okf registry list --json` now answers in the object envelope every other `--json` view uses — `{ registry, count, bundles }` — naming the registry file it read, so a `$OKF_HOME` mismatch is visible in the payload; the skill reference records the shape.
693
+ * **Sync**: caught the bundle up with @refs and cross-bundle search — the [CLI](cli.md) records that every `<bundle-dir>` now also takes `@slug` (a registered bundle) or bare `@` (the registry default), resolved in one seam and failing hard on an explicit ask; the [bundle registry](registry.md) gains its grown role as the CLI's name-resolution layer, not just the server's boot list; and [ranked search](capabilities/search.md) documents the one cross-bundle verb — several leading @refs or `--all`, merged rankings labeled per bundle, `--all` forgiving a vanished directory while explicit refs are strict — plus why the graph deliberately stays per-bundle.
694
+ * **Sync**: caught the bundle up with the unreleased bundle registry and multi-bundle hub — a new [bundle registry](registry.md) concept (the per-user JSON list under `$OKF_HOME`, the forgiving-implicit/strict-explicit slug rule, the chosen default and its first-entry fallback, path-as-identity, the `missing` marker over silent pruning, and the atomic write) enumerated in the root index, the [overview](overview.md), the [CLI](cli.md)'s Act group, and the [core/shell split](design/core-shell-split.md)'s shell list and diagram. The [graph server](capabilities/graph-server.md) records the hub (`/b/<slug>/` mounts paid for by the page's already mount-relative endpoints, the default redirect, the browsable `/b/` index, the boot-time read that makes a change need a restart), the server-only bundle switcher, and the ≤768px responsive chrome; the [library API](capabilities/library-api.md) notes that the registry and hub load on demand, so the embedding surface stays fixed.
695
+ * **Sync**: the plugin's `/okf:gem` command became a pass-through shim — the [agent skill](capabilities/agent-skill.md) now records that all routing (Commands table, intent inference, the not-a-bundle `migrate` suggestion) lives only in `SKILL.md`, where the drift test guards it, and the command just hands its arguments to the skill unchanged.
696
+ * **Sync**: caught the bundle up with the skill's new `migrate` verb — the [agent skill](capabilities/agent-skill.md) now records the second authoring on-ramp (adopt existing documentation in place: frontmatter and reserved files added, bodies kept verbatim, `okf validate --json` as the worklist) alongside `produce`'s distillation, and its plugin-channel section notes that `/okf:gem` suggests `migrate` when its target directory turns out not to be a bundle.
697
+
698
+
699
+ ## 2026-07-15
700
+
701
+ * **Sync**: caught the bundle up with the unreleased gzip transport — the [graph server](capabilities/graph-server.md) now records that `okf server` gzips every response a client accepts (`Rack::Deflater` wrapped at the `run_server` boot seam), transparent to the browser and at [no new dependency](design/runtime-dependencies.md) since Deflater ships inside the `rack` already required; the note draws the boundary that the wrap is boot policy, not the app — a host mounting `OKF::Server::App` and the static `okf render` file each carry their own compression — and adds the reciprocal edge into the runtime-dependencies constraint.
702
+ * **Sync**: caught the bundle up with the unreleased `okf render` verb — the [graph server](capabilities/graph-server.md) now documents its static twin (one command writes the whole page as a single self-contained file, the bundle baked in, to host where there is no server) and the data-access design that makes one template serve both modes: the browser's reads flow through getter functions whose source an injected `EMBED` switch selects — live `fetch()` under `okf server`, the embedded payload under `okf render`. The [server trust boundary](design/server-trust-boundary.md) records that render inlines each body through `json_for_script` and still sanitizes it with DOMPurify, so the embedded path carries both defenses; the [CLI](cli.md) verb table gains `render` (Act, best-effort), and the [overview](overview.md) and [capabilities](capabilities/) index listing note the static export.
703
+
704
+
705
+ ## 2026-07-13
706
+
707
+ * **Sync**: the [graph server](capabilities/graph-server.md) gains a fullscreen diagram viewer — a rendered Mermaid block is now click-to-inspect (re-rendered from source into a pan-and-zoom overlay, Panzoom lazy-loaded beside Mermaid), and the concept's self-contained-page section records the two lazy CDN assets.
708
+ * **Sync**: caught the bundle up with the server's authored-layer round — the [graph server](capabilities/graph-server.md) now renders the §6 index map and §7 log in the browser (the Files | Indexes tree tabs), resolves in-app links to reserved files and bare directories, and lets folder/area nodes open their directory map, all backed by new `/index` and `/log` endpoints; this closes the parity gap from the other side of search — the CLI had the map, now the browser does too.
709
+ * **Sync**: caught the bundle up with the unreleased `search` verb — a new [ranked text search](capabilities/search.md) concept (the pure `OKF::Bundle::Search`, its weights, snippets, and the retrieval eval), the capability now enumerated in the [overview](overview.md) table and diagram, the [CLI](cli.md) verb table, the [read views](capabilities/read-views.md), the [library API](capabilities/library-api.md) standalone pieces, the [core/shell split](design/core-shell-split.md) core list, and the root and [capabilities](capabilities/) index listings; the [agent skill](capabilities/agent-skill.md) now records its search playbook and search-first routing.
710
+ * **Sync**: caught the bundle up with 1.2.0–1.4.0 — the [agent skill](capabilities/agent-skill.md) gains the Claude Code plugin channel (generated copy, `rake plugin:sync`, drift test, `/okf:gem`, curation hook), and the [graph server](capabilities/graph-server.md) gains link-preview metadata plus the UX round: in-app relative-link navigation, file-tree mode, resizable persisted panes.
711
+ * **Curation**: pruned the tag vocabulary from 39 to 23 — dropped the group-name echoes (`format` inside `format/`, `model` inside `model/`, `overview` on the Overview) and the singleton title echoes (`bundle`, `concept`, `validation`, `linting`, `library`, `api`, `skill`, `links`, `citations`, `frontmatter`, `dependencies`, `compatibility`), and merged `purity` into `pure`, which now connects the [core/shell split](design/core-shell-split.md) to the three pure model components.
712
+
713
+
714
+ ## 2026-07-12
715
+
716
+ * **Sync**: caught the bundle up with the gem at 1.1.0 — the [graph server](capabilities/graph-server.md) now sanitizes each fetched body with DOMPurify before rendering, so the [server trust boundary](design/server-trust-boundary.md) closes the on-demand render path (its [design listing](design/) reworded to match), and the [library API](capabilities/library-api.md) notes that `require "okf"` loads the library alone now that the CLI and skill load on demand.
717
+ * **Sync**: caught the bundle up with the CLI at 1.0.0 — documented the new `index` command (the §6 progressive-disclosure map, the read view that sees the reserved `index.md` layer), compact-by-default JSON with `--pretty`, and `--fields`/`--except` projection on the list views, in [read views](capabilities/read-views.md) plus the `index`-verb enumerations in the [CLI](cli.md), the [overview](overview.md), and the [capabilities](capabilities/) index listing.
718
+
719
+
720
+ ## 2026-07-11
721
+
722
+ * **Creation**: seeded the bundle documenting okf-gem's capabilities at version 0.1.0 — the [overview](overview.md), the [CLI](cli.md), and the format (`@okf-eco format/`), [model](model/), [capabilities](capabilities/), and [design](design/) areas.
723
+ * **Update**: added Mermaid diagrams (tagged `diagram`) to five concepts — [overview](overview.md), the [core/shell split](design/core-shell-split.md), the [graph server](capabilities/graph-server.md), the [library API](capabilities/library-api.md), and cross-links (`@okf-eco format/cross-links`).
724
+ * **Sync**: caught the bundle up with the CLI — documented the new `types` command, the cross-view `--type`/`--area`/`--tag` filters, and `tags --by type|area` in [read views](capabilities/read-views.md), the [CLI](cli.md) front end, the [graph](model/graph.md) indexes, and the [capabilities](capabilities/) index listing.