okf 2.0.0 → 2.1.1
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/agent-skill.md +112 -0
- data/.okf/capabilities/bundles-manager.md +133 -0
- data/.okf/capabilities/graph-server.md +678 -0
- data/.okf/capabilities/index.md +26 -0
- data/.okf/capabilities/library-api.md +82 -0
- data/.okf/capabilities/linter.md +83 -0
- data/.okf/capabilities/read-views.md +228 -0
- data/.okf/capabilities/render.md +66 -0
- data/.okf/capabilities/search.md +295 -0
- data/.okf/capabilities/validator.md +60 -0
- data/.okf/cli.md +193 -0
- data/.okf/design/browser-tests.md +211 -0
- data/.okf/design/core-shell-split.md +73 -0
- data/.okf/design/index.md +17 -0
- data/.okf/design/integration-first.md +140 -0
- data/.okf/design/packaging.md +65 -0
- data/.okf/design/ruby-floor.md +53 -0
- data/.okf/design/runtime-dependencies.md +82 -0
- data/.okf/design/search-engines.md +154 -0
- data/.okf/design/server-trust-boundary.md +139 -0
- data/.okf/index.md +40 -0
- data/.okf/log.md +662 -0
- data/.okf/model/bundle.md +47 -0
- data/.okf/model/concept.md +75 -0
- data/.okf/model/graph.md +59 -0
- data/.okf/model/index.md +9 -0
- data/.okf/model/skeleton.md +76 -0
- data/.okf/overview.md +87 -0
- data/.okf/registry.md +265 -0
- data/.okf/structure/format-layer.md +59 -0
- data/.okf/structure/index.md +22 -0
- data/.okf/structure/search.md +53 -0
- data/.okf/structure/the-analysers.md +60 -0
- data/.okf/structure/the-cli.md +99 -0
- data/.okf/structure/the-disk-shell.md +67 -0
- data/.okf/structure/the-model.md +81 -0
- data/.okf/structure/the-server.md +74 -0
- data/.okf/structure/the-skill.md +52 -0
- data/.okf/testing/adding-a-verb.md +76 -0
- data/.okf/testing/index.md +12 -0
- data/.okf/testing/the-harness.md +45 -0
- data/CHANGELOG.md +83 -16
- data/README.md +205 -21
- data/lib/okf/cli.rb +20 -2
- data/lib/okf/skill/SKILL.md +15 -7
- data/lib/okf/skill/playbooks/maintain.md +3 -1
- data/lib/okf/skill/playbooks/produce.md +4 -2
- data/lib/okf/skill/reference/authoring.md +23 -26
- data/lib/okf/skill/reference/cli/checks.md +171 -0
- data/lib/okf/skill/reference/cli/graph.md +49 -0
- data/lib/okf/skill/reference/cli/map.md +98 -0
- data/lib/okf/skill/reference/cli/registry.md +70 -0
- data/lib/okf/skill/reference/cli/search.md +130 -0
- data/lib/okf/skill/reference/cli/serve.md +83 -0
- data/lib/okf/skill/reference/cli/views.md +59 -0
- data/lib/okf/skill/reference/cli.md +33 -603
- data/lib/okf/skill/reference/spec-map.md +32 -0
- data/lib/okf/version.rb +1 -1
- metadata +54 -5
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Component
|
|
3
|
+
title: OKF::Bundle
|
|
4
|
+
description: The pure in-memory collection of concepts that validate, lint, and graph run over.
|
|
5
|
+
resource: gems/okf/lib/okf/bundle.rb
|
|
6
|
+
tags: [pure]
|
|
7
|
+
generated:
|
|
8
|
+
by: human:maintainer
|
|
9
|
+
at: 2026-08-13T12:00:00Z
|
|
10
|
+
sources:
|
|
11
|
+
- title: gems/okf/lib/okf/bundle.rb
|
|
12
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf/lib/okf/bundle.rb
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Overview
|
|
16
|
+
|
|
17
|
+
`OKF::Bundle` is a set of [concepts](concept.md) held together in memory, with no
|
|
18
|
+
disk involved. It is the object the three judging capabilities operate on:
|
|
19
|
+
`#validate`, `#lint`, and `#graph` each hand the bundle to a dedicated pure
|
|
20
|
+
class and return a result. A bundle also carries the reserved files
|
|
21
|
+
(`index.md`, `log.md`) and — importantly — an `unparseable` list.
|
|
22
|
+
|
|
23
|
+
# Best-effort by construction
|
|
24
|
+
|
|
25
|
+
When a bundle is built from disk, files the reader cannot use do not vanish and do
|
|
26
|
+
not abort the build: they are collected in `bundle.unparseable` — frontmatter that
|
|
27
|
+
would not parse, and files that would not open, since a locked file is one file's
|
|
28
|
+
problem and not the bundle's. That is what
|
|
29
|
+
lets [graph](graph.md), the [server](../capabilities/graph-server.md), and the
|
|
30
|
+
[read views](../capabilities/read-views.md) render everything that *is* valid
|
|
31
|
+
while the [CLI](../cli.md) notes the skips on stderr — §11's best-effort posture,
|
|
32
|
+
made structural.
|
|
33
|
+
|
|
34
|
+
# Build it from data, not only from files
|
|
35
|
+
|
|
36
|
+
Because the bundle is pure, an embedding application can construct concepts
|
|
37
|
+
straight from its own records — no Markdown round-trip — and still get validate,
|
|
38
|
+
lint, and graph for free. This is the surface the
|
|
39
|
+
[library API](../capabilities/library-api.md) exposes to, say, a Rails store that
|
|
40
|
+
already holds knowledge as rows. It also feeds the shared `#catalog`, the data
|
|
41
|
+
behind every read view — and the rollups beside it, each kept in one home
|
|
42
|
+
after a hand copy diverged: `#stats` (whose `by_dir` reads the directory
|
|
43
|
+
index, so an empty directory reports its honest zero), `#tag_groups` (the
|
|
44
|
+
vocabulary-curation cut, within-group counts beside cross-set totals), and
|
|
45
|
+
`Bundle::References` (the §6.3 inventory — the one model that sees a bundle's
|
|
46
|
+
non-markdown files, fed a manifest by the shell). The CLI verbs and the MCP
|
|
47
|
+
tools consume the same three.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Component
|
|
3
|
+
title: OKF::Concept
|
|
4
|
+
description: The pure in-memory model of a single OKF file — frontmatter, body, and a stable id.
|
|
5
|
+
resource: gems/okf/lib/okf/concept.rb
|
|
6
|
+
tags: [pure]
|
|
7
|
+
generated:
|
|
8
|
+
by: human:maintainer
|
|
9
|
+
at: 2026-08-14T12:00:00Z
|
|
10
|
+
sources:
|
|
11
|
+
- title: gems/okf/lib/okf/concept.rb
|
|
12
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf/lib/okf/concept.rb
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Overview
|
|
16
|
+
|
|
17
|
+
`OKF::Concept` is the atomic node: a `path`, a parsed
|
|
18
|
+
`frontmatter` (`@okf-eco format/frontmatter`) hash, and a Markdown `body`. It is
|
|
19
|
+
[pure](../design/core-shell-split.md) — it holds no file handle and does no I/O.
|
|
20
|
+
The on-disk counterpart is `OKF::Concept::File`, part of the
|
|
21
|
+
[library API](../capabilities/library-api.md).
|
|
22
|
+
|
|
23
|
+
# The id is the concept's identity
|
|
24
|
+
|
|
25
|
+
`#id` is the concept's **stable identifier** across the whole system — the graph
|
|
26
|
+
node key, the link target, and the thing you name a concept for. By default it is
|
|
27
|
+
the `path` minus `.md` (e.g. `model/graph.md` → `model/graph`), which is why you
|
|
28
|
+
name a file for what it *is*, not where it sits. A frontmatter `id`, when set,
|
|
29
|
+
pins the identity explicitly — the path-derived name is the fallback, not the only
|
|
30
|
+
source — so a concept can keep its id across a move.
|
|
31
|
+
|
|
32
|
+
The override is this gem's **extension, not spec**: §2 defines the concept id
|
|
33
|
+
as the path minus `.md`, full stop. Pinning one puts the concept in two worlds
|
|
34
|
+
on purpose — the identity views ([catalog, hubs, `--dir`,
|
|
35
|
+
search](../capabilities/read-views.md)) follow the id, because the edges do,
|
|
36
|
+
while the physical views (`index`, `dirs`, stats' `by_dir`) keep the file where
|
|
37
|
+
it lives, because an index is a physical listing. An integration test holds a
|
|
38
|
+
concept whose id leaves its directory and asserts both worlds at once, so the
|
|
39
|
+
split stays a decision; the authoring advice is the default's — rename the
|
|
40
|
+
file, not the id.
|
|
41
|
+
|
|
42
|
+
# What it derives from its own content
|
|
43
|
+
|
|
44
|
+
The concept parses its body on demand into the structural facts the rest of the
|
|
45
|
+
gem consumes:
|
|
46
|
+
|
|
47
|
+
- `#type`, `#title`, `#description`, `#resource`, `#tags` — typed reads over
|
|
48
|
+
the frontmatter;
|
|
49
|
+
- the §5 families, each carrying §13.1's fallbacks inline: `#generated` /
|
|
50
|
+
`#generated_at` / `#generated_by` (a legacy `timestamp` lifts into `at`,
|
|
51
|
+
per-key, with no actor ever invented), `#sources` (the native list, or the
|
|
52
|
+
legacy `# Citations` (`@okf-eco format/citations`) body list whenever the native
|
|
53
|
+
value yields zero mappings), `#verified` (a bare mapping reads as a
|
|
54
|
+
one-element list; degenerate shapes fold to unverified), `#trust_tier` /
|
|
55
|
+
`#trust` (derived per §5.3, never stored), `#status`/`#declared_status`
|
|
56
|
+
(absent reads stable; both keep the producer's spelling, because every
|
|
57
|
+
surface that *displays* a status prints what was written — the fold belongs
|
|
58
|
+
to `.effective_status`, which is what narrowing compares through), `#stale_after_date`/`#stale_on?(today)` (§5.5 — stale
|
|
59
|
+
on the day itself; the clock is always an argument, never read);
|
|
60
|
+
- the §10 contract of an Attested Computation: `#runtime`, `#parameters`,
|
|
61
|
+
`#computation`, `#executor`, `#attester`, `#attested_computation?`;
|
|
62
|
+
- `#usage_window` — validated for shape, deliberately consumed by nothing: no
|
|
63
|
+
gem surface computes over usage counts, so an effective-window resolver
|
|
64
|
+
would be speculative;
|
|
65
|
+
- `#declared_generated?`, `#legacy_timestamp?`, `#legacy_citations?` — raw-key
|
|
66
|
+
detection for lint and the surfaces; detection never influences reading;
|
|
67
|
+
- `#links` — every raw cross-link (`@okf-eco format/cross-links`) target in the body, in order; the bundle-relative ones become graph edges;
|
|
68
|
+
- `#external_links` — the subset of those that are URLs or `mailto:` (not edges);
|
|
69
|
+
- `#to_markdown` — the inverse of the frontmatter parser (`#citations` is gone,
|
|
70
|
+
subsumed by `#sources`);
|
|
71
|
+
- `#lint` — the concept-scoped [lint](../capabilities/linter.md) checks in isolation.
|
|
72
|
+
|
|
73
|
+
A concept never decides conformance alone; a [bundle](bundle.md) does, because
|
|
74
|
+
some checks (duplicate titles, missing link targets) are only meaningful across
|
|
75
|
+
the set.
|
data/.okf/model/graph.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Component
|
|
3
|
+
title: OKF::Bundle::Graph
|
|
4
|
+
description: The in-memory knowledge graph — concepts as nodes, cross-links as directed edges, with type and tag indexes.
|
|
5
|
+
resource: gems/okf/lib/okf/bundle/graph.rb
|
|
6
|
+
tags: [graph, pure]
|
|
7
|
+
generated:
|
|
8
|
+
by: human:maintainer
|
|
9
|
+
at: 2026-08-13T12:00:00Z
|
|
10
|
+
sources:
|
|
11
|
+
- title: gems/okf/lib/okf/bundle/graph.rb
|
|
12
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf/lib/okf/bundle/graph.rb
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Overview
|
|
16
|
+
|
|
17
|
+
`OKF::Bundle::Graph` turns a [bundle](bundle.md) into nodes and edges:
|
|
18
|
+
[concepts](concept.md) become nodes keyed by id, and bundle-relative
|
|
19
|
+
cross-links (`@okf-eco format/cross-links`) become directed edges. It is pure — it
|
|
20
|
+
carries no presentation concerns; sizing and colour belong to a renderer like
|
|
21
|
+
the [graph server](../capabilities/graph-server.md).
|
|
22
|
+
|
|
23
|
+
# Fidelity is a build option
|
|
24
|
+
|
|
25
|
+
The same graph ships at three weights, so a client downloads only what it needs
|
|
26
|
+
and fetches the rest on demand:
|
|
27
|
+
|
|
28
|
+
| Build | Node payload |
|
|
29
|
+
|-------|--------------|
|
|
30
|
+
| default (`body: true`) | id, type, title, description, tags, **body** |
|
|
31
|
+
| `body: false` | everything but the body |
|
|
32
|
+
| `minimal: true` | just id and title — the leanest payload to draw |
|
|
33
|
+
|
|
34
|
+
# Indexes come free at every weight
|
|
35
|
+
|
|
36
|
+
Regardless of node fidelity, the graph exposes two inverted indexes computed from
|
|
37
|
+
every concept:
|
|
38
|
+
|
|
39
|
+
- `type_index` — `{ type => [id, …] }`, so even a minimal client can colour nodes
|
|
40
|
+
by [`type`](concept.md). A concept whose type is missing *or blank* indexes
|
|
41
|
+
under `Untyped`: §11 condition 2 rejects the two identically (the
|
|
42
|
+
[validator](../capabilities/validator.md) asks `OKF.blank?`, not `empty?`), so
|
|
43
|
+
sorting them into different buckets would give `type: " "` a row of its own,
|
|
44
|
+
labelled with spaces;
|
|
45
|
+
- `tag_index` — `{ tag => [id, …] }`, so it can filter by tag.
|
|
46
|
+
|
|
47
|
+
Those indexes, plus `unlinked_ids` (degree-0 nodes), are what the
|
|
48
|
+
[read views](../capabilities/read-views.md) — `types`, `tags`, `stats`, `loose`
|
|
49
|
+
— are built from, and what their `--type`/`--dir`/`--tag` filters match against.
|
|
50
|
+
|
|
51
|
+
# Reduced to a skeleton
|
|
52
|
+
|
|
53
|
+
Where this graph is the full wiring, the [skeleton](skeleton.md) is the same
|
|
54
|
+
graph made holdable: concepts collapsed into their directory, the links between
|
|
55
|
+
two directories collapsed into one weighted arc, and every link tagged with the
|
|
56
|
+
cut it survives. It is the reduction [`graph --traffic`](../capabilities/read-views.md)
|
|
57
|
+
reads for cohesion and the [graph page](../capabilities/graph-server.md) lays a
|
|
58
|
+
large bundle out on — built from this graph's own nodes and edges, and just as
|
|
59
|
+
pure.
|
data/.okf/model/index.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# The model
|
|
2
|
+
|
|
3
|
+
The pure, in-memory data structures the gem builds a bundle out of — no disk, no
|
|
4
|
+
stdio. Everything else reads or renders these.
|
|
5
|
+
|
|
6
|
+
* [Concept](concept.md) - one file's worth of knowledge: frontmatter plus body, with a stable id.
|
|
7
|
+
* [Bundle](bundle.md) - a collection of concepts you validate, lint, and graph.
|
|
8
|
+
* [Graph](graph.md) - concepts as nodes, cross-links as edges, plus type and tag indexes.
|
|
9
|
+
* [Skeleton](skeleton.md) - the graph reduced: directories, the weighted arcs between them, and each link with the cut it survives.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Component
|
|
3
|
+
title: OKF::Bundle::Skeleton
|
|
4
|
+
description: The graph reduced to what a reader can hold — directories, the weighted arcs between them, and each link with the cut it survives.
|
|
5
|
+
resource: gems/okf/lib/okf/bundle/skeleton.rb
|
|
6
|
+
tags: [graph, pure]
|
|
7
|
+
generated:
|
|
8
|
+
by: human:maintainer
|
|
9
|
+
at: 2026-07-23T12:00:00Z
|
|
10
|
+
sources:
|
|
11
|
+
- title: gems/okf/lib/okf/bundle/skeleton.rb
|
|
12
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf/lib/okf/bundle/skeleton.rb
|
|
13
|
+
- title: gems/okf/lib/okf/cli/graph.rb
|
|
14
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf/lib/okf/cli/graph.rb
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Overview
|
|
18
|
+
|
|
19
|
+
`OKF::Bundle::Skeleton` is the [graph](graph.md) reduced to what a reader can
|
|
20
|
+
hold in their head. It is built from a graph's nodes and edges, does no I/O, and
|
|
21
|
+
decides nothing about how any of it is drawn — the same purity the graph keeps,
|
|
22
|
+
one level of abstraction up. Two consumers read it: [`graph
|
|
23
|
+
--traffic`](../capabilities/read-views.md) prints its directory reduction, and
|
|
24
|
+
the [graph page](../capabilities/graph-server.md)'s link layer lays a large
|
|
25
|
+
bundle out on the backbone it names.
|
|
26
|
+
|
|
27
|
+
A dense bundle is not dense the way a hub-and-spoke picture is. Measured on a
|
|
28
|
+
47-concept bundle with 227 links, the top hub takes 13 inbound and the median 4 —
|
|
29
|
+
no 80/20 to exploit, so dropping two-thirds of the *concepts* still leaves 53
|
|
30
|
+
edges. The density lives **between directories**: 173 of those 227 links (76%)
|
|
31
|
+
cross a directory boundary. That is the reduction worth drawing, and it is why
|
|
32
|
+
this class exists at all.
|
|
33
|
+
|
|
34
|
+
# The two things it produces
|
|
35
|
+
|
|
36
|
+
- **`dirs` + `arcs`** — the reduction as counts: one row per directory, one
|
|
37
|
+
weighted arc per ordered pair of directories. This is what `graph --traffic`
|
|
38
|
+
prints, with **cohesion** (a directory's internal share of its own traffic)
|
|
39
|
+
derived from them. Directories come off the concept *id* (`OKF.dir_of`), not
|
|
40
|
+
the file path, so this agrees with the graph's own `catalog` and `--dir`
|
|
41
|
+
filter — the side that follows the id, because the edges do.
|
|
42
|
+
- **`edges`, each with its `keep_at`** — the smallest cut at which a link
|
|
43
|
+
survives. Nothing prints these; they are what lets the graph page lay a large
|
|
44
|
+
bundle out on its strongest links first, inlined into the page as `EDGE_CUT`
|
|
45
|
+
because the layout needs them before any `fetch()` could answer.
|
|
46
|
+
|
|
47
|
+
Both are emitted **unthresholded**, and `#suggested_cut` *names* where to cut
|
|
48
|
+
rather than cutting — so a caller narrows the picture without this class ever
|
|
49
|
+
having to know what a picture is.
|
|
50
|
+
|
|
51
|
+
# The cut is fitted, not fixed
|
|
52
|
+
|
|
53
|
+
A fixed arc cut cannot serve both ends of the size range: measured at weight 3
|
|
54
|
+
across ten bundles, it left **2 arcs on one and 136 on another** — too tight to
|
|
55
|
+
be a picture at one end, no reduction at all at the other. What stays roughly
|
|
56
|
+
constant as a bundle grows is not the arc count but the arcs *per box*, since a
|
|
57
|
+
node-link diagram reads at about one to two edges per node regardless of size. So
|
|
58
|
+
`#suggested_cut` targets 1.5 arcs per directory and reports the weight that
|
|
59
|
+
delivers it, floored at 8 for the small end. Cohesion is computed over **every**
|
|
60
|
+
arc regardless of the cut, so narrowing the drawn picture never moves the
|
|
61
|
+
evidence under it.
|
|
62
|
+
|
|
63
|
+
# The spine is a sparsifier, not a sample
|
|
64
|
+
|
|
65
|
+
The `keep_at === 0` set is the **spine**: each concept's single most-connected
|
|
66
|
+
neighbour. It is chosen, not sampled, and the property that earns it the name is
|
|
67
|
+
that it **touches every linked concept**, so a layout run over it alone strands
|
|
68
|
+
nothing and the arrangement is a real one rather than a sketch to be redone.
|
|
69
|
+
|
|
70
|
+
It is the local-degree sparsifier (Lindner et al.) with a union rule — an edge
|
|
71
|
+
survives if *either* endpoint kept it — which is what stops it from stranding the
|
|
72
|
+
quiet half of a bundle the way a global "drop the weakest edges" rule does. It is
|
|
73
|
+
**not** the disparity filter, the usual name in this territory: that reads an
|
|
74
|
+
edge's weight against its endpoint's total, and every link here weighs exactly 1,
|
|
75
|
+
which makes every proportion identical and the filter a coin toss. Weighted-graph
|
|
76
|
+
tools do not transfer to an unweighted graph just because both are graphs.
|
data/.okf/overview.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Overview
|
|
3
|
+
title: okf at a glance
|
|
4
|
+
description: A light Ruby gem that reads, searches, validates, lints, and serves Open Knowledge Format v0.2 bundles.
|
|
5
|
+
tags: [okf, gem, diagram]
|
|
6
|
+
generated:
|
|
7
|
+
by: human:maintainer
|
|
8
|
+
at: 2026-08-13T12:00:00Z
|
|
9
|
+
sources:
|
|
10
|
+
- title: README.md
|
|
11
|
+
resource: https://github.com/serradura/okf/blob/main/README.md
|
|
12
|
+
- title: AGENTS.md
|
|
13
|
+
resource: https://github.com/serradura/okf/blob/main/AGENTS.md
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Overview
|
|
17
|
+
|
|
18
|
+
**okf** — the gem on RubyGems — operates on OKF v0.2 (`@okf-eco format/okf-format`)
|
|
19
|
+
bundles: directories of Markdown files with YAML frontmatter that humans and
|
|
20
|
+
agents both read from one source. It does not define new knowledge storage; it
|
|
21
|
+
gives you leverage over knowledge that already lives as Markdown.
|
|
22
|
+
|
|
23
|
+
```mermaid
|
|
24
|
+
flowchart LR
|
|
25
|
+
skill["companion<br/>agent skill"] -. authors/curate .-> bundle[("OKF v0.2 bundle<br/>Markdown + YAML")]
|
|
26
|
+
bundle --> model["pure model<br/>Concept · Bundle · Graph"]
|
|
27
|
+
skill -. execute .-> cli
|
|
28
|
+
subgraph cli ["okf CLI"]
|
|
29
|
+
validate["validate — legal? §11"]
|
|
30
|
+
lint["lint — well-curated?"]
|
|
31
|
+
search["search — which concept?"]
|
|
32
|
+
server["server — explore"]
|
|
33
|
+
end
|
|
34
|
+
model --> cli
|
|
35
|
+
model --> library["library API<br/>embed in Ruby"]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Over such a bundle the gem gives you seven capabilities behind one
|
|
39
|
+
[command-line tool](cli.md):
|
|
40
|
+
|
|
41
|
+
| Capability | What it answers | Verb |
|
|
42
|
+
| -------------------------------------------------------- | --------------------------------- | ---------------- |
|
|
43
|
+
| [Companion agent skill](capabilities/agent-skill.md) | Can an agent author it? | `skill` |
|
|
44
|
+
| [Conformance validator](capabilities/validator.md) | Is this a legal OKF bundle? (§11) | `validate` |
|
|
45
|
+
| [Curation linter](capabilities/linter.md) | Is it navigable, complete, fresh? | `lint` / `loose` |
|
|
46
|
+
| [Ranked text search](capabilities/search.md) | Which concept covers X? | `search` |
|
|
47
|
+
| [Interactive graph server](capabilities/graph-server.md) | Can I explore it visually? | `server` |
|
|
48
|
+
| [Static render](capabilities/render.md) | Can I ship a serverless snapshot? | `render` |
|
|
49
|
+
| [Library API](capabilities/library-api.md) | Can my Ruby program use it? | (in-process) |
|
|
50
|
+
|
|
51
|
+
Beside the gem's seven, the sibling surfaces. The
|
|
52
|
+
MCP server (`@okf-mcp design/the-tool-set`) (`okf-mcp`) projects the same kernel
|
|
53
|
+
onto the Model Context Protocol so any MCP-capable agent host reads these
|
|
54
|
+
bundles without a terminal — ten read-only tools, concepts as resources a host
|
|
55
|
+
can attach on its own, and the [skill's](capabilities/agent-skill.md) playbooks
|
|
56
|
+
as prompts. The enforcement layer (`@okf-eco gems/okf-pro`) (`okf-pro`)
|
|
57
|
+
goes the other way and is the only surface here that **writes**: it generates an
|
|
58
|
+
agent's knowledge repository — bundle, hooks, pre-commit, CI, skill — and then
|
|
59
|
+
holds it to a few invariants at all three doors, under a contract where a gate
|
|
60
|
+
that cannot check refuses rather than shrugs.
|
|
61
|
+
|
|
62
|
+
Alongside those, a family of [read views](capabilities/read-views.md) —
|
|
63
|
+
`index`, `catalog`, `files`, `types`, `tags`, `stats`, `graph` — print the bundle at a
|
|
64
|
+
glance so an agent reads it without a browser.
|
|
65
|
+
|
|
66
|
+
Knowledge rarely lives in one bundle, so `okf server` hosts one, several, or every
|
|
67
|
+
bundle in a per-user [registry](registry.md) — one hub, one switcher, no per-repo
|
|
68
|
+
server to remember.
|
|
69
|
+
|
|
70
|
+
# The two ideas it inherits from the format
|
|
71
|
+
|
|
72
|
+
- **Dual audience.** Every file serves a human skimming it _and_ an agent
|
|
73
|
+
extracting from it, so bodies are structural Markdown and
|
|
74
|
+
links (`@okf-eco format/cross-links`) are plain Markdown links — both readers already
|
|
75
|
+
understand them.
|
|
76
|
+
- **The graph is emergent.** Files are nodes, Markdown links are edges. You never
|
|
77
|
+
declare a graph; the gem [builds one](model/graph.md) from how concepts link.
|
|
78
|
+
|
|
79
|
+
# Design ethos
|
|
80
|
+
|
|
81
|
+
The gem is deliberately light so it runs on the Ruby an OS already ships. That
|
|
82
|
+
ethos is not incidental — it is enforced by [hard constraints](design/):
|
|
83
|
+
a [Ruby 2.4 floor](design/ruby-floor.md), exactly
|
|
84
|
+
[three runtime dependencies](design/runtime-dependencies.md), and a
|
|
85
|
+
[core/shell split](design/core-shell-split.md) that keeps all logic pure and
|
|
86
|
+
testable without disk. Everything else — no ActiveSupport, no build step, no
|
|
87
|
+
JavaScript toolchain — follows from those.
|
data/.okf/registry.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Component
|
|
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`.
|
|
5
|
+
resource: gems/okf/lib/okf/registry.rb
|
|
6
|
+
tags: [cli, shell, registry]
|
|
7
|
+
generated:
|
|
8
|
+
by: human:maintainer
|
|
9
|
+
at: 2026-07-24T12:00:00Z
|
|
10
|
+
sources:
|
|
11
|
+
- title: gems/okf/lib/okf/registry.rb
|
|
12
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf/lib/okf/registry.rb
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Overview
|
|
16
|
+
|
|
17
|
+
`OKF::Registry` is the gem's only piece of *durable user state*: an ordered list
|
|
18
|
+
of bundle references, so `okf registry set` today and a bare `okf server`
|
|
19
|
+
tomorrow share one list. It is a plain JSON file — `$OKF_HOME/registry.json`,
|
|
20
|
+
`$OKF_HOME` defaulting to `~/.okf` — and that is a design choice, not a stopgap: a
|
|
21
|
+
database would break the [two-dependency rule](design/runtime-dependencies.md),
|
|
22
|
+
and the file is per-user, hand-editable, and greppable. It is part of the
|
|
23
|
+
[shell](design/core-shell-split.md); it reads and writes a file. That is the
|
|
24
|
+
*global* registry; a project can keep its own, discovered from the working
|
|
25
|
+
directory — see [Global by default, project-local by discovery](#global-by-default-project-local-by-discovery).
|
|
26
|
+
|
|
27
|
+
The registry stores *references*, never content. It holds a path, a slug, and a
|
|
28
|
+
title — the bundles themselves stay where they are on disk, owned by the repos
|
|
29
|
+
they document. Nothing is copied, so nothing can go stale except the path itself.
|
|
30
|
+
|
|
31
|
+
# Slugs: implicit is forgiving, explicit is strict
|
|
32
|
+
|
|
33
|
+
A slug is the bundle's mount key (`/b/<slug>/`) and its name in the
|
|
34
|
+
[switcher](capabilities/graph-server.md). Where it comes from decides what a
|
|
35
|
+
collision means:
|
|
36
|
+
|
|
37
|
+
| Source | On collision | Why |
|
|
38
|
+
|--------|--------------|-----|
|
|
39
|
+
| the directory basename (`registry set ./docs`) | silently suffixed — `docs-2`, `docs-3` | you never asked for a name; the gem picks a free one |
|
|
40
|
+
| an explicit `--as SLUG`, or `registry rename` | raises | you *did* ask for that name, so quietly serving a different one is a lie |
|
|
41
|
+
|
|
42
|
+
That asymmetry is the whole rule: **the gem may invent a name, but it may never
|
|
43
|
+
substitute one you chose.** Both paths run the same normalization the ephemeral
|
|
44
|
+
(unregistered) bundles use, so a directory mounts under the same slug whether it
|
|
45
|
+
was registered or passed straight to `okf server` — with exactly one exception,
|
|
46
|
+
the reserved `all/` below, which the registry alone has a reason to rename.
|
|
47
|
+
|
|
48
|
+
The rule extends cleanly to a name the grammar has already spoken for. `@all`
|
|
49
|
+
means *every registered bundle* to [`search`](capabilities/search.md), so no one
|
|
50
|
+
bundle may answer to `all` — it is reserved. A directory named `all/` therefore
|
|
51
|
+
registers as `all-2` (the basename was only a guess, so a suffix is right), while
|
|
52
|
+
`--as all` is refused (the ask was deliberate, so substituting `all-2` would be
|
|
53
|
+
the lie).
|
|
54
|
+
|
|
55
|
+
The reservation is the *registry's*, not the slug helper's, and that boundary is
|
|
56
|
+
load-bearing in both directions. Inward, all three ways a slug enters this list
|
|
57
|
+
are covered — minting, an explicit ask, and **reading the file**. The third is the
|
|
58
|
+
one that cannot refuse. `all` reaches the file two ways nothing can take back: a
|
|
59
|
+
release from before the name was reserved wrote it (a directory named `all/`
|
|
60
|
+
slugged exactly that), or a hand typed it into the file the format invites you to
|
|
61
|
+
edit. So the read *mints around it* — the entry lists, mounts, and answers to
|
|
62
|
+
`all-2`, and the next write persists the name.
|
|
63
|
+
|
|
64
|
+
**The read normalizes for the same reason**, and it is the same bug one step
|
|
65
|
+
wider: two of the three ways in normalized and the third did not. A hand-typed
|
|
66
|
+
`"slug": "My Docs"` listed perfectly well while `@my-docs` missed it — and so did
|
|
67
|
+
`rename` and `default`, which look an entry up through the very normalization the
|
|
68
|
+
read had skipped. The two verbs that could repair the entry were the two that
|
|
69
|
+
could not see it. A slug registration would have handed back untouched is left
|
|
70
|
+
alone (including one already suffixed, so fixing a sick entry never renames a
|
|
71
|
+
healthy one); everything else is minted around what the other entries hold.
|
|
72
|
+
<!-- rule:okf-registry-read-normalizes -->
|
|
73
|
+
|
|
74
|
+
That asymmetry was also the [graph server](capabilities/graph-server.md)'s XSS
|
|
75
|
+
trigger: slugs reach the bundle switcher's HTML, and the only way one could carry
|
|
76
|
+
a quote was to arrive un-normalized through this read. Normalizing closes it at
|
|
77
|
+
the source — the escape is hardened too, because a page whose safety depends on a
|
|
78
|
+
guarantee three layers away is not one you can reason about locally.
|
|
79
|
+
|
|
80
|
+
Refusing the file was the first answer here, and it was worth the correction it
|
|
81
|
+
took. A name the grammar has taken makes **one entry** unnameable; rejecting the
|
|
82
|
+
registry makes **every** entry unreachable — and takes `del` and `rename`, the two
|
|
83
|
+
verbs that could fix it, down on the very read they need to survive, leaving
|
|
84
|
+
hand-editing JSON as the only way out. A guard whose failure mode is worse than
|
|
85
|
+
what it guards against is not a guard. Minting is also simply the rule already
|
|
86
|
+
stated above, read one line further: the gem may invent a name, and here the name
|
|
87
|
+
on disk cannot be used, so inventing one is the only move that is not a lie.
|
|
88
|
+
<!-- rule:okf-registry-reserved-mint -->
|
|
89
|
+
|
|
90
|
+
Outward, it stops there: an ephemeral `okf server ./all` has no
|
|
91
|
+
registry and no refs, so there is no name to protect, and it mounts at `/b/all/` —
|
|
92
|
+
suffixing it would invent a `/b/all-2/` whose `/b/all/` does not exist. `all/` is
|
|
93
|
+
therefore the one directory whose registered slug and ephemeral slug differ, and
|
|
94
|
+
they differ because only one of the two worlds has a grammar that spells `all`.
|
|
95
|
+
|
|
96
|
+
The rule cuts one layer deeper, at the empty string. Minting a slug from a
|
|
97
|
+
basename must *produce* something, so `slugify` falls back to a placeholder when
|
|
98
|
+
nothing survives normalization. Looking one up must not: a lookup that inherits
|
|
99
|
+
that fallback makes `@***` resolve to whatever bundle happens to be slugged
|
|
100
|
+
`bundle` — the gem substituting a name you never chose, which is the one thing
|
|
101
|
+
the rule forbids. So `normalize` (no fallback) backs every lookup and every
|
|
102
|
+
explicit ask, and `slugify` (placeholder) backs only basename minting.
|
|
103
|
+
|
|
104
|
+
# The default is a position, not a stored name
|
|
105
|
+
|
|
106
|
+
The first entry still on disk is the bundle a bare `okf server` opens at `/`, and
|
|
107
|
+
`registry default <slug>` moves that entry to the front. That is the whole
|
|
108
|
+
feature.
|
|
109
|
+
|
|
110
|
+
"Still on disk" is the one qualifier position needs, and it is not a fallback in
|
|
111
|
+
disguise. The hub drops a bundle whose directory has vanished rather than serving
|
|
112
|
+
a hole, so a default that ignored the gap would put `registry list`'s `*` on a
|
|
113
|
+
bundle `/` never opens — the star names what `/` opens, so it has to skip what `/`
|
|
114
|
+
skips. The rule stays derivable from the file plus the disk, with nothing stored
|
|
115
|
+
and nothing to reconcile. Its mirror is that `registry default <slug>` *refuses* a
|
|
116
|
+
vanished directory, exactly as `registry set` refuses to register one: both are
|
|
117
|
+
explicit asks, and a move the default would then skip would answer with a slug the
|
|
118
|
+
user did not type.
|
|
119
|
+
|
|
120
|
+
The alternative — storing the chosen slug — looks simpler and is not. A stored
|
|
121
|
+
slug is a *foreign key* into the same list it lives in, and a foreign key demands
|
|
122
|
+
referential integrity from every operation that touches the list: carry it
|
|
123
|
+
through a `rename`, re-point it when `add --as` renames in place, clear it on a
|
|
124
|
+
`remove`, and fall back when it dangles anyway. Four obligations, each a place to
|
|
125
|
+
forget. Position owes nothing: a rename touches the name and leaves the row where
|
|
126
|
+
it is, a `del` lets the next row become first, and a default that is not in the
|
|
127
|
+
list is *unrepresentable* rather than merely handled. The registry was already
|
|
128
|
+
documented as ordered, so this is state it kept for free.
|
|
129
|
+
|
|
130
|
+
What it costs is a file that visibly reorders, which is why `registry default`
|
|
131
|
+
says so in its own help — the JSON is meant to be read and hand-edited, and a
|
|
132
|
+
reordering write should never be a surprise.
|
|
133
|
+
|
|
134
|
+
Identity is the **path**, not the slug: re-registering a directory already in the
|
|
135
|
+
registry refreshes its title in place instead of adding a twin.
|
|
136
|
+
|
|
137
|
+
# It names bundles for the whole CLI, not just the server
|
|
138
|
+
|
|
139
|
+
The registry began as the server's boot list and grew into the
|
|
140
|
+
[CLI](cli.md)'s name-resolution layer: wherever a verb takes a `<dir>`,
|
|
141
|
+
`@slug` resolves through it and bare `@` picks the default — the same slugs the
|
|
142
|
+
hub mounts at `/b/<slug>/`, so the name you click is the name you type. That is
|
|
143
|
+
what turns registering from "tell the server" into "give this bundle a name":
|
|
144
|
+
[`search`](capabilities/search.md) crosses several of them in one query, and no
|
|
145
|
+
verb needs a path once the bundle has a slug.
|
|
146
|
+
|
|
147
|
+
It is also no longer terminal-only. The graph page's ⚙ Bundles panel drives
|
|
148
|
+
`default`, `rename` and `remove` from a browser, through
|
|
149
|
+
[this class and its messages](capabilities/bundles-manager.md) rather than around
|
|
150
|
+
them. `add` stays terminal-only, because a browser cannot hand over a filesystem
|
|
151
|
+
path. The file stays the record: every write goes through here, and the hub
|
|
152
|
+
re-reads it per request rather than trusting a snapshot, so an `okf registry
|
|
153
|
+
rename` in another terminal shows on a refresh.
|
|
154
|
+
|
|
155
|
+
# Groups: a named set of bundles
|
|
156
|
+
|
|
157
|
+
A **group** is a slug that names not one bundle but a *list* of members — bundle
|
|
158
|
+
or group slugs, so groups nest — and resolves, recursively and path-deduped, to
|
|
159
|
+
the bundle leaves underneath. It is the durable form of typing `@a @b @c`: once
|
|
160
|
+
several bundles earn a name together (`okf registry group backend @orders
|
|
161
|
+
@billing`), `@backend` stands in for the set. `group`/`ungroup` add and remove
|
|
162
|
+
members; emptying a group deletes it, since an empty set resolves to nothing.
|
|
163
|
+
|
|
164
|
+
Groups live in **their own list** (`{ bundles: […], groups: […] }`), not among
|
|
165
|
+
the entries — a deliberate separation. The first-is-default rule and every
|
|
166
|
+
`File.directory?` guard assume an entry has a path, and a group has none;
|
|
167
|
+
threading a nil path through all of them to host a pathless member would be the
|
|
168
|
+
foreign-key tax the default rule already refused. A separate list leaves the
|
|
169
|
+
bundle invariants untouched and makes a group exactly what it is: a view over
|
|
170
|
+
them.
|
|
171
|
+
|
|
172
|
+
**One namespace, two kinds.** A slug names a bundle *or* a group, never both, so
|
|
173
|
+
`@backend` is unambiguous — the collision check that already spanned entries and
|
|
174
|
+
the reserved `all` now spans groups too, in both directions (`registry set --as
|
|
175
|
+
backend` is refused while a group holds it, and vice versa). And because a member
|
|
176
|
+
list stores slugs, the two lifecycle verbs keep those references live: `rename`
|
|
177
|
+
**cascades** the new name across every group that named the slug, and `del`
|
|
178
|
+
**cascade-drops** it (a group emptied that way is deleted). Skipping either would
|
|
179
|
+
orphan a member silently — the same drift the path-not-slug identity rule avoids
|
|
180
|
+
for the default.
|
|
181
|
+
|
|
182
|
+
**Only a set-taking verb consumes one.** [`search`](capabilities/search.md) and
|
|
183
|
+
[`server`](capabilities/graph-server.md) are the two verbs that already take
|
|
184
|
+
several bundles; a group feeds exactly them (`okf search @backend …` merges the
|
|
185
|
+
members into one ranking, `okf server @backend` mounts each). Every single-bundle
|
|
186
|
+
verb refuses a `@group` with exit 2 — the same second-bundle rule that stops
|
|
187
|
+
`okf lint a b` from linting `a` and ignoring `b`, because a group resolving to
|
|
188
|
+
three bundles is that ambiguity by another spelling. `@all` is unchanged: it
|
|
189
|
+
still names every registered *bundle*, and a group is a named subset of what it
|
|
190
|
+
already covers. A cycle is refused at write time and guarded again at resolution,
|
|
191
|
+
since the file is hand-editable.
|
|
192
|
+
<!-- rule:okf-registry-groups-cascade -->
|
|
193
|
+
|
|
194
|
+
# It tolerates a world that changes underneath it
|
|
195
|
+
|
|
196
|
+
A registry entry is a bet that a directory still exists, and the registry never
|
|
197
|
+
prunes on its own — deleting a bundle from disk must not silently rewrite a list
|
|
198
|
+
the user curated. Instead `listing` marks the entry `missing`, so
|
|
199
|
+
[`registry list`](cli.md) shows the gap and the user decides. Reads are equally
|
|
200
|
+
forgiving: the original bare-array file shape still parses, while a *corrupt*
|
|
201
|
+
file raises with the fix — "fix or delete the file" —
|
|
202
|
+
rather than starting from an empty list and silently dropping every bundle.
|
|
203
|
+
|
|
204
|
+
That message invites a hand-edit, which is why the shape is checked and not just
|
|
205
|
+
the syntax: valid JSON is not a valid registry, and an entry missing its `path`
|
|
206
|
+
must fail here, as a usage error naming the file, rather than surviving to crash
|
|
207
|
+
a `File.directory?` three frames away. The [CLI](cli.md) does its half by
|
|
208
|
+
loading through a guard, so a broken file reaches the user as an error from
|
|
209
|
+
whatever verb they ran — not a backtrace from a verb that never rescued one.
|
|
210
|
+
|
|
211
|
+
Writes go to a temp file and are promoted with `rename`, the same atomic
|
|
212
|
+
promotion the [bundle writer](capabilities/library-api.md) uses, so a booting
|
|
213
|
+
server never reads a torn file. Two racing writers stay last-writer-wins: this is
|
|
214
|
+
a per-user file, and locking would buy nothing worth the complexity.
|
|
215
|
+
|
|
216
|
+
# Global by default, project-local by discovery
|
|
217
|
+
|
|
218
|
+
The registry has two homes, and which one answers is decided by *where you stand*,
|
|
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-registry.json`
|
|
221
|
+
that `okf registry init` drops in a directory; okf finds it by walking up from the
|
|
222
|
+
working directory, and while you are inside its tree it **replaces** the global one
|
|
223
|
+
— every registry op, and every [`@slug`](cli.md), resolves through it. So a bare
|
|
224
|
+
`okf server` inside a repo serves that repo's bundles with no `$OKF_HOME` setup,
|
|
225
|
+
and a project carries its own named set without touching the user's global list.
|
|
226
|
+
|
|
227
|
+
**The file's presence is the whole state.** There is no stored "local mode", the
|
|
228
|
+
same way the [default is a position, not a stored name](#the-default-is-a-position-not-a-stored-name):
|
|
229
|
+
a mode flag would be one more thing to set, dangle, and reconcile, where the file
|
|
230
|
+
being *there* is self-evident and self-cleaning. The nearest one on the path up
|
|
231
|
+
wins, so nested registries resolve nearest-first, and `okf registry list` names the
|
|
232
|
+
file it found so which one is answering is never a guess.
|
|
233
|
+
|
|
234
|
+
`$OKF_HOME` still names *where the global registry lives*; it does **not** veto a
|
|
235
|
+
nearer local one. That direction is deliberate: `$OKF_HOME` is commonly exported
|
|
236
|
+
once and left, so letting it win would silently defeat the feature for exactly the
|
|
237
|
+
users who set up a project registry. The escape hatch is therefore a per-invocation
|
|
238
|
+
signal, not a second sticky variable — `OKF_NO_DISCOVERY=1`, set inline, forces the
|
|
239
|
+
global registry for a fixed-cwd caller (CI, a tool) that cannot just `cd` out.
|
|
240
|
+
|
|
241
|
+
# A project-local registry stores portable paths
|
|
242
|
+
|
|
243
|
+
The global registry stores absolute paths — correct for `~/.okf`, whose bundles
|
|
244
|
+
are scattered across the disk with no shared anchor. A committed project registry
|
|
245
|
+
needs the opposite: a bundle **inside** the registry's own tree is stored *relative*
|
|
246
|
+
to the `.okf-registry.json`, so the file travels with the repo — a checkout on
|
|
247
|
+
another machine, or a container mounting it, resolves the same bundles unchanged. A
|
|
248
|
+
bundle **outside** the tree keeps an absolute path, because a relative path that
|
|
249
|
+
climbs out cannot be re-anchored anywhere useful, and being honest that it will not
|
|
250
|
+
travel beats a `../../..` that breaks on the first move.
|
|
251
|
+
|
|
252
|
+
The relative form lives **only on disk**. A path resolves to absolute the moment it
|
|
253
|
+
is read, so `entry.path`, [`registry list`](cli.md), and the server mount all go on
|
|
254
|
+
seeing the absolute paths they always did — the portability is a property of the
|
|
255
|
+
file, invisible to every consumer. And because only the write side relativizes, an
|
|
256
|
+
existing absolute local entry migrates to relative on its next write: a registry
|
|
257
|
+
written before this existed heals itself the first time it changes.
|
|
258
|
+
<!-- rule:okf-registry-local-discovery -->
|
|
259
|
+
|
|
260
|
+
# It costs an embedding app nothing
|
|
261
|
+
|
|
262
|
+
`require "okf"` does not load it. The registry is reached only from the
|
|
263
|
+
[CLI](cli.md), which requires it at the moment a registry verb or a bare `server`
|
|
264
|
+
runs — the same on-demand rule the [library API](capabilities/library-api.md)
|
|
265
|
+
keeps for the command-line machinery.
|