okf 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.okf/capabilities/bundles-manager.md +11 -0
- data/.okf/capabilities/search.md +3 -1
- data/.okf/cli.md +31 -10
- data/.okf/log.md +62 -0
- data/.okf/registry.md +170 -3
- data/.okf/structure/the-cli.md +1 -1
- data/.okf/structure/the-disk-shell.md +11 -2
- data/.okf/testing/adding-a-verb.md +1 -1
- data/CHANGELOG.md +105 -0
- data/README.md +33 -4
- data/lib/okf/cli/command.rb +8 -3
- data/lib/okf/cli/registry.rb +306 -47
- data/lib/okf/cli.rb +1 -1
- data/lib/okf/registry.rb +448 -22
- data/lib/okf/render/graph/template.html.erb +6 -2
- data/lib/okf/server/hub.rb +6 -2
- data/lib/okf/skill/reference/cli/registry.md +49 -6
- data/lib/okf/skill/reference/cli.md +1 -1
- data/lib/okf/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0aee1cdfc7b13016c423050f98de4994431fa68ae4afe3869b5555b5408c3315
|
|
4
|
+
data.tar.gz: '058369e3e6131e6af6de0b3e599bdc7716e3296f0ff1caacd59f905088e5c4ce'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7d91fe567c067d175cfa3274773e984720384315d2eb8fb790da247ee256706b48e079ba4a4d9e5b909a4e6f480f968574d4526e6d6b80b4e443b57785d9ac36
|
|
7
|
+
data.tar.gz: e075baf062df84f4567200cfe4d375280dcf5cd9b76aad7d9802d1ae28cc00245982c6604e7e5fea080b15c47cee0644c5f75b7e2468379fc386de985ad1fe45
|
|
@@ -53,6 +53,17 @@ red. The verdict keeps the [validate/lint separation](validator.md): a curation
|
|
|
53
53
|
finding is a warning and the bundle stays open, and only a non-conformant bundle
|
|
54
54
|
reads as broken.
|
|
55
55
|
|
|
56
|
+
A bundle that arrived through a registry [link](../registry.md) is listed,
|
|
57
|
+
mounted and served like any other, and is **read-only**: the file that owns it is
|
|
58
|
+
another registry, so the model refuses `rename`, `remove` and `default` against
|
|
59
|
+
it. The page carries that in both halves, because one without the other is a
|
|
60
|
+
worse page than neither. The row shows `via @onm` and **offers no actions menu** —
|
|
61
|
+
three buttons that can only fail are not a menu — and the request is refused
|
|
62
|
+
anyway if it arrives, since the guard lives in `Registry` rather than here. That
|
|
63
|
+
is the same "hiding a button is a UI, refusing the request is the boundary" split
|
|
64
|
+
the read-only gate above keeps, reached from the other direction: there the
|
|
65
|
+
server declines to write at all, here one row does.
|
|
66
|
+
|
|
56
67
|
Rows are matched to entries by **directory**, not by slug. A rename changes the
|
|
57
68
|
slug and nothing else, and a row that lost its identity over a rename is the bug
|
|
58
69
|
that avoids.
|
data/.okf/capabilities/search.md
CHANGED
|
@@ -225,7 +225,9 @@ even with zero matches — only an invalid `--regexp` pattern, `-e` paired with
|
|
|
225
225
|
Knowledge rarely lives in one bundle, so search is the one verb that spans the
|
|
226
226
|
[registry](../registry.md): leading @slugs pick bundles explicitly
|
|
227
227
|
(`okf search @handbook @notes auth`), and `@all` is the ref that means every
|
|
228
|
-
registered one
|
|
228
|
+
registered one — including the bundles a [link](../registry.md) folds in, since
|
|
229
|
+
those are registered here in every sense that matters to a reader looking for an
|
|
230
|
+
answer. Every row is labeled by its bundle's slug.
|
|
229
231
|
|
|
230
232
|
**Merged rows are comparable by construction, and each engine earns that
|
|
231
233
|
differently.** The scan's score is absolute — summed field weights, with no
|
data/.okf/cli.md
CHANGED
|
@@ -74,21 +74,42 @@ side should not enrol them in the user's durable list. Registering is always the
|
|
|
74
74
|
explicit act of `okf registry set`.
|
|
75
75
|
|
|
76
76
|
`registry` is an umbrella verb — `init`, `list`, `set`, `del`, `default`,
|
|
77
|
-
`rename`, `group`, `ungroup` — over one persistent
|
|
77
|
+
`rename`, `group`, `ungroup`, `link`, `unlink`, `import` — over one persistent
|
|
78
|
+
file, and it
|
|
79
|
+
is **one row in the map**. It carried ten for a while, which made a third of `okf
|
|
80
|
+
help` about registry management when registry management is not a third of what
|
|
81
|
+
okf does; the row now points at `okf registry --help`, which prints the ten with
|
|
82
|
+
their grammar and the `-g` they share. The map's job is to name the verbs. A verb
|
|
83
|
+
whose own help is the manual is the same progressive disclosure the bundles are
|
|
84
|
+
built on, applied to the CLI's front page — and the row has to *say* the help
|
|
85
|
+
exists, since nothing else tells a reader an umbrella has subcommands at all. `$OKF_HOME` points every
|
|
78
86
|
one of them at a different global registry, which is what keeps the tests off the
|
|
79
87
|
real `~/.okf`; `registry init` and discovery add a [project-local](registry.md#global-by-default-project-local-by-discovery)
|
|
80
88
|
one that replaces it while you stand in its tree, with `OKF_NO_DISCOVERY=1` to
|
|
81
89
|
force the global one.
|
|
82
90
|
|
|
83
|
-
One lever, not two
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
## One lever, not two
|
|
92
|
+
|
|
93
|
+
A *cross-cutting* lever is an env var, never a flag. An earlier design carried a
|
|
94
|
+
`--home DIR` flag, which had to be remembered on the three verbs that offered it
|
|
95
|
+
and forgotten on every other one — a flag whose whole job was to name a location
|
|
96
|
+
the env var already named. `$OKF_HOME` composes where a flag cannot: it reaches
|
|
97
|
+
every verb at once without being typed, it survives into a subprocess, and if the
|
|
98
|
+
directory ever holds more than `registry.json` it keeps meaning the same thing.
|
|
99
|
+
`OKF_NO_DISCOVERY` earns its keep the same way, and this is still why there is no
|
|
100
|
+
`--global` on `lint`, on `search`, or on the eleven others: a per-verb flag for
|
|
101
|
+
resolution would be fourteen places to thread it and fourteen to forget.
|
|
102
|
+
|
|
103
|
+
The `registry` umbrella is the exception, and the line is *subject*, not
|
|
104
|
+
convenience. Every other verb takes a bundle and merely happens to resolve a name
|
|
105
|
+
through a registry; `registry`'s argument **is** a registry file. Saying which one
|
|
106
|
+
is therefore an argument to that verb in the way `--as` is, not a lever bolted
|
|
107
|
+
across the CLI — so `-g`/`--global` lives on its subcommands (all but `init`,
|
|
108
|
+
whose whole job is to create a *local* file) and nowhere else. The env var is
|
|
109
|
+
still the right answer for a whole session or a CI job; the flag is the right
|
|
110
|
+
answer for one command, and for the plain fact that a capability reachable only
|
|
111
|
+
through an env var is one most people never discover. Both name the same
|
|
112
|
+
behavior, so neither can drift from the other.
|
|
92
113
|
|
|
93
114
|
# Every output names its bundle, in the identity the caller used
|
|
94
115
|
|
data/.okf/log.md
CHANGED
|
@@ -1,5 +1,67 @@
|
|
|
1
1
|
# Update Log
|
|
2
2
|
|
|
3
|
+
## 2026-08-22
|
|
4
|
+
|
|
5
|
+
* **The registry can now take a row out of another registry file, and the
|
|
6
|
+
project-local file has a shorter name.** [`registry
|
|
7
|
+
import`](/registry.md#import-the-copy-that-owns-what-it-takes) is the opposite
|
|
8
|
+
trade from `link`: a link holds a live pointer to a whole file and the other
|
|
9
|
+
side keeps owning what it lends, while an import copies chosen references and
|
|
10
|
+
hands over ownership. Both are needed because they answer different questions —
|
|
11
|
+
"compose that repository's curation" against "I want *that one bundle*, here,
|
|
12
|
+
as mine". Without it the second had one answer: read the path out of `okf
|
|
13
|
+
registry list -g` and retype it into `registry set`, laundering through the
|
|
14
|
+
clipboard information already on screen.
|
|
15
|
+
|
|
16
|
+
Three decisions did the work. The source is `--from`, not a second meaning for
|
|
17
|
+
`-g`, because `-g` names the registry acted *on* across all ten sibling
|
|
18
|
+
subcommands and a verb that names two files should name the second rather than
|
|
19
|
+
invert the first. Slugs are preserved and a collision **refuses**, which is
|
|
20
|
+
where import and link deliberately disagree: a linked name was never chosen
|
|
21
|
+
here so `link_slug` mints around it, an imported one was typed so the "never
|
|
22
|
+
substitute a name you chose" rule applies in full. And a group brings the
|
|
23
|
+
groups nested inside it, because recreating a name here that resolved to a
|
|
24
|
+
larger set there is that same substitution with nothing on screen to reveal it.
|
|
25
|
+
Everything is validated before anything is written, so an import lands whole or
|
|
26
|
+
leaves the file byte-for-byte alone — a refusal that already moved three of four
|
|
27
|
+
rows is not a refusal.
|
|
28
|
+
|
|
29
|
+
* **`.okf.json` is the project-local registry's name; `.okf-registry.json` is
|
|
30
|
+
still discovered.** The file is committed, so retiring the old name outright
|
|
31
|
+
would break every repository carrying one to save eight characters. Both are
|
|
32
|
+
checked *per directory* on the way up rather than one name swept to the root
|
|
33
|
+
and then the other, or a legacy file at a repo root would beat a `.okf.json`
|
|
34
|
+
two levels down and "the nearest one wins" would quietly mean something else.
|
|
35
|
+
The deprecation note is the `registry` umbrella's alone — the one verb whose
|
|
36
|
+
subject is a registry file, and the one nobody runs in a loop or pipes into
|
|
37
|
+
something else, which is exactly what `lint` and `search` are.
|
|
38
|
+
|
|
39
|
+
## 2026-08-21
|
|
40
|
+
|
|
41
|
+
* **The [registry](/registry.md) composes other registry files, and its umbrella
|
|
42
|
+
grew a `-g`.** A repository that curates its own bundles — this one commits a
|
|
43
|
+
`.okf-registry.json` naming five — could not lend that curation to `~/.okf`
|
|
44
|
+
without registering every row again by hand and re-syncing forever. `okf
|
|
45
|
+
registry link <name> <file>` points the global registry at the file instead:
|
|
46
|
+
its bundles resolve here at read time under their own slugs, `@<name>` is the
|
|
47
|
+
set, and nothing is copied, so the target goes on owning its rows. Two
|
|
48
|
+
restrictions carry the whole design. Only the *global* registry follows links,
|
|
49
|
+
which makes depth one structural rather than enforced — a linked file's own
|
|
50
|
+
links are never read, so no chain forms and there is no cycle to detect. And a
|
|
51
|
+
link is read-only: `rename`, `del`, `default`, `set --as` and `group` all
|
|
52
|
+
refuse a slug it owns, from the `⚙ Bundles` panel as from the terminal, because
|
|
53
|
+
the guard lives in the model rather than in the [CLI](/cli.md).
|
|
54
|
+
|
|
55
|
+
A linked name is minted around a collision rather than refused (`central` →
|
|
56
|
+
`onm-central`), which is the "implicit is forgiving" rule reaching a name this
|
|
57
|
+
registry did not choose — and it is the design's one *computed* slug, so
|
|
58
|
+
`registry list` prints the moved row with the name it carries in its source
|
|
59
|
+
file. `-g`/`--global` is the smaller half: `OKF_NO_DISCOVERY=1` already forced
|
|
60
|
+
the global registry, but a lever reachable only through an env var is one most
|
|
61
|
+
people never find. It is the `registry` umbrella's alone — the one verb whose
|
|
62
|
+
subject *is* a registry file — so [the env-var rule](/cli.md#one-lever-not-two)
|
|
63
|
+
keeps its scope rather than losing its edge.
|
|
64
|
+
|
|
3
65
|
## 2026-08-19
|
|
4
66
|
|
|
5
67
|
* **This bundle became okf's own, and holds everything about okf.** It began the
|
data/.okf/registry.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Component
|
|
3
3
|
title: The bundle registry
|
|
4
|
-
description: An ordered list of bundle references persisted as JSON — global under $OKF_HOME, or project-local via `okf registry init` and discovered from the working directory — the kernel behind a bare `okf server`.
|
|
4
|
+
description: An ordered list of bundle references persisted as JSON — global under $OKF_HOME, or project-local via `okf registry init` and discovered from the working directory, composable through links to other registry files and able to import rows out of them — the kernel behind a bare `okf server`.
|
|
5
5
|
resource: gems/okf/lib/okf/registry.rb
|
|
6
6
|
tags: [cli, shell, registry]
|
|
7
7
|
generated:
|
|
@@ -217,7 +217,7 @@ a per-user file, and locking would buy nothing worth the complexity.
|
|
|
217
217
|
|
|
218
218
|
The registry has two homes, and which one answers is decided by *where you stand*,
|
|
219
219
|
not by a flag. The global one is the `$OKF_HOME/registry.json` above — one per
|
|
220
|
-
user, shared across every repo. The project-local one is a `.okf
|
|
220
|
+
user, shared across every repo. The project-local one is a `.okf.json`
|
|
221
221
|
that `okf registry init` drops in a directory; okf finds it by walking up from the
|
|
222
222
|
working directory, and while you are inside its tree it **replaces** the global one
|
|
223
223
|
— every registry op, and every [`@slug`](cli.md), resolves through it. So a bare
|
|
@@ -238,12 +238,36 @@ users who set up a project registry. The escape hatch is therefore a per-invocat
|
|
|
238
238
|
signal, not a second sticky variable — `OKF_NO_DISCOVERY=1`, set inline, forces the
|
|
239
239
|
global registry for a fixed-cwd caller (CI, a tool) that cannot just `cd` out.
|
|
240
240
|
|
|
241
|
+
That file was called `.okf-registry.json` first, and both names are still
|
|
242
|
+
discovered — a local registry is *committed*, so retiring the old one outright
|
|
243
|
+
would break every repository carrying it to save eight characters. The two are
|
|
244
|
+
checked **per directory** on the way up, not one name swept to the root and then
|
|
245
|
+
the other: otherwise a legacy file at a repo's root would beat a `.okf.json` two
|
|
246
|
+
levels down, and "the nearest one wins" would quietly mean something else. Inside
|
|
247
|
+
one directory the short name wins.
|
|
248
|
+
|
|
249
|
+
The deprecation is said **once, by the `registry` umbrella, and nowhere else**.
|
|
250
|
+
That verb is the one whose subject *is* a registry file and the one nobody runs in
|
|
251
|
+
a loop or pipes into something, which is exactly what `lint` and `search` are — a
|
|
252
|
+
note there is noise people learn to redirect away rather than act on. A legacy
|
|
253
|
+
file that is in force gets the one move that retires it; a legacy file sitting
|
|
254
|
+
beside the `.okf.json` that beat it gets named too, because reading one while the
|
|
255
|
+
other lies there unread is a silent wrong answer unless somebody says so.
|
|
256
|
+
|
|
257
|
+
`-g`/`--global` is that same signal spelled as an argument, and it is the
|
|
258
|
+
`registry` umbrella's alone (see [the CLI](cli.md#one-lever-not-two)). It exists
|
|
259
|
+
because a lever reachable only through an env var is a lever most users never
|
|
260
|
+
find — and the umbrella is the one verb whose *subject* is a registry file, so
|
|
261
|
+
naming which file to act on is an argument to it rather than a flag bolted onto
|
|
262
|
+
fourteen unrelated verbs. `init` is the exception that proves it: its whole job is
|
|
263
|
+
to create a *local* file, so `-g` there names nothing and is refused.
|
|
264
|
+
|
|
241
265
|
# A project-local registry stores portable paths
|
|
242
266
|
|
|
243
267
|
The global registry stores absolute paths — correct for `~/.okf`, whose bundles
|
|
244
268
|
are scattered across the disk with no shared anchor. A committed project registry
|
|
245
269
|
needs the opposite: a bundle **inside** the registry's own tree is stored *relative*
|
|
246
|
-
to the `.okf
|
|
270
|
+
to the `.okf.json`, so the file travels with the repo — a checkout on
|
|
247
271
|
another machine, or a container mounting it, resolves the same bundles unchanged. A
|
|
248
272
|
bundle **outside** the tree keeps an absolute path, because a relative path that
|
|
249
273
|
climbs out cannot be re-anchored anywhere useful, and being honest that it will not
|
|
@@ -257,6 +281,149 @@ existing absolute local entry migrates to relative on its next write: a registry
|
|
|
257
281
|
written before this existed heals itself the first time it changes.
|
|
258
282
|
<!-- rule:okf-registry-local-discovery -->
|
|
259
283
|
|
|
284
|
+
# Links: the global registry composes other registry files
|
|
285
|
+
|
|
286
|
+
A **link** is a pointer from the global registry to another registry file. That
|
|
287
|
+
file's bundles resolve through the pointer at read time, under their own slugs,
|
|
288
|
+
and nothing is copied — the same "stores references, never content" rule the
|
|
289
|
+
entries keep, one level up. `okf registry link onm ~/ONM/registry.json` and
|
|
290
|
+
`@onm-central` (or whatever slugs that file holds) answer here; the link name
|
|
291
|
+
itself resolves as a group over exactly its bundles, so `@onm` is the set.
|
|
292
|
+
|
|
293
|
+
The case it exists for is **a repository that already curates its own bundles**.
|
|
294
|
+
This repo commits a `.okf.json` naming five; before links, using them
|
|
295
|
+
from `~/.okf` meant registering all five again by hand and re-syncing whenever
|
|
296
|
+
the repo's list changed. A link points at the file the repo already maintains, so
|
|
297
|
+
the curation is composed rather than duplicated, and it keeps resolving its own
|
|
298
|
+
relative paths because the target is anchored on its own directory — exactly as
|
|
299
|
+
it would from inside that checkout. Two registry files stop being two worlds you
|
|
300
|
+
switch `$OKF_HOME` between and become one view.
|
|
301
|
+
|
|
302
|
+
**Only the global registry follows links.** A project-local one parses them and
|
|
303
|
+
preserves them across a write, but never resolves them — and that single
|
|
304
|
+
restriction is the whole depth rule. A linked file's own links are not read, so
|
|
305
|
+
no chain forms, no prefix compounds, and there is no cycle to detect. The
|
|
306
|
+
alternative was transitive resolution, and it fails on ownership rather than on
|
|
307
|
+
effort: the target is a file this registry does not own, so a cycle-check at
|
|
308
|
+
write time goes stale the moment someone adds a link back on the other side.
|
|
309
|
+
Transitivity would move cycle detection from write time — cheap, one file,
|
|
310
|
+
refusable — to read time, across N files, with nobody to blame. The groups above
|
|
311
|
+
nest safely for exactly the reason links do not: every member lives in one file,
|
|
312
|
+
behind one guard.
|
|
313
|
+
<!-- rule:okf-registry-links-global-only -->
|
|
314
|
+
|
|
315
|
+
# A linked name is minted around a collision, never refused
|
|
316
|
+
|
|
317
|
+
A slug arriving from a linked file answers to itself when the name is free, and
|
|
318
|
+
to `<link>-<slug>` when it is not (`-2` beyond that, through the same `dedupe` a
|
|
319
|
+
basename goes through). That is the [implicit is forgiving, explicit is
|
|
320
|
+
strict](#slugs-implicit-is-forgiving-explicit-is-strict) table one row wider, and
|
|
321
|
+
the row falls on the forgiving side for the reason the table gives: a name in
|
|
322
|
+
someone else's file was never *chosen* here, so inventing around it is licensed —
|
|
323
|
+
while refusing would let one foreign row take down an entire link. The link
|
|
324
|
+
*name* is the strict half, and is refused on collision like any `--as`: you typed
|
|
325
|
+
that one.
|
|
326
|
+
|
|
327
|
+
Precedence is fixed so the derivation is reproducible: the registry's own bundles
|
|
328
|
+
always win the bare name, and between links the file's order decides. Both are
|
|
329
|
+
position, which is state the registry already keeps — the same reason the
|
|
330
|
+
[default](#the-default-is-a-position-not-a-stored-name) is one.
|
|
331
|
+
|
|
332
|
+
What this costs, and it is the design's one genuinely computed name: a slug can
|
|
333
|
+
*move* when an unrelated link is added. Everything else in this file is stable in
|
|
334
|
+
the file. The mitigation is disclosure rather than a mechanism — `registry link`
|
|
335
|
+
says what it moved as it writes, and `registry list` prints the moved row with the
|
|
336
|
+
slug it carries in its source file (`onm-central … [central]`), which is the only
|
|
337
|
+
place a shifted ref is visible. Linked entries also append **after** the local
|
|
338
|
+
ones, so while this registry owns any bundle at all the default stays local.
|
|
339
|
+
|
|
340
|
+
**A linked group is listed with the rest, not beside them.** `groups_listing`
|
|
341
|
+
returns this registry's own groups first, then the linked ones, each carrying the
|
|
342
|
+
`link` it came from — one list, because `group?` *resolves* a linked group and a
|
|
343
|
+
listing that named only the local half would answer about a smaller set than the
|
|
344
|
+
same object can resolve. That gap is invisible at the call site and inherited by
|
|
345
|
+
every consumer: `okf-mcp`'s `list_bundles` and the TUI's groups view both read
|
|
346
|
+
this one method, and both would have hidden a group they could already open. A
|
|
347
|
+
caller that wants only the groups it may edit filters on `link` — which is the
|
|
348
|
+
question they are actually asking, and it is now askable.
|
|
349
|
+
|
|
350
|
+
# A link is read-only, and the refusal lives in the model
|
|
351
|
+
|
|
352
|
+
`rename`, `del`, `default`, `set --as` and `group` all refuse a slug a link owns,
|
|
353
|
+
with a message naming the file that does own it and the `unlink` that would drop
|
|
354
|
+
it. Two of those are worth their own line. A **group** may not hold a linked slug:
|
|
355
|
+
a group stores names, and a linked name lives only while its link resolves, so
|
|
356
|
+
holding one would dangle the group the moment the link goes — the foreign key the
|
|
357
|
+
default rule already refused. And **`registry set` on a directory a link already
|
|
358
|
+
carries** is refused rather than quietly adding a twin, because entries are
|
|
359
|
+
identified by path and the path is already spoken for.
|
|
360
|
+
|
|
361
|
+
The third was a hole this rule had left open, and it failed in the worst
|
|
362
|
+
available way. A group's slug is its *update* path everywhere else — `registry
|
|
363
|
+
group backend @more` adds to the existing one — so `group onm @alpha`, naming a
|
|
364
|
+
link or a group that came with one, took that path: it merged the member, printed
|
|
365
|
+
`grouped onm → …`, and lost it, because `write` persists only the groups this
|
|
366
|
+
registry owns. A refusal is the fix, but the shape is what matters: a write that
|
|
367
|
+
reports success and does not happen is worse than one that raises, and it was
|
|
368
|
+
reachable from the CLI, the TUI and the browser panel alike.
|
|
369
|
+
|
|
370
|
+
The refusals live in this class, not in the [CLI](cli.md), and that placement is
|
|
371
|
+
load-bearing: the graph page's ⚙ Bundles panel posts into these same methods
|
|
372
|
+
([bundles manager](capabilities/bundles-manager.md)), so a guard one layer up
|
|
373
|
+
would leave the browser doing what the terminal refuses. A link whose target has
|
|
374
|
+
gone or cannot be parsed is *reported* — `(missing)`, `(unreadable)` — and
|
|
375
|
+
resolves to nothing, the same tolerance a vanished bundle directory gets, because
|
|
376
|
+
one dead pointer must not take down the registry that holds it.
|
|
377
|
+
|
|
378
|
+
# Import: the copy that owns what it takes
|
|
379
|
+
|
|
380
|
+
`okf registry import <@slug…>` copies chosen bundles — and the groups that hold
|
|
381
|
+
them — out of another registry file into the one in force. It is the **opposite
|
|
382
|
+
trade** from a link, and the pair is the point:
|
|
383
|
+
|
|
384
|
+
| | link | import |
|
|
385
|
+
| --- | --- | --- |
|
|
386
|
+
| what moves | nothing; a pointer | the reference, copied |
|
|
387
|
+
| scope | the whole target file | the slugs you name |
|
|
388
|
+
| ownership | the target keeps it, read-only | yours, editable |
|
|
389
|
+
| freshness | live on every read | a snapshot |
|
|
390
|
+
|
|
391
|
+
Copying a *reference* is not copying content, so the rule at the top of this file
|
|
392
|
+
still holds: the bundle stays in the repository that owns it, and what lands here
|
|
393
|
+
is the same path-and-name row `registry set` would have written. What import saves
|
|
394
|
+
is the laundering. The scenario is standing inside a repo, running `okf registry
|
|
395
|
+
list -g` to see what the global registry holds, and wanting one of them here — the
|
|
396
|
+
path is already on screen, and without this verb the only way to move it is
|
|
397
|
+
through the clipboard.
|
|
398
|
+
|
|
399
|
+
The source is `--from`, defaulting to the global registry, which inside a repo is
|
|
400
|
+
the only other one you have. It is a second flag rather than a second meaning for
|
|
401
|
+
`-g` because [`-g` names the registry acted *on*](cli.md#one-lever-not-two) on
|
|
402
|
+
every other subcommand; this verb names two files, so the second gets its own
|
|
403
|
+
name rather than inverting the first.
|
|
404
|
+
|
|
405
|
+
**A collision refuses.** This is where import and link disagree, and the
|
|
406
|
+
disagreement is the slug rule, not an inconsistency: a linked name was never
|
|
407
|
+
chosen here, so it is [minted around](#a-linked-name-is-minted-around-a-collision-never-refused);
|
|
408
|
+
an imported name lands in *this* file because you typed it, so it is refused
|
|
409
|
+
exactly as `rename` refuses. The gem may invent a name it made up; it may not
|
|
410
|
+
substitute one you chose. A bundle already registered here under a different name
|
|
411
|
+
refuses too, and *first* — when both are true, "that bundle is already here as
|
|
412
|
+
@docs" is the answer and "the name is taken" is only the symptom.
|
|
413
|
+
|
|
414
|
+
**A group brings everything it reaches**, including a group nested inside it.
|
|
415
|
+
Recreating a name here that resolved to a larger set there would be that same
|
|
416
|
+
quiet substitution, with nothing on screen to reveal it. Members are stored
|
|
417
|
+
verbatim, which is the payoff of preserving slugs: a name means the same thing on
|
|
418
|
+
both sides, so there is nothing to remap — the work `fold_linked_bundles` must do
|
|
419
|
+
precisely because a link mints its names.
|
|
420
|
+
|
|
421
|
+
**Nothing is applied until everything is checked.** Every ask is resolved and
|
|
422
|
+
refused against the current state first, then one `write` publishes the lot. Half
|
|
423
|
+
an import is a registry the user has to unpick by hand, reported as a success —
|
|
424
|
+
and a refusal that already moved three of four rows is not a refusal.
|
|
425
|
+
<!-- rule:okf-registry-import-all-or-nothing -->
|
|
426
|
+
|
|
260
427
|
# It costs an embedding app nothing
|
|
261
428
|
|
|
262
429
|
`require "okf"` does not load it. The registry is reached only from the
|
data/.okf/structure/the-cli.md
CHANGED
|
@@ -23,7 +23,7 @@ And one file per verb, each registering itself at load:
|
|
|
23
23
|
| `lib/okf/cli/skill.rb` | `skill` |
|
|
24
24
|
| `lib/okf/cli/server.rb` | `server` |
|
|
25
25
|
| `lib/okf/cli/render.rb` | `render` |
|
|
26
|
-
| `lib/okf/cli/registry.rb` | `registry` and its
|
|
26
|
+
| `lib/okf/cli/registry.rb` | `registry` and its eleven subcommands |
|
|
27
27
|
| `lib/okf/cli/lint.rb` | `lint` |
|
|
28
28
|
| `lib/okf/cli/loose.rb` | `loose` |
|
|
29
29
|
| `lib/okf/cli/validate.rb` | `validate` |
|
|
@@ -50,17 +50,26 @@ logic belongs instead.
|
|
|
50
50
|
|
|
51
51
|
`Registry` is the `@slug` layer. `HOME_ENV`/`DEFAULT_HOME` is the global
|
|
52
52
|
registry under `$OKF_HOME` (default `~/.okf`); `LOCAL_FILE` is the project-local
|
|
53
|
-
`.okf-registry.json` that `discover` finds by walking up from the working
|
|
53
|
+
`.okf.json` (or the legacy `.okf-registry.json`) that `discover` finds by walking up from the working
|
|
54
54
|
directory. **A discovered local registry replaces the global one outright** — it
|
|
55
55
|
does not merge — and `NO_DISCOVERY_ENV` (`OKF_NO_DISCOVERY=1`) is the escape
|
|
56
56
|
hatch that forces the global one, which is what a test that must not see the
|
|
57
|
-
developer's registry sets.
|
|
57
|
+
developer's registry sets. `Registry.load`'s `follow_links:` is the same
|
|
58
|
+
decision one step further: it is true only for the global registry, so a local
|
|
59
|
+
one parses links and never resolves them.
|
|
58
60
|
|
|
59
61
|
`relative_base` is why a local registry's paths stay relative and the file
|
|
60
62
|
travels with the repository. `Registry#reopen` preserves it; `Registry.new(path)`
|
|
61
63
|
does not, and reaching for the latter is how a reload comes to resolve every
|
|
62
64
|
bundle against the wrong base.
|
|
63
65
|
|
|
66
|
+
`Link` points the global registry at another registry file; `resolve_links`
|
|
67
|
+
folds that file's bundles in (through `link_slug`, which prefixes only on a
|
|
68
|
+
collision) and `refuse_linked` is the single guard every write goes through —
|
|
69
|
+
here rather than in the CLI, because the server's bundles panel calls these same
|
|
70
|
+
methods. `open_linked` constructs the target with `follow_links: false`, which is
|
|
71
|
+
the whole of the depth rule.
|
|
72
|
+
|
|
64
73
|
`Group` is a named, recursive set of bundles. `RESERVED_SLUGS` is `all`, because
|
|
65
74
|
`@all` means every bundle and may not be shadowed. `slugify`, `dedupe`,
|
|
66
75
|
`normalize` and `path_shaped?` are the naming rules a `registry set` goes
|
|
@@ -11,7 +11,7 @@ resource: lib/okf/cli/command.rb
|
|
|
11
11
|
|
|
12
12
|
# Before adding one, check it is not already there
|
|
13
13
|
|
|
14
|
-
Seventeen commands and
|
|
14
|
+
Seventeen commands and eleven subcommands already exist, catalogued in
|
|
15
15
|
[cli](/cli.md) and [read-views](/capabilities/read-views.md), and `okf help`
|
|
16
16
|
prints the same list. Most of what a new verb would want is a *flag* on an
|
|
17
17
|
existing view rather than a view of its own: the shared filters and projections
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,111 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.2.0] - 2026-08-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`okf registry import <@slug…> [--from FILE]` — copy chosen bundles out of
|
|
13
|
+
another registry file into the one in force, where they become yours.** It is
|
|
14
|
+
the opposite trade from `link` and the pair is the point: a link holds a live
|
|
15
|
+
pointer to a whole file and the other side keeps owning what it lends, while an
|
|
16
|
+
import copies chosen references and hands over ownership, so what lands renames,
|
|
17
|
+
defaults and groups like anything you registered by hand. The case it exists for
|
|
18
|
+
is standing in a repo, running `okf registry list -g`, and wanting one of the
|
|
19
|
+
bundles it shows — before this the only way to move it was to read the path off
|
|
20
|
+
the screen and retype it into `registry set`.
|
|
21
|
+
|
|
22
|
+
The source is `--from`, defaulting to the global registry, because `-g` goes on
|
|
23
|
+
meaning the registry acted *on*, as it does on every sibling subcommand. A group
|
|
24
|
+
ask brings its members and any group nested inside it, recreated under the same
|
|
25
|
+
names. Slugs are preserved, so a collision **refuses** (`--as` renames a single
|
|
26
|
+
ask) rather than being minted around the way a linked name is — a linked name was
|
|
27
|
+
never chosen here, an imported one was typed. A bundle already registered here
|
|
28
|
+
under another name refuses first, naming that name. Every ask is validated before
|
|
29
|
+
anything is written, so an import lands whole or leaves the file byte-for-byte
|
|
30
|
+
alone.
|
|
31
|
+
|
|
32
|
+
- **`.okf.json` is the project-local registry's filename.** `okf registry init`
|
|
33
|
+
writes it, and it is what the docs now name. The older `.okf-registry.json` is
|
|
34
|
+
**still discovered**, so no committed registry breaks: both names are checked in
|
|
35
|
+
each directory on the way up — per directory, not one name swept to the root and
|
|
36
|
+
then the other, or a legacy file at a repo's root would beat a `.okf.json` two
|
|
37
|
+
levels down and "the nearest one wins" would mean something else. Within one
|
|
38
|
+
directory the short name wins, and a legacy file left beside it is named on
|
|
39
|
+
stderr rather than silently ignored.
|
|
40
|
+
|
|
41
|
+
The deprecation note is the `registry` umbrella's alone, and it is one stderr
|
|
42
|
+
line: that verb's subject *is* a registry file, and it is not the one people run
|
|
43
|
+
in loops or pipe into something else, which is exactly what `lint` and `search`
|
|
44
|
+
are. Renaming is `git mv .okf-registry.json .okf.json`, and the note says so. One
|
|
45
|
+
transitional cost worth naming: a checkout renamed to `.okf.json` is invisible to
|
|
46
|
+
an installed okf older than 2.2.0, which will fall back to the global registry
|
|
47
|
+
until that install is upgraded.
|
|
48
|
+
|
|
49
|
+
- **`okf registry link <name> <file>` — the global registry points at another
|
|
50
|
+
registry file, and that file's bundles resolve here.** Nothing is copied: the
|
|
51
|
+
target keeps owning its rows, so an edit there shows on the next read. The case
|
|
52
|
+
it exists for is a repository that already curates its own bundles in a
|
|
53
|
+
committed `.okf-registry.json` — a link composes that curation instead of
|
|
54
|
+
duplicating it, and the target resolves its own relative paths exactly as it
|
|
55
|
+
would from inside that checkout. A linked bundle answers to its own slug, or to
|
|
56
|
+
`<name>-<slug>` when the name is already taken here; `@<name>` resolves as a
|
|
57
|
+
group over the set; `okf registry unlink <name>` drops it.
|
|
58
|
+
|
|
59
|
+
Links are the **global** registry's alone. A project-local one parses them and
|
|
60
|
+
never resolves them, which is what makes depth one structural — a linked file's
|
|
61
|
+
own links are never followed, so no chain forms and there is no cycle to guard.
|
|
62
|
+
A link is read-only: `rename`, `del`, `default`, `set --as` and `group` refuse a
|
|
63
|
+
slug it owns, naming the file that does, from the browser's ⚙ Bundles panel as
|
|
64
|
+
from the terminal. A target that has gone or cannot be parsed is listed
|
|
65
|
+
`(missing)`/`(unreadable)` and resolves to nothing, rather than taking down the
|
|
66
|
+
registry holding it.
|
|
67
|
+
|
|
68
|
+
- `registry group` and `registry ungroup` refuse a group a link brought in —
|
|
69
|
+
the link's own set, or one the linked file curates. Naming one took the
|
|
70
|
+
update path, reported `grouped …`, and discarded the change on the next read,
|
|
71
|
+
because only the groups this registry owns are written. A write that reports
|
|
72
|
+
success and does not happen is worse than one that raises.
|
|
73
|
+
|
|
74
|
+
- **`-g`/`--global` on every `registry` subcommand but `init`.** It forces the
|
|
75
|
+
`$OKF_HOME` registry for one command, so `okf registry list -g` reads the global
|
|
76
|
+
one from inside a repo carrying its own, and `okf registry set <dir> -g`
|
|
77
|
+
registers there without leaving. `OKF_NO_DISCOVERY=1` already did this for a
|
|
78
|
+
whole session and still does; the flag exists because a lever reachable only
|
|
79
|
+
through an env var is one most people never find. It stays on the `registry`
|
|
80
|
+
umbrella — the one verb whose subject *is* a registry file — and nowhere else.
|
|
81
|
+
|
|
82
|
+
- The graph page's ⚙ Bundles panel marks a linked row `via @onm` and **offers no
|
|
83
|
+
actions menu** on it. Rename, Remove and Default are all refused for a linked
|
|
84
|
+
bundle, so the menu could only have produced three errors. The request is
|
|
85
|
+
refused whether or not the button exists — the guard is in `Registry`, not in
|
|
86
|
+
the page — which is the same split the read-only server already keeps: hiding
|
|
87
|
+
a control is a UI, refusing the request is the boundary.
|
|
88
|
+
|
|
89
|
+
- **`okf help` shows one `registry` row instead of ten, and `okf registry --help`
|
|
90
|
+
prints the subcommands.** Ten rows for one verb made a third of the map about
|
|
91
|
+
registry management, and the map is there to name the verbs rather than to be
|
|
92
|
+
every verb's manual. The row names the help that replaces it, since nothing
|
|
93
|
+
else tells a reader an umbrella has subcommands. A bare `okf registry` still
|
|
94
|
+
lists the registry, and `okf registry <command> --help` still answers for that
|
|
95
|
+
command.
|
|
96
|
+
|
|
97
|
+
### Changed
|
|
98
|
+
|
|
99
|
+
- `okf registry list --json` groups now carry a `link` key — `null` for a group
|
|
100
|
+
this registry owns, the link's name for one that arrived through a link.
|
|
101
|
+
`Registry#groups_listing` returns both kinds in one list, own groups first,
|
|
102
|
+
because `#group?` resolves a linked group and a listing that named only the
|
|
103
|
+
local half would answer about a smaller set than the same object can resolve —
|
|
104
|
+
the drift every library consumer would inherit silently. A caller that wants
|
|
105
|
+
only the editable groups filters on `link`.
|
|
106
|
+
|
|
107
|
+
- `okf registry list --json` gains a `links` array, and each bundle row gains
|
|
108
|
+
`link` (the link it arrived through, `null` when the registry owns it) and
|
|
109
|
+
`origin` (its slug in that file, differing only where a collision moved the
|
|
110
|
+
name). The on-disk registry gains a `"links"` key, written empty when there are
|
|
111
|
+
none; an older file with no such key reads unchanged.
|
|
112
|
+
|
|
8
113
|
## [2.1.1] - 2026-08-20
|
|
9
114
|
|
|
10
115
|
### Fixed
|
data/README.md
CHANGED
|
@@ -131,6 +131,8 @@ okf graph <dir|@slug> [--hubs] [--traffic] # the raw graph; --hubs ranks c
|
|
|
131
131
|
okf server [DIR|@slug…] [-p PORT] [--bind ADDR] # the live graph: one bundle, or all of them
|
|
132
132
|
okf render <dir|@slug> [-o FILE] # the same page as one static, self-contained file
|
|
133
133
|
okf registry init | list | set | del | default | rename | group | ungroup # name & group your bundles
|
|
134
|
+
okf registry link | unlink <name> <file> # fold another registry file's bundles in
|
|
135
|
+
okf registry import <@slug…> [--from FILE] # copy bundles out of another registry into this one
|
|
134
136
|
okf skill <dest> # install the companion agent skill
|
|
135
137
|
okf --version
|
|
136
138
|
```
|
|
@@ -173,17 +175,44 @@ at once — a durable subset for the two verbs that take several bundles.
|
|
|
173
175
|
|
|
174
176
|
The registry lives under `$OKF_HOME` (default `~/.okf`) — one per user. For one
|
|
175
177
|
scoped to a single project instead, `okf registry init` drops a
|
|
176
|
-
`.okf
|
|
177
|
-
|
|
178
|
+
`.okf.json` in the current directory (the older `.okf-registry.json` is still
|
|
179
|
+
discovered, and `okf registry` says so once so you can `git mv` it); okf then
|
|
180
|
+
discovers it by walking up from wherever you run, and every registry op — and every `@slug` — resolves through
|
|
178
181
|
it in place of the global one. So a bare `okf server` inside that repo serves *its*
|
|
179
|
-
bundles with no `$OKF_HOME` setup. The nearest registry wins
|
|
180
|
-
`
|
|
182
|
+
bundles with no `$OKF_HOME` setup. The nearest registry wins; `-g` on any
|
|
183
|
+
`registry` subcommand reaches the global one for a single command (`okf registry
|
|
184
|
+
list -g`, `okf registry set ./docs -g`), and `OKF_NO_DISCOVERY=1` does it for a
|
|
185
|
+
whole shell.
|
|
181
186
|
|
|
182
187
|
Commit that file and it travels with the repo: a bundle under the project root is
|
|
183
188
|
stored relative to the registry, so a checkout on another machine — or a container
|
|
184
189
|
that mounts the repo — resolves the same bundles unchanged. (Bundles outside the
|
|
185
190
|
tree keep absolute paths, which do not travel.)
|
|
186
191
|
|
|
192
|
+
A repository that already curates its own bundles can lend that list rather than
|
|
193
|
+
have it copied. `okf registry link onm ~/ONM/.okf.json` points the global
|
|
194
|
+
registry at another registry file: its bundles resolve under their own slugs,
|
|
195
|
+
`@onm` names the set, and nothing is duplicated — edit the other file and the
|
|
196
|
+
change shows on the next read. They are read-only from here, since the file that
|
|
197
|
+
owns them is elsewhere.
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
okf registry link okf ~/code/okf/.okf.json # that repo's own curation, composed
|
|
201
|
+
okf search @all rate limit # now spans both files
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
When you want one bundle rather than a whole file, `import` is the opposite
|
|
205
|
+
trade — it copies the reference, and what lands is yours to rename, default and
|
|
206
|
+
group. The source is `--from`, defaulting to the global registry, which inside a
|
|
207
|
+
repo is the only other one you have. A group ask brings its members with it,
|
|
208
|
+
slugs are preserved (a collision refuses rather than being renamed behind your
|
|
209
|
+
back), and nothing is written unless every ask checks out.
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
okf registry list -g # what does the global registry hold?
|
|
213
|
+
okf registry import handbook specs # take those two, here, as mine
|
|
214
|
+
```
|
|
215
|
+
|
|
187
216
|
Behind the hub each bundle mounts at `/b/<slug>/`, `/b/` lists them all, and the
|
|
188
217
|
`⌘/Ctrl-K` palette both switches bundles and **searches every one at once** — type
|
|
189
218
|
a few words and the matching concepts appear with their bundle and a snippet, from
|
data/lib/okf/cli/command.rb
CHANGED
|
@@ -577,12 +577,17 @@ module OKF
|
|
|
577
577
|
|
|
578
578
|
# The registry a verb resolves against — the single seam that opts the CLI
|
|
579
579
|
# into discovery. `cwd: Dir.pwd` is what makes OKF::Registry.load walk up for
|
|
580
|
-
# a project-local .okf
|
|
580
|
+
# a project-local .okf.json; a library caller passing no cwd stays
|
|
581
581
|
# global-only. The registry subcommands and `server` open through here too,
|
|
582
582
|
# so a bare `okf server` inside a repo serves that repo's bundles.
|
|
583
|
-
|
|
583
|
+
# +global+ forces the $OKF_HOME registry by withholding the cwd discovery
|
|
584
|
+
# needs — the per-command form of OKF_NO_DISCOVERY, and the only thing
|
|
585
|
+
# `okf registry -g` does. It is an argument to the `registry` verb rather
|
|
586
|
+
# than a flag on all fourteen, because that verb's *subject* is a registry
|
|
587
|
+
# file; every other verb keeps inheriting the env var's one signal.
|
|
588
|
+
def open_registry(global: false)
|
|
584
589
|
require "okf/registry"
|
|
585
|
-
OKF::Registry.load(cwd: Dir.pwd)
|
|
590
|
+
OKF::Registry.load(cwd: global ? nil : Dir.pwd)
|
|
586
591
|
end
|
|
587
592
|
|
|
588
593
|
# Resolve one @ref through the active registry — a discovered project-local
|