okf 1.9.0 → 1.11.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +696 -133
  3. data/README.md +250 -334
  4. data/lib/okf/bundle/folder.rb +24 -5
  5. data/lib/okf/bundle/linter.rb +1 -1
  6. data/lib/okf/bundle/search/index.rb +13 -3
  7. data/lib/okf/bundle/search.rb +91 -11
  8. data/lib/okf/bundle.rb +26 -2
  9. data/lib/okf/cli/catalog.rb +66 -0
  10. data/lib/okf/cli/command.rb +657 -0
  11. data/lib/okf/cli/dirs.rb +118 -0
  12. data/lib/okf/cli/files.rb +68 -0
  13. data/lib/okf/cli/graph.rb +82 -0
  14. data/lib/okf/cli/index.rb +169 -0
  15. data/lib/okf/cli/lint.rb +139 -0
  16. data/lib/okf/cli/loose.rb +78 -0
  17. data/lib/okf/cli/registry.rb +229 -0
  18. data/lib/okf/cli/render.rb +66 -0
  19. data/lib/okf/cli/search.rb +285 -0
  20. data/lib/okf/cli/server.rb +186 -0
  21. data/lib/okf/cli/skill.rb +57 -0
  22. data/lib/okf/cli/stats.rb +113 -0
  23. data/lib/okf/cli/tags.rb +144 -0
  24. data/lib/okf/cli/types.rb +37 -0
  25. data/lib/okf/cli/validate.rb +66 -0
  26. data/lib/okf/cli.rb +425 -1706
  27. data/lib/okf/render/graph/template.html.erb +1285 -129
  28. data/lib/okf/render/graph.rb +46 -2
  29. data/lib/okf/server/app.rb +71 -4
  30. data/lib/okf/server/hub/not_found.rb +663 -0
  31. data/lib/okf/server/hub.rb +512 -38
  32. data/lib/okf/skill/SKILL.md +26 -19
  33. data/lib/okf/skill/playbooks/consume.md +3 -3
  34. data/lib/okf/skill/playbooks/curate.md +3 -1
  35. data/lib/okf/skill/playbooks/maintain.md +7 -5
  36. data/lib/okf/skill/playbooks/menu.md +5 -0
  37. data/lib/okf/skill/playbooks/refine.md +93 -0
  38. data/lib/okf/skill/playbooks/search.md +7 -7
  39. data/lib/okf/skill/reference/cli.md +122 -22
  40. data/lib/okf/version.rb +1 -1
  41. data/lib/okf.rb +9 -0
  42. metadata +38 -8
@@ -13,7 +13,7 @@ description: >-
13
13
  working in a repo that already carries an OKF bundle — a `.okf/` directory or a
14
14
  root `index.md` carrying `okf_version`.
15
15
  user-invocable: true
16
- argument-hint: "[search|produce|migrate|maintain|consume|curate|doctor|<okf-cli-verb>] [dir|@slug] [--flags]"
16
+ argument-hint: "[search|produce|migrate|maintain|refine|consume|curate|doctor|<okf-cli-verb>] [dir|@slug] [--flags]"
17
17
  allowed-tools: Read Write Edit Grep Glob Bash
18
18
  ---
19
19
 
@@ -83,10 +83,10 @@ flags. The division of labour is the whole game:
83
83
 
84
84
  - **Shell out — never eyeball —** anything a verb computes: conformance (§9), what
85
85
  exists, what links where, where a term lives, what's stale, the map. Every read
86
- verb takes `--json` and the list views filter by type/area/tag, so ask the narrow
86
+ verb takes `--json` and the list views filter by type/dir/tag, so ask the narrow
87
87
  question instead of paging the bundle.
88
- - **Skeleton first, bodies last.** `index --no-body`, `search`, `graph --minimal`,
89
- and `--fields` projections each answer for a fraction of a dump's bytes; full
88
+ - **Skeleton first, bodies last.** `dirs`, `search`, `graph --minimal`, and
89
+ `--fields` projections each answer for a fraction of a dump's bytes; full
90
90
  bodies are the final step of a retrieval, never the first. <!-- rule:okf-skeleton-first -->
91
91
  - **You judge — the CLI can't —** meaning: contradictions, semantic staleness
92
92
  (parses fine, no longer true), whether a loose file is terminal-by-design, whether
@@ -102,12 +102,15 @@ shapes, the tag-curation views, the server's trust boundary.
102
102
 
103
103
  ## Orient before you touch anything
104
104
 
