okf 2.0.0 → 2.1.1

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/.okf/capabilities/agent-skill.md +112 -0
  3. data/.okf/capabilities/bundles-manager.md +133 -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 +295 -0
  11. data/.okf/capabilities/validator.md +60 -0
  12. data/.okf/cli.md +193 -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 +662 -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 +265 -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 +67 -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 +83 -16
  44. data/README.md +205 -21
  45. data/lib/okf/cli.rb +20 -2
  46. data/lib/okf/skill/SKILL.md +15 -7
  47. data/lib/okf/skill/playbooks/maintain.md +3 -1
  48. data/lib/okf/skill/playbooks/produce.md +4 -2
  49. data/lib/okf/skill/reference/authoring.md +23 -26
  50. data/lib/okf/skill/reference/cli/checks.md +171 -0
  51. data/lib/okf/skill/reference/cli/graph.md +49 -0
  52. data/lib/okf/skill/reference/cli/map.md +98 -0
  53. data/lib/okf/skill/reference/cli/registry.md +70 -0
  54. data/lib/okf/skill/reference/cli/search.md +130 -0
  55. data/lib/okf/skill/reference/cli/serve.md +83 -0
  56. data/lib/okf/skill/reference/cli/views.md +59 -0
  57. data/lib/okf/skill/reference/cli.md +33 -603
  58. data/lib/okf/skill/reference/spec-map.md +32 -0
  59. data/lib/okf/version.rb +1 -1
  60. metadata +54 -5
