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.
- checksums.yaml +4 -4
- data/.okf/capabilities/agent-skill.md +112 -0
- data/.okf/capabilities/bundles-manager.md +144 -0
- data/.okf/capabilities/graph-server.md +678 -0
- data/.okf/capabilities/index.md +26 -0
- data/.okf/capabilities/library-api.md +82 -0
- data/.okf/capabilities/linter.md +83 -0
- data/.okf/capabilities/read-views.md +228 -0
- data/.okf/capabilities/render.md +66 -0
- data/.okf/capabilities/search.md +297 -0
- data/.okf/capabilities/validator.md +60 -0
- data/.okf/cli.md +214 -0
- data/.okf/design/browser-tests.md +211 -0
- data/.okf/design/core-shell-split.md +73 -0
- data/.okf/design/index.md +17 -0
- data/.okf/design/integration-first.md +140 -0
- data/.okf/design/packaging.md +65 -0
- data/.okf/design/ruby-floor.md +53 -0
- data/.okf/design/runtime-dependencies.md +82 -0
- data/.okf/design/search-engines.md +154 -0
- data/.okf/design/server-trust-boundary.md +139 -0
- data/.okf/index.md +40 -0
- data/.okf/log.md +724 -0
- data/.okf/model/bundle.md +47 -0
- data/.okf/model/concept.md +75 -0
- data/.okf/model/graph.md +59 -0
- data/.okf/model/index.md +9 -0
- data/.okf/model/skeleton.md +76 -0
- data/.okf/overview.md +87 -0
- data/.okf/registry.md +432 -0
- data/.okf/structure/format-layer.md +59 -0
- data/.okf/structure/index.md +22 -0
- data/.okf/structure/search.md +53 -0
- data/.okf/structure/the-analysers.md +60 -0
- data/.okf/structure/the-cli.md +99 -0
- data/.okf/structure/the-disk-shell.md +76 -0
- data/.okf/structure/the-model.md +81 -0
- data/.okf/structure/the-server.md +74 -0
- data/.okf/structure/the-skill.md +52 -0
- data/.okf/testing/adding-a-verb.md +76 -0
- data/.okf/testing/index.md +12 -0
- data/.okf/testing/the-harness.md +45 -0
- data/CHANGELOG.md +161 -16
- data/README.md +226 -17
- data/lib/okf/cli/command.rb +8 -3
- data/lib/okf/cli/registry.rb +306 -47
- data/lib/okf/cli.rb +1 -1
- data/lib/okf/registry.rb +448 -22
- data/lib/okf/render/graph/template.html.erb +6 -2
- data/lib/okf/server/hub.rb +6 -2
- data/lib/okf/skill/reference/cli/registry.md +49 -6
- data/lib/okf/skill/reference/cli.md +1 -1
- data/lib/okf/version.rb +1 -1
- 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`.
|