105
- Picking up a bundle you don't already know — to consume or maintain — run `okf
106
- index <dir|@slug>` (the §6 map: every directory's index body, rollups, and listings) and
107
- read `log.md` (the §7 baseline of what changed last) **before** greping or opening
108
- leaves. It is the cheapest high-signal context, and the only reliable way to catch
109
- enumeration drift: **grep cannot find an index entry that is missing** — you can't
110
- search for the word that should be there but isn't. <!-- rule:okf-orient-index -->
105
+ Picking up a bundle you don't already know — to consume or maintain — start with
106
+ `okf dirs <dir|@slug>`: one row per *directory*, so it stays small on a bundle of
107
+ any size and it names the branches every other view narrows to. Then open the one
108
+ you want with `okf index <dir|@slug> --dir <branch>` (the §6 map: that directory's
109
+ index body, rollups, and listing), and read `log.md` (the §7 baseline of what
110
+ changed last) — all of it **before** greping or opening leaves. Reach for `index`
111
+ rather than grep for the one reason that outranks convenience: **grep cannot find
112
+ an index entry that is missing**, so enumeration drift is invisible to it — you
113
+ can't search for the word that should be there but isn't. <!-- rule:okf-orient-index -->
111
114
  Per-verb steps are in the
112
115
  playbooks (the Commands table below; no `okf` installed? read the root
113
116
  `index.md` plus each area's `index.md`).
@@ -123,10 +126,12 @@ when you need chapter and verse.
123
126
 
124
127
  **No subcommand?** Infer intent: "document this / capture X" → `produce`;
125
128
  "convert / migrate / OKFy these existing docs into a bundle" → `migrate`; "the
126
- code changed, update the docs" → `maintain`; "what do we know about X / where
127
- is X documented" → `search`; a repo already carrying a bundle plus a task
128
- needing its knowledge → `consume`; "check / graph / preview it" → run the
129
- matching CLI verb and interpret the result. When genuinely ambiguous, ask.
129
+ code changed, update the docs" → `maintain`; "restructure / rebalance the
130
+ bundle / is the structure right / get more out of it" → `refine`; "what do we
131
+ know about X / where is X documented" → `search`; a repo already carrying a
132
+ bundle plus a task needing its knowledge → `consume`; "check / graph / preview
133
+ it" → run the matching CLI verb and interpret the result. When genuinely
134
+ ambiguous, ask.
130
135
 
131
136
  **Which target?** A leading `@` is a *registry ref*, not a path: `@slug` names a
132
137
  bundle registered with `okf registry set`, bare `@` the default — route it
@@ -158,15 +163,17 @@ Read the referenced playbook before executing — it *is* the procedure.
158
163
  | `produce` | Author | create or extend a bundle | [playbooks/produce.md](playbooks/produce.md) |
159
164
  | `migrate` | Author | convert existing docs in place: frontmatter + reserved files, bodies verbatim | [playbooks/migrate.md](playbooks/migrate.md) |
160
165
  | `maintain` | Author | sync the bundle's content with reality after a change | [playbooks/maintain.md](playbooks/maintain.md) |
166
+ | `refine` | Author | optimize the bundle's structure: evidence-driven, cohesion-first; proposes, never auto-applies | [playbooks/refine.md](playbooks/refine.md) |
161
167
  | `consume` | Use | use the bundle as context for a task | [playbooks/consume.md](playbooks/consume.md) |
162
168
  | `curate` | Curate | structural upkeep as it stands: validate + lint + loose | [playbooks/curate.md](playbooks/curate.md) |
163
169
  | `doctor` | Setup | install and verify the CLI, then doctor the bundle | [playbooks/doctor.md](playbooks/doctor.md) |
164
- | `<okf-cli-verb>` | Read | validate, lint, loose, index, catalog, files, tags, types, stats, graph, server, render, registry, skill | `okf <verb> --help` + [reference/cli.md](reference/cli.md) |
170
+ | `<okf-cli-verb>` | Read | validate, lint, loose, index, catalog, files, tags, types, stats, graph, server, render, registry, skill — **plus any verb an installed extension adds** (`okf help` is authoritative, this list is not) | `okf <verb> --help` + [reference/cli.md](reference/cli.md) |
165
171
 
166
- Two boundaries worth keeping sharp: `curate` is structural upkeep only — when
167
- the *content* no longer matches reality, that is `maintain` — and `doctor` is
168
- the one playbook that does not assume the CLI is installed. In Claude Code with
169
- the okf plugin, `/okf:gem` routes these same verbs.
172
+ Three boundaries worth keeping sharp: `curate` is structural upkeep only — when
173
+ the *content* no longer matches reality, that is `maintain`, and when the
174
+ content is right but the *shape* underserves retrieval, that is `refine` — and
175
+ `doctor` is the one playbook that does not assume the CLI is installed. In
176
+ Claude Code with the okf plugin, `/okf:gem` routes these same verbs.
170
177
 
171
178
  ## The lifecycle is a flywheel, not phases
172
179
 
@@ -1,8 +1,8 @@
1
1
  # Playbook: consume — use a bundle as context
2
2
 
3
- 1. **Orient first** (the [SKILL.md](../SKILL.md) reflex): `okf index <dir|@slug>` maps
4
- the whole bundle in one pass — every directory's index body, rollups, and listings —
5
- and `log.md` gives recent history. Address a registered bundle by `@slug` (bare
3
+ 1. **Orient first** (the [SKILL.md](../SKILL.md) reflex): `okf dirs <dir|@slug>`
4
+ gives the shape, `okf index <dir|@slug> --dir <branch>` opens the branch you
5
+ want — its index body, rollups and listing — and `log.md` gives recent history. Address a registered bundle by `@slug` (bare
6
6
  `@` = the default); if the cwd carries no bundle, `okf registry list` finds one
7
7
  instead of a directory hunt. Then follow links only into the concepts the
8
8
  task needs. For a *pointed question* rather than broad context, switch to the
@@ -9,7 +9,9 @@ reachability, backlog, completeness, hygiene. It is not `maintain`, the
9
9
  skill's workflow for when the project changed and the bundle's *content*
10
10
  must catch up with reality; reach for that one when what is written stopped
11
11
  being true. Curating can surface semantic staleness, and when it does,
12
- switch to `maintain` for those concepts.
12
+ switch to `maintain` for those concepts. And it never moves knowledge: when
13
+ the content is right but the shape underserves retrieval — fat areas,
14
+ mis-homed hubs, an uncurated tag layer — that is [refine](refine.md).
13
15
 
14
16
  1. Locate the bundle: the directory you were given, if any; otherwise a
15
17
  `.okf/` directory or a root `index.md` whose frontmatter carries
@@ -1,11 +1,13 @@
1
1
  # Playbook: maintain — keep a bundle in sync with reality
2
2
 
3
3
  Reach for this when the project changed and the bundle's *content* must catch
4
- up. The modelling craft behind steps 3 and 7 lives in
5
- [authoring.md](../reference/authoring.md).
4
+ up. Restructuring the bundle itself — moving concepts, adding areas — is
5
+ [refine](refine.md), not maintain. The modelling craft behind steps 3 and 7
6
+ lives in [authoring.md](../reference/authoring.md).
6
7
 
7
- 1. **Orient before hunting.** Run `okf index <dir>` (the §6 map — every directory's
8
- index body, rollups, and listings), read `log.md` (the §7 baseline: what changed
8
+ 1. **Orient before hunting.** Run `okf dirs <dir>` (the shape), then `okf index
9
+ <dir> --dir <branch>` on the branch the change touches (the §6 map: its index
10
+ body, rollups and listing), read `log.md` (the §7 baseline: what changed
9
11
  last), and `okf stats <dir>` (size and shape) *before* you grep. It is the
10
12
  cheapest context and it primes the hunt — and it is the only reliable way to
11
13
  catch enumeration drift, because **grep cannot find an index entry that is
@@ -44,7 +46,7 @@ up. The modelling craft behind steps 3 and 7 lives in
44
46
  defect.** Loose ≠ orphan: an index listing makes a file *reachable* (not an
45
47
  orphan) but is not a graph edge, so an indexed file can still float here.
46
48
  7. **Curate the tag vocabulary** when the pass
47
- touched tags, or when `okf tags <dir>` shows a long tail of singletons. Run `okf tags <dir> --by area` and
49
+ touched tags, or when `okf tags <dir>` shows a long tail of singletons. Run `okf tags <dir> --by dir` and
48
50
  `--by type` — the grouped view is the analysis; read each group top-down:
49
51
  - **twins** — two tags riding the exact same concepts (equal counts sort them
50
52
  adjacent). Merge into one unless each genuinely names a different theme.
@@ -33,6 +33,11 @@ is the lede.
33
33
  the working tree has uncommitted changes to the code the bundle describes
34
34
  (`git status`), prefer **`maintain`**: that is exactly the drift it exists
35
35
  to close.
36
+ - **clean, but the shape strains** — one area dwarfing the rest in
37
+ `okf stats`, tags spread thin across dirs in `okf tags --by dir`, hubs
38
+ whose inbound links are mostly foreign in `okf graph --hubs` → offer
39
+ **`refine`** (evidence-driven restructuring; it proposes before it
40
+ touches anything).
36
41
  4. **Freshness is off by default.** If the bundle carries timestamps, note that a
37
42
  plain `lint` said nothing about staleness and `okf lint <root> --stale-after
