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,26 @@
1
+ # Capabilities
2
+
3
+ The seven things the baseline gem does over a bundle, plus the read views that
4
+ print it at a glance — and the sibling surfaces built on the same kernel. All of
5
+ them run over the same [pure model](../model/); the gem's own are fronted by the
6
+ [CLI](../cli.md).
7
+
8
+ # Judge
9
+
10
+ * [Conformance validator](validator.md) - the §11 legal check; the only capability that can fail a bundle.
11
+ * [Curation linter](linter.md) - advisory quality report across eight categories; never rejects.
12
+
13
+ # Serve & read
14
+
15
+ * [Interactive graph server](graph-server.md) - a self-contained HTML graph over HTTP — one bundle or many behind a hub — mountable as a Rack app.
16
+ * MCP server (`@okf-mcp design/the-tool-set`) - the sibling `okf-mcp` gem: the kernel's capabilities as MCP tools, resources and prompts for any agent host.
17
+ * [Bundles manager](bundles-manager.md) - the hub's `/b/` page: every bundle with its size, health and default marker, and the forms that manage the registry from a browser.
18
+ * [Static render](render.md) - the same page written to one self-contained static file, the bundle baked in, to host where there is no server (`okf render`).
19
+ * [Read views](read-views.md) - `index`, `catalog`, `files`, `types`, `tags`, `stats`, `loose`, `graph` — the browser views as text, plus the `index` map.
20
+ * [Ranked text search](search.md) - deterministic ranked retrieval over metadata and bodies; answers "which concept covers X?" in a few rows.
21
+
22
+ # Use & author
23
+
24
+ * [Library API](library-api.md) - the Ruby surface: pure model plus on-disk handles.
25
+ * [Companion agent skill](agent-skill.md) - the skill shipped inside the gem that teaches an agent to author OKF.
26
+ * Enforcement layer (`@okf-eco gems/okf-pro`) - the sibling `okf-pro` gem: writes an agent's knowledge repository, then holds it to a few invariants at three doors under a fail-closed contract.
@@ -0,0 +1,82 @@
1
+ ---
2
+ type: Capability
3
+ title: Library API
4
+ description: The Ruby surface — a pure in-memory model plus on-disk handles, an "ActiveRecord for the filesystem".
5
+ resource: gems/okf/lib/okf.rb
6
+ tags: [ruby, diagram]
7
+ generated:
8
+ by: human:maintainer
9
+ at: 2026-08-13T12:00:00Z
10
+ sources:
11
+ - title: README.md — Library
12
+ resource: https://github.com/serradura/okf/blob/main/README.md
13
+ - title: gems/okf/lib/okf.rb
14
+ resource: https://github.com/serradura/okf/blob/main/gems/okf/lib/okf.rb
15
+ ---
16
+
17
+ # Overview
18
+
19
+ Everything the [CLI](../cli.md) does is available in-process. `require "okf"`
20
+ gives you two layers, split cleanly by the [core/shell rule](../design/core-shell-split.md):
21
+
22
+ - **pure, in-memory** — [`OKF::Concept`](../model/concept.md) and
23
+ [`OKF::Bundle`](../model/bundle.md), which you build, interrogate, validate,
24
+ lint, and graph with **no disk involved**;
25
+ - **on-disk handles** — `OKF::Concept::File` and `OKF::Bundle::Folder`, which add
26
+ `load` / `save` / `reload` / `delete` on top of the pure model.
27
+
28
+ `require "okf"` stops at those two layers: the [CLI](../cli.md) and the skill
29
+ installer load only when asked for (from `okf/exe/okf`, or an explicit
30
+ `require "okf/cli"` / `require "okf/skill"`), so an app embedding the library
31
+ never drags in the command-line machinery. The
32
+ [registry](../registry.md) and the server's hub sit behind the same door — the CLI
33
+ requires them at the moment a registry verb or a multi-bundle `server` runs, so
34
+ the library surface stays the same size no matter what the executable grows.
35
+
36
+ ```mermaid
37
+ classDiagram
38
+ class Concept {
39
+ <<pure>>
40
+ }
41
+ class Bundle {
42
+ <<pure>>
43
+ }
44
+ class Graph {
45
+ <<pure>>
46
+ }
47
+ class ConceptFile {
48
+ <<on-disk>>
49
+ }
50
+ class Folder {
51
+ <<on-disk>>
52
+ }
53
+ Bundle "1" o-- "*" Concept : contains
54
+ Bundle ..> Graph : builds
55
+ ConceptFile ..> Concept : wraps
56
+ Folder ..> Bundle : wraps
57
+ ```
58
+
59
+ `ConceptFile` is `OKF::Concept::File`; `Folder` is `OKF::Bundle::Folder` — each
60
+ on-disk handle wraps a pure counterpart and adds load/save/reload/delete.
61
+
62
+ # Build knowledge without touching Markdown
63
+
64
+ The pure layer is the surface an embedding app uses to reuse the gem over
65
+ knowledge it already holds as records. Construct concepts from data, assemble a
66
+ bundle, and call `#validate`, `#lint`, or `#graph` — no Markdown round-trip
67
+ needed. The lower-level pieces work standalone too:
68
+ `OKF::Bundle::Validator.call`, `OKF::Bundle::Linter.call`,
69
+ `OKF::Bundle::Search.call`, `OKF::Bundle::Graph.build`,
70
+ `OKF::Markdown::Frontmatter.parse`.
71
+
72
+ # Folder is an ActiveRecord for the filesystem
73
+
74
+ `OKF::Bundle::Folder.load(dir)` reads a directory into a pure bundle;
75
+ `Folder.new(bundle:, root:).save` materializes one back — and **validates §11
76
+ before publishing** through an atomic writer, so it never leaves a broken bundle
77
+ on disk. `OKF::Server::App.new(folder)` turns a folder straight into the
78
+ [graph server](graph-server.md) — mountable anywhere, and the one option a mount
79
+ owns is `search_endpoint:`, since the page resolves it against the reader's URL
80
+ and only the host knows its own prefix (the `/search` route answers either way).
81
+ And `OKF::Render::Graph.static(folder)` bakes that
82
+ same page into one self-contained file — the Ruby side of [`okf render`](render.md).
@@ -0,0 +1,83 @@
1
+ ---
2
+ type: Capability
3
+ title: Curation linter (lint)
4
+ description: An advisory curation-quality report across eight categories and twenty-six checks — pinned severities, an explicit clock, and it never rejects a bundle.
5
+ resource: gems/okf/lib/okf/bundle/linter.rb
6
+ tags: [curation, cli]
7
+ generated:
8
+ by: human:maintainer
9
+ at: 2026-08-14T12:00:00Z
10
+ sources:
11
+ - title: gems/okf/lib/okf/bundle/linter.rb
12
+ resource: https://github.com/serradura/okf/blob/main/gems/okf/lib/okf/bundle/linter.rb
13
+ ---
14
+
15
+ # Overview
16
+
17
+ `okf lint` answers the question [validate](validator.md) is forbidden to touch:
18
+ *is this bundle well-curated — navigable, complete, trustworthy?* It reports over
19
+ exactly the soft things §11 tolerates, has its own `OKF::Bundle::Linter` and
20
+ report, and never emits a conformance error. It is **advisory**: exit `0` even
21
+ with findings unless you opt in with `--fail-on warn` (any warn) or
22
+ `--fail-on info` (any finding at all).
23
+
24
+ # Eight categories, twenty-six checks — with pinned severities
25
+
26
+ | Category | Checks |
27
+ |----------|--------|
28
+ | Reachability | `orphan`, `not_in_index`, `disconnected_component`, `unlinked` |
29
+ | Backlog | `missing_concept`, `broken_index_entry` |
30
+ | Completeness | `stub`, `missing_title`, `missing_description`, `missing_generated` |
31
+ | Freshness | `expired`, `stale` |
32
+ | Provenance | `uncited_external`, `broken_source`, `unattributed_claim`, `unused_source`, `unprefixed_actor` |
33
+ | Attestation | `incomplete_computation`, `broken_attestation_ref` |
34
+ | Migration | `legacy_timestamp`, `legacy_citations` |
35
+ | Hygiene | `duplicate_title`, `unused_reference_def`, `undefined_reference`, `self_link`, `log_order` |
36
+
37
+ `log_order` is the first log-side check: §9 describes the log as date-grouped
38
+ entries newest first — prose, not an RFC keyword, so disorder is curation
39
+ slack here, never a [validator](validator.md) error, and only shape-valid
40
+ headings are compared (a malformed date is already the validator's, reported
41
+ once). `unprefixed_actor` reads both §7 identity fields with one consequence
42
+ each: a bare `verified[].by` reads as machine-confirmed (the §5.3 misread the
43
+ tier system exists to prevent), while a bare `generated.by` feeds no tier and
44
+ costs the audit trail — nobody downstream can tell a person from a process.
45
+
46
+ **Severity is API.** Every id has a pinned level in one tested constant
47
+ (`Linter::SEVERITIES`): machine consumers gate repo edits and CI on `:warn`
48
+ and drop `:info`, so a severity change is a behavior change for them — a new
49
+ gateable state gets a flag (`--fail-on info`), never a severity promotion.
50
+ Two of the calls carry their argument in the code: `unattributed_claim` warns
51
+ while its join-twin `unused_source` informs (a dangling footnote misattributes
52
+ a claim; an uncited source is only slack), and `expired` informs because a
53
+ `stale_after` passes on the calendar, not on a change — a warn would fail a
54
+ `--fail-on warn` gate on a morning nobody chose. The Migration pair is info
55
+ for the same reason: §13 says a v0.1 bundle is consumable forever, and a
56
+ migration campaign gates explicitly with
57
+ `--only legacy_timestamp,legacy_citations --fail-on info`.
58
+
59
+ Select with `--only` / `--except` (by check id), tune the stub threshold with
60
+ `--min-body`, and get the whole report as a machine substrate with `--json` —
61
+ which also carries the bundle's posture: a `trust` tier distribution (in the
62
+ hyphenated wire spelling) and an effective-`status` frequency.
63
+
64
+ # The clock contract
65
+
66
+ The linter is pure and never reads a clock: `expired` runs only when the
67
+ caller supplies `today:` (the CLI passes today, or `--today YYYY-MM-DD` for a
68
+ reproducible report), and `stale` only when `--stale-after
69
+ <90d | 12w | 2026-01-01>` supplies a cutoff over `generated_at`. Every
70
+ clock-gated check that was selected but could not run is *named* in
71
+ `stats[:skipped_checks]` — a gate that is sometimes absent and does not
72
+ confess converts "unchecked" into "checked and fine". The flag and the
73
+ frontmatter field that share the `stale-after` spelling are different
74
+ mechanisms: the flag is the reader's age cutoff (`stale`), the field the
75
+ author's declared expiry (`expired`).
76
+
77
+ # Where lint stops and an agent begins
78
+
79
+ `lint` is structural: it cannot judge **contradictions** or **semantic**
80
+ staleness (a concept that parses fine but no longer matches reality). Those need
81
+ meaning. `lint --json` is precisely the structured input an agent reasons over to
82
+ close that gap. The [`loose`](read-views.md) view is a folder-grouped lens over
83
+ the single `unlinked` check.
@@ -0,0 +1,228 @@
1
+ ---
2
+ type: Capability
3
+ title: Read views (index, dirs, catalog, files, references, types, tags, stats, loose, graph)
4
+ description: The server's browser panels reproduced on the CLI, plus the index map, so an agent reads a bundle at a glance without a browser.
5
+ tags: [read, cli, json]
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-08-13T12:00:00Z
9
+ sources:
10
+ - title: cli.md — read views
11
+ resource: https://github.com/serradura/okf/blob/main/gems/okf/lib/okf/skill/reference/cli.md
12
+ ---
13
+
14
+ # Overview
15
+
16
+ The [graph server](graph-server.md) renders a bundle in a browser; these verbs
17
+ render the same knowledge as text, so an agent (or a terminal) reads it directly.
18
+ Each prints a scannable human view by default and machine JSON with `--json`, and
19
+ all are advisory reads that exit `0`. They share
20
+ [`OKF::Bundle#catalog`](../model/bundle.md) and the
21
+ [graph indexes](../model/graph.md) for their data. When the question is lexical —
22
+ "which concept covers X?" — [ranked search](search.md) cuts across every field
23
+ these views group by, for the price of a few rows.
24
+
25
+ # The views
26
+
27
+ | Verb | Shows | Grouped by |
28
+ |------|-------|------------|
29
+ | `index` | each directory's index body, type/tag rollup, child dirs, and concept listing | directory (root first) |
30
+ | `dirs` | every directory with the concepts living directly in it | directory (root first) |
31
+ | `catalog` | concepts with type, tags, link counts, status | top-level dir |
32
+ | `files` | files with titles | folder |
33
+ | `references` | the on-disk `references/` tree (§6.3) with each file's citers and the dangling §6.2 pointers | folder |
34
+ | `types` | types (`@okf-eco format/frontmatter`) with their concepts | count |
35
+ | `tags` | tags (`@okf-eco format/frontmatter`) with their concepts | count |
36
+ | `stats` | rollups: concepts, dirs, types, cross-links, tags | — |
37
+ | `loose` | degree-0 concepts (no links (`@okf-eco format/cross-links`) in or out) | folder |
38
+ | `graph` | the raw nodes and edges; `--hubs` ranks inbound links by source top-level dir; `--traffic` collapses concepts into dirs and links into weighted arcs, with cohesion | — (`--minimal` / `--no-body`) |
39
+
40
+ `dirs` and `stats` answer about the *same* set of directories, deliberately:
41
+ both read `Bundle#directory_index`, the map `--dir` is resolved against. Grouping
42
+ the catalog instead — which knows only the directories that happen to hold a
43
+ concept — made the two verbs disagree about how big a bundle was (`stats` said 2
44
+ where `dirs` listed 3 on a bundle whose root carries an index.md and no
45
+ concepts), and worse, left a directory out of `by_dir` that `--dir` answers
46
+ about. Counts stay direct, so a directory holding nothing itself reports the zero
47
+ it holds rather than disappearing; `by_dir.keys` is therefore the complete list
48
+ of what `--dir` can name.
49
+
50
+ `references` is the one view whose rows are not concepts: the reader models
51
+ only markdown, so a `.py` attester or a `.sql` computation exists to no other
52
+ surface, and this verb lists the `references/` tree from disk with which
53
+ concepts cite each file through the §6.2 path-valued fields — plus every
54
+ pointer that resolves to nothing, the bare-path trap (§6.2 resolves a bare
55
+ `references/…` relative to the concept) named with its leading-slash fix.
56
+ Folder scope is deliberate: §6.3 is a naming convention, not a requirement,
57
+ and a computation stored beside its concept is legal and not inventoried.
58
+
59
+ Every one of them names the bundle it answers about, in the identity the caller
60
+ used — the rule the [CLI](../cli.md) keeps: `bundle` is always the directory,
61
+ `slug` always a registry slug, and a header that reads `@handbook (/path)` when a
62
+ [ref](../registry.md) named it. `graph` was the last holdout, printing a bare pair
63
+ of counts over a bare `nodes`/`edges` payload; an agent holding several bundles
64
+ had nothing in that answer to tell them apart.
65
+
66
+ # `index` is the orient-first map (§8)
67
+
68
+ Alone among the read views, `index` shows the reserved `index.md` layer: the
69
+ concept views skip those structural files, so only `index` renders the
70
+ progressive-disclosure map (`@okf-eco format/okf-format`) — one entry per directory
71
+ (root first) with its authored index body, a type/tag rollup over the concepts
72
+ living directly there, its child directories, and the concept listing. `--dir`
73
+ narrows to a directory and its subtree and repeats (`root` names the bundle
74
+ root, unless one is really called that) — bringing the chain up to the root with
75
+ it, marked `↑`, since orientation
76
+ is the whole point of this view and a branch shown alone has lost it —
77
+ `--depth N` bounds how far below the starting point that reaches, and
78
+ `--no-body` drops the prose to a skeleton. The last two are what make the map
79
+ usable at scale: every directory is a section, so a few hundred concepts is a map
80
+ nobody reads whole — `--depth 1 --no-body` orients, `--dir <branch> --depth 1`
81
+ descends, and the pair walks the tree a level at a time instead of paging it. It is the cheapest orientation when picking up a
82
+ bundle, and the only view that exposes *enumeration drift* — a listing entry that
83
+ should exist but is missing, which no grep can find. A directory that holds
84
+ concepts but no `index.md` gets its listing synthesized and tagged `(no index.md)`,
85
+ a prompt to write a real map rather than a defect.
86
+
87
+ # `dirs` is the shape, `index` is the contents
88
+
89
+ `dirs` answers the question `--dir` is pointed at: every directory the bundle
90
+ has — those holding concepts, those carrying an `index.md` or a scoped
91
+ `log.md`, and the empty intermediates that exist only to connect the tree —
92
+ with two counts each. A directory whose only file is its history is real (the
93
+ log verbs read it); one holding only a file the reader skipped is not, and
94
+ stays out of the map on every surface at once.
95
+
96
+ `count` is **direct**, never cumulative: the column sums to the bundle's concept
97
+ count, and a directory holding nothing but sub-directories reads `0` rather than
98
+ borrowing its children's weight. That honesty is also its limit, and why
99
+ `subtree` sits beside it: truncate the listing on a deep bundle and every row at
100
+ the top of the tree reads `0`, which is precisely where "where is the mass?" is
101
+ being asked. `subtree` is defined as *exactly what `--dir` on that row returns*,
102
+ so the number and the flag can never disagree — and the root's subtree is
103
+ therefore its own direct count, since `.` is a prefix of nothing. The human
104
+ table shows the second column only where some directory actually nests; a flat
105
+ bundle would only see the first one repeated.
106
+
107
+ `--dir` (repeatable) takes a subtree — **and the chain up to the root with it**,
108
+ so a branch is never shown adrift of the authored context that says what it is.
109
+ Those rows are marked (`↑`, `ancestor: true`) and stay out of `total`, which is
110
+ what keeps a row's `subtree` equal to the total `--dir` on that row returns;
111
+ `--no-ancestors` drops them. A `--dir` naming nothing gains no chain, since a
112
+ lone root row would read as a partial answer to a query that matched nothing.
113
+ `--depth N` bounds how far below the starting point the descent reaches — the `--dir` when one is given, the bundle root
114
+ otherwise. **Relative, not absolute**, so `--dir a/b --depth 1` reads "a/b and
115
+ one level under it" without the caller first working out how deep `a/b` is, and
116
+ the two compose the way a reader descending a tree actually moves. `--depth 0`
117
+ is the starting point alone. The deprecated `--area` combines with **neither**
118
+ `--depth` nor `--dir` and is refused (exit 2) for one reason wearing two shapes:
119
+ it is *exact*. With `--depth` it names no starting point to be relative to, so
120
+ the pair unioned the area with every directory at that depth from the root; with
121
+ `--dir` one side is exact where the other is a prefix, so the map came back with
122
+ the area *and* the subtree. Both read like an answer and are an answer to neither
123
+ question.
124
+
125
+ Matching folds case, but a row is found by its *stored* spelling, so the chain is
126
+ walked folded and handed back in the map's own words. It used to be handed back
127
+ folded, which silently dropped every ancestor of a directory spelled with a
128
+ capital — the chain the flag exists to draw, missing exactly the row that places
129
+ the branch.
130
+
131
+ One line per directory where `index` is the whole map, so it is the cheaper of
132
+ the two when the question is shape rather than contents — and `dirs` is the first
133
+ thing to run on an unfamiliar bundle. Cheaper structurally, not by measurement:
134
+ `dirs` emits one row per *directory* and `index` one listing row per *concept*
135
+ even under `--no-body`, so their costs scale with different things. The root stores `.` and
136
+ prints `(root)` — the split every grouped view keeps, so a table and its `--json`
137
+ never disagree about which spelling is the data.
138
+
139
+ # JSON output — compact, and projectable
140
+
141
+ `--json` is **compact by default** — single-line, the token-efficient substrate an
142
+ agent consumes; `--pretty` (which implies `--json`) indents the same JSON for a
143
+ human. On the per-item list views — `index`, `catalog`, `files`, `dirs` — `--fields a,b`
144
+ keeps only those properties and `--except a,b` drops them (mutually exclusive; an
145
+ unknown name is a usage error that lists the valid ones). Projection runs before
146
+ emission, so an agent never pays tokens for a field it dropped: `okf index <dir>
147
+ --except body,listing` is the lean directory skeleton, the difference between a few
148
+ hundred bytes and hundreds of KB on a large bundle.
149
+
150
+ # Narrowing and regrouping
151
+
152
+ The four list views — `catalog`, `files`, `types`, `tags` — accept the filters
153
+ *orthogonal* to how they group, so you ask a narrow question instead of paging
154
+ the whole bundle (matching is case-insensitive):
155
+
156
+ | View | Filters it accepts |
157
+ |------|--------------------|
158
+ | `catalog`, `files` | `--type`, `--dir`, `--tag` |
159
+ | `types` | `--dir`, `--tag` (it already groups by type) |
160
+ | `tags` | `--type`, `--dir` (it already groups by tag) |
161
+
162
+ `--dir` is one rule: a concept matches when its directory *is* the path or sits
163
+ below it, so `--dir platform` reaches `platform/services/api` and `--dir .` (or
164
+ `root`) means the root alone. A trailing slash is accepted and ignored, because
165
+ the human views print one — `index` labels a row `platform/services/` — and a
166
+ flag that refuses the label the tool just printed answers "nothing found" to a
167
+ directory that is full.
168
+
169
+ The `root` spelling has one exception, and it is the shape that showed why an
170
+ alias is never free: a bundle holding a real `root/` directory. There the word
171
+ names that directory, not the bundle root, because the alias only ever bought a
172
+ shell quote and the directory would otherwise be unaddressable — answering for
173
+ the root instead, exit 0, nothing said. The resolution is against the bundle's
174
+ own directories rather than the string alone, which is also what the *stored*
175
+ spellings must not go through: `dirs` folded each row's own dir through the
176
+ same helper, so a row named `root` computed its `subtree` against the bundle
177
+ root and disagreed with what `--dir` on that row returns.
178
+
179
+ "The bundle's own directories" has to mean one list, and that is the lesson the
180
+ first attempt missed. The concept views asked the **catalog**, which knows only
181
+ directories that hold concepts, while `dirs` and `index` asked the directory
182
+ map, which counts an `index.md` too — so a `root/` carrying nothing but its
183
+ index stayed folded in `catalog` and named in `dirs`, in the same bundle, on the
184
+ same flag. One question gets one source (`Bundle#directories`) — and the list
185
+ counts every file kind that makes a directory real, because the same bug came
186
+ back one kind over: a `root/` holding only a scoped `log.md` was invisible to
187
+ the set until the log joined the seed. The other half
188
+ is *when*: the alias is a fact about a bundle, so a run naming several resolves
189
+ it once across the served set — resolving inside the per-bundle loop let a
190
+ single `--dir root` mean the subtree in one bundle and the root in the next,
191
+ merged into one ranking with nothing saying so. It replaces `--area`, which saw only a concept id's
192
+ first path segment — a word the OKF format (`@okf-eco format/okf-format`) never used —
193
+ and which still works, warning on stderr, until a later release drops it.
194
+
195
+ `tags --by type|dir` regroups the tag index under each concept dimension with
196
+ within-group counts — the view for curating a
197
+ tag (`@okf-eco format/frontmatter`) vocabulary: which tags cluster in which directory,
198
+ which type leans on which tags. Each row also carries the tag's total across
199
+ the narrowed set, printed `count/total` when they differ (`async 2/3`) and as
200
+ a plain count when the tag is wholly local — so a tag's *locality* (domain
201
+ confined to one directory, or concern cutting across several) reads per row. The
202
+ same evidence question for the graph: `graph --hubs` ranks every concept with
203
+ inbound links (`@okf-eco format/cross-links`) by inbound degree and groups each
204
+ hub's links by *source top-level dir* — whether a hub is well-homed, answered
205
+ mechanically.
206
+
207
+ `graph --traffic` asks it one grain coarser, about **directories**. It reads the
208
+ [skeleton](../model/skeleton.md): concepts collapse into the directory they live in
209
+ and the links between two directories into one weighted arc, so each row carries
210
+ that directory's traffic split three ways — internal, out, in — plus
211
+ **cohesion**, its internal share. That is
212
+ cohesion-versus-coupling applied to a knowledge tree, and it is the only read at
213
+ the grain `refine` actually decides at: `--hubs` says whether a *concept* is
214
+ well-homed, `--traffic` says whether a *directory* is holding together, acting
215
+ as a shared vocabulary everyone cites, or behaving like a projection that should
216
+ have been an index. `--cut` is fitted to the bundle rather than fixed, because a
217
+ fixed weight left 2 arcs on one bundle and 136 on another; cohesion is computed
218
+ over every arc regardless of the cut, so narrowing the picture never moves the
219
+ evidence.
220
+
221
+ # `loose` is a curation lens, not an error
222
+
223
+ `loose` is the folder-grouped view over [lint](linter.md)'s `unlinked` check —
224
+ distinct from `orphan`. An `index.md` listing makes a file *reachable* (not an
225
+ orphan) but is **not a graph edge**, so a listed file can still float here. A
226
+ loose file may be perfectly fine — a terminal leaf like a backlog item is loose
227
+ by design — so `loose` surfaces the set for a human or agent to judge and always
228
+ exits `0`.
@@ -0,0 +1,66 @@
1
+ ---
2
+ type: Capability
3
+ title: Static render (render)
4
+ description: One self-contained HTML file with the whole graph baked in — the same page `okf server` serves, written to disk so it hosts where nothing runs (`okf render`).
5
+ resource: gems/okf/lib/okf/render/graph.rb
6
+ tags: [server, graph]
7
+ generated:
8
+ by: human:maintainer
9
+ at: 2026-08-13T12:00:00Z
10
+ sources:
11
+ - title: gems/okf/lib/okf/cli/render.rb
12
+ resource: https://github.com/serradura/okf/blob/main/gems/okf/lib/okf/cli/render.rb
13
+ - title: gems/okf/lib/okf/render/graph.rb
14
+ resource: https://github.com/serradura/okf/blob/main/gems/okf/lib/okf/render/graph.rb
15
+ ---
16
+
17
+ # Overview
18
+
19
+ `okf render` writes the [graph page](graph-server.md) as one static,
20
+ self-contained HTML file with the whole bundle baked in — the *same* page
21
+ [`okf server`](graph-server.md) serves live, one switch apart — so the graph
22
+ hosts anywhere there is no server to answer a `fetch()`. GitHub Pages is the
23
+ motivating case: commit the file and the graph is browsable with no process
24
+ running behind it.
25
+
26
+ # One template, two modes
27
+
28
+ There is no second renderer to keep in sync with the server, because there is no
29
+ second renderer. One `OKF::Render::Graph` — the view paired with the pure
30
+ [graph model](../model/graph.md) — draws the page for both modes, and `okf render`
31
+ bakes the bundle in through its `.static`/`.payload`. Every data read the page
32
+ makes — a body, a description, the
33
+ catalog, the §8 map, the §9 logs — flows through a small set of getter functions,
34
+ and an injected `EMBED` constant chooses their source: `null` when served, so the
35
+ getters `fetch()` the live endpoints; the whole payload when rendered, so they
36
+ resolve from the page itself. One interface, two adapters, and the views never
37
+ know which is behind them. The [bundle switcher](graph-server.md) obeys the same
38
+ discipline in reverse — a static file injects an empty sibling list, so the
39
+ affordance that could only dead-end never appears.
40
+
41
+ # It carries both XSS guards
42
+
43
+ Baking the bodies in does not lower the page's defenses. An embedded body takes
44
+ the *inlined* path and the *rendered* one at once: `json_for_script` escapes it at
45
+ inject time so a `</script>` inside a body cannot break out of its `<script>`, and
46
+ it is still run through `DOMPurify.sanitize(marked.parse(...))` before the getter
47
+ hands it to the DOM. The [same trust boundary](../design/server-trust-boundary.md)
48
+ the live server keeps, carried onto the one static path — a rendered file is no
49
+ laxer than the server it was baked from.
50
+
51
+ # Output, flags, and the price
52
+
53
+ `okf render <dir>` prints the page to stdout — `okf render ./docs > public/index.html` —
54
+ or writes `-o FILE` directly, reporting the concept count when it does. A `-o`
55
+ path it cannot write is a usage error (exit `2`), not a backtrace — the same
56
+ [best-effort contract](../cli.md) the read views keep. `--layout NAME` bakes in a
57
+ graph layout, `--map` bakes the [Map view](graph-server.md) as the opening state
58
+ (no arrows, the directories boxed), and `-t`/`-l` set the title and the link the
59
+ page unfurls as a preview card, since a static file cannot be told them later. The price is weight:
60
+ every body is inlined, where a live [`okf server`](graph-server.md) ships a minimal
61
+ graph and pulls bodies on demand — so the server stays the choice for a bundle too
62
+ large to ship whole, and the static file carries no compression of its own
63
+ (whatever host serves it compresses instead). That weight buys something back:
64
+ because every body is already in the page, the static file's
65
+ [full-text search](graph-server.md) reaches **bodies and source text** offline,
66
+ where the live server — bodies still lazy — can only index their metadata.