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