okf 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.okf/capabilities/agent-skill.md +112 -0
  3. data/.okf/capabilities/bundles-manager.md +144 -0
  4. data/.okf/capabilities/graph-server.md +678 -0
  5. data/.okf/capabilities/index.md +26 -0
  6. data/.okf/capabilities/library-api.md +82 -0
  7. data/.okf/capabilities/linter.md +83 -0
  8. data/.okf/capabilities/read-views.md +228 -0
  9. data/.okf/capabilities/render.md +66 -0
  10. data/.okf/capabilities/search.md +297 -0
  11. data/.okf/capabilities/validator.md +60 -0
  12. data/.okf/cli.md +214 -0
  13. data/.okf/design/browser-tests.md +211 -0
  14. data/.okf/design/core-shell-split.md +73 -0
  15. data/.okf/design/index.md +17 -0
  16. data/.okf/design/integration-first.md +140 -0
  17. data/.okf/design/packaging.md +65 -0
  18. data/.okf/design/ruby-floor.md +53 -0
  19. data/.okf/design/runtime-dependencies.md +82 -0
  20. data/.okf/design/search-engines.md +154 -0
  21. data/.okf/design/server-trust-boundary.md +139 -0
  22. data/.okf/index.md +40 -0
  23. data/.okf/log.md +724 -0
  24. data/.okf/model/bundle.md +47 -0
  25. data/.okf/model/concept.md +75 -0
  26. data/.okf/model/graph.md +59 -0
  27. data/.okf/model/index.md +9 -0
  28. data/.okf/model/skeleton.md +76 -0
  29. data/.okf/overview.md +87 -0
  30. data/.okf/registry.md +432 -0
  31. data/.okf/structure/format-layer.md +59 -0
  32. data/.okf/structure/index.md +22 -0
  33. data/.okf/structure/search.md +53 -0
  34. data/.okf/structure/the-analysers.md +60 -0
  35. data/.okf/structure/the-cli.md +99 -0
  36. data/.okf/structure/the-disk-shell.md +76 -0
  37. data/.okf/structure/the-model.md +81 -0
  38. data/.okf/structure/the-server.md +74 -0
  39. data/.okf/structure/the-skill.md +52 -0
  40. data/.okf/testing/adding-a-verb.md +76 -0
  41. data/.okf/testing/index.md +12 -0
  42. data/.okf/testing/the-harness.md +45 -0
  43. data/CHANGELOG.md +161 -16
  44. data/README.md +226 -17
  45. data/lib/okf/cli/command.rb +8 -3
  46. data/lib/okf/cli/registry.rb +306 -47
  47. data/lib/okf/cli.rb +1 -1
  48. data/lib/okf/registry.rb +448 -22
  49. data/lib/okf/render/graph/template.html.erb +6 -2
  50. data/lib/okf/server/hub.rb +6 -2
  51. data/lib/okf/skill/reference/cli/registry.md +49 -6
  52. data/lib/okf/skill/reference/cli.md +1 -1
  53. data/lib/okf/version.rb +1 -1
  54. metadata +46 -5
