okf 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.okf/capabilities/agent-skill.md +112 -0
- data/.okf/capabilities/bundles-manager.md +144 -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 +297 -0
- data/.okf/capabilities/validator.md +60 -0
- data/.okf/cli.md +214 -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 +724 -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 +432 -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 +76 -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 +161 -16
- data/README.md +226 -17
- data/lib/okf/cli/command.rb +8 -3
- data/lib/okf/cli/registry.rb +306 -47
- data/lib/okf/cli.rb +1 -1
- data/lib/okf/registry.rb +448 -22
- data/lib/okf/render/graph/template.html.erb +6 -2
- data/lib/okf/server/hub.rb +6 -2
- data/lib/okf/skill/reference/cli/registry.md +49 -6
- data/lib/okf/skill/reference/cli.md +1 -1
- data/lib/okf/version.rb +1 -1
- metadata +46 -5
data/.okf/registry.md
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
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, composable through links to other registry files and able to import rows out of them — 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.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
|
+
That file was called `.okf-registry.json` first, and both names are still
|
|
242
|
+
discovered — a local registry is *committed*, so retiring the old one outright
|
|
243
|
+
would break every repository carrying it to save eight characters. The two are
|
|
244
|
+
checked **per directory** on the way up, not one name swept to the root and then
|
|
245
|
+
the other: otherwise a legacy file at a repo's root would beat a `.okf.json` two
|
|
246
|
+
levels down, and "the nearest one wins" would quietly mean something else. Inside
|
|
247
|
+
one directory the short name wins.
|
|
248
|
+
|
|
249
|
+
The deprecation is said **once, by the `registry` umbrella, and nowhere else**.
|
|
250
|
+
That verb is the one whose subject *is* a registry file and the one nobody runs in
|
|
251
|
+
a loop or pipes into something, which is exactly what `lint` and `search` are — a
|
|
252
|
+
note there is noise people learn to redirect away rather than act on. A legacy
|
|
253
|
+
file that is in force gets the one move that retires it; a legacy file sitting
|
|
254
|
+
beside the `.okf.json` that beat it gets named too, because reading one while the
|
|
255
|
+
other lies there unread is a silent wrong answer unless somebody says so.
|
|
256
|
+
|
|
257
|
+
`-g`/`--global` is that same signal spelled as an argument, and it is the
|
|
258
|
+
`registry` umbrella's alone (see [the CLI](cli.md#one-lever-not-two)). It exists
|
|
259
|
+
because a lever reachable only through an env var is a lever most users never
|
|
260
|
+
find — and the umbrella is the one verb whose *subject* is a registry file, so
|
|
261
|
+
naming which file to act on is an argument to it rather than a flag bolted onto
|
|
262
|
+
fourteen unrelated verbs. `init` is the exception that proves it: its whole job is
|
|
263
|
+
to create a *local* file, so `-g` there names nothing and is refused.
|
|
264
|
+
|
|
265
|
+
# A project-local registry stores portable paths
|
|
266
|
+
|
|
267
|
+
The global registry stores absolute paths — correct for `~/.okf`, whose bundles
|
|
268
|
+
are scattered across the disk with no shared anchor. A committed project registry
|
|
269
|
+
needs the opposite: a bundle **inside** the registry's own tree is stored *relative*
|
|
270
|
+
to the `.okf.json`, so the file travels with the repo — a checkout on
|
|
271
|
+
another machine, or a container mounting it, resolves the same bundles unchanged. A
|
|
272
|
+
bundle **outside** the tree keeps an absolute path, because a relative path that
|
|
273
|
+
climbs out cannot be re-anchored anywhere useful, and being honest that it will not
|
|
274
|
+
travel beats a `../../..` that breaks on the first move.
|
|
275
|
+
|
|
276
|
+
The relative form lives **only on disk**. A path resolves to absolute the moment it
|
|
277
|
+
is read, so `entry.path`, [`registry list`](cli.md), and the server mount all go on
|
|
278
|
+
seeing the absolute paths they always did — the portability is a property of the
|
|
279
|
+
file, invisible to every consumer. And because only the write side relativizes, an
|
|
280
|
+
existing absolute local entry migrates to relative on its next write: a registry
|
|
281
|
+
written before this existed heals itself the first time it changes.
|
|
282
|
+
<!-- rule:okf-registry-local-discovery -->
|
|
283
|
+
|
|
284
|
+
# Links: the global registry composes other registry files
|
|
285
|
+
|
|
286
|
+
A **link** is a pointer from the global registry to another registry file. That
|
|
287
|
+
file's bundles resolve through the pointer at read time, under their own slugs,
|
|
288
|
+
and nothing is copied — the same "stores references, never content" rule the
|
|
289
|
+
entries keep, one level up. `okf registry link onm ~/ONM/registry.json` and
|
|
290
|
+
`@onm-central` (or whatever slugs that file holds) answer here; the link name
|
|
291
|
+
itself resolves as a group over exactly its bundles, so `@onm` is the set.
|
|
292
|
+
|
|
293
|
+
The case it exists for is **a repository that already curates its own bundles**.
|
|
294
|
+
This repo commits a `.okf.json` naming five; before links, using them
|
|
295
|
+
from `~/.okf` meant registering all five again by hand and re-syncing whenever
|
|
296
|
+
the repo's list changed. A link points at the file the repo already maintains, so
|
|
297
|
+
the curation is composed rather than duplicated, and it keeps resolving its own
|
|
298
|
+
relative paths because the target is anchored on its own directory — exactly as
|
|
299
|
+
it would from inside that checkout. Two registry files stop being two worlds you
|
|
300
|
+
switch `$OKF_HOME` between and become one view.
|
|
301
|
+
|
|
302
|
+
**Only the global registry follows links.** A project-local one parses them and
|
|
303
|
+
preserves them across a write, but never resolves them — and that single
|
|
304
|
+
restriction is the whole depth rule. A linked file's own links are not read, so
|
|
305
|
+
no chain forms, no prefix compounds, and there is no cycle to detect. The
|
|
306
|
+
alternative was transitive resolution, and it fails on ownership rather than on
|
|
307
|
+
effort: the target is a file this registry does not own, so a cycle-check at
|
|
308
|
+
write time goes stale the moment someone adds a link back on the other side.
|
|
309
|
+
Transitivity would move cycle detection from write time — cheap, one file,
|
|
310
|
+
refusable — to read time, across N files, with nobody to blame. The groups above
|
|
311
|
+
nest safely for exactly the reason links do not: every member lives in one file,
|
|
312
|
+
behind one guard.
|
|
313
|
+
<!-- rule:okf-registry-links-global-only -->
|
|
314
|
+
|
|
315
|
+
# A linked name is minted around a collision, never refused
|
|
316
|
+
|
|
317
|
+
A slug arriving from a linked file answers to itself when the name is free, and
|
|
318
|
+
to `<link>-<slug>` when it is not (`-2` beyond that, through the same `dedupe` a
|
|
319
|
+
basename goes through). That is the [implicit is forgiving, explicit is
|
|
320
|
+
strict](#slugs-implicit-is-forgiving-explicit-is-strict) table one row wider, and
|
|
321
|
+
the row falls on the forgiving side for the reason the table gives: a name in
|
|
322
|
+
someone else's file was never *chosen* here, so inventing around it is licensed —
|
|
323
|
+
while refusing would let one foreign row take down an entire link. The link
|
|
324
|
+
*name* is the strict half, and is refused on collision like any `--as`: you typed
|
|
325
|
+
that one.
|
|
326
|
+
|
|
327
|
+
Precedence is fixed so the derivation is reproducible: the registry's own bundles
|
|
328
|
+
always win the bare name, and between links the file's order decides. Both are
|
|
329
|
+
position, which is state the registry already keeps — the same reason the
|
|
330
|
+
[default](#the-default-is-a-position-not-a-stored-name) is one.
|
|
331
|
+
|
|
332
|
+
What this costs, and it is the design's one genuinely computed name: a slug can
|
|
333
|
+
*move* when an unrelated link is added. Everything else in this file is stable in
|
|
334
|
+
the file. The mitigation is disclosure rather than a mechanism — `registry link`
|
|
335
|
+
says what it moved as it writes, and `registry list` prints the moved row with the
|
|
336
|
+
slug it carries in its source file (`onm-central … [central]`), which is the only
|
|
337
|
+
place a shifted ref is visible. Linked entries also append **after** the local
|
|
338
|
+
ones, so while this registry owns any bundle at all the default stays local.
|
|
339
|
+
|
|
340
|
+
**A linked group is listed with the rest, not beside them.** `groups_listing`
|
|
341
|
+
returns this registry's own groups first, then the linked ones, each carrying the
|
|
342
|
+
`link` it came from — one list, because `group?` *resolves* a linked group and a
|
|
343
|
+
listing that named only the local half would answer about a smaller set than the
|
|
344
|
+
same object can resolve. That gap is invisible at the call site and inherited by
|
|
345
|
+
every consumer: `okf-mcp`'s `list_bundles` and the TUI's groups view both read
|
|
346
|
+
this one method, and both would have hidden a group they could already open. A
|
|
347
|
+
caller that wants only the groups it may edit filters on `link` — which is the
|
|
348
|
+
question they are actually asking, and it is now askable.
|
|
349
|
+
|
|
350
|
+
# A link is read-only, and the refusal lives in the model
|
|
351
|
+
|
|
352
|
+
`rename`, `del`, `default`, `set --as` and `group` all refuse a slug a link owns,
|
|
353
|
+
with a message naming the file that does own it and the `unlink` that would drop
|
|
354
|
+
it. Two of those are worth their own line. A **group** may not hold a linked slug:
|
|
355
|
+
a group stores names, and a linked name lives only while its link resolves, so
|
|
356
|
+
holding one would dangle the group the moment the link goes — the foreign key the
|
|
357
|
+
default rule already refused. And **`registry set` on a directory a link already
|
|
358
|
+
carries** is refused rather than quietly adding a twin, because entries are
|
|
359
|
+
identified by path and the path is already spoken for.
|
|
360
|
+
|
|
361
|
+
The third was a hole this rule had left open, and it failed in the worst
|
|
362
|
+
available way. A group's slug is its *update* path everywhere else — `registry
|
|
363
|
+
group backend @more` adds to the existing one — so `group onm @alpha`, naming a
|
|
364
|
+
link or a group that came with one, took that path: it merged the member, printed
|
|
365
|
+
`grouped onm → …`, and lost it, because `write` persists only the groups this
|
|
366
|
+
registry owns. A refusal is the fix, but the shape is what matters: a write that
|
|
367
|
+
reports success and does not happen is worse than one that raises, and it was
|
|
368
|
+
reachable from the CLI, the TUI and the browser panel alike.
|
|
369
|
+
|
|
370
|
+
The refusals live in this class, not in the [CLI](cli.md), and that placement is
|
|
371
|
+
load-bearing: the graph page's ⚙ Bundles panel posts into these same methods
|
|
372
|
+
([bundles manager](capabilities/bundles-manager.md)), so a guard one layer up
|
|
373
|
+
would leave the browser doing what the terminal refuses. A link whose target has
|
|
374
|
+
gone or cannot be parsed is *reported* — `(missing)`, `(unreadable)` — and
|
|
375
|
+
resolves to nothing, the same tolerance a vanished bundle directory gets, because
|
|
376
|
+
one dead pointer must not take down the registry that holds it.
|
|
377
|
+
|
|
378
|
+
# Import: the copy that owns what it takes
|
|
379
|
+
|
|
380
|
+
`okf registry import <@slug…>` copies chosen bundles — and the groups that hold
|
|
381
|
+
them — out of another registry file into the one in force. It is the **opposite
|
|
382
|
+
trade** from a link, and the pair is the point:
|
|
383
|
+
|
|
384
|
+
| | link | import |
|
|
385
|
+
| --- | --- | --- |
|
|
386
|
+
| what moves | nothing; a pointer | the reference, copied |
|
|
387
|
+
| scope | the whole target file | the slugs you name |
|
|
388
|
+
| ownership | the target keeps it, read-only | yours, editable |
|
|
389
|
+
| freshness | live on every read | a snapshot |
|
|
390
|
+
|
|
391
|
+
Copying a *reference* is not copying content, so the rule at the top of this file
|
|
392
|
+
still holds: the bundle stays in the repository that owns it, and what lands here
|
|
393
|
+
is the same path-and-name row `registry set` would have written. What import saves
|
|
394
|
+
is the laundering. The scenario is standing inside a repo, running `okf registry
|
|
395
|
+
list -g` to see what the global registry holds, and wanting one of them here — the
|
|
396
|
+
path is already on screen, and without this verb the only way to move it is
|
|
397
|
+
through the clipboard.
|
|
398
|
+
|
|
399
|
+
The source is `--from`, defaulting to the global registry, which inside a repo is
|
|
400
|
+
the only other one you have. It is a second flag rather than a second meaning for
|
|
401
|
+
`-g` because [`-g` names the registry acted *on*](cli.md#one-lever-not-two) on
|
|
402
|
+
every other subcommand; this verb names two files, so the second gets its own
|
|
403
|
+
name rather than inverting the first.
|
|
404
|
+
|
|
405
|
+
**A collision refuses.** This is where import and link disagree, and the
|
|
406
|
+
disagreement is the slug rule, not an inconsistency: a linked name was never
|
|
407
|
+
chosen here, so it is [minted around](#a-linked-name-is-minted-around-a-collision-never-refused);
|
|
408
|
+
an imported name lands in *this* file because you typed it, so it is refused
|
|
409
|
+
exactly as `rename` refuses. The gem may invent a name it made up; it may not
|
|
410
|
+
substitute one you chose. A bundle already registered here under a different name
|
|
411
|
+
refuses too, and *first* — when both are true, "that bundle is already here as
|
|
412
|
+
@docs" is the answer and "the name is taken" is only the symptom.
|
|
413
|
+
|
|
414
|
+
**A group brings everything it reaches**, including a group nested inside it.
|
|
415
|
+
Recreating a name here that resolved to a larger set there would be that same
|
|
416
|
+
quiet substitution, with nothing on screen to reveal it. Members are stored
|
|
417
|
+
verbatim, which is the payoff of preserving slugs: a name means the same thing on
|
|
418
|
+
both sides, so there is nothing to remap — the work `fold_linked_bundles` must do
|
|
419
|
+
precisely because a link mints its names.
|
|
420
|
+
|
|
421
|
+
**Nothing is applied until everything is checked.** Every ask is resolved and
|
|
422
|
+
refused against the current state first, then one `write` publishes the lot. Half
|
|
423
|
+
an import is a registry the user has to unpick by hand, reported as a success —
|
|
424
|
+
and a refusal that already moved three of four rows is not a refusal.
|
|
425
|
+
<!-- rule:okf-registry-import-all-or-nothing -->
|
|
426
|
+
|
|
427
|
+
# It costs an embedding app nothing
|
|
428
|
+
|
|
429
|
+
`require "okf"` does not load it. The registry is reached only from the
|
|
430
|
+
[CLI](cli.md), which requires it at the moment a registry verb or a bare `server`
|
|
431
|
+
runs — the same on-demand rule the [library API](capabilities/library-api.md)
|
|
432
|
+
keeps for the command-line machinery.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Component
|
|
3
|
+
title: The Format Layer
|
|
4
|
+
description: "Pure, and the bottom of everything: path normalisation with a root-escape guard, the one YAML door, and the link and citation grammars §5 and §8 are written in."
|
|
5
|
+
tags: [structure, format, pure, yaml, links]
|
|
6
|
+
generated:
|
|
7
|
+
by: human:maintainer
|
|
8
|
+
at: 2026-08-19T12:00:00Z
|
|
9
|
+
resource: lib/okf/path.rb
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# The files
|
|
13
|
+
|
|
14
|
+
| file | what it owns |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `lib/okf.rb` | `OKF::Error`, `OKF.blank?`, `OKF.iso8601`, `OKF.dir_of`, `SPEC_VERSION` |
|
|
17
|
+
| `lib/okf/version.rb` | `OKF::VERSION` |
|
|
18
|
+
| `lib/okf/path.rb` | `Path.normalize_relative!`, `join_under!`, `under?` — and `Path::Error` |
|
|
19
|
+
| `lib/okf/safe_read.rb` | `SafeRead.contained_path!`, `read!` — containment for a caller that already holds a root |
|
|
20
|
+
| `lib/okf/markdown/frontmatter.rb` | parse, dump, `stringify_keys` — **the only** YAML door |
|
|
21
|
+
| `lib/okf/markdown/links.rb` | §6: inline, reference and footnote links, and how a raw target resolves |
|
|
22
|
+
| `lib/okf/markdown/citations.rb` | §8: the `# Citations` section and its entries |
|
|
23
|
+
|
|
24
|
+
# Containment is a primitive here, not a habit
|
|
25
|
+
|
|
26
|
+
`Path.normalize_relative!` rejects any `..` segment outright, which is what makes
|
|
27
|
+
"a concept cannot link out of its own bundle" a property of the format rather
|
|
28
|
+
than a convention. `join_under!` and `under?` are the two questions every writer
|
|
29
|
+
and every reader asks before touching a path, and `SafeRead.read!` is the pair of
|
|
30
|
+
them plus the read, for callers that already hold a root — it resolves symlinks
|
|
31
|
+
and refuses one that leaves.
|
|
32
|
+
|
|
33
|
+
Every layer above reaches for these rather than composing its own check. A second
|
|
34
|
+
containment implementation is the shape this class of bug takes.
|
|
35
|
+
|
|
36
|
+
# One YAML door
|
|
37
|
+
|
|
38
|
+
**All YAML goes through `Markdown::Frontmatter`** — `safe_load`, with `Date` and
|
|
39
|
+
`Time` permitted and aliases off. `PSYCH_KEYWORDS` is the Psych < 3.1
|
|
40
|
+
positional-argument shim, and it lives here precisely so that
|
|
41
|
+
`YAML.safe_load`/`YAML.load` is called in exactly one place in the gem.
|
|
42
|
+
|
|
43
|
+
`stringify_keys` exists so that ActiveSupport does not, which is the same reason
|
|
44
|
+
`OKF.blank?` does.
|
|
45
|
+
|
|
46
|
+
# The link grammar is where the graph comes from
|
|
47
|
+
|
|
48
|
+
`Links.extract` walks prose lines only — `FENCE` and `CODE_SPAN` take fenced
|
|
49
|
+
blocks and code spans out first, because a link inside a code sample is a
|
|
50
|
+
document about a link, not an edge. `INLINE_LINK`, `REFERENCE_LINK` and
|
|
51
|
+
`DEFINITION` cover the three markdown spellings; `FOOTNOTE_REFERENCE` and
|
|
52
|
+
`FOOTNOTE_DEFINITION` are §8's keying. `SCHEME` and `MAILTO` are what keep an
|
|
53
|
+
external URL from being read as a relative path.
|
|
54
|
+
|
|
55
|
+
What is built on top of these is [the-model](/structure/the-model.md).
|
|
56
|
+
|
|
57
|
+
`Links.resolve` is the one that turns a raw target into a bundle-relative path,
|
|
58
|
+
and it is why a link split across a newline produces no edge — the extractor
|
|
59
|
+
reads a line at a time.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Structure
|
|
2
|
+
|
|
3
|
+
Every file under `lib/`, grouped by the layer that owns it. One concept owns
|
|
4
|
+
each file, and `test/unit/bundle_catalog_test.rb` fails if that stops being true
|
|
5
|
+
in either direction — a file no concept names, or a concept naming a file that
|
|
6
|
+
is gone.
|
|
7
|
+
|
|
8
|
+
Fifty files, eight layers, and one rule underneath all of them: **the core is
|
|
9
|
+
pure and the shell does the I/O.** `test/unit/boundary_test.rb` fails if a pure
|
|
10
|
+
file names a shell class or touches `File`, `Dir`, `FileUtils` or stdio. Put new
|
|
11
|
+
logic in the core; put new I/O in the shell.
|
|
12
|
+
|
|
13
|
+
Read it bottom-up — each layer depends only on the ones below it.
|
|
14
|
+
|
|
15
|
+
* [The Format Layer](format-layer.md) - `lib/okf.rb`, `path.rb`, `safe_read.rb`, `version.rb`, `markdown/` — pure: paths, containment, frontmatter, links, citations.
|
|
16
|
+
* [The Model](the-model.md) - `concept.rb`, `bundle.rb`, `bundle/graph.rb`, `references.rb`, `row_filter.rb`, `skeleton.rb` — pure: a bundle in memory, and every derived view of it.
|
|
17
|
+
* [The Analysers](the-analysers.md) - `bundle/validator*.rb`, `bundle/linter*.rb` — pure: §11 conformance, and curation quality, kept deliberately apart.
|
|
18
|
+
* [Search](search.md) - `bundle/search.rb` and its two engines — pure: the facade owns the rows, the engines own the matching.
|
|
19
|
+
* [The Disk Shell](the-disk-shell.md) - `concept/file.rb`, `bundle/reader.rb`, `writer.rb`, `folder.rb`, `registry.rb` — where directories become bundles and back.
|
|
20
|
+
* [The Server and the Page](the-server.md) - `server/app.rb`, `hub.rb`, `hub/not_found.rb`, `runner.rb`, `render/graph.rb` — one ERB template, served or baked.
|
|
21
|
+
* [The CLI](the-cli.md) - `cli.rb`, `cli/command.rb`, and the seventeen verb files — the only layer that parses argv, prints, and exits.
|
|
22
|
+
* [The Skill](the-skill.md) - `skill.rb` — the companion agent skill and its installer.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Component
|
|
3
|
+
title: Search — One Facade, Two Engines
|
|
4
|
+
description: The facade owns the rows, the snippets, the ranking fields and the engine registry; an engine owns only matching, declares its capabilities, and is chosen by what the query needs.
|
|
5
|
+
tags: [structure, search, pure, extension-point]
|
|
6
|
+
generated:
|
|
7
|
+
by: human:maintainer
|
|
8
|
+
at: 2026-08-19T12:00:00Z
|
|
9
|
+
resource: lib/okf/bundle/search.rb
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# The files
|
|
13
|
+
|
|
14
|
+
| file | what it owns |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `lib/okf/bundle/search.rb` | the facade: rows, snippets, weights, the `Corpus`, and the engine registry |
|
|
17
|
+
| `lib/okf/bundle/search/scan.rb` | the default engine — raw-text scan, the only one that does `regexp` |
|
|
18
|
+
| `lib/okf/bundle/search/index.rb` | the minifts engine — BM25+, the only one that does `fuzzy` and `prefix` |
|
|
19
|
+
|
|
20
|
+
# The split
|
|
21
|
+
|
|
22
|
+
The facade decides **what a result is**: `WEIGHTS` (which field counts how much),
|
|
23
|
+
`FIELDS`, `SNIPPET_FIELDS`, `SNIPPET_RADIUS`, and the row shape every caller
|
|
24
|
+
reads. An engine decides only **which documents matched**, and says what it can
|
|
25
|
+
do through `CAPABILITIES`.
|
|
26
|
+
|
|
27
|
+
`Search.engine_for(required)` picks by capability, not by name: `ROUTABLE` is the
|
|
28
|
+
set a query can *demand* (`regexp`, `fuzzy`), `DEFAULT_ENGINE` is `:scan`, and
|
|
29
|
+
`UnsupportedQuery` / `UnknownEngine` are the two honest refusals. `available?`
|
|
30
|
+
lets an engine decline at runtime — the index engine needs `minifts` present.
|
|
31
|
+
|
|
32
|
+
`Corpus` is the cross-bundle form, behind `Search.across`.
|
|
33
|
+
|
|
34
|
+
# Why scan leads
|
|
35
|
+
|
|
36
|
+
The scan is the default because a one-shot CLI cannot amortise an index build:
|
|
37
|
+
3.00 s versus 0.24 s at 1,000 concepts. That is a real argument against the
|
|
38
|
+
`minifts` dependency and it is recorded as such — but `--fuzzy` and parity with
|
|
39
|
+
the graph page's browser-side ranking both still need it, and a cached index
|
|
40
|
+
would restore the case outright.
|
|
41
|
+
|
|
42
|
+
Parity is the subtler half: the Ruby index engine is a bit-for-bit port of the
|
|
43
|
+
browser's MiniSearch, pinned to the same version the page lazy-loads, so an
|
|
44
|
+
`--engine index` result and a search typed into the page rank identically.
|
|
45
|
+
|
|
46
|
+
The verb that drives it is in [the-cli](/structure/the-cli.md).
|
|
47
|
+
|
|
48
|
+
# `Search.register` is an extension point
|
|
49
|
+
|
|
50
|
+
Append-only, idempotent by id, duck-type checked at registration —
|
|
51
|
+
**deliberately the same shape as `CLI.register`**. An engine is a module
|
|
52
|
+
answering `id`, `capabilities`, `available?`, `call`, and optionally `prepare`.
|
|
53
|
+
Adding one is a registration, not an edit to the facade.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Component
|
|
3
|
+
title: The Analysers — Validate and Lint, Kept Apart
|
|
4
|
+
description: Two pure analysers with a boundary that is a spec requirement rather than a preference — conformance may not reject curation problems, and curation may not emit conformance errors.
|
|
5
|
+
tags: [structure, validate, lint, pure, spec]
|
|
6
|
+
generated:
|
|
7
|
+
by: human:maintainer
|
|
8
|
+
at: 2026-08-19T12:00:00Z
|
|
9
|
+
resource: lib/okf/bundle/validator.rb
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# The files
|
|
13
|
+
|
|
14
|
+
| file | what it owns |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `lib/okf/bundle/validator.rb` | §11 conformance: hard errors, plus soft convention warnings |
|
|
17
|
+
| `lib/okf/bundle/validator/result.rb` | the errors/warnings collection and `valid?` |
|
|
18
|
+
| `lib/okf/bundle/linter.rb` | the curation report — every check, and their severities |
|
|
19
|
+
| `lib/okf/bundle/linter/report.rb` | findings by severity, the stats, and `healthy?` |
|
|
20
|
+
|
|
21
|
+
# The boundary is the point
|
|
22
|
+
|
|
23
|
+
**`validate` and `lint` stay separate**, and the line is the spec's, not taste.
|
|
24
|
+
§11 forbids the validator from *rejecting* a broken cross-link or a missing
|
|
25
|
+
optional field — those are warnings at most. Curation findings belong to lint,
|
|
26
|
+
which never emits a conformance error.
|
|
27
|
+
|
|
28
|
+
A new check goes to one side or the other, and getting it wrong changes what
|
|
29
|
+
exit code a user's CI sees: 0 ok, 1 a failing bundle, 2 a usage error.
|
|
30
|
+
|
|
31
|
+
`Validator::CONVENTION_CHECKS` is the soft half — the warnings that are about
|
|
32
|
+
convention rather than conformance, so a caller can tell them apart.
|
|
33
|
+
|
|
34
|
+
# Validator: one method per spec clause
|
|
35
|
+
|
|
36
|
+
The private methods are named for what they check — `validate_families`,
|
|
37
|
+
`validate_generated`, `validate_verified`, `validate_sources`,
|
|
38
|
+
`validate_usage_window`, `validate_lifecycle`, `validate_computation`,
|
|
39
|
+
`validate_parameters`, `validate_contract_mapping`, `validate_okf_version`,
|
|
40
|
+
`validate_index`, `validate_log`, `validate_reserved`, `validate_unparseable`.
|
|
41
|
+
A new §5 family is a new one of these; that is the shape to follow.
|
|
42
|
+
|
|
43
|
+
`validate_unparseable` is the one worth noticing: a file the reader could not
|
|
44
|
+
parse is an error, not an absence. A validator that skipped it would report
|
|
45
|
+
clean over the file most likely to be broken.
|
|
46
|
+
|
|
47
|
+
# Linter: severities are data, and downstream depends on them
|
|
48
|
+
|
|
49
|
+
`SEVERITIES` is the whole check list with each check's level, and `CHECKS` is
|
|
50
|
+
its keys. It is a public fact rather than an implementation detail — okf-pro
|
|
51
|
+
pins a frozen snapshot of it, because a released kernel that reclassified a
|
|
52
|
+
check would change what that gem's gate blocks on.
|
|
53
|
+
|
|
54
|
+
`only:` and `except:` select checks; `stale_before:`/`today:` supply the clock
|
|
55
|
+
that `expired` and `stale` need. **A check that could not run is reported, not
|
|
56
|
+
dropped**: `Report#to_h` carries `skipped_checks`, and `healthy?` over a silent
|
|
57
|
+
skip would be the same lie in a smaller box.
|
|
58
|
+
|
|
59
|
+
`DEFAULT_MIN_BODY` and `HUB_LIMIT` are the two tunables; `ACTOR_FORMS` is the
|
|
60
|
+
`generated.by` grammar, shared with the validator through `Concept`.
|