38
43
  90d` is the check that would.
@@ -0,0 +1,93 @@
1
+ # Playbook: refine — restructure a bundle to get the most from OKF
2
+
3
+ Reach for this when the bundle's *content* is right but its *shape* may not be:
4
+ areas grown fat by additive passes, hubs homed by history, a tag layer that
5
+ never became the second index. Refine optimizes the projection — the same
6
+ knowledge, arranged to serve progressive disclosure, the emergent graph,
7
+ cross-cutting tags, and capture-once-link-many. It is not [curate](curate.md)
8
+ (upkeep of the structure as it stands) and not [maintain](maintain.md) (content
9
+ catching up with reality): refine changes where knowledge lives, never what it
10
+ says. Its permitted edits are structural — move a concept, extract a duplicated
11
+ fact to one home, section an index, retag, relink, and write the connective
12
+ sentence a link lives in; summarizing, updating, or correcting a body is
13
+ maintain's job, reached by switching verbs, not by stretching this one.
14
+
15
+ The frame that governs every move: the directory tree is a **lossy projection
16
+ of the link graph**. A tree gives each concept one parent, so the tree encodes
17
+ only the single dominant decomposition; every genuinely many-to-many
18
+ relationship rides links and tags, never new directories. And cohesion outranks
19
+ balance — a move has semantic cost, so balance is a tiebreaker and a fatness
20
+ alarm, never the objective. <!-- rule:okf-cohesion-over-balance -->
21
+
22
+ 1. **Orient.** `okf dirs <dir|@slug>` (directories, fan-out, depth — and
23
+ `subtree` says where the weight sits), `log.md` (how the bundle grew), `okf
24
+ stats` (totals). Additive growth
25
+ optimizes each pass locally, never the whole — that is the drift this
26
+ playbook corrects.
27
+ 2. **Measure — the CLI is the evidence.** Baseline `validate` / `lint
28
+ --stale-after` / `loose` first: refine assumes a sound bundle, and hard
29
+ errors are [curate](curate.md)'s job. Then the two structural reads:
30
+ - `okf tags <dir> --by dir` — each row carries `count/total`, so a tag's
31
+ **locality** reads directly: a tag wholly inside one area names a *domain*
32
+ (the directories are right); one spread across areas names a *concern*.
33
+ - `okf graph <dir> --hubs` — concepts ranked by inbound links, each with
34
+ the areas those links come from: the **origin test** for every hub.
35
+ 3. **Diagnose — you are the judgment.** The measurements are evidence, never
36
+ verdicts:
37
+ - **Concerns never become containers.** A directory built around a spread
38
+ tag ("everything async") prunes nothing — most needs would enter it.
39
+ The cross-cut stays a tag. <!-- rule:okf-concern-not-container -->
40
+ - **A directory must prune.** The positive test for any area, existing or
41
+ proposed: does knowing "it's in there" eliminate a large, even slice? A
42
+ good node splits its parent into chunks that are nameable, mutually
43
+ exclusive, and roughly comparable in size. And small is not merge-worthy
44
+ on its own — a two-concept area that is a genuinely distinct domain
45
+ stays. <!-- rule:okf-directory-prunes -->
46
+ - **The hub origin test.** Inbound majority from the hub's own area:
47
+ well-homed, leave it. A dominant *foreign* area: that area is the better
48
+ home. Foreign majority with *no* dominant area: a shared primitive — the
49
+ only admission ticket into a shared-core area (without that test, a
50
+ `foundation/` rots into a `misc/`). Two comparable strong ties, one of
51
+ them home: stay and carry the other as a tag — moving trades one
52
+ imbalance for another. And in a design bundle expect the central
53
+ decisions to fail this test wholesale: that is centrality, not
54
+ mis-homing.
55
+ - **Fatness alarm, not fatness rule.** A fat area (≳20–25 concepts) wants
56
+ **heading sections inside its `index.md`** first — the same prune as
57
+ sub-directories, for zero extra hops and no new enumeration to keep
58
+ sound. Directory nesting pays only at hundreds of concepts, and only
59
+ where the index's own headings already form separable, nameable
60
+ sub-groups — fatness alone never justifies depth; the sections that
61
+ formed are the evidence the split exists.
62
+ - **Duplication.** Read the area overviews for a fact re-explained in
63
+ several (drifting tables are the tell); capture-once-link-many says
64
+ extract it into one concept and link from the rest. Extraction is the one
65
+ refine move that touches bodies, and it redistributes rather than
66
+ rewrites: assemble the canonical concept from the copies, keep every
67
+ copy's unique domain-specific detail (in the extract, or in the one-line
68
+ note left beside each link), and where the copies *disagree*, which is
69
+ true is a [maintain](maintain.md) question — verify against reality or
70
+ flag the conflict in the proposal, never silently pick a winner while
71
+ merging. <!-- rule:okf-extract-not-rewrite -->
72
+ - **Vocabulary twins.** The [maintain](maintain.md) tag-curation recipe
73
+ (twins, echoes, singletons) applies to `type` too — `okf types <dir>`.
74
+ 4. **Plan — tier by leverage ÷ churn, free levers first.** Tag curation,
75
+ index heading-sectioning, and extraction before any file move; a move only
76
+ when the origin test demands one, and each gated by **do-nothing**: skip it
77
+ unless its value beats its churn. Record what you *declined* and why — the
78
+ decline list is what stops the next pass from re-proposing it.
79
+ 5. **Propose — never auto-apply.** Refine's output is a short report (the
80
+ evidence, the tiers, the declines) plus a ready-to-run execution prompt the
81
+ user can hand back later: a scope line, the governing principles above, the
82
+ explicit prohibitions, the closeout gate as acceptance. Analysis and
83
+ execution are separate on purpose — the judgment is spent once, frozen, and
84
+ then executed without re-derivation. <!-- rule:okf-refine-proposes -->
85
+ 6. **Execute only on approval**, then walk the
86
+ [Closeout gate](../reference/authoring.md#closeout--the-finishing-gate):
87
+ every touched `index.md` re-enumerated, links absolute bundle-relative so
88
+ they survived the moves, a dated `log.md` entry carrying the *why*,
89
+ validate/lint/loose clean, and a before/after of step 2's evidence.
90
+
91
+ Two traps: never split a cohesive cluster (a deliberately paired mirror flow)
92
+ to hit a size band, and never tag a concept with its own directory's name — a
93
+ group-name echo adds no edge.
@@ -12,12 +12,12 @@ reads, and full bodies are read last, and only the winners.
12
12
  the root `index.md` then each relevant area's `index.md` by hand. No bundle in
13
13
  the cwd? `okf registry list` names the registered ones — address them by
14
14
  `@slug`, don't hunt sibling directories.
15
- 2. **Ingest the map and decide where to look.** `okf index <dir|@slug> --no-body` is
16
- the skeleton: every directory with its concept count, types, tags, children.
17
- *You* do the semantic matching here — the question names a meaning, the map
18
- names areas; connect them by judgment, not string equality. When an area
19
- looks right, `okf index <dir> --area <name>` buys its authored index body and
20
- listing (titles + descriptions) for the price of one directory.
15
+ 2. **Ingest the map and decide where to look.** `okf dirs <dir|@slug>` is the
16
+ skeleton: every directory with what lives under it. *You* do the semantic
17
+ matching here — the question names a meaning, the map names directories;
18
+ connect them by judgment, not string equality. When one looks right, `okf
19
+ index <dir|@slug> --dir <name>` buys its authored index body and listing
20
+ (titles + descriptions) for the price of one directory.
21
21
  <!-- rule:okf-search-map-first -->
22
22
  3. **Cut across with the finder when the question is lexical.** An exact
23
23
  symbol, an error code, a column name, a phrase — things structure won't
@@ -52,7 +52,7 @@ reads, and full bodies are read last, and only the winners.
52
52
  default when you can. <!-- rule:okf-search-fuzzy-is-a-switch -->
53
53
 
54
54
  Scope any of them with what the map taught you:
55
- `--area billing`, `--type Decision`, `--tag idempotency`, `--in body`.
55
+ `--dir billing`, `--type Decision`, `--tag idempotency`, `--in body`.
56
56
  Matches rank by where they hit, and the snippet often *is* the answer.
57
57
  When the answer may live in another registered bundle, span them — leading
58
58
  @slugs (`okf search @handbook @notes <terms>`) or `@all` for every registered
@@ -20,6 +20,12 @@ The surface is self-describing — `okf --help` maps every verb, `okf <verb> --h
20
20
  its flags. Ask the tool for what exists; this file carries only what `--help`
