okf 1.13.0 → 2.1.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +222 -0
  3. data/README.md +34 -11
  4. data/lib/okf/bundle/folder.rb +27 -1
  5. data/lib/okf/bundle/graph.rb +12 -3
  6. data/lib/okf/bundle/linter.rb +470 -47
  7. data/lib/okf/bundle/reader.rb +10 -4
  8. data/lib/okf/bundle/references.rb +111 -0
  9. data/lib/okf/bundle/row_filter.rb +53 -0
  10. data/lib/okf/bundle/search.rb +20 -2
  11. data/lib/okf/bundle/validator/result.rb +6 -3
  12. data/lib/okf/bundle/validator.rb +267 -26
  13. data/lib/okf/bundle/writer.rb +1 -1
  14. data/lib/okf/bundle.rb +105 -4
  15. data/lib/okf/cli/catalog.rb +1 -1
  16. data/lib/okf/cli/command.rb +27 -8
  17. data/lib/okf/cli/files.rb +1 -1
  18. data/lib/okf/cli/index.rb +1 -1
  19. data/lib/okf/cli/lint.rb +70 -12
  20. data/lib/okf/cli/references.rb +97 -0
  21. data/lib/okf/cli/search.rb +2 -1
  22. data/lib/okf/cli/stats.rb +3 -39
  23. data/lib/okf/cli/tags.rb +6 -43
  24. data/lib/okf/cli/types.rb +1 -1
  25. data/lib/okf/cli/validate.rb +3 -3
  26. data/lib/okf/cli.rb +24 -3
  27. data/lib/okf/concept.rb +362 -10
  28. data/lib/okf/markdown/citations.rb +41 -4
  29. data/lib/okf/markdown/frontmatter.rb +1 -1
  30. data/lib/okf/markdown/links.rb +67 -7
  31. data/lib/okf/render/graph/template.html.erb +173 -41
  32. data/lib/okf/render/graph.rb +11 -3
  33. data/lib/okf/server/app.rb +47 -15
  34. data/lib/okf/server/hub.rb +1 -1
  35. data/lib/okf/skill/SKILL.md +28 -18
  36. data/lib/okf/skill/playbooks/curate.md +8 -3
  37. data/lib/okf/skill/playbooks/doctor.md +3 -1
  38. data/lib/okf/skill/playbooks/maintain.md +10 -7
  39. data/lib/okf/skill/playbooks/menu.md +5 -4
  40. data/lib/okf/skill/playbooks/migrate.md +31 -8
  41. data/lib/okf/skill/playbooks/produce.md +20 -11
  42. data/lib/okf/skill/playbooks/search.md +2 -2
  43. data/lib/okf/skill/reference/SPEC.md +739 -187
  44. data/lib/okf/skill/reference/authoring.md +164 -48
  45. data/lib/okf/skill/reference/cli/checks.md +171 -0
  46. data/lib/okf/skill/reference/cli/graph.md +49 -0
  47. data/lib/okf/skill/reference/cli/map.md +98 -0
  48. data/lib/okf/skill/reference/cli/registry.md +70 -0
  49. data/lib/okf/skill/reference/cli/search.md +130 -0
  50. data/lib/okf/skill/reference/cli/serve.md +83 -0
  51. data/lib/okf/skill/reference/cli/views.md +59 -0
  52. data/lib/okf/skill/reference/cli.md +44 -501
  53. data/lib/okf/skill/reference/spec-map.md +32 -0
  54. data/lib/okf/skill/templates/attested-computation.md +41 -0
  55. data/lib/okf/skill/templates/concept.md +13 -6
  56. data/lib/okf/skill/templates/root-index.md +1 -1
  57. data/lib/okf/version.rb +1 -1
  58. data/lib/okf.rb +22 -2
  59. metadata +13 -1
@@ -5,6 +5,32 @@
5
5
  reimplemented in this skill. They run the deterministic `okf` executable shipped by
6
6
  the companion gem — the single source of truth for OKF mechanics. Your job is to
7
7
  invoke it correctly and interpret the result, not to reason out conformance by hand.
8
+ ## Which file answers what
9
+
10
+ This file is the **index and the shared contract**: what every verb has in
11
+ common — refs, exit codes, `--json`, the filters — lives here, and each verb's
12
+ own semantics, traps, and JSON shape live in one file below. Read this one, then
13
+ the single row the question calls for, and stop; a leaf names its prerequisite
14
+ when it has one.
15
+
16
+ | File | Verbs | What it answers |
17
+ |------|-------|-----------------|
18
+ | [cli/checks.md](cli/checks.md) | `validate` `lint` `loose` `references` | what makes a bundle non-conformant, every lint check and the severity it is pinned at, the two clocks that share the `stale_after` spelling, why loose ≠ orphan, which dangling pointers each surface can see |
19
+ | [cli/search.md](cli/search.md) | `search` | ranked retrieval over metadata and bodies — exact by default, the two engines and what the index silently loses, `@all` across bundles, the two JSON envelopes |
20
+ | [cli/map.md](cli/map.md) | `index` `dirs` | orientation: the §8 directory map and the cluster sizes, how `--dir`/`--depth`/ancestors compose, what a synthesized listing means, how not to page the bundle |
21
+ | [cli/views.md](cli/views.md) | `catalog` `files` `tags` `types` `stats` | the browser panels as text — per-concept metadata, the folder tree, tag and type rollups, bundle totals, and the JSON each emits |
22
+ | [cli/serve.md](cli/serve.md) | `server` `render` | the interactive page and its static twin — what renders, what is fetched live, many bundles behind one hub, the trust boundary both share |
23
+ | [cli/registry.md](cli/registry.md) | `registry` (`init` `set` `del` `default` `rename` `group` `ungroup` `list`) | naming bundles once: which file is written and how it is found, path-keyed vs slug-keyed verbs, groups, why the default is a position |
24
+ | [cli/graph.md](cli/graph.md) | `graph` | the raw node/edge dump and what it costs, plus the two rankings [refine](../playbooks/refine.md) reads — `--hubs` by concept, `--traffic` by directory |
25
+
26
+ A question that ends in *what does the spec actually say* leaves the CLI
27
+ entirely: [spec-map.md](spec-map.md) names the § and
28
+ [SPEC.md](SPEC.md) carries its words.
29
+
30
+ A verb `okf help` lists and no row here documents is an **extension's**, not a
31
+ gap in the docs — ask `okf <verb> --help`. A row whose file is missing is a note
32
+ to make, not a reason to stop: grep `reference/cli/` for the verb and proceed.
33
+ <!-- rule:okf-cli-index -->
8
34
 
9
35
  ## When it isn't installed
10
36
 
@@ -24,7 +50,8 @@ cannot: each verb's semantics, its traps, and its JSON shape.
24
50
  its own, listed under `installed extensions:` in `okf help`. So a verb that
25
51
  `--help` shows and this file does not document is **normal, not a documentation
26
52
  error** — ask `okf <verb> --help` for it, and expect nothing here about its
27
- semantics or JSON. Everything below documents the built-ins only.
53
+ semantics or JSON. Every file this index routes to documents the built-ins
54
+ only.
28
55
 
29
56
  **`--json` is compact by design.** Every emitting verb prints single-line JSON —
30
57
  the token-efficient substrate you consume; `--pretty` (which implies `--json`)
@@ -41,7 +68,7 @@ for a field you dropped — e.g. `okf index <dir> --except body,listing` is the
41
68
  directory *skeleton* (structure + rollups), and on a large bundle that is the
42
69
  difference between a few hundred bytes and hundreds of KB, since the per-item rows
43
70
  (`listing`) dominate at scale. `okf index --no-body` is shorthand for dropping just
44
- `body`.
71
+ `body`. <!-- rule:okf-project-json -->
45
72
 
46
73
  **Every output names its bundle.** Two keys, one meaning each: `bundle` is
47
74
  always a directory, `slug` always a registry slug. Name a bundle by `@slug` and
@@ -65,7 +92,7 @@ or `okf index @` work from any directory, no path recall needed.
65
92
 
66
93
  **Exit codes:** `0` success · `1` non-conformant bundle (or a `lint --fail-on`
67
94
  threshold crossed) · `2` usage error. `graph`, `server`, and `render` are best-effort
68
- 9): a file the reader cannot use — frontmatter that will not parse, or a file it
95
+ 11): a file the reader cannot use — frontmatter that will not parse, or a file it
69
96
  cannot open at all — is skipped and noted on stderr, never fatal. The note counts;
70
97
  `validate` names each file and why.
71
98
 
@@ -73,324 +100,20 @@ cannot open at all — is skipped and noted on stderr, never fatal. The note cou
73
100
  `server` mounts them; hand a second bundle to any other verb — two dirs, two refs,
74
101
  or a mix — and it is a usage error (exit 2), never a silent answer about the first.
75
102
  To ask the same question of several bundles, ask `search`, or ask each in turn.
76
-
77
- ## validate — the hard gate (§9)
78
-
79
- Implements the spec's §9 conformance definition exactly:
80
-
81
- - **§9.1** every non-reserved file has a parseable YAML frontmatter block;
82
- - **§9.2** every such block has a non-empty `type`;
83
- - **§9.3** any `index.md`/`log.md` present follows §6/§7 (a nested `index.md` has
84
- no frontmatter, a root `index.md` carries only `okf_version`, `log.md` date
85
- headings are ISO `YYYY-MM-DD`).
86
-
87
- `ERROR`s are the three conditions above; the bundle is non-conformant until every
88
- one is fixed. `warn`s are soft missing recommended fields, non-list tags, an
89
- unparseable timestamp, and **broken cross-links, which §5.3 explicitly tolerates**.
90
- Fix warnings when cheap; never block on them. Use `--json` in CI.
91
-
92
- ## lint — curation quality (advisory)
93
-
94
- Asks the complementary question to `validate`: not "is this legal OKF?" but "is
95
- this well-curated, navigable, trustworthy?" — precisely over the things §9 forbids
96
- `validate` from rejecting. It has its own report, never emits conformance errors,
97
- and **exits `0` even with findings** unless you pass `--fail-on warn`.
98
-
99
- Six conceptual categories, each backed by individual checks (names in parens):
100
-
101
- - **reachability** — orphans, concepts not in any index, disconnected islands,
102
- and unlinked (degree-0) files
103
- (`orphan`, `not_in_index`, `disconnected_component`, `unlinked`)
104
- - **backlog** — demand-ranked missing concepts (linked-to but absent), broken index entries
105
- (`missing_concept`, `broken_index_entry`)
106
- - **completeness** — stubs, missing `title` / `description` / `timestamp`
107
- (`stub`, `missing_title`, `missing_description`, `missing_timestamp`)
108
- - **freshness** — concepts older than a cutoff (`stale`) — **only computed when you
109
- pass `--stale-after`; a plain `okf lint` never reports staleness at all**
110
- - **provenance** — uncited external claims, broken citations, spec §8
111
- (`uncited_external`, `broken_citation`)
112
- - **hygiene** — duplicate titles, unused/undefined reference links, self-links
113
- (`duplicate_title`, `unused_reference_def`, `undefined_reference`, `self_link`)
114
-
115
- `--only` / `--except` filter by the **individual check names above**, not the
116
- category labels — `okf lint <dir> --only orphan,stub` works; `--only reachability`
117
- is an error. Two knobs tune specific checks: `--min-body N` sets the `stub` body
118
- threshold in characters (default 50), and `--stale-after DUR` sets the `stale`
119
- cutoff — a duration like `90d` or `12w`, or an ISO date like `2026-01-01` (a bare
120
- number is rejected).
121
-
122
- `lint --json` is the structured substrate you consume to reason about the two
123
- things lint deliberately does **not** compute — contradictions and *semantic*
124
- staleness — which need understanding of meaning.
125
-
126
- ## loose — files with no graph connections (by folder)
127
-
128
- Lists the **loose** files — concepts with graph **degree 0**: no cross-links in
129
- *or* out — grouped by folder. It is a focused, folder-organized view over `lint`'s
130
- `unlinked` check (`okf loose <dir>` ≈ `okf lint <dir> --only unlinked`, regrouped),
131
- for the "which files float in the graph?" question. Advisory: **exits `0`**; `--json`
132
- emits `{ bundle, count, loose: [{ id, title, dir }] }`.
133
-
134
- **Loose ≠ orphan** — the trap. `lint`'s `orphan` is about *reachability*, and an
135
- `index.md` listing makes a file reachable, so an indexed file is never an orphan.
136
- But an index listing is **not a graph edge**: a file can be listed in an index yet
137
- have no cross-links, so it floats in the graph while `lint` reports it as reachable.
138
- `loose`/`unlinked` catch exactly that gap. A loose file is not automatically a
139
- defect — a terminal leaf (a backlog item, a spec reference) can be loose by design;
140
- `loose` surfaces the set so you can judge intent (see the
141
- [maintain playbook](../playbooks/maintain.md)).
142
-
143
- ## search — ranked text retrieval (metadata + body)
144
-
145
- The browser page's search brought to the CLI and extended to bodies, so "which
146
- concept covers X?" costs rows, not body reads. `okf search <dir> <term…>`:
147
- terms AND together — every term must hit at least one searched field, not
148
- necessarily the same one — matched **literally against raw text** by default, or
149
- as Ruby regular expressions with `--regexp`/`-e` (an invalid pattern is a usage
150
- error, exit 2). `--fuzzy` forgives typos; pairing it with `-e` is a usage error,
151
- since a pattern is matched literally rather than by edit distance.
152
- `--in a,b` restricts the searched fields (title, id, tags, type, description,
153
- body); the shared `--type/--dir/--tag` filters narrow the candidates *first*,
154
- so a search scoped by what `index` taught you stays surgical.
155
-
156
- **The default is exact, so an exact query means what it looks like.** A phrase in
157
- one argument (`"dedup key"`), a dotted version (`7.2.0`), an underscored
158
- identifier (`customer_id`), a mid-word fragment (`ustomer`) and a word written in
159
- `backticks` all match literally. This is what the scan engine buys, and it is the
160
- default precisely because those queries are the common ones and the alternative
161
- loses them silently. <!-- rule:okf-search-exact-identifiers -->
162
-
163
- **`--engine index` is the other engine, and the one to reach for when ranking
164
- matters more than exactness.** The engine is normally chosen by what the query
165
- needs — `--fuzzy` routes to the index, anything else stays on the default scan —
166
- and nothing is printed about the choice. `--engine NAME` overrides that for the
167
- case the flags cannot express: a matching *model* requires no capability, so no
168
- flag selects one. Under the index, terms match whole tokens and their prefixes
169
- (`dedup` finds `deduplication`), rows rank by BM25+, and it is the engine the
170
- browser page runs — so name it when reconciling a CLI answer with the page. The
171
- cost is real: its tokenizer splits on punctuation, so identifiers shatter
172
- (`customer_id` → `customer` + `id`), an infix finds nothing, and a backtick is
173
- never split off at all, so a word inside a code span is unfindable — a large
174
- silent loss, since technical prose is full of them. **Do not count on ranking to
175
- rescue it** — BM25 normalizes by field length, so a short concept dense in `7`,
176
- `2` and `0` can outrank the one that actually says `7.2.0`. Naming an engine that
177
- cannot do what you also asked (`--engine index -e`) is a usage error naming one
178
- that can. <!-- rule:okf-search-engine-choice -->
179
-
180
- **The capabilities, and which engine has them.** An engine is selected by what
181
- the query *requires*; only a matching model has to be named, because requiring
182
- nothing is not something a flag can express:
183
-
184
- | Flag | Capability | Engine | What it does |
185
- |---|---|---|---|
186
- | *(none)* | — | scan | literal substring over raw text; scores by summed field weight |
187
- | `-e` / `--regexp` | `regexp` | scan | each term is a Ruby regexp, case-insensitive; invalid → exit 2 |
188
- | `--fuzzy` | `fuzzy` | **index** | edit distance 0.2 × term length — and switches engine |
189
- | `--engine index` | — | index | whole-token + prefix matching, BM25+ ranking, browser parity |
190
- | `--engine scan` | — | scan | the default, spelled out |
191
-
192
- Two consequences worth holding. **`--fuzzy` is an engine switch, not a mode**: it
193
- carries the whole index with it, so a run that wanted one typo forgiven also gets
194
- shattered identifiers and unfindable code spans — fix the spelling and stay on
195
- the default when you can. And **`-e` moves nothing** now, because the default
196
- engine already offers `regexp`; it changes how a term is *read* (pattern rather
197
- than literal), not where it is matched. <!-- rule:okf-search-fuzzy-is-a-switch -->
198
-
199
- `prefix` is a capability the index declares but no flag selects — it is always on
200
- there. **It is not a reason to reach for the index**: a substring match already
201
- covers every prefix and then some, so `dedup` finds `deduplication` under both
202
- engines, while `duplication` and `uplicat` find it under the default only. Prefix
203
- is what the index needs to catch up to raw text, not a capability it adds on top.
204
- The index's real advantages over the default are exactly three — relevance
205
- ranking, typo tolerance, and page parity.
206
-
207
- **Search spans bundles.** Leading @refs pick several registered bundles
208
- (`okf search @handbook @notes auth`); **`@all`** is the ref that means every one.
209
- Rows from different bundles are ranked together and comparable, and each row
210
- carries its bundle's slug. Under `--engine index` the bundles go into **one
211
- corpus** — BM25 prices a term by how rare it is, so separately-ranked lists would
212
- not compare — which makes a score relative to the whole answer: the same concept
213
- scores lower searched beside others than searched alone. The default scan needs
214
- no such trick — its score is absolute, so a row is worth the same either way.
215
- This is the
216
- cross-bundle retrieval the in-page search does not have: one question, every
217
- bundle you keep. <!-- rule:okf-search-all -->
218
-
219
- `@all` is a ref, not a flag, which is what keeps the grammar single: slot 1 is
220
- always a bundle identity, so a directory there is a directory and nothing can
221
- flip it into a term. Being a ref, it is normalized like one — `@ALL` and `@All`
222
- name every bundle just as `@One` names the bundle registered from dir `One`. It composes accordingly — `@all @docs` expands and dedupes
223
- (all ⊇ docs), needing no diagnostic. **Asking for everything tolerates gaps;
224
- naming one bundle demands it**: `@all` skips a bundle whose directory has
225
- vanished with a note on stderr, while `@docs` fails hard. `@all` is only
226
- `search`'s: every other verb answers about one bundle, so it refuses `@all` by
227
- name rather than letting the answer depend on how many bundles you happen to
228
- have registered. `all` is reserved as a slug — a directory named `all/` registers
229
- as `all-2`, `--as all` is refused, and an `all` row already in the registry file
230
- (hand-typed, or written before the name was reserved) is read as `all-2` rather
231
- than taken as grounds to reject the file — so `@all` is never ambiguous, and the
232
- reservation never strands a registry it inherited. **The read normalizes every
233
- slug** the same way registration would, so a hand-typed `"slug": "My Docs"` lists
234
- and resolves as `my-docs`; an entry the listing shows is always an entry `@slug`,
235
- `rename`, and `default` can name.
236
-
237
- `--fields` projects the shape the mode actually emits: `slug` is available in
238
- registry mode, and a usage error naming the real fields on a path-named search,
239
- which has no slug to give. Two sharp edges: every *leading* @-arg is taken as a ref, so a literal @-term
240
- (`@babel/core`, a Ruby `@ivar`) needs a non-@ term before it or `-e '\@term'` —
241
- the CLI notes both traps on stderr — and any ref, even one, switches the JSON
242
- envelope (next paragraph).
243
-
244
- Rows rank by where they hit — title 5, id 4, tags 3, type/description 2, body 1 —
245
- summed as an absolute score by the default scan, and carried as per-field boost
246
- into **BM25+** under `--engine index`. Each row carries one bounded context
247
- snippet from the strongest match that needs context (description or body). Every row still names the fields that hit (`matched`), so a result stays
248
- citable rather than being a bare relevance number. Exact by default: the
249
- consuming agent is the fuzzy layer — when terms miss, learn the bundle's
250
- vocabulary from `tags`/`types` and re-ask in its own words, rather than
251
- hammering synonyms or reaching for `--fuzzy` before you have looked. Advisory read: **exit 0 even with zero matches**.
252
- JSON, plain-dir mode: `{ bundle, query, count, matches: [{ id, title, type,
253
- dir, top_dir, tags, matched, score, snippet }] }`. Registry mode — any leading @ref,
254
- `@all` or a `@group` among them (a group fans out to its member bundles) — swaps the envelope: `{ bundles: [{ slug, dir }, …],
255
- query, count, matches: [{ slug, id, … }] }`; a parser must branch on which form
256
- it called. The head maps each slug to its dir once, so a row resolves to
257
- `<dir>/<id>.md` without a second lookup and without repeating a path per row.
258
- Both are projectable with `--fields/--except`, and projection is literal — when
259
- merging bundles, put `slug` in your `--fields` list or the row label drops and
260
- same-id concepts from different bundles become indistinguishable. The retrieval procedure that puts this verb in sequence —
261
- map first, finder second, bodies last — is the
262
- [search playbook](../playbooks/search.md).
263
-
264
- ## index — the progressive-disclosure map (§6)
265
-
266
- The "orient before you read" view, and the read verb that sees the layer the
267
- concept views can't: `index.md` files are reserved/structural, so
268
- `catalog`/`files`/… never show them (in the browser, the Indexes tab and
269
- folder clicks render this same map). `okf index <dir>` prints one entry per directory
270
- that holds concepts or carries an `index.md`, root first — the authored index body
271
- (frontmatter stripped), a `type`/`tag` rollup over the concepts that live directly
272
- there, its child directories, and the concept listing. Run it first when picking up
273
- an existing bundle: it is the cheapest high-signal orientation, and it surfaces
274
- enumeration drift a grep can't (you can't grep for a listing entry that is *missing*).
275
-
276
- `--dir PATH` narrows to a directory **and everything below it**, and is
277
- **repeatable** — `--dir model --dir format` shows both; `root` (or `.`) names the
278
- bundle root, unless the bundle really has a `root/` directory, which owns the
279
- word. A `--dir` also brings the **chain from the root down to it**, so a branch is
280
- never shown adrift of the authored context that says what it is — the root
281
- `index.md`'s prose first among it. Those rows print with a leading `↑` and carry
282
- `ancestor: true`; `--no-ancestors` drops them. Ascent and descent are separate
283
- axes, so `--depth` never bounds the chain: `--dir X --depth 0` is X alone, plus
284
- how you get to X. A `--dir` that names nothing gains no chain — a lone root row
285
- would read as a partial answer to a query that matched nothing.
286
-
287
- `--depth N` bounds how far below the starting point the map reaches
288
- (the `--dir` when one is given, else the bundle root), counted **relatively**:
289
- `--depth 1` is the top of the tree, `--dir X --depth 1` is one branch of it, and
290
- the pair walks down a level at a time.
291
-
292
- **On a bundle of any size the map is unreadable whole** — every directory is a
293
- section, and even `--no-body` keeps one listing row per *concept* — so narrow
294
- rather than paging it: `okf dirs` is the orientation, `--dir <branch> --depth 1`
295
- is the step down into it,
296
- and `--except body,listing` on top of either is the lean JSON skeleton. Full
297
- `index` output on a few hundred concepts runs to hundreds of KB; the same map at
298
- `--depth 1` is a couple of KB.
299
-
300
- `--no-body` drops the prose to a
301
- skeleton (headers, rollups, child pointers). For a directory that has concepts but
302
- **no `index.md`**, the listing is **synthesized** from the concepts' descriptions
303
- and tagged `(no index.md)` — §6 explicitly permits synthesizing a map on the fly.
304
-
305
- It is a **read view**: advisory, always exit 0. A synthesized directory is a
306
- *signal* (a map worth writing), never a defect — `index` emits no lint findings and
307
- never fails a bundle. JSON: `{ bundle, count, directories: [{ dir, index_path,
308
- present, synthesized, count, types, tags, subdirs, body, listing: [{ id, title,
309
- description, type, tags }] }] }` — `ancestor` marks a row that is there to place
310
- the branch rather than to answer about it.
311
-
312
- ## dirs — the bundle's clusters and their sizes
313
-
314
- `okf dirs <dir>` lists every directory the bundle has — the ones holding
315
- concepts, the ones carrying an `index.md`, and the empty intermediates that only
316
- exist to connect the tree — with the number of concepts living **directly** in
317
- each and the number in its **subtree**. A cluster *is* a directory here, so this
318
- is the view that tells you what `--dir` can be pointed at and how much sits
319
- behind each choice.
320
-
321
- Two numbers, because one cannot answer the question. `count` is direct, so the
322
- column sums to the bundle's concept total and a dir holding only sub-directories
323
- reads `0` rather than a hidden rollup. `subtree` is defined as *exactly what
324
- `--dir <that row>` returns*, so the row and the flag can never disagree — which
325
- is also why the root's subtree is its own direct count (`.` is a prefix of
326
- nothing). Without it a truncated listing is all zeroes at the top of a deep tree,
327
- which is where you most need to know where the mass is. The human table shows the
328
- second column only where some dir actually nests.
329
-
330
- `--dir PATH` (repeatable) narrows to a directory and its subtree, and brings the
331
- **chain up to the root** with it so the branch is placed rather than shown
332
- adrift — those rows are marked `↑`, carry `ancestor: true`, and stay out of
333
- `total` (`--no-ancestors` drops them). `--depth N` keeps only N levels below the
334
- starting point — the `--dir` when one is given,
335
- the bundle root otherwise. Relative, not absolute, so `--dir a/b --depth 1`
336
- reads "a/b and one level under it" without your first working out how deep `a/b`
337
- is. `--depth 0` is the starting point alone. A `--depth` that is not a whole
338
- number is a usage error (exit 2).
339
-
340
- **This is the first command to run on a bundle you do not know** — the same first
341
- move [SKILL.md](../SKILL.md) prescribes. `okf dirs <dir>` is one row per
342
- directory, so its size tracks the tree rather than the concept count: it tells
343
- you the shape and where the weight sits, `--depth 1` trims it further on a deep
344
- bundle, and you then descend with `okf index --dir`, one level at a time.
345
-
346
- The root prints `(root)` and stores `.` — the split every grouped view keeps, so
347
- a table and its `--json` never disagree about which spelling is the data. JSON:
348
- `{ bundle, total, count, dirs: [{ dir, ancestor, count, subtree, subdirs }] }`,
349
- root first. `count` is rows printed, chain included; `total` sums the direct
350
- counts of the rows you actually asked for, which is what keeps a row's `subtree`
351
- equal to the `total` that `--dir` on that row returns.
352
-
353
- ## catalog / files / tags / types / stats — the server views, as text
354
-
355
- The browser server (below) has Catalog, Files, Tags and Stats panels; these
356
- verbs reproduce them on the CLI so an agent can read a bundle without a browser.
357
- All are advisory reads (exit 0) sharing one data source (per-concept metadata plus
358
- in/out link degree). Add `--json` to any for a machine substrate.
359
-
360
- - **`catalog`** — every concept with its metadata (type, status, tags, timestamp,
361
- in/out link degree, description), grouped by top-level dir (`dir` on every row
362
- carries the full path, `top_dir` the first segment). The "what's here, in
363
- detail" view. JSON: `{ bundle, count, concepts: [{ id, title, type, description,
364
- tags, timestamp, status, backlog_ref, dir, top_dir, links_out, links_in }] }`.
365
- - **`files`** — the folder tree: each concept's filename + title, grouped by
366
- directory. The "how it's organised" view. JSON: `{ bundle, count, files: [{ path,
367
- id, dir, type, title, description }] }`.
368
- - **`tags`** — every tag with the concepts that carry it, ordered by count
369
- descending. The "what themes dominate" view. JSON: `{ bundle, count, tags: [{ tag,
370
- count, concepts: [id, …] }] }`. `--by type|dir` regroups the list per concept
371
- dimension with **within-group** counts (a tag spanning groups appears in each);
372
- each row also carries the tag's **total** across the narrowed set, printed
373
- `count/total` when they differ — so a tag's locality reads per row (a plain
374
- count = wholly local; `2/7` = a cross-cutting spread). The substrate for tag
375
- curation and for [refine](../playbooks/refine.md)'s domain-vs-concern read;
376
- the judgment recipes live in the [maintain playbook](../playbooks/maintain.md)
377
- and the [refine playbook](../playbooks/refine.md). JSON: `{ bundle, count, by,
378
- groups: [{ <dim>, count, tags: [{ tag, count, total, concepts }] }] }`.
379
- - **`types`** — every type with the concepts that carry it, ordered by count
380
- descending. The "what kinds of knowledge" view. JSON: `{ bundle, count, types:
381
- [{ type, count, concepts: [id, …] }] }`.
382
- - **`stats`** — bundle rollups: concept / dir / type / cross-link / distinct-tag
383
- totals plus per-type and per-dir breakdowns. The "shape at a glance" view. JSON:
384
- `{ bundle, concepts, dirs, top_dirs, concept_types, cross_links, distinct_tags,
385
- by_type, by_dir, by_top_dir }` (`top_dirs`/`by_top_dir` are the first-segment
386
- rollup). `dirs`/`by_dir` cover every directory `okf dirs`
387
- lists — counts are direct, so a directory holding nothing itself is present at
388
- `0` rather than missing, and `by_dir.keys` is a complete list of what `--dir`
389
- can address.
390
-
391
- The four list views narrow with the same filters the browser panels offer —
392
- `--type TYPE`, `--dir PATH`, `--tag TAG`; each takes the ones orthogonal to
393
- itself (`tags` can't filter by tag). Matching is case-insensitive; `--type` and
103
+ <!-- rule:okf-one-bundle-per-verb -->
104
+
105
+ ## The shared filters — `--type` `--dir` `--tag` `--status` `--trust`
106
+
107
+ The four list views of [cli/views.md](cli/views.md) narrow with the same
108
+ filters the browser panels offer
109
+ `--type TYPE`, `--dir PATH`, `--tag TAG`, `--status STATUS`, `--trust TIER`
110
+ (`search` takes them too); each takes the ones orthogonal to itself (`tags`
111
+ can't filter by tag). `--status` matches the *effective* status (absent reads
112
+ `stable`, §5.4) and `--trust` the derived tier, either spelling
113
+ (`machine-confirmed` or `machine_confirmed`) — on a v0.1 bundle
114
+ `--status stable` and `--trust unverified` match everything, which is §13.1
115
+ reading, not an error, and an unknown value matches nothing at exit 0.
116
+ Matching is case-insensitive; `--type` and
394
117
  `--tag` are exact, `--dir` takes the named directory **and everything below it**
395
118
  (`--dir platform` reaches `platform/services/api`). A concept at the bundle root
396
119
  lives in `.`, which `--dir` also accepts as plain `root` (no shell quoting) —
@@ -407,183 +130,3 @@ for `tags --by area`. Both go in a later release; write `--dir` in anything new.
407
130
  On `index` it combines with neither `--depth` nor `--dir` — exit 2, because it is
408
131
  *exact* and both of those select a range, so the pair used to return the area
409
132
  plus whatever the other flag selected: an answer to neither question.
410
-
411
- Reach for `stats` first to size a bundle, `catalog`/`files` to enumerate it, `tags`
412
- to find thematic clusters — all without standing up the server.
413
-
414
- ## server — interactive graph server
415
-
416
- Starts a local HTTP server (`okf server <dir>`; `-p`/`--port`, default 8808, and
417
- `--bind`) and prints its URL — stop it with Ctrl-C. The page boots from a lean
418
- payload (nodes carry only `id` and `title`, plus compact type/tag indexes) and
419
- fetches each concept's markdown body **live from disk** as you click it, so the
420
- initial load stays small and edits show without a restart. Mermaid code blocks
421
- in a body render as diagrams, and a click (or tap) opens the diagram full
422
- screen with drag-to-pan and wheel/pinch zoom. Concepts render as nodes
423
- coloured by `type` and sized by degree, links as edges, with a detail panel
424
- (rendered markdown, "Links to" / "Linked from" backlinks), layout switching,
425
- type/dir/tag filters on every view (the dir chips take a directory *and* its
426
- subtree, the same rule `--dir` uses), and search. Cluster mode groups the
427
- concepts into one box per directory, nested to a depth picked beside the layout
428
- select — depth 1 is the flat view, and a flat bundle is offered no control. The authored layer is in the
429
- UI too: the Files view carries **Files | Indexes** tabs — the Indexes tab
430
- lists the log first (the chronological index), then every `index.md` — and
431
- folder nodes in file-tree mode and directory boxes in cluster mode open a
432
- directory's §6 map in the inspector (authored, or synthesized when none
433
- exists). Links to an `index.md`, `log.md`, or bare directory navigate instead
434
- of dead-ending, and the log is fetched fresh on every read, so a
435
- just-appended entry shows without a restart. `?view=index` jumps straight to
436
- the Indexes tab. It is a Rack app, so the same server can be mounted in a
437
- host app (e.g. Rails).
438
-
439
- **Hosting many bundles (the hub).** `okf server` takes zero or more dirs.
440
- One dir is the classic single bundle at `/`. Two or more mounts each under
441
- `/b/<slug>/` behind a hub, `/` redirects to the default, and `/b/` is a
442
- self-contained **bundle index** (every hosted bundle, concept counts, default
443
- marked — the browser counterpart of `okf registry`). An unknown slug 404s as a
444
- page listing the hosted bundles, so a stale bookmark after a rename gets a way
445
- home. With **no** dir it serves the *persistent registry*, a plain JSON file
446
- under `$OKF_HOME` (default `~/.okf`), managed by the
447
- `okf registry` umbrella — like git's `remote` family, and split by what each
448
- verb keys on.
449
- **`okf registry init`** creates a *project-local* registry instead: a
450
- `.okf-registry.json` in the current directory, which okf discovers by walking up
451
- from the working directory and uses in place of the global one while you are
452
- inside its tree (the nearest wins, so nested registries resolve nearest-first).
453
- Every registry op — and every `@slug` — then resolves through it, so a bare
454
- `okf server` inside a repo serves that repo's bundles with no `$OKF_HOME` setup;
455
- `okf registry list` names the local file it found. `OKF_NO_DISCOVERY=1` forces
456
- the global registry — the escape hatch for a fixed-cwd caller (CI, a tool). A
457
- local registry stores **portable** paths: a bundle inside its tree is written
458
- relative to the `.okf-registry.json`, so committing the file lets it travel with
459
- the repo (a checkout elsewhere, a container mounting it) and resolve unchanged;
460
- a bundle outside the tree stays absolute, since it cannot travel. Paths still read
461
- back absolute wherever the CLI reports them.
462
- **Entry verbs** take a path: `okf registry set <dir>` adds it
463
- (slug from the basename, or `--as`, which errors on a collision; `--default`
464
- puts it first), and because the entry is keyed by path, `set` on an
465
- already-registered dir updates it in place — refreshing its title, and renaming
466
- it when `--as` is given. `okf registry del <dir|@slug>` removes a bundle *or* a group — by name, so an
467
- entry whose directory is already gone still deletes, and removing a bundle
468
- **cascade-drops** it from every group that named it (a group emptied that way is
469
- deleted). Slug *or* dir, never both readings at
470
- once: an argument with a `/` in it names a location and only a location, so
471
- `del ./notes` refuses when no entry points there rather than stripping to the
472
- slug `notes` and deleting a bundle somewhere else entirely.
473
- <!-- rule:okf-registry-del-path-or-slug -->
474
- **Slug verbs** take the name — bare, or as an `@slug`: `okf registry default <@slug>`
475
- chooses which bundle `/` opens **by moving that entry to the front** (a group is
476
- refused — the default is one bundle), and
477
- `okf registry rename <@slug> <new>` renames a bundle *or* group slug (mount path
478
- and switcher name), **cascading** the new name into every group's member list —
479
- `<new>` is a name being minted, so it is never a ref. The registry is ordered and **the first entry still on disk is the
480
- default** — that is the whole rule, so the first bundle you register is the
481
- default until you move another one, a rename keeps its position, and a `del`
482
- promotes whatever is next. A vanished directory is stepped over (the server
483
- cannot open one, so starring it would name a bundle `/` never serves), and
484
- `registry default @slug` refuses one outright — the same refusal `registry set`
485
- gives a directory that is not there. The file is hand-editable and reorders
486
- visibly, which is the point: there is no stored slug that can dangle.
487
- <!-- rule:okf-registry-default-position -->
488
- **Group verbs** name a *set* of bundles under one slug — a durable subset for the
489
- two verbs that take several bundles. `okf registry group <slug> <@member…>`
490
- creates a group, or adds members to one (a union); members are bundle *or* group
491
- slugs, so groups nest. A group shares the slug namespace with bundles (a slug
492
- names one *or* the other, never both), `all` stays reserved, and a member set that
493
- would make the group reach itself is refused. `okf registry ungroup <slug>
494
- <@member…>` removes members; emptying a group deletes it. A group resolves,
495
- recursively and path-deduped, to its bundle leaves — which **only `okf search`
496
- and `okf server` consume**: every single-bundle verb (`lint`, `index`, …) refuses
497
- a `@group` with exit 2, the same rule that refuses a second bundle. `@all` is
498
- unchanged — it still names every registered *bundle*, groups being named subsets
499
- of that.
500
- `okf registry list` (or a bare
501
- `okf registry`) stars the default and flags vanished dirs `(missing)` — the
502
- server skips those with a note — and lists any groups with their members and
503
- resolved leaf count; `--json` answers
504
- `{ registry: <file>, count, bundles: [{ slug, title, dir, mount, default,
505
- missing }], groups: [{ slug, members, resolved }] }`, naming the file it read so a
506
- `$OKF_HOME` mismatch is visible. The hub roster is a
507
- **boot-time snapshot**: restart `okf server` after registry changes. Behind a
508
- hub the page gains a **bundle switcher** (⌘/Ctrl-K, or the rail button with its
509
- bundle-count badge): the current bundle is pinned, the default chipped; ⏎
510
- opens, ⌘/Ctrl-⏎ opens a new tab, and the current view carries over. Switching
511
- is a server-only affordance — a static `render` file has no siblings and shows
512
- none.
513
-
514
- **Bundle-less run.** Register bundles once, then `okf server` (no dir) hosts
515
- them all with the registry's first entry still on disk at `/` — the way to keep
516
- several bundles a keystroke apart without re-passing paths.
517
- `okf server @a @b` serves a registry subset, each mounted under its registered
518
- slug — but as with any dirs-given run, the *first argument* lands at `/`; the
519
- registry's own order applies only to the bundle-less run. A `@group` argument
520
- fans out to its member bundles in the same way (`okf server @backend`), its first
521
- member landing at `/`; `okf search @group <term…>` merges the group's members
522
- into one ranking, exactly as naming them individually would.
523
-
524
- **Trust boundary:** the page renders each fetched markdown body through
525
- DOMPurify and escapes everything it inlines (every `<` in the graph data is
526
- escaped, so it cannot break out of its `<script>`), but it still loads its
527
- viewer libraries (Cytoscape, marked, DOMPurify — plus Mermaid and Panzoom,
528
- lazy-loaded on first use) from a CDN and renders whatever
529
- links the bundle carries — so only serve bundles you trust.
530
-
531
- ## render — static graph export
532
-
533
- Writes the same interactive page as one static, self-contained HTML file
534
- (`okf render <dir>`), so the graph hosts where there is no server — GitHub Pages,
535
- an object store, an attachment. Prints to stdout (`okf render <dir> > graph.html`)
536
- or writes `-o FILE`; `--title`/`--link`/`--layout` mirror `server`. It is the same
537
- template `server` renders, one switch apart: rather than fetching each body,
538
- description, catalog, index, and log live, `render` bakes the whole bundle into
539
- the page and the browser reads from that embedded payload — no server, no build
540
- step. The trade-off is weight (every body is inlined), so `server` stays the
541
- choice for a bundle too large to ship whole.
542
-
543
- **Trust boundary:** the same two guards as `server` — every inlined body is
544
- `</script>`-escaped like the graph data and still sanitized by DOMPurify when
545
- rendered — so a static file is no laxer than the live server. Only render bundles
546
- you trust.
547
-
548
- ## graph — the raw structure
549
-
550
- Prints the node/edge graph. `--json` emits a machine-readable dump — the
551
- `bundle`/`slug` head every view carries, then `nodes` (with
552
- `id`/`type`/`title`/`description`/`tags` **and, by default, every `body`** — the
553
- part that dominates the bytes on a real bundle) plus `edges` — you can pipe into
554
- other analysis. A concept with a missing *or blank* `type` indexes under
555
- `Untyped`: §9.2 rejects both identically, so both land in one bucket. To *plan* a traversal, structure is all you need: `--no-body`
556
- drops each node's body, and `--minimal` ships only `id`/`title` plus the type/tag
557
- indexes — the lean shape the `server` page boots from. Reach for the full dump
558
- only when the task truly consumes every body; for one question, the
559
- [search verb](#search--ranked-text-retrieval-metadata--body) is orders cheaper.
560
-
561
- `--hubs` swaps the dump for the **inbound ranking**: every concept with at
562
- least one inbound link, ranked by inbound degree, each with its links grouped
563
- by *source top-level dir* (`core/status ×3 flows 2, billing 1`) — the evidence for
564
- [refine](../playbooks/refine.md)'s hub origin test ("is this hub well-homed?").
565
- A source at the bundle root counts under `(root)`. JSON: `{ bundle, count,
566
- hubs: [{ id, top_dir, inbound, by_top_dir: { <top_dir>: n } }] }`. Advisory read, exit 0;
567
- `--minimal`/`--no-body` shape node payloads and change nothing here.
568
-
569
- `--traffic` asks the same question one grain coarser: **directories**, not
570
- concepts. Every concept collapses into the directory it lives in and every link
571
- between two directories collapses into one weighted arc, so a bundle's wiring
572
- becomes a table you can read — measured on one 47-concept bundle, 227 links
573
- collapsed into 50 arcs, of which the fitted cut draws 22. Each row carries the
574
- directory's traffic split three ways
575
- (`internal` / `out` / `in`) plus **cohesion**, its internal share of the total:
576
- the evidence for [refine](../playbooks/refine.md)'s container test, where
577
- `--hubs` only ever answered about concepts. Rows lead with the lowest cohesion,
578
- so the directories with a case to answer come first, and a directory with no
579
- traffic at all prints `—` rather than a `0%` it did not earn.
580
-
581
- `--cut N` is the least arc weight drawn. It defaults to a value **fitted to the
582
- bundle** — enough arcs for roughly 1.5 per directory, floored at 8 — because a
583
- fixed weight cannot serve both ends: measured at weight 3 across ten bundles it
584
- left 2 arcs on one and 136 on another. The JSON says which you got. Cohesion is
585
- computed over *every* arc and never the drawn ones, so tightening the cut
586
- changes the picture and never the evidence. JSON: `{ bundle, cut, fitted, dirs:
587
- [{ dir, parent, count, subtree, internal, out, in, cohesion }], arcs: [{ source,
588
- target, weight }], total_arcs }` — a fraction of the full dump (2.6 KB against
589
- 27 KB on that bundle), and the shape rather than the contents. Advisory, exit 0.
@@ -0,0 +1,32 @@
1
+ # What each SPEC section governs
2
+
3
+ Kind: index. Answers: which § of the vendored spec settles a question, so you
4
+ consult one section instead of re-reading all of [SPEC.md](SPEC.md).
5
+
6
+ The spec is vendored **verbatim** from upstream — its header names the commit
7
+ and the licence — so it is never edited here and never split; this file is its
8
+ map, kept beside it. The § numbers are the stable keys: cite `§6.2`, never a
9
+ heading or a line number. Every section is here, including the two an authoring
10
+ question rarely reaches: an index that quietly drops what it judges uninteresting
11
+ misroutes the reader who wanted exactly that. <!-- rule:okf-spec-map -->
12
+
13
+ | § | Governs | Reach for it when |
14
+ |---|---------|-------------------|
15
+ | §1 | motivation, goals, and the **non-goals** | arguing why OKF at all; checking a proposal against what the format refuses to be |
16
+ | §2 | terminology — bundle, concept, concept ID, frontmatter, body, link, source, provenance, credibility signal, actor, trust tier, attested computation, executor, receipt, attester | a word is load-bearing in a disagreement, or a field name reads two ways |
17
+ | §3 | bundle structure, reserved filenames | laying out directories |
18
+ | §4 | concept documents & frontmatter | writing or validating a concept |
19
+ | §5.1 | provenance — `sources` and its credibility signals | any external or empirical claim |
20
+ | §5.2 / §5.3 | trust — `generated`, `verified`, and the derived tiers | recording who wrote or confirmed content |
21
+ | §5.4 / §5.5 | lifecycle — `status`, `stale_after` | marking drafts, deprecations, expiries |
22
+ | §6 / §6.1 | cross-links; **broken links are tolerated** | linking; judging a "broken" link |
23
+ | §6.2 / §6.3 | path-valued fields; the `references/` convention | pointing at non-concept assets |
24
+ | §7 | the actor convention | filling any `by` |
25
+ | §8 | index files & progressive disclosure | orienting; writing or synthesizing an index |
26
+ | §9 | log files | recording history |
27
+ | §10 | attested computations | a concept that *is* a sanctioned computation |
28
+ | §11 | conformance — the hard gate | what `validate` may and may not reject |
29
+ | §12 | versioning (`okf_version`) | the root index's one allowed field |
30
+ | §13 | changes from v0.1 | migrating a bundle; reading an unmigrated one |
31
+ | Appendix A | one worked bundle exercising every family, as a v0.1 → v0.2 migration | you want the whole shape at once rather than a rule at a time |
32
+