okf 1.9.0 → 1.10.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/CHANGELOG.md +493 -133
- data/README.md +101 -10
- data/lib/okf/bundle/folder.rb +4 -0
- data/lib/okf/bundle.rb +25 -1
- data/lib/okf/cli/catalog.rb +66 -0
- data/lib/okf/cli/command.rb +495 -0
- data/lib/okf/cli/files.rb +68 -0
- data/lib/okf/cli/graph.rb +82 -0
- data/lib/okf/cli/index.rb +127 -0
- data/lib/okf/cli/lint.rb +139 -0
- data/lib/okf/cli/loose.rb +78 -0
- data/lib/okf/cli/registry.rb +229 -0
- data/lib/okf/cli/render.rb +66 -0
- data/lib/okf/cli/search.rb +285 -0
- data/lib/okf/cli/server.rb +179 -0
- data/lib/okf/cli/skill.rb +57 -0
- data/lib/okf/cli/stats.rb +88 -0
- data/lib/okf/cli/tags.rb +122 -0
- data/lib/okf/cli/types.rb +37 -0
- data/lib/okf/cli/validate.rb +66 -0
- data/lib/okf/cli.rb +418 -1703
- data/lib/okf/render/graph/template.html.erb +1020 -61
- data/lib/okf/render/graph.rb +46 -2
- data/lib/okf/server/app.rb +10 -4
- data/lib/okf/server/hub/not_found.rb +663 -0
- data/lib/okf/server/hub.rb +504 -38
- data/lib/okf/skill/SKILL.md +14 -10
- data/lib/okf/skill/playbooks/curate.md +3 -1
- data/lib/okf/skill/playbooks/maintain.md +3 -2
- data/lib/okf/skill/playbooks/menu.md +5 -0
- data/lib/okf/skill/playbooks/refine.md +92 -0
- data/lib/okf/skill/reference/cli.md +22 -4
- data/lib/okf/version.rb +1 -1
- metadata +19 -1
data/README.md
CHANGED
|
@@ -225,10 +225,15 @@ docker run --rm -v "$PWD:/data" ghcr.io/serradura/okf validate .
|
|
|
225
225
|
docker run --rm -v "$PWD:/data" -p 8808:8808 ghcr.io/serradura/okf server . --bind 0.0.0.0
|
|
226
226
|
```
|
|
227
227
|
|
|
228
|
+
A container has to bind `0.0.0.0` for the host to reach it at all, which is
|
|
229
|
+
exactly the bind that makes the registry read-only. Reading the graph is
|
|
230
|
+
unaffected, and there is no flag that opens the registry on that bind — it is a
|
|
231
|
+
per-user file, managed from the machine that owns it or from `okf registry`.
|
|
232
|
+
|
|
228
233
|
Then open <http://127.0.0.1:8808>. Images are published for `linux/amd64` and
|
|
229
234
|
`linux/arm64` on
|
|
230
235
|
[ghcr.io](https://github.com/serradura/okf-gem/pkgs/container/okf): `:latest`
|
|
231
|
-
tracks the newest release, or pin a version like `:1.
|
|
236
|
+
tracks the newest release, or pin a version like `:1.10.0`.
|
|
232
237
|
|
|
233
238
|
Tired of the long line? Install a Docker-backed [`okf` command](https://docker.okfgem.com),
|
|
234
239
|
so every verb drops the `docker run` prefix and reads exactly like the native CLI
|
|
@@ -246,13 +251,20 @@ instead: `irm https://docker.okfgem.com/install.ps1 | iex`.
|
|
|
246
251
|
|
|
247
252
|
## Command line
|
|
248
253
|
|
|
254
|
+
These verbs are written to be read by an **agent first and a person second** —
|
|
255
|
+
that is what the skill drives, with no wrapper in between. Every read verb takes
|
|
256
|
+
`--json`, `index`/`catalog`/`files` project down to the fields you ask for
|
|
257
|
+
(`--fields`/`--except`), so nothing pays for output it will not read, and the
|
|
258
|
+
exit codes are stable enough to branch on in CI. The same commands render as
|
|
259
|
+
scannable plain text when a human is the one looking.
|
|
260
|
+
|
|
249
261
|
```bash
|
|
250
262
|
okf validate <dir|@slug> [--json] # check OKF v0.1 conformance (§9)
|
|
251
263
|
okf lint <dir|@slug> [--json] [--fail-on warn] [...] # report curation-quality issues
|
|
252
264
|
okf loose <dir|@slug> [--json] # list files with no graph links, by folder
|
|
253
|
-
okf search <dir|@slug…|@all> <term…> [
|
|
265
|
+
okf search <dir|@slug…|@all> <term…> [--regexp|--fuzzy] # ranked retrieval; @slugs or @all span bundles
|
|
254
266
|
okf index <dir|@slug> [--json] [--area A] [--no-body] # progressive-disclosure map (§6): bodies, rollups, listings
|
|
255
|
-
okf server [DIR|@slug…] [-p PORT] [--bind ADDR] [...] # serve one bundle, or many behind a hub (⌘K
|
|
267
|
+
okf server [DIR|@slug…] [-p PORT] [--bind ADDR] [...] # serve one bundle, or many behind a hub (⌘K searches every one)
|
|
256
268
|
okf render <dir|@slug> [-o FILE] [--layout NAME] [...] # export the graph as one static, self-contained HTML file
|
|
257
269
|
okf registry list [--json] # list registered bundles (* marks the default)
|
|
258
270
|
okf registry set <dir|@slug> [--as SLUG] [--default] # add or update a bundle (a bare `server` serves it)
|
|
@@ -269,8 +281,11 @@ okf registry default <@slug> | rename <@slug> <new> # move a bundle to th
|
|
|
269
281
|
# set`, or bare @ for the default. Anywhere a <dir> goes, an @slug goes:
|
|
270
282
|
# okf lint @handbook works from anywhere. Set $OKF_HOME to point every verb
|
|
271
283
|
# at another registry.
|
|
272
|
-
okf graph <dir|@slug> [--json] [--minimal] [--
|
|
284
|
+
okf graph <dir|@slug> [--json] [--minimal] [--hubs] # print the knowledge graph (--hubs ranks by inbound links)
|
|
273
285
|
okf catalog | files | tags | types | stats <dir|@slug> [--json] # the browser views, on the CLI
|
|
286
|
+
# `tags --by type|area` regroups the tag index per concept dimension; each row
|
|
287
|
+
# shows count/total, so a tag confined to one area (a domain) reads differently
|
|
288
|
+
# from one spread across several (a cross-cutting concern).
|
|
274
289
|
okf skill <dest> [--here] [--force] # install the companion agent skill
|
|
275
290
|
okf --version
|
|
276
291
|
```
|
|
@@ -303,12 +318,20 @@ _The graph server on this repo's own [`.okf`](.okf) bundle, with the
|
|
|
303
318
|
|
|
304
319
|
The page is one template from a phone to a desktop: on small screens the
|
|
305
320
|
navigation rail becomes a drawer, the toolbar folds into a `⚙` sheet, and the
|
|
306
|
-
panels go full-bleed — rotate a tablet and the layout re-evaluates.
|
|
307
|
-
|
|
321
|
+
panels go full-bleed — rotate a tablet and the layout re-evaluates. On a touch
|
|
322
|
+
screen a tap opens a preview card at the bottom edge rather than a panel over
|
|
323
|
+
the whole viewport, so the graph stays on screen and live while you read: drag
|
|
324
|
+
the card up for the neighbourhood, tap a link in it and it walks there in place.
|
|
325
|
+
It is keyboard-first too: `⌘/Ctrl-K` opens a command palette in every mode (views
|
|
308
326
|
always; bundles too when a [hub](#one-registry-many-bundles) is serving), `/`
|
|
309
327
|
jumps to the current view's search, and `?` answers with a sheet of every
|
|
310
328
|
shortcut.
|
|
311
329
|
|
|
330
|
+
The search box says what it is doing while it does it: a live `7/8` count of
|
|
331
|
+
what the filter kept, and — when a word matches nothing here — a panel naming
|
|
332
|
+
the bundle and the query, offering to search every other bundle instead. The
|
|
333
|
+
query carries over, so nothing is typed twice.
|
|
334
|
+
|
|
312
335
|
To skip the server entirely, **`okf render <dir>`** writes that same page as one
|
|
313
336
|
self-contained HTML file, the whole bundle baked in, so you can publish the
|
|
314
337
|
graph on GitHub Pages or any static host.
|
|
@@ -329,10 +352,32 @@ okf server # no args: the whole registry behind one
|
|
|
329
352
|
|
|
330
353
|
The first entry still on disk is the **default** — the bundle a bare `okf
|
|
331
354
|
server` opens at `/` and a bare `@` names; `okf registry default @slug` moves an
|
|
332
|
-
entry to the front. Behind the hub each bundle mounts at `/b/<slug>/`,
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
355
|
+
entry to the front. Behind the hub each bundle mounts at `/b/<slug>/`, and the
|
|
356
|
+
`⌘/Ctrl-K` palette both switches bundles and **searches every one of them at
|
|
357
|
+
once** — type a few words and the matching concepts appear with their bundle and
|
|
358
|
+
a snippet, from wherever you are.
|
|
359
|
+
|
|
360
|
+
The ⚙ in the rail opens **Bundles** — the registry on the graph page itself, so
|
|
361
|
+
switching the default, renaming an entry or dropping one no longer means finding
|
|
362
|
+
`/b/` first. Each row carries its size, its health as a *word*, and which one `/`
|
|
363
|
+
opens. There is no Add: registering a bundle means naming a filesystem path,
|
|
364
|
+
which a browser cannot hand over and an agent can (`okf registry set <dir>`), and
|
|
365
|
+
the panel says so instead of leaving the absence to be noticed.
|
|
366
|
+
|
|
367
|
+
`/b/` is the **bundles list**: every bundle with its size, its health
|
|
368
|
+
(conformance plus curation, so a warning is a warning and not a failure), the
|
|
369
|
+
default marked, and any entry whose folder has gone missing shown rather than
|
|
370
|
+
quietly dropped.
|
|
371
|
+
|
|
372
|
+
Managing the registry without a terminal happens on the graph page instead, in
|
|
373
|
+
the ⚙ **Bundles** panel — make default, rename, remove, where you are already
|
|
374
|
+
reading. Those controls are the one thing that does not follow you onto a
|
|
375
|
+
network: bind anywhere but loopback and they are refused outright, with no flag
|
|
376
|
+
that says otherwise, since `--bind 0.0.0.0` is how a personal tool becomes a
|
|
377
|
+
public one. `--read-only` declines them on loopback too, and is the only switch
|
|
378
|
+
there is. Writes rebuild the hub's set as they go, so a rename takes effect on
|
|
379
|
+
the next click; a registry change made *elsewhere* while it runs still wants a
|
|
380
|
+
restart to be served. Set `$OKF_HOME` to point every verb at a different registry.
|
|
336
381
|
|
|
337
382
|
`graph` and `server` are best-effort (§9): a file with invalid frontmatter is
|
|
338
383
|
skipped (and noted on stderr), not fatal, so one bad file never breaks the rest.
|
|
@@ -362,6 +407,22 @@ OKF lint — docs
|
|
|
362
407
|
⚠ 3 warn, 31 info
|
|
363
408
|
```
|
|
364
409
|
|
|
410
|
+
That `hubs:` line is the bundle's centre of gravity — the concepts the most links
|
|
411
|
+
point *at*, with their inbound count. **`okf graph <dir> --hubs`** expands it into
|
|
412
|
+
the full ranking and, for each hub, the areas those links come *from*:
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
$ okf graph docs --hubs
|
|
416
|
+
Hubs — docs (38 of 40 concepts with inbound links)
|
|
417
|
+
|
|
418
|
+
storage/reconcile ×16 storage 6, shape 4, exploration 3, (root) 1, …
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
That second half is what decides whether a concept sits where it belongs: a hub
|
|
422
|
+
pulled in mostly from outside its own folder is usually homed by history rather
|
|
423
|
+
than by meaning. Together with `okf tags <dir> --by area`, it is the evidence the
|
|
424
|
+
skill's [`refine`](#agent-skill) verb reads before proposing anything.
|
|
425
|
+
|
|
365
426
|
`loose` lists the files that float in the graph: concepts with no cross-links
|
|
366
427
|
in or out (graph degree 0), grouped by folder. It is a curation lens over
|
|
367
428
|
`lint`'s `unlinked` check, distinct from `orphan`. An `index.md` listing makes a
|
|
@@ -391,11 +452,21 @@ The skill routes a small set of verbs. In Claude Code they run as `/okf:gem
|
|
|
391
452
|
| `produce` | Create or extend a bundle from code, docs, or knowledge in people's heads |
|
|
392
453
|
| `migrate` | Adopt existing Markdown docs in place: frontmatter and reserved files added, bodies kept verbatim |
|
|
393
454
|
| `maintain` | Sync the bundle's content with reality after the code or docs change |
|
|
455
|
+
| `refine` | Restructure it for retrieval: evidence-first, cohesion over balance — proposes, never applies |
|
|
394
456
|
| `consume` | Use the bundle as context for a task, writing back what you learn |
|
|
395
457
|
| `curate` | Structural upkeep as it stands: `validate` + `lint` + `loose` |
|
|
396
458
|
| `doctor` | Install and verify the CLI, then doctor the bundle |
|
|
397
459
|
| `<okf-cli-verb>` | Run any CLI verb (`validate`, `lint`, `search`, `index`, `server`, the read views) and interpret its output |
|
|
398
460
|
|
|
461
|
+
Three of those look alike and are not, which is the distinction worth learning
|
|
462
|
+
first: **`curate`** keeps the bundle *sound* (the structure as it stands),
|
|
463
|
+
**`maintain`** keeps it *true* (the code changed, so the content must catch up),
|
|
464
|
+
and **`refine`** changes *where knowledge lives* — the folder a concept sits in,
|
|
465
|
+
a fact re-explained in three overviews. Reach for `refine` when nothing is wrong
|
|
466
|
+
and everything is hard to find: a folder grown to twenty concepts, a hub homed by
|
|
467
|
+
history, tags that neither connect nor mark. It reads the evidence, then hands
|
|
468
|
+
you a proposal — it never rearranges your bundle on its own.
|
|
469
|
+
|
|
399
470
|
Point it at your agent's config directory (or its skills directory) and the tree
|
|
400
471
|
settles in its own `skills/okf/` folder, so a shared skills directory never gets
|
|
401
472
|
the files loose:
|
|
@@ -513,6 +584,26 @@ Two loop concerns from the format's own guidance, _contradictions_ and _semantic
|
|
|
513
584
|
staleness, need to understand meaning and are not computed here; `lint --json`
|
|
514
585
|
is the structured input an agent consumes to reason about those.
|
|
515
586
|
|
|
587
|
+
## Extending okf
|
|
588
|
+
|
|
589
|
+
This is the other kind of plugin, and it is worth separating from the
|
|
590
|
+
[Claude Code plugin](#claude-code-plugin) below: that one teaches an *agent* to
|
|
591
|
+
use okf, while this one adds behavior to the `okf` command everyone runs.
|
|
592
|
+
|
|
593
|
+
Publish a gem named `okf-*` that carries an `okf/plugin.rb`, and installing it is
|
|
594
|
+
the whole installation: your verb answers to `okf`, appears in `okf help` under
|
|
595
|
+
`installed extensions:`, and behaves like a built-in — nothing to register,
|
|
596
|
+
nothing to configure, no list of known addons in this gem. The same seam takes
|
|
597
|
+
retrieval backends, which is what `okf search --engine NAME` chooses between.
|
|
598
|
+
|
|
599
|
+
Two promises make that safe to install: nothing an addon registers can displace a
|
|
600
|
+
built-in, and a broken addon is skipped and reported rather than taking the CLI
|
|
601
|
+
down with it.
|
|
602
|
+
|
|
603
|
+
The [extension points](.okf/design/extension-points.md) concept has the contract
|
|
604
|
+
and the threat model; `test/integration/cli/cli_plugin_test.rb` is a working
|
|
605
|
+
example to copy.
|
|
606
|
+
|
|
516
607
|
## Server trust boundary
|
|
517
608
|
|
|
518
609
|
> [!NOTE]
|
data/lib/okf/bundle/folder.rb
CHANGED
data/lib/okf/bundle.rb
CHANGED
|
@@ -119,13 +119,31 @@ module OKF
|
|
|
119
119
|
status: concept.frontmatter["status"]&.to_s,
|
|
120
120
|
backlog_ref: concept.frontmatter["backlog_ref"]&.to_s,
|
|
121
121
|
dir: File.dirname("#{id}.md"),
|
|
122
|
-
area:
|
|
122
|
+
area: area_of(id),
|
|
123
123
|
links_out: out_degree[id],
|
|
124
124
|
links_in: in_degree[id]
|
|
125
125
|
}
|
|
126
126
|
end.sort_by { |entry| entry[:id] }
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
+
# Concepts ranked by inbound link degree, each with the areas its inbound
|
|
130
|
+
# links come from — the evidence for "is this hub well-homed?": a hub whose
|
|
131
|
+
# inbound majority is foreign to its own area is a move candidate, one with
|
|
132
|
+
# a single dominant foreign area already names its better home. Only
|
|
133
|
+
# concepts with at least one inbound link appear. Pure: derived from the
|
|
134
|
+
# graph edges. Shared by the `okf graph --hubs` view.
|
|
135
|
+
def hubs
|
|
136
|
+
inbound = {}
|
|
137
|
+
graph(minimal: true).edges.each do |edge|
|
|
138
|
+
(inbound[edge[:target]] ||= Hash.new(0))[area_of(edge[:source])] += 1
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
inbound.map do |id, sources|
|
|
142
|
+
by_area = sources.sort_by { |area, count| [ -count, area ] }.to_h
|
|
143
|
+
{ id: id, area: area_of(id), inbound: by_area.values.reduce(0, :+), by_area: by_area }
|
|
144
|
+
end.sort_by { |row| [ -row[:inbound], row[:id] ] }
|
|
145
|
+
end
|
|
146
|
+
|
|
129
147
|
# The progressive-disclosure map (spec §6): one entry per directory that holds
|
|
130
148
|
# concepts or carries an index.md, sorted with the root (".") first. Each entry
|
|
131
149
|
# gives the authored index body (frontmatter stripped) when an index.md is
|
|
@@ -171,6 +189,12 @@ module OKF
|
|
|
171
189
|
|
|
172
190
|
private
|
|
173
191
|
|
|
192
|
+
# A concept's top-level area, derived from its id — the same derivation the
|
|
193
|
+
# catalog exposes, so every grouped view labels the bundle root "(root)".
|
|
194
|
+
def area_of(id)
|
|
195
|
+
id.include?("/") ? id.split("/").first : "(root)"
|
|
196
|
+
end
|
|
197
|
+
|
|
174
198
|
# Every directory to show: those holding concepts or an index.md, plus each of
|
|
175
199
|
# their ancestors up to the root, so the subdir tree stays connected even when
|
|
176
200
|
# an intermediate directory holds nothing directly. Sorted with "." first.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OKF
|
|
4
|
+
class CLI
|
|
5
|
+
# Every concept with its metadata, grouped by area. The widest of the read
|
|
6
|
+
# views, and the one the others narrow down from.
|
|
7
|
+
class Catalog < Command
|
|
8
|
+
def self.id
|
|
9
|
+
:catalog
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.group
|
|
13
|
+
:read
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.help_rows
|
|
17
|
+
[
|
|
18
|
+
[ "catalog <dir|@slug> [--json] [filters]", "list concepts with metadata, by area" ]
|
|
19
|
+
]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call(argv)
|
|
23
|
+
options = { json: false }
|
|
24
|
+
parser = OptionParser.new do |o|
|
|
25
|
+
o.banner = "Usage: okf catalog <dir|@slug> [--type T] [--area A] [--tag T] [--json]"
|
|
26
|
+
json_flags(o, options, "emit the catalog as JSON")
|
|
27
|
+
projection_flags(o, options)
|
|
28
|
+
filter_flags(o, options, :type, :area, :tag)
|
|
29
|
+
help_flag(o)
|
|
30
|
+
end
|
|
31
|
+
dir = positional_dir(parser, argv) or return 2
|
|
32
|
+
|
|
33
|
+
folder = OKF::Bundle::Folder.load(dir)
|
|
34
|
+
report_skipped(folder)
|
|
35
|
+
entries = folder.catalog
|
|
36
|
+
selected = filter_entries(entries, options)
|
|
37
|
+
return print_catalog_json(dir, selected, options) if options[:json]
|
|
38
|
+
|
|
39
|
+
print_catalog(dir, selected, entries.size)
|
|
40
|
+
0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def print_catalog(dir, entries, total)
|
|
46
|
+
@out.puts "Catalog — #{bundle_label(dir)} (#{counted(entries.size, total, "concept")})"
|
|
47
|
+
entries.group_by { |entry| entry[:area] }.sort_by(&:first).each do |area, group|
|
|
48
|
+
@out.puts
|
|
49
|
+
@out.puts " #{area == "(root)" ? "(root)" : "#{area}/"} (#{group.size})"
|
|
50
|
+
group.each do |entry|
|
|
51
|
+
links = entry[:links_out] + entry[:links_in]
|
|
52
|
+
meta = [ entry[:type], (links.positive? ? "↳#{links}" : nil), entry[:status] ].compact.join(" · ")
|
|
53
|
+
@out.puts " #{entry[:title]} · #{meta}"
|
|
54
|
+
@out.puts " #{truncate(entry[:description], 92)}" unless entry[:description].empty?
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def print_catalog_json(dir, entries, options)
|
|
60
|
+
emit_list_json(dir, "concepts", entries.map { |entry| stringify(entry) }, options)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
register(Catalog)
|
|
65
|
+
end
|
|
66
|
+
end
|