21
21
  cannot: each verb's semantics, its traps, and its JSON shape.
22
22
 
23
+ **The verb list is open, not closed.** An installed extension gem adds verbs of
24
+ its own, listed under `installed extensions:` in `okf help`. So a verb that
25
+ `--help` shows and this file does not document is **normal, not a documentation
26
+ error** — ask `okf <verb> --help` for it, and expect nothing here about its
27
+ semantics or JSON. Everything below documents the built-ins only.
28
+
23
29
  **`--json` is compact by design.** Every emitting verb prints single-line JSON —
24
30
  the token-efficient substrate you consume; `--pretty` (which implies `--json`)
25
31
  indents it for a human. The bytes differ, the JSON is identical, so parse either.
@@ -144,7 +150,7 @@ as Ruby regular expressions with `--regexp`/`-e` (an invalid pattern is a usage
144
150
  error, exit 2). `--fuzzy` forgives typos; pairing it with `-e` is a usage error,
145
151
  since a pattern is matched literally rather than by edit distance.
146
152
  `--in a,b` restricts the searched fields (title, id, tags, type, description,
147
- body); the shared `--type/--area/--tag` filters narrow the candidates *first*,
153
+ body); the shared `--type/--dir/--tag` filters narrow the candidates *first*,
148
154
  so a search scoped by what `index` taught you stays surgical.
