okf 1.4.0 → 1.6.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 +71 -0
- data/README.md +88 -43
- data/lib/okf/bundle/search.rb +155 -0
- data/lib/okf/bundle.rb +2 -2
- data/lib/okf/cli.rb +98 -4
- data/lib/okf/server/app.rb +59 -0
- data/lib/okf/server/graph/template.html.erb +271 -45
- data/lib/okf/server/graph.rb +25 -9
- data/lib/okf/skill/SKILL.md +23 -21
- data/lib/okf/skill/playbooks/consume.md +4 -2
- data/lib/okf/skill/playbooks/maintain.md +6 -4
- data/lib/okf/skill/playbooks/menu.md +10 -6
- data/lib/okf/skill/playbooks/search.md +42 -0
- data/lib/okf/skill/reference/authoring.md +2 -2
- data/lib/okf/skill/reference/cli.md +73 -18
- data/lib/okf/version.rb +1 -1
- data/lib/okf.rb +1 -0
- metadata +5 -28
- data/.okf/capabilities/agent-skill.md +0 -46
- data/.okf/capabilities/graph-server.md +0 -63
- data/.okf/capabilities/index.md +0 -20
- data/.okf/capabilities/library-api.md +0 -72
- data/.okf/capabilities/linter.md +0 -49
- data/.okf/capabilities/read-views.md +0 -84
- data/.okf/capabilities/validator.md +0 -40
- data/.okf/cli.md +0 -52
- data/.okf/design/core-shell-split.md +0 -58
- data/.okf/design/index.md +0 -10
- data/.okf/design/ruby-floor.md +0 -45
- data/.okf/design/runtime-dependencies.md +0 -44
- data/.okf/design/server-trust-boundary.md +0 -40
- data/.okf/format/citations.md +0 -33
- data/.okf/format/cross-links.md +0 -52
- data/.okf/format/frontmatter.md +0 -38
- data/.okf/format/index.md +0 -9
- data/.okf/format/okf-format.md +0 -43
- data/.okf/index.md +0 -18
- data/.okf/log.md +0 -10
- data/.okf/model/bundle.md +0 -38
- data/.okf/model/concept.md +0 -44
- data/.okf/model/graph.md +0 -44
- data/.okf/model/index.md +0 -8
- data/.okf/overview.md +0 -67
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 45d6619cf9144fbaf4026e51ad238213e916c6796ffd1199d189d6b64c47352e
|
|
4
|
+
data.tar.gz: c26b86f1b2173feefd02159a2dd280f93a4e8ee915eb077ef49558fced6c8ac3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 254038bdb6383fe94882793c6b2d2433917536793670d23fad1be4c2678e9a8c042164c997ba8ae64950ced447ca850e4263bb1eee9ab2230b33b2b83f8efd94
|
|
7
|
+
data.tar.gz: 1ada5ef52a4848d340619a1ccf10bc35ddbae7504c3e53cc447f824e44f90d145579a024ae17c15c38eb84cc0592353b20967adda709c631a38a298161e6f99f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.6.0] - 2026-07-15
|
|
4
|
+
|
|
5
|
+
- New CLI verb: `okf render <dir> [-o FILE]` — the live graph as one static,
|
|
6
|
+
self-contained HTML file, so it hosts where a server can't (GitHub Pages, an
|
|
7
|
+
object store, an attachment). It is the same page `okf server` serves, one
|
|
8
|
+
switch apart: the browser's five on-demand reads — bodies, descriptions,
|
|
9
|
+
catalog, index, logs — now route through named getters that resolve from an
|
|
10
|
+
injected `EMBED` payload instead of the network, so the whole bundle rides
|
|
11
|
+
inside the file with no server and no build step. Prints to stdout (`okf
|
|
12
|
+
render docs > public/index.html`) or writes `-o FILE`. The embedded data is
|
|
13
|
+
`</script>`-escaped exactly like the boot payload and every body still renders
|
|
14
|
+
through `DOMPurify.sanitize(marked.parse(...))`, so the trust boundary holds;
|
|
15
|
+
the trade-off is weight — each body is inlined, so a big bundle makes a big
|
|
16
|
+
file, and `okf server` stays the choice at scale.
|
|
17
|
+
- Official Docker image: `ghcr.io/serradura/okf`, a portable CLI that runs every
|
|
18
|
+
`okf` command (the graph server included) with no Ruby on the host. It is built
|
|
19
|
+
from source and published multi-arch (`linux/amd64`, `linux/arm64`) to the
|
|
20
|
+
GitHub Container Registry on each release tag, so the image always matches the
|
|
21
|
+
gem. Mount a bundle at `/data`; for `server`, add `--bind 0.0.0.0` and publish
|
|
22
|
+
`-p 8808:8808`. See the README's Docker section.
|
|
23
|
+
|
|
24
|
+
## [1.5.0] - 2026-07-13
|
|
25
|
+
|
|
26
|
+
- New CLI verb: `okf search <dir> <term…>` — deterministic ranked retrieval
|
|
27
|
+
over concept metadata *and bodies*, the browser page's search brought to the
|
|
28
|
+
CLI. Terms AND together as case-insensitive substrings, or as Ruby regexps
|
|
29
|
+
with `--regexp`/`-e`; `--in` restricts the searched fields; the shared
|
|
30
|
+
`--type/--area/--tag` filters and `--fields/--except` projections apply.
|
|
31
|
+
Matches rank by where they hit (title > id > tags > type/description > body)
|
|
32
|
+
and carry a bounded context snippet, so "which concept covers X?" costs a
|
|
33
|
+
few rows instead of a body read. Advisory read: exit 0 even with no matches.
|
|
34
|
+
Deliberately not fuzzy — the consuming agent is the fuzzy layer.
|
|
35
|
+
- The skill learns retrieval as a first-class verb: a new `search` playbook
|
|
36
|
+
(progressive disclosure end to end: ingest `okf index`, decide where to
|
|
37
|
+
look, cut across with `okf search`, read only the winning bodies),
|
|
38
|
+
search-aware routing in SKILL.md and the menu/consume playbooks, and
|
|
39
|
+
`/okf:gem search <query>` first in the Claude Code plugin's routing.
|
|
40
|
+
- Retrieval eval in the suite: the progressive path (index skeleton → search →
|
|
41
|
+
one body) must answer a planted question in under 25% of the bytes of the
|
|
42
|
+
full graph dump, so the playbook's economics stay true by construction.
|
|
43
|
+
- Graph server: the authored layer joins the UI. The Files view carries two
|
|
44
|
+
tabs — **Files** (the per-directory concept groups, foldable) and
|
|
45
|
+
**Indexes** (the log first, as the chronological index, then every
|
|
46
|
+
`index.md`, root before nested) — with the files filters moved up into the
|
|
47
|
+
top bar. The rail's **Index** item, the `2` key, and `?view=index` are
|
|
48
|
+
shortcuts straight to the Indexes tab. Folder nodes in file-tree mode and
|
|
49
|
+
area boxes in cluster mode are clickable and open that directory's §6 map
|
|
50
|
+
in the inspector (authored, or the synthesized listing when none exists).
|
|
51
|
+
Links to an `index.md`, a `log.md`, or a bare directory (`model/`) navigate
|
|
52
|
+
everywhere a body renders instead of striking through as dead, and the log
|
|
53
|
+
is fetched fresh on every read, so a just-appended entry shows without a
|
|
54
|
+
restart. A reserved file's "Open in graph" jumps to its folder in the file
|
|
55
|
+
tree, map in the inspector. New `/index` and `/log` endpoints back it all.
|
|
56
|
+
- Graph server: Mermaid diagrams in concept bodies are click-to-inspect. A
|
|
57
|
+
click (or tap) opens the diagram full screen — drag to pan, wheel or pinch
|
|
58
|
+
to zoom, buttons and double-click reset, Esc closes — powered by
|
|
59
|
+
[Panzoom](https://github.com/timmywil/panzoom), lazy-loaded from the CDN
|
|
60
|
+
exactly like Mermaid itself.
|
|
61
|
+
- The Claude Code plugin's `/okf:gem` command now weighs the shape of a
|
|
62
|
+
free-form ask: a question about what the bundle knows routes through the
|
|
63
|
+
search playbook and answers from retrieved concepts instead of guessing.
|
|
64
|
+
- Skill efficiency audit: every playbook now takes the CLI's lean paths.
|
|
65
|
+
`maintain` hunts affected concepts with `okf search` and pulls edges via
|
|
66
|
+
`graph --json --minimal` instead of the full-body dump, `menu` reads the
|
|
67
|
+
plain-text reports it only scans, and SKILL.md pins the discipline as a
|
|
68
|
+
rule: skeleton first, bodies last.
|
|
69
|
+
- Docs: the CLI reference's server section now reflects the DOMPurify
|
|
70
|
+
sanitization that landed in 1.1.0 (it still said bodies render unsanitized),
|
|
71
|
+
and the server page's link-preview image points at the renamed
|
|
72
|
+
`okfgem.com/og-demo-v2.png`.
|
|
73
|
+
|
|
3
74
|
## [1.4.0] - 2026-07-12
|
|
4
75
|
|
|
5
76
|
- Graph server UX round. Selecting a node now makes one camera move instead of
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<a href="https://okfgem.com">
|
|
3
3
|
<picture>
|
|
4
4
|
<source media="(prefers-color-scheme: dark)" srcset=".github/hero-dark.png">
|
|
5
|
-
<img src=".github/hero-light.png" width="100%" alt="okf-gem:
|
|
5
|
+
<img src=".github/hero-light.png" width="100%" alt="okf-gem: the complete harness for the Open Knowledge Format. An Agent Skill, a CLI and library, and a Graph let your agent author, curate, and consume your project's knowledge. 100% local.">
|
|
6
6
|
</picture>
|
|
7
7
|
</a>
|
|
8
8
|
</p>
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
<p align="center">
|
|
11
11
|
<a href="https://rubygems.org/gems/okf"><img src="https://img.shields.io/gem/v/okf" alt="Gem version"></a>
|
|
12
12
|
<a href="https://rubygems.org/gems/okf"><img src="https://img.shields.io/gem/dt/okf" alt="Downloads"></a>
|
|
13
|
+
<a href="https://github.com/serradura/okf-gem/pkgs/container/okf"><img src="https://img.shields.io/badge/ghcr.io-okf-2496ED?logo=docker&logoColor=white" alt="Docker image"></a>
|
|
13
14
|
<a href="https://github.com/serradura/okf-gem/actions/workflows/main.yml"><img src="https://github.com/serradura/okf-gem/actions/workflows/main.yml/badge.svg" alt="CI"></a>
|
|
14
15
|
<a href="https://github.com/serradura/okf-gem"><img src="https://img.shields.io/badge/ruby-%3E%3D%202.4-black" alt="Ruby >= 2.4"></a>
|
|
15
16
|
<a href="LICENSE.txt"><img src="https://img.shields.io/badge/license-Apache--2.0-blue" alt="License: Apache-2.0"></a>
|
|
@@ -18,26 +19,23 @@
|
|
|
18
19
|
</p>
|
|
19
20
|
|
|
20
21
|
<p align="center">
|
|
21
|
-
<b><a href="https://okfgem.com">
|
|
22
|
+
<b><a href="https://okfgem.com">Site</a></b> ·
|
|
23
|
+
<b><a href="https://okfgem.com/docs/">Docs</a></b> ·
|
|
22
24
|
<b><a href="https://demo.okfgem.com">Live demo</a></b> ·
|
|
23
|
-
<b><a href="https://claude.okfgem.com">Claude
|
|
25
|
+
<b><a href="https://claude.okfgem.com">Claude plugin</a></b> ·
|
|
26
|
+
<b><a href="https://docker.okfgem.com">Docker image</a></b>
|
|
24
27
|
</p>
|
|
25
28
|
|
|
26
|
-
**okf-gem**
|
|
27
|
-
**Open Knowledge Format (OKF)** v0.1 bundles:
|
|
29
|
+
**okf-gem** (`okf` on RubyGems) is the complete harness for
|
|
30
|
+
**Open Knowledge Format (OKF)** v0.1 bundles: create, maintain, and consume
|
|
31
|
+
your project's knowledge with your agent. The package is **Agent Skill + CLI/Lib + Graph**: an agent skill that authors and curates, a CLI and Ruby library that validate, lint, search, and embed, and a graph to explore, live or static, in one gem that runs 100% local. A bundle is a directory of Markdown files with YAML frontmatter that humans and agents read from one source; each file is a _concept_. The gem does not define a new place to keep knowledge; it gives you leverage over knowledge that already lives as Markdown.
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
> validates, lints, indexes, and serves your Markdown as a graph. In Claude Code,
|
|
31
|
-
> add the plugin and let it set everything up: `/plugin marketplace add
|
|
32
|
-
serradura/okf-gem`, then `/plugin install okf@okfgem`, then `/okf:gem`. On the
|
|
33
|
-
> command line: `gem install okf`, then `okf validate <dir>`.
|
|
34
|
-
|
|
35
|
-
Here is what it is able to do:
|
|
33
|
+
The package, end to end:
|
|
36
34
|
|
|
37
35
|
<p align="center">
|
|
38
36
|
<picture>
|
|
39
37
|
<source media="(prefers-color-scheme: dark)" srcset=".github/overview-dark.png">
|
|
40
|
-
<img src=".github/overview-light.png" width="
|
|
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 three modes: okf server (a live local server), okf render (the same page exported as one static, self-contained HTML file you can host anywhere), 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.">
|
|
41
39
|
</picture>
|
|
42
40
|
</p>
|
|
43
41
|
|
|
@@ -46,13 +44,13 @@ command-line tool (the library API is also usable in-process). Each capability
|
|
|
46
44
|
below links to the concept that documents it: this gem's own knowledge is an OKF
|
|
47
45
|
bundle, so you can read its design in the format it defends.
|
|
48
46
|
|
|
49
|
-
| Capability
|
|
50
|
-
|
|
|
51
|
-
| [Companion agent skill](.okf/capabilities/agent-skill.md)
|
|
52
|
-
| [Conformance validator](.okf/capabilities/validator.md)
|
|
53
|
-
| [Curation linter](.okf/capabilities/linter.md)
|
|
54
|
-
| [Interactive graph
|
|
55
|
-
| [Library API](.okf/capabilities/library-api.md)
|
|
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
|
+
| [Interactive graph](.okf/capabilities/graph-server.md) | Explore it — live or static? | `server` / `render` |
|
|
53
|
+
| [Library API](.okf/capabilities/library-api.md) | Can my Ruby program use it? | in-process |
|
|
56
54
|
|
|
57
55
|
> [!TIP]
|
|
58
56
|
> **Browse the gem as knowledge, not just docs.** This README is the front door;
|
|
@@ -90,18 +88,21 @@ work with it.
|
|
|
90
88
|
Knowledge already has several homes near an agent, and each holds a different
|
|
91
89
|
thing. None of the others is built for curated, durable team knowledge:
|
|
92
90
|
|
|
93
|
-
| | OKF bundle (this)
|
|
94
|
-
| ------------------------------ |
|
|
95
|
-
| Holds | curated team knowledge
|
|
96
|
-
| Versioned with the code | ✅
|
|
97
|
-
| Portable across agents | ✅ plain Markdown + YAML
|
|
98
|
-
| Typed and queryable | ✅ frontmatter + graph
|
|
99
|
-
| Reviewed in PRs | ✅
|
|
100
|
-
| Scales past one context window | ✅ progressive disclosure
|
|
101
|
-
| Checked by tooling | ✅ (`okf validate` + `lint`)
|
|
102
|
-
|
|
103
|
-
The last
|
|
104
|
-
|
|
91
|
+
| | OKF bundle (this) | `CLAUDE.md` / `AGENTS.md` | Agent auto-memory | Wiki / Notion |
|
|
92
|
+
| ------------------------------ | ----------------------------------------------------- | -------------------------- | ------------------------ | ---------------- |
|
|
93
|
+
| Holds | curated team knowledge | standing instructions | what one agent picked up | human docs |
|
|
94
|
+
| Versioned with the code | ✅ | ✅ | ❌ | ❌ |
|
|
95
|
+
| Portable across agents | ✅ plain Markdown + YAML | ⚠️ per-harness conventions | ❌ per-agent store | ⚠️ export needed |
|
|
96
|
+
| Typed and queryable | ✅ frontmatter + graph | ❌ prose | ❌ | ⚠️ partially |
|
|
97
|
+
| Reviewed in PRs | ✅ | ✅ | ❌ implicit | ⚠️ rarely |
|
|
98
|
+
| Scales past one context window | ✅ progressive disclosure<br>(`okf index` + `search`) | ❌ loaded whole | ⚠️ partially | n/a |
|
|
99
|
+
| Checked by tooling | ✅ exit codes for CI<br>(`okf validate` + `lint`) | ❌ | ❌ | ❌ |
|
|
100
|
+
|
|
101
|
+
The last two rows are this gem's job. Scaling past one context window is
|
|
102
|
+
progressive disclosure — `okf index` reads the map, `okf search` pulls only the
|
|
103
|
+
concepts a task needs, so the bundle is never loaded whole. And drift never
|
|
104
|
+
hides here: the other homes have no detector, but `okf validate` and `lint` turn
|
|
105
|
+
a bundle's drift into findings you can gate on in CI.
|
|
105
106
|
|
|
106
107
|
## What a bundle looks like
|
|
107
108
|
|
|
@@ -163,14 +164,48 @@ putting the `okf` command on your `PATH`:
|
|
|
163
164
|
bundle exec rake install
|
|
164
165
|
```
|
|
165
166
|
|
|
167
|
+
### Run it with Docker (no Ruby needed)
|
|
168
|
+
|
|
169
|
+
Prefer not to install Ruby? The official image bundles the CLI, so every `okf`
|
|
170
|
+
command runs against a bundle you mount at `/data`:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
# validate / lint / search / index … mirror the CLI, over the mounted bundle
|
|
174
|
+
docker run --rm -v "$PWD:/data" ghcr.io/serradura/okf validate .
|
|
175
|
+
|
|
176
|
+
# serve the live graph: bind 0.0.0.0 so the host can reach it, and publish the port
|
|
177
|
+
docker run --rm -v "$PWD:/data" -p 8808:8808 ghcr.io/serradura/okf server . --bind 0.0.0.0
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Then open <http://127.0.0.1:8808>. Images are published for `linux/amd64` and
|
|
181
|
+
`linux/arm64` on
|
|
182
|
+
[ghcr.io](https://github.com/serradura/okf-gem/pkgs/container/okf): `:latest`
|
|
183
|
+
tracks the newest release, or pin a version like `:1.5.0`.
|
|
184
|
+
|
|
185
|
+
Tired of the long line? Install a Docker-backed [`okf` command](https://docker.okfgem.com),
|
|
186
|
+
so every verb drops the `docker run` prefix and reads exactly like the native CLI
|
|
187
|
+
(mount, port, and bind handled for you). Do this only on a machine without the
|
|
188
|
+
gem:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
curl -fsSL https://docker.okfgem.com/install.sh | sh # or grab the script by hand
|
|
192
|
+
okf validate .
|
|
193
|
+
okf server .
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
On Windows the image runs under Docker Desktop (WSL2); install with PowerShell
|
|
197
|
+
instead: `irm https://docker.okfgem.com/install.ps1 | iex`.
|
|
198
|
+
|
|
166
199
|
## Command line
|
|
167
200
|
|
|
168
201
|
```bash
|
|
169
202
|
okf validate <dir> [--json] # check OKF v0.1 conformance (§9)
|
|
170
203
|
okf lint <dir> [--json] [--fail-on warn] [...] # report curation-quality issues
|
|
171
204
|
okf loose <dir> [--json] # list files with no graph links, by folder
|
|
205
|
+
okf search <dir> <term…> [-e] [--in a,b] [...] # ranked text retrieval across metadata + bodies
|
|
172
206
|
okf index <dir> [--json] [--area A] [--no-body] # progressive-disclosure map (§6): bodies, rollups, listings
|
|
173
207
|
okf server <dir> [-p PORT] [--bind ADDR] [...] # serve the interactive graph over HTTP
|
|
208
|
+
okf render <dir> [-o FILE] [--layout NAME] [...] # export the graph as one static, self-contained HTML file
|
|
174
209
|
okf graph <dir> [--json] [--minimal] [--no-body] # print the knowledge graph
|
|
175
210
|
okf catalog | files | tags | types | stats <dir> [--json] # the browser views, on the CLI
|
|
176
211
|
okf skill <dest> [--here] [--force] # install the companion agent skill
|
|
@@ -190,17 +225,23 @@ OKF v0.1 conformance — docs
|
|
|
190
225
|
|
|
191
226
|
$ okf server docs
|
|
192
227
|
serving 37 concepts at http://127.0.0.1:8808 (Ctrl-C to stop)
|
|
228
|
+
|
|
229
|
+
$ okf render docs > public/index.html # the same page, static — host it anywhere
|
|
193
230
|
```
|
|
194
231
|
|
|
195
232
|
<picture>
|
|
196
233
|
<source media="(prefers-color-scheme: dark)" srcset=".github/server-dark.png">
|
|
197
|
-
<img src=".github/server-
|
|
234
|
+
<img src=".github/server-light.png" alt="The okf graph server: a force-directed knowledge graph with a concept selected, its neighbors highlighted, and the inspector panel showing the concept's type, tags, cross-links, and rendered Markdown body.">
|
|
198
235
|
</picture>
|
|
199
236
|
|
|
200
237
|
_The graph server on this repo's own [`.okf`](.okf) bundle, with the
|
|
201
238
|
`capabilities/graph-server` concept selected. Try it live at
|
|
202
239
|
**[demo.okfgem.com](https://demo.okfgem.com)**._
|
|
203
240
|
|
|
241
|
+
To skip the server entirely, **`okf render <dir>`** writes that same page as one
|
|
242
|
+
self-contained HTML file, the whole bundle baked in, so you can publish the
|
|
243
|
+
graph on GitHub Pages or any static host.
|
|
244
|
+
|
|
204
245
|
`graph` and `server` are best-effort (§9): a file with invalid frontmatter is
|
|
205
246
|
skipped (and noted on stderr), not fatal, so one bad file never breaks the rest.
|
|
206
247
|
The [graph server](.okf/capabilities/graph-server.md) concept walks the request
|
|
@@ -251,15 +292,16 @@ skill plus a post-edit curation hook.
|
|
|
251
292
|
The skill routes a small set of verbs. In Claude Code they run as `/okf:gem
|
|
252
293
|
<verb>`; used standalone, the skill infers the verb from your request.
|
|
253
294
|
|
|
254
|
-
| Verb | What it does
|
|
255
|
-
| ---------------- |
|
|
256
|
-
| _(none)_ | Orient on the bundle and recommend the highest-value next move
|
|
257
|
-
| `
|
|
258
|
-
| `
|
|
259
|
-
| `
|
|
260
|
-
| `
|
|
261
|
-
| `
|
|
262
|
-
|
|
|
295
|
+
| Verb | What it does |
|
|
296
|
+
| ---------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
297
|
+
| _(none)_ | Orient on the bundle and recommend the highest-value next move |
|
|
298
|
+
| `search` | Answer a question from the bundle, token-lean: the map, the finder, only the winning bodies |
|
|
299
|
+
| `produce` | Create or extend a bundle from code, docs, or knowledge in people's heads |
|
|
300
|
+
| `maintain` | Sync the bundle's content with reality after the code or docs change |
|
|
301
|
+
| `consume` | Use the bundle as context for a task, writing back what you learn |
|
|
302
|
+
| `curate` | Structural upkeep as it stands: `validate` + `lint` + `loose` |
|
|
303
|
+
| `doctor` | Install and verify the CLI, then doctor the bundle |
|
|
304
|
+
| `<okf-cli-verb>` | Run any CLI verb (`validate`, `lint`, `search`, `index`, `server`, the read views) and interpret its output |
|
|
263
305
|
|
|
264
306
|
Point it at your agent's config directory (or its skills directory) and the tree
|
|
265
307
|
settles in its own `skills/okf/` folder, so a shared skills directory never gets
|
|
@@ -323,6 +365,8 @@ folder.bundle # => OKF::Bundle (the pure bundle it read)
|
|
|
323
365
|
folder.concepts # => [OKF::Concept] (reserved files excluded)
|
|
324
366
|
folder.validate; folder.lint; folder.graph # delegate to the pure core
|
|
325
367
|
folder.concept("tables/orders") # => OKF::Concept::File
|
|
368
|
+
|
|
369
|
+
require "okf/server/app" # the server loads on demand, like the CLI does
|
|
326
370
|
OKF::Server::App.new(folder) # => a Rack app: the interactive graph server
|
|
327
371
|
|
|
328
372
|
# build in memory, then write it out (validates §9 before publishing):
|
|
@@ -335,7 +379,8 @@ file.save; file.delete; file.reload
|
|
|
335
379
|
|
|
336
380
|
The lower-level pieces are usable on their own too: `OKF::Bundle::Validator.call(bundle)`,
|
|
337
381
|
`OKF::Bundle::Linter.call(bundle, min_body: 50)`, `OKF::Bundle::Graph.build(bundle)`,
|
|
338
|
-
`OKF::Markdown::Frontmatter.parse(markdown)`.
|
|
382
|
+
`OKF::Markdown::Frontmatter.parse(markdown)`. Mounting the graph in Rails, auth
|
|
383
|
+
included: the [Rails guide](https://okfgem.com/docs/guides/rails/) walks it.
|
|
339
384
|
|
|
340
385
|
### Conformance model
|
|
341
386
|
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OKF
|
|
4
|
+
class Bundle
|
|
5
|
+
# Deterministic text retrieval over an in-memory bundle — the browser page's
|
|
6
|
+
# search brought server-side and extended to bodies. Terms are ANDed: every
|
|
7
|
+
# term must hit at least one searched field, though not necessarily the same
|
|
8
|
+
# one. A term is a case-insensitive substring, or a Ruby regular expression
|
|
9
|
+
# with `regexp: true`. Matches rank by where they hit (a title hit outranks a
|
|
10
|
+
# body hit) and carry one bounded context snippet, so answering "which concept
|
|
11
|
+
# covers X?" costs a row, not a body read.
|
|
12
|
+
#
|
|
13
|
+
# Deliberately not fuzzy: the consuming agent is the fuzzy layer — synonyms
|
|
14
|
+
# and vocabulary drift are judgment over the index map, not string distance.
|
|
15
|
+
#
|
|
16
|
+
# Pure — no disk, no stdio. The CLI's `okf search` and any embedding app share
|
|
17
|
+
# it: OKF::Bundle::Search.call(bundle, [ "dedup", "key" ]).
|
|
18
|
+
class Search
|
|
19
|
+
# The searchable fields with their rank weight, strongest signal first.
|
|
20
|
+
# A concept's score sums the weights of the fields that matched; hitting a
|
|
21
|
+
# field twice does not stack. Tags match against the space-joined list,
|
|
22
|
+
# mirroring the server page's haystack.
|
|
23
|
+
WEIGHTS = {
|
|
24
|
+
"title" => 5,
|
|
25
|
+
"id" => 4,
|
|
26
|
+
"tags" => 3,
|
|
27
|
+
"type" => 2,
|
|
28
|
+
"description" => 2,
|
|
29
|
+
"body" => 1
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
FIELDS = WEIGHTS.keys.freeze
|
|
33
|
+
|
|
34
|
+
# Fields whose match is only meaningful with surrounding context. The other
|
|
35
|
+
# fields already appear whole on the result row.
|
|
36
|
+
SNIPPET_FIELDS = %w[description body].freeze
|
|
37
|
+
|
|
38
|
+
# Characters of context kept on each side of the first matched term.
|
|
39
|
+
SNIPPET_RADIUS = 44
|
|
40
|
+
|
|
41
|
+
def self.call(bundle, terms, fields: nil, regexp: false)
|
|
42
|
+
new(bundle, terms, fields: fields, regexp: regexp).results
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Raises RegexpError on an invalid pattern with `regexp: true` — the caller
|
|
46
|
+
# owns turning that into a usage error.
|
|
47
|
+
def initialize(bundle, terms, fields: nil, regexp: false)
|
|
48
|
+
@bundle = bundle
|
|
49
|
+
raw = Array(terms).reject { |term| OKF.blank?(term) }
|
|
50
|
+
@matchers = raw.map { |term| regexp ? Regexp.new(term.to_s, Regexp::IGNORECASE) : term.to_s.downcase }
|
|
51
|
+
@fields = fields.nil? || fields.empty? ? FIELDS : fields
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Ranked match rows, catalog-style identity plus where the terms hit:
|
|
55
|
+
# [{ id:, title:, type:, area:, tags:, matched: [field, …], score:, snippet: }, …]
|
|
56
|
+
# ordered by score descending, then id. No terms means no matches.
|
|
57
|
+
def results
|
|
58
|
+
return [] if @matchers.empty?
|
|
59
|
+
|
|
60
|
+
@bundle.concepts
|
|
61
|
+
.map { |concept| match(concept) }
|
|
62
|
+
.compact
|
|
63
|
+
.sort_by { |row| [ -row[:score], row[:id] ] }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def match(concept)
|
|
69
|
+
texts = searchable_texts(concept)
|
|
70
|
+
matched = matched_fields(texts)
|
|
71
|
+
return nil if matched.nil?
|
|
72
|
+
|
|
73
|
+
{
|
|
74
|
+
id: concept.id,
|
|
75
|
+
title: (concept.title || concept.id).to_s,
|
|
76
|
+
type: concept.type.to_s,
|
|
77
|
+
area: area_of(concept.id),
|
|
78
|
+
tags: Array(concept.tags).map(&:to_s),
|
|
79
|
+
matched: matched,
|
|
80
|
+
score: matched.map { |field| WEIGHTS[field] }.reduce(0, :+),
|
|
81
|
+
snippet: snippet(texts, matched)
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# { field => original-case text } for the fields this search reads.
|
|
86
|
+
def searchable_texts(concept)
|
|
87
|
+
texts = {
|
|
88
|
+
"id" => concept.id,
|
|
89
|
+
"title" => concept.title.to_s,
|
|
90
|
+
"type" => concept.type.to_s,
|
|
91
|
+
"description" => concept.description.to_s,
|
|
92
|
+
"tags" => Array(concept.tags).join(" "),
|
|
93
|
+
"body" => concept.body
|
|
94
|
+
}
|
|
95
|
+
texts.each_with_object({}) do |(field, text), acc|
|
|
96
|
+
acc[field] = text if @fields.include?(field)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The union of fields any term hit, in WEIGHTS order — or nil when some term
|
|
101
|
+
# hit nothing (terms are ANDed).
|
|
102
|
+
def matched_fields(texts)
|
|
103
|
+
hits = @matchers.map do |matcher|
|
|
104
|
+
fields = texts.keys.select { |field| hit?(matcher, texts[field]) }
|
|
105
|
+
return nil if fields.empty?
|
|
106
|
+
|
|
107
|
+
fields
|
|
108
|
+
end
|
|
109
|
+
FIELDS.select { |field| hits.flatten.include?(field) }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def hit?(matcher, text)
|
|
113
|
+
matcher.is_a?(Regexp) ? matcher.match?(text) : text.downcase.include?(matcher)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# One bounded context window around the first term that hit the strongest
|
|
117
|
+
# snippet-worthy field; "" when the match needs no context (id/title/type/tags).
|
|
118
|
+
def snippet(texts, matched)
|
|
119
|
+
field = SNIPPET_FIELDS.find { |candidate| matched.include?(candidate) }
|
|
120
|
+
return "" if field.nil?
|
|
121
|
+
|
|
122
|
+
matcher = @matchers.find { |candidate| hit?(candidate, texts[field]) }
|
|
123
|
+
context(texts[field], matcher)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def context(text, matcher)
|
|
127
|
+
flat = text.gsub(/\s+/, " ").strip
|
|
128
|
+
at, length = locate(flat, matcher)
|
|
129
|
+
from = [ at - SNIPPET_RADIUS, 0 ].max
|
|
130
|
+
to = at + length + SNIPPET_RADIUS
|
|
131
|
+
clip = flat[from, to - from].to_s.strip
|
|
132
|
+
clip = "…#{clip}" if from.positive?
|
|
133
|
+
clip = "#{clip}…" if to < flat.length
|
|
134
|
+
clip
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# [ position, length ] of the matcher's first hit in the flattened text —
|
|
138
|
+
# [ 0, 0 ] when a pattern that hit the raw text cannot be found again after
|
|
139
|
+
# whitespace collapsing (e.g. an explicit \n), so the window opens at the top.
|
|
140
|
+
def locate(flat, matcher)
|
|
141
|
+
if matcher.is_a?(Regexp)
|
|
142
|
+
found = matcher.match(flat)
|
|
143
|
+
found ? [ found.begin(0), found[0].length ] : [ 0, 0 ]
|
|
144
|
+
else
|
|
145
|
+
[ flat.downcase.index(matcher) || 0, matcher.length ]
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# A concept's top-level area, mirroring the catalog's definition.
|
|
150
|
+
def area_of(id)
|
|
151
|
+
id.include?("/") ? id.split("/").first : "(root)"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
data/lib/okf/bundle.rb
CHANGED
|
@@ -136,8 +136,8 @@ module OKF
|
|
|
136
136
|
# the map on the fly (§6 permits exactly that). Grouped by the concept's file
|
|
137
137
|
# path — index files are physical directory listings, so a custom frontmatter
|
|
138
138
|
# `id` must not move a concept out of the directory it lives in. Pure: derived
|
|
139
|
-
# from the concepts and the reserved index text, no disk. Shared by the
|
|
140
|
-
# index` view and the server's /index
|
|
139
|
+
# from the concepts and the reserved index text, no disk. Shared by the
|
|
140
|
+
# `okf index` view and the server's Index panel (/index).
|
|
141
141
|
def directory_index
|
|
142
142
|
by_dir = concepts.group_by { |concept| File.dirname(concept.path) }
|
|
143
143
|
dirs = directory_set(by_dir.keys)
|