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,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).
|