@@ -0,0 +1,154 @@
1
+ ---
2
+ type: Constraint
3
+ title: Search engines are adapters, and the facade owns the row
4
+ description: One facade over N retrieval engines — the scan by default, the index when a query needs it or names it — with a shared conformance suite standing in for the oracle rule that multiple engines made impossible.
5
+ resource: gems/okf/lib/okf/bundle/search.rb
6
+ tags: [architecture, search, extensibility, testing]
7
+ generated:
8
+ by: human:maintainer
9
+ at: 2026-07-24T12:00:00Z
10
+ sources:
11
+ - title: gems/okf/lib/okf/bundle/search.rb
12
+ resource: https://github.com/serradura/okf/blob/main/gems/okf/lib/okf/bundle/search.rb
13
+ - title: gems/okf/test/unit/bundle/search/engine_conformance.rb
14
+ resource: https://github.com/serradura/okf/blob/main/gems/okf/test/unit/bundle/search/engine_conformance.rb
15
+ - title: gems/okf/test/unit/bundle/search/accepted_losses_test.rb
16
+ resource: https://github.com/serradura/okf/blob/main/gems/okf/test/unit/bundle/search/accepted_losses_test.rb
17
+ ---
18
+
19
+ # Overview
20
+
21
+ [`OKF::Bundle::Search`](../capabilities/search.md) is a **facade**, not an
22
+ implementation. It owns everything that defines what a result *is*, and delegates
23
+ only the retrieval question to an engine:
24
+
25
+ | The facade owns | An engine answers |
26
+ |---|---|
27
+ | entry points (`.call`, `.across`) | which documents match |
28
+ | document assembly and the `"<slug>\0<id>"` key | how well each matches |
29
+ | the row hash and its key order | which fields the terms hit |
30
+ | the snippet window, `top_dir`, the final sort | which matcher to point the snippet at |
31
+
32
+ The split exists because an engine that built its own rows could disagree about
33
+ what a match *means* — two engines, two answer shapes, and a merged ranking that
34
+ looks sorted and compares nothing. Putting the row in the facade makes that
35
+ unrepresentable rather than merely discouraged.
36
+
37
+ # Selection is by capability, then by name
38
+
39
+ An engine declares three things — `id`, `capabilities`, `available?` — and the
40
+ router picks the first available engine offering **every** capability the query
41
+ requires, default first, then registration order:
42
+
43
+ ```
44
+ --fuzzy requires :fuzzy → Search::Index
45
+ -e requires :regexp → the default already offers it
46
+ neither → the default (:scan)
47
+ nothing qualifies → UnsupportedQuery, which the CLI makes exit 2
48
+ ```
49
+
50
+ Capability routing came first and was, for one release, the *only* selector —
51
+ "the flags a user already reaches for are the selector" — on the reasoning that a
52
+ second vocabulary would have to be kept consistent with the first. That was
53
+ wrong in one specific way, and the gap is worth recording: **a capability flag can
54
+ only express what a query needs, not which matching model answers it.** Raw-text
55
+ matching requires nothing; BM25 ranking requires nothing. Neither can be asked for
56
+ by requiring something, so under capability routing alone the non-default engine
57
+ was unreachable at any price.
58
+
59
+ `--engine NAME` is the override that closes it, and naming an engine is an
60
+ override rather than a hint: an engine that cannot do what was *also* asked is an
61
+ error (`UnsupportedQuery` naming the engine), never a silent fallback, since
62
+ falling back would answer a different question than the one posed. An unknown
63
+ name lists what is registered.
64
+
65
+ # The default is the scan
66
+
67
+ The default engine is `Search::Scan`, and the reason is that a CLI process builds
68
+ an index, asks one question and exits — a build with a single query to amortize
69
+ it over. End to end: 3.00 s against 0.24 s at 1,000 concepts, 0.83 s against
70
+ 0.18 s at 250. The build is ~95% of the index path's cost at every size.
71
+
72
+ Recall settles it. Raw-text matching has no tokenizer, so it has none of the
73
+ tokenizer's holes: MiniSearch splits on `\p{Z}\p{P}`, and a backtick is `Sk`
74
+ while `$` is `Sc`, so a word inside a code span is stored as the token
75
+ `` `minifts` `` and the query `minifts` does not match it. On this bundle that
76
+ was 2 hits where the scan found 5. A loss that returns plausible rows while
77
+ omitting most of the answer is the worst kind to have on a path nobody opted
78
+ into.
79
+
80
+ What the index gives back — BM25+ ranking, prefix matching, `--fuzzy`, and
81
+ parity with the browser page — is now reached by asking. That the *page* still
82
+ runs MiniSearch means the CLI and the page rank identically only under
83
+ `--engine index`; the claim used to be unconditional and is not any more.
84
+
85
+ This has now been revisited on the server side. An engine may expose `prepare`,
86
+ which builds whatever it would otherwise build per call and hands it back; a
87
+ `Search::Corpus` holds one per engine id and passes it as `prepared:`. The scan
88
+ declares no `prepare` and is handed none — the seam is opt-in, so adding it broke
89
+ no engine and required nothing of an addon. The arithmetic above still holds for
90
+ the CLI, which has one query to amortize over; it does not for a server, which
91
+ has every keystroke after boot. See
92
+ [the search capability](../capabilities/search.md).
93
+
94
+ `available?` is not decorative. The base gem's two engines are always available —
95
+ `minifts` is a hard dependency with no native extension — but an addon backed by
96
+ SQLite can fail to build, and the rule it inherits is that a broken build
97
+ *degrades* rather than crashes. An engine whose store is missing answers
98
+ `available? == false`; it never raises at query time.
99
+
100
+ # `Search.register` is the second extension point
101
+
102
+ ```ruby
103
+ OKF::Bundle::Search.register(engine) # append-only, idempotent by id
104
+ OKF::Bundle::Search.engines # frozen snapshot, preference order
105
+ ```
106
+
107
+ Append-only and idempotent by `id`, so a double `require` cannot double the
108
+ registry and an addon cannot quietly displace a built-in. Capabilities outside
109
+ the declared vocabulary are refused at registration — a typo like `:regex` would
110
+ otherwise present as "my engine is silently never chosen".
111
+
112
+ The built-ins register at *their* load, an addon at *its*, so `require "okf"`
113
+ yields exactly `[Index, Scan]`. A clean-subprocess probe pins that — the same
114
+ guard that keeps the [CLI shell out of the library load](core-shell-split.md) —
115
+ because a `gem install` that silently changed what `okf search` answers would be
116
+ a surprise nobody opted into.
117
+
118
+ # The conformance suite replaced the oracle rule
119
+
120
+ The earlier plan for addon search was that `Bundle::Search` defines semantics and
121
+ every backend must return "the same match set, modulo ranking order". **That rule
122
+ cannot survive more than one engine.** The index and the scan disagree about match
123
+ sets by design — a phrase, an infix, a dotted identifier — so naming either the
124
+ oracle would make the other a bug.
125
+
126
+ What replaced it is narrower and actually holds: a shared conformance suite,
127
+ included by one test class per engine, asserting only what must be true
128
+ regardless of engine — the row shape and key order, ANDed terms, what `fields:`
129
+ restricts, the ordering, the empty answers, and that `across` keeps same-id
130
+ concepts distinct. Capability-gated blocks run only for engines declaring the
131
+ capability, so an engine earns its own semantics by declaring them rather than by
132
+ being excused from a rule.
133
+
134
+ A registered engine with no conformance class is itself a test failure. That is
135
+ the property the oracle rule was reaching for and could not express.
136
+
137
+ **What the conformance suite cannot do, and what covers it.** It asserts that
138
+ engines agree about the *shape* of an answer — row keys, ordering, ANDed terms,
139
+ what `fields:` restricts. It cannot notice that every engine is missing a third
140
+ of the matches, because consistency is not correctness. That blind spot is not
141
+ hypothetical: the index swap made every word inside a code span unfindable, and
142
+ the suite stayed green throughout.
143
+
144
+ `recall_test.rb` covers it by running real queries over a corpus of tokenization
145
+ hazards and measuring the index against the scan. The scan earns the oracle role
146
+ there and *only* there — for recall it is sound, because raw-text matching finds
147
+ any word that is present; for ranking and match sets the two engines disagree by
148
+ design. The test pins the known holes and fails when the set changes in either
149
+ direction, so a new hazard is named rather than discovered later in use.
150
+
151
+ This follows the same discipline as the [core/shell split](core-shell-split.md):
152
+ a boundary is only real when a test fails on crossing it, and it is checked the
153
+ same way [integration first](integration-first.md) checks the CLI — against what
154
+ a caller actually gets, not what an internal returns.
@@ -0,0 +1,139 @@
1
+ ---
2
+ type: Constraint
3
+ title: The server trust boundary
4
+ description: The trust boundary for serving a bundle you may not fully trust — both XSS paths into the page are closed, the registry write routes carry their own locks, and every read is realpath-contained so a symlinked file cannot escape the bundle root.
5
+ resource: gems/okf/lib/okf/render/graph/template.html.erb
6
+ tags: [security, server, xss, containment]
7
+ generated:
8
+ by: human:maintainer
9
+ at: 2026-08-13T12:00:00Z
10
+ sources:
11
+ - title: README.md — Server trust boundary
12
+ resource: https://github.com/serradura/okf/blob/main/README.md
13
+ - title: gems/okf/lib/okf/render/graph/template.html.erb
14
+ resource: https://github.com/serradura/okf/blob/main/gems/okf/lib/okf/render/graph/template.html.erb
15
+ - title: gems/okf/lib/okf/safe_read.rb
16
+ resource: https://github.com/serradura/okf/blob/main/gems/okf/lib/okf/safe_read.rb
17
+ ---
18
+
19
+ # Overview
20
+
21
+ The [graph server](../capabilities/graph-server.md) renders whatever bundle you
22
+ point it at, and a bundle is just files, so the page has to assume a body might
23
+ carry active content. Two defenses handle that — one for each path into the page.
24
+
25
+ # Where the boundary sits
26
+
27
+ There are two data paths into the page, and each carries its own guard:
28
+
29
+ | Path | Handling | Safe? |
30
+ | -------------------------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
31
+ | Graph data **inlined** into the page | through `json_for_script`, which escapes `<` | yes — it cannot break out of its `<script>` |
32
+ | Concept bodies **fetched** on demand (`/node?id=`) | `marked` renders the Markdown, then `DOMPurify.sanitize` scrubs it before it reaches the DOM | yes — scripts, handlers, and `javascript:` URLs are stripped |
33
+
34
+ The description and the §5 trust line shown in the inspector take a third path,
35
+ and it moved when `/node/meta` became JSON. The server no longer sends a
36
+ fragment to escape — it sends values (`description`, and the null-stripped
37
+ `trust` mapping) — so the guard is that `renderMeta` puts every one of them into
38
+ the DOM as **text**: a text node for the description, `textContent` for each
39
+ chip. Nothing on this path is ever parsed as markup, which is why no sanitizer
40
+ stands on it. A render path that reached for `innerHTML` here would need one.
41
+
42
+ # The static render carries both guards
43
+
44
+ [`okf render`](../capabilities/render.md) bakes every body into the page
45
+ instead of fetching it, so an embedded body takes the *inlined* path **and** the
46
+ rendered one: `json_for_script` escapes it at inject time (a `</script>` inside a
47
+ body cannot break out of its `<script>`), and it is still
48
+ `DOMPurify.sanitize(marked.parse(...))`'d when the getter hands it to the DOM. The
49
+ same two defenses, now both on the one path — a static file is no laxer than the
50
+ server, and the embedded description and trust line go through the same
51
+ text-only `renderMeta` as above — composed client-side in both modes, from
52
+ `/node/meta` when served and from the baked catalog row when not.
53
+
54
+ # Both guards are asserted, against a bundle that attacks them
55
+
56
+ `okf/test/browser/specs/sanitization.spec.js` drives
57
+ `okf/test/browser/fixtures/hostile` — a conformant OKF bundle whose content is
58
+ trying to execute script in the page rendering it — in both render modes. The
59
+ payloads set flags on `window`, so the assertion is not "the markup looks
60
+ clean" but *the script did not run*.
61
+ <!-- rule:okf-verify-the-sanitizer -->
62
+
63
+ This was a gap the browser suite's coverage review turned up: for a long time
64
+ the only checks were that the string `DOMPurify` appeared in the emitted page
65
+ and that it was a function at boot, both of which a render path skipping the
66
+ sanitizer passes cleanly. The table above described intent, not a contract.
67
+
68
+ Each guard was then mutation-checked, because a security test that cannot fail
69
+ is worse than none:
70
+
71
+ | Mutation | Result |
72
+ |---|---|
73
+ | `DOMPurify.sanitize(marked.parse(…))` → `marked.parse(…)` | 4 body specs red; `__xssImg` **fired** — real code execution |
74
+ | `esc()` back to `&<>` only (pre-c2cedb6) | the tag breakout spec red; a live `onmouseover` in the DOM |
75
+ | `json_for_script` without its `<` escape | all 14 red — the `</script>` in a title closes the block and the page never boots |
76
+
77
+ The first of those carries a lesson for anyone extending the fixture: with the
78
+ sanitizer removed, the `<script>` payload did **not** fire, because `innerHTML`
79
+ does not execute script tags. Only the `<img onerror>` did. A fixture carrying
80
+ script tags alone would have gone green against a page with no sanitizer at
81
+ all — proving the defense while the hole stood open.
82
+
83
+ # A second boundary: the server can now be asked to change something
84
+
85
+ Everything above is about content coming *in* to the page. The
86
+ [registry routes](../capabilities/bundles-manager.md) opened the other
87
+ direction — four `POST` routes that write the [registry](../registry.md) — and it
88
+ carries its own three locks rather than borrowing these: writable-at-all (loopback
89
+ by default, declined with `--read-only`, refused outright anywhere else), a registry to write to, and same-origin
90
+ plus a per-boot token. Sanitizing has nothing to say about a well-formed request
91
+ that should not have been honoured, which is why that gate is described where it
92
+ lives instead of being folded in here.
93
+
94
+ # A third boundary: a file may not be where its name says
95
+
96
+ Everything above trusts that a file inside the bundle *is* inside the bundle. A
97
+ symlink breaks that: its name sits under the root, but its target need not, and
98
+ `File.expand_path` — the lexical guard every read used — resolves the name, not
99
+ the link. So a bundle carrying `services/billing.md → /etc/passwd`, or an
100
+ `index.md` symlinked outside the root, had its target read and served: verbatim
101
+ over the [graph server](../capabilities/graph-server.md) and `okf render`, and —
102
+ the reason it finally mattered — over okf-mcp's tool set (`@okf-mcp design/the-tool-set`)
103
+ `--http`, to any host that could reach the port. Two code comments and a test
104
+ name asserted a containment that was never enforced; none tested it.
105
+
106
+ The guard is the target's real path now, not its name. Every read a bundle
107
+ serves — the Reader's bulk load, the single-concept handle, the live `log.md`
108
+ re-read, and the MCP concept and index reads — goes through one shell primitive
109
+ (`OKF::SafeRead`) that resolves `File.realpath`, refuses a result outside the
110
+ real root (`Path.under?` is the pure decision, the I/O stays in the shell), and
111
+ reads from the *resolved* path so a swap cannot slip between the check and the
112
+ open. An escaping file is quarantined like any unreadable one, so one planted
113
+ link cannot take a whole bundle down, and an escaping or vanished concept reads
114
+ as a plain not-found rather than an error naming the target.
115
+ <!-- rule:okf-realpath-contains-reads -->
116
+
117
+ The durable half is the rule the old comments claimed and did not hold: **a
118
+ lexical path check guards the name; a symlink escapes by its target, and only
119
+ `realpath` sees where a name actually leads.** The boundary is drawn where it
120
+ can be, not oversold past it — the same discipline the
121
+ extension seam (`@okf-eco design/extension-points`) applies to its prefix: a hardlink shares
122
+ its target's inode and keeps an in-root path that `realpath` cannot distinguish,
123
+ and neither a hardlink nor a symlink survives a `git clone`, a copy or a tarball.
124
+ So the portable, adversarial bundle is a symlink's to plant and `realpath`'s to
125
+ close; the hardlink case is a local, non-portable one, named rather than claimed
126
+ covered, because the obvious guard (reject `st_nlink > 1`) would break a bundle
127
+ on a deduplicating filesystem.
128
+
129
+ # What sanitizing does not cover
130
+
131
+ DOMPurify removes the code, not the content. The page still fetches and shows the
132
+ links, images, and Mermaid diagrams a body names (Mermaid runs in its `strict`
133
+ mode), and it runs third-party code from a CDN — Cytoscape, marked and DOMPurify
134
+ at boot, with Mermaid, Panzoom, the extra layout engines and
135
+ [MiniSearch](../capabilities/graph-server.md) lazily on first use. Each of those
136
+ is trust extended to the CDN as much as to the bundle; MiniSearch alone is pinned
137
+ to an exact version (`7.2.0`), because it has to *agree* with the Ruby port rather
138
+ than merely work. So the rule is no longer _only serve bundles you trust_ — it is
139
+ the ordinary care you would give any document from a source you do not know.
data/.okf/index.md ADDED
@@ -0,0 +1,40 @@
1
+ ---
2
+ okf_version: "0.2"
3
+ ---
4
+
5
+ # okf knowledge bundle
6
+
7
+ Everything about **okf**, the kernel of this ecosystem: what it does, how the
8
+ code is arranged, why each rule is a rule, and how a change is proven. It is
9
+ written to be read *before* opening `lib/`, so an agent about to add a verb, a
10
+ check or a search engine does not re-derive the layering and does not rebuild
11
+ something the model already answers.
12
+
13
+ `AGENTS.md` beside this bundle is the gem's maintainer guide. It keeps the
14
+ contract, the commands and the release steps, and routes here for everything
15
+ else; the repository root's guide is the ecosystem's and states no contract of
16
+ this gem's at all.
17
+
18
+ `structure/` is pinned: `test/unit/bundle_catalog_test.rb` fails when a file
19
+ under `lib/` is named by no concept, when a concept names a file that is gone,
20
+ or when the verb table in [cli](cli.md) disagrees with `OKF::CLI.builtins`. The
21
+ code is the truth and this bundle is the claim, so the two cannot drift quietly.
22
+
23
+ One thing is deliberately elsewhere. The **format** — what a citation is, what
24
+ §5 declares, what a bundle must contain — is the repository's own bundle
25
+ (`@okf-eco`), because the skill, the plugin, the three sibling gems and any future
26
+ non-Ruby implementation all speak it, and a reader asking about §5.1 is not
27
+ asking about a Ruby gem. A concept cannot link out of its own bundle, so
28
+ references to it name it in prose: `@okf-eco format/frontmatter`.
29
+
30
+ * [Overview](overview.md) - The gem at a glance: the seven capabilities and the design ethos behind them.
31
+ * [Command line](cli.md) - The `okf` executable — the one layer that parses argv, prints, and exits.
32
+ * [Bundle registry](registry.md) - The list of bundles a machine or a project knows, addressable as `@slug`.
33
+
34
+ # Areas
35
+
36
+ * [Structure](structure/) - Every file under `lib/`, grouped by the layer that owns it — pinned to the tree by a test.
37
+ * [The model](model/) - The pure in-memory data structures: concept, bundle, graph, skeleton.
38
+ * [Capabilities](capabilities/) - The things the gem does: validate, lint, search, serve, render, the library, the skill.
39
+ * [Design](design/) - The enforced boundaries that keep the gem light and honest.
40
+ * [Testing](testing/) - How a change is proven, and the walk a new verb owes.