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.
@@ -0,0 +1,70 @@
1
+ # `registry` — naming bundles once
2
+
3
+ Kind: reference. Answers: which file a registry op writes and how okf finds it,
4
+ which verbs key on a path and which on a slug, what a group is and which two
5
+ verbs consume one, and why the default is a position rather than a stored slug.
6
+
7
+ The *persistent registry* is a plain JSON file under `$OKF_HOME` (default
8
+ `~/.okf`), managed by the `okf registry` umbrella — like git's `remote` family,
9
+ and split by what each verb keys on. It is what every `@slug` resolves through
10
+ ([cli.md](../cli.md)) and what a bundle-less `okf server` hosts
11
+ ([serve.md](serve.md)).
12
+
13
+ **`okf registry init`** creates a *project-local* registry instead: a
14
+ `.okf-registry.json` in the current directory, which okf discovers by walking up
15
+ from the working directory and uses in place of the global one while you are
16
+ inside its tree (the nearest wins, so nested registries resolve nearest-first).
17
+ Every registry op — and every `@slug` — then resolves through it, so a bare
18
+ `okf server` inside a repo serves that repo's bundles with no `$OKF_HOME` setup;
19
+ `okf registry list` names the local file it found. `OKF_NO_DISCOVERY=1` forces
20
+ the global registry — the escape hatch for a fixed-cwd caller (CI, a tool). A
21
+ local registry stores **portable** paths: a bundle inside its tree is written
22
+ relative to the `.okf-registry.json`, so committing the file lets it travel with
23
+ the repo (a checkout elsewhere, a container mounting it) and resolve unchanged;
24
+ a bundle outside the tree stays absolute, since it cannot travel. Paths still read
25
+ back absolute wherever the CLI reports them.
26
+ **Entry verbs** take a path: `okf registry set <dir>` adds it
27
+ (slug from the basename, or `--as`, which errors on a collision; `--default`
28
+ puts it first), and because the entry is keyed by path, `set` on an
29
+ already-registered dir updates it in place — refreshing its title, and renaming
30
+ it when `--as` is given. `okf registry del <dir|@slug>` removes a bundle *or* a group — by name, so an
31
+ entry whose directory is already gone still deletes, and removing a bundle
32
+ **cascade-drops** it from every group that named it (a group emptied that way is
33
+ deleted). Slug *or* dir, never both readings at
34
+ once: an argument with a `/` in it names a location and only a location, so
35
+ `del ./notes` refuses when no entry points there rather than stripping to the
36
+ slug `notes` and deleting a bundle somewhere else entirely.
37
+ <!-- rule:okf-registry-del-path-or-slug -->
38
+ **Slug verbs** take the name — bare, or as an `@slug`: `okf registry default <@slug>`
39
+ chooses which bundle `/` opens **by moving that entry to the front** (a group is
40
+ refused — the default is one bundle), and
41
+ `okf registry rename <@slug> <new>` renames a bundle *or* group slug (mount path
42
+ and switcher name), **cascading** the new name into every group's member list —
43
+ `<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
44
+ default** — that is the whole rule, so the first bundle you register is the
45
+ default until you move another one, a rename keeps its position, and a `del`
46
+ promotes whatever is next. A vanished directory is stepped over (the server
47
+ cannot open one, so starring it would name a bundle `/` never serves), and
48
+ `registry default @slug` refuses one outright — the same refusal `registry set`
49
+ gives a directory that is not there. The file is hand-editable and reorders
50
+ visibly, which is the point: there is no stored slug that can dangle.
51
+ <!-- rule:okf-registry-default-position -->
52
+ **Group verbs** name a *set* of bundles under one slug — a durable subset for the
53
+ two verbs that take several bundles. `okf registry group <slug> <@member…>`
54
+ creates a group, or adds members to one (a union); members are bundle *or* group
55
+ slugs, so groups nest. A group shares the slug namespace with bundles (a slug
56
+ names one *or* the other, never both), `all` stays reserved, and a member set that
57
+ would make the group reach itself is refused. `okf registry ungroup <slug>
58
+ <@member…>` removes members; emptying a group deletes it. A group resolves,
59
+ recursively and path-deduped, to its bundle leaves — which **only `okf search`
60
+ and `okf server` consume**: every single-bundle verb (`lint`, `index`, …) refuses
61
+ a `@group` with exit 2, the same rule that refuses a second bundle. `@all` is
62
+ unchanged — it still names every registered *bundle*, groups being named subsets
63
+ of that.
64
+ `okf registry list` (or a bare
65
+ `okf registry`) stars the default and flags vanished dirs `(missing)` — the
66
+ server skips those with a note — and lists any groups with their members and
67
+ resolved leaf count; `--json` answers
68
+ `{ registry: <file>, count, bundles: [{ slug, title, dir, mount, default,
69
+ missing }], groups: [{ slug, members, resolved }] }`, naming the file it read so a
70
+ `$OKF_HOME` mismatch is visible.
@@ -0,0 +1,130 @@
1
+ # `search` — ranked text retrieval (metadata + body)
2
+
3
+ Kind: reference. Answers: how terms are matched and by which engine, what the
4
+ index buys and what it silently loses, how one query spans several bundles, and
5
+ which of the two JSON envelopes comes back.
6
+
7
+ The shared contract — `@slug` refs, exit codes, `--json`, the filters this verb
8
+ also takes — is in [cli.md](../cli.md) and is not repeated here.
9
+
10
+ The browser page's search brought to the CLI and extended to bodies, so "which
11
+ concept covers X?" costs rows, not body reads. `okf search <dir> <term…>`:
12
+ terms AND together — every term must hit at least one searched field, not
13
+ necessarily the same one — matched **literally against raw text** by default, or
14
+ as Ruby regular expressions with `--regexp`/`-e` (an invalid pattern is a usage
15
+ error, exit 2). `--fuzzy` forgives typos; pairing it with `-e` is a usage error,
16
+ since a pattern is matched literally rather than by edit distance.
17
+ `--in a,b` restricts the searched fields (title, id, tags, type, description,
18
+ sources, body — `sources` is each entry's title and resource joined, so a
19
+ migrated bundle keeps the recall its `# Citations` body text used to give it);
20
+ the shared `--type/--dir/--tag/--status/--trust` filters narrow the candidates
21
+ *first*, so a search scoped by what `index` taught you stays surgical.
22
+
23
+ **The default is exact, so an exact query means what it looks like.** A phrase in
24
+ one argument (`"dedup key"`), a dotted version (`7.2.0`), an underscored
25
+ identifier (`customer_id`), a mid-word fragment (`ustomer`) and a word written in
26
+ `backticks` all match literally. This is what the scan engine buys, and it is the
27
+ default precisely because those queries are the common ones and the alternative
28
+ loses them silently. <!-- rule:okf-search-exact-identifiers -->
29
+
30
+ **`--engine index` is the other engine, and the one to reach for when ranking
31
+ matters more than exactness.** The engine is normally chosen by what the query
32
+ needs — `--fuzzy` routes to the index, anything else stays on the default scan —
33
+ and nothing is printed about the choice. `--engine NAME` overrides that for the
34
+ case the flags cannot express: a matching *model* requires no capability, so no
35
+ flag selects one. Under the index, terms match whole tokens and their prefixes
36
+ (`dedup` finds `deduplication`), rows rank by BM25+, and it is the engine the
37
+ browser page runs — so name it when reconciling a CLI answer with the page. The
38
+ cost is real: its tokenizer splits on punctuation, so identifiers shatter
39
+ (`customer_id` → `customer` + `id`), an infix finds nothing, and a backtick is
40
+ never split off at all, so a word inside a code span is unfindable — a large
41
+ silent loss, since technical prose is full of them. **Do not count on ranking to
42
+ rescue it** — BM25 normalizes by field length, so a short concept dense in `7`,
43
+ `2` and `0` can outrank the one that actually says `7.2.0`. Naming an engine that
44
+ cannot do what you also asked (`--engine index -e`) is a usage error naming one
45
+ that can. <!-- rule:okf-search-engine-choice -->
46
+
47
+ **The capabilities, and which engine has them.** An engine is selected by what
48
+ the query *requires*; only a matching model has to be named, because requiring
49
+ nothing is not something a flag can express:
50
+
51
+ | Flag | Capability | Engine | What it does |
52
+ |---|---|---|---|
53
+ | *(none)* | — | scan | literal substring over raw text; scores by summed field weight |
54
+ | `-e` / `--regexp` | `regexp` | scan | each term is a Ruby regexp, case-insensitive; invalid → exit 2 |
55
+ | `--fuzzy` | `fuzzy` | **index** | edit distance 0.2 × term length — and switches engine |
56
+ | `--engine index` | — | index | whole-token + prefix matching, BM25+ ranking, browser parity |
57
+ | `--engine scan` | — | scan | the default, spelled out |
58
+
59
+ Two consequences worth holding. **`--fuzzy` is an engine switch, not a mode**: it
60
+ carries the whole index with it, so a run that wanted one typo forgiven also gets
61
+ shattered identifiers and unfindable code spans — fix the spelling and stay on
62
+ the default when you can. And **`-e` moves nothing** now, because the default
63
+ engine already offers `regexp`; it changes how a term is *read* (pattern rather
64
+ than literal), not where it is matched. <!-- rule:okf-search-fuzzy-is-a-switch -->
65
+
66
+ `prefix` is a capability the index declares but no flag selects — it is always on
67
+ there. **It is not a reason to reach for the index**: a substring match already
68
+ covers every prefix and then some, so `dedup` finds `deduplication` under both
69
+ engines, while `duplication` and `uplicat` find it under the default only. Prefix
70
+ is what the index needs to catch up to raw text, not a capability it adds on top.
71
+ The index's real advantages over the default are exactly three — relevance
72
+ ranking, typo tolerance, and page parity.
73
+
74
+ **Search spans bundles.** Leading @refs pick several registered bundles
75
+ (`okf search @handbook @notes auth`); **`@all`** is the ref that means every one.
76
+ Rows from different bundles are ranked together and comparable, and each row
77
+ carries its bundle's slug. Under `--engine index` the bundles go into **one
78
+ corpus** — BM25 prices a term by how rare it is, so separately-ranked lists would
79
+ not compare — which makes a score relative to the whole answer: the same concept
80
+ scores lower searched beside others than searched alone. The default scan needs
81
+ no such trick — its score is absolute, so a row is worth the same either way.
82
+ This is the
83
+ cross-bundle retrieval the in-page search does not have: one question, every
84
+ bundle you keep. <!-- rule:okf-search-all -->
85
+
86
+ `@all` is a ref, not a flag, which is what keeps the grammar single: slot 1 is
87
+ always a bundle identity, so a directory there is a directory and nothing can
88
+ flip it into a term. Being a ref, it is normalized like one — `@ALL` and `@All`
89
+ name every bundle just as `@One` names the bundle registered from dir `One`. It composes accordingly — `@all @docs` expands and dedupes
90
+ (all ⊇ docs), needing no diagnostic. **Asking for everything tolerates gaps;
91
+ naming one bundle demands it**: `@all` skips a bundle whose directory has
92
+ vanished with a note on stderr, while `@docs` fails hard. `@all` is only
93
+ `search`'s: every other verb answers about one bundle, so it refuses `@all` by
94
+ name rather than letting the answer depend on how many bundles you happen to
95
+ have registered. `all` is reserved as a slug — a directory named `all/` registers
96
+ as `all-2`, `--as all` is refused, and an `all` row already in the registry file
97
+ (hand-typed, or written before the name was reserved) is read as `all-2` rather
98
+ than taken as grounds to reject the file — so `@all` is never ambiguous, and the
99
+ reservation never strands a registry it inherited. **The read normalizes every
100
+ slug** the same way registration would, so a hand-typed `"slug": "My Docs"` lists
101
+ and resolves as `my-docs`; an entry the listing shows is always an entry `@slug`,
102
+ `rename`, and `default` can name.
103
+
104
+ `--fields` projects the shape the mode actually emits: `slug` is available in
105
+ registry mode, and a usage error naming the real fields on a path-named search,
106
+ which has no slug to give. Two sharp edges: every *leading* @-arg is taken as a ref, so a literal @-term
107
+ (`@babel/core`, a Ruby `@ivar`) needs a non-@ term before it or `-e '\@term'` —
108
+ the CLI notes both traps on stderr — and any ref, even one, switches the JSON
109
+ envelope (next paragraph).
110
+
111
+ Rows rank by where they hit — title 5, id 4, tags 3, type/description 2, body 1 —
112
+ summed as an absolute score by the default scan, and carried as per-field boost
113
+ into **BM25+** under `--engine index`. Each row carries one bounded context
114
+ snippet from the strongest match that needs context (description or body). Every row still names the fields that hit (`matched`), so a result stays
115
+ citable rather than being a bare relevance number. Exact by default: the
116
+ consuming agent is the fuzzy layer — when terms miss, learn the bundle's
117
+ vocabulary from `tags`/`types` and re-ask in its own words, rather than
118
+ hammering synonyms or reaching for `--fuzzy` before you have looked. Advisory read: **exit 0 even with zero matches**.
119
+ JSON, plain-dir mode: `{ bundle, query, count, matches: [{ id, title, type,
120
+ dir, top_dir, tags, matched, score, snippet }] }`. Registry mode — any leading @ref,
121
+ `@all` or a `@group` among them (a group fans out to its member bundles) — swaps the envelope: `{ bundles: [{ slug, dir }, …],
122
+ query, count, matches: [{ slug, id, … }] }`; a parser must branch on which form
123
+ it called. The head maps each slug to its dir once, so a row resolves to
124
+ `<dir>/<id>.md` without a second lookup and without repeating a path per row.
125
+ Both are projectable with `--fields/--except`, and projection is literal — when
126
+ merging bundles, put `slug` in your `--fields` list or the row label drops and
127
+ same-id concepts from different bundles become indistinguishable. The retrieval procedure that puts this verb in sequence —
128
+ map first, finder second, bodies last — is the
129
+ [search playbook](../../playbooks/search.md).
130
+
@@ -0,0 +1,83 @@
1
+ # Serving the graph — `server` and `render`
2
+
3
+ Kind: reference. Answers: what the interactive page shows and what it fetches
4
+ live, how several bundles mount behind one hub, what the static export trades
5
+ away, and the trust boundary both modes share.
6
+
7
+ The shared contract — `@slug` refs, exit codes, `--json` — is in
8
+ [cli.md](../cli.md) and is not repeated here.
9
+
10
+ ## server — interactive graph server
11
+
12
+ Starts a local HTTP server (`okf server <dir>`; `-p`/`--port`, default 8808, and
13
+ `--bind`) and prints its URL — stop it with Ctrl-C. The page boots from a lean
14
+ payload (nodes carry only `id` and `title`, plus compact type/tag indexes) and
15
+ fetches each concept's markdown body **live from disk** as you click it, so the
16
+ initial load stays small and edits show without a restart. Mermaid code blocks
17
+ in a body render as diagrams, and a click (or tap) opens the diagram full
18
+ screen with drag-to-pan and wheel/pinch zoom. Concepts render as nodes
19
+ coloured by `type` and sized by degree, links as edges, with a detail panel
20
+ (rendered markdown, "Links to" / "Linked from" backlinks), layout switching,
21
+ type/dir/tag filters on every view (the dir chips take a directory *and* its
22
+ subtree, the same rule `--dir` uses), and search. Cluster mode groups the
23
+ concepts into one box per directory, nested to a depth picked beside the layout
24
+ select — depth 1 is the flat view, and a flat bundle is offered no control. The authored layer is in the
25
+ UI too: the Files view carries **Files | Indexes** tabs — the Indexes tab
26
+ lists the log first (the chronological index), then every `index.md` — and
27
+ folder nodes in file-tree mode and directory boxes in cluster mode open a
28
+ directory's §8 map in the inspector (authored, or synthesized when none
29
+ exists). Links to an `index.md`, `log.md`, or bare directory navigate instead
30
+ of dead-ending, and the log is fetched fresh on every read, so a
31
+ just-appended entry shows without a restart. `?view=index` jumps straight to
32
+ the Indexes tab. It is a Rack app, so the same server can be mounted in a
33
+ host app (e.g. Rails).
34
+
35
+ **Hosting many bundles (the hub).** `okf server` takes zero or more dirs.
36
+ One dir is the classic single bundle at `/`. Two or more mounts each under
37
+ `/b/<slug>/` behind a hub, `/` redirects to the default, and `/b/` is a
38
+ self-contained **bundle index** (every hosted bundle, concept counts, default
39
+ marked — the browser counterpart of `okf registry`). An unknown slug 404s as a
40
+ page listing the hosted bundles, so a stale bookmark after a rename gets a way
41
+ home. With **no** dir it serves the *persistent registry*. The hub roster is a
42
+ **boot-time snapshot**: restart `okf server` after registry changes. Behind a
43
+ hub the page gains a **bundle switcher** (⌘/Ctrl-K, or the rail button with its
44
+ bundle-count badge): the current bundle is pinned, the default chipped; ⏎
45
+ opens, ⌘/Ctrl-⏎ opens a new tab, and the current view carries over. Switching
46
+ is a server-only affordance — a static `render` file has no siblings and shows
47
+ none.
48
+
49
+ **Bundle-less run.** Register bundles once, then `okf server` (no dir) hosts
50
+ them all with the registry's first entry still on disk at `/` — the way to keep
51
+ several bundles a keystroke apart without re-passing paths.
52
+ `okf server @a @b` serves a registry subset, each mounted under its registered
53
+ slug — but as with any dirs-given run, the *first argument* lands at `/`; the
54
+ registry's own order applies only to the bundle-less run. A `@group` argument
55
+ fans out to its member bundles in the same way (`okf server @backend`), its first
56
+ member landing at `/`; `okf search @group <term…>` merges the group's members
57
+ into one ranking, exactly as naming them individually would.
58
+ The registry itself — the file it reads, how okf finds it, and the verbs that
59
+ write it — is in [registry.md](registry.md).
60
+
61
+ **Trust boundary:** the page renders each fetched markdown body through
62
+ DOMPurify and escapes everything it inlines (every `<` in the graph data is
63
+ escaped, so it cannot break out of its `<script>`), but it still loads its
64
+ viewer libraries (Cytoscape, marked, DOMPurify — plus Mermaid and Panzoom,
65
+ lazy-loaded on first use) from a CDN and renders whatever
66
+ links the bundle carries — so only serve bundles you trust.
67
+ ## render — static graph export
68
+
69
+ Writes the same interactive page as one static, self-contained HTML file
70
+ (`okf render <dir>`), so the graph hosts where there is no server — GitHub Pages,
71
+ an object store, an attachment. Prints to stdout (`okf render <dir> > graph.html`)
72
+ or writes `-o FILE`; `--title`/`--link`/`--layout` mirror `server`. It is the same
73
+ template `server` renders, one switch apart: rather than fetching each body,
74
+ description, catalog, index, and log live, `render` bakes the whole bundle into
75
+ the page and the browser reads from that embedded payload — no server, no build
76
+ step. The trade-off is weight (every body is inlined), so `server` stays the
77
+ choice for a bundle too large to ship whole.
78
+
79
+ **Trust boundary:** the same two guards as `server` — every inlined body is
80
+ `</script>`-escaped like the graph data and still sanitized by DOMPurify when
81
+ rendered — so a static file is no laxer than the live server. Only render bundles
82
+ you trust.
83
+
@@ -0,0 +1,59 @@
1
+ # The server views as text — `catalog`, `files`, `tags`, `types`, `stats`
2
+
3
+ Kind: reference. Answers: which of the five reads a question wants, what each
4
+ row carries, and the JSON each emits.
5
+
6
+ The filters all five narrow with — `--type`, `--dir`, `--tag`, `--status`,
7
+ `--trust` — are in [cli.md](../cli.md) with the rest of the shared contract,
8
+ since `search` takes them too.
9
+
10
+ The browser server ([serve.md](serve.md)) has Catalog, Files, Tags and Stats
11
+ panels; these
12
+ verbs reproduce them on the CLI so an agent can read a bundle without a browser.
13
+ All are advisory reads (exit 0) sharing one data source (per-concept metadata plus
14
+ in/out link degree). Add `--json` to any for a machine substrate.
15
+
16
+ - **`catalog`** — every concept with its metadata (type, status, trust, tags,
17
+ provenance, in/out link degree, description), grouped by top-level dir (`dir`
18
+ on every row carries the full path, `top_dir` the first segment). The "what's
19
+ here, in detail" view. JSON: `{ bundle, count, concepts: [{ id, title, type,
20
+ description, tags, generated_at, generated_by, generated, trust, status,
21
+ stale_after, sources, backlog_ref, dir, top_dir, links_out, links_in }] }`.
22
+ Four of those deserve a sentence: `generated` is the raw boolean ("does the
23
+ document *declare* a generated mapping"), which is what tells hand-written
24
+ apart from v0.1-with-timestamp — `generated_at` alone conflates them, because
25
+ §13.1 lifts a legacy `timestamp` into it. `trust` is the derived §5.3 tier as
26
+ a hyphenated literal (`unverified` | `machine-confirmed` | `human-reviewed`) —
27
+ compare against exactly those. `status` is the *declared* value, `null` when
28
+ absent (the row never fabricates frontmatter; the `--status` filter is what
29
+ applies the §5.4 default). `sources` is a count. Temporal fields render
30
+ ISO 8601 (`stale_after` as `YYYY-MM-DD`). The `timestamp` column is retired —
31
+ `--fields timestamp` is a usage error naming the valid fields.
32
+ - **`files`** — the folder tree: each concept's filename + title, grouped by
33
+ directory. The "how it's organised" view. JSON: `{ bundle, count, files: [{ path,
34
+ id, dir, type, title, description }] }`.
35
+ - **`tags`** — every tag with the concepts that carry it, ordered by count
36
+ descending. The "what themes dominate" view. JSON: `{ bundle, count, tags: [{ tag,
37
+ count, concepts: [id, …] }] }`. `--by type|dir` regroups the list per concept
38
+ dimension with **within-group** counts (a tag spanning groups appears in each);
39
+ each row also carries the tag's **total** across the narrowed set, printed
40
+ `count/total` when they differ — so a tag's locality reads per row (a plain
41
+ count = wholly local; `2/7` = a cross-cutting spread). The substrate for tag
42
+ curation and for [refine](../../playbooks/refine.md)'s domain-vs-concern read;
43
+ the judgment recipes live in the [maintain playbook](../../playbooks/maintain.md)
44
+ and the [refine playbook](../../playbooks/refine.md). JSON: `{ bundle, count, by,
45
+ groups: [{ <dim>, count, tags: [{ tag, count, total, concepts }] }] }`.
46
+ - **`types`** — every type with the concepts that carry it, ordered by count
47
+ descending. The "what kinds of knowledge" view. JSON: `{ bundle, count, types:
48
+ [{ type, count, concepts: [id, …] }] }`.
49
+ - **`stats`** — bundle rollups: concept / dir / type / cross-link / distinct-tag
50
+ totals plus per-type and per-dir breakdowns. The "shape at a glance" view. JSON:
51
+ `{ bundle, concepts, dirs, top_dirs, concept_types, cross_links, distinct_tags,
52
+ by_type, by_dir, by_top_dir }` (`top_dirs`/`by_top_dir` are the first-segment
53
+ rollup). `dirs`/`by_dir` cover every directory `okf dirs`
54
+ lists — counts are direct, so a directory holding nothing itself is present at
55
+ `0` rather than missing, and `by_dir.keys` is a complete list of what `--dir`
56
+ can address.
57
+
58
+ Reach for `stats` first to size a bundle, `catalog`/`files` to enumerate it, `tags`
59
+ to find thematic clusters — all without standing up the server.