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,211 @@
1
+ ---
2
+ type: Constraint
3
+ title: The graph page is proven in a real browser
4
+ description: A string assertion over rendered HTML cannot see a collapsed canvas or a folded breakpoint, so the page is driven in Chromium — in both render modes, with any thrown error failing the run.
5
+ resource: gems/okf/test/browser
6
+ tags: [testing, render, server, architecture]
7
+ generated:
8
+ by: human:maintainer
9
+ at: 2026-07-22T12:00:00Z
10
+ sources:
11
+ - title: gems/okf/test/browser/README.md
12
+ resource: https://github.com/serradura/okf/blob/main/gems/okf/test/browser/README.md
13
+ - title: gems/okf/test/browser/COVERAGE.md
14
+ resource: https://github.com/serradura/okf/blob/main/gems/okf/test/browser/COVERAGE.md
15
+ ---
16
+
17
+ # Overview
18
+
19
+ [`okf render` and `okf server`](../capabilities/render.md) share one ERB
20
+ template carrying ~1,300 lines of inline JS and CSS. Its regressions are not
21
+ the kind a string assertion catches: a view that returns with a canvas
22
+ Cytoscape measured at 0×0, a filter that stops composing with search, a
23
+ breakpoint folding the wrong element, a handler that throws while the DOM
24
+ still looks plausible. `okf/test/integration/render/` proves the page is
25
+ *emitted* correctly and cannot prove it *works*.
26
+
27
+ `okf/test/browser/` closes that gap with Playwright: real Chromium, DOM state and
28
+ computed CSS at real viewport widths. It is the same argument
29
+ [integration-first](integration-first.md) makes for the CLI, applied to the
30
+ one surface the CLI cannot reach.
31
+
32
+ # Every spec runs in both render modes
33
+
34
+ The template has two data paths that diverge in a load-bearing way: served
35
+ live it fetches `/node`, `/catalog`, `/index` and `/log` on demand; rendered
36
+ statically it reads the same payloads out of a baked `EMBED` constant. A pass
37
+ in one proves nothing about the other, so the suite defines two Playwright
38
+ projects and runs every spec twice — one against a booted `okf server`, one
39
+ against a `file://` static render generated fresh each run.
40
+
41
+ Where the modes honestly differ the spec says so and asserts both answers.
42
+ Full-text search is the worked example: bodies enter the index only when they
43
+ are present, and they are present only in a static bake, so the same query
44
+ finds a concept in one mode and not the other. Pinning a single expectation
45
+ would certify a lie in whichever mode it did not describe.
46
+ <!-- rule:okf-both-render-modes -->
47
+
48
+ # A thrown error fails the run
49
+
50
+ The shared fixture watches `pageerror` and console errors and fails the test
51
+ even when every assertion passed. This is the check no per-behavior test
52
+ provides: a handler that throws leaves a plausible-looking DOM that
53
+ assertions walk straight past, and "I changed the filter and the catalog
54
+ quietly stopped rendering" is exactly the failure this file keeps producing.
55
+ It is also the only thing giving the suite reach into surfaces it does not
56
+ otherwise test.
57
+
58
+ # Outside the default task, and outside CI
59
+
60
+ It needs node and a ~120MB Chromium, neither of which belongs on the
61
+ [Ruby 2.4 floor](ruby-floor.md) matrix, and the gem takes on no
62
+ [runtime dependency](runtime-dependencies.md) from it. So it is opt-in locally
63
+ (`rake test:browser`) and does not run in CI at all.
64
+
65
+ It did, for several releases, as a separate job marked `continue-on-error` — red
66
+ when it failed, but never gating a merge. The argument was that this is a
67
+ judgement about *what the signal is worth*: the page loads Cytoscape, marked and
68
+ DOMPurify from a CDN at boot — a dependency the
69
+ [trust boundary](server-trust-boundary.md) already names — so a red job can mean
70
+ a regression or can mean jsdelivr was slow.
71
+
72
+ The measurement settled it against the job. It failed **5 of its last 7 runs**
73
+ while the Ruby matrix stayed green, so the check was wrong far more often than it
74
+ was right, and the same sentence that justified it ("a check that cries wolf gets
75
+ muted within a month") describes what it became. It also cost something the
76
+ argument never priced: a ✗ on the repository's front page is read by a visitor as
77
+ a broken gem, not as a slow CDN, and that reading is the expensive one.
78
+
79
+ So the suite is a maintainer obligation run locally, enforced by nothing — the
80
+ same standing as the 2.4 Docker floor and the PR shape. Restoring it to CI needs
81
+ the flakiness gone first, not a second hedge on top of the first: cache
82
+ `okf/test/browser/vendor/` between runs so a cold runner stops reaching for jsdelivr,
83
+ and only then judge the signal on what is left.
84
+
85
+ The suite now answers those requests from a local read-through cache
86
+ (`okf/test/browser/vendor-cache.js`): a miss fetches and writes a gitignored
87
+ `vendor/`, a hit serves from disk, and a warm run touches no network at all —
88
+ proven by making the fetch path `throw` and watching 64 cases still pass. It is
89
+ keyed on the **request URL**, not on a list of the versions the template pins,
90
+ because such a list is a second copy of those pins that can drift into serving a
91
+ library the page no longer loads — the one failure a cache is most likely to
92
+ hide. Keyed on the URL, a version bump is simply a miss. `OKF_NO_VENDOR_CACHE=1`
93
+ bypasses it entirely, which is the way to check the template's pins still
94
+ resolve against the real CDN rather than against whatever `vendor/` kept.
95
+
96
+ That cache is a developer's offline suite, not CI's: `vendor/` is build output, so
97
+ a runner starts cold and still fetches from jsdelivr on the first boot. Which is
98
+ why it did nothing for the failure rate above, and why restoring the directory
99
+ between runs is the first move if the job is ever brought back.
100
+
101
+ The cache was built to make the suite *faster* and does not, which is worth
102
+ recording because the arithmetic that predicted otherwise is easy to re-derive
103
+ and wrong. It ran: Playwright gives each test a fresh context with an empty
104
+ cache, so the ~330 ms of boot scripts is re-paid across 400-odd cases. Chromium
105
+ in fact reuses those subresources across contexts inside a worker's browser
106
+ process, so the download was only ever paid once per worker. The controlled
107
+ measurement — 34 cases pinned to a single worker — is 28.7 s without the cache
108
+ and 29.0 s with it. Full-suite wall clock cannot settle the question at all:
109
+ three runs of the same 412 cases came in at 3.4 m, 3.6 m and 2.8 m, a spread
110
+ several times the effect, and reading the 2.8 m as a win is the mistake the
111
+ noise invites. The suite is bound by CPU — around 500% across five workers, on
112
+ rendering and Cytoscape layout — which is where the CDN wait was already
113
+ hiding. The lesson generalizes past this cache: a per-request timing multiplied
114
+ by a case count is a hypothesis, and the only thing that settles it is an A/B
115
+ with the parallelism held down.
116
+
117
+ Which leaves the obligation where it was: a change to the template is not done
118
+ until the suite is green locally. An automated gate nobody trusts is weaker
119
+ than a rule the maintainer keeps.
120
+ <!-- rule:okf-browser-suite-before-merge -->
121
+
122
+ # Coverage is measured against the page's own history
123
+
124
+ The suite's worth is measured the same way
125
+ [integration coverage](integration-first.md) is — as a map, not a score, and
126
+ against the honest denominator. Reading all 44 commits behind the template
127
+ yields ~230 behavioral contracts, ~94 of them fixes for bugs that actually
128
+ shipped. A regression fix is the sharpest test target there is: a failure mode
129
+ already proven reachable in this file.
130
+
131
+ `okf/test/browser/COVERAGE.md` now enumerates every contract per-commit and marks
132
+ each covered / partial / uncovered: of 181 net-live contracts, 176 (97%) are
133
+ covered, with a ranked worklist of what remains — its Priority 1 (regression
134
+ fixes reachable from the existing fixtures) is cleared, and Priority-2 is well
135
+ underway. By the narrower
136
+ regression-fix-only lens that is roughly 60 of 94, up from 10 — worked gap by gap,
137
+ each new spec mutation-checked.
138
+ It is strong on the interaction spine, the filters, the file tree, link
139
+ resolution, both XSS defenses, the mobile chrome, the first-visit notes, the
140
+ index layer, the diagram viewer and both halves of the command palette (the hub
141
+ bundle-switcher is reached through a two-bundle server the config boots, down to
142
+ the ⌘⏎ new-tab chord it honours; the Mermaid re-theme and the
143
+ `prefers-color-scheme` boot fallback are pinned too). The one regression that
144
+ resisted every external observable — one-camera-move-per-click, where settled
145
+ position, pan-event bursts and motion span all failed to tell a gutted `centerOn`
146
+ from the fix — was closed only by making the page observable: a test-only counter
147
+ (`window.__camCenters`) the spec reads at the synchronous instant after the tap,
148
+ 0 when the pan is deferred and 1 when it fires immediately. That is the honest
149
+ cost of a sub-frame timing contract, and it is deliberately the exception, not
150
+ the pattern. Its sibling, the graph-collapse-on-return, needed no counter at all:
151
+ once traced it turned out not to be a resize race but the boot fit landing on a
152
+ hidden 0×0 canvas and clamping the zoom to minZoom, and it left a stable
153
+ end-state signal (the clamped zoom) that closed it deterministically at the
154
+ source.
155
+
156
+ Three dividends of the work land here. Writing the specs turned up three real,
157
+ shipped bugs no string assertion could see, all now fixed: cluster-mode selection
158
+ faded the whole graph (a compound parent's opacity cascades to its nodes); a
159
+ log's "Open in graph" button stayed visible though the code hides it, because
160
+ `.btn.text{display:inline-flex}` outranked `.btn[hidden]` at equal specificity
161
+ (fixed by a `.btn.text[hidden]` rule, the precedent already used for `.fp-head`);
162
+ and the graph collapsed on return, whose cause was misdiagnosed as a resize race
163
+ for months until the browser tools showed the one animation running was a *fit*
164
+ of a hidden canvas (fixed by guarding `fitGraph` against a zero-size container).
165
+ All three are the assert-the-collapsible rule below paying out: a defect
166
+ invisible on inspection, caught by reading computed state, red before the fix and
167
+ green after. The third also earns its own lesson — a load-sensitive flake was the
168
+ symptom of a timer racing boot, not noise to route around with a `fixme`.
169
+ `COVERAGE.md` carries the full ranked list and what remains — now chiefly the
170
+ sub-frame periphery, both named camera defects having been closed.
171
+
172
+ # Writing a spec: read the page, then assert
173
+
174
+ Assertions must be able to fail for a real reason, which here means two things
175
+ beyond the [test-first rule](integration-first.md).
176
+
177
+ **Read what the page renders, not what the code looks like it renders.** Four
178
+ of the first green run's assertions were wrong this way — panel labels are
179
+ sentence-case in markup and uppercased by `text-transform`; the Index rail
180
+ item opens a file rather than pressing the filter beside it.
181
+
182
+ **Assert the thing that can actually collapse.** The suite's first
183
+ canvas-resize test read `cy.width()`, which reports the live container and
184
+ stays correct while the render is collapsed — it passed with every resize path
185
+ deleted. A test that cannot fail is worse than no test, because it is counted.
186
+ Mutation-check a new spec by breaking the code it covers and confirming it
187
+ goes red for the predicted reason.
188
+ <!-- rule:okf-assert-the-collapsible -->
189
+
190
+ Both traps were hit again by the cluster-drag specs, which is why they are stated
191
+ here rather than in a changelog. Asserting a node sits *inside* its box cannot
192
+ fail: a compound parent is sized from its children, so it holds even when nothing
193
+ was laid out. And aiming at the box's *bounding box* is not aiming at the box —
194
+ measured, the label strip at the top and the bottom fifth fall through to the
195
+ canvas, so the drag tested the background and would have passed with the bug in
196
+ place. Both were rewritten against what was measured: coordinates that move, and
197
+ a hit region of 0.25–0.65.
198
+
199
+ **Wait for the signal, never for a duration.** A fixed sleep after clustering
200
+ raced the layout: `clusterLayout` awaits a lazy CDN load, and under `animate:'end'`
201
+ nothing moves *while* fcose computes — so two identical position samples arrive
202
+ before the tiling does and read as "settled". The wait is now three facts in
203
+ order: the layout started (`layoutstart`; `layoutstop` never fires here, and
204
+ polling for it only ever timed out), it moved something, and it stopped.
205
+
206
+ What is left after that is ~1 in 100, and the two drag specs carry `retries: 2`
207
+ for it. That is a deliberate exception to the rule that retries mask defects, and
208
+ it holds only because the race is in the **setup**: a real mouse gesture aimed at
209
+ live geometry on a canvas whose camera is still animating. On a settled canvas the
210
+ identical gesture succeeded 8/8, so the retry re-runs the aim, not the proof —
211
+ and two failures running is a regression, not this.
@@ -0,0 +1,73 @@
1
+ ---
2
+ type: Constraint
3
+ title: The core/shell split
4
+ description: A pure functional core that never touches disk or stdio, and a thin shell that owns all I/O — enforced by a test.
5
+ resource: gems/okf/test/unit/boundary_test.rb
6
+ tags: [architecture, pure, testing, diagram]
7
+ generated:
8
+ by: human:maintainer
9
+ at: 2026-07-18T19:00:00Z
10
+ sources:
11
+ - title: gems/okf/test/unit/boundary_test.rb
12
+ resource: https://github.com/serradura/okf/blob/main/gems/okf/test/unit/boundary_test.rb
13
+ ---
14
+
15
+ # Overview
16
+
17
+ The gem is two halves. The **core** is pure — [`Concept`](../model/concept.md),
18
+ [`Bundle`](../model/bundle.md), [`Graph`](../model/graph.md), the
19
+ [validator](../capabilities/validator.md), the [linter](../capabilities/linter.md),
20
+ the [search](../capabilities/search.md),
21
+ the format layer (`@okf-eco format/`) — logic that returns data and does no I/O. The
22
+ **shell** owns everything that touches the world: the on-disk handles
23
+ (`Concept::File`, `Bundle::{Reader,Writer,Folder}`), the
24
+ [renderer](../capabilities/render.md) that draws the page, the
25
+ [server](../capabilities/graph-server.md) and its hub, the
26
+ [registry](../registry.md), and the [CLI](../cli.md).
27
+
28
+ ```mermaid
29
+ flowchart TB
30
+ subgraph shell ["Shell — the only layer that does I/O"]
31
+ CLI["CLI"]
32
+ Render["Render::Graph"]
33
+ Server["Server::App · Hub"]
34
+ Registry["Registry"]
35
+ RW["Reader · Writer · Folder"]
36
+ CF["Concept::File"]
37
+ end
38
+ subgraph core ["Core — pure: no File / Dir / stdio"]
39
+ Concept["Concept"]
40
+ Bundle["Bundle"]
41
+ Graph["Graph"]
42
+ VL["Validator · Linter · Search"]
43
+ MD["Markdown layer"]
44
+ end
45
+ shell ==>|depends on| core
46
+ core -. "never — boundary_test.rb fails the build" .-> shell
47
+ ```
48
+
49
+ # It is enforced, not just intended
50
+
51
+ `okf/test/unit/boundary_test.rb` fails the build if a core file names a shell class or
52
+ reaches for `File` / `Dir` / `FileUtils` / stdio. The dependency rule is executable,
53
+ so the boundary cannot rot silently: **put new I/O in the shell, put new logic in
54
+ the core, pure.**
55
+
56
+ "Pure" here means *no I/O*, not *no dependencies*: a core file may require a
57
+ third-party library as long as the library is itself pure computation. `Search`
58
+ requires [`minifts`](../design/runtime-dependencies.md) and stays in the core,
59
+ because an in-memory index touches neither disk nor stdio. A gem that read a
60
+ config file or logged to stderr would not get the same pass.
61
+
62
+ # Why it pays off
63
+
64
+ - **Testable without disk** — every feature runs against an in-memory
65
+ [bundle](../model/bundle.md), so the suite is fast and the 2.4 Docker check is
66
+ cheap. It is also what makes [integration first](integration-first.md)
67
+ affordable: a shell this thin can be driven for real, with argv and streams and
68
+ exit codes, in milliseconds — so the layer a user touches never has to be
69
+ proven by proxy.
70
+ - **Embeddable** — the [library API](../capabilities/library-api.md) exposes the
71
+ pure core to host apps that never want the gem's filesystem opinions.
72
+ - **Best-effort reads** — the reader collects unparseable files instead of
73
+ raising, so the pure graph still renders while the shell reports the skips.
@@ -0,0 +1,17 @@
1
+ # Design constraints
2
+
3
+ The boundaries that keep this gem light and honest. Each is enforced by
4
+ something — a test, a build task, a CI job — rather than intended.
5
+
6
+ The two that are *not* this gem's live in the repository bundle: the extension
7
+ seam every sibling arrives through (`@okf-eco design/extension-points`) and the
8
+ monorepo layout (`@okf-eco decisions/monorepo-layout`).
9
+
10
+ * [The core/shell split](core-shell-split.md) - Pure logic and I/O are separated, and a test fails the build when they are not.
11
+ * [Integration tests are the critical layer](integration-first.md) - The CLI is the product, so the suite that drives it end to end outranks the unit tests.
12
+ * [The graph page is proven in a real browser](browser-tests.md) - A string assertion cannot see a collapsed canvas, so Chromium drives the page in both render modes.
13
+ * [Ruby 2.4](ruby-floor.md) - The floor is rack's own, and the point is running on the Ruby an OS already ships.
14
+ * [Runtime dependencies](runtime-dependencies.md) - Exactly three, and a fourth needs an argument as strong as the third's.
15
+ * [Search engines](search-engines.md) - One facade, two engines, chosen by what the query needs rather than by name.
16
+ * [The server trust boundary](server-trust-boundary.md) - What the server will answer for, and what it refuses regardless of flags.
17
+ * [What ships, and the two ways it has gone wrong](packaging.md) - `spec.files` is subtractive, `.dockerignore` implies the reject list one way only, and a symlink installs dangling on the old half of the matrix.
@@ -0,0 +1,140 @@
1
+ ---
2
+ type: Constraint
3
+ title: Integration tests are the critical layer
4
+ description: The CLI is the product, so the suite that drives it end to end outranks the unit tests — and its coverage is measured alone, because the full number flatters.
5
+ resource: gems/okf/test/integration/cli
6
+ tags: [testing, cli, architecture]
7
+ generated:
8
+ by: human:maintainer
9
+ at: 2026-07-20T12:00:00Z
10
+ sources:
11
+ - title: gems/okf/AGENTS.md — Testing
12
+ resource: https://github.com/serradura/okf/blob/main/gems/okf/AGENTS.md
13
+ - title: gems/gems/okf/test/integration/cli/cli_integration_case.rb
14
+ resource: https://github.com/serradura/okf/blob/main/gems/okf/test/integration/cli/cli_integration_case.rb
15
+ ---
16
+
17
+ # Overview
18
+
19
+ A unit test proves a method behaves; an integration test proves the *product*
20
+ behaves. For this gem the product is the [CLI](../cli.md) — real argv, real
21
+ streams, real exit codes, real files — so when the two compete for effort,
22
+ integration wins. That is a ranking, not a slogan: `okf/test/integration/cli/` is
23
+ where a new verb is proven, and a verb without its file is not done.
24
+
25
+ The [core/shell split](core-shell-split.md) is what makes both layers cheap: the
26
+ pure core is unit-testable without disk, and the shell is thin enough that
27
+ driving it for real costs milliseconds.
28
+
29
+ # The folders are the three ways a user names a bundle
30
+
31
+ ```
32
+ okf/test/integration/cli/
33
+ cli_integration_case.rb the shared base: okf(), with_registry(), okf_server()
34
+ fixtures/ bundles more than one group uses
35
+ by_dir/ `okf lint ./docs` — named by path
36
+ by_registry/ `okf lint @handbook` — named through the registry
37
+ across_bundles/ `okf search @a @b` — several at once
38
+ cli_help_test.rb … the verbs that name no bundle
39
+ cli_plugin_test.rb the extension seam — a plugin on the load path
40
+ ```
41
+
42
+ `cli_plugin_test.rb` is the odd one, deliberately: it names no bundle and tests
43
+ no verb of ours. It writes `okf/plugin.rb` into a temp dir on `$LOAD_PATH`, which
44
+ is indistinguishable from an installed gem's `lib/`, so the
45
+ seam (`@okf-eco design/extension-points`) is driven for real without building and installing a
46
+ gem to drive it.
47
+
48
+ Same command, same flags, three identities — because the identity is where the
49
+ CLI decides *what to answer about*, and a verb that works by path can still be
50
+ broken by [ref](../registry.md). One file per command **and** per subcommand:
51
+ `registry list`/`set`/`del`/`default`/`rename` are five files, since each is a
52
+ surface invoked on its own.
53
+
54
+ `across_bundles/` covers every bundle-taking verb, not only the two that merge.
55
+ For the eleven with no multi-bundle form, the test proves a second bundle is
56
+ *rejected* — that boundary was a real silent-wrong-answer bug (`okf lint a b`
57
+ once linted `a`, ignored `b`, and exited `0`), so it is guarded, not assumed.
58
+
59
+ # Coverage is measured on the layer alone
60
+
61
+ ```bash
62
+ bundle exec rake test:integration # + coverage/integration/
63
+ ```
64
+
65
+ The full suite's number flatters: unit tests call classes directly and reach code
66
+ no user can. Run integration by itself and the figure becomes a *map* instead of
67
+ a score — read it that way. Low coverage in `bundle/writer.rb` or
68
+ `concept/file.rb` is expected and honest: no CLI verb writes a bundle, so those
69
+ belong to the [library API](../capabilities/library-api.md) to prove. Low coverage
70
+ in `cli/`, `registry.rb`, or `server/` is a **hole** — a path a user can reach
71
+ that no user-shaped test walks.
72
+
73
+ And prove that completeness by **reading the uncovered lines, not by judgment**.
74
+ A green run and a flattering aggregate hide the same thing — a branch only the
75
+ unit tests reach — so after a feature, diff
76
+ `coverage/integration/.resultset.json` for the uncovered lines in the files you
77
+ changed. Three shapes hide there by habit, because a unit test walked them
78
+ first: the *second* output format (the human listing when only `--json` was
79
+ asserted, or the reverse), an *error* branch and the exit code it carries, and
80
+ *malformed-input* robustness (a hand-edited [registry](../registry.md) — a
81
+ cycle, an unnormalised slug, a missing field). The registry groups shipped with
82
+ nine integration tests that read as exhaustive and left six such branches, a
83
+ whole human-rendering path among them, proven only by unit tests until the
84
+ resultset named them.
85
+
86
+ # Fixtures are the cheap part
87
+
88
+ `fixtures/` is the substrate the whole layer stands on: a committed bundle is
89
+ cheaper than a mock, more honest, and a reviewer can read it. When a path is
90
+ unreachable from the fixtures that exist, **add one** — never bend a test toward
91
+ what the fixtures happen to make easy, and never leave a path untested because
92
+ building its world felt like work.
93
+
94
+ Two of them are the argument. `rooted` exists because `tags --by dir`'s `(root)`
95
+ label — the one printed without a trailing slash — was unreachable from all twelve
96
+ fixtures before it: none carried a *tagged* root-level concept. `mentions` exists
97
+ because none contained a literal `@`, so
98
+ [search](../capabilities/search.md)'s `-e '\@term'` escape could be shown not to
99
+ error but never shown to *find*. A branch no fixture can reach is a branch nobody
100
+ has ever proven.
101
+
102
+ Fixtures follow **common closure**: one that a single group uses lives under that
103
+ group, so it changes when those tests change; one that several share stays in the
104
+ shared `fixtures/`. The base resolves group-local first, shared second, so a test
105
+ says `fixture("navigation")` without knowing which it is.
106
+
107
+ # Why it pays off
108
+
109
+ The suite is not decoration — writing it is what found the bugs. The pass that
110
+ built this layer turned up a `render -o` backtrace where the exit contract
111
+ promised `2`, counts that disagreed with their nouns, three spellings of "no
112
+ usable type" in three buckets, a [`graph`](../capabilities/read-views.md) that
113
+ named no bundle at all, and the silent second-bundle answer above. Four
114
+ independent reviewers had read the same code first and found none of them.
115
+
116
+ Assertions must be able to fail for a real reason: run the CLI, read what it
117
+ actually prints, then assert *that*. Asserting what you assume the code does is
118
+ how a green suite certifies a bug.
119
+
120
+ # The order is the proof
121
+
122
+ A change starts with a failing integration test, not with the fix — and the
123
+ failure has to be *read*, not merely observed: it must fail for the predicted
124
+ reason, since a test that fails on a missing fixture or a typo'd regex has proven
125
+ nothing about the bug. Then the code, then a re-run the test passes unedited.
126
+ <!-- rule:okf-test-first -->
127
+
128
+ That order is the only thing that establishes a test *can* fail. Written
129
+ afterwards, a test can only certify the code it was read off; written alongside,
130
+ a bug and its test come to agree with each other and stay wrong together — the
131
+ green suite certifying a bug, arrived at from the other direction. Pure refactors
132
+ are the exception rather than a licence, because they change no behavior: the
133
+ existing suite is their test and a green run is the proof the contract held.
134
+
135
+ The registry's [two derivations of the default](../registry.md) are the worked
136
+ example. The star-versus-`/` disagreement was written as a red test first, and the
137
+ run printed both halves of the bug at once — `/` redirecting to `conformant` while
138
+ the listing starred a `doomed (missing)` entry. Neither the reviewer nor the fix
139
+ had to be trusted: the test failed for exactly the predicted reason, the fix
140
+ turned it green, and it has guarded the agreement since.
@@ -0,0 +1,65 @@
1
+ ---
2
+ type: Constraint
3
+ title: "What ships, and the two ways it has gone wrong"
4
+ description: "`spec.files` is `git ls-files` minus a reject list, which makes `.dockerignore` imply the gemspec one way only — and a symlink in the package installs dangling on the old half of the supported matrix rather than failing."
5
+ resource: gems/okf/okf.gemspec
6
+ tags: [packaging, gemspec, docker, rubygems]
7
+ generated:
8
+ by: human:maintainer
9
+ at: 2026-08-19T12:00:00Z
10
+ sources:
11
+ - title: RubyGems Gem::Package::SymlinkError
12
+ resource: https://github.com/rubygems/rubygems/blob/master/lib/rubygems/package.rb
13
+ ---
14
+
15
+ # The list is subtractive
16
+
17
+ `spec.files` comes from `git ls-files` run with `chdir:` into the gem's own
18
+ directory, minus a reject list: `test/`, `bin/`, the Gemfile, the Rakefile,
19
+ `.gitignore`, `.rubocop.yml`, `AGENTS.md`, `CLAUDE.md`, and the gemspec itself.
20
+
21
+ Two consequences follow from it being subtractive rather than additive.
22
+ Everything at the repository root is invisible to it, so a new *root* file needs
23
+ no reject at all. But a new top-level file **inside the gem** ships unless the
24
+ gemspec rejects it — check `gem build` output when adding one.
25
+
26
+ `.okf/` is deliberately not rejected: an installed okf carries a real bundle, its
27
+ own — the [companion skill](../capabilities/agent-skill.md) ships the same way,
28
+ from `lib/okf/skill/`, for a reader to open with the tool they just installed.
29
+ `test/unit/packaging_test.rb` pins that it ships, and pins that `AGENTS.md` and
30
+ `CLAUDE.md` do not — the guide is for someone with a checkout, and `CLAUDE.md`
31
+ is one line pointing at it, so shipping the pointer without its target puts a
32
+ reference to nothing inside the published gem.
33
+
34
+ # `.dockerignore` implies the reject list, one way only
35
+
36
+ Anything `.dockerignore` drops from under the gem's directory must **also** be
37
+ rejected by the gemspec, or be gitignored. `git ls-files` reads the *index*, so
38
+ a path excluded from the Docker build context is still listed in `spec.files`,
39
+ and `gem build` then fails on a file that is not there.
40
+
41
+ **The converse does not hold, and must not be "restored" for symmetry.** `bin/`,
42
+ `Gemfile` and `Rakefile` are rejected from the gem and stay in the build context
43
+ on purpose; nothing breaks by shipping them to the builder.
44
+
45
+ One detail that looks like a bug and is not: `.dockerignore`'s bare `.okf` entry
46
+ drops the *repository's* bundle, not this gem's, because Docker anchors a
47
+ pattern with no `**` at the context root.
48
+
49
+ # Nothing in `spec.files` may be a symlink
50
+
51
+ `gem build` does not resolve one. It writes a symlink into the package, warns,
52
+ and succeeds.
53
+
54
+ What happens next depends on the installer's age, and the old half of the
55
+ supported matrix is the dangerous half. RubyGems >= 3.2 refuses to extract a
56
+ link pointing outside the gem (`Gem::Package::SymlinkError`) — loud, and
57
+ survivable. **RubyGems < 3.2 has no guard at all**, so on Ruby 2.7 (RubyGems
58
+ 3.1.6, inside the supported range) `gem install` exits 0 and installs a
59
+ *dangling* file. The gem installs cleanly and carries no licence, and nothing
60
+ says so.
61
+
62
+ So `LICENSE.txt` and `NOTICE` are real duplicates of the repository root's
63
+ rather than links to them, and `test/unit/packaging_test.rb` pins three separate
64
+ claims: that they are not symlinks, that they are byte-identical to the root's,
65
+ and that they are actually in `spec.files`.
@@ -0,0 +1,53 @@
1
+ ---
2
+ type: Constraint
3
+ title: The Ruby 2.4 floor
4
+ description: The gem runs on every Ruby since 2.4 so it works on the interpreter an OS already ships.
5
+ tags: [ruby, portability]
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-07-17T16:00:00Z
9
+ sources:
10
+ - title: gems/okf/okf.gemspec
11
+ resource: https://github.com/serradura/okf/blob/main/gems/okf/okf.gemspec
12
+ - title: gems/okf/AGENTS.md — The contract
13
+ resource: https://github.com/serradura/okf/blob/main/gems/okf/AGENTS.md
14
+ ---
15
+
16
+ # Overview
17
+
18
+ `required_ruby_version >= 2.4.0`. The point is to run on the Ruby an operating
19
+ system already ships, without asking anyone to install a newer one — the same
20
+ floor as [rack](runtime-dependencies.md), the gem's core dependency. This is why
21
+ the gem stays deliberately light.
22
+
23
+ # The floor bans APIs RuboCop won't catch
24
+
25
+ RuboCop parses at 2.4 and catches syntax, but **not** newer standard-library
26
+ methods, so those are a manual discipline. A non-exhaustive list of what is off
27
+ limits:
28
+
29
+ - **2.5** — `delete_prefix`/`delete_suffix`, `transform_keys`, `Dir.children`,
30
+ `yield_self`;
31
+ - **2.6** — `to_h { }`, `then`, endless string slices `str[i..]`, `YAML.safe_load`
32
+ keyword args (allowed **only** inside the
33
+ Frontmatter shim (`@okf-eco format/frontmatter`));
34
+ - **2.7** — `filter_map`, `tally`, numbered block params;
35
+ - **3.x** — endless methods, hash shorthand.
36
+
37
+ These constraints apply to `okf/test/` too, because the suite runs on 2.4 as well.
38
+
39
+ # The truth test
40
+
41
+ "Works on my Ruby" is not verification here. The floor is checked in CI across
42
+ every supported Ruby, and locally by copying the tree into a throwaway build dir,
43
+ dropping `Gemfile.lock` (the lockfile is written by a modern Bundler that 2.4's
44
+ own cannot read), and mounting the checkout **read-only** so the run cannot write
45
+ one back. Run it from the **repository root** — `$PWD` becomes `/src`, and the
46
+ command steps into the gem on the other side, because the floor is a property of
47
+ `okf` rather than of the repository and the siblings (`@okf-eco decisions/monorepo-layout`) will
48
+ not share it:
49
+
50
+ ```bash
51
+ docker run --rm -v "$PWD":/src:ro ruby:2.4 bash -c \
52
+ "cp -a /src /build && cd /build/okf && rm -f Gemfile.lock && bundle install --quiet && bundle exec rake test"
53
+ ```
@@ -0,0 +1,82 @@
1
+ ---
2
+ type: Constraint
3
+ title: Three runtime dependencies, each challenged
4
+ description: The gem depends on rack, webrick and minifts only — no ActiveSupport, no build step, no JavaScript toolchain, and no native extension.
5
+ resource: gems/okf/okf.gemspec
6
+ tags: [rack, portability]
7
+ generated:
8
+ by: human:maintainer
9
+ at: 2026-07-19T03:00:00Z
10
+ sources:
11
+ - title: gems/okf/okf.gemspec
12
+ resource: https://github.com/serradura/okf/blob/main/gems/okf/okf.gemspec
13
+ - title: minifts
14
+ resource: https://github.com/serradura/minifts
15
+ ---
16
+
17
+ # Overview
18
+
19
+ The runtime dependencies are exactly three, and a fourth is a design decision to
20
+ be challenged, not a convenience:
21
+
22
+ | Gem | Why |
23
+ |-----|-----|
24
+ | `rack` (`>= 2.2`) | the [server](../capabilities/graph-server.md) is a mountable Rack app |
25
+ | `webrick` (`>= 1.4`) | the default runner — unbundled from Ruby in 3.0, so it must be declared |
26
+ | `minifts` (`~> 1.0`) | [search](../capabilities/search.md)'s index engine — BM25+ ranking, prefix and fuzzy matching, on request |
27
+
28
+ # What the third one had to prove
29
+
30
+ `minifts` was admitted because it costs the footprint nothing that the two
31
+ before it were chosen to protect. It is pure Ruby with **zero runtime
32
+ dependencies of its own**, so it adds one gem rather than a subtree; it holds the
33
+ same [Ruby 2.4 floor](ruby-floor.md); and it is not a native extension, which is
34
+ the whole point — it is what lets ranked full-text search arrive *without*
35
+ SQLite + FTS5 and the C toolchain, build step and platform matrix that come with
36
+ it. The ceiling moves up; it does not disappear. A corpus large enough to
37
+ outgrow an in-memory index is still FTS5's to answer.
38
+
39
+ It buys a second thing that no third-party gem usually can. `minifts` is a
40
+ bit-for-bit port of the JavaScript MiniSearch the [browser
41
+ page](../capabilities/graph-server.md) already loads, so `--engine index` and the
42
+ browser rank identically by construction rather than by two implementations
43
+ agreeing for a while — and an index built in Ruby can be searched in the browser,
44
+ which is what a cached, pre-built index would need.
45
+
46
+ **The case got weaker, and the entry stays honest about it.** `minifts` now backs
47
+ a **non-default** engine: the scan took the default back, because a one-shot CLI
48
+ cannot amortize an index build (3.00 s against 0.24 s at 1,000 concepts) and
49
+ because raw text has none of the tokenizer's recall holes. A dependency that only
50
+ serves an opt-in path is a dependency carrying less weight than the one admitted
51
+ here. It is not close to retirement — `--fuzzy` has no other implementation,
52
+ BM25+ ranking has no other source, and page parity has no other route — but the
53
+ argument that justified it was *ranked search by default*, and that is no longer
54
+ what it delivers. If a cached prebuilt index makes the index viable as the
55
+ default again, this entry is restored rather than merely re-argued.
56
+
57
+ # No ActiveSupport, on purpose
58
+
59
+ The gem refuses the usual reach for ActiveSupport. Two small pieces exist
60
+ precisely so it is not needed:
61
+
62
+ - `OKF.blank?` — the emptiness check;
63
+ - `OKF::Markdown::Frontmatter.stringify_keys` — the key coercion, living in
64
+ the one YAML gateway (`@okf-eco format/frontmatter`).
65
+
66
+ # What the leanness buys
67
+
68
+ Together with the [Ruby 2.4 floor](ruby-floor.md), a three-dependency footprint —
69
+ none of them native, none of them dragging a tree — is what lets the gem run on
70
+ the interpreter an OS already ships: no build step, no bundler for the
71
+ [served page](server-trust-boundary.md), no JavaScript toolchain. Leanness is a
72
+ feature of this gem, not an accident.
73
+
74
+ # A packaging note
75
+
76
+ `spec.files` comes from `git ls-files` run with `chdir:` into the gem's own
77
+ directory, minus `test/`, `bin/`, the `Gemfile`, the `Rakefile` and a couple more.
78
+ Because of that `chdir:`, everything at the repository root is invisible to it —
79
+ so a new *repo*-level file never ships, and needs no reject. A new top-level file
80
+ **inside the gem** does ship unless the gemspec rejects it; check `gem build`
81
+ output when adding one. See the monorepo layout (`@okf-eco decisions/monorepo-layout`) for the
82
+ one-directional rule that ties the reject list to `.dockerignore`.