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,99 @@
1
+ ---
2
+ type: Component
3
+ title: The CLI — a Registry, a Base Class, and One File per Verb
4
+ description: The only layer that parses argv, prints and exits; a verb is a class answering four questions about itself and one about a run, and the require order at the bottom of `cli.rb` is the order `okf help` prints.
5
+ tags: [structure, cli, shell, extension-point, plugins]
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-08-19T12:00:00Z
9
+ resource: lib/okf/cli.rb
10
+ ---
11
+
12
+ # The files
13
+
14
+ | file | what it owns |
15
+ |---|---|
16
+ | `lib/okf/cli.rb` | the command registry, the dispatcher, plugin discovery, and `okf help` |
17
+ | `lib/okf/cli/command.rb` | the base every verb inherits: streams, refs, shared flags, printers |
18
+
19
+ And one file per verb, each registering itself at load:
20
+
21
+ | verb file | verb |
22
+ |---|---|
23
+ | `lib/okf/cli/skill.rb` | `skill` |
24
+ | `lib/okf/cli/server.rb` | `server` |
25
+ | `lib/okf/cli/render.rb` | `render` |
26
+ | `lib/okf/cli/registry.rb` | `registry` and its eleven subcommands |
27
+ | `lib/okf/cli/lint.rb` | `lint` |
28
+ | `lib/okf/cli/loose.rb` | `loose` |
29
+ | `lib/okf/cli/validate.rb` | `validate` |
30
+ | `lib/okf/cli/search.rb` | `search` |
31
+ | `lib/okf/cli/index.rb` | `index` |
32
+ | `lib/okf/cli/dirs.rb` | `dirs` |
33
+ | `lib/okf/cli/stats.rb` | `stats` |
34
+ | `lib/okf/cli/types.rb` | `types` |
35
+ | `lib/okf/cli/tags.rb` | `tags` |
36
+ | `lib/okf/cli/files.rb` | `files` |
37
+ | `lib/okf/cli/references.rb` | `references` |
38
+ | `lib/okf/cli/catalog.rb` | `catalog` |
39
+ | `lib/okf/cli/graph.rb` | `graph` |
40
+
41
+ **That table's order is the require order at the bottom of `cli.rb`, and the
42
+ require order IS the order `okf help` lists the verbs in.** A test pins the
43
+ result, but the coupling is in the source. What each verb *answers* is the
44
+ [cli](/cli.md)'s group table, pinned against `OKF::CLI.builtins` by
45
+ the same test; the walk a new one owes is
46
+ [adding-a-verb](/testing/adding-a-verb.md).
47
+
48
+ # What a verb is
49
+
50
+ A `CLI::Command` subclass answering four questions about itself — `.id`,
51
+ `.group`, `.help_rows`, `.hidden?` — and one about a run: `#call(argv)`,
52
+ returning the exit status. **Privacy is the boundary**: `#call` is the whole
53
+ public surface, so a helper cannot become a verb by accident. `DUCK_TYPE` is
54
+ that contract, checked at registration.
55
+
56
+ `GROUPS` is the ordering of the sections `okf help` prints; `ROW_FIELDS` is the
57
+ `--fields`/`--except` projection vocabulary.
58
+
59
+ # What the base class already gives you
60
+
61
+ Do not re-implement any of these in a verb:
62
+
63
+ * **refs** — `all_ref?`, and the `@slug` / bare `@` / `@group` resolution every
64
+ verb inherits, so `okf lint @handbook` works without the verb knowing about
65
+ registries.
66
+ * **flags** — `json_flags`, `help_flag`, `projection_flags`, `filter_flags`,
67
+ `depth_flag`. `FILTER_KEYS` is the shared filter vocabulary.
68
+ * **filtering** — `filter_entries`, `dir_scope`, `under_dir?`, and the `--area`
69
+ deprecation shim in `fold_area`.
70
+ * **printing** — `print_inverted_index` is the shared shape behind `types`,
71
+ `tags` and friends.
72
+ * **arity** — `no_extras?` is what makes a second bundle an exit-2 usage error
73
+ for the verbs that take only one. That was a real silent-wrong-answer bug:
74
+ `okf lint a b` once linted `a`, ignored `b`, and exited 0.
75
+
76
+ # `CLI.register` is the extension point
77
+
78
+ Append-only, idempotent by id, duck-type checked — **deliberately the same shape
79
+ as `Search.register`**. Any gem with `okf/plugin.rb` on its load path can
80
+ register a verb and okf finds it, with no edit here and no list of known addons
81
+ (a test greps `cli.rb` to keep it that way).
82
+
83
+ Discovery is **lazy**: a built-in never triggers a scan, so only an unknown verb
84
+ or `okf help` pays for it. A plugin that raises is skipped and reported on
85
+ stderr, never fatal. `declined` and `register_declined` are how a collision is
86
+ reported rather than silently won.
87
+
88
+ `PLUGIN_GEM_PREFIX` is `okf-`: **only gems named `okf-*` are loaded**, the same
89
+ convention Jekyll and Vagrant use. Argue it as a convention if it is revisited —
90
+ the threat it closes is thin, and overselling it invites false confidence.
91
+
92
+ One rule underneath it *is* load-bearing: **naming a gem must never load it.**
93
+ `plugin_gem_name` reads the spec's `full_gem_path` and requires nothing, because
94
+ a refusal that happens after the `require` is not a refusal; a test pins it. A
95
+ path belonging to no gem stays trusted — `ruby -I`, a Gemfile `path:`, a
96
+ checkout — someone put it there.
97
+
98
+ `seal_builtins!` is the line between what ships and what was installed, and it
99
+ is what `extension?` reads to print the "installed extensions" section.
@@ -0,0 +1,76 @@
1
+ ---
2
+ type: Component
3
+ title: The Disk Shell
4
+ description: Where a directory becomes a Bundle and back — the reader, the atomic writer that validates before it publishes, the folder handle everything above it uses, and the registry.
5
+ tags: [structure, shell, io, registry, atomic]
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-08-19T12:00:00Z
9
+ resource: lib/okf/bundle/folder.rb
10
+ ---
11
+
12
+ # The files
13
+
14
+ | file | what it owns |
15
+ |---|---|
16
+ | `lib/okf/concept/file.rb` | one concept as an on-disk handle: read, save, delete, reload |
17
+ | `lib/okf/bundle/reader.rb` | a directory to a `Bundle`, unparseable files kept rather than dropped |
18
+ | `lib/okf/bundle/writer.rb` | a `Bundle` to a directory — locked, validated, then promoted atomically |
19
+ | `lib/okf/bundle/folder.rb` | the on-disk bundle handle every layer above actually holds |
20
+ | `lib/okf/registry.rb` | which bundles a machine or a project knows, addressed as `@slug` |
21
+
22
+ These are the shell. Everything they call into is pure, and
23
+ `test/unit/boundary_test.rb` keeps the arrow pointing one way.
24
+
25
+ # Folder is the handle, not a convenience
26
+
27
+ `Folder.load(dir)` is what the CLI, the server, the TUI and the MCP shell all
28
+ hold. It delegates `validate`, `lint`, `graph`, `skeleton`, `catalog`, `hubs`,
29
+ `directories`, `directory_index`, `stats`, `tag_groups`, `references` and
30
+ `log_entries` to the pure model, and adds only what needs the disk:
31
+ `concept_source`, `reference_files`, `reload`, `save`, and `Folder.label`.
32
+
33
+ Reach for `Folder`, not `Reader` — the reader is how a folder is built, once.
34
+
35
+ # The writer publishes or it does not
36
+
37
+ `Writer#call` takes a lock, writes the whole tree to a temporary path, runs the
38
+ **validator** against it, and only then promotes it into place. A bundle that
39
+ would not validate is never published, and a crash mid-write leaves the old tree
40
+ intact. `AlreadyExistsError` and `ValidationErrorFromResult` are the two
41
+ refusals; `safe_markdown_path!` is the containment check, borrowed from the
42
+ format layer rather than rewritten.
43
+
44
+ No CLI verb writes a bundle. This is the library API's surface, which is why its
45
+ integration coverage is low *by design*, and why a new verb reaching for it is
46
+ worth a second look — [adding-a-verb](/testing/adding-a-verb.md) says where
47
+ logic belongs instead.
48
+
49
+ # Registry: two files, one answer
50
+
51
+ `Registry` is the `@slug` layer. `HOME_ENV`/`DEFAULT_HOME` is the global
52
+ registry under `$OKF_HOME` (default `~/.okf`); `LOCAL_FILE` is the project-local
53
+ `.okf.json` (or the legacy `.okf-registry.json`) that `discover` finds by walking up from the working
54
+ directory. **A discovered local registry replaces the global one outright** — it
55
+ does not merge — and `NO_DISCOVERY_ENV` (`OKF_NO_DISCOVERY=1`) is the escape
56
+ hatch that forces the global one, which is what a test that must not see the
57
+ developer's registry sets. `Registry.load`'s `follow_links:` is the same
58
+ decision one step further: it is true only for the global registry, so a local
59
+ one parses links and never resolves them.
60
+
61
+ `relative_base` is why a local registry's paths stay relative and the file
62
+ travels with the repository. `Registry#reopen` preserves it; `Registry.new(path)`
63
+ does not, and reaching for the latter is how a reload comes to resolve every
64
+ bundle against the wrong base.
65
+
66
+ `Link` points the global registry at another registry file; `resolve_links`
67
+ folds that file's bundles in (through `link_slug`, which prefixes only on a
68
+ collision) and `refuse_linked` is the single guard every write goes through —
69
+ here rather than in the CLI, because the server's bundles panel calls these same
70
+ methods. `open_linked` constructs the target with `follow_links: false`, which is
71
+ the whole of the depth rule.
72
+
73
+ `Group` is a named, recursive set of bundles. `RESERVED_SLUGS` is `all`, because
74
+ `@all` means every bundle and may not be shadowed. `slugify`, `dedupe`,
75
+ `normalize` and `path_shaped?` are the naming rules a `registry set` goes
76
+ through.
@@ -0,0 +1,81 @@
1
+ ---
2
+ type: Component
3
+ title: The Model
4
+ description: A bundle and its concepts in memory, with no disk and no stdio anywhere in it — plus the four derived views every other layer reads instead of recomputing.
5
+ tags: [structure, model, pure, graph]
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-08-19T12:00:00Z
9
+ resource: lib/okf/bundle.rb
10
+ ---
11
+
12
+ # The files
13
+
14
+ | file | what it owns |
15
+ |---|---|
16
+ | `lib/okf/concept.rb` | one concept: frontmatter, body, and every §5 question about it |
17
+ | `lib/okf/bundle.rb` | the set: concepts, reserved files, and the rollups |
18
+ | `lib/okf/bundle/graph.rb` | nodes and edges, plus the type and tag indexes |
19
+ | `lib/okf/bundle/references.rb` | the `references/` inventory and who cites what |
20
+ | `lib/okf/bundle/row_filter.rb` | one predicate for `--type/--dir/--tag/--status/--trust` |
21
+ | `lib/okf/bundle/skeleton.rb` | directories, the arcs between them, and the suggested cut |
22
+
23
+ All six are **pure**. No `File`, no `Dir`, no stdio — `test/unit/boundary_test.rb`
24
+ fails the build if that changes.
25
+
26
+ # Concept: the §5 vocabulary lives here
27
+
28
+ `Concept` is where the spec's provenance model is implemented, and the parts
29
+ worth knowing before adding a field:
30
+
31
+ * `RESERVED_FILENAMES` and `reserved?` — `index.md` and `log.md` are not concepts.
32
+ * `STATUSES` / `DEFAULT_STATUS` / `effective_status` — §4.1's lifecycle, with the
33
+ default applied once so no caller has to remember it.
34
+ * `generated`, `generated_at`, `generated_by`, `declared_generated?` — a
35
+ *declared* provenance, never a derived one.
36
+ * `verified`, `fold_tier`, `shows_trust?` — the trust tiers, and the predicate
37
+ that decides whether a bundle even has a trust dimension to show. That last one
38
+ is a single predicate on purpose: a UI that gates a chip one way and a facet
39
+ another promises rows it will not return.
40
+ * `ISO_DATE`, `ISO_CUTOFF`, `ATTESTED_COMPUTATION`, `HUMAN_ACTOR` — the literals
41
+ the validator and the linter both read, rather than each spelling them.
42
+
43
+ `CONCEPT_SCOPED_CHECKS` is the list of lint checks that are about one concept, and
44
+ it is here rather than in the linter because it is a fact about the model.
45
+
46
+ # Bundle: the rollups every other layer reads
47
+
48
+ `Bundle` holds `concepts`, `reserved` and `unparseable` — an unreadable file is
49
+ kept as an `Entry` with its error rather than dropped, because a validator that
50
+ silently skips what it could not parse reports a clean bundle.
51
+
52
+ `catalog`, `stats`, `hubs`, `directories`, `directory_index`, `tag_groups` and
53
+ `paths_by_id` are the derived views. Read one rather than recomputing it: the
54
+ CLI, the server, the TUI and the MCP shell all answer from these, which is what
55
+ keeps four surfaces from disagreeing about the same number.
56
+
57
+ `okf_version` is what the bundle *declares* (§12), never a literal — the health
58
+ of every downstream v0.1-versus-v0.2 decision depends on that distinction.
59
+ `VIRTUAL_ROOT` is the path a rootless in-memory bundle is contained against.
60
+
61
+ The two analysers that read all of this are [the-analysers](/structure/the-analysers.md).
62
+
63
+ # Graph, Skeleton, References, RowFilter
64
+
65
+ `Graph.build` turns concepts into nodes and their links into edges, with
66
+ `type_index` and `tag_index` alongside. `unlinked_ids` is the orphan set.
67
+ `minimal:` and `body:` are how a caller asks for less than the whole thing —
68
+ the payload the graph page embeds is not the payload `okf graph --json` prints.
69
+
70
+ `Skeleton` is the directory-level view: `dirs`, `arcs` between them, and
71
+ `suggested_cut` — the weight at which the arc diagram stops being a hairball.
72
+ `cuts_for` maps that back onto concrete edges.
73
+
74
+ `References` inventories the `references/` folder and the concepts citing each
75
+ file, including the dangling ones.
76
+
77
+ `RowFilter.matches?` is the *single* predicate behind every `--type`, `--dir`,
78
+ `--tag`, `--status` and `--trust` filter in the CLI, the server and the TUI.
79
+ `shows_trust?` is the same gate `Concept` exposes, reachable from a catalog row.
80
+ A second filter implementation is how two views come to disagree about what
81
+ "matching" means.
@@ -0,0 +1,74 @@
1
+ ---
2
+ type: Component
3
+ title: The Server and the Page
4
+ description: One ERB template that is the whole UI, served by a Rack app or baked to a file by `render` — plus the hub that mounts many bundles and owns the only writes in the server.
5
+ tags: [structure, server, rack, render, xss]
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-08-19T12:00:00Z
9
+ resource: lib/okf/render/graph.rb
10
+ ---
11
+
12
+ # The files
13
+
14
+ | file | what it owns |
15
+ |---|---|
16
+ | `lib/okf/render/graph.rb` | the ERB render — `Graph.static` bakes a file, the same class serves the page |
17
+ | `lib/okf/server/app.rb` | the Rack app for one bundle: the page and its JSON endpoints |
18
+ | `lib/okf/server/hub.rb` | N bundles at `/b/<slug>/`, plus the routes only a set can answer |
19
+ | `lib/okf/server/hub/not_found.rb` | the 404 page: what was asked for, what exists, and the nearest match |
20
+ | `lib/okf/server/runner.rb` | the built-in WEBrick to Rack bridge — no rackup file needed |
21
+
22
+ The template itself is `graph/template.html.erb` beside `render/graph.rb`, and it
23
+ is ~1,300 lines of inline JS and CSS. Both halves open with a section map; the JS
24
+ one also names the three seams that actually couple the sections
25
+ (`applyGraphFilter`, `setView`, the lazy caches). Read it before editing —
26
+ `grep -n '── '` on the template prints the same list with live line numbers.
27
+
28
+ # One template, two modes
29
+
30
+ `Render::Graph.static` bakes a self-contained file with the payload embedded;
31
+ `Server::App` serves the same template and lets the browser `fetch()` bodies on
32
+ demand. **The two modes diverge exactly there** — baked `EMBED` versus fetched
33
+ endpoints — which is why every browser spec runs twice, once against each.
34
+
35
+ `LAYOUTS` is the five Cytoscape layouts; `MIN_SIZE`/`MAX_SIZE` the node scaling.
36
+
37
+ # The page stays self-contained, and two XSS defenses hold the line
38
+
39
+ Only Cytoscape, marked and DOMPurify load from a CDN at boot; Mermaid, Panzoom,
40
+ MiniSearch and the extra layout engines lazy-load on first use. No htmx, no
41
+ bundler, no build step.
42
+
43
+ Two defenses, and a new render path that skips either one reopens the hole:
44
+
45
+ * **`json_for_script`** escapes `<` so inlined data cannot break out of its
46
+ `<script>` — `LT_ESCAPE` is that literal.
47
+ * **`DOMPurify.sanitize(marked.parse(...))`** runs on every fetched body before
48
+ it reaches `innerHTML`.
49
+
50
+ # App: the endpoints
51
+
52
+ `/` is the page; `/node`, `/node/meta`, `/catalog`, `/tags`, `/types`, `/index`
53
+ and `/log` are the JSON it pulls. `SEARCH_ENGINE` is `:index` here — a server
54
+ *can* amortise the build, which is the opposite of the CLI's default — and
55
+ `warm_search` is where that build happens, at boot rather than on the first
56
+ query.
57
+
58
+ The bundles it serves are loaded through [the-disk-shell](/structure/the-disk-shell.md).
59
+
60
+ # Hub: many bundles, and the only writes
61
+
62
+ `Hub` mounts each bundle under `MOUNT` (`/b/<slug>/`) and adds the routes a set
63
+ can answer that one bundle cannot: `GET /search` across all of them, `GET /b/`
64
+ for the listing, and `POST /registry/{default,rename,remove,add}`.
65
+
66
+ **Those four writes are the only writes in the whole server**, they are off
67
+ unless `writable:`, and they are guarded: `authentic?` and `same_origin?` with a
68
+ `token`, because a page that can rename a bundle is a page a hostile site would
69
+ like to submit a form to.
70
+
71
+ `Hub::NotFound` is a real page rather than a status code — it names what was
72
+ asked for, lists what exists, and offers the nearest slug by edit distance. It
73
+ carries its own CSS, mark and script because the 404 must render when the
74
+ bundle it was asked for does not exist.
@@ -0,0 +1,52 @@
1
+ ---
2
+ type: Component
3
+ title: The Skill and Its Installer
4
+ description: The companion agent skill ships from exactly one tree in this gem, and two generated copies elsewhere in the repository are regenerated rather than edited.
5
+ tags: [structure, skill, generated, single-source]
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-08-19T12:00:00Z
9
+ resource: lib/okf/skill.rb
10
+ ---
11
+
12
+ # The file
13
+
14
+ | file | what it owns |
15
+ |---|---|
16
+ | `lib/okf/skill.rb` | `Skill.install` — the installer, its `ASSETS` tree, and `Skill::Error` |
17
+
18
+ `ASSETS` points at `lib/okf/skill/`, and **that tree is the single canonical
19
+ copy of the skill.** `okf skill <dest>` installs from it, so edit it there and
20
+ nowhere else.
21
+
22
+ `NAME` is `okf`, `SKILLS_DIR` is `skills`, and `nest:` decides whether the
23
+ install lands in a `skills/okf/` subdirectory or directly in the destination —
24
+ `--here` is the flag that turns it off. `force:` overwrites.
25
+
26
+ # Two generated copies exist, and neither is editable
27
+
28
+ `plugin/skills/okf` and `skills/okf` at the repository root are *generated*
29
+ copies — the Claude Code plugin's, and the one a generic skill installer reads.
30
+ `rake skill:sync` writes both from this tree and stamps the plugin manifest's
31
+ version.
32
+
33
+ Two guards fail on drift, both by file list **and** SHA-256 checksum:
34
+ `rake skill:verify`, which `build` depends on, and `test/plugin/sync_test.rb`.
35
+ So a release with a stale copy is impossible rather than a CI failure after the
36
+ fact.
37
+
38
+ # The markers in the skill text
39
+
40
+ Guidance lines in the skill carry stable anchors — `<!-- check:<lint-check-id> -->`
41
+ where a deterministic check enforces the point, `<!-- rule:okf-<slug> -->` for
42
+ pure-judgment craft. They render invisibly and sync verbatim into every copy, so
43
+ keep them on the line they annotate when you edit it. They exist so an eval can
44
+ pin a claim and a concept can cite one.
45
+
46
+ # Loading
47
+
48
+ The other shell that loads on demand is [the-cli](/structure/the-cli.md).
49
+
50
+ `skill.rb` is one of the two argv-facing shells that **do not** load with
51
+ `require "okf"` — the CLI is the other. `exe/okf` requires them, and so must any
52
+ test that drives them. An embedding application never pays for either.
@@ -0,0 +1,76 @@
1
+ ---
2
+ type: Playbook
3
+ title: Adding a Verb, or a Subcommand
4
+ description: The steps a new command owes, in order — the file it lives in, the base class it must not re-implement, the three folders it earns a test in, and the catalogue entry the pin will demand.
5
+ tags: [testing, playbook, cli]
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-08-19T12:00:00Z
9
+ resource: lib/okf/cli/command.rb
10
+ ---
11
+
12
+ # Before adding one, check it is not already there
13
+
14
+ Seventeen commands and eleven subcommands already exist, catalogued in
15
+ [cli](/cli.md) and [read-views](/capabilities/read-views.md), and `okf help`
16
+ prints the same list. Most of what a new verb would want is a *flag* on an
17
+ existing view rather than a view of its own: the shared filters and projections
18
+ — `--type/--dir/--tag/--status/--trust` and `--fields/--except` — compose with
19
+ every list already.
20
+
21
+ # The walk
22
+
23
+ 1. **Write the failing test first**, in `test/integration/cli/`, in a file named
24
+ for the verb. Run it. It must fail for the reason you predicted — not because
25
+ a fixture is missing or a regex has a typo, which prove nothing about the
26
+ behaviour. A test written *after* the fix certifies only the code it was read
27
+ off.
28
+ 2. **One file per verb**, at `lib/okf/cli/<verb>.rb`, registering itself at load.
29
+ A subcommand family stays in its parent's file, but earns its own *test*
30
+ file — `registry set` is a surface a user invokes on its own.
31
+ 3. **Subclass `CLI::Command` and add nothing it already has.** Refs, the shared
32
+ flags, `filter_entries`, `print_inverted_index`, `no_extras?` — the list is in
33
+ [the-cli](/structure/the-cli.md). Re-implementing one of these is how two
34
+ verbs come to disagree about what `--dir` means.
35
+ 4. **`#call` is the whole public surface.** Everything else is private, so a
36
+ helper cannot become a verb by accident. Answer `.id`, `.group`,
37
+ `.help_rows`, `.hidden?`.
38
+ 5. **Add its `require` to the block at the bottom of `cli.rb`**, in the position
39
+ you want it to appear in `okf help` — that order *is* the help order, and a
40
+ test pins the result.
41
+ 6. **Decide the arity, explicitly.** If it takes one bundle, `no_extras?` must
42
+ make a second an exit-2 usage error. If it takes several, it belongs in the
43
+ `across_bundles/` group with the ones that do.
44
+ 7. **Prove it in every folder it has** — `by_dir/`, `by_registry/`, and
45
+ `across_bundles/` (which for a single-bundle verb means proving the *refusal*).
46
+ Then exercise the whole surface, not the happy path: every flag once, every
47
+ output format it offers, every exit code it can return, and the combinations
48
+ that actually interact.
49
+ 8. **Update what describes it.** A new file under `lib/` needs its line in the
50
+ concept in [structure/](/structure/) that owns its layer, and the verb needs
51
+ its cell in [cli](/cli.md)'s group table.
52
+ `test/unit/bundle_catalog_test.rb` fails on either. So does the gem's README,
53
+ which owes a line for every verb; nothing enforces that one.
54
+ 9. **Run the same test unedited**, then read the uncovered lines:
55
+ `bundle exec rake test:integration`, then diff
56
+ `coverage/integration/.resultset.json` for the files you changed. Three
57
+ shapes hide there by habit, because a unit test walked them first: the
58
+ *second* output format, an *error* branch and the exit code it carries, and
59
+ *malformed-input* robustness.
60
+
61
+ # Where the logic goes
62
+
63
+ Not in the verb. The CLI parses argv, prints and exits; the question belongs to
64
+ the pure model or to an analyser, so that the server, the TUI and the MCP shell
65
+ get the same answer without asking the CLI. If a verb is computing something,
66
+ it is probably a method on `Bundle` that has not been written yet.
67
+
68
+ New I/O goes in the shell, new logic in the core, and
69
+ `test/unit/boundary_test.rb` fails the build if a pure file forgets.
70
+
71
+ # The exit codes are a contract
72
+
73
+ `0` ok, `1` a failing bundle, `2` a usage error. And the older half of that
74
+ contract: **`validate` and `lint` stay separate** — a conformance check may not
75
+ live in lint and a curation finding may not fail validate. See
76
+ [structure/the-analysers](/structure/the-analysers.md).
@@ -0,0 +1,12 @@
1
+ # Testing
2
+
3
+ Why the suite is shaped the way it is — integration first, its coverage read as
4
+ a map rather than a score, and the graph page proven in a real browser — is in
5
+ [Design](/design/): [integration-first](/design/integration-first.md) and
6
+ [browser-tests](/design/browser-tests.md).
7
+
8
+ What is here is the procedure: the ordered walk a new surface owes, ending at the
9
+ checks that will refuse it if a step is skipped.
10
+
11
+ * [Adding a Verb](adding-a-verb.md) - The steps a new command or subcommand owes, in order.
12
+ * [The test harness](the-harness.md) - One base class for the whole suite, and why the 2.4 floor binds `test/` exactly as it binds `lib/`.
@@ -0,0 +1,45 @@
1
+ ---
2
+ type: Component
3
+ title: The test harness
4
+ description: One base class for the whole suite — plain Minitest plus `test "..."` and block setup/teardown — and it runs on 2.4, so the gem's API floor binds the tests exactly as it binds `lib/`.
5
+ resource: gems/okf/test/test_helper.rb
6
+ tags: [testing, minitest, ruby-floor]
7
+ generated:
8
+ by: human:maintainer
9
+ at: 2026-08-19T12:00:00Z
10
+ ---
11
+
12
+ # One base class
13
+
14
+ Every test in this gem subclasses `OKF::TestCase`, defined in
15
+ `test/test_helper.rb`. It is plain Minitest with two pieces of sugar:
16
+
17
+ * `test "a sentence" do … end` instead of `def test_a_sentence`, so a failure
18
+ names the behaviour in prose rather than in snake case;
19
+ * block `setup` / `teardown`, which compose rather than requiring `super`.
20
+
21
+ Nothing else is the base class. A test that reaches for `Minitest::Test`
22
+ directly loses both and reads unlike its neighbours, which is the whole reason
23
+ the sugar exists — the three siblings each ported the same class for the same
24
+ reason, so the suites read alike across the monorepo.
25
+
26
+ # It runs on 2.4, so the floor binds it
27
+
28
+ The suite runs on every supported Ruby, 2.4 included. That makes the
29
+ [Ruby floor](../design/ruby-floor.md)'s forbidden-API list a rule about `test/`
30
+ as much as about `lib/` — a `filter_map` in a test fails the floor container
31
+ exactly as one in a source file does, and RuboCop will not catch either.
32
+
33
+ SimpleCov is the one exception, and it is conditional rather than absent: it
34
+ needs 2.5+, so `test_helper.rb` loads it inside a `begin`/`rescue` and the suite
35
+ simply runs without coverage where it cannot load. Coverage is a reporting
36
+ convenience; the floor is a contract, and the contract wins.
37
+
38
+ # What a test may assume about the disk
39
+
40
+ Fixtures are real directories under `test/fixtures/`, not mocks. A branch that
41
+ no fixture can reach is a branch nobody has ever proven, so the answer to an
42
+ unreachable path is a new fixture rather than a bent assertion — the general
43
+ form of that obligation is `@okf-eco design/how-a-change-is-proven`, and how the
44
+ critical layer is organised is
45
+ [integration first](../design/integration-first.md).