149
155
 
150
156
  **The default is exact, so an exact query means what it looks like.** A phrase in
@@ -267,8 +273,30 @@ there, its child directories, and the concept listing. Run it first when picking
267
273
  an existing bundle: it is the cheapest high-signal orientation, and it surfaces
268
274
  enumeration drift a grep can't (you can't grep for a listing entry that is *missing*).
269
275
 
270
- `--area A` narrows to a directory and is **repeatable** — `--area model --area
271
- format` shows both; `root` names the bundle root. `--no-body` drops the prose to a
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. A `--dir` also brings the **chain from the root down to it**, so a branch is
279
+ never shown adrift of the authored context that says what it is — the root
280
+ `index.md`'s prose first among it. Those rows print with a leading `↑` and carry
281
+ `ancestor: true`; `--no-ancestors` drops them. Ascent and descent are separate
282
+ axes, so `--depth` never bounds the chain: `--dir X --depth 0` is X alone, plus
283
+ how you get to X. A `--dir` that names nothing gains no chain — a lone root row
284
+ would read as a partial answer to a query that matched nothing.
285
+
286
+ `--depth N` bounds how far below the starting point the map reaches
287
+ (the `--dir` when one is given, else the bundle root), counted **relatively**:
288
+ `--depth 1` is the top of the tree, `--dir X --depth 1` is one branch of it, and
289
+ the pair walks down a level at a time.
290
+
291
+ **On a bundle of any size the map is unreadable whole** — every directory is a
292
+ section, and even `--no-body` keeps one listing row per *concept* — so narrow
293
+ rather than paging it: `okf dirs` is the orientation, `--dir <branch> --depth 1`
294
+ is the step down into it,
295
+ and `--except body,listing` on top of either is the lean JSON skeleton. Full
296
+ `index` output on a few hundred concepts runs to hundreds of KB; the same map at
297
+ `--depth 1` is a couple of KB.
298
+
299
+ `--no-body` drops the prose to a
272
300
  skeleton (headers, rollups, child pointers). For a directory that has concepts but
273
301
  **no `index.md`**, the listing is **synthesized** from the concepts' descriptions
274
302
  and tagged `(no index.md)` — §6 explicitly permits synthesizing a map on the fly.
@@ -277,7 +305,49 @@ It is a **read view**: advisory, always exit 0. A synthesized directory is a
277
305
  *signal* (a map worth writing), never a defect — `index` emits no lint findings and
