okf 1.7.0 → 1.9.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.
@@ -115,8 +115,9 @@ bundle-root [root-index](../templates/root-index.md), [log](../templates/log.md)
115
115
  ## Playbooks
116
116
 
117
117
  The step-by-step playbooks live in [../playbooks/](../playbooks/), one file per
118
- verb (produce, maintain, consume, curate, doctor), routed by the Commands table
119
- in [SKILL.md](../SKILL.md). The Closeout below is their shared finishing gate.
118
+ verb (search, produce, migrate, maintain, consume, curate, doctor), routed by the
119
+ Commands table in [SKILL.md](../SKILL.md). The Closeout below is their shared
120
+ finishing gate.
120
121
 
121
122
  ## Closeout — the finishing gate
122
123
 
@@ -6,14 +6,13 @@ reimplemented in this skill. They run the deterministic `okf` executable shipped
6
6
  the companion gem — the single source of truth for OKF mechanics. Your job is to
7
7
  invoke it correctly and interpret the result, not to reason out conformance by hand.
8
8
 
9
- ## Presence guard
9
+ ## When it isn't installed
10
10
 
11
- Check the tool exists before relying on it. If it is missing, the gem is not
12
- installed say so and stop; never fabricate a result:
13
-
14
- ```bash
15
- command -v okf >/dev/null || echo "okf CLI not found install it: 'gem install okf' (or from a checkout: 'cd gem && bundle exec rake install')"
16
- ```
11
+ Don't probe for the tool before using it just run the verb. A shell `okf:
12
+ command not found` is the only thing that means the gem isn't installed: say so
13
+ and stop (`gem install okf`, or from a checkout `cd gem && bundle exec rake
14
+ install`); never fabricate a result. Any line that starts `error:` is the CLI
15
+ *answering*a bundle or usage result to read, not a missing toolchain.
17
16
 
18
17
  ## Invocation
19
18
 
@@ -38,9 +37,36 @@ difference between a few hundred bytes and hundreds of KB, since the per-item ro
38
37
  (`listing`) dominate at scale. `okf index --no-body` is shorthand for dropping just
39
38
  `body`.
40
39
 
40
+ **Every output names its bundle.** Two keys, one meaning each: `bundle` is
41
+ always a directory, `slug` always a registry slug. Name a bundle by `@slug` and
42
+ the answer comes back in that identity — `OKF lint — @handbook (/path/to/one)`,
43
+ and `{ "bundle": "/path/to/one", "slug": "handbook", … }` — so an agent holding
44
+ several bundles never has to remember which invocation produced which output.
45
+ A bundle named by path carries no `slug`: it may not have one, and inventing a
46
+ name it was never given would imply a registration that does not exist.
47
+
48
+ **@slug — point any verb at a registered bundle.** Wherever a `<dir>` goes,
49
+ `@slug` names a bundle registered via `okf registry set`, and bare `@` the
50
+ registry's default. They resolve through `$OKF_HOME` (default `~/.okf`) — the
51
+ single lever on which registry *any* verb reads, and it names exactly one, with
52
+ no fallback behind it. The slug is normalized as registration
53
+ normalized it — `@One` finds the bundle from dir `One` — but never to a
54
+ placeholder: `@***` names nothing, not a bundle. An unknown slug, a
55
+ registered-but-gone directory, or a malformed registry file is a usage error
56
+ (exit 2) whose message names the registry file consulted and the next move —
57
+ an explicit ask fails hard, never silently skipped. So `okf lint @handbook`
58
+ or `okf index @` work from any directory, no path recall needed.
59
+
41
60
  **Exit codes:** `0` success · `1` non-conformant bundle (or a `lint --fail-on`
42
61
  threshold crossed) · `2` usage error. `graph`, `server`, and `render` are best-effort
43
- (§9): a file with invalid frontmatter is skipped and noted on stderr, never fatal.
62
+ (§9): a file the reader cannot use frontmatter that will not parse, or a file it
63
+ cannot open at all — is skipped and noted on stderr, never fatal. The note counts;
64
+ `validate` names each file and why.
65
+
66
+ **One bundle per verb, except two.** Only `search` merges several bundles and only
67
+ `server` mounts them; hand a second bundle to any other verb — two dirs, two refs,
68
+ or a mix — and it is a usage error (exit 2), never a silent answer about the first.
69
+ To ask the same question of several bundles, ask `search`, or ask each in turn.
44
70
 
45
71
  ## validate — the hard gate (§9)
46
72
 
@@ -113,22 +139,121 @@ defect — a terminal leaf (a backlog item, a spec reference) can be loose by de
113
139
  The browser page's search brought to the CLI and extended to bodies, so "which
114
140
  concept covers X?" costs rows, not body reads. `okf search <dir> <term…>`:
115
141
  terms AND together — every term must hit at least one searched field, not
116
- necessarily the same one — as case-insensitive substrings, or as Ruby regular
117
- expressions with `--regexp`/`-e` (an invalid pattern is a usage error, exit 2).
142
+ necessarily the same one — matched **literally against raw text** by default, or
143
+ as Ruby regular expressions with `--regexp`/`-e` (an invalid pattern is a usage
144
+ error, exit 2). `--fuzzy` forgives typos; pairing it with `-e` is a usage error,
145
+ since a pattern is matched literally rather than by edit distance.
118
146
  `--in a,b` restricts the searched fields (title, id, tags, type, description,
119
147
  body); the shared `--type/--area/--tag` filters narrow the candidates *first*,
120
148
  so a search scoped by what `index` taught you stays surgical.
121
149
 
122
- Rows rank by **where** they hit title 5, id 4, tags 3, type/description 2,
123
- body 1, summed over matched fields and carry one bounded context snippet from
124
- the strongest match that needs context (description or body). Deliberately not
125
- fuzzy: the consuming agent is the fuzzy layer when terms miss, learn the
126
- bundle's vocabulary from `tags`/`types` and re-ask in its own words, rather
127
- than hammering synonyms. Advisory read: **exit 0 even with zero matches**.
128
- JSON: `{ bundle, query, count, matches: [{ id, title, type, area, tags,
129
- matched, score, snippet }] }`, projectable with `--fields/--except`. The
130
- retrieval procedure that puts this verb in sequence map first, finder second,
131
- bodies last is the [search playbook](../playbooks/search.md).
150
+ **The default is exact, so an exact query means what it looks like.** A phrase in
151
+ one argument (`"dedup key"`), a dotted version (`7.2.0`), an underscored
152
+ identifier (`customer_id`), a mid-word fragment (`ustomer`) and a word written in
153
+ `backticks` all match literally. This is what the scan engine buys, and it is the
154
+ default precisely because those queries are the common ones and the alternative
155
+ loses them silently. <!-- rule:okf-search-exact-identifiers -->
156
+
157
+ **`--engine index` is the other engine, and the one to reach for when ranking
158
+ matters more than exactness.** The engine is normally chosen by what the query
159
+ needs`--fuzzy` routes to the index, anything else stays on the default scan —
160
+ and nothing is printed about the choice. `--engine NAME` overrides that for the
161
+ case the flags cannot express: a matching *model* requires no capability, so no
162
+ flag selects one. Under the index, terms match whole tokens and their prefixes
163
+ (`dedup` finds `deduplication`), rows rank by BM25+, and it is the engine the
164
+ browser page runs — so name it when reconciling a CLI answer with the page. The
165
+ cost is real: its tokenizer splits on punctuation, so identifiers shatter
166
+ (`customer_id` → `customer` + `id`), an infix finds nothing, and a backtick is
167
+ never split off at all, so a word inside a code span is unfindable — a large
168
+ silent loss, since technical prose is full of them. **Do not count on ranking to
169
+ rescue it** — BM25 normalizes by field length, so a short concept dense in `7`,
170
+ `2` and `0` can outrank the one that actually says `7.2.0`. Naming an engine that
171
+ cannot do what you also asked (`--engine index -e`) is a usage error naming one
172
+ that can. <!-- rule:okf-search-engine-choice -->
173
+
174
+ **The capabilities, and which engine has them.** An engine is selected by what
175
+ the query *requires*; only a matching model has to be named, because requiring
176
+ nothing is not something a flag can express:
177
+
178
+ | Flag | Capability | Engine | What it does |
179
+ |---|---|---|---|
180
+ | *(none)* | — | scan | literal substring over raw text; scores by summed field weight |
181
+ | `-e` / `--regexp` | `regexp` | scan | each term is a Ruby regexp, case-insensitive; invalid → exit 2 |
182
+ | `--fuzzy` | `fuzzy` | **index** | edit distance 0.2 × term length — and switches engine |
183
+ | `--engine index` | — | index | whole-token + prefix matching, BM25+ ranking, browser parity |
184
+ | `--engine scan` | — | scan | the default, spelled out |
185
+
186
+ Two consequences worth holding. **`--fuzzy` is an engine switch, not a mode**: it
187
+ carries the whole index with it, so a run that wanted one typo forgiven also gets
188
+ shattered identifiers and unfindable code spans — fix the spelling and stay on
189
+ the default when you can. And **`-e` moves nothing** now, because the default
190
+ engine already offers `regexp`; it changes how a term is *read* (pattern rather
191
+ than literal), not where it is matched. <!-- rule:okf-search-fuzzy-is-a-switch -->
192
+
193
+ `prefix` is a capability the index declares but no flag selects — it is always on
194
+ there. **It is not a reason to reach for the index**: a substring match already
195
+ covers every prefix and then some, so `dedup` finds `deduplication` under both
196
+ engines, while `duplication` and `uplicat` find it under the default only. Prefix
197
+ is what the index needs to catch up to raw text, not a capability it adds on top.
198
+ The index's real advantages over the default are exactly three — relevance
199
+ ranking, typo tolerance, and page parity.
200
+
201
+ **Search spans bundles.** Leading @refs pick several registered bundles
202
+ (`okf search @handbook @notes auth`); **`@all`** is the ref that means every one.
203
+ Rows from different bundles are ranked together and comparable, and each row
204
+ carries its bundle's slug. Under `--engine index` the bundles go into **one
205
+ corpus** — BM25 prices a term by how rare it is, so separately-ranked lists would
206
+ not compare — which makes a score relative to the whole answer: the same concept
207
+ scores lower searched beside others than searched alone. The default scan needs
208
+ no such trick — its score is absolute, so a row is worth the same either way.
209
+ This is the
210
+ cross-bundle retrieval the in-page search does not have: one question, every
211
+ bundle you keep. <!-- rule:okf-search-all -->
212
+
213
+ `@all` is a ref, not a flag, which is what keeps the grammar single: slot 1 is
214
+ always a bundle identity, so a directory there is a directory and nothing can
215
+ flip it into a term. Being a ref, it is normalized like one — `@ALL` and `@All`
216
+ name every bundle just as `@One` names the bundle registered from dir `One`. It composes accordingly — `@all @docs` expands and dedupes
217
+ (all ⊇ docs), needing no diagnostic. **Asking for everything tolerates gaps;
218
+ naming one bundle demands it**: `@all` skips a bundle whose directory has
219
+ vanished with a note on stderr, while `@docs` fails hard. `@all` is only
220
+ `search`'s: every other verb answers about one bundle, so it refuses `@all` by
221
+ name rather than letting the answer depend on how many bundles you happen to
222
+ have registered. `all` is reserved as a slug — a directory named `all/` registers
223
+ as `all-2`, `--as all` is refused, and an `all` row already in the registry file
224
+ (hand-typed, or written before the name was reserved) is read as `all-2` rather
225
+ than taken as grounds to reject the file — so `@all` is never ambiguous, and the
226
+ reservation never strands a registry it inherited. **The read normalizes every
227
+ slug** the same way registration would, so a hand-typed `"slug": "My Docs"` lists
228
+ and resolves as `my-docs`; an entry the listing shows is always an entry `@slug`,
229
+ `rename`, and `default` can name.
230
+
231
+ `--fields` projects the shape the mode actually emits: `slug` is available in
232
+ registry mode, and a usage error naming the real fields on a path-named search,
233
+ which has no slug to give. Two sharp edges: every *leading* @-arg is taken as a ref, so a literal @-term
234
+ (`@babel/core`, a Ruby `@ivar`) needs a non-@ term before it or `-e '\@term'` —
235
+ the CLI notes both traps on stderr — and any ref, even one, switches the JSON
236
+ envelope (next paragraph).
237
+
238
+ Rows rank by where they hit — title 5, id 4, tags 3, type/description 2, body 1 —
239
+ summed as an absolute score by the default scan, and carried as per-field boost
240
+ into **BM25+** under `--engine index`. Each row carries one bounded context
241
+ snippet from the strongest match that needs context (description or body). Every row still names the fields that hit (`matched`), so a result stays
242
+ citable rather than being a bare relevance number. Exact by default: the
243
+ consuming agent is the fuzzy layer — when terms miss, learn the bundle's
244
+ vocabulary from `tags`/`types` and re-ask in its own words, rather than
245
+ hammering synonyms or reaching for `--fuzzy` before you have looked. Advisory read: **exit 0 even with zero matches**.
246
+ JSON, plain-dir mode: `{ bundle, query, count, matches: [{ id, title, type,
247
+ area, tags, matched, score, snippet }] }`. Registry mode — any leading @ref,
248
+ `@all` among them — swaps the envelope: `{ bundles: [{ slug, dir }, …],
249
+ query, count, matches: [{ slug, id, … }] }`; a parser must branch on which form
250
+ it called. The head maps each slug to its dir once, so a row resolves to
251
+ `<dir>/<id>.md` without a second lookup and without repeating a path per row.
252
+ Both are projectable with `--fields/--except`, and projection is literal — when
253
+ merging bundles, put `slug` in your `--fields` list or the row label drops and
254
+ same-id concepts from different bundles become indistinguishable. The retrieval procedure that puts this verb in sequence —
255
+ map first, finder second, bodies last — is the
256
+ [search playbook](../playbooks/search.md).
132
257
 
133
258
  ## index — the progressive-disclosure map (§6)
134
259
 
@@ -216,6 +341,56 @@ just-appended entry shows without a restart. `?view=index` jumps straight to
216
341
  the Indexes tab. It is a Rack app, so the same server can be mounted in a
217
342
  host app (e.g. Rails).
218
343
 
344
+ **Hosting many bundles (the hub).** `okf server` takes zero or more dirs.
345
+ One dir is the classic single bundle at `/`. Two or more mounts each under
346
+ `/b/<slug>/` behind a hub, `/` redirects to the default, and `/b/` is a
347
+ self-contained **bundle index** (every hosted bundle, concept counts, default
348
+ marked — the browser counterpart of `okf registry`). An unknown slug 404s as a
349
+ page listing the hosted bundles, so a stale bookmark after a rename gets a way
350
+ home. With **no** dir it serves the *persistent registry*, a plain JSON file
351
+ under `$OKF_HOME` (default `~/.okf`), managed by the
352
+ `okf registry` umbrella — like git's `remote` family, and split by what each
353
+ verb keys on. **Entry verbs** take a path: `okf registry set <dir>` adds it
354
+ (slug from the basename, or `--as`, which errors on a collision; `--default`
355
+ puts it first), and because the entry is keyed by path, `set` on an
356
+ already-registered dir updates it in place — refreshing its title, and renaming
357
+ it when `--as` is given. `okf registry del <dir|@slug>` removes one — by name, so an entry whose
358
+ directory is already gone still deletes. Slug *or* dir, never both readings at
359
+ once: an argument with a `/` in it names a location and only a location, so
360
+ `del ./notes` refuses when no entry points there rather than stripping to the
361
+ slug `notes` and deleting a bundle somewhere else entirely.
362
+ <!-- rule:okf-registry-del-path-or-slug -->
363
+ **Slug verbs** take the name — bare, or as an `@slug`: `okf registry default <@slug>`
364
+ chooses which bundle `/` opens **by moving that entry to the front**, and
365
+ `okf registry rename <@slug> <new>` renames a slug (mount path and switcher
366
+ name) — `<new>` is a name being minted, so it is never a ref. The registry is ordered and **the first entry still on disk is the
367
+ default** — that is the whole rule, so the first bundle you register is the
368
+ default until you move another one, a rename keeps its position, and a `del`
369
+ promotes whatever is next. A vanished directory is stepped over (the server
370
+ cannot open one, so starring it would name a bundle `/` never serves), and
371
+ `registry default @slug` refuses one outright — the same refusal `registry set`
372
+ gives a directory that is not there. The file is hand-editable and reorders
373
+ visibly, which is the point: there is no stored slug that can dangle.
374
+ <!-- rule:okf-registry-default-position -->
375
+ `okf registry list` (or a bare
376
+ `okf registry`) stars the default and flags vanished dirs `(missing)` — the
377
+ server skips those with a note; `--json` answers
378
+ `{ registry: <file>, count, bundles: [{ slug, title, dir, mount, default,
379
+ missing }] }`, naming the file it read so a `$OKF_HOME` mismatch is visible. The hub roster is a
380
+ **boot-time snapshot**: restart `okf server` after registry changes. Behind a
381
+ hub the page gains a **bundle switcher** (⌘/Ctrl-K, or the rail button with its
382
+ bundle-count badge): the current bundle is pinned, the default chipped; ⏎
383
+ opens, ⌘/Ctrl-⏎ opens a new tab, and the current view carries over. Switching
384
+ is a server-only affordance — a static `render` file has no siblings and shows
385
+ none.
386
+
387
+ **Bundle-less run.** Register bundles once, then `okf server` (no dir) hosts
388
+ them all with the registry's first entry still on disk at `/` — the way to keep
389
+ several bundles a keystroke apart without re-passing paths.
390
+ `okf server @a @b` serves a registry subset, each mounted under its registered
391
+ slug — but as with any dirs-given run, the *first argument* lands at `/`; the
392
+ registry's own order applies only to the bundle-less run.
393
+
219
394
  **Trust boundary:** the page renders each fetched markdown body through
220
395
  DOMPurify and escapes everything it inlines (every `<` in the graph data is
221
396
  escaped, so it cannot break out of its `<script>`), but it still loads its
@@ -242,10 +417,12 @@ you trust.
242
417
 
243
418
  ## graph — the raw structure
244
419
 
245
- Prints the node/edge graph. `--json` emits a machine-readable dump (`nodes` with
420
+ Prints the node/edge graph. `--json` emits a machine-readable dump the
421
+ `bundle`/`slug` head every view carries, then `nodes` (with
246
422
  `id`/`type`/`title`/`description`/`tags` **and, by default, every `body`** — the
247
- part that dominates the bytes on a real bundle plus `edges`) you can pipe into
248
- other analysis. To *plan* a traversal, structure is all you need: `--no-body`
423
+ part that dominates the bytes on a real bundle) plus `edges` you can pipe into
424
+ other analysis. A concept with a missing *or blank* `type` indexes under
425
+ `Untyped`: §9.2 rejects both identically, so both land in one bucket. To *plan* a traversal, structure is all you need: `--no-body`
249
426
  drops each node's body, and `--minimal` ships only `id`/`title` plus the type/tag
250
427
  indexes — the lean shape the `server` page boots from. Reach for the full dump
251
428
  only when the task truly consumes every body; for one question, the
data/lib/okf/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OKF
4
- VERSION = "1.7.0"
4
+ VERSION = "1.9.0"
5
5
  end
data/lib/okf.rb CHANGED
@@ -40,6 +40,14 @@ module OKF
40
40
  require "okf/bundle"
41
41
  require "okf/bundle/graph"
42
42
  require "okf/bundle/search"
43
+ # These two lines ARE the engine preference order. Each engine registers itself
44
+ # at load, `Search.engines` is registration order, and the router walks it after
45
+ # putting DEFAULT_ENGINE first — so reordering these requires reorders which
46
+ # engine answers a query two engines could both answer. `loading_test.rb` pins
47
+ # the result (`[:index, :scan]`) so the coupling cannot drift unnoticed, but the
48
+ # coupling is here, not there.
49
+ require "okf/bundle/search/index"
50
+ require "okf/bundle/search/scan"
43
51
  require "okf/bundle/validator"
44
52
  require "okf/bundle/validator/result"
45
53
  require "okf/bundle/linter"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: okf
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rodrigo Serradura
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: '1.4'
40
+ - !ruby/object:Gem::Dependency
41
+ name: minifts
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.0'
40
54
  description: |
41
55
  OKF (Open Knowledge Format) is portable knowledge: Markdown files with YAML
42
56
  frontmatter that both humans and agents read from one source. This gem is the
@@ -66,6 +80,8 @@ files:
66
80
  - lib/okf/bundle/linter/report.rb
67
81
  - lib/okf/bundle/reader.rb
68
82
  - lib/okf/bundle/search.rb
83
+ - lib/okf/bundle/search/index.rb
84
+ - lib/okf/bundle/search/scan.rb
69
85
  - lib/okf/bundle/validator.rb
70
86
  - lib/okf/bundle/validator/result.rb
71
87
  - lib/okf/bundle/writer.rb
@@ -76,9 +92,11 @@ files:
76
92
  - lib/okf/markdown/frontmatter.rb
77
93
  - lib/okf/markdown/links.rb
78
94
  - lib/okf/path.rb
95
+ - lib/okf/registry.rb
96
+ - lib/okf/render/graph.rb
97
+ - lib/okf/render/graph/template.html.erb
79
98
  - lib/okf/server/app.rb
80
- - lib/okf/server/graph.rb
81
- - lib/okf/server/graph/template.html.erb
99
+ - lib/okf/server/hub.rb
82
100
  - lib/okf/server/runner.rb
83
101
  - lib/okf/skill.rb
84
102
  - lib/okf/skill/SKILL.md