@@ -0,0 +1,59 @@
1
+ ---
2
+ type: Component
3
+ title: The Format Layer
4
+ description: "Pure, and the bottom of everything: path normalisation with a root-escape guard, the one YAML door, and the link and citation grammars §5 and §8 are written in."
5
+ tags: [structure, format, pure, yaml, links]
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-08-19T12:00:00Z
9
+ resource: lib/okf/path.rb
10
+ ---
11
+
12
+ # The files
13
+
14
+ | file | what it owns |
15
+ |---|---|
16
+ | `lib/okf.rb` | `OKF::Error`, `OKF.blank?`, `OKF.iso8601`, `OKF.dir_of`, `SPEC_VERSION` |
17
+ | `lib/okf/version.rb` | `OKF::VERSION` |
18
+ | `lib/okf/path.rb` | `Path.normalize_relative!`, `join_under!`, `under?` — and `Path::Error` |
19
+ | `lib/okf/safe_read.rb` | `SafeRead.contained_path!`, `read!` — containment for a caller that already holds a root |
20
+ | `lib/okf/markdown/frontmatter.rb` | parse, dump, `stringify_keys` — **the only** YAML door |
21
+ | `lib/okf/markdown/links.rb` | §6: inline, reference and footnote links, and how a raw target resolves |
22
+ | `lib/okf/markdown/citations.rb` | §8: the `# Citations` section and its entries |
23
+
24
+ # Containment is a primitive here, not a habit
25
+
26
+ `Path.normalize_relative!` rejects any `..` segment outright, which is what makes
27
+ "a concept cannot link out of its own bundle" a property of the format rather
28
+ than a convention. `join_under!` and `under?` are the two questions every writer
29
+ and every reader asks before touching a path, and `SafeRead.read!` is the pair of
30
+ them plus the read, for callers that already hold a root — it resolves symlinks
31
+ and refuses one that leaves.
32
+
33
+ Every layer above reaches for these rather than composing its own check. A second
34
+ containment implementation is the shape this class of bug takes.
35
+
36
+ # One YAML door
37
+
38
+ **All YAML goes through `Markdown::Frontmatter`** — `safe_load`, with `Date` and
39
+ `Time` permitted and aliases off. `PSYCH_KEYWORDS` is the Psych < 3.1
40
+ positional-argument shim, and it lives here precisely so that
41
+ `YAML.safe_load`/`YAML.load` is called in exactly one place in the gem.
42
+
43
+ `stringify_keys` exists so that ActiveSupport does not, which is the same reason
44
+ `OKF.blank?` does.
45
+
46
+ # The link grammar is where the graph comes from
47
+
48
+ `Links.extract` walks prose lines only — `FENCE` and `CODE_SPAN` take fenced
49
+ blocks and code spans out first, because a link inside a code sample is a
50
+ document about a link, not an edge. `INLINE_LINK`, `REFERENCE_LINK` and
51
+ `DEFINITION` cover the three markdown spellings; `FOOTNOTE_REFERENCE` and
52
+ `FOOTNOTE_DEFINITION` are §8's keying. `SCHEME` and `MAILTO` are what keep an
53
+ external URL from being read as a relative path.
54
+
55
+ What is built on top of these is [the-model](/structure/the-model.md).
56
+
57
+ `Links.resolve` is the one that turns a raw target into a bundle-relative path,
58
+ and it is why a link split across a newline produces no edge — the extractor
59
+ reads a line at a time.
@@ -0,0 +1,22 @@
1
+ # Structure
2
+
3
+ Every file under `lib/`, grouped by the layer that owns it. One concept owns
4
+ each file, and `test/unit/bundle_catalog_test.rb` fails if that stops being true
5
+ in either direction — a file no concept names, or a concept naming a file that
6
+ is gone.
7
+
8
+ Fifty files, eight layers, and one rule underneath all of them: **the core is
9
+ pure and the shell does the I/O.** `test/unit/boundary_test.rb` fails if a pure
10
+ file names a shell class or touches `File`, `Dir`, `FileUtils` or stdio. Put new
11
+ logic in the core; put new I/O in the shell.
12
+
13
+ Read it bottom-up — each layer depends only on the ones below it.
14
+
15
+ * [The Format Layer](format-layer.md) - `lib/okf.rb`, `path.rb`, `safe_read.rb`, `version.rb`, `markdown/` — pure: paths, containment, frontmatter, links, citations.
16
+ * [The Model](the-model.md) - `concept.rb`, `bundle.rb`, `bundle/graph.rb`, `references.rb`, `row_filter.rb`, `skeleton.rb` — pure: a bundle in memory, and every derived view of it.
17
+ * [The Analysers](the-analysers.md) - `bundle/validator*.rb`, `bundle/linter*.rb` — pure: §11 conformance, and curation quality, kept deliberately apart.
18
+ * [Search](search.md) - `bundle/search.rb` and its two engines — pure: the facade owns the rows, the engines own the matching.
19
+ * [The Disk Shell](the-disk-shell.md) - `concept/file.rb`, `bundle/reader.rb`, `writer.rb`, `folder.rb`, `registry.rb` — where directories become bundles and back.
20
+ * [The Server and the Page](the-server.md) - `server/app.rb`, `hub.rb`, `hub/not_found.rb`, `runner.rb`, `render/graph.rb` — one ERB template, served or baked.
21
+ * [The CLI](the-cli.md) - `cli.rb`, `cli/command.rb`, and the seventeen verb files — the only layer that parses argv, prints, and exits.
22
+ * [The Skill](the-skill.md) - `skill.rb` — the companion agent skill and its installer.
@@ -0,0 +1,53 @@
1
+ ---
2
+ type: Component
3
+ title: Search — One Facade, Two Engines
4
+ description: The facade owns the rows, the snippets, the ranking fields and the engine registry; an engine owns only matching, declares its capabilities, and is chosen by what the query needs.
5
+ tags: [structure, search, pure, extension-point]
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-08-19T12:00:00Z
9
+ resource: lib/okf/bundle/search.rb
10
+ ---
11
+
12
+ # The files
13
+
14
+ | file | what it owns |
15
+ |---|---|
16
+ | `lib/okf/bundle/search.rb` | the facade: rows, snippets, weights, the `Corpus`, and the engine registry |
17
+ | `lib/okf/bundle/search/scan.rb` | the default engine — raw-text scan, the only one that does `regexp` |
18
+ | `lib/okf/bundle/search/index.rb` | the minifts engine — BM25+, the only one that does `fuzzy` and `prefix` |
19
+
20
+ # The split
21
+
22
+ The facade decides **what a result is**: `WEIGHTS` (which field counts how much),
23
+ `FIELDS`, `SNIPPET_FIELDS`, `SNIPPET_RADIUS`, and the row shape every caller
24
+ reads. An engine decides only **which documents matched**, and says what it can
25
+ do through `CAPABILITIES`.
26
+
27
+ `Search.engine_for(required)` picks by capability, not by name: `ROUTABLE` is the
28
+ set a query can *demand* (`regexp`, `fuzzy`), `DEFAULT_ENGINE` is `:scan`, and
29
+ `UnsupportedQuery` / `UnknownEngine` are the two honest refusals. `available?`
30
+ lets an engine decline at runtime — the index engine needs `minifts` present.
31
+
32
+ `Corpus` is the cross-bundle form, behind `Search.across`.
33
+
34
+ # Why scan leads
35
+
36
+ The scan is the default because a one-shot CLI cannot amortise an index build:
37
+ 3.00 s versus 0.24 s at 1,000 concepts. That is a real argument against the
38
+ `minifts` dependency and it is recorded as such — but `--fuzzy` and parity with
39
+ the graph page's browser-side ranking both still need it, and a cached index
40
+ would restore the case outright.
41
+
42
+ Parity is the subtler half: the Ruby index engine is a bit-for-bit port of the
43
+ browser's MiniSearch, pinned to the same version the page lazy-loads, so an
44
+ `--engine index` result and a search typed into the page rank identically.
45
+
46
+ The verb that drives it is in [the-cli](/structure/the-cli.md).
47
+
48
+ # `Search.register` is an extension point
49
+
50
+ Append-only, idempotent by id, duck-type checked at registration —
51
+ **deliberately the same shape as `CLI.register`**. An engine is a module
52
+ answering `id`, `capabilities`, `available?`, `call`, and optionally `prepare`.
53
+ Adding one is a registration, not an edit to the facade.
@@ -0,0 +1,60 @@
1
+ ---
2
+ type: Component
3
+ title: The Analysers — Validate and Lint, Kept Apart
4
+ description: Two pure analysers with a boundary that is a spec requirement rather than a preference — conformance may not reject curation problems, and curation may not emit conformance errors.
5
+ tags: [structure, validate, lint, pure, spec]
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-08-19T12:00:00Z
9
+ resource: lib/okf/bundle/validator.rb
10
+ ---
11
+
12
+ # The files
13
+
14
+ | file | what it owns |
15
+ |---|---|
16
+ | `lib/okf/bundle/validator.rb` | §11 conformance: hard errors, plus soft convention warnings |
17
+ | `lib/okf/bundle/validator/result.rb` | the errors/warnings collection and `valid?` |
18
+ | `lib/okf/bundle/linter.rb` | the curation report — every check, and their severities |
19
+ | `lib/okf/bundle/linter/report.rb` | findings by severity, the stats, and `healthy?` |
20
+
21
+ # The boundary is the point
22
+
23
+ **`validate` and `lint` stay separate**, and the line is the spec's, not taste.
24
+ §11 forbids the validator from *rejecting* a broken cross-link or a missing
25
+ optional field — those are warnings at most. Curation findings belong to lint,
26
+ which never emits a conformance error.
27
+
28
+ A new check goes to one side or the other, and getting it wrong changes what
29
+ exit code a user's CI sees: 0 ok, 1 a failing bundle, 2 a usage error.
30
+
31
+ `Validator::CONVENTION_CHECKS` is the soft half — the warnings that are about
32
+ convention rather than conformance, so a caller can tell them apart.
33
+
34
+ # Validator: one method per spec clause
35
+
36
+ The private methods are named for what they check — `validate_families`,
37
+ `validate_generated`, `validate_verified`, `validate_sources`,
38
+ `validate_usage_window`, `validate_lifecycle`, `validate_computation`,
39
+ `validate_parameters`, `validate_contract_mapping`, `validate_okf_version`,
40
+ `validate_index`, `validate_log`, `validate_reserved`, `validate_unparseable`.
41
+ A new §5 family is a new one of these; that is the shape to follow.
42
+
43
+ `validate_unparseable` is the one worth noticing: a file the reader could not
44
+ parse is an error, not an absence. A validator that skipped it would report
45
+ clean over the file most likely to be broken.
46
+
47
+ # Linter: severities are data, and downstream depends on them
48
+
49
+ `SEVERITIES` is the whole check list with each check's level, and `CHECKS` is
50
+ its keys. It is a public fact rather than an implementation detail — okf-pro
51
+ pins a frozen snapshot of it, because a released kernel that reclassified a
52
+ check would change what that gem's gate blocks on.
53
+
54
+ `only:` and `except:` select checks; `stale_before:`/`today:` supply the clock
55
+ that `expired` and `stale` need. **A check that could not run is reported, not
56
+ dropped**: `Report#to_h` carries `skipped_checks`, and `healthy?` over a silent
57
+ skip would be the same lie in a smaller box.
58
+
59
+ `DEFAULT_MIN_BODY` and `HUB_LIMIT` are the two tunables; `ACTOR_FORMS` is the
60
+ `generated.by` grammar, shared with the validator through `Concept`.
@@ -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 eight 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,67 @@
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-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.
58
+
59
+ `relative_base` is why a local registry's paths stay relative and the file
60
+ travels with the repository. `Registry#reopen` preserves it; `Registry.new(path)`
61
+ does not, and reaching for the latter is how a reload comes to resolve every
62
+ bundle against the wrong base.
63
+
64
+ `Group` is a named, recursive set of bundles. `RESERVED_SLUGS` is `all`, because
65
+ `@all` means every bundle and may not be shadowed. `slugify`, `dedupe`,
66
+ `normalize` and `path_shaped?` are the naming rules a `registry set` goes
67
+ 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 eight 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/`.