278
306
  never fails a bundle. JSON: `{ bundle, count, directories: [{ dir, index_path,
279
307
  present, synthesized, count, types, tags, subdirs, body, listing: [{ id, title,
280
- description, type, tags }] }] }`.
308
+ description, type, tags }] }] }` — `ancestor` marks a row that is there to place
309
+ the branch rather than to answer about it.
310
+
311
+ ## dirs — the bundle's clusters and their sizes
312
+
313
+ `okf dirs <dir>` lists every directory the bundle has — the ones holding
314
+ concepts, the ones carrying an `index.md`, and the empty intermediates that only
315
+ exist to connect the tree — with the number of concepts living **directly** in
316
+ each and the number in its **subtree**. A cluster *is* a directory here, so this
317
+ is the view that tells you what `--dir` can be pointed at and how much sits
318
+ behind each choice.
319
+
320
+ Two numbers, because one cannot answer the question. `count` is direct, so the
321
+ column sums to the bundle's concept total and a dir holding only sub-directories
322
+ reads `0` rather than a hidden rollup. `subtree` is defined as *exactly what
323
+ `--dir <that row>` returns*, so the row and the flag can never disagree — which
324
+ is also why the root's subtree is its own direct count (`.` is a prefix of
325
+ nothing). Without it a truncated listing is all zeroes at the top of a deep tree,
326
+ which is where you most need to know where the mass is. The human table shows the
327
+ second column only where some dir actually nests.
328
+
329
+ `--dir PATH` (repeatable) narrows to a directory and its subtree, and brings the
330
+ **chain up to the root** with it so the branch is placed rather than shown
331
+ adrift — those rows are marked `↑`, carry `ancestor: true`, and stay out of
332
+ `total` (`--no-ancestors` drops them). `--depth N` keeps only N levels below the
333
+ starting point — the `--dir` when one is given,
334
+ the bundle root otherwise. Relative, not absolute, so `--dir a/b --depth 1`
335
+ reads "a/b and one level under it" without your first working out how deep `a/b`
336
+ is. `--depth 0` is the starting point alone. A `--depth` that is not a whole
337
+ number is a usage error (exit 2).
338
+
339
+ **This is the first command to run on a bundle you do not know** — the same first
340
+ move [SKILL.md](../SKILL.md) prescribes. `okf dirs <dir>` is one row per
341
+ directory, so its size tracks the tree rather than the concept count: it tells
342
+ you the shape and where the weight sits, `--depth 1` trims it further on a deep
343
+ bundle, and you then descend with `okf index --dir`, one level at a time.
344
+
345
+ The root prints `(root)` and stores `.` — the split every grouped view keeps, so
346
+ a table and its `--json` never disagree about which spelling is the data. JSON:
347
+ `{ bundle, total, count, dirs: [{ dir, ancestor, count, subtree, subdirs }] }`,
348
+ root first. `count` is rows printed, chain included; `total` sums the direct
349
+ counts of the rows you actually asked for, which is what keeps a row's `subtree`
350
+ equal to the `total` that `--dir` on that row returns.
281
351
 
282
352
  ## catalog / files / tags / types / stats — the server views, as text
283
353
 
@@ -287,7 +357,8 @@ All are advisory reads (exit 0) sharing one data source (per-concept metadata pl
287
357
  in/out link degree). Add `--json` to any for a machine substrate.
288
358
 
289
359
  - **`catalog`** — every concept with its metadata (type, status, tags, timestamp,
290
- in/out link degree, description), grouped by top-level area. The "what's here, in
360
+ in/out link degree, description), grouped by top-level area (`dir` on every row
361
+ carries the full path). The "what's here, in
291
362
  detail" view. JSON: `{ bundle, count, concepts: [{ id, title, type, description,
292
363
  tags, timestamp, status, backlog_ref, dir, area, links_out, links_in }] }`.
293
364
  - **`files`** — the folder tree: each concept's filename + title, grouped by
@@ -295,26 +366,44 @@ in/out link degree). Add `--json` to any for a machine substrate.
295
366
  id, dir, type, title, description }] }`.
296
367
  - **`tags`** — every tag with the concepts that carry it, ordered by count
