okf 1.7.0 → 1.8.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 +151 -0
- data/README.md +71 -20
- data/lib/okf/bundle/folder.rb +9 -2
- data/lib/okf/bundle/graph.rb +5 -1
- data/lib/okf/bundle/linter.rb +6 -1
- data/lib/okf/bundle/reader.rb +21 -4
- data/lib/okf/cli.rb +833 -117
- data/lib/okf/registry.rb +370 -0
- data/lib/okf/server/app.rb +19 -4
- data/lib/okf/server/graph/template.html.erb +578 -46
- data/lib/okf/server/graph.rb +28 -2
- data/lib/okf/server/hub.rb +207 -0
- data/lib/okf/skill/SKILL.md +1 -1
- data/lib/okf/skill/playbooks/search.md +3 -0
- data/lib/okf/skill/reference/cli.md +126 -8
- data/lib/okf/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6c6e09a9ce22d65f6ffe880b9e8ad3925bb648e4c985e2249ad13a83c17cc620
|
|
4
|
+
data.tar.gz: 177086b26883d9004852181dbc5f935f5112ad2b5fde3277fb29a5db766b650a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d39fa2b520b3c33e76dd3c03111da96a7668809101c035e494ad47df31f993bf661ed3bb5aa0d3b3e9cba81b5087ef95a4398082954033949a20a91ff152feb5
|
|
7
|
+
data.tar.gz: 6109c082b1d89144bd6f60ddedf93a70af41ca23914d5cf9cc243acad3b418211317c988cd2e28fa23632187c79ab629b1b6473a8d6f0d711700db4736c3898f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,156 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [1.8.0] - 2026-07-17
|
|
6
|
+
|
|
7
|
+
- A persistent bundle registry and a multi-bundle hub. `okf registry`
|
|
8
|
+
(list / set / del / default / rename) keeps a per-user list in a plain JSON
|
|
9
|
+
file at `$OKF_HOME/registry.json` (default `~/.okf`), and `okf server` reads
|
|
10
|
+
its mode from its arguments: one dir is the classic single bundle at `/`,
|
|
11
|
+
several mount ephemerally behind a hub at `/b/<slug>/`, none serves the whole
|
|
12
|
+
registry with its default at `/`. Behind a hub the page gains a bundle
|
|
13
|
+
switcher (⌘/Ctrl-K, or the rail button), `/b/` is a browsable index, and an
|
|
14
|
+
unknown slug 404s as a page with a way home. The hub reads its bundles at
|
|
15
|
+
boot — restart after registry changes.
|
|
16
|
+
- The registry is **ordered, and the first entry still on disk is the default** —
|
|
17
|
+
the bundle a bare `okf server` opens at `/`. `okf registry default @slug` moves
|
|
18
|
+
that entry to the front, and `okf registry set --default` registers straight to
|
|
19
|
+
it; until you do either, the first bundle you registered is the default. Nothing
|
|
20
|
+
else has to be maintained: a rename keeps its position, a `del` promotes
|
|
21
|
+
whatever is next, and the file cannot name a default that is not there. A
|
|
22
|
+
vanished directory is stepped over rather than starred — `registry list`'s `*`
|
|
23
|
+
always names the bundle `/` opens — and `registry default @slug` refuses one,
|
|
24
|
+
just as `registry set` refuses to register a directory that is not there.
|
|
25
|
+
- `$OKF_HOME` is the single lever on which registry a command reads: set it and
|
|
26
|
+
every verb follows, from `okf registry list` to an `@slug` on `okf lint`. It
|
|
27
|
+
names exactly one registry, with no fallback to `~/.okf` behind it, and an
|
|
28
|
+
empty value counts as unset rather than planting `registry.json` in the
|
|
29
|
+
current directory.
|
|
30
|
+
- `@slug`: wherever a command takes a `<dir>`, `@slug` names a registered
|
|
31
|
+
bundle and bare `@` the registry default — `okf lint @handbook`,
|
|
32
|
+
`okf render @ -o graph.html`. A slug is normalized like registration was
|
|
33
|
+
(`@One` finds the bundle from dir `One`) but never to a placeholder, so
|
|
34
|
+
`@***` is a bad ref rather than a silent hit. An unknown slug, a
|
|
35
|
+
registered-but-gone directory, or a malformed registry file is a usage error
|
|
36
|
+
naming the registry file and the next move. A hub built from `@slug`s
|
|
37
|
+
(`okf server @a @b`) mounts each bundle under its registered slug, the first
|
|
38
|
+
at `/`, and a registered slug reserves its mount ahead of any plain
|
|
39
|
+
directory that shares the name.
|
|
40
|
+
- `@slug` is spelled where it is used, not just where it is explained, and it is
|
|
41
|
+
the one token — `okf help`'s map (`lint <dir|@slug>`) and each command's own
|
|
42
|
+
banner show it the same way, with a note under the map defining it: the slug
|
|
43
|
+
from `okf registry set`, or bare `@` for the default. It was documented once,
|
|
44
|
+
in prose at the foot of `okf help`, past where a reader who already knows the
|
|
45
|
+
verb ever looks, so seventeen surfaces took a registered bundle while
|
|
46
|
+
advertising a bare `<dir>`. The registry-editing verbs — `del`, `default`,
|
|
47
|
+
`rename` — take the slug bare (no `@`) or as an `@slug`; the read verbs need
|
|
48
|
+
the `@`, since a bare word there is a path.
|
|
49
|
+
- `okf <command> -h` prints that command's own banner and flags. Help now answers
|
|
50
|
+
on stdout with an exit code like every other command: it was OptionParser's
|
|
51
|
+
officious handler, which printed past the caller's injected streams and ended
|
|
52
|
+
the process with `exit` instead of returning a status.
|
|
53
|
+
- `okf search` spans bundles: several leading `@slug`s, or `@all` for every
|
|
54
|
+
registered one. Rankings merge across bundles with every row labeled by its
|
|
55
|
+
bundle's slug (a `bundles` list and a per-match `slug` key in the JSON).
|
|
56
|
+
Asking for everything tolerates gaps — `@all` skips a bundle whose directory
|
|
57
|
+
has vanished, with a note — while naming one insists on it, and `@all @docs`
|
|
58
|
+
simply dedupes. `all` is reserved *in the registry*, on all three ways in: a
|
|
59
|
+
directory named `all/` registers as `all-2`, `--as all` is refused, and a row
|
|
60
|
+
already claiming the name in the registry file — hand-typed, or written before
|
|
61
|
+
the name was reserved — is read as `all-2`, so the reservation never strands a
|
|
62
|
+
registry it inherited. An ephemeral `okf server ./all` still mounts at
|
|
63
|
+
`/b/all/` — no registry, no refs, nothing to reserve.
|
|
64
|
+
- The registry validates its file's shape, not just its JSON syntax: a
|
|
65
|
+
hand-edited entry missing `path` is a usage error naming the file instead of
|
|
66
|
+
a `TypeError`, and `okf registry --json set <dir>` — a subcommand behind a
|
|
67
|
+
flag — is a usage error rather than silently listing and exiting 0.
|
|
68
|
+
- The graph page's ⌘/Ctrl-K palette opens in **every** mode and reaches a view as
|
|
69
|
+
well as a bundle. It was wired only behind a hub, so a standalone
|
|
70
|
+
`okf server ./docs` and every `okf render` page — the two modes most people meet
|
|
71
|
+
first — had no palette at all. Bundles still lead it and own the empty box,
|
|
72
|
+
because switching bundles is what it is for; views wait until what you type
|
|
73
|
+
reaches one, arrive underneath, and stay muted until the cursor does. Each view
|
|
74
|
+
carries the rail's own icon and label, read from the rail so the two cannot
|
|
75
|
+
drift. Where there is no hub there is no bundle to switch to, and views become
|
|
76
|
+
the whole list.
|
|
77
|
+
- The inspector's type and tags are filter handles: clicking one focuses the
|
|
78
|
+
graph on that facet — the same jump the stats bars make — and clicking it again
|
|
79
|
+
clears it. The chip lights while its facet is the only filter in play, which is
|
|
80
|
+
exactly when a second click is an undo, so what you see and what the next click
|
|
81
|
+
does are the same question. With another filter set it re-focuses instead,
|
|
82
|
+
rather than throwing away more than the click put there.
|
|
83
|
+
- The inspector's *Links to* / *Linked from* rows read as concepts rather than a
|
|
84
|
+
wall of accent-coloured text. Each carries its type's dot — the colour that node
|
|
85
|
+
already wears in the graph beside it — with the type named and the section
|
|
86
|
+
counted, so the column answers what kind of neighbourhood a concept has before a
|
|
87
|
+
title is read. The rows share one panel with hairline dividers and a hover fill:
|
|
88
|
+
the container carries the click affordance, which leaves colour free to mean type
|
|
89
|
+
and nothing else.
|
|
90
|
+
- The inspector's widen chevron splits the screen instead of taking 70% of it. The
|
|
91
|
+
panel drag-resizes, so the chevron is a preset rather than a maximum, and burying
|
|
92
|
+
the graph to read one concept was the wrong thing to default to.
|
|
93
|
+
- The graph page answers `?` with a sheet of every keyboard shortcut, reachable
|
|
94
|
+
from a rail button too — a shortcut list you can only open with a shortcut helps
|
|
95
|
+
whoever needs it least. `/` focuses the current view's search where it has one,
|
|
96
|
+
skipping the view that only reads; the sheet is written against the key handler
|
|
97
|
+
it documents, since a shortcut list that has drifted is worse than none.
|
|
98
|
+
- Fixed: a file the reader could not **open** (permissions) threw its errno out
|
|
99
|
+
of the read, so a single locked file took the whole bundle down through every
|
|
100
|
+
verb that reads one — `lint`, `validate`, `catalog`, `server`, `registry set`
|
|
101
|
+
— as a backtrace, under an exit code claiming the bundle was non-conformant.
|
|
102
|
+
§9's best-effort promise covers it now, the same as frontmatter that will not
|
|
103
|
+
parse: the file is skipped, noted on stderr, and reported by `validate` under
|
|
104
|
+
§9.1 naming the file and the errno. One bad file never breaks the rest.
|
|
105
|
+
The stderr note reads `skipped N unusable file(s)` — it counts two kinds now,
|
|
106
|
+
so it names neither and points at `validate`, which names both.
|
|
107
|
+
|
|
108
|
+
- Fixed: `okf registry del <path>` could delete the wrong bundle. A path that
|
|
109
|
+
matched no registered directory fell through to a normalized *slug* lookup, so
|
|
110
|
+
`del ./notes` — naming a local directory — removed whichever entry happened to
|
|
111
|
+
be slugged `notes`, wherever it pointed, and reported `removed notes` with exit
|
|
112
|
+
0. An argument with a `/` in it now names a location and only a location.
|
|
113
|
+
- Fixed: the registry read trusted stored slugs verbatim while both write paths
|
|
114
|
+
normalized, so a hand-typed `"slug": "My Docs"` listed fine but could not be
|
|
115
|
+
named by `@my-docs`, `registry rename`, or `registry default` — the verbs that
|
|
116
|
+
could repair it were the ones that could not see it. The read normalizes now,
|
|
117
|
+
leaving an already-usable slug untouched. This also removes the only way a
|
|
118
|
+
quote could reach a slug, and with it a DOM XSS in the server's bundle
|
|
119
|
+
switcher, whose JS escape covered `& < >` but not quotes; the escape now covers
|
|
120
|
+
quotes too, so the page does not depend on a guarantee three layers away.
|
|
121
|
+
- Fixed: `okf lint` bucketed a whitespace-only `type` under its own literal
|
|
122
|
+
heading while `types`/`graph`/`stats` bucketed it as `Untyped`, so two verbs
|
|
123
|
+
reported type inventories for the same bundle that would not reconcile. §9.2
|
|
124
|
+
makes a blank type as non-conformant as a missing one; both sides say so now.
|
|
125
|
+
- Fixed: `okf search <dir> --fields slug` passed the field guard and returned one
|
|
126
|
+
empty object per match with exit 0. Only registry mode labels rows with a slug,
|
|
127
|
+
so the two modes now declare the shape each actually emits and a path-named
|
|
128
|
+
search names the fields it does have.
|
|
129
|
+
- Fixed: `okf registry set` reported `(0 concepts)` for a bundle whose files it
|
|
130
|
+
could not read; it notes the skipped files like every other reading verb.
|
|
131
|
+
`okf registry rename DOCS handbook` echoed the argv rather than the slug it
|
|
132
|
+
renamed, naming a bundle that never existed. An unwritable `$OKF_HOME` raised
|
|
133
|
+
a bare `Errno::EACCES` at exit 1 instead of a usage error at exit 2.
|
|
134
|
+
- Fixed: a long path in the Files view's Indexes tab pushed its `map`/`log` badge
|
|
135
|
+
past the right edge of the list. The badge was already pinned and unshrinkable;
|
|
136
|
+
the filename beside it was the problem — a bare text node, and an anonymous flex
|
|
137
|
+
item's automatic minimum size is the full width of its text, so it refused to
|
|
138
|
+
give way and drove the badge out instead. It truncates now, with the full path on
|
|
139
|
+
hover. Only a wide enough path in a narrow enough pane reached it, which is why
|
|
140
|
+
it never showed on mobile, where the list runs full width.
|
|
141
|
+
- Fixed: the bundle switcher scrolled its own first row out of view as it opened.
|
|
142
|
+
It rendered, and scrolled the active row into view, while the dialog was still
|
|
143
|
+
hidden — and a list that is not being displayed measures zero, so the scroll
|
|
144
|
+
landed arbitrarily.
|
|
145
|
+
- Fixed: the `3` (Files) shortcut did nothing once the Indexes tab was open, and
|
|
146
|
+
the palette's Index row would have blanked the page. "Index" is not a view —
|
|
147
|
+
there is no `#view-index`, only the Files view showing its Indexes tab — so
|
|
148
|
+
`setView('files')` from that tab early-returned, and `setView('index')` named a
|
|
149
|
+
view that does not exist. The keyboard and the palette each re-implemented what
|
|
150
|
+
the rail button already did right; both now click the rail item, so the one
|
|
151
|
+
correct path is the only one, and the palette's `current` badge reads the same
|
|
152
|
+
active-tab answer the rail's own highlight does.
|
|
153
|
+
|
|
3
154
|
## [1.7.0] - 2026-07-16
|
|
4
155
|
|
|
5
156
|
- `okf server`: responses are gzipped when the client accepts it
|
data/README.md
CHANGED
|
@@ -35,7 +35,7 @@ The package, end to end:
|
|
|
35
35
|
<p align="center">
|
|
36
36
|
<picture>
|
|
37
37
|
<source media="(prefers-color-scheme: dark)" srcset=".github/overview-dark.png">
|
|
38
|
-
<img src=".github/overview-light.png" width="760" alt="The package: the Agent Skill (your coding agent authors and curates, you stay the editor) writes and maintains the bundle, a folder of Markdown + YAML in your repo where one concept is one file and links between files are the knowledge graph. The bundle is read by the CLI/Lib (validate: legal OKF per section 9; lint: well-curated and fresh; search: ranked retrieval; require okf for Ruby objects) and by the Graph, in
|
|
38
|
+
<img src=".github/overview-light.png" width="760" alt="The package: the Agent Skill (your coding agent authors and curates, you stay the editor) writes and maintains the bundle, a folder of Markdown + YAML in your repo where one concept is one file and links between files are the knowledge graph. The bundle is read by the CLI/Lib (validate: legal OKF per section 9; lint: well-curated and fresh; search: ranked retrieval; require okf for Ruby objects) and by the Graph, in four modes: okf server (a live local server), okf render (the same page exported as one static, self-contained HTML file you can host anywhere), okf registry (every registered bundle behind one hub), and OKF::Server::App (the Rack app mounted in a Rails route). One gem, 100% local, Ruby 2.4 or newer, only rack and webrick as dependencies.">
|
|
39
39
|
</picture>
|
|
40
40
|
</p>
|
|
41
41
|
|
|
@@ -44,13 +44,20 @@ command-line tool (the library API is also usable in-process). Each capability
|
|
|
44
44
|
below links to the concept that documents it: this gem's own knowledge is an OKF
|
|
45
45
|
bundle, so you can read its design in the format it defends.
|
|
46
46
|
|
|
47
|
-
| Capability
|
|
48
|
-
|
|
|
49
|
-
| [Companion agent skill](.okf/capabilities/agent-skill.md)
|
|
50
|
-
| [Conformance validator](.okf/capabilities/validator.md)
|
|
51
|
-
| [Curation linter](.okf/capabilities/linter.md)
|
|
52
|
-
| [
|
|
53
|
-
| [
|
|
47
|
+
| Capability | What it answers | Verb |
|
|
48
|
+
| ------------------------------------------------------------- | --------------------------------- | ---------------- |
|
|
49
|
+
| [Companion agent skill](.okf/capabilities/agent-skill.md) | Can an agent author it? | `skill` |
|
|
50
|
+
| [Conformance validator](.okf/capabilities/validator.md) | Is this a legal OKF bundle? (§9) | `validate` |
|
|
51
|
+
| [Curation linter](.okf/capabilities/linter.md) | Is it navigable, complete, fresh? | `lint` / `loose` |
|
|
52
|
+
| [Ranked text search](.okf/capabilities/search.md) | Which concept covers X? | `search` |
|
|
53
|
+
| [Interactive graph server](.okf/capabilities/graph-server.md) | Can I explore it visually? | `server` |
|
|
54
|
+
| [Static render](.okf/capabilities/render.md) | Can I ship a serverless snapshot? | `render` |
|
|
55
|
+
| [Library API](.okf/capabilities/library-api.md) | Can my Ruby program use it? | in-process |
|
|
56
|
+
|
|
57
|
+
And because knowledge rarely lives in one bundle, a per-user
|
|
58
|
+
[registry](.okf/registry.md) gives each bundle a name: `okf registry set ./docs`
|
|
59
|
+
once, then `@docs` works anywhere a `<dir>` does — from any directory — and a
|
|
60
|
+
bare `okf server` hosts every registered bundle behind one hub.
|
|
54
61
|
|
|
55
62
|
> [!TIP]
|
|
56
63
|
> **Browse the gem as knowledge, not just docs.** This README is the front door;
|
|
@@ -218,7 +225,7 @@ docker run --rm -v "$PWD:/data" -p 8808:8808 ghcr.io/serradura/okf server . --bi
|
|
|
218
225
|
Then open <http://127.0.0.1:8808>. Images are published for `linux/amd64` and
|
|
219
226
|
`linux/arm64` on
|
|
220
227
|
[ghcr.io](https://github.com/serradura/okf-gem/pkgs/container/okf): `:latest`
|
|
221
|
-
tracks the newest release, or pin a version like `:1.
|
|
228
|
+
tracks the newest release, or pin a version like `:1.8.0`.
|
|
222
229
|
|
|
223
230
|
Tired of the long line? Install a Docker-backed [`okf` command](https://docker.okfgem.com),
|
|
224
231
|
so every verb drops the `docker run` prefix and reads exactly like the native CLI
|
|
@@ -237,15 +244,30 @@ instead: `irm https://docker.okfgem.com/install.ps1 | iex`.
|
|
|
237
244
|
## Command line
|
|
238
245
|
|
|
239
246
|
```bash
|
|
240
|
-
okf validate <dir> [--json]
|
|
241
|
-
okf lint <dir> [--json] [--fail-on warn] [...]
|
|
242
|
-
okf loose <dir> [--json]
|
|
243
|
-
okf search <dir> <term…> [-e] [
|
|
244
|
-
okf index <dir> [--json] [--area A] [--no-body]
|
|
245
|
-
okf server
|
|
246
|
-
okf render <dir> [-o FILE] [--layout NAME] [...]
|
|
247
|
-
okf
|
|
248
|
-
okf
|
|
247
|
+
okf validate <dir|@slug> [--json] # check OKF v0.1 conformance (§9)
|
|
248
|
+
okf lint <dir|@slug> [--json] [--fail-on warn] [...] # report curation-quality issues
|
|
249
|
+
okf loose <dir|@slug> [--json] # list files with no graph links, by folder
|
|
250
|
+
okf search <dir|@slug…|@all> <term…> [-e] [...] # ranked retrieval; @slugs or @all span bundles
|
|
251
|
+
okf index <dir|@slug> [--json] [--area A] [--no-body] # progressive-disclosure map (§6): bodies, rollups, listings
|
|
252
|
+
okf server [DIR|@slug…] [-p PORT] [--bind ADDR] [...] # serve one bundle, or many behind a hub (⌘K to switch)
|
|
253
|
+
okf render <dir|@slug> [-o FILE] [--layout NAME] [...] # export the graph as one static, self-contained HTML file
|
|
254
|
+
okf registry list [--json] # list registered bundles (* marks the default)
|
|
255
|
+
okf registry set <dir|@slug> [--as SLUG] [--default] # add or update a bundle (a bare `server` serves it)
|
|
256
|
+
okf registry del <dir|@slug> # remove a bundle from the registry
|
|
257
|
+
okf registry default <@slug> | rename <@slug> <new> # move a bundle to the front (the default) / rename it
|
|
258
|
+
|
|
259
|
+
# The registry is a plain JSON file at $OKF_HOME/registry.json (default ~/.okf).
|
|
260
|
+
# It is ordered, and the first entry is the default: `registry default @slug`
|
|
261
|
+
# moves that entry to the front. The subcommand leads and flags follow it:
|
|
262
|
+
# `registry set <dir> --as X`, never `registry --json set <dir>`.
|
|
263
|
+
# A running server reads it at boot — restart after changes.
|
|
264
|
+
# Behind a multi-bundle server, /b/ lists every bundle and ⌘/Ctrl-K switches.
|
|
265
|
+
# @slug names a registered bundle instead of a path — the slug from `registry
|
|
266
|
+
# set`, or bare @ for the default. Anywhere a <dir> goes, an @slug goes:
|
|
267
|
+
# okf lint @handbook works from anywhere. Set $OKF_HOME to point every verb
|
|
268
|
+
# at another registry.
|
|
269
|
+
okf graph <dir|@slug> [--json] [--minimal] [--no-body] # print the knowledge graph
|
|
270
|
+
okf catalog | files | tags | types | stats <dir|@slug> [--json] # the browser views, on the CLI
|
|
249
271
|
okf skill <dest> [--here] [--force] # install the companion agent skill
|
|
250
272
|
okf --version
|
|
251
273
|
```
|
|
@@ -276,10 +298,39 @@ _The graph server on this repo's own [`.okf`](.okf) bundle, with the
|
|
|
276
298
|
`capabilities/graph-server` concept selected. Try it live at
|
|
277
299
|
**[demo.okfgem.com](https://demo.okfgem.com)**._
|
|
278
300
|
|
|
301
|
+
The page is one template from a phone to a desktop: on small screens the
|
|
302
|
+
navigation rail becomes a drawer, the toolbar folds into a `⚙` sheet, and the
|
|
303
|
+
panels go full-bleed — rotate a tablet and the layout re-evaluates. It is
|
|
304
|
+
keyboard-first too: `⌘/Ctrl-K` opens a command palette in every mode (views
|
|
305
|
+
always; bundles too when a [hub](#one-registry-many-bundles) is serving), `/`
|
|
306
|
+
jumps to the current view's search, and `?` answers with a sheet of every
|
|
307
|
+
shortcut.
|
|
308
|
+
|
|
279
309
|
To skip the server entirely, **`okf render <dir>`** writes that same page as one
|
|
280
310
|
self-contained HTML file, the whole bundle baked in, so you can publish the
|
|
281
311
|
graph on GitHub Pages or any static host.
|
|
282
312
|
|
|
313
|
+
### One registry, many bundles
|
|
314
|
+
|
|
315
|
+
The [registry](.okf/registry.md) is a per-user, ordered list of bundles in one
|
|
316
|
+
plain JSON file (`$OKF_HOME/registry.json`, default `~/.okf`) — hand-editable,
|
|
317
|
+
greppable, no database. It stores references, never content: the bundles stay in
|
|
318
|
+
the repos that own them.
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
okf registry set ./docs --as handbook # give the bundle a name
|
|
322
|
+
okf lint @handbook # @slug works wherever a <dir> does, from anywhere
|
|
323
|
+
okf search @all rate limit # ranked retrieval across every registered bundle
|
|
324
|
+
okf server # no args: the whole registry behind one hub
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
The first entry still on disk is the **default** — the bundle a bare `okf
|
|
328
|
+
server` opens at `/` and a bare `@` names; `okf registry default @slug` moves an
|
|
329
|
+
entry to the front. Behind the hub each bundle mounts at `/b/<slug>/`, `/b/` is
|
|
330
|
+
a browsable index, and the `⌘/Ctrl-K` palette switches bundles without leaving
|
|
331
|
+
the page. The hub reads the registry at boot, so restart it after registry
|
|
332
|
+
changes; set `$OKF_HOME` to point every verb at a different registry.
|
|
333
|
+
|
|
283
334
|
`graph` and `server` are best-effort (§9): a file with invalid frontmatter is
|
|
284
335
|
skipped (and noted on stderr), not fatal, so one bad file never breaks the rest.
|
|
285
336
|
The [graph server](.okf/capabilities/graph-server.md) concept walks the request
|
|
@@ -519,8 +570,8 @@ ruby -Ilib exe/okf validate <dir> # run the CLI from a checkout
|
|
|
519
570
|
The suite runs on every supported Ruby; to check the 2.4 floor locally:
|
|
520
571
|
|
|
521
572
|
```bash
|
|
522
|
-
docker run --rm -v "$PWD":/
|
|
523
|
-
|
|
573
|
+
docker run --rm -v "$PWD":/src:ro ruby:2.4 bash -c \
|
|
574
|
+
"cp -a /src /build && cd /build && rm -f Gemfile.lock && bundle install --quiet && bundle exec rake test"
|
|
524
575
|
```
|
|
525
576
|
|
|
526
577
|
## Contributing
|
data/lib/okf/bundle/folder.rb
CHANGED
|
@@ -51,11 +51,18 @@ module OKF
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
# Human-readable "parent/dir" name — the default HTML title.
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
# The bundle's display label, "parent/dir" — path arithmetic, no disk. It
|
|
55
|
+
# is a class method so a caller that only wants the label (the registry
|
|
56
|
+
# naming an entry) can have it without a Reader.read of every file.
|
|
57
|
+
def self.label(root)
|
|
58
|
+
pathname = Pathname.new(root)
|
|
56
59
|
"#{pathname.parent.basename}/#{pathname.basename}"
|
|
57
60
|
end
|
|
58
61
|
|
|
62
|
+
def name
|
|
63
|
+
self.class.label(@root)
|
|
64
|
+
end
|
|
65
|
+
|
|
59
66
|
# A single-file handle for one concept id (read live from disk), or nil when no
|
|
60
67
|
# concept in the loaded bundle has that id. The id may be a frontmatter `id`, so
|
|
61
68
|
# it is resolved to a path through the bundle rather than assumed to be "id.md".
|
data/lib/okf/bundle/graph.rb
CHANGED
|
@@ -86,8 +86,12 @@ module OKF
|
|
|
86
86
|
concept.tags.is_a?(Array) ? concept.tags : []
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
+
# Blank, not just nil: §9.2 makes a whitespace-only `type` as non-conformant
|
|
90
|
+
# as a missing one (the validator says so with the same OKF.blank?), so the
|
|
91
|
+
# index must not sort them into different buckets. Otherwise `type: " "`
|
|
92
|
+
# earns its own row, labelled with spaces, next to Untyped.
|
|
89
93
|
def self.default(value, fallback)
|
|
90
|
-
|
|
94
|
+
OKF.blank?(value) ? fallback : value.to_s
|
|
91
95
|
end
|
|
92
96
|
|
|
93
97
|
def initialize(nodes:, edges:, type_index: {}, tag_index: {})
|
data/lib/okf/bundle/linter.rb
CHANGED
|
@@ -290,7 +290,12 @@ module OKF
|
|
|
290
290
|
@report.stat(:backlog, count_findings(:missing_concept))
|
|
291
291
|
@report.stat(:components, components.size)
|
|
292
292
|
@report.stat(:hubs, hubs)
|
|
293
|
-
|
|
293
|
+
# Through Graph.default, not a second `|| "Untyped"`: §9.2 makes a
|
|
294
|
+
# whitespace-only type as non-conformant as a missing one, so the two must
|
|
295
|
+
# land in one bucket. Spelling the rule twice is how lint came to report a
|
|
296
|
+
# `" "` bucket that `types` and `graph` had never heard of — the same
|
|
297
|
+
# concepts counted by both verbs, into inventories that will not reconcile.
|
|
298
|
+
@report.stat(:types, frequency(@concepts.map { |c| Graph.default(c.type, "Untyped") }))
|
|
294
299
|
@report.stat(:tags, frequency(@concepts.flat_map { |c| c.tags.is_a?(Array) ? c.tags : [] }))
|
|
295
300
|
end
|
|
296
301
|
|
data/lib/okf/bundle/reader.rb
CHANGED
|
@@ -8,10 +8,14 @@ module OKF
|
|
|
8
8
|
#
|
|
9
9
|
# It parses eagerly: each concept file becomes an OKF::Concept, each
|
|
10
10
|
# index.md/log.md is kept as raw text (its structure is validated as text), and
|
|
11
|
-
# a
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
11
|
+
# a file the reader cannot use — frontmatter that does not parse, or a file it
|
|
12
|
+
# cannot open at all — is retained as an unparseable entry (carrying the
|
|
13
|
+
# ParseError message or the errno, so §9.1 can report it) rather than dropped
|
|
14
|
+
# or raised. That tolerance is the whole §9 best-effort promise: one bad file
|
|
15
|
+
# never breaks the rest, and this is the read every verb shares. Every read
|
|
16
|
+
# goes through Path.join_under! so a symlinked or crafted path cannot escape
|
|
17
|
+
# the bundle root — that guard still raises, because a path leaving the root
|
|
18
|
+
# is not a bad file, it is a bundle lying about its shape.
|
|
15
19
|
class Reader
|
|
16
20
|
def self.read(dir)
|
|
17
21
|
new(dir).read
|
|
@@ -39,6 +43,19 @@ module OKF
|
|
|
39
43
|
end
|
|
40
44
|
rescue Markdown::Frontmatter::ParseError => e
|
|
41
45
|
unparseable << Entry.new(path: path, content: content, error: e.message)
|
|
46
|
+
rescue SystemCallError => e
|
|
47
|
+
# A file that cannot be opened is one unusable file, not a broken
|
|
48
|
+
# bundle. Letting the errno out of here breaks "one bad file never
|
|
49
|
+
# breaks the rest" for every verb at once — the read is the one path
|
|
50
|
+
# they all share — and it breaks it in the worst way: a backtrace,
|
|
51
|
+
# under an exit code that claims the bundle is non-conformant. So it
|
|
52
|
+
# joins the same bucket a bad frontmatter block does, and §9.1 reports
|
|
53
|
+
# it naming the file and the errno.
|
|
54
|
+
#
|
|
55
|
+
# Its content is "" rather than nil: unknown, but every analyzer reads
|
|
56
|
+
# it as text, and empty is the honest shape of a file we never saw —
|
|
57
|
+
# no links to resolve, no encoding to be invalid, nothing claimed.
|
|
58
|
+
unparseable << Entry.new(path: path, content: "", error: e.message)
|
|
42
59
|
end
|
|
43
60
|
end
|
|
44
61
|
|