okf 1.8.0 → 1.9.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 +218 -1
- data/README.md +10 -7
- data/lib/okf/bundle/folder.rb +16 -0
- data/lib/okf/bundle/search/index.rb +65 -0
- data/lib/okf/bundle/search/scan.rb +89 -0
- data/lib/okf/bundle/search.rb +262 -66
- data/lib/okf/bundle.rb +2 -2
- data/lib/okf/cli.rb +95 -25
- data/lib/okf/{server → render}/graph/template.html.erb +535 -116
- data/lib/okf/{server → render}/graph.rb +39 -7
- data/lib/okf/server/app.rb +7 -44
- data/lib/okf/skill/SKILL.md +27 -16
- data/lib/okf/skill/playbooks/consume.md +5 -3
- data/lib/okf/skill/playbooks/maintain.md +1 -1
- data/lib/okf/skill/playbooks/search.md +47 -7
- data/lib/okf/skill/reference/authoring.md +3 -2
- data/lib/okf/skill/reference/cli.md +76 -17
- data/lib/okf/version.rb +1 -1
- data/lib/okf.rb +8 -0
- metadata +19 -3
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
require "rack/utils"
|
|
4
4
|
|
|
5
5
|
module OKF
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
# The view layer: turns a bundle into the interactive graph page. Pairs with the
|
|
7
|
+
# pure OKF::Bundle::Graph (the data model) — Bundle builds the graph, Render
|
|
8
|
+
# draws it. A shell (reads the template, escapes with rack/utils), but knows
|
|
9
|
+
# nothing about HTTP: OKF::Server::App serves what this produces, and `okf
|
|
10
|
+
# render` writes it to a file, from the one class.
|
|
11
|
+
module Render
|
|
12
|
+
# Renders an OKF::Bundle::Graph as the interactive graph page. The markup lives
|
|
13
|
+
# in graph/template.html.erb; #render returns the HTML string.
|
|
10
14
|
#
|
|
11
15
|
# The page boots from a *minimal* payload — nodes carry only id + title, plus
|
|
12
16
|
# compact TYPES/TAGS inverted indexes for colouring and filtering. It has two
|
|
@@ -16,9 +20,9 @@ module OKF
|
|
|
16
20
|
# metadata, catalog, index and log are pulled from OKF::Server::App on
|
|
17
21
|
# demand via fetch, so the initial payload stays small and bodies read
|
|
18
22
|
# live from disk (edits show without a restart). Nothing extra embedded.
|
|
19
|
-
# render mode (embed: payload) — `okf render` bakes the whole bundle in
|
|
20
|
-
# the same fetch getters resolve from the injected payload
|
|
21
|
-
# the single file needs no server (e.g.
|
|
23
|
+
# render mode (embed: payload) — `okf render` bakes the whole bundle in via
|
|
24
|
+
# .static below: the same fetch getters resolve from the injected payload
|
|
25
|
+
# instead, so the single file needs no server (e.g. GitHub Pages).
|
|
22
26
|
#
|
|
23
27
|
# NOTE (trust boundary): the page loads Cytoscape + marked from a CDN, so it
|
|
24
28
|
# needs network for those libraries even in render mode. Fetched/embedded
|
|
@@ -38,6 +42,28 @@ module OKF
|
|
|
38
42
|
# built from the backslash code point so no literal escape appears here.
|
|
39
43
|
LT_ESCAPE = (92.chr(Encoding::UTF_8) + "u003c").freeze
|
|
40
44
|
|
|
45
|
+
# `okf render`: the whole page as one self-contained file, the bundle baked
|
|
46
|
+
# in, so it hosts where no server answers a fetch. Takes any bundle handle
|
|
47
|
+
# (an OKF::Bundle::Folder) and returns the HTML string.
|
|
48
|
+
def self.static(folder, title: nil, link: nil, layout: "cose")
|
|
49
|
+
new(folder.graph(minimal: true), title: title || folder.name, link: link, layout: layout, embed: payload(folder)).render
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# What the baked page carries in place of the endpoints a live server would
|
|
53
|
+
# answer. Every key here is data a client getter reads from EMBED instead of
|
|
54
|
+
# fetching — and each derives from the *same* folder method the matching
|
|
55
|
+
# OKF::Server::App endpoint uses, so the bake and the live server cannot
|
|
56
|
+
# drift (/node/meta is the exception: the fragment is derived on the client
|
|
57
|
+
# from the catalog's raw description, so no map is baked for it).
|
|
58
|
+
def self.payload(folder)
|
|
59
|
+
{
|
|
60
|
+
catalog: folder.catalog,
|
|
61
|
+
index: folder.directory_index,
|
|
62
|
+
logs: folder.log_entries,
|
|
63
|
+
bodies: folder.concepts.each_with_object({}) { |concept, map| map[concept.id] = concept.body.to_s }
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
41
67
|
# +node_endpoint+/+meta_endpoint+ are the (mount-relative) URLs the page
|
|
42
68
|
# fetches a concept's raw markdown and metadata fragment from — relative so
|
|
43
69
|
# the page works whether served at "/" or mounted under a Rails prefix.
|
|
@@ -74,6 +100,12 @@ module OKF
|
|
|
74
100
|
html_escape(graph_name)
|
|
75
101
|
end
|
|
76
102
|
|
|
103
|
+
# The bundle's own name, for the client: what the header already shows, so
|
|
104
|
+
# the page can label the root with it instead of `(root)` or `/`.
|
|
105
|
+
def name_json
|
|
106
|
+
json_for_script(graph_name)
|
|
107
|
+
end
|
|
108
|
+
|
|
77
109
|
def og_title
|
|
78
110
|
html_escape("OKF · #{graph_name}")
|
|
79
111
|
end
|
data/lib/okf/server/app.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "rack"
|
|
4
4
|
|
|
5
|
-
require "okf/
|
|
5
|
+
require "okf/render/graph"
|
|
6
6
|
|
|
7
7
|
module OKF
|
|
8
8
|
module Server
|
|
@@ -11,7 +11,7 @@ module OKF
|
|
|
11
11
|
#
|
|
12
12
|
# mount OKF::Server::App.new(folder) => "/knowledge"
|
|
13
13
|
#
|
|
14
|
-
# The page (OKF::
|
|
14
|
+
# The page (OKF::Render::Graph) boots from a *minimal* graph (id + title + edges
|
|
15
15
|
# + type/tag indexes) and pulls each concept's markdown body and description from
|
|
16
16
|
# here on demand, so the initial payload stays small and bodies are read live
|
|
17
17
|
# from disk (edits show without a restart). Part of the shell — it does I/O.
|
|
@@ -62,13 +62,6 @@ module OKF
|
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
# The same interactive page, but with the whole bundle baked in — bodies,
|
|
66
|
-
# catalog, index and logs — so it needs no server. This is what `okf render`
|
|
67
|
-
# writes: the fetch getters resolve from the embedded payload, not from here.
|
|
68
|
-
def render_static
|
|
69
|
-
Graph.new(graph, title: @title || @folder.name, link: @link, layout: @layout, embed: embed_payload).render
|
|
70
|
-
end
|
|
71
|
-
|
|
72
65
|
# The 404 both this app and the Hub answer with, so the two cannot drift.
|
|
73
66
|
def self.not_found
|
|
74
67
|
[ 404, { "content-type" => "text/plain; charset=utf-8" }, [ "not found\n" ] ]
|
|
@@ -97,50 +90,20 @@ module OKF
|
|
|
97
90
|
{ directories: @folder.directory_index }
|
|
98
91
|
end
|
|
99
92
|
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
93
|
+
# The §7 history the Log panel renders: every log.md with its content, root
|
|
94
|
+
# scope first, read live from disk. Built by OKF::Bundle::Folder#log_entries,
|
|
95
|
+
# shared with `okf render`'s bake so the served and baked logs cannot drift.
|
|
103
96
|
def logs
|
|
104
|
-
|
|
105
|
-
{ logs: entries.map { |path| { path: path, dir: File.dirname(path), content: log_content(path) } } }
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def log_content(path)
|
|
109
|
-
File.read(File.join(@folder.root, path), encoding: "UTF-8")
|
|
110
|
-
rescue SystemCallError
|
|
111
|
-
@folder.bundle.reserved_content(path)
|
|
97
|
+
{ logs: @folder.log_entries }
|
|
112
98
|
end
|
|
113
99
|
|
|
114
100
|
def page
|
|
115
|
-
@page ||= Graph.new(
|
|
101
|
+
@page ||= OKF::Render::Graph.new(
|
|
116
102
|
graph, title: @title || @folder.name, link: @link, layout: @layout,
|
|
117
103
|
siblings: @siblings, self_slug: @self_slug, hub_path: @hub_path
|
|
118
104
|
).render
|
|
119
105
|
end
|
|
120
106
|
|
|
121
|
-
# Everything the on-demand endpoints would serve, baked for render mode. The
|
|
122
|
-
# arrays match what each client getter extracts from the JSON envelope; the
|
|
123
|
-
# per-concept maps mirror /node (raw, unstripped body) and /node/meta (the
|
|
124
|
-
# same escaped fragment). Read from the in-memory bundle — no live disk read,
|
|
125
|
-
# since a static file is a snapshot, not a window on edits.
|
|
126
|
-
def embed_payload
|
|
127
|
-
{
|
|
128
|
-
catalog: @folder.catalog,
|
|
129
|
-
index: @folder.directory_index,
|
|
130
|
-
logs: logs[:logs],
|
|
131
|
-
bodies: bodies,
|
|
132
|
-
meta: meta
|
|
133
|
-
}
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def bodies
|
|
137
|
-
@folder.bundle.concepts.each_with_object({}) { |concept, map| map[concept.id] = concept.body.to_s }
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def meta
|
|
141
|
-
@folder.bundle.concepts.each_with_object({}) { |concept, map| map[concept.id] = description_fragment(concept) }
|
|
142
|
-
end
|
|
143
|
-
|
|
144
107
|
def node_body(id)
|
|
145
108
|
concept = concept_for(id)
|
|
146
109
|
return not_found if concept.nil?
|
data/lib/okf/skill/SKILL.md
CHANGED
|
@@ -5,14 +5,15 @@ description: >-
|
|
|
5
5
|
directory of markdown files with YAML frontmatter that humans and agents read
|
|
6
6
|
from one source. Use when capturing knowledge into a bundle (a service, schema,
|
|
7
7
|
metric, decision, runbook: "document this in OKF", "capture this as a concept"),
|
|
8
|
+
converting existing docs into one ("migrate/OKFy our docs into a bundle"),
|
|
8
9
|
retrieving from one without reading it whole ("what do we know about X?", "where
|
|
9
|
-
is X documented?", "search the bundle"), updating one after code or docs
|
|
10
|
-
("update the knowledge bundle"), checking its conformance or curation
|
|
11
|
-
("validate/lint the bundle"), serving or rendering it as a graph, or
|
|
12
|
-
repo that already carries an OKF bundle — a `.okf/` directory or a
|
|
13
|
-
carrying `okf_version`.
|
|
10
|
+
is X documented?", "search the bundle"), updating one after code or docs
|
|
11
|
+
change ("update the knowledge bundle"), checking its conformance or curation
|
|
12
|
+
quality ("validate/lint the bundle"), serving or rendering it as a graph, or
|
|
13
|
+
working in a repo that already carries an OKF bundle — a `.okf/` directory or a
|
|
14
|
+
root `index.md` carrying `okf_version`.
|
|
14
15
|
user-invocable: true
|
|
15
|
-
argument-hint: "[search|produce|maintain|consume|<okf-cli-verb>] [dir] [--flags]"
|
|
16
|
+
argument-hint: "[search|produce|migrate|maintain|consume|curate|doctor|<okf-cli-verb>] [dir|@slug] [--flags]"
|
|
16
17
|
allowed-tools: Read Write Edit Grep Glob Bash
|
|
17
18
|
---
|
|
18
19
|
|
|
@@ -66,12 +67,16 @@ earn your keep as the expert, not the executable.
|
|
|
66
67
|
|
|
67
68
|
## The CLI is your eyes — you are the judgment
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
The `okf` executable answers every mechanical question deterministically, and its
|
|
71
|
+
read views show everything the browser UI does. **Don't probe for it — just run
|
|
72
|
+
the verb.** A proactive `command -v okf` before every task spends a whole tool
|
|
73
|
+
round proving what the next command reveals for free; the CLI's own failure is a
|
|
74
|
+
cheaper, truer signal. (The two deliberate exceptions are [menu](playbooks/menu.md)
|
|
75
|
+
and [doctor](playbooks/doctor.md) — both decide *whether to install*, so they check
|
|
76
|
+
first.) The one distinction to hold: a shell `okf: command not
|
|
77
|
+
found` is the *only* thing that means "install it" (→ [doctor](playbooks/doctor.md));
|
|
78
|
+
every line that starts `error:` is okf *answering* — a bundle or usage result to
|
|
79
|
+
read and act on, never a missing toolchain to send to doctor.
|
|
75
80
|
|
|
76
81
|
Don't memorize the surface — `okf --help` maps every verb, `okf <verb> --help` its
|
|
77
82
|
flags. The division of labour is the whole game:
|
|
@@ -98,7 +103,7 @@ shapes, the tag-curation views, the server's trust boundary.
|
|
|
98
103
|
## Orient before you touch anything
|
|
99
104
|
|
|
100
105
|
Picking up a bundle you don't already know — to consume or maintain — run `okf
|
|
101
|
-
index <dir>` (the §6 map: every directory's index body, rollups, and listings) and
|
|
106
|
+
index <dir|@slug>` (the §6 map: every directory's index body, rollups, and listings) and
|
|
102
107
|
read `log.md` (the §7 baseline of what changed last) **before** greping or opening
|
|
103
108
|
leaves. It is the cheapest high-signal context, and the only reliable way to catch
|
|
104
109
|
enumeration drift: **grep cannot find an index entry that is missing** — you can't
|
|
@@ -123,9 +128,15 @@ is X documented" → `search`; a repo already carrying a bundle plus a task
|
|
|
123
128
|
needing its knowledge → `consume`; "check / graph / preview it" → run the
|
|
124
129
|
matching CLI verb and interpret the result. When genuinely ambiguous, ask.
|
|
125
130
|
|
|
126
|
-
**Which
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
**Which target?** A leading `@` is a *registry ref*, not a path: `@slug` names a
|
|
132
|
+
bundle registered with `okf registry set`, bare `@` the default — route it
|
|
133
|
+
straight to `okf <verb> @slug` and skip the directory hunt (`okf search` spans
|
|
134
|
+
several: `@a @b`, or `@all`). A plain path is used as given. Given no target and a
|
|
135
|
+
cwd that carries no bundle, `okf registry list` is the next move, not a hunt
|
|
136
|
+
across sibling directories. Producing a *new* bundle with no path? Default to
|
|
137
|
+
`.okf/` at the repo root, but first detect whether the project already keeps its
|
|
138
|
+
bundle elsewhere (e.g. `docs/`) and prefer that; commit it alongside the code it
|
|
139
|
+
describes.
|
|
129
140
|
|
|
130
141
|
**Target isn't a bundle?** When a verb points at a directory that holds markdown
|
|
131
142
|
but no root `index.md` carrying `okf_version` — `validate` failing wholesale on
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# Playbook: consume — use a bundle as context
|
|
2
2
|
|
|
3
|
-
1. **Orient first** (the [SKILL.md](../SKILL.md) reflex): `okf index <dir>` maps
|
|
4
|
-
whole bundle in one pass — every directory's index body, rollups, and listings —
|
|
5
|
-
and `log.md` gives recent history.
|
|
3
|
+
1. **Orient first** (the [SKILL.md](../SKILL.md) reflex): `okf index <dir|@slug>` maps
|
|
4
|
+
the whole bundle in one pass — every directory's index body, rollups, and listings —
|
|
5
|
+
and `log.md` gives recent history. Address a registered bundle by `@slug` (bare
|
|
6
|
+
`@` = the default); if the cwd carries no bundle, `okf registry list` finds one
|
|
7
|
+
instead of a directory hunt. Then follow links only into the concepts the
|
|
6
8
|
task needs. For a *pointed question* rather than broad context, switch to the
|
|
7
9
|
[search playbook](search.md): map → finder (`okf search`) → only the winning
|
|
8
10
|
bodies. For a large bundle, `okf graph --json --minimal` gives the whole link
|
|
@@ -43,7 +43,7 @@ up. The modelling craft behind steps 3 and 7 lives in
|
|
|
43
43
|
by design only through its index — leave it. **Terminal-by-design is not a
|
|
44
44
|
defect.** Loose ≠ orphan: an index listing makes a file *reachable* (not an
|
|
45
45
|
orphan) but is not a graph edge, so an indexed file can still float here.
|
|
46
|
-
7. **Curate the tag vocabulary**
|
|
46
|
+
7. **Curate the tag vocabulary** when the pass
|
|
47
47
|
touched tags, or when `okf tags <dir>` shows a long tail of singletons. Run `okf tags <dir> --by area` and
|
|
48
48
|
`--by type` — the grouped view is the analysis; read each group top-down:
|
|
49
49
|
- **twins** — two tags riding the exact same concepts (equal counts sort them
|
|
@@ -6,9 +6,13 @@ can query cheaply is dead weight. The discipline is progressive disclosure
|
|
|
6
6
|
(spec §6): every step pays a few hundred bytes to decide what the next step
|
|
7
7
|
reads, and full bodies are read last, and only the winners.
|
|
8
8
|
|
|
9
|
-
1. **
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
1. **Just run it — no presence probe.** Point the finder at a path or an `@slug`
|
|
10
|
+
(a registered bundle; bare `@` = the default). Only a shell `okf: command not
|
|
11
|
+
found` means install (→ [doctor](doctor.md)); with no CLI possible at all, read
|
|
12
|
+
the root `index.md` then each relevant area's `index.md` by hand. No bundle in
|
|
13
|
+
the cwd? `okf registry list` names the registered ones — address them by
|
|
14
|
+
`@slug`, don't hunt sibling directories.
|
|
15
|
+
2. **Ingest the map and decide where to look.** `okf index <dir|@slug> --no-body` is
|
|
12
16
|
the skeleton: every directory with its concept count, types, tags, children.
|
|
13
17
|
*You* do the semantic matching here — the question names a meaning, the map
|
|
14
18
|
names areas; connect them by judgment, not string equality. When an area
|
|
@@ -17,8 +21,37 @@ reads, and full bodies are read last, and only the winners.
|
|
|
17
21
|
<!-- rule:okf-search-map-first -->
|
|
18
22
|
3. **Cut across with the finder when the question is lexical.** An exact
|
|
19
23
|
symbol, an error code, a column name, a phrase — things structure won't
|
|
20
|
-
surface — go to `okf search <dir> <terms
|
|
21
|
-
|
|
24
|
+
surface — go to `okf search <dir> <terms>`. Terms AND together and are matched
|
|
25
|
+
**literally against raw text**, so an exact query means what it looks like: a
|
|
26
|
+
phrase, a dotted version (`7.2.0`), an underscored identifier (`customer_id`),
|
|
27
|
+
a mid-word fragment (`ustomer`) and a word written in `backticks` all match
|
|
28
|
+
the way you typed them. <!-- rule:okf-search-exact-identifiers -->
|
|
29
|
+
|
|
30
|
+
**Match the engine to the shape of the query, not to habit** — the default
|
|
31
|
+
answers most of them, and the two engines fail in opposite directions:
|
|
32
|
+
<!-- rule:okf-search-engine-choice -->
|
|
33
|
+
|
|
34
|
+
| Your query is | Reach for | Because |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| an identifier, version, path, phrase, or anything in `` `backticks` `` | *nothing — the default* | matched literally; the index shatters all of these |
|
|
37
|
+
| a mid-word fragment (`ustomer`) | *nothing — the default* | an infix is not a token, so the index cannot reach it |
|
|
38
|
+
| a pattern (`err_[a-z]+_409`) | `-e` | Ruby regexp over raw text; still the scan |
|
|
39
|
+
| a partial word (`dedup` → `deduplication`) | *nothing — the default* | substring covers prefixes, and suffixes and infixes too |
|
|
40
|
+
| a theme, where you want the best match to lead | `--engine index` | BM25+ ranks by relevance, not by summed field weight |
|
|
41
|
+
| possibly mistyped | `--fuzzy` | edit distance 0.2 × term length — the index's alone |
|
|
42
|
+
| being reconciled with the browser page | `--engine index` | same MiniSearch build, so the two rank alike |
|
|
43
|
+
|
|
44
|
+
The index has exactly **three** things the default lacks: relevance ranking,
|
|
45
|
+
typo tolerance, and page parity. Its `prefix` capability is not a fourth — a
|
|
46
|
+
substring match already reaches every prefix, so `prefix` is what the index
|
|
47
|
+
needs to *catch up*, not a reason to choose it.
|
|
48
|
+
|
|
49
|
+
**`--fuzzy` is an engine switch, not a mode.** It routes to the index, so a
|
|
50
|
+
run that only wanted a typo forgiven also gets token matching, shattered
|
|
51
|
+
identifiers and unfindable code spans. Fix the spelling and stay on the
|
|
52
|
+
default when you can. <!-- rule:okf-search-fuzzy-is-a-switch -->
|
|
53
|
+
|
|
54
|
+
Scope any of them with what the map taught you:
|
|
22
55
|
`--area billing`, `--type Decision`, `--tag idempotency`, `--in body`.
|
|
23
56
|
Matches rank by where they hit, and the snippet often *is* the answer.
|
|
24
57
|
When the answer may live in another registered bundle, span them — leading
|
|
@@ -40,6 +73,13 @@ Anti-patterns, each a real token bill:
|
|
|
40
73
|
- **Grep before map.** Grep cannot find the entry that is *missing*, and it
|
|
41
74
|
returns line noise where `search` returns ranked concepts. Grep is the
|
|
42
75
|
fallback when the CLI is absent, not the first move.
|
|
43
|
-
- **Mechanical synonym retries.** The finder is exact by
|
|
76
|
+
- **Mechanical synonym retries.** The finder is exact by default; *you* are the
|
|
44
77
|
fuzzy layer. When terms miss, learn the bundle's vocabulary — `okf tags
|
|
45
|
-
<dir>`, `okf types <dir>` — and re-ask in its own words.
|
|
78
|
+
<dir>`, `okf types <dir>` — and re-ask in its own words. `--fuzzy` forgives a
|
|
79
|
+
*typo*, not a wrong vocabulary, so it is the wrong reach for this.
|
|
80
|
+
- **Flag-shopping a query that found nothing.** Cycling `--fuzzy`, then
|
|
81
|
+
`--engine index`, then `-e` over the same terms is guessing, and each engine
|
|
82
|
+
fails differently enough that one of them eventually returns *something* —
|
|
83
|
+
which is how a wrong answer gets found. Zero matches is usually a vocabulary
|
|
84
|
+
result, not an engine result: go back to the map and the tag list. Reach for a
|
|
85
|
+
different engine when you can say which property of the query needs it.
|
|
@@ -115,8 +115,9 @@ bundle-root [root-index](../templates/root-index.md), [log](../templates/log.md)
|
|
|
115
115
|
## Playbooks
|
|
116
116
|
|
|
117
117
|
The step-by-step playbooks live in [../playbooks/](../playbooks/), one file per
|
|
118
|
-
verb (produce, maintain, consume, curate, doctor), routed by the
|
|
119
|
-
in [SKILL.md](../SKILL.md). The Closeout below is their shared
|
|
118
|
+
verb (search, produce, migrate, maintain, consume, curate, doctor), routed by the
|
|
119
|
+
Commands table in [SKILL.md](../SKILL.md). The Closeout below is their shared
|
|
120
|
+
finishing gate.
|
|
120
121
|
|
|
121
122
|
## Closeout — the finishing gate
|
|
122
123
|
|
|
@@ -6,14 +6,13 @@ reimplemented in this skill. They run the deterministic `okf` executable shipped
|
|
|
6
6
|
the companion gem — the single source of truth for OKF mechanics. Your job is to
|
|
7
7
|
invoke it correctly and interpret the result, not to reason out conformance by hand.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## When it isn't installed
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
```
|
|
11
|
+
Don't probe for the tool before using it — just run the verb. A shell `okf:
|
|
12
|
+
command not found` is the only thing that means the gem isn't installed: say so
|
|
13
|
+
and stop (`gem install okf`, or from a checkout `cd gem && bundle exec rake
|
|
14
|
+
install`); never fabricate a result. Any line that starts `error:` is the CLI
|
|
15
|
+
*answering* — a bundle or usage result to read, not a missing toolchain.
|
|
17
16
|
|
|
18
17
|
## Invocation
|
|
19
18
|
|
|
@@ -140,16 +139,74 @@ defect — a terminal leaf (a backlog item, a spec reference) can be loose by de
|
|
|
140
139
|
The browser page's search brought to the CLI and extended to bodies, so "which
|
|
141
140
|
concept covers X?" costs rows, not body reads. `okf search <dir> <term…>`:
|
|
142
141
|
terms AND together — every term must hit at least one searched field, not
|
|
143
|
-
necessarily the same one —
|
|
144
|
-
expressions with `--regexp`/`-e` (an invalid pattern is a usage
|
|
142
|
+
necessarily the same one — matched **literally against raw text** by default, or
|
|
143
|
+
as Ruby regular expressions with `--regexp`/`-e` (an invalid pattern is a usage
|
|
144
|
+
error, exit 2). `--fuzzy` forgives typos; pairing it with `-e` is a usage error,
|
|
145
|
+
since a pattern is matched literally rather than by edit distance.
|
|
145
146
|
`--in a,b` restricts the searched fields (title, id, tags, type, description,
|
|
146
147
|
body); the shared `--type/--area/--tag` filters narrow the candidates *first*,
|
|
147
148
|
so a search scoped by what `index` taught you stays surgical.
|
|
148
149
|
|
|
150
|
+
**The default is exact, so an exact query means what it looks like.** A phrase in
|
|
151
|
+
one argument (`"dedup key"`), a dotted version (`7.2.0`), an underscored
|
|
152
|
+
identifier (`customer_id`), a mid-word fragment (`ustomer`) and a word written in
|
|
153
|
+
`backticks` all match literally. This is what the scan engine buys, and it is the
|
|
154
|
+
default precisely because those queries are the common ones and the alternative
|
|
155
|
+
loses them silently. <!-- rule:okf-search-exact-identifiers -->
|
|
156
|
+
|
|
157
|
+
**`--engine index` is the other engine, and the one to reach for when ranking
|
|
158
|
+
matters more than exactness.** The engine is normally chosen by what the query
|
|
159
|
+
needs — `--fuzzy` routes to the index, anything else stays on the default scan —
|
|
160
|
+
and nothing is printed about the choice. `--engine NAME` overrides that for the
|
|
161
|
+
case the flags cannot express: a matching *model* requires no capability, so no
|
|
162
|
+
flag selects one. Under the index, terms match whole tokens and their prefixes
|
|
163
|
+
(`dedup` finds `deduplication`), rows rank by BM25+, and it is the engine the
|
|
164
|
+
browser page runs — so name it when reconciling a CLI answer with the page. The
|
|
165
|
+
cost is real: its tokenizer splits on punctuation, so identifiers shatter
|
|
166
|
+
(`customer_id` → `customer` + `id`), an infix finds nothing, and a backtick is
|
|
167
|
+
never split off at all, so a word inside a code span is unfindable — a large
|
|
168
|
+
silent loss, since technical prose is full of them. **Do not count on ranking to
|
|
169
|
+
rescue it** — BM25 normalizes by field length, so a short concept dense in `7`,
|
|
170
|
+
`2` and `0` can outrank the one that actually says `7.2.0`. Naming an engine that
|
|
171
|
+
cannot do what you also asked (`--engine index -e`) is a usage error naming one
|
|
172
|
+
that can. <!-- rule:okf-search-engine-choice -->
|
|
173
|
+
|
|
174
|
+
**The capabilities, and which engine has them.** An engine is selected by what
|
|
175
|
+
the query *requires*; only a matching model has to be named, because requiring
|
|
176
|
+
nothing is not something a flag can express:
|
|
177
|
+
|
|
178
|
+
| Flag | Capability | Engine | What it does |
|
|
179
|
+
|---|---|---|---|
|
|
180
|
+
| *(none)* | — | scan | literal substring over raw text; scores by summed field weight |
|
|
181
|
+
| `-e` / `--regexp` | `regexp` | scan | each term is a Ruby regexp, case-insensitive; invalid → exit 2 |
|
|
182
|
+
| `--fuzzy` | `fuzzy` | **index** | edit distance 0.2 × term length — and switches engine |
|
|
183
|
+
| `--engine index` | — | index | whole-token + prefix matching, BM25+ ranking, browser parity |
|
|
184
|
+
| `--engine scan` | — | scan | the default, spelled out |
|
|
185
|
+
|
|
186
|
+
Two consequences worth holding. **`--fuzzy` is an engine switch, not a mode**: it
|
|
187
|
+
carries the whole index with it, so a run that wanted one typo forgiven also gets
|
|
188
|
+
shattered identifiers and unfindable code spans — fix the spelling and stay on
|
|
189
|
+
the default when you can. And **`-e` moves nothing** now, because the default
|
|
190
|
+
engine already offers `regexp`; it changes how a term is *read* (pattern rather
|
|
191
|
+
than literal), not where it is matched. <!-- rule:okf-search-fuzzy-is-a-switch -->
|
|
192
|
+
|
|
193
|
+
`prefix` is a capability the index declares but no flag selects — it is always on
|
|
194
|
+
there. **It is not a reason to reach for the index**: a substring match already
|
|
195
|
+
covers every prefix and then some, so `dedup` finds `deduplication` under both
|
|
196
|
+
engines, while `duplication` and `uplicat` find it under the default only. Prefix
|
|
197
|
+
is what the index needs to catch up to raw text, not a capability it adds on top.
|
|
198
|
+
The index's real advantages over the default are exactly three — relevance
|
|
199
|
+
ranking, typo tolerance, and page parity.
|
|
200
|
+
|
|
149
201
|
**Search spans bundles.** Leading @refs pick several registered bundles
|
|
150
202
|
(`okf search @handbook @notes auth`); **`@all`** is the ref that means every one.
|
|
151
|
-
|
|
152
|
-
|
|
203
|
+
Rows from different bundles are ranked together and comparable, and each row
|
|
204
|
+
carries its bundle's slug. Under `--engine index` the bundles go into **one
|
|
205
|
+
corpus** — BM25 prices a term by how rare it is, so separately-ranked lists would
|
|
206
|
+
not compare — which makes a score relative to the whole answer: the same concept
|
|
207
|
+
scores lower searched beside others than searched alone. The default scan needs
|
|
208
|
+
no such trick — its score is absolute, so a row is worth the same either way.
|
|
209
|
+
This is the
|
|
153
210
|
cross-bundle retrieval the in-page search does not have: one question, every
|
|
154
211
|
bundle you keep. <!-- rule:okf-search-all -->
|
|
155
212
|
|
|
@@ -178,12 +235,14 @@ which has no slug to give. Two sharp edges: every *leading* @-arg is taken as a
|
|
|
178
235
|
the CLI notes both traps on stderr — and any ref, even one, switches the JSON
|
|
179
236
|
envelope (next paragraph).
|
|
180
237
|
|
|
181
|
-
Rows rank by
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
238
|
+
Rows rank by where they hit — title 5, id 4, tags 3, type/description 2, body 1 —
|
|
239
|
+
summed as an absolute score by the default scan, and carried as per-field boost
|
|
240
|
+
into **BM25+** under `--engine index`. Each row carries one bounded context
|
|
241
|
+
snippet from the strongest match that needs context (description or body). Every row still names the fields that hit (`matched`), so a result stays
|
|
242
|
+
citable rather than being a bare relevance number. Exact by default: the
|
|
243
|
+
consuming agent is the fuzzy layer — when terms miss, learn the bundle's
|
|
244
|
+
vocabulary from `tags`/`types` and re-ask in its own words, rather than
|
|
245
|
+
hammering synonyms or reaching for `--fuzzy` before you have looked. Advisory read: **exit 0 even with zero matches**.
|
|
187
246
|
JSON, plain-dir mode: `{ bundle, query, count, matches: [{ id, title, type,
|
|
188
247
|
area, tags, matched, score, snippet }] }`. Registry mode — any leading @ref,
|
|
189
248
|
`@all` among them — swaps the envelope: `{ bundles: [{ slug, dir }, …],
|
data/lib/okf/version.rb
CHANGED
data/lib/okf.rb
CHANGED
|
@@ -40,6 +40,14 @@ module OKF
|
|
|
40
40
|
require "okf/bundle"
|
|
41
41
|
require "okf/bundle/graph"
|
|
42
42
|
require "okf/bundle/search"
|
|
43
|
+
# These two lines ARE the engine preference order. Each engine registers itself
|
|
44
|
+
# at load, `Search.engines` is registration order, and the router walks it after
|
|
45
|
+
# putting DEFAULT_ENGINE first — so reordering these requires reorders which
|
|
46
|
+
# engine answers a query two engines could both answer. `loading_test.rb` pins
|
|
47
|
+
# the result (`[:index, :scan]`) so the coupling cannot drift unnoticed, but the
|
|
48
|
+
# coupling is here, not there.
|
|
49
|
+
require "okf/bundle/search/index"
|
|
50
|
+
require "okf/bundle/search/scan"
|
|
43
51
|
require "okf/bundle/validator"
|
|
44
52
|
require "okf/bundle/validator/result"
|
|
45
53
|
require "okf/bundle/linter"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: okf
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rodrigo Serradura
|
|
@@ -37,6 +37,20 @@ dependencies:
|
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '1.4'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: minifts
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.0'
|
|
40
54
|
description: |
|
|
41
55
|
OKF (Open Knowledge Format) is portable knowledge: Markdown files with YAML
|
|
42
56
|
frontmatter that both humans and agents read from one source. This gem is the
|
|
@@ -66,6 +80,8 @@ files:
|
|
|
66
80
|
- lib/okf/bundle/linter/report.rb
|
|
67
81
|
- lib/okf/bundle/reader.rb
|
|
68
82
|
- lib/okf/bundle/search.rb
|
|
83
|
+
- lib/okf/bundle/search/index.rb
|
|
84
|
+
- lib/okf/bundle/search/scan.rb
|
|
69
85
|
- lib/okf/bundle/validator.rb
|
|
70
86
|
- lib/okf/bundle/validator/result.rb
|
|
71
87
|
- lib/okf/bundle/writer.rb
|
|
@@ -77,9 +93,9 @@ files:
|
|
|
77
93
|
- lib/okf/markdown/links.rb
|
|
78
94
|
- lib/okf/path.rb
|
|
79
95
|
- lib/okf/registry.rb
|
|
96
|
+
- lib/okf/render/graph.rb
|
|
97
|
+
- lib/okf/render/graph/template.html.erb
|
|
80
98
|
- lib/okf/server/app.rb
|
|
81
|
-
- lib/okf/server/graph.rb
|
|
82
|
-
- lib/okf/server/graph/template.html.erb
|
|
83
99
|
- lib/okf/server/hub.rb
|
|
84
100
|
- lib/okf/server/runner.rb
|
|
85
101
|
- lib/okf/skill.rb
|