297
368
  descending. The "what themes dominate" view. JSON: `{ bundle, count, tags: [{ tag,
298
- count, concepts: [id, …] }] }`. `--by type|area` regroups the list per concept
299
- dimension with **within-group** counts (a tag spanning groups appears in each) —
300
- the substrate for tag curation; the judgment recipe lives in the
301
- [maintain playbook](../playbooks/maintain.md). JSON: `{ bundle, count, by,
302
- groups: [{ <dim>, count, tags: […] }] }`.
369
+ count, concepts: [id, …] }] }`. `--by type|dir` regroups the list per concept
370
+ dimension with **within-group** counts (a tag spanning groups appears in each);
371
+ each row also carries the tag's **total** across the narrowed set, printed
372
+ `count/total` when they differ — so a tag's locality reads per row (a plain
373
+ count = wholly local; `2/7` = a cross-cutting spread). The substrate for tag
374
+ curation and for [refine](../playbooks/refine.md)'s domain-vs-concern read;
375
+ the judgment recipes live in the [maintain playbook](../playbooks/maintain.md)
376
+ and the [refine playbook](../playbooks/refine.md). JSON: `{ bundle, count, by,
377
+ groups: [{ <dim>, count, tags: [{ tag, count, total, concepts }] }] }`.
303
378
  - **`types`** — every type with the concepts that carry it, ordered by count
304
379
  descending. The "what kinds of knowledge" view. JSON: `{ bundle, count, types:
305
380
  [{ type, count, concepts: [id, …] }] }`.
306
- - **`stats`** — bundle rollups: concept / area / type / cross-link / distinct-tag
307
- totals plus per-type and per-area breakdowns. The "shape at a glance" view. JSON:
308
- `{ bundle, concepts, areas, concept_types, cross_links, distinct_tags, by_type, by_area }`.
381
+ - **`stats`** — bundle rollups: concept / dir / type / cross-link / distinct-tag
382
+ totals plus per-type and per-dir breakdowns. The "shape at a glance" view. JSON:
383
+ `{ bundle, concepts, dirs, areas, concept_types, cross_links, distinct_tags,
384
+ by_type, by_dir, by_area }` (`areas`/`by_area` are the deprecated first-segment
385
+ cut, kept for one release). `dirs`/`by_dir` cover every directory `okf dirs`
386
+ lists — counts are direct, so a directory holding nothing itself is present at
387
+ `0` rather than missing, and `by_dir.keys` is a complete list of what `--dir`
388
+ can address.
309
389
 
310
390
  The four list views narrow with the same filters the browser panels offer —
311
- `--type TYPE`, `--area AREA`, `--tag TAG`; each takes the ones orthogonal to
312
- itself (`tags` can't filter by tag). Matching is case-insensitive and exact; a
313
- concept at the bundle root lives in the `(root)` area, which `--area` also accepts
314
- as plain `root` (no shell quoting). A filter that matches nothing is an empty view,
315
- not an error: `okf tags <dir> --area billing --json` answers "which tags does the
316
- billing area use?", `okf catalog <dir> --tag auth` answers "what carries the auth
317
- tag?".
391
+ `--type TYPE`, `--dir PATH`, `--tag TAG`; each takes the ones orthogonal to
392
+ itself (`tags` can't filter by tag). Matching is case-insensitive; `--type` and
393
+ `--tag` are exact, `--dir` takes the named directory **and everything below it**
394
+ (`--dir platform` reaches `platform/services/api`). A concept at the bundle root
395
+ lives in `.`, which `--dir` also accepts as plain `root` (no shell quoting). A
396
+ filter that matches nothing is an empty view, not an error: `okf tags <dir> --dir
397
+ billing --json` answers "which tags does the billing cluster use?",
398
+ `okf catalog <dir> --tag auth` answers "what carries the auth tag?".
399
+
400
+ **`--area` is deprecated.** It still works — matching the *first path segment*
401
+ only, its old behavior unchanged — and prints `warning: --area is deprecated, use
402
+ --dir` on stderr (stdout stays clean, so a `--json` consumer is unaffected). Same
403
+ for `tags --by area`. Both go in a later release; write `--dir` in anything new.
404
+ On `index` it combines with neither `--depth` nor `--dir` — exit 2, because it is
405
+ *exact* and both of those select a range, so the pair used to return the area
406
+ plus whatever the other flag selected: an answer to neither question.
318
407
 
319
408
  Reach for `stats` first to size a bundle, `catalog`/`files` to enumerate it, `tags`
320
409
  to find thematic clusters — all without standing up the server.
@@ -330,10 +419,13 @@ in a body render as diagrams, and a click (or tap) opens the diagram full
330
419
  screen with drag-to-pan and wheel/pinch zoom. Concepts render as nodes
331
420
  coloured by `type` and sized by degree, links as edges, with a detail panel
332
421
  (rendered markdown, "Links to" / "Linked from" backlinks), layout switching,
333
- type/area/tag filters on every view, and search. The authored layer is in the
422
+ type/dir/tag filters on every view (the dir chips take a directory *and* its
423
+ subtree, the same rule `--dir` uses), and search. Cluster mode groups the
424
+ concepts into one box per directory, nested to a depth picked beside the layout
425
+ select — depth 1 is the flat view, and a flat bundle is offered no control. The authored layer is in the
334
426
  UI too: the Files view carries **Files | Indexes** tabs — the Indexes tab
335
427
  lists the log first (the chronological index), then every `index.md` — and
336
- folder nodes in file-tree mode and area boxes in cluster mode open a
428
+ folder nodes in file-tree mode and directory boxes in cluster mode open a
337
429
  directory's §6 map in the inspector (authored, or synthesized when none
338
430
  exists). Links to an `index.md`, `log.md`, or bare directory navigate instead
339
431
  of dead-ending, and the log is fetched fresh on every read, so a
@@ -427,3 +519,11 @@ drops each node's body, and `--minimal` ships only `id`/`title` plus the type/ta
427
519
  indexes — the lean shape the `server` page boots from. Reach for the full dump
428
520
  only when the task truly consumes every body; for one question, the
429
521
  [search verb](#search--ranked-text-retrieval-metadata--body) is orders cheaper.
522
+
523
+ `--hubs` swaps the dump for the **inbound ranking**: every concept with at
524
+ least one inbound link, ranked by inbound degree, each with its links grouped
525
+ by *source area* (`core/status ×3 flows 2, billing 1`) — the evidence for
526
+ [refine](../playbooks/refine.md)'s hub origin test ("is this hub well-homed?").
527
+ A source at the bundle root counts under `(root)`. JSON: `{ bundle, count,
528
+ hubs: [{ id, area, inbound, by_area: { <area>: n } }] }`. Advisory read, exit 0;
529
+ `--minimal`/`--no-body` shape node payloads and change nothing here.
data/lib/okf/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OKF
4
- VERSION = "1.9.0"
4
+ VERSION = "1.11.0"
5
5
  end
data/lib/okf.rb CHANGED
@@ -25,6 +25,15 @@ module OKF
25
25
  value.respond_to?(:empty?) ? value.empty? : false
26
26
  end
27
27
 
28
+ # The directory a concept lives in, derived from its §2 id: the id *is* the
29
+ # path minus `.md`, so putting the suffix back and taking the dirname is the
30
+ # definition rather than a parse of it. One home for it because three views
31
+ # answer with a `dir` — the catalog, the search rows, the linter's per-directory
32
+ # checks — and a rule spelled three times is three things to keep in step.
33
+ def self.dir_of(id)
34
+ File.dirname("#{id}.md")
35
+ end
36
+
28
37
  require "okf/version"
29
38
 
30
39
  # ── kernel: cross-cutting primitives ──
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: okf
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.0
4
+ version: 1.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rodrigo Serradura
@@ -54,11 +54,22 @@ dependencies:
54
54
  description: |
55
55
  OKF (Open Knowledge Format) is portable knowledge: Markdown files with YAML
56
56
  frontmatter that both humans and agents read from one source. This gem is the
57
- Ruby-native way to work with it. Its companion agent skill authors and curates
58
- a bundle; the `okf` command-line tool validates the result for v0.1 (§9)
59
- conformance, lints its curation quality, and serves it as an interactive graph
60
- (a mountable Rack app). The same validate, lint, and graph run in-process
61
- through a library API (OKF::Bundle and friends).
57
+ Ruby-native way to work with it.
58
+
59
+ Its companion agent skill authors and curates a bundle. The `okf` command-line
60
+ tool validates the result for v0.1 (§9) conformance, lints its curation
61
+ quality, and answers questions about it: ranked full-text search, and a
62
+ progressive-disclosure map that reads a large bundle a directory at a time
63
+ rather than loading it whole. `okf server` opens it as an interactive
64
+ knowledge graph and `okf render` bakes that same page into one self-contained
65
+ HTML file you can host anywhere. A per-user registry names your bundles, so
66
+ every verb reaches them by @slug from any directory and one search can span
67
+ them all.
68
+
69
+ Everything the CLI does also runs in-process through a library API
70
+ (OKF::Bundle and friends), and the graph server is a mountable Rack app. It
71
+ adds no service to your stack: rack, webrick and minifts are the only runtime
72
+ dependencies, and it runs on every Ruby since 2.4.
62
73
  email:
63
74
  - rodrigo.serradura@gmail.com
64
75
  executables:
@@ -86,6 +97,23 @@ files:
86
97
  - lib/okf/bundle/validator/result.rb
87
98
  - lib/okf/bundle/writer.rb
88
99
  - lib/okf/cli.rb
100
+ - lib/okf/cli/catalog.rb
101
+ - lib/okf/cli/command.rb
102
+ - lib/okf/cli/dirs.rb
103
+ - lib/okf/cli/files.rb
104
+ - lib/okf/cli/graph.rb
105
+ - lib/okf/cli/index.rb
106
+ - lib/okf/cli/lint.rb
107
+ - lib/okf/cli/loose.rb
108
+ - lib/okf/cli/registry.rb
109
+ - lib/okf/cli/render.rb
110
+ - lib/okf/cli/search.rb
111
+ - lib/okf/cli/server.rb
112
+ - lib/okf/cli/skill.rb
113
+ - lib/okf/cli/stats.rb
114
+ - lib/okf/cli/tags.rb
115
+ - lib/okf/cli/types.rb
116
+ - lib/okf/cli/validate.rb
89
117
  - lib/okf/concept.rb
90
118
  - lib/okf/concept/file.rb
91
119
  - lib/okf/markdown/citations.rb
@@ -97,6 +125,7 @@ files:
97
125
  - lib/okf/render/graph/template.html.erb
98
126
  - lib/okf/server/app.rb
99
127
  - lib/okf/server/hub.rb
128
+ - lib/okf/server/hub/not_found.rb
100
129
  - lib/okf/server/runner.rb
101
130
  - lib/okf/skill.rb
102
131
  - lib/okf/skill/SKILL.md
@@ -107,6 +136,7 @@ files:
107
136
  - lib/okf/skill/playbooks/menu.md
108
137
  - lib/okf/skill/playbooks/migrate.md
109
138
  - lib/okf/skill/playbooks/produce.md
139
+ - lib/okf/skill/playbooks/refine.md
110
140
  - lib/okf/skill/playbooks/search.md
111
141
  - lib/okf/skill/reference/APACHE-2.0.txt
112
142
  - lib/okf/skill/reference/SPEC.md
@@ -142,6 +172,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
142
172
  requirements: []
143
173
  rubygems_version: 4.0.16
144
174
  specification_version: 4
145
- summary: 'The complete toolkit for the Open Knowledge Format: an agent skill, a CLI
146
- and library, and a live knowledge graph. 100% local.'
175
+ summary: 'The complete Open Knowledge Format toolkit: an agent skill, a CLI and library,
176
+ ranked search, and a live knowledge graph. 100% local.'
147
177
  test_files: []