okf 2.0.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.
@@ -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
@@ -73,407 +100,12 @@ 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.
103
+ <!-- rule:okf-one-bundle-per-verb -->
76
104
 
77
- ## validatethe hard gate (§11)
105
+ ## The shared filters `--type` `--dir` `--tag` `--status` `--trust`
78
106
 
79
- Implements the spec's §11 conformance definition exactly:
80
-
81
- - **§11 cond. 1** every non-reserved file has a parseable YAML frontmatter block;
82
- - **§11 cond. 2** every such block has a non-empty `type`;
83
- - **§11 cond. 3** any `index.md`/`log.md` present follows §8/§9 (a nested
84
- `index.md` has no frontmatter, a root `index.md` carries only `okf_version`,
85
- `log.md` date 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, **broken cross-links, which §6.1 explicitly tolerates**,
90
- the shape of every §5/§10 family (`generated` not a mapping, a non-integer
91
- `usage_count`, a `stale_after` that is not `YYYY-MM-DD`, a missing `runtime` on
92
- an Attested Computation, …), and an `okf_version` the gem does not know (read
93
- best-effort under §12; an absent one never warns). Absence of an optional family
94
- is never a fault — a pure v0.1 bundle validates with zero warnings.
95
-
96
- In `--json`, every warning carries `check` (a stable id) and `source` — `spec`
97
- when the SPEC's own words state the rule, `convention` for a shape this gem asks
98
- for beyond them (`verified[].by` presence, integer `usage_count`, a per-entry
99
- `usage_window` mapping, `parameters[].name`, `executor`/`attester` `resource`).
100
- Gate on `source` when you want only the spec-normative set; errors keep their
101
- two-key `{ path, message }` shape. Fix warnings when cheap; never block on them.
102
- Use `--json` in CI.
103
-
104
- ## lint — curation quality (advisory)
105
-
106
- Asks the complementary question to `validate`: not "is this legal OKF?" but "is
107
- this well-curated, navigable, trustworthy?" — precisely over the things §11
108
- forbids `validate` from rejecting. It has its own report, never emits
109
- conformance errors, and **exits `0` even with findings** unless you pass
110
- `--fail-on warn` (exit 1 on any `warn` finding) or `--fail-on info` (exit 1 on
111
- any finding at all).
112
-
113
- **Severity is API.** Every check has a pinned level — `warn` or `info` — and
114
- machine consumers gate on it, so the levels below are stable, not advisory. A
115
- finding you want to gate on that is `info` gets `--fail-on info` (usually with
116
- `--only`), never a hope that its severity changes.
117
-
118
- Eight categories, each backed by individual checks (severity in brackets):
119
-
120
- - **Reachability** — `orphan` [warn], `not_in_index` [warn],
121
- `disconnected_component` [info], `unlinked` [info]
122
- - **Backlog** — `missing_concept` [info], `broken_index_entry` [warn]
123
- - **Completeness** — `stub` [info], `missing_title` [info],
124
- `missing_description` [info], `missing_generated` [info] (quiet on either
125
- spelling — a legacy `timestamp` still counts as a recorded change)
126
- - **Freshness** — `expired` [info] (§5.5: past the concept's own declared
127
- `stale_after`, on the day itself), `stale` [warn] (older than the
128
- reader-supplied `--stale-after` cutoff, keyed on `generated_at`)
129
- - **Provenance** — `uncited_external` [info] (external body links and no
130
- sources, in either spelling), `broken_source` [warn] (an in-bundle `.md`
131
- source target that names no concept; URLs and scope descriptors are out of
132
- scope, and a non-`.md` asset is out of reach — the reader models concepts,
133
- so lint never sees the file; `okf references` is the view that checks those
134
- pointers), `unattributed_claim` [warn] (a footnote
135
- no `sources[].id` answers — it *misattributes* a claim, which is why it
136
- outranks its join-twin), `unused_source` [info] (a keyed source no footnote
137
- cites — slack, not a defect), `unprefixed_actor` [info] (a `verified[].by`
138
- outside §7's three forms reads as machine-confirmed; a `generated.by`
139
- outside them feeds no tier but leaves a reader unable to tell a person
140
- from a process; info so it informs, never blocks). A missing `generated.by` is the *validator's* warning —
141
- REQUIRED-within is shape, not curation — so lint never double-reports it
142
- - **Attestation** — `incomplete_computation` [warn] (an Attested Computation
143
- providing its computation neither way, or both ways — §10.3 says a
144
- `computation:` path is used *instead of* the body fence),
145
- `broken_attestation_ref` [warn] (on an `Attested Computation`, a
146
- `computation`, `executor.resource` or `attester.resource` naming an
147
- in-bundle `.md` that is not there — a contract no consumer can follow; the
148
- keys are read only on that type, since §4.1 lets any other concept use them
149
- for its own purpose). Its reach is exactly the `.md` files: URLs are out
150
- of scope, and a `.sql` or `.py` target is invisible to *every* check here,
151
- because the linter reads the concept model and the model carries only
152
- markdown — `okf references` is the surface that sees those files and reports
153
- a pointer that misses, whatever the extension. Remember §6.2 reads a bare
154
- `references/…` as relative to the concept, so from a nested concept it wants
155
- the leading `/`
156
- - **Migration** — `legacy_timestamp` [info], `legacy_citations` [info]: one
157
- finding per bundle naming the files still in a retired v0.1 spelling, with
158
- the rewrite instructions in the message. Info on purpose — §13 says a v0.1
159
- bundle is consumable forever, so `--fail-on warn` must not turn red on one.
160
- A migration campaign gates explicitly:
161
- `okf lint <dir> --only legacy_timestamp,legacy_citations --fail-on info`,
162
- exit 1 until clean.
163
- - **Hygiene** — `duplicate_title` [info], `unused_reference_def` [info],
164
- `undefined_reference` [warn], `self_link` [info], `log_order` [info] (§9
165
- reads a log newest-first; disorder is slack, never a §11 error)
166
-
167
- `--only` / `--except` filter by the **individual check names above**, not the
168
- category labels — `okf lint <dir> --only orphan,stub` works; `--only reachability`
169
- is an error. Two knobs tune specific checks: `--min-body N` sets the `stub` body
170
- threshold in characters (default 50), and `--stale-after DUR` sets the `stale`
171
- cutoff — a duration like `90d` or `12w`, or an ISO date like `2026-01-01` (a bare
172
- number is rejected).
173
-
174
- **Two different clocks, one unlucky name.** The `--stale-after` *flag* and the
175
- `stale_after:` *frontmatter field* are different mechanisms that happen to share
176
- a spelling. The flag is the **reader's** age cutoff: "flag anything not touched
177
- since DUR", keyed on `generated_at`, feeding the `stale` check. The field is the
178
- **author's** declared expiry: "do not trust this past DATE", feeding the
179
- `expired` check. Never read one as the other, and never show them adjacent
180
- without the distinction.
181
-
182
- **The clock is explicit.** `expired` compares against a day the CLI supplies —
183
- today by default, or `--today YYYY-MM-DD` for a reproducible report (CI wants
184
- this). The pure library runs no clock check unless handed `today:`, and every
185
- clock-gated check that was selected but could not run is *named* in
186
- `stats.skipped_checks` (the human report prints one `skipped:` line) — a gate
187
- that is sometimes absent and does not confess converts "unchecked" into
188
- "checked and fine".
189
-
190
- The report's stats carry the bundle's posture too: `trust` (the §5.3 tier
191
- distribution, in the hyphenated wire spelling) and `status` (effective-status
192
- frequency).
193
-
194
- `lint --json` is the structured substrate you consume to reason about the two
195
- things lint deliberately does **not** compute — contradictions and *semantic*
196
- staleness — which need understanding of meaning.
197
-
198
- ## loose — files with no graph connections (by folder)
199
-
200
- Lists the **loose** files — concepts with graph **degree 0**: no cross-links in
201
- *or* out — grouped by folder. It is a focused, folder-organized view over `lint`'s
202
- `unlinked` check (`okf loose <dir>` ≈ `okf lint <dir> --only unlinked`, regrouped),
203
- for the "which files float in the graph?" question. Advisory: **exits `0`**; `--json`
204
- emits `{ bundle, count, loose: [{ id, title, dir }] }`.
205
-
206
- **Loose ≠ orphan** — the trap. `lint`'s `orphan` is about *reachability*, and an
207
- `index.md` listing makes a file reachable, so an indexed file is never an orphan.
208
- But an index listing is **not a graph edge**: a file can be listed in an index yet
209
- have no cross-links, so it floats in the graph while `lint` reports it as reachable.
210
- `loose`/`unlinked` catch exactly that gap. A loose file is not automatically a
211
- defect — a terminal leaf (a backlog item, a spec reference) can be loose by design;
212
- `loose` surfaces the set so you can judge intent (see the
213
- [maintain playbook](../playbooks/maintain.md)).
214
-
215
- ## search — ranked text retrieval (metadata + body)
216
-
217
- The browser page's search brought to the CLI and extended to bodies, so "which
218
- concept covers X?" costs rows, not body reads. `okf search <dir> <term…>`:
219
- terms AND together — every term must hit at least one searched field, not
220
- necessarily the same one — matched **literally against raw text** by default, or
221
- as Ruby regular expressions with `--regexp`/`-e` (an invalid pattern is a usage
222
- error, exit 2). `--fuzzy` forgives typos; pairing it with `-e` is a usage error,
223
- since a pattern is matched literally rather than by edit distance.
224
- `--in a,b` restricts the searched fields (title, id, tags, type, description,
225
- sources, body — `sources` is each entry's title and resource joined, so a
226
- migrated bundle keeps the recall its `# Citations` body text used to give it);
227
- the shared `--type/--dir/--tag/--status/--trust` filters narrow the candidates
228
- *first*, so a search scoped by what `index` taught you stays surgical.
229
-
230
- **The default is exact, so an exact query means what it looks like.** A phrase in
231
- one argument (`"dedup key"`), a dotted version (`7.2.0`), an underscored
232
- identifier (`customer_id`), a mid-word fragment (`ustomer`) and a word written in
233
- `backticks` all match literally. This is what the scan engine buys, and it is the
234
- default precisely because those queries are the common ones and the alternative
235
- loses them silently. <!-- rule:okf-search-exact-identifiers -->
236
-
237
- **`--engine index` is the other engine, and the one to reach for when ranking
238
- matters more than exactness.** The engine is normally chosen by what the query
239
- needs — `--fuzzy` routes to the index, anything else stays on the default scan —
240
- and nothing is printed about the choice. `--engine NAME` overrides that for the
241
- case the flags cannot express: a matching *model* requires no capability, so no
242
- flag selects one. Under the index, terms match whole tokens and their prefixes
243
- (`dedup` finds `deduplication`), rows rank by BM25+, and it is the engine the
244
- browser page runs — so name it when reconciling a CLI answer with the page. The
245
- cost is real: its tokenizer splits on punctuation, so identifiers shatter
246
- (`customer_id` → `customer` + `id`), an infix finds nothing, and a backtick is
247
- never split off at all, so a word inside a code span is unfindable — a large
248
- silent loss, since technical prose is full of them. **Do not count on ranking to
249
- rescue it** — BM25 normalizes by field length, so a short concept dense in `7`,
250
- `2` and `0` can outrank the one that actually says `7.2.0`. Naming an engine that
251
- cannot do what you also asked (`--engine index -e`) is a usage error naming one
252
- that can. <!-- rule:okf-search-engine-choice -->
253
-
254
- **The capabilities, and which engine has them.** An engine is selected by what
255
- the query *requires*; only a matching model has to be named, because requiring
256
- nothing is not something a flag can express:
257
-
258
- | Flag | Capability | Engine | What it does |
259
- |---|---|---|---|
260
- | *(none)* | — | scan | literal substring over raw text; scores by summed field weight |
261
- | `-e` / `--regexp` | `regexp` | scan | each term is a Ruby regexp, case-insensitive; invalid → exit 2 |
262
- | `--fuzzy` | `fuzzy` | **index** | edit distance 0.2 × term length — and switches engine |
263
- | `--engine index` | — | index | whole-token + prefix matching, BM25+ ranking, browser parity |
264
- | `--engine scan` | — | scan | the default, spelled out |
265
-
266
- Two consequences worth holding. **`--fuzzy` is an engine switch, not a mode**: it
267
- carries the whole index with it, so a run that wanted one typo forgiven also gets
268
- shattered identifiers and unfindable code spans — fix the spelling and stay on
269
- the default when you can. And **`-e` moves nothing** now, because the default
270
- engine already offers `regexp`; it changes how a term is *read* (pattern rather
271
- than literal), not where it is matched. <!-- rule:okf-search-fuzzy-is-a-switch -->
272
-
273
- `prefix` is a capability the index declares but no flag selects — it is always on
274
- there. **It is not a reason to reach for the index**: a substring match already
275
- covers every prefix and then some, so `dedup` finds `deduplication` under both
276
- engines, while `duplication` and `uplicat` find it under the default only. Prefix
277
- is what the index needs to catch up to raw text, not a capability it adds on top.
278
- The index's real advantages over the default are exactly three — relevance
279
- ranking, typo tolerance, and page parity.
280
-
281
- **Search spans bundles.** Leading @refs pick several registered bundles
282
- (`okf search @handbook @notes auth`); **`@all`** is the ref that means every one.
283
- Rows from different bundles are ranked together and comparable, and each row
284
- carries its bundle's slug. Under `--engine index` the bundles go into **one
285
- corpus** — BM25 prices a term by how rare it is, so separately-ranked lists would
286
- not compare — which makes a score relative to the whole answer: the same concept
287
- scores lower searched beside others than searched alone. The default scan needs
288
- no such trick — its score is absolute, so a row is worth the same either way.
289
- This is the
290
- cross-bundle retrieval the in-page search does not have: one question, every
291
- bundle you keep. <!-- rule:okf-search-all -->
292
-
293
- `@all` is a ref, not a flag, which is what keeps the grammar single: slot 1 is
294
- always a bundle identity, so a directory there is a directory and nothing can
295
- flip it into a term. Being a ref, it is normalized like one — `@ALL` and `@All`
296
- name every bundle just as `@One` names the bundle registered from dir `One`. It composes accordingly — `@all @docs` expands and dedupes
297
- (all ⊇ docs), needing no diagnostic. **Asking for everything tolerates gaps;
298
- naming one bundle demands it**: `@all` skips a bundle whose directory has
299
- vanished with a note on stderr, while `@docs` fails hard. `@all` is only
300
- `search`'s: every other verb answers about one bundle, so it refuses `@all` by
301
- name rather than letting the answer depend on how many bundles you happen to
302
- have registered. `all` is reserved as a slug — a directory named `all/` registers
303
- as `all-2`, `--as all` is refused, and an `all` row already in the registry file
304
- (hand-typed, or written before the name was reserved) is read as `all-2` rather
305
- than taken as grounds to reject the file — so `@all` is never ambiguous, and the
306
- reservation never strands a registry it inherited. **The read normalizes every
307
- slug** the same way registration would, so a hand-typed `"slug": "My Docs"` lists
308
- and resolves as `my-docs`; an entry the listing shows is always an entry `@slug`,
309
- `rename`, and `default` can name.
310
-
311
- `--fields` projects the shape the mode actually emits: `slug` is available in
312
- registry mode, and a usage error naming the real fields on a path-named search,
313
- which has no slug to give. Two sharp edges: every *leading* @-arg is taken as a ref, so a literal @-term
314
- (`@babel/core`, a Ruby `@ivar`) needs a non-@ term before it or `-e '\@term'` —
315
- the CLI notes both traps on stderr — and any ref, even one, switches the JSON
316
- envelope (next paragraph).
317
-
318
- Rows rank by where they hit — title 5, id 4, tags 3, type/description 2, body 1 —
319
- summed as an absolute score by the default scan, and carried as per-field boost
320
- into **BM25+** under `--engine index`. Each row carries one bounded context
321
- snippet from the strongest match that needs context (description or body). Every row still names the fields that hit (`matched`), so a result stays
322
- citable rather than being a bare relevance number. Exact by default: the
323
- consuming agent is the fuzzy layer — when terms miss, learn the bundle's
324
- vocabulary from `tags`/`types` and re-ask in its own words, rather than
325
- hammering synonyms or reaching for `--fuzzy` before you have looked. Advisory read: **exit 0 even with zero matches**.
326
- JSON, plain-dir mode: `{ bundle, query, count, matches: [{ id, title, type,
327
- dir, top_dir, tags, matched, score, snippet }] }`. Registry mode — any leading @ref,
328
- `@all` or a `@group` among them (a group fans out to its member bundles) — swaps the envelope: `{ bundles: [{ slug, dir }, …],
329
- query, count, matches: [{ slug, id, … }] }`; a parser must branch on which form
330
- it called. The head maps each slug to its dir once, so a row resolves to
331
- `<dir>/<id>.md` without a second lookup and without repeating a path per row.
332
- Both are projectable with `--fields/--except`, and projection is literal — when
333
- merging bundles, put `slug` in your `--fields` list or the row label drops and
334
- same-id concepts from different bundles become indistinguishable. The retrieval procedure that puts this verb in sequence —
335
- map first, finder second, bodies last — is the
336
- [search playbook](../playbooks/search.md).
337
-
338
- ## index — the progressive-disclosure map (§8)
339
-
340
- The "orient before you read" view, and the read verb that sees the layer the
341
- concept views can't: `index.md` files are reserved/structural, so
342
- `catalog`/`files`/… never show them (in the browser, the Indexes tab and
343
- folder clicks render this same map). `okf index <dir>` prints one entry per directory
344
- that holds concepts or carries an `index.md`, root first — the authored index body
345
- (frontmatter stripped), a `type`/`tag` rollup over the concepts that live directly
346
- there, its child directories, and the concept listing. Run it first when picking up
347
- an existing bundle: it is the cheapest high-signal orientation, and it surfaces
348
- enumeration drift a grep can't (you can't grep for a listing entry that is *missing*).
349
-
350
- `--dir PATH` narrows to a directory **and everything below it**, and is
351
- **repeatable** — `--dir model --dir format` shows both; `root` (or `.`) names the
352
- bundle root, unless the bundle really has a `root/` directory, which owns the
353
- word. A `--dir` also brings the **chain from the root down to it**, so a branch is
354
- never shown adrift of the authored context that says what it is — the root
355
- `index.md`'s prose first among it. Those rows print with a leading `↑` and carry
356
- `ancestor: true`; `--no-ancestors` drops them. Ascent and descent are separate
357
- axes, so `--depth` never bounds the chain: `--dir X --depth 0` is X alone, plus
358
- how you get to X. A `--dir` that names nothing gains no chain — a lone root row
359
- would read as a partial answer to a query that matched nothing.
360
-
361
- `--depth N` bounds how far below the starting point the map reaches
362
- (the `--dir` when one is given, else the bundle root), counted **relatively**:
363
- `--depth 1` is the top of the tree, `--dir X --depth 1` is one branch of it, and
364
- the pair walks down a level at a time.
365
-
366
- **On a bundle of any size the map is unreadable whole** — every directory is a
367
- section, and even `--no-body` keeps one listing row per *concept* — so narrow
368
- rather than paging it: `okf dirs` is the orientation, `--dir <branch> --depth 1`
369
- is the step down into it,
370
- and `--except body,listing` on top of either is the lean JSON skeleton. Full
371
- `index` output on a few hundred concepts runs to hundreds of KB; the same map at
372
- `--depth 1` is a couple of KB.
373
-
374
- `--no-body` drops the prose to a
375
- skeleton (headers, rollups, child pointers). For a directory that has concepts but
376
- **no `index.md`**, the listing is **synthesized** from the concepts' descriptions
377
- and tagged `(no index.md)` — §8 explicitly permits synthesizing a map on the fly.
378
-
379
- It is a **read view**: advisory, always exit 0. A synthesized directory is a
380
- *signal* (a map worth writing), never a defect — `index` emits no lint findings and
381
- never fails a bundle. JSON: `{ bundle, count, directories: [{ dir, index_path,
382
- present, synthesized, count, types, tags, subdirs, body, listing: [{ id, title,
383
- description, type, tags }] }] }` — `ancestor` marks a row that is there to place
384
- the branch rather than to answer about it.
385
-
386
- ## dirs — the bundle's clusters and their sizes
387
-
388
- `okf dirs <dir>` lists every directory the bundle has — the ones holding
389
- concepts, the ones carrying an `index.md`, and the empty intermediates that only
390
- exist to connect the tree — with the number of concepts living **directly** in
391
- each and the number in its **subtree**. A cluster *is* a directory here, so this
392
- is the view that tells you what `--dir` can be pointed at and how much sits
393
- behind each choice.
394
-
395
- Two numbers, because one cannot answer the question. `count` is direct, so the
396
- column sums to the bundle's concept total and a dir holding only sub-directories
397
- reads `0` rather than a hidden rollup. `subtree` is defined as *exactly what
398
- `--dir <that row>` returns*, so the row and the flag can never disagree — which
399
- is also why the root's subtree is its own direct count (`.` is a prefix of
400
- nothing). Without it a truncated listing is all zeroes at the top of a deep tree,
401
- which is where you most need to know where the mass is. The human table shows the
402
- second column only where some dir actually nests.
403
-
404
- `--dir PATH` (repeatable) narrows to a directory and its subtree, and brings the
405
- **chain up to the root** with it so the branch is placed rather than shown
406
- adrift — those rows are marked `↑`, carry `ancestor: true`, and stay out of
407
- `total` (`--no-ancestors` drops them). `--depth N` keeps only N levels below the
408
- starting point — the `--dir` when one is given,
409
- the bundle root otherwise. Relative, not absolute, so `--dir a/b --depth 1`
410
- reads "a/b and one level under it" without your first working out how deep `a/b`
411
- is. `--depth 0` is the starting point alone. A `--depth` that is not a whole
412
- number is a usage error (exit 2).
413
-
414
- **This is the first command to run on a bundle you do not know** — the same first
415
- move [SKILL.md](../SKILL.md) prescribes. `okf dirs <dir>` is one row per
416
- directory, so its size tracks the tree rather than the concept count: it tells
417
- you the shape and where the weight sits, `--depth 1` trims it further on a deep
418
- bundle, and you then descend with `okf index --dir`, one level at a time.
419
-
420
- The root prints `(root)` and stores `.` — the split every grouped view keeps, so
421
- a table and its `--json` never disagree about which spelling is the data. JSON:
422
- `{ bundle, total, count, dirs: [{ dir, ancestor, count, subtree, subdirs }] }`,
423
- root first. `count` is rows printed, chain included; `total` sums the direct
424
- counts of the rows you actually asked for, which is what keeps a row's `subtree`
425
- equal to the `total` that `--dir` on that row returns.
426
-
427
- ## catalog / files / tags / types / stats — the server views, as text
428
-
429
- The browser server (below) has Catalog, Files, Tags and Stats panels; these
430
- verbs reproduce them on the CLI so an agent can read a bundle without a browser.
431
- All are advisory reads (exit 0) sharing one data source (per-concept metadata plus
432
- in/out link degree). Add `--json` to any for a machine substrate.
433
-
434
- - **`catalog`** — every concept with its metadata (type, status, trust, tags,
435
- provenance, in/out link degree, description), grouped by top-level dir (`dir`
436
- on every row carries the full path, `top_dir` the first segment). The "what's
437
- here, in detail" view. JSON: `{ bundle, count, concepts: [{ id, title, type,
438
- description, tags, generated_at, generated_by, generated, trust, status,
439
- stale_after, sources, backlog_ref, dir, top_dir, links_out, links_in }] }`.
440
- Four of those deserve a sentence: `generated` is the raw boolean ("does the
441
- document *declare* a generated mapping"), which is what tells hand-written
442
- apart from v0.1-with-timestamp — `generated_at` alone conflates them, because
443
- §13.1 lifts a legacy `timestamp` into it. `trust` is the derived §5.3 tier as
444
- a hyphenated literal (`unverified` | `machine-confirmed` | `human-reviewed`) —
445
- compare against exactly those. `status` is the *declared* value, `null` when
446
- absent (the row never fabricates frontmatter; the `--status` filter is what
447
- applies the §5.4 default). `sources` is a count. Temporal fields render
448
- ISO 8601 (`stale_after` as `YYYY-MM-DD`). The `timestamp` column is retired —
449
- `--fields timestamp` is a usage error naming the valid fields.
450
- - **`files`** — the folder tree: each concept's filename + title, grouped by
451
- directory. The "how it's organised" view. JSON: `{ bundle, count, files: [{ path,
452
- id, dir, type, title, description }] }`.
453
- - **`tags`** — every tag with the concepts that carry it, ordered by count
454
- descending. The "what themes dominate" view. JSON: `{ bundle, count, tags: [{ tag,
455
- count, concepts: [id, …] }] }`. `--by type|dir` regroups the list per concept
456
- dimension with **within-group** counts (a tag spanning groups appears in each);
457
- each row also carries the tag's **total** across the narrowed set, printed
458
- `count/total` when they differ — so a tag's locality reads per row (a plain
459
- count = wholly local; `2/7` = a cross-cutting spread). The substrate for tag
460
- curation and for [refine](../playbooks/refine.md)'s domain-vs-concern read;
461
- the judgment recipes live in the [maintain playbook](../playbooks/maintain.md)
462
- and the [refine playbook](../playbooks/refine.md). JSON: `{ bundle, count, by,
463
- groups: [{ <dim>, count, tags: [{ tag, count, total, concepts }] }] }`.
464
- - **`types`** — every type with the concepts that carry it, ordered by count
465
- descending. The "what kinds of knowledge" view. JSON: `{ bundle, count, types:
466
- [{ type, count, concepts: [id, …] }] }`.
467
- - **`stats`** — bundle rollups: concept / dir / type / cross-link / distinct-tag
468
- totals plus per-type and per-dir breakdowns. The "shape at a glance" view. JSON:
469
- `{ bundle, concepts, dirs, top_dirs, concept_types, cross_links, distinct_tags,
470
- by_type, by_dir, by_top_dir }` (`top_dirs`/`by_top_dir` are the first-segment
471
- rollup). `dirs`/`by_dir` cover every directory `okf dirs`
472
- lists — counts are direct, so a directory holding nothing itself is present at
473
- `0` rather than missing, and `by_dir.keys` is a complete list of what `--dir`
474
- can address.
475
-
476
- The four list views narrow with the same filters the browser panels offer —
107
+ The four list views of [cli/views.md](cli/views.md) narrow with the same
108
+ filters the browser panels offer —
477
109
  `--type TYPE`, `--dir PATH`, `--tag TAG`, `--status STATUS`, `--trust TIER`
478
110
  (`search` takes them too); each takes the ones orthogonal to itself (`tags`
479
111
  can't filter by tag). `--status` matches the *effective* status (absent reads
@@ -498,205 +130,3 @@ for `tags --by area`. Both go in a later release; write `--dir` in anything new.
498
130
  On `index` it combines with neither `--depth` nor `--dir` — exit 2, because it is
499
131
  *exact* and both of those select a range, so the pair used to return the area
500
132
  plus whatever the other flag selected: an answer to neither question.
501
-
502
- Reach for `stats` first to size a bundle, `catalog`/`files` to enumerate it, `tags`
503
- to find thematic clusters — all without standing up the server.
504
-
505
- ## references — the `references/` inventory (§6.3)
506
-
507
- Lists every file under `references/` — including the non-markdown ones no other
508
- verb can see, since the concept model carries only markdown — with which
509
- concepts cite each file through the §6.2 path-valued fields (`resource`,
510
- `sources[].resource`, `computation`, `executor.resource`, `attester.resource`),
511
- plus every pointer into `references/` that resolves to nothing. Advisory:
512
- **exits `0`** even with dangling pointers — the findings are the output. JSON:
513
- `{ bundle, dangling, count, references: [{ path, dir, kind, referenced_by }] }`,
514
- with `--fields`/`--except` projecting the rows. A file that is itself a concept
515
- (§6.3 allows both) is marked `kind: "concept"`; body links are the graph's
516
- business and are not counted here.
517
-
518
- **The dangling list is where §6.2's bare-path trap surfaces.** A bare
519
- `references/attesters/rev.py` written from `metrics/` resolves relative to the
520
- concept — `metrics/references/attesters/rev.py`, nothing — and when the
521
- leading-slash spelling would have hit, the entry says so:
522
- `/references/attesters/rev.py exists — missing leading slash?`. Reach is any
523
- extension, which is exactly what `broken_source` and `broken_attestation_ref`
524
- cannot offer (their exemptions above), so run it wherever a bundle carries
525
- attester code or computation files.
526
-
527
- ## server — interactive graph server
528
-
529
- Starts a local HTTP server (`okf server <dir>`; `-p`/`--port`, default 8808, and
530
- `--bind`) and prints its URL — stop it with Ctrl-C. The page boots from a lean
531
- payload (nodes carry only `id` and `title`, plus compact type/tag indexes) and
532
- fetches each concept's markdown body **live from disk** as you click it, so the
533
- initial load stays small and edits show without a restart. Mermaid code blocks
534
- in a body render as diagrams, and a click (or tap) opens the diagram full
535
- screen with drag-to-pan and wheel/pinch zoom. Concepts render as nodes
536
- coloured by `type` and sized by degree, links as edges, with a detail panel
537
- (rendered markdown, "Links to" / "Linked from" backlinks), layout switching,
538
- type/dir/tag filters on every view (the dir chips take a directory *and* its
539
- subtree, the same rule `--dir` uses), and search. Cluster mode groups the
540
- concepts into one box per directory, nested to a depth picked beside the layout
541
- select — depth 1 is the flat view, and a flat bundle is offered no control. The authored layer is in the
542
- UI too: the Files view carries **Files | Indexes** tabs — the Indexes tab
543
- lists the log first (the chronological index), then every `index.md` — and
544
- folder nodes in file-tree mode and directory boxes in cluster mode open a
545
- directory's §8 map in the inspector (authored, or synthesized when none
546
- exists). Links to an `index.md`, `log.md`, or bare directory navigate instead
547
- of dead-ending, and the log is fetched fresh on every read, so a
548
- just-appended entry shows without a restart. `?view=index` jumps straight to
549
- the Indexes tab. It is a Rack app, so the same server can be mounted in a
550
- host app (e.g. Rails).
551
-
552
- **Hosting many bundles (the hub).** `okf server` takes zero or more dirs.
553
- One dir is the classic single bundle at `/`. Two or more mounts each under
554
- `/b/<slug>/` behind a hub, `/` redirects to the default, and `/b/` is a
555
- self-contained **bundle index** (every hosted bundle, concept counts, default
556
- marked — the browser counterpart of `okf registry`). An unknown slug 404s as a
557
- page listing the hosted bundles, so a stale bookmark after a rename gets a way
558
- home. With **no** dir it serves the *persistent registry*, a plain JSON file
559
- under `$OKF_HOME` (default `~/.okf`), managed by the
560
- `okf registry` umbrella — like git's `remote` family, and split by what each
561
- verb keys on.
562
- **`okf registry init`** creates a *project-local* registry instead: a
563
- `.okf-registry.json` in the current directory, which okf discovers by walking up
564
- from the working directory and uses in place of the global one while you are
565
- inside its tree (the nearest wins, so nested registries resolve nearest-first).
566
- Every registry op — and every `@slug` — then resolves through it, so a bare
567
- `okf server` inside a repo serves that repo's bundles with no `$OKF_HOME` setup;
568
- `okf registry list` names the local file it found. `OKF_NO_DISCOVERY=1` forces
569
- the global registry — the escape hatch for a fixed-cwd caller (CI, a tool). A
570
- local registry stores **portable** paths: a bundle inside its tree is written
571
- relative to the `.okf-registry.json`, so committing the file lets it travel with
572
- the repo (a checkout elsewhere, a container mounting it) and resolve unchanged;
573
- a bundle outside the tree stays absolute, since it cannot travel. Paths still read
574
- back absolute wherever the CLI reports them.
575
- **Entry verbs** take a path: `okf registry set <dir>` adds it
576
- (slug from the basename, or `--as`, which errors on a collision; `--default`
577
- puts it first), and because the entry is keyed by path, `set` on an
578
- already-registered dir updates it in place — refreshing its title, and renaming
579
- it when `--as` is given. `okf registry del <dir|@slug>` removes a bundle *or* a group — by name, so an
580
- entry whose directory is already gone still deletes, and removing a bundle
581
- **cascade-drops** it from every group that named it (a group emptied that way is
582
- deleted). Slug *or* dir, never both readings at
583
- once: an argument with a `/` in it names a location and only a location, so
584
- `del ./notes` refuses when no entry points there rather than stripping to the
585
- slug `notes` and deleting a bundle somewhere else entirely.
586
- <!-- rule:okf-registry-del-path-or-slug -->
587
- **Slug verbs** take the name — bare, or as an `@slug`: `okf registry default <@slug>`
588
- chooses which bundle `/` opens **by moving that entry to the front** (a group is
589
- refused — the default is one bundle), and
590
- `okf registry rename <@slug> <new>` renames a bundle *or* group slug (mount path
591
- and switcher name), **cascading** the new name into every group's member list —
592
- `<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
593
- default** — that is the whole rule, so the first bundle you register is the
594
- default until you move another one, a rename keeps its position, and a `del`
595
- promotes whatever is next. A vanished directory is stepped over (the server
596
- cannot open one, so starring it would name a bundle `/` never serves), and
597
- `registry default @slug` refuses one outright — the same refusal `registry set`
598
- gives a directory that is not there. The file is hand-editable and reorders
599
- visibly, which is the point: there is no stored slug that can dangle.
600
- <!-- rule:okf-registry-default-position -->
601
- **Group verbs** name a *set* of bundles under one slug — a durable subset for the
602
- two verbs that take several bundles. `okf registry group <slug> <@member…>`
603
- creates a group, or adds members to one (a union); members are bundle *or* group
604
- slugs, so groups nest. A group shares the slug namespace with bundles (a slug
605
- names one *or* the other, never both), `all` stays reserved, and a member set that
606
- would make the group reach itself is refused. `okf registry ungroup <slug>
607
- <@member…>` removes members; emptying a group deletes it. A group resolves,
608
- recursively and path-deduped, to its bundle leaves — which **only `okf search`
609
- and `okf server` consume**: every single-bundle verb (`lint`, `index`, …) refuses
610
- a `@group` with exit 2, the same rule that refuses a second bundle. `@all` is
611
- unchanged — it still names every registered *bundle*, groups being named subsets
612
- of that.
613
- `okf registry list` (or a bare
614
- `okf registry`) stars the default and flags vanished dirs `(missing)` — the
615
- server skips those with a note — and lists any groups with their members and
616
- resolved leaf count; `--json` answers
617
- `{ registry: <file>, count, bundles: [{ slug, title, dir, mount, default,
618
- missing }], groups: [{ slug, members, resolved }] }`, naming the file it read so a
619
- `$OKF_HOME` mismatch is visible. The hub roster is a
620
- **boot-time snapshot**: restart `okf server` after registry changes. Behind a
621
- hub the page gains a **bundle switcher** (⌘/Ctrl-K, or the rail button with its
622
- bundle-count badge): the current bundle is pinned, the default chipped; ⏎
623
- opens, ⌘/Ctrl-⏎ opens a new tab, and the current view carries over. Switching
624
- is a server-only affordance — a static `render` file has no siblings and shows
625
- none.
626
-
627
- **Bundle-less run.** Register bundles once, then `okf server` (no dir) hosts
628
- them all with the registry's first entry still on disk at `/` — the way to keep
629
- several bundles a keystroke apart without re-passing paths.
630
- `okf server @a @b` serves a registry subset, each mounted under its registered
631
- slug — but as with any dirs-given run, the *first argument* lands at `/`; the
632
- registry's own order applies only to the bundle-less run. A `@group` argument
633
- fans out to its member bundles in the same way (`okf server @backend`), its first
634
- member landing at `/`; `okf search @group <term…>` merges the group's members
635
- into one ranking, exactly as naming them individually would.
636
-
637
- **Trust boundary:** the page renders each fetched markdown body through
638
- DOMPurify and escapes everything it inlines (every `<` in the graph data is
639
- escaped, so it cannot break out of its `<script>`), but it still loads its
640
- viewer libraries (Cytoscape, marked, DOMPurify — plus Mermaid and Panzoom,
641
- lazy-loaded on first use) from a CDN and renders whatever
642
- links the bundle carries — so only serve bundles you trust.
643
-
644
- ## render — static graph export
645
-
646
- Writes the same interactive page as one static, self-contained HTML file
647
- (`okf render <dir>`), so the graph hosts where there is no server — GitHub Pages,
648
- an object store, an attachment. Prints to stdout (`okf render <dir> > graph.html`)
649
- or writes `-o FILE`; `--title`/`--link`/`--layout` mirror `server`. It is the same
650
- template `server` renders, one switch apart: rather than fetching each body,
651
- description, catalog, index, and log live, `render` bakes the whole bundle into
652
- the page and the browser reads from that embedded payload — no server, no build
653
- step. The trade-off is weight (every body is inlined), so `server` stays the
654
- choice for a bundle too large to ship whole.
655
-
656
- **Trust boundary:** the same two guards as `server` — every inlined body is
657
- `</script>`-escaped like the graph data and still sanitized by DOMPurify when
658
- rendered — so a static file is no laxer than the live server. Only render bundles
659
- you trust.
660
-
661
- ## graph — the raw structure
662
-
663
- Prints the node/edge graph. `--json` emits a machine-readable dump — the
664
- `bundle`/`slug` head every view carries, then `nodes` (with
665
- `id`/`type`/`title`/`description`/`tags` **and, by default, every `body`** — the
666
- part that dominates the bytes on a real bundle) plus `edges` — you can pipe into
667
- other analysis. A concept with a missing *or blank* `type` indexes under
668
- `Untyped`: §11 condition 2 rejects both identically, so both land in one bucket. To *plan* a traversal, structure is all you need: `--no-body`
669
- drops each node's body, and `--minimal` ships only `id`/`title` plus the type/tag
670
- indexes — the lean shape the `server` page boots from. Reach for the full dump
671
- only when the task truly consumes every body; for one question, the
672
- [search verb](#search--ranked-text-retrieval-metadata--body) is orders cheaper.
673
-
674
- `--hubs` swaps the dump for the **inbound ranking**: every concept with at
675
- least one inbound link, ranked by inbound degree, each with its links grouped
676
- by *source top-level dir* (`core/status ×3 flows 2, billing 1`) — the evidence for
677
- [refine](../playbooks/refine.md)'s hub origin test ("is this hub well-homed?").
678
- A source at the bundle root counts under `(root)`. JSON: `{ bundle, count,
679
- hubs: [{ id, top_dir, inbound, by_top_dir: { <top_dir>: n } }] }`. Advisory read, exit 0;
680
- `--minimal`/`--no-body` shape node payloads and change nothing here.
681
-
682
- `--traffic` asks the same question one grain coarser: **directories**, not
683
- concepts. Every concept collapses into the directory it lives in and every link
684
- between two directories collapses into one weighted arc, so a bundle's wiring
685
- becomes a table you can read — measured on one 47-concept bundle, 227 links
686
- collapsed into 50 arcs, of which the fitted cut draws 22. Each row carries the
687
- directory's traffic split three ways
688
- (`internal` / `out` / `in`) plus **cohesion**, its internal share of the total:
689
- the evidence for [refine](../playbooks/refine.md)'s container test, where
690
- `--hubs` only ever answered about concepts. Rows lead with the lowest cohesion,
691
- so the directories with a case to answer come first, and a directory with no
692
- traffic at all prints `—` rather than a `0%` it did not earn.
693
-
694
- `--cut N` is the least arc weight drawn. It defaults to a value **fitted to the
695
- bundle** — enough arcs for roughly 1.5 per directory, floored at 8 — because a
696
- fixed weight cannot serve both ends: measured at weight 3 across ten bundles it
697
- left 2 arcs on one and 136 on another. The JSON says which you got. Cohesion is
698
- computed over *every* arc and never the drawn ones, so tightening the cut
699
- changes the picture and never the evidence. JSON: `{ bundle, cut, fitted, dirs:
700
- [{ dir, parent, count, subtree, internal, out, in, cohesion }], arcs: [{ source,
701
- target, weight }], total_arcs }` — a fraction of the full dump (2.6 KB against
702
- 27 KB on that bundle), and the shape rather than the contents. Advisory, exit 0.