okf 1.9.0 → 1.11.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +696 -133
  3. data/README.md +250 -334
  4. data/lib/okf/bundle/folder.rb +24 -5
  5. data/lib/okf/bundle/linter.rb +1 -1
  6. data/lib/okf/bundle/search/index.rb +13 -3
  7. data/lib/okf/bundle/search.rb +91 -11
  8. data/lib/okf/bundle.rb +26 -2
  9. data/lib/okf/cli/catalog.rb +66 -0
  10. data/lib/okf/cli/command.rb +657 -0
  11. data/lib/okf/cli/dirs.rb +118 -0
  12. data/lib/okf/cli/files.rb +68 -0
  13. data/lib/okf/cli/graph.rb +82 -0
  14. data/lib/okf/cli/index.rb +169 -0
  15. data/lib/okf/cli/lint.rb +139 -0
  16. data/lib/okf/cli/loose.rb +78 -0
  17. data/lib/okf/cli/registry.rb +229 -0
  18. data/lib/okf/cli/render.rb +66 -0
  19. data/lib/okf/cli/search.rb +285 -0
  20. data/lib/okf/cli/server.rb +186 -0
  21. data/lib/okf/cli/skill.rb +57 -0
  22. data/lib/okf/cli/stats.rb +113 -0
  23. data/lib/okf/cli/tags.rb +144 -0
  24. data/lib/okf/cli/types.rb +37 -0
  25. data/lib/okf/cli/validate.rb +66 -0
  26. data/lib/okf/cli.rb +425 -1706
  27. data/lib/okf/render/graph/template.html.erb +1285 -129
  28. data/lib/okf/render/graph.rb +46 -2
  29. data/lib/okf/server/app.rb +71 -4
  30. data/lib/okf/server/hub/not_found.rb +663 -0
  31. data/lib/okf/server/hub.rb +512 -38
  32. data/lib/okf/skill/SKILL.md +26 -19
  33. data/lib/okf/skill/playbooks/consume.md +3 -3
  34. data/lib/okf/skill/playbooks/curate.md +3 -1
  35. data/lib/okf/skill/playbooks/maintain.md +7 -5
  36. data/lib/okf/skill/playbooks/menu.md +5 -0
  37. data/lib/okf/skill/playbooks/refine.md +93 -0
  38. data/lib/okf/skill/playbooks/search.md +7 -7
  39. data/lib/okf/skill/reference/cli.md +122 -22
  40. data/lib/okf/version.rb +1 -1
  41. data/lib/okf.rb +9 -0
  42. metadata +38 -8
data/CHANGELOG.md CHANGED
@@ -1,7 +1,463 @@
1
1
  # Changelog
2
2
 
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.11.0] - 2026-07-22
9
+
10
+ ### Added
11
+
12
+ - **`okf dirs <dir|@slug> [--json]`** — the bundle's directories (its clusters)
13
+ with the number of concepts living **directly** in each, root first and the
14
+ total last. Every dir the tree has, including the empty intermediates that
15
+ exist only to connect it — a dir holding nothing but sub-directories reads
16
+ `0`, not a hidden rollup, so the column sums to the bundle's concept count.
17
+ JSON: `{ bundle, total, count, dirs: [{ dir, count, subdirs }] }`.
18
+ - **`--depth N` on `index` and `dirs`** — how many directory levels below the
19
+ starting point to keep, where the starting point is the `--dir` when one is
20
+ given and the bundle root otherwise. Relative rather than absolute, so
21
+ `--dir a/b --depth 1` reads "a/b and one level under it" without first working
22
+ out how deep `a/b` is, and the two flags walk a tree a level at a time.
23
+ `--depth 0` is the starting point alone; anything but a whole number is a
24
+ usage error (exit 2). This is what makes `index` usable at scale — every
25
+ directory in it is a section, so a few hundred concepts is a map nobody reads
26
+ whole. On one 414-concept bundle `index --no-body` went 12.5 KB → 1.3 KB at
27
+ `--depth 1`, and `index --json` 313 KB → 2.8 KB with `--depth 1 --except
28
+ body,listing`.
29
+ - **`--dir` on `index` and `dirs` brings the chain up to the root with it**, so a
30
+ branch is never shown adrift of the authored context that says what it is —
31
+ the root `index.md`'s prose first among it. Those rows print with a leading
32
+ `↑`, carry `ancestor: true`, and stay out of `total`; `--no-ancestors` drops
33
+ them. Ascent and descent are separate axes, so `--depth` never bounds the
34
+ chain: `--dir X --depth 0` is X alone, plus how you get to X. A `--dir` that
35
+ names nothing gains no chain, since a lone root row would read as a partial
36
+ answer to a query that in fact matched nothing.
37
+ - **`dirs` gains `--dir` (repeatable) and a `subtree` count** per row: the
38
+ concepts at or below that directory, defined as exactly what `--dir` on the
39
+ row returns, so the number and the flag can never disagree. Without it a
40
+ truncated listing is all zeroes at the top of a deep tree — which is where
41
+ "where is the mass?" is actually asked. The human table shows the column only
42
+ where some directory nests; `--json` always carries it.
43
+ - **`--dir PATH`** joins the shared filter set on `search`, `catalog`, `files`,
44
+ `types` and `tags`, and `index` gains it as a repeatable selector. One rule:
45
+ a concept matches when its dir *is* the path or sits below it — so `--dir
46
+ platform` reaches `platform/services/api`, `--dir platform/services` narrows,
47
+ and `--dir .` means the root alone with no special case. `root` is the
48
+ unquoted spelling of `.`; matching folds case.
49
+ - **`tags --by dir`** cuts the tag index by the whole directory path, where
50
+ `--by area` only ever saw the first segment.
51
+ - **`stats` gains `dirs` and `by_dir`** (the full-path cut, direct counts), and
52
+ its human breakdown now reads **By dir**. Both are read off the same map `okf
53
+ dirs` lists and `--dir` is answered against, so the two verbs cannot report a
54
+ different number of directories — a directory holding nothing directly appears
55
+ at `0` rather than being dropped, since it is still one `--dir` addresses.
56
+ - **Search rows carry `dir`** — the full path, `.` at the root — beside the
57
+ first-segment `area` they already had.
58
+ - **`dirs` takes `--fields`/`--except`** like the other list views, over the row
59
+ shape it already declared.
60
+ - **A single-bundle `okf server` answers `GET /search?q=`.** The route was the
61
+ hub's alone — conceived as the *cross-bundle* one — which left `okf server
62
+ ./docs` with a ⌘K palette that could find nothing, though one bundle is a legal
63
+ one-element set. `App` owns the payload and the hub calls it, so the shape is
64
+ defined once; a row from a single-bundle server carries no `slug`, which is how
65
+ it avoids answering as if it were a set. A static `okf render` still advertises
66
+ no endpoint: there is no server behind it to ask.
67
+ - **The route always answers; advertising it is the caller's call.** The page
68
+ resolves the endpoint *relative to the URL the reader is on*, so only whoever
69
+ mounted the app knows what to call it: `okf server` mounts at the root and
70
+ passes `search`, while `App.new(folder)` on its own advertises nothing, since
71
+ a default would point an app mounted at `/knowledge` back at its host's root.
72
+ - **The search index is built once and held.** Every request used to rebuild the
73
+ whole corpus — measured 1.45 s per search on a 414-concept bundle, flat across
74
+ repeats, with the build ~95% of it. `Search.prepare` holds a corpus (documents,
75
+ the key→concept map, the built index) and `Search.with` queries it: **0.016 –
76
+ 0.052 s** per search, with the 1.39 s build moved into boot, where `okf server`
77
+ warms it deliberately. An engine opts in by exposing `prepare`; the scan
78
+ declares none and is handed none, so no engine or addon had to change. The
79
+ trade is staleness — a corpus is a snapshot, like the graph — and the hub drops
80
+ its corpus on any registry write, since a held index outliving the set it was
81
+ built from is a wrong answer rather than a slow one.
82
+ - **`⌥ drag` moves a cluster box**, and is listed in the `?` sheet.
83
+
84
+ ### Changed
85
+
86
+ - **A bundle is named by its slug, everywhere it is chosen.** The ⌘K switcher,
87
+ the Bundles panel and the hub's `/b/` page all led with the derived
88
+ `parent/dir` label and left the slug in muted grey beside it — the address in
89
+ the name's place, when a bundle is addressed by `@okf-gem` and `/b/okf-gem/`.
90
+ Rows now carry `@slug` as the name and the folder as the fact under it, shown
91
+ only where it is not the name repeated. `/b/` drops the short label outright:
92
+ it only ever stood in for the full path, which is on the row already.
93
+ - **A `.okf` directory is labelled by the project that holds it.**
94
+ `Bundle::Folder.label` reads `repo/.okf` as `repo`. The `parent/dir` pair
95
+ exists because a bundle directory's own name is rarely unique — except when it
96
+ is `.okf`, the conventional container, and then a registry of eight projects
97
+ was eight rows all saying `.okf`. This also reaches `okf registry list` and the
98
+ default `okf server` title. (A `.okf` with no parent to borrow keeps its own
99
+ name; that case used to compose into `//.okf`.)
100
+ - **A force layout settles, then moves once.** `animate:true` reads to these
101
+ engines as "render every tick of the simulation" — the visible bounce, and
102
+ hundreds of full re-renders for one settle. It is `'end'` now: the same
103
+ simulation run headless, the nodes moved once into the same final positions.
104
+ Past 250 nodes even that transition is dropped, because at that size the move
105
+ itself is the jank.
106
+ - **A cluster box is scenery, not a handle.** Its empty interior is the largest
107
+ drag target on the canvas, so dragging to look around dragged the *directory*
108
+ instead of the view — worse the bigger the cluster. It takes `grabbable:false`
109
+ **and** `pannable:true`: ungrabbable alone stops the box moving, but the node
110
+ still swallows the drag, and it is `pannable` that hands the gesture to the
111
+ canvas the way empty background does. `Alt`+drag gives the handle back, since
112
+ moving a box is a real gesture, just not the constant one — `Alt` rather than
113
+ `Ctrl`, which on macOS is the system secondary click. A tap still opens the
114
+ directory's map.
115
+ - **`f` is no longer a shortcut.** A bare letter bound globally fires on every
116
+ keystroke the page did not route into an input, and fullscreen is not a mode to
117
+ enter by accident. The button stays and is now the only way in; the shortcut
118
+ sheet no longer advertises a key nothing is bound to.
119
+ - **The skill names one first move.** It had prescribed three different ones
120
+ across seven places — SKILL.md, four playbooks and the CLI reference — and that
121
+ disagreement is the deliberation an agent pays for on every retrieval. Every
122
+ site now says `okf dirs` first, then `okf index --dir <branch>` to descend,
123
+ chosen structurally: `dirs` emits one row per *directory* where `index` emits
124
+ one listing row per *concept* even under `--no-body`, so the two scale with
125
+ different things.
126
+
127
+ - **Cluster mode nests.** The graph page grouped concepts into one flat row of
128
+ boxes, one per *first path segment* — the same lossy projection `--area` was.
129
+ A cluster is a directory now, and the boxes nest as the directories do, to a
130
+ depth picked from a select beside the layout one. Depth **1** is the default
131
+ and draws exactly the old view; a flat bundle is offered no control at all.
132
+ At depth N every directory of depth ≤ N gets a box (intermediates that hold no
133
+ concepts of their own included, since they hold sub-boxes), and a concept
134
+ attaches to its own directory's box truncated to N. The root box still holds
135
+ direct-root concepts and never nests another. Box ids carry the directory
136
+ verbatim (`box::platform/services`, `box::.`), so a tap opens that directory's
137
+ map with no label to unmangle.
138
+ - **The page speaks `dir` too**: the filter group is **Dirs**, listing every
139
+ directory (not just first segments) and filtering by the same
140
+ directory-and-below rule `--dir` uses — in the graph, catalog and tags views —
141
+ and the Stats panel's breakdown is **By dir**, keyed by the whole path.
142
+
143
+ ### Fixed
144
+
145
+ - **The rail marks Index while the root map is open.** Index is a shortcut into
146
+ Files, so the two share one `data-view` — and the rail read only that, lighting
147
+ **Files** on the one screen a reader reached by clicking **Index**. The open
148
+ file is what distinguishes them, so it is what the rail reads; a *nested*
149
+ `index.md` is still Files.
150
+ - **`--dir` accepts the label the views print.** `fold_dir` never stripped a
151
+ trailing slash, while `okf index` labels a row `tables/` — so pasting a printed
152
+ row back into the flag matched nothing and exited 0, an empty result under a
153
+ count that agreed with it.
154
+ - **The `--dir` chain keeps its case.** It was walked over case-folded paths and
155
+ then matched against the map with `include?`, which does not fold, so every
156
+ ancestor of a directory spelled with a capital vanished from the chain that
157
+ exists to place the branch.
158
+ - **`--area` with `--depth` or `--dir` is refused (exit 2)** instead of unioning
159
+ the area with what the other flag selects. The deprecated flag is exact: with
160
+ `--depth` it names no starting point to be relative to, and with `--dir` one
161
+ side is exact where the other is a prefix, so the map came back with the area
162
+ *and* the subtree — an answer to neither question. A deprecated flag that
163
+ quietly widens is worse than one that is merely old.
164
+ - **A cleared filter no longer leaves a cluster unlaid.** The tiling runs over
165
+ the visible elements only (fcose throws on a node whose label went
166
+ `display:none` mid-run), but nothing re-tiled when a filter was later loosened —
167
+ so concepts hidden when clustering began came back at their pre-cluster
168
+ coordinates and stretched their box across the canvas. Worst case the filter
169
+ matched nothing, the layout returned early, and clearing it showed a view
170
+ nothing had laid out at all.
171
+ - **A palette hit in a single-bundle server no longer 404s or reloads the page.**
172
+ A row with no `slug` was read as naming a *foreign* bundle, so the href became
173
+ `../undefined/`, the row rendered an "undefined" chip, and the click took the
174
+ page-load branch — reloading the whole index to reach a node already on screen.
175
+ Three sites, one absent field.
176
+ - **A focused form field no longer zooms the page on iOS.** Safari zooms whenever
177
+ a focused control is under 16px and never zooms back out, so on a phone every
178
+ `/` left the reader pinching to recover. Keyed on `(max-width:768px)` *or*
179
+ `(pointer:coarse)`, because neither covers the other — a phone is narrow, a
180
+ tablet in landscape is not and zooms just the same.
181
+ - **A nested cluster no longer throws when a filter empties it mid-layout.**
182
+ fcose measures every node it is handed, so hiding nodes while its tiling
183
+ animation ran threw on a label it could no longer measure. The layout is
184
+ handed the visible elements only — which is also the right answer, since a
185
+ hidden concept has no business influencing where the visible ones land.
186
+ - **An intermediate directory box no longer takes its branch off the canvas.**
187
+ The empty-box rule read a compound's direct children, and a box holding only
188
+ sub-boxes has none, so it always counted as empty. It reads leaf descendants
189
+ now.
190
+
191
+ ### Removed
192
+
193
+ - **`OKF::Server::Hub::SEARCH_LIMIT` and `Hub::SEARCH_ENGINE`.** Both moved to
194
+ `OKF::Server::App`, which now defines the `/search` payload both hosts answer
195
+ with (`App.search_payload`). The hub's copies were left behind unreferenced —
196
+ two constants for one cap is two places to raise it and one of them silently
197
+ losing. Use `OKF::Server::App::SEARCH_LIMIT` / `App::SEARCH_ENGINE`.
198
+
199
+ ### Deprecated
200
+
201
+ - **`--area`, and `tags --by area`.** OKF's own vocabulary for grouping is
202
+ *directories* (`grep -ci area SPEC.md` → 0); "area" was this gem's invention,
203
+ and defining it as a concept id's first path segment threw away every level
204
+ below it. `dir` is now the only machine word — full path, `.` at the root,
205
+ rendered `(root)` for humans — and "cluster" stays prose for what a dir
206
+ groups. Both deprecated spellings keep their **old behavior exactly** and warn
207
+ once per run on stderr (`--json` on stdout is unaffected); they go in a later
208
+ release, along with `by_area` and the `area` row field.
209
+
210
+ ## [1.10.0] - 2026-07-21
211
+
212
+ ### Added
213
+
214
+ - **The skill gains a `refine` verb** (`playbooks/refine.md`): restructure a
215
+ bundle to get the most from OKF's capabilities — progressive disclosure, the
216
+ emergent graph, cross-cutting tags, capture-once-link-many. It is the third
217
+ authoring boundary: `curate` keeps the structure sound as it stands,
218
+ `maintain` keeps the content true, `refine` changes where knowledge lives —
219
+ evidence-first (tag locality, the hub origin test, a fatness alarm),
220
+ cohesion-over-balance, free levers before file moves, and it *proposes* (a
221
+ report plus a frozen execution prompt), never auto-applies.
222
+ - **`okf graph --hubs`** — the inbound ranking: every concept with at least one
223
+ inbound link, ranked by inbound degree, each with its links grouped by
224
+ *source area* (`core/status ×3 flows 2, billing 1`). This is the refine
225
+ playbook's hub origin test made mechanical: a hub whose inbound majority is
226
+ foreign to its own area is a move candidate. JSON: `{ bundle, count, hubs:
227
+ [{ id, area, inbound, by_area }] }`.
228
+ - **The registry has a browser surface.** A meeting with non-technical readers
229
+ settled what the TUI could not: `okf registry set/del/default/rename` is the
230
+ right surface for the people who *write* bundles and the wrong one for the
231
+ people who read them. The graph page's rail grows a **Bundles panel** behind
232
+ ⚙ — every bundle the server knows about, with its title, `@slug`, folder,
233
+ concept count and a health verdict — and four routes behind it:
234
+ `POST /registry/{default,rename,remove,add}`, the only non-GET routes the
235
+ server has.
236
+ - **Management is the default, and `--read-only` declines it.** The flag names
237
+ the restriction rather than the capability, because the audience this was
238
+ built for should not need a command line to use the page they were pointed
239
+ at. A loopback bind is writable without a flag; any other address is refused
240
+ outright, with no flag that opens it — `--bind 0.0.0.0` is how a personal
241
+ tool becomes a public one, and a write surface does not follow it there.
242
+ - **Four gates on every write**: is this server writable at all; is there a
243
+ registry to write to (an ephemeral `okf server ./a ./b` answers `409` rather
244
+ than leaving the missing controls a mystery); is the verb one of the four (a
245
+ frozen list — "call whatever method the path names" is how a router becomes
246
+ an `eval`); and did this come from this page (same-origin *and* a per-boot
247
+ token, since the token lives in a page another site can get a reader to
248
+ submit, and Origin alone would trust every tab open on the host). A
249
+ read-only server hides the controls *and* refuses the request that skipped
250
+ them: hiding a button is a UI, refusing the request is the boundary.
251
+ - **A write rebuilds the hub's bundles from disk** before it answers. That is
252
+ the step easy to skip and impossible to skip safely — a write leaving the
253
+ running server on the old set is a lie the next click believes.
254
+ - **`/b/` stops managing and keeps the page.** Both surfaces carried the same
255
+ four verbs for a while, and two implementations of one contract is the thing
256
+ that drifts, so the forms came out and the routes stayed. `/b/` answers
257
+ *which bundles are there* — and remains the empty state a hub with zero
258
+ bundles still needs — while the panel answers *change this one* where the
259
+ reader already is. With nothing to post it holds no token either.
260
+ - **There is no Add on either surface.** A browser cannot hand over a
261
+ filesystem path — the File System Access API yields an opaque handle, and is
262
+ Chromium-only besides — so registering stays the agent's act. The route
263
+ exists for other callers; nothing in the UI reaches it.
264
+ - **"Workspace" is retired** from the docs and the UI. The things are Bundles
265
+ and the thing holding them is the registry; a page saying one word while the
266
+ CLI says another is two products wearing one name.
267
+ - **The hub searches every bundle it hosts.** `GET /search?q=` is the only route
268
+ in the server that knows about more than one bundle: `Search.across` over one
269
+ shared index, so BM25 weighs a term against the whole corpus instead of
270
+ stapling per-bundle lists together. Capped at 50 with the total reported,
271
+ because a silent cap reads as a complete answer. The engine is named `:index`
272
+ outright rather than left to route off `fuzzy: true` — that reached the right
273
+ engine only because nothing else declares the capability, which is correctness
274
+ by coincidence, and an addon declaring `:fuzzy` would have taken the route
275
+ silently. A long-lived server also amortizes an index build over every
276
+ keystroke where a one-shot CLI cannot, and minifts is a port of the browser's
277
+ own MiniSearch, so a palette hit and an in-page search rank alike.
278
+ - **The palette's Concepts group comes last**, and not because it matters
279
+ least: it is the only group that arrives asynchronously, and a group landing
280
+ above the cursor moves the row under the reader's fingers between the
281
+ keystroke and the Enter.
282
+ - **The topbar search box says what it filters, and where to go when it finds
283
+ nothing.** It and ⌘K looked alike and meant different things — the box
284
+ *filters* what is on screen, the palette *finds* across every bundle a hub
285
+ hosts — and the box carried neither fact: it emptied the graph in silence and
286
+ never mentioned the palette, so a reader whose word lived in another bundle
287
+ got a blank canvas and no way out. Three additions, all inside the box: a
288
+ **chip** naming the chord (⌘K / Ctrl-K, OS-aware) that opens the palette, a
289
+ **live count** (`7/8`) that makes an empty result a number which reached zero
290
+ rather than a view that went blank, and on zero a **panel** naming the bundle
291
+ and the query — ⏎ hands it to the palette prefilled and already searching, esc
292
+ clears.
293
+ - **`okf` is extensible.** Any gem that puts `okf/plugin.rb` on its load path can
294
+ register a verb, and it answers to `okf` — listed in `okf help` under
295
+ `installed extensions:`, dispatched like a built-in. There is no list of known
296
+ addons in this gem and no configuration step for the user: installing the gem
297
+ is the whole installation. It is the same seam `Search.register` opened for
298
+ search engines, and the same idiom — append-only, idempotent by id, so an addon
299
+ can never quietly displace a built-in.
300
+ - **Discovery is lazy**, which is what makes it affordable. A built-in verb
301
+ resolves against the registry and dispatches without scanning at all; only an
302
+ unknown verb or `okf help` — which has to know everything by definition —
303
+ pays the ~11ms `Gem.find_latest_files` costs on the 2.4 floor. A one-shot CLI
304
+ that will not build a search index for a single query should not pay for
305
+ discovery to answer a verb it shipped with.
306
+ - **Extensions must come from gems named `okf-*`**, the convention Jekyll and
307
+ Vagrant use for the same job: it makes what counts as an okf extension
308
+ explicit and stops an unrelated gem claiming the `okf/plugin.rb` path by
309
+ accident. One that is not so named is discovered, skipped, and reported on
310
+ stderr. A path belonging to *no* gem — a checkout, `ruby -I`, a Gemfile
311
+ `path:` — stays trusted, because someone put it there deliberately. It is a
312
+ mild guard too — loading a plugin runs its code — but the naming convention
313
+ is the reason, not the threat model, which is thin: under Bundler discovery
314
+ is bundle-scoped anyway, so the Gemfile is already an allowlist.
315
+ - **The rule holds when it cannot get an answer**, which is a separate promise
316
+ from the rule itself. A gem name that cannot be read — one corrupt gemspec
317
+ anywhere on the machine — is refused rather than treated as "belongs to no
318
+ gem", and the refusal names the exception that caused it. A discovery that
319
+ fails outright is reported too, since an empty list and no message is
320
+ indistinguishable from a machine with nothing installed.
321
+ - **A broken addon is skipped and reported, never fatal** — the same
322
+ best-effort posture the reader takes with an unparseable file. The note goes
323
+ to stderr, so a `--json` run's stdout stays a clean machine substrate.
324
+
325
+ - **The graph page is proven in a real browser.** `test/browser/` drives the
326
+ page `okf server` and `okf render` share in Chromium, asserting DOM state and
327
+ computed CSS at real viewport widths, and failing any test where the page
328
+ threw. Every spec runs twice — once served, once against a `file://` static
329
+ render — because the two modes diverge on fetched endpoints vs. a baked
330
+ `EMBED`, and a pass in one proves nothing about the other. It is opt-in
331
+ (`rake test:browser`, outside the default task) and non-blocking in CI, since
332
+ the page boots against a CDN and a slow jsdelivr must not gate a merge. The
333
+ three fixes below are what writing it turned up: shipped defects invisible to
334
+ a string assertion over the rendered HTML, each reproduced red and pinned
335
+ green.
336
+ - **Coverage is mapped per-contract, not guessed.** `test/browser/COVERAGE.md`
337
+ enumerates every behavioral contract the page introduced across its history
338
+ and marks each covered / partial / uncovered against a named spec — **176 of
339
+ 181 net-live (97%)**. The five that remain are each a documented blocker, not
340
+ a missing test: an absence-proof with no line to break, a node-overlap check
341
+ no cytoscape layout makes both deterministic *and* mutation-sensitive, a
342
+ map-visibility observable another contract already owns, a palette scroll
343
+ whose observable is a tautology, and an unbuilt focus-trap. Reaching the
344
+ branches the flat 8-concept fixture cannot took four further purpose-built
345
+ bundles beside the hostile one, each served on its own port and baked to its
346
+ own static page so the main fixture's count assertions stay put — nested
347
+ directories, forty-five tags, a five-directory-deep reserved path, and a
348
+ hundred-node ring that drives the graph past its own fit box. Every new spec
349
+ is mutation-checked: break the code it covers, confirm it goes red for the
350
+ predicted reason, restore. The map also caught one of its own stale rows — a
351
+ note listed as an uncovered gap had in fact been deleted from the page, and is
352
+ now marked superseded rather than owed.
353
+ - **The page's CDN libraries are served from a local cache** — a read-through
354
+ cache keyed on the request URL, so a warm run needs no network and a version
355
+ bump is a miss rather than a stale hit; `OKF_NO_VENDOR_CACHE=1` bypasses it,
356
+ which is how you check the pins still resolve. It buys robustness, not
357
+ speed: measured at one worker, 28.7s without and 29.0s with, because the
358
+ suite is CPU-bound and Chromium already reused those files across contexts.
359
+
360
+ ### Changed
361
+
362
+ - **`okf tags --by` rows carry each tag's total.** The grouped view printed only
363
+ within-group counts, so a tag's spread meant cross-referencing groups by
364
+ hand; each row now shows `count/total` when they differ (`async 2/3`) and
365
+ the plain count when the tag is wholly local — locality at a glance, the
366
+ domain-vs-concern read. The JSON rows gain a `total` key; filters recompute
367
+ it over the narrowed set.
368
+
369
+ - **A wrong turn at the hub lands on a directory, not an apology.** The 404 is
370
+ rebuilt on the app shell, and it reads as what it is: the **asked path is the
371
+ heading**, set in mono where a dropped slash is legible as a shape, with "not
372
+ found" demoted to the eyebrow above it, since a reader arrives already knowing
373
+ they are lost. A near-miss slug is a **row** wearing the same anatomy as the
374
+ list under it, already marked, with ⏎ pointed at it; rows carry the folder
375
+ that actually distinguishes `site/.okf` from `minifts/.okf`; and colour marks
376
+ exceptions only, so a healthy row draws no verdict edge at all. Moving through
377
+ the list is **Tab's** job — every row is an `<a href>`, and a hand-rolled ↑↓
378
+ cursor was tried and deleted as a second focus model beside the real one. A
379
+ query matching no bundle is offered the cross-bundle search that would match
380
+ it, the same escalation the graph page's box makes.
381
+
382
+ - **On a touch screen a tap opens a card, not the whole viewport.** At ≤768px
383
+ the inspector is `grid-template-columns:0 1fr`, so tapping a dot measured the
384
+ stage at 0px wide: the graph was not covered, it was gone. Exploring on a
385
+ phone became open → read → close → tap the next dot, and you could never see a
386
+ concept and its neighbourhood at once, which is the one thing a graph is for.
387
+ A preview card now rises at the bottom edge over a graph that keeps every
388
+ pixel and stays live — drag it up for the neighbourhood and the body, tap a
389
+ row and it swaps in place while the camera walks. Folder and index taps fill
390
+ the card too; they used to write into an invisible panel, so tree and cluster
391
+ modes were silently dead on touch. The branch is wider than the chrome's
392
+ (≤768px, or ≤1024px portrait), because a portrait tablet has the same bug and
393
+ wants the same gesture.
394
+
395
+ - **Type chips select instead of deselecting.** Three chip groups carried two
396
+ grammars: areas and tags were additive — nothing selected means everything, a
397
+ click narrows, a second click undoes — while types were subtractive, every
398
+ type showing until you clicked one away. Same component, same panel, opposite
399
+ meaning, and the catalog's and tags view's own type chips were already
400
+ additive, so the rule a reader learned in one view was wrong in the next.
401
+ Types now select, and two of them compound into a union the old model could
402
+ not express at all — it could say "not the other four", never "Services and
403
+ Charters". The change is a net deletion.
404
+
405
+ - **The CLI is one file per verb.** `lib/okf/cli.rb` was 1,794 lines and a
406
+ 15-arm `case`; it is now a registry and a dispatcher, with the verbs under
407
+ `lib/okf/cli/` and the shared surface on a `Command` base class. Behaviour is
408
+ unchanged — every existing test passes untouched — but `okf help` is now
409
+ composed from what the commands say about themselves rather than from a
410
+ heredoc that had to be remembered separately.
411
+
412
+ ### Fixed
413
+
414
+ - Selecting a node in cluster mode faded the entire graph rather than
415
+ emphasising the selection. Dimming set opacity on the unrelated elements, but
416
+ in cluster mode those include the compound area boxes — and a parent's opacity
417
+ cascades to the nodes inside it, so dimming the boxes faded the very leaves
418
+ being highlighted. The highlight was real in each node's own opacity and
419
+ invisible on screen: measured parent-inclusive, the selection sat at 0.1
420
+ against an unrelated node's 0.01. `focusNode` now dims the leaves and edges
421
+ and never the `:parent` boxes, which is a no-op outside cluster mode where
422
+ there are no parents. After: selection and neighbours at 1, the rest at 0.1.
423
+
424
+ - A log's "Open in graph" button stayed visible, carrying a stale
425
+ `onclick` from the last map or concept — the "answers about a different file"
426
+ symptom, returning through CSS. The code hides it correctly
427
+ (`#fp-graph.hidden = true`), but `.btn.text{display:inline-flex}` outranks
428
+ `.btn[hidden]{display:none}` at equal specificity (0,2,0), so the later rule
429
+ won and the button rendered 143px wide with the attribute present. A
430
+ `.btn.text[hidden]` rule settles it, the same fix the sibling `.fp-head[hidden]`
431
+ already carried.
432
+
433
+ - Leaving the graph for another view and returning redrew it at a
434
+ fraction of its size, and stayed that way. The cause was misdiagnosed as a
435
+ resize race for months; tracing `cy.animate`'s caller showed the one animation
436
+ running was a *fit*. `fitGraph` computes zoom from the container's own width,
437
+ and the one-shot boot fit (`setTimeout(fitGraph, 400)`) fires on whatever view
438
+ is up by then — so leaving the graph inside that window fits a hidden 0×0
439
+ canvas, `(w-2*pad)/bb.w` goes negative, and the zoom clamps to `minZoom`.
440
+ `fitGraph` now returns early on a zero-size canvas and the graph keeps its last
441
+ good zoom. The hazard was already known at the other end: the boot fit is not
442
+ registered at all for a `?view=`/`?select=`/`#hash` deep link, whose comment
443
+ names this same min-zoom clamp — it was the navigate-away case that went
444
+ uncovered.
445
+
446
+ - `okf help`'s map advertised `search … [-e|--fuzzy]`, pairing a shorthand
447
+ it never expanded with an unrelated flag, so the one hint that an engine is
448
+ selectable read as though `-e` might *be* the engine switch. The row now says
449
+ `[--regexp|--fuzzy]`, which is what the command's own banner says; `-e` still
450
+ works and `search --help` still spells it out.
451
+ - **`okf skill <a> <b>` installed into `<a>` and exited 0.** It hand-rolled
452
+ its own argument handling instead of using the shared pair every `<dir>` verb
453
+ goes through, so a second destination was silently dropped — the user named two
454
+ places and the tool wrote one, saying nothing. It is a usage error now (exit 2),
455
+ refused before anything is written.
456
+
3
457
  ## [1.9.0] - 2026-07-19
4
458
 
459
+ ### Added
460
+
5
461
  - **`okf search` gains an opt-in full-text index engine.** `--engine index` — and
6
462
  `--fuzzy`, which implies it — routes to
7
463
  [minifts](https://github.com/serradura/minifts), the pure-Ruby port of the same
@@ -14,6 +470,93 @@
14
470
  browser's own setting. Search stays exact unless you ask;
15
471
  - **parity with the graph page**, which runs the same MiniSearch build, so the
16
472
  two rank identically when the index is named.
473
+
474
+ - **`--engine NAME` picks the engine outright**, for the case a capability flag
475
+ cannot express: a matching *model* requires nothing, so no flag selects one.
476
+ Naming an engine that cannot do what was also asked is a usage error naming one
477
+ that can (`--engine index -e` → *try --engine scan*), and an unknown name lists
478
+ what is available. `--help` reads the registry, so an addon's engine appears
479
+ without the CLI knowing it exists.
480
+
481
+ - **The graph can draw the index layer, under any layout.** The §6 map was
482
+ visible only inside file-tree mode, where a folder node stood in for a
483
+ directory's `index.md`. **Show indexes** makes it a layer: each map is a tile
484
+ edged to the concepts it lists and the maps below it, dressed by the same
485
+ selector as file-tree mode's folder node, because the two are the same thing
486
+ twice over — clicking either opens that directory's `index.md`. Both are accent
487
+ squares with dashed edges into them, so colour separates *kinds* rather than
488
+ modes: a directory is not a concept and no longer reads as one. Authorship shows
489
+ as form — solid where an author wrote a map, hollow and dashed where the bundle
490
+ only implies one — so the toggle reads as curation as much as navigation.
491
+ - **Moving between the modes lands in one click.** Tearing the layer down ran
492
+ its own layout while file-tree mode ran `breadthfirst` a beat later, two
493
+ layouts racing the same canvas; and because the layer is fetched, a promise
494
+ resolving after a mode change could land inside file-tree mode. A `relayout`
495
+ flag settles the first, a per-toggle ticket the second.
496
+ - **File-tree mode disables the toggle** rather than doubling the folders it
497
+ already draws.
498
+ - **One label on every file's graph button.** It read "Explore the knowledge
499
+ graph" on the root index and "Open core/ in graph" on a nested one, which made
500
+ a single action look like three. The question is the same whatever is open, so
501
+ the label is too — and it lives in the markup, where it cannot go stale.
502
+ - **Opening a map from the reader keeps the reader's graph.** It forced
503
+ file-tree mode, discarding whatever layout was running, and dimmed the canvas
504
+ to the map's immediate neighbours. It now switches the *layer* on rather than
505
+ the *mode* and leaves the layout alone. A reader already in file-tree mode
506
+ stays there.
507
+ - **Selecting anything emphasises it the same way.** A concept dimmed the graph
508
+ to its neighbourhood, a map did nothing at all, and a folder node did nothing
509
+ either — three meanings for one gesture. One `focusNode` now serves all three.
510
+ - **Drawn, never modelled.** `index.md` is reserved, so these nodes are built
511
+ from `/index` straight onto the canvas; `NODES`, `/catalog` and the type and
512
+ tag indexes never learn they exist. Filters pass them over — a map has no type
513
+ or tags — but a map whose concepts are all filtered away leaves with them.
514
+
515
+ - **A first-visit note tells a newcomer the index exists.** The `index.md` an
516
+ author wrote to be read first was reachable only by finding the Indexes tab and
517
+ clicking a row, so a reader meeting a bundle for the first time met unlabelled
518
+ dots with no way in. The page still opens on the graph — it is what makes a
519
+ bundle legible at a glance, at every width — and a dismissible note at the
520
+ bottom now says what the picture is, how to touch it, and where the index is.
521
+ **Read the index** goes straight there; the dismissal is remembered.
522
+ - **It absorbed the old mobile-only tip** rather than stacking a second banner
523
+ under it, and it is written for a finger throughout, since a phone is where a
524
+ first-time reader is least oriented.
525
+ - **The wording follows the device on two gates, not one.** What a reader does
526
+ follows `(pointer:coarse)` — a touch tablet in landscape is wider than 768px
527
+ and still taps; a narrow desktop window is narrower and still clicks. What a
528
+ reader can reach follows `(max-width:768px)`, because that is when the rail
529
+ collapses behind `☰`. Short viewports tighten; short *and* wide puts the
530
+ question beside the button, taking a landscape phone from half the screen to
531
+ under a third.
532
+ - **A second note points at `☰`** on compact layouts only, anchored under the
533
+ button it names rather than at the bottom of the screen. It fires on leaving
534
+ the graph by any route, so dismissing the first note does not cost it, and
535
+ opening `☰` answers it — but only once it is on screen, since `☰` is the only
536
+ way off the graph there and the first tap always comes first.
537
+ - **Deep links are unaffected**, and `?select=`/`#hash` now switch to the graph
538
+ before selecting, since the page can be standing elsewhere when they are read.
539
+
540
+ - The graph page's search box grows a full-text index. One MiniSearch index —
541
+ lazy-loaded from the CDN on first search, pinned to the `7.2.0` the Ruby
542
+ MiniSearch port tracks so an `okf search --engine index` result and the
543
+ browser's rank identically — now backs the graph, catalog and files views: ranked, multi-term
544
+ (`AND`), prefix (as-you-type) and typo-tolerant, over title, id, type, tags and
545
+ **description** — plus each concept's **body** wherever the page already holds
546
+ it (`okf render` bakes every body in, so a static file searches bodies offline;
547
+ the live server keeps bodies lazy, so its index stays metadata-only until a
548
+ backend body index arrives). The graph could not be searched by a leaf's
549
+ description before; now it can. The Files view's **Indexes** tab gets its own
550
+ full-text index too, over each `index.md`/`log.md`'s body — not just its
551
+ filename. Until an index loads — or if the CDN is unreachable — each view falls
552
+ back to its own substring filter, so the box is never dead.
553
+
554
+ - `Esc` clears the graph selection. A dense graph leaves almost no empty canvas
555
+ to click for deselecting; `Esc` now drops the highlight (and lets the URL hash
556
+ forget the node) the same way tapping empty canvas does.
557
+
558
+ ### Changed
559
+
17
560
  - **The default search is unchanged** — literal, case-insensitive substring
18
561
  matching over the same fields with the same weights as 1.8.0. The index is
19
562
  opt-in rather than default because a one-shot CLI builds an index, asks one
@@ -31,12 +574,7 @@
31
574
  field length, so a short concept dense in `7`, `2` and `0` can outrank the one
32
575
  that actually says `7.2.0`. The default has none of these, because raw-text
33
576
  matching has no tokenizer.
34
- - **`--engine NAME` picks the engine outright**, for the case a capability flag
35
- cannot express: a matching *model* requires nothing, so no flag selects one.
36
- Naming an engine that cannot do what was also asked is a usage error naming one
37
- that can (`--engine index -e` → *try --engine scan*), and an unknown name lists
38
- what is available. `--help` reads the registry, so an addon's engine appears
39
- without the CLI knowing it exists.
577
+
40
578
  - **Search engines are adapters.** `OKF::Bundle::Search` became a facade over N
41
579
  engines instead of one class with a `regexp ? scan : index` branch. The facade
42
580
  keeps everything that defines a result — documents, the row and its key order,
@@ -54,6 +592,7 @@
54
592
  match sets by design, so neither can be the oracle. Every registered engine
55
593
  runs the same contract, with capability-gated blocks for its own semantics,
56
594
  and a registered engine with no conformance class fails the suite.
595
+
57
596
  - **Cross-bundle search ranks one corpus under `--engine index`.** BM25 prices a
58
597
  term by how rare it is, so ranking each bundle separately and interleaving the
59
598
  lists would produce a ranking that looks sorted and compares nothing; the
@@ -62,51 +601,20 @@
62
601
  scores lower searched beside other bundles than alone. The default's scores are
63
602
  absolute and need no such treatment.
64
603
 
65
- - **The graph can draw the index layer, under any layout.** The §6 map was
66
- visible only inside file-tree mode, where a folder node stood in for a
67
- directory's `index.md`. **Show indexes** makes it a layer: each map is a tile
68
- edged to the concepts it lists and the maps below it, dressed by the same
69
- selector as file-tree mode's folder node, because the two are the same thing
70
- twice over — clicking either opens that directory's `index.md`. Both are accent
71
- squares with dashed edges into them, so colour separates *kinds* rather than
72
- modes: a directory is not a concept and no longer reads as one. Authorship shows
73
- as form — solid where an author wrote a map, hollow and dashed where the bundle
74
- only implies one — so the toggle reads as curation as much as navigation.
75
- - **Moving between the modes lands in one click.** Tearing the layer down ran
76
- its own layout while file-tree mode ran `breadthfirst` a beat later, two
77
- layouts racing the same canvas; and because the layer is fetched, a promise
78
- resolving after a mode change could land inside file-tree mode. A `relayout`
79
- flag settles the first, a per-toggle ticket the second.
80
- - **File-tree mode disables the toggle** rather than doubling the folders it
81
- already draws.
82
- - **One label on every file's graph button.** It read "Explore the knowledge
83
- graph" on the root index and "Open core/ in graph" on a nested one, which made
84
- a single action look like three. The question is the same whatever is open, so
85
- the label is too — and it lives in the markup, where it cannot go stale.
86
- - **Opening a map from the reader keeps the reader's graph.** It forced
87
- file-tree mode, discarding whatever layout was running, and dimmed the canvas
88
- to the map's immediate neighbours. It now switches the *layer* on rather than
89
- the *mode* and leaves the layout alone. A reader already in file-tree mode
90
- stays there.
91
- - **Selecting anything emphasises it the same way.** A concept dimmed the graph
92
- to its neighbourhood, a map did nothing at all, and a folder node did nothing
93
- either — three meanings for one gesture. One `focusNode` now serves all three.
94
- - **Drawn, never modelled.** `index.md` is reserved, so these nodes are built
95
- from `/index` straight onto the canvas; `NODES`, `/catalog` and the type and
96
- tag indexes never learn they exist. Filters pass them over — a map has no type
97
- or tags — but a map whose concepts are all filtered away leaves with them.
98
604
  - **Collapsing the root folds the file list away** on phones and tablets, where
99
605
  the list is stacked on top of the reader and closing the root otherwise left a
100
606
  single row above a column of nothing. Reopening the list undoes that collapse,
101
607
  so it is one gesture rather than two states to dig out of — the fold remembers
102
608
  *why* it happened, and a list folded because a file was opened comes back
103
609
  exactly as it was left.
610
+
104
611
  - **The bundle names its own root.** `(root)` and `/` are what a filesystem calls
105
612
  it, not what a reader does. The tree's root row, file-tree mode's root node, the
106
613
  index layer's root map and the inspector's directory map now all carry the name
107
614
  the page header already shows, `--title` included. `areaOf` keeps its own
108
615
  `(root)`: that is the area vocabulary `okf stats --by area` and `tags --by area`
109
616
  print, not a UI label.
617
+
110
618
  - **The Indexes tab dissolves into the file tree.** The authored layer lived on a
111
619
  second tab as a flat list of paths, which put a directory's own map somewhere
112
620
  other than the directory. `index.md` and `log.md` are rows now, at the top of
@@ -129,30 +637,7 @@
129
637
  - **Fixed on the way:** the reader header rendered empty — an unlabelled badge
130
638
  and a graph button pointing nowhere — whenever no file was open, because
131
639
  `.fp-head{display:flex}` outranks the UA sheet's `[hidden]{display:none}`.
132
- - **A first-visit note tells a newcomer the index exists.** The `index.md` an
133
- author wrote to be read first was reachable only by finding the Indexes tab and
134
- clicking a row, so a reader meeting a bundle for the first time met unlabelled
135
- dots with no way in. The page still opens on the graph — it is what makes a
136
- bundle legible at a glance, at every width — and a dismissible note at the
137
- bottom now says what the picture is, how to touch it, and where the index is.
138
- **Read the index** goes straight there; the dismissal is remembered.
139
- - **It absorbed the old mobile-only tip** rather than stacking a second banner
140
- under it, and it is written for a finger throughout, since a phone is where a
141
- first-time reader is least oriented.
142
- - **The wording follows the device on two gates, not one.** What a reader does
143
- follows `(pointer:coarse)` — a touch tablet in landscape is wider than 768px
144
- and still taps; a narrow desktop window is narrower and still clicks. What a
145
- reader can reach follows `(max-width:768px)`, because that is when the rail
146
- collapses behind `☰`. Short viewports tighten; short *and* wide puts the
147
- question beside the button, taking a landscape phone from half the screen to
148
- under a third.
149
- - **A second note points at `☰`** on compact layouts only, anchored under the
150
- button it names rather than at the bottom of the screen. It fires on leaving
151
- the graph by any route, so dismissing the first note does not cost it, and
152
- opening `☰` answers it — but only once it is on screen, since `☰` is the only
153
- way off the graph there and the first tap always comes first.
154
- - **Deep links are unaffected**, and `?select=`/`#hash` now switch to the graph
155
- before selecting, since the page can be standing elsewhere when they are read.
640
+
156
641
  - **The file tree nests.** Directories were a sorted list of full paths, which
157
642
  made `core` and `core/configurations` read as two unrelated folders and left
158
643
  the shape of a bundle invisible. Each row is now one path segment indented by
@@ -164,39 +649,6 @@
164
649
  folders standing instead of a single `(root)` row. Unfolding clears the whole
165
650
  set, root included, so a root closed by hand is still reversible from there.
166
651
 
167
- - The graph page's search box grows a full-text index. One MiniSearch index —
168
- lazy-loaded from the CDN on first search, pinned to the `7.2.0` the Ruby
169
- MiniSearch port tracks so an `okf search --engine index` result and the
170
- browser's rank identically — now backs the graph, catalog and files views: ranked, multi-term
171
- (`AND`), prefix (as-you-type) and typo-tolerant, over title, id, type, tags and
172
- **description** — plus each concept's **body** wherever the page already holds
173
- it (`okf render` bakes every body in, so a static file searches bodies offline;
174
- the live server keeps bodies lazy, so its index stays metadata-only until a
175
- backend body index arrives). The graph could not be searched by a leaf's
176
- description before; now it can. The Files view's **Indexes** tab gets its own
177
- full-text index too, over each `index.md`/`log.md`'s body — not just its
178
- filename. Until an index loads — or if the CDN is unreachable — each view falls
179
- back to its own substring filter, so the box is never dead.
180
- - The Files tree's folder collapse works during a search. An active search or
181
- type/tag filter used to force every folder open, so fold clicks did nothing;
182
- folders now honor their collapsed state always (a collapsed group still shows
183
- its header, so a match is never hidden). A **fold/unfold-all** control in the
184
- Files tab header collapses or expands every visible group at once.
185
- - Clustering no longer leaves phantom empty boxes. When a filter or a search hid
186
- every concept in an area, the cluster's labelled box lingered as an empty
187
- rectangle; the box now hides when no child survives and returns when one does —
188
- the same rule the fit already used to leave stale boxes out of view, now
189
- applied to what is drawn.
190
- - `Esc` clears the graph selection. A dense graph leaves almost no empty canvas
191
- to click for deselecting; `Esc` now drops the highlight (and lets the URL hash
192
- forget the node) the same way tapping empty canvas does.
193
- - A title-less concept now wears one name in every view. `catalog` and the §6
194
- index listing fell back a concept with no `title` to its full id — `area/thing`
195
- — while the graph node fell back blank-aware to the basename — `thing` — so the
196
- same concept answered to two labels across two views of one bundle, and a
197
- `title: ""` slipped past the nil-only `||` to catalog as an empty string. Both
198
- now fall back the graph's way (`File.basename`, blank-aware), so the label is
199
- the same wherever the concept appears.
200
652
  - `okf render` stops baking a redundant description map. The static page derived
201
653
  its `/node/meta` fragments from a separate `meta` payload that held nothing but
202
654
  each concept's description, HTML-escaped — data the embedded `catalog` already
@@ -204,6 +656,7 @@
204
656
  same escape the server applies at `/node/meta`), so the `meta` key leaves the
205
657
  baked payload and the description lives in one place. Both XSS guards are
206
658
  unchanged; `okf server` is untouched.
659
+
207
660
  - The bare not-a-directory error now teaches the registry grammar. A verb given
208
661
  a target that is neither a directory nor an `@ref` moved from
209
662
  `error: <arg> is not a directory` to
@@ -211,6 +664,7 @@
211
664
  so a consumer who typed a query or a bad path meets `@slug` addressing at the
212
665
  error instead of hunting for it. (`@all` stays out of the message — it is
213
666
  `search`'s alone, and the error seam is shared by every verb.)
667
+
214
668
  - The bundled skill teaches `@slug` as a first-class target and stops probing for
215
669
  the CLI. `SKILL.md`'s "Which directory?" is now "Which target?" — a leading `@`
216
670
  is a registry ref routed straight to `okf <verb> @slug`, with the fallback
@@ -219,8 +673,32 @@
219
673
  probe is gone: run the verb, and treat a shell `command not found` as the only
220
674
  signal to install, so the common case pays no guard round.
221
675
 
676
+ ### Fixed
677
+
678
+ - The Files tree's folder collapse works during a search. An active search or
679
+ type/tag filter used to force every folder open, so fold clicks did nothing;
680
+ folders now honor their collapsed state always (a collapsed group still shows
681
+ its header, so a match is never hidden). A **fold/unfold-all** control in the
682
+ Files tab header collapses or expands every visible group at once.
683
+
684
+ - Clustering no longer leaves phantom empty boxes. When a filter or a search hid
685
+ every concept in an area, the cluster's labelled box lingered as an empty
686
+ rectangle; the box now hides when no child survives and returns when one does —
687
+ the same rule the fit already used to leave stale boxes out of view, now
688
+ applied to what is drawn.
689
+
690
+ - A title-less concept now wears one name in every view. `catalog` and the §6
691
+ index listing fell back a concept with no `title` to its full id — `area/thing`
692
+ — while the graph node fell back blank-aware to the basename — `thing` — so the
693
+ same concept answered to two labels across two views of one bundle, and a
694
+ `title: ""` slipped past the nil-only `||` to catalog as an empty string. Both
695
+ now fall back the graph's way (`File.basename`, blank-aware), so the label is
696
+ the same wherever the concept appears.
697
+
222
698
  ## [1.8.0] - 2026-07-17
223
699
 
700
+ ### Added
701
+
224
702
  - A persistent bundle registry and a multi-bundle hub. `okf registry`
225
703
  (list / set / del / default / rename) keeps a per-user list in a plain JSON
226
704
  file at `$OKF_HOME/registry.json` (default `~/.okf`), and `okf server` reads
@@ -230,6 +708,7 @@
230
708
  switcher (⌘/Ctrl-K, or the rail button), `/b/` is a browsable index, and an
231
709
  unknown slug 404s as a page with a way home. The hub reads its bundles at
232
710
  boot — restart after registry changes.
711
+
233
712
  - The registry is **ordered, and the first entry still on disk is the default** —
234
713
  the bundle a bare `okf server` opens at `/`. `okf registry default @slug` moves
235
714
  that entry to the front, and `okf registry set --default` registers straight to
@@ -239,11 +718,13 @@
239
718
  vanished directory is stepped over rather than starred — `registry list`'s `*`
240
719
  always names the bundle `/` opens — and `registry default @slug` refuses one,
241
720
  just as `registry set` refuses to register a directory that is not there.
721
+
242
722
  - `$OKF_HOME` is the single lever on which registry a command reads: set it and
243
723
  every verb follows, from `okf registry list` to an `@slug` on `okf lint`. It
244
724
  names exactly one registry, with no fallback to `~/.okf` behind it, and an
245
725
  empty value counts as unset rather than planting `registry.json` in the
246
726
  current directory.
727
+
247
728
  - `@slug`: wherever a command takes a `<dir>`, `@slug` names a registered
248
729
  bundle and bare `@` the registry default — `okf lint @handbook`,
249
730
  `okf render @ -o graph.html`. A slug is normalized like registration was
@@ -254,6 +735,34 @@
254
735
  (`okf server @a @b`) mounts each bundle under its registered slug, the first
255
736
  at `/`, and a registered slug reserves its mount ahead of any plain
256
737
  directory that shares the name.
738
+
739
+ - `okf search` spans bundles: several leading `@slug`s, or `@all` for every
740
+ registered one. Rankings merge across bundles with every row labeled by its
741
+ bundle's slug (a `bundles` list and a per-match `slug` key in the JSON).
742
+ Asking for everything tolerates gaps — `@all` skips a bundle whose directory
743
+ has vanished, with a note — while naming one insists on it, and `@all @docs`
744
+ simply dedupes. `all` is reserved *in the registry*, on all three ways in: a
745
+ directory named `all/` registers as `all-2`, `--as all` is refused, and a row
746
+ already claiming the name in the registry file — hand-typed, or written before
747
+ the name was reserved — is read as `all-2`, so the reservation never strands a
748
+ registry it inherited. An ephemeral `okf server ./all` still mounts at
749
+ `/b/all/` — no registry, no refs, nothing to reserve.
750
+
751
+ - The inspector's type and tags are filter handles: clicking one focuses the
752
+ graph on that facet — the same jump the stats bars make — and clicking it again
753
+ clears it. The chip lights while its facet is the only filter in play, which is
754
+ exactly when a second click is an undo, so what you see and what the next click
755
+ does are the same question. With another filter set it re-focuses instead,
756
+ rather than throwing away more than the click put there.
757
+
758
+ - The graph page answers `?` with a sheet of every keyboard shortcut, reachable
759
+ from a rail button too — a shortcut list you can only open with a shortcut helps
760
+ whoever needs it least. `/` focuses the current view's search where it has one,
761
+ skipping the view that only reads; the sheet is written against the key handler
762
+ it documents, since a shortcut list that has drifted is worse than none.
763
+
764
+ ### Changed
765
+
257
766
  - `@slug` is spelled where it is used, not just where it is explained, and it is
258
767
  the one token — `okf help`'s map (`lint <dir|@slug>`) and each command's own
259
768
  banner show it the same way, with a note under the map defining it: the slug
@@ -263,25 +772,17 @@
263
772
  advertising a bare `<dir>`. The registry-editing verbs — `del`, `default`,
264
773
  `rename` — take the slug bare (no `@`) or as an `@slug`; the read verbs need
265
774
  the `@`, since a bare word there is a path.
775
+
266
776
  - `okf <command> -h` prints that command's own banner and flags. Help now answers
267
777
  on stdout with an exit code like every other command: it was OptionParser's
268
778
  officious handler, which printed past the caller's injected streams and ended
269
779
  the process with `exit` instead of returning a status.
270
- - `okf search` spans bundles: several leading `@slug`s, or `@all` for every
271
- registered one. Rankings merge across bundles with every row labeled by its
272
- bundle's slug (a `bundles` list and a per-match `slug` key in the JSON).
273
- Asking for everything tolerates gaps — `@all` skips a bundle whose directory
274
- has vanished, with a note — while naming one insists on it, and `@all @docs`
275
- simply dedupes. `all` is reserved *in the registry*, on all three ways in: a
276
- directory named `all/` registers as `all-2`, `--as all` is refused, and a row
277
- already claiming the name in the registry file — hand-typed, or written before
278
- the name was reserved — is read as `all-2`, so the reservation never strands a
279
- registry it inherited. An ephemeral `okf server ./all` still mounts at
280
- `/b/all/` — no registry, no refs, nothing to reserve.
780
+
281
781
  - The registry validates its file's shape, not just its JSON syntax: a
282
782
  hand-edited entry missing `path` is a usage error naming the file instead of
283
783
  a `TypeError`, and `okf registry --json set <dir>` — a subcommand behind a
284
784
  flag — is a usage error rather than silently listing and exiting 0.
785
+
285
786
  - The graph page's ⌘/Ctrl-K palette opens in **every** mode and reaches a view as
286
787
  well as a bundle. It was wired only behind a hub, so a standalone
287
788
  `okf server ./docs` and every `okf render` page — the two modes most people meet
@@ -291,12 +792,7 @@
291
792
  carries the rail's own icon and label, read from the rail so the two cannot
292
793
  drift. Where there is no hub there is no bundle to switch to, and views become
293
794
  the whole list.
294
- - The inspector's type and tags are filter handles: clicking one focuses the
295
- graph on that facet — the same jump the stats bars make — and clicking it again
296
- clears it. The chip lights while its facet is the only filter in play, which is
297
- exactly when a second click is an undo, so what you see and what the next click
298
- does are the same question. With another filter set it re-focuses instead,
299
- rather than throwing away more than the click put there.
795
+
300
796
  - The inspector's *Links to* / *Linked from* rows read as concepts rather than a
301
797
  wall of accent-coloured text. Each carries its type's dot — the colour that node
302
798
  already wears in the graph beside it — with the type named and the section
@@ -304,15 +800,14 @@
304
800
  title is read. The rows share one panel with hairline dividers and a hover fill:
305
801
  the container carries the click affordance, which leaves colour free to mean type
306
802
  and nothing else.
803
+
307
804
  - The inspector's widen chevron splits the screen instead of taking 70% of it. The
308
805
  panel drag-resizes, so the chevron is a preset rather than a maximum, and burying
309
806
  the graph to read one concept was the wrong thing to default to.
310
- - The graph page answers `?` with a sheet of every keyboard shortcut, reachable
311
- from a rail button too — a shortcut list you can only open with a shortcut helps
312
- whoever needs it least. `/` focuses the current view's search where it has one,
313
- skipping the view that only reads; the sheet is written against the key handler
314
- it documents, since a shortcut list that has drifted is worse than none.
315
- - Fixed: a file the reader could not **open** (permissions) threw its errno out
807
+
808
+ ### Fixed
809
+
810
+ - A file the reader could not **open** (permissions) threw its errno out
316
811
  of the read, so a single locked file took the whole bundle down through every
317
812
  verb that reads one — `lint`, `validate`, `catalog`, `server`, `registry set`
318
813
  — as a backtrace, under an exit code claiming the bundle was non-conformant.
@@ -322,12 +817,13 @@
322
817
  The stderr note reads `skipped N unusable file(s)` — it counts two kinds now,
323
818
  so it names neither and points at `validate`, which names both.
324
819
 
325
- - Fixed: `okf registry del <path>` could delete the wrong bundle. A path that
820
+ - `okf registry del <path>` could delete the wrong bundle. A path that
326
821
  matched no registered directory fell through to a normalized *slug* lookup, so
327
822
  `del ./notes` — naming a local directory — removed whichever entry happened to
328
823
  be slugged `notes`, wherever it pointed, and reported `removed notes` with exit
329
824
  0. An argument with a `/` in it now names a location and only a location.
330
- - Fixed: the registry read trusted stored slugs verbatim while both write paths
825
+
826
+ - The registry read trusted stored slugs verbatim while both write paths
331
827
  normalized, so a hand-typed `"slug": "My Docs"` listed fine but could not be
332
828
  named by `@my-docs`, `registry rename`, or `registry default` — the verbs that
333
829
  could repair it were the ones that could not see it. The read normalizes now,
@@ -335,31 +831,37 @@
335
831
  quote could reach a slug, and with it a DOM XSS in the server's bundle
336
832
  switcher, whose JS escape covered `& < >` but not quotes; the escape now covers
337
833
  quotes too, so the page does not depend on a guarantee three layers away.
338
- - Fixed: `okf lint` bucketed a whitespace-only `type` under its own literal
834
+
835
+ - `okf lint` bucketed a whitespace-only `type` under its own literal
339
836
  heading while `types`/`graph`/`stats` bucketed it as `Untyped`, so two verbs
340
837
  reported type inventories for the same bundle that would not reconcile. §9.2
341
838
  makes a blank type as non-conformant as a missing one; both sides say so now.
342
- - Fixed: `okf search <dir> --fields slug` passed the field guard and returned one
839
+
840
+ - `okf search <dir> --fields slug` passed the field guard and returned one
343
841
  empty object per match with exit 0. Only registry mode labels rows with a slug,
344
842
  so the two modes now declare the shape each actually emits and a path-named
345
843
  search names the fields it does have.
346
- - Fixed: `okf registry set` reported `(0 concepts)` for a bundle whose files it
844
+
845
+ - `okf registry set` reported `(0 concepts)` for a bundle whose files it
347
846
  could not read; it notes the skipped files like every other reading verb.
348
847
  `okf registry rename DOCS handbook` echoed the argv rather than the slug it
349
848
  renamed, naming a bundle that never existed. An unwritable `$OKF_HOME` raised
350
849
  a bare `Errno::EACCES` at exit 1 instead of a usage error at exit 2.
351
- - Fixed: a long path in the Files view's Indexes tab pushed its `map`/`log` badge
850
+
851
+ - A long path in the Files view's Indexes tab pushed its `map`/`log` badge
352
852
  past the right edge of the list. The badge was already pinned and unshrinkable;
353
853
  the filename beside it was the problem — a bare text node, and an anonymous flex
354
854
  item's automatic minimum size is the full width of its text, so it refused to
355
855
  give way and drove the badge out instead. It truncates now, with the full path on
356
856
  hover. Only a wide enough path in a narrow enough pane reached it, which is why
357
857
  it never showed on mobile, where the list runs full width.
358
- - Fixed: the bundle switcher scrolled its own first row out of view as it opened.
858
+
859
+ - The bundle switcher scrolled its own first row out of view as it opened.
359
860
  It rendered, and scrolled the active row into view, while the dialog was still
360
861
  hidden — and a list that is not being displayed measures zero, so the scroll
361
862
  landed arbitrarily.
362
- - Fixed: the `3` (Files) shortcut did nothing once the Indexes tab was open, and
863
+
864
+ - The `3` (Files) shortcut did nothing once the Indexes tab was open, and
363
865
  the palette's Index row would have blanked the page. "Index" is not a view —
364
866
  there is no `#view-index`, only the Files view showing its Indexes tab — so
365
867
  `setView('files')` from that tab early-returned, and `setView('index')` named a
@@ -370,11 +872,14 @@
370
872
 
371
873
  ## [1.7.0] - 2026-07-16
372
874
 
875
+ ### Added
876
+
373
877
  - `okf server`: responses are gzipped when the client accepts it
374
878
  (`Rack::Deflater` at the boot seam). Lossless and transparent — the browser
375
879
  decompresses automatically — and no new dependency, since `Rack::Deflater`
376
880
  ships inside rack. Clients that send no `Accept-Encoding` keep getting
377
881
  identity responses. `okf render`'s static HTML is untouched.
882
+
378
883
  - The agent skill gains a `migrate` verb (`playbooks/migrate.md`): convert
379
884
  existing documentation into a conformant bundle **in place** — frontmatter
380
885
  and reserved files added, bodies kept verbatim (`produce` keeps
@@ -382,11 +887,15 @@
382
887
  inference, the menu playbook now leads with it when a target already holds
383
888
  markdown docs, and pointing any verb at a directory that is not a bundle now
384
889
  suggests `migrate` instead of grinding through the validate errors.
890
+
891
+ ### Changed
892
+
385
893
  - The graph page's link-preview image points at the renamed
386
894
  `okfgem.com/og-demo-v3.png`. The site's OG art was refreshed to drop "Live
387
895
  Graph" from the package formula (it is `Agent Skill + CLI/Lib + Graph` now
388
896
  that `okf render` makes the graph live *or* static), and the filename carries
389
897
  the version so social scrapers pick the new art up.
898
+
390
899
  - The plugin's `/okf:gem` command is now a pass-through shim: it hands its
391
900
  arguments to the okf skill unchanged, making `SKILL.md` the single router
392
901
  for every channel. The routing prose the command used to duplicate had no
@@ -396,6 +905,8 @@
396
905
 
397
906
  ## [1.6.0] - 2026-07-15
398
907
 
908
+ ### Added
909
+
399
910
  - New CLI verb: `okf render <dir> [-o FILE]` — the live graph as one static,
400
911
  self-contained HTML file, so it hosts where a server can't (GitHub Pages, an
401
912
  object store, an attachment). It is the same page `okf server` serves, one
@@ -408,6 +919,7 @@
408
919
  through `DOMPurify.sanitize(marked.parse(...))`, so the trust boundary holds;
409
920
  the trade-off is weight — each body is inlined, so a big bundle makes a big
410
921
  file, and `okf server` stays the choice at scale.
922
+
411
923
  - Official Docker image: `ghcr.io/serradura/okf`, a portable CLI that runs every
412
924
  `okf` command (the graph server included) with no Ruby on the host. It is built
413
925
  from source and published multi-arch (`linux/amd64`, `linux/arm64`) to the
@@ -417,6 +929,8 @@
417
929
 
418
930
  ## [1.5.0] - 2026-07-13
419
931
 
932
+ ### Added
933
+
420
934
  - New CLI verb: `okf search <dir> <term…>` — deterministic ranked retrieval
421
935
  over concept metadata *and bodies*, the browser page's search brought to the
422
936
  CLI. Terms AND together as case-insensitive substrings, or as Ruby regexps
@@ -426,14 +940,17 @@
426
940
  and carry a bounded context snippet, so "which concept covers X?" costs a
427
941
  few rows instead of a body read. Advisory read: exit 0 even with no matches.
428
942
  Deliberately not fuzzy — the consuming agent is the fuzzy layer.
943
+
429
944
  - The skill learns retrieval as a first-class verb: a new `search` playbook
430
945
  (progressive disclosure end to end: ingest `okf index`, decide where to
431
946
  look, cut across with `okf search`, read only the winning bodies),
432
947
  search-aware routing in SKILL.md and the menu/consume playbooks, and
433
948
  `/okf:gem search <query>` first in the Claude Code plugin's routing.
949
+
434
950
  - Retrieval eval in the suite: the progressive path (index skeleton → search →
435
951
  one body) must answer a planted question in under 25% of the bytes of the
436
952
  full graph dump, so the playbook's economics stay true by construction.
953
+
437
954
  - Graph server: the authored layer joins the UI. The Files view carries two
438
955
  tabs — **Files** (the per-directory concept groups, foldable) and
439
956
  **Indexes** (the log first, as the chronological index, then every
@@ -447,19 +964,27 @@
447
964
  is fetched fresh on every read, so a just-appended entry shows without a
448
965
  restart. A reserved file's "Open in graph" jumps to its folder in the file
449
966
  tree, map in the inspector. New `/index` and `/log` endpoints back it all.
967
+
450
968
  - Graph server: Mermaid diagrams in concept bodies are click-to-inspect. A
451
969
  click (or tap) opens the diagram full screen — drag to pan, wheel or pinch
452
970
  to zoom, buttons and double-click reset, Esc closes — powered by
453
971
  [Panzoom](https://github.com/timmywil/panzoom), lazy-loaded from the CDN
454
972
  exactly like Mermaid itself.
973
+
974
+ ### Changed
975
+
455
976
  - The Claude Code plugin's `/okf:gem` command now weighs the shape of a
456
977
  free-form ask: a question about what the bundle knows routes through the
457
978
  search playbook and answers from retrieved concepts instead of guessing.
979
+
458
980
  - Skill efficiency audit: every playbook now takes the CLI's lean paths.
459
981
  `maintain` hunts affected concepts with `okf search` and pulls edges via
460
982
  `graph --json --minimal` instead of the full-body dump, `menu` reads the
461
983
  plain-text reports it only scans, and SKILL.md pins the discipline as a
462
984
  rule: skeleton first, bodies last.
985
+
986
+ ### Fixed
987
+
463
988
  - Docs: the CLI reference's server section now reflects the DOMPurify
464
989
  sanitization that landed in 1.1.0 (it still said bodies render unsanitized),
465
990
  and the server page's link-preview image points at the renamed
@@ -467,6 +992,8 @@
467
992
 
468
993
  ## [1.4.0] - 2026-07-12
469
994
 
995
+ ### Changed
996
+
470
997
  - Graph server UX round. Selecting a node now makes one camera move instead of
471
998
  two (the pan used to race the opening panel and the debounced canvas resize,
472
999
  a dizzying double movement; rapid clicks also queued animations — both fixed).
@@ -485,14 +1012,19 @@
485
1012
 
486
1013
  ## [1.3.0] - 2026-07-12
487
1014
 
1015
+ ### Added
1016
+
488
1017
  - The graph server page now emits link-preview metadata: Open Graph and Twitter
489
1018
  Card tags with a social image, plus `theme-color` and `color-scheme`, so a
490
1019
  shared `okf server` URL unfurls as a proper card in chat and social apps.
1020
+
491
1021
  - Docs: a themed README hero (light and dark), a GitHub social preview image,
492
1022
  and Website / Live demo / Claude Code plugin links.
493
1023
 
494
1024
  ## [1.2.0] - 2026-07-12
495
1025
 
1026
+ ### Added
1027
+
496
1028
  - Claude Code plugin. The repository now doubles as a plugin marketplace:
497
1029
  `/plugin marketplace add serradura/okf-gem`, then `/plugin install okf@okfgem`.
498
1030
  The plugin carries the canonical skill (a generated copy; `rake plugin:sync`
@@ -516,12 +1048,8 @@
516
1048
 
517
1049
  ## [1.1.0] - 2026-07-12
518
1050
 
519
- - The graph server now sanitizes every concept body before rendering it. The
520
- page runs marked's HTML output through [DOMPurify](https://github.com/cure53/DOMPurify)
521
- (loaded from the same CDN as Cytoscape and marked) on the way to the DOM, so a
522
- bundle carrying active content in a Markdown body can no longer script the
523
- viewer. Inlined graph data was already escaped through `json_for_script`; this
524
- closes the other path.
1051
+ ### Changed
1052
+
525
1053
  - `require "okf"` now loads the pure library only. The two argv-facing shells —
526
1054
  `OKF::CLI` and the `OKF::Skill` installer — load on demand, from `exe/okf` or
527
1055
  an explicit `require "okf/cli"` / `require "okf/skill"`. `optparse` moves with
@@ -529,28 +1057,45 @@
529
1057
  in-memory model and on-disk handles no longer pulls in the command-line
530
1058
  machinery. The CLI itself is unchanged.
531
1059
 
1060
+ ### Security
1061
+
1062
+ - The graph server now sanitizes every concept body before rendering it. The
1063
+ page runs marked's HTML output through [DOMPurify](https://github.com/cure53/DOMPurify)
1064
+ (loaded from the same CDN as Cytoscape and marked) on the way to the DOM, so a
1065
+ bundle carrying active content in a Markdown body can no longer script the
1066
+ viewer. Inlined graph data was already escaped through `json_for_script`; this
1067
+ closes the other path.
1068
+
532
1069
  ## [1.0.0] - 2026-07-12
533
1070
 
534
1071
  Initial release.
535
1072
 
1073
+ ### Added
1074
+
536
1075
  - `OKF::Concept` / `OKF::Bundle`: pure in-memory model of an OKF v0.1 bundle,
537
1076
  buildable straight from data (no disk) with link, citation, and markdown
538
1077
  round-trip primitives.
1078
+
539
1079
  - `OKF::Bundle::Validator`: the spec §9 conformance gate (hard errors) with the
540
1080
  spec's soft guidance reported as warnings — broken cross-links are tolerated,
541
1081
  as §5.3 requires.
1082
+
542
1083
  - `OKF::Bundle::Linter`: advisory curation-quality report across reachability,
543
1084
  backlog, completeness, freshness, provenance, and hygiene, with `--json` as a
544
1085
  machine substrate.
1086
+
545
1087
  - `OKF::Bundle::Graph`: the knowledge graph (nodes, edges, type/tag indexes) at
546
1088
  selectable fidelity.
1089
+
547
1090
  - On-disk handles: `OKF::Bundle::Folder`, `OKF::Bundle::Reader`,
548
1091
  `OKF::Bundle::Writer` (atomic, validate-before-publish), and
549
1092
  `OKF::Concept::File`.
1093
+
550
1094
  - `OKF::Server::App`: the interactive graph as a mountable Rack app — five views
551
1095
  (graph, catalog, files, tags, stats) with type/area/tag filtering throughout,
552
1096
  bodies fetched live from disk — served by a built-in WEBrick runner
553
1097
  (`okf server`).
1098
+
554
1099
  - `okf` CLI: `validate`, `lint`, `loose`, and `graph`, plus the read views as
555
1100
  text — `index`, `catalog`, `files`, `tags`, `types`, `stats` — at full parity
556
1101
  with the browser: every list view narrows with `--type`/`--area`/`--tag`
@@ -558,6 +1103,7 @@ Initial release.
558
1103
  `tags --by type|area` regroups the tag index per concept dimension with
559
1104
  within-group counts — the tag-curation view. `server` boots the graph page;
560
1105
  `skill` installs the companion skill.
1106
+
561
1107
  - `okf index`: a read view over the progressive-disclosure layer (spec §6) — one
562
1108
  entry per directory that holds concepts or carries an `index.md`, root first,
563
1109
  with its authored index body (frontmatter stripped), a type/tag rollup over the
@@ -565,19 +1111,36 @@ Initial release.
565
1111
  directory with concepts but no `index.md` has its listing synthesized (§6 permits
566
1112
  it) and is flagged. `--area` (repeatable), `--no-body`, and `--json`; advisory,
567
1113
  always exit 0. Backed by the pure `OKF::Bundle#directory_index`.
1114
+
568
1115
  - JSON output is **compact by default** across every emitting verb (the
569
1116
  token-efficient machine substrate, matching the server); `--pretty` indents it
570
1117
  for reading and implies `--json`. JSON semantics are identical either way — only
571
1118
  whitespace differs — so any parser is unaffected.
1119
+
572
1120
  - JSON property projection on the list views: `index`, `catalog`, and `files`
573
1121
  take `--fields a,b` (emit only these properties) or `--except a,b` (emit all but
574
1122
  these), so an agent never pays tokens for fields it will not read. The flags are
575
1123
  mutually exclusive, imply `--json`, match property names case-insensitively, and
576
1124
  reject an unknown name (exit 2) listing the valid ones; `okf index --no-body` is
577
1125
  shorthand for dropping the `body` field.
1126
+
578
1127
  - Bundled companion agent skill (`okf skill <dest>`): SKILL.md carrying the
579
1128
  judgment (the CLI surface stays self-describing via `--help`) — including the
580
1129
  orient-before-you-read protocol and the CLI/judgment boundary — the OKF v0.1
581
1130
  spec, authoring and CLI references (tag-vocabulary curation, the SPEC-section
582
1131
  map, the closeout gate), and concept/index/log templates.
1132
+
583
1133
  - Runs on Ruby >= 2.4 with two runtime dependencies: rack and webrick.
1134
+
1135
+ [1.11.0]: https://github.com/serradura/okf-gem/compare/v1.10.0...v1.11.0
1136
+ [1.10.0]: https://github.com/serradura/okf-gem/compare/v1.9.0...v1.10.0
1137
+ [1.9.0]: https://github.com/serradura/okf-gem/compare/v1.8.0...v1.9.0
1138
+ [1.8.0]: https://github.com/serradura/okf-gem/compare/v1.7.0...v1.8.0
1139
+ [1.7.0]: https://github.com/serradura/okf-gem/compare/v1.6.0...v1.7.0
1140
+ [1.6.0]: https://github.com/serradura/okf-gem/compare/v1.5.0...v1.6.0
1141
+ [1.5.0]: https://github.com/serradura/okf-gem/compare/v1.4.0...v1.5.0
1142
+ [1.4.0]: https://github.com/serradura/okf-gem/compare/v1.3.0...v1.4.0
1143
+ [1.3.0]: https://github.com/serradura/okf-gem/compare/v1.2.0...v1.3.0
1144
+ [1.2.0]: https://github.com/serradura/okf-gem/compare/v1.1.0...v1.2.0
1145
+ [1.1.0]: https://github.com/serradura/okf-gem/compare/v1.0.0...v1.1.0
1146
+ [1.0.0]: https://github.com/serradura/okf-gem/releases/tag/v1.0.0