okf 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +151 -0
- data/README.md +71 -20
- data/lib/okf/bundle/folder.rb +9 -2
- data/lib/okf/bundle/graph.rb +5 -1
- data/lib/okf/bundle/linter.rb +6 -1
- data/lib/okf/bundle/reader.rb +21 -4
- data/lib/okf/cli.rb +833 -117
- data/lib/okf/registry.rb +370 -0
- data/lib/okf/server/app.rb +19 -4
- data/lib/okf/server/graph/template.html.erb +578 -46
- data/lib/okf/server/graph.rb +28 -2
- data/lib/okf/server/hub.rb +207 -0
- data/lib/okf/skill/SKILL.md +1 -1
- data/lib/okf/skill/playbooks/search.md +3 -0
- data/lib/okf/skill/reference/cli.md +126 -8
- data/lib/okf/version.rb +1 -1
- metadata +3 -1
data/lib/okf/server/graph.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rack/utils"
|
|
4
|
+
|
|
3
5
|
module OKF
|
|
4
6
|
module Server
|
|
5
7
|
# Renders an OKF::Bundle::Graph as the interactive graph page served by
|
|
@@ -40,7 +42,12 @@ module OKF
|
|
|
40
42
|
# fetches a concept's raw markdown and metadata fragment from — relative so
|
|
41
43
|
# the page works whether served at "/" or mounted under a Rails prefix.
|
|
42
44
|
# +embed+ is the render-mode payload (nil = server mode); see the class doc.
|
|
43
|
-
|
|
45
|
+
# +siblings+/+self_slug+/+hub_path+ carry the hub's bundle switcher into the
|
|
46
|
+
# page (server mode only). nil — the standalone-server and `okf render`
|
|
47
|
+
# default — injects an empty SIBLINGS, so the switcher never appears in a
|
|
48
|
+
# single bundle or a static file.
|
|
49
|
+
def initialize(graph, title: nil, link: nil, layout: "cose", node_endpoint: "node", meta_endpoint: "node/meta", embed: nil,
|
|
50
|
+
siblings: nil, self_slug: nil, hub_path: nil)
|
|
44
51
|
@graph = graph
|
|
45
52
|
@title = title
|
|
46
53
|
@link = link
|
|
@@ -48,6 +55,9 @@ module OKF
|
|
|
48
55
|
@node_endpoint = node_endpoint
|
|
49
56
|
@meta_endpoint = meta_endpoint
|
|
50
57
|
@embed = embed
|
|
58
|
+
@siblings = siblings
|
|
59
|
+
@self_slug = self_slug
|
|
60
|
+
@hub_path = hub_path
|
|
51
61
|
end
|
|
52
62
|
|
|
53
63
|
def render
|
|
@@ -102,6 +112,20 @@ module OKF
|
|
|
102
112
|
json_for_script(@embed)
|
|
103
113
|
end
|
|
104
114
|
|
|
115
|
+
# The hub switcher's data: the other bundles (empty when standalone/static),
|
|
116
|
+
# this bundle's slug, and the hub root — all </script>-escaped like the rest.
|
|
117
|
+
def siblings_json
|
|
118
|
+
json_for_script(@siblings || [])
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self_slug_json
|
|
122
|
+
json_for_script(@self_slug)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def hub_path_json
|
|
126
|
+
json_for_script(@hub_path)
|
|
127
|
+
end
|
|
128
|
+
|
|
105
129
|
# JSON-encode for safe embedding in an inline <script>: escaping every `<` to
|
|
106
130
|
# its JSON unicode escape neutralizes </script>, <!-- and <script in one
|
|
107
131
|
# stroke, and the result stays valid JSON *and* JavaScript.
|
|
@@ -120,8 +144,10 @@ module OKF
|
|
|
120
144
|
end
|
|
121
145
|
end
|
|
122
146
|
|
|
147
|
+
# Rack's, not a hand-rolled one — this output goes into attributes
|
|
148
|
+
# (`href="…"`), so the escape set is load-bearing rather than cosmetic.
|
|
123
149
|
def html_escape(str)
|
|
124
|
-
str.to_s
|
|
150
|
+
Rack::Utils.escape_html(str.to_s)
|
|
125
151
|
end
|
|
126
152
|
end
|
|
127
153
|
end
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rack"
|
|
4
|
+
|
|
5
|
+
require "okf/server/app"
|
|
6
|
+
|
|
7
|
+
module OKF
|
|
8
|
+
module Server
|
|
9
|
+
# Multiplexes N bundles behind one server. Each bundle is mounted at
|
|
10
|
+
# /b/<slug>/ and served by its own OKF::Server::App; `/` redirects to the
|
|
11
|
+
# default bundle (explicitly chosen, or the first), or shows an empty-state
|
|
12
|
+
# page when none are registered. The graph page is already mount-relative (its fetch endpoints
|
|
13
|
+
# are relative), so hosting under a prefix needs only a clean PATH_INFO strip
|
|
14
|
+
# here plus a trailing-slash redirect. Part of the shell — it is a Rack app.
|
|
15
|
+
#
|
|
16
|
+
# GET / 302 -> /b/<default>/ (empty-state page when no bundles)
|
|
17
|
+
# GET /b/ the bundle index — every hosted bundle, default marked
|
|
18
|
+
# GET /b/<slug> 301 -> /b/<slug>/ (query string preserved)
|
|
19
|
+
# GET /b/<slug>/... delegated to that bundle's App (the prefix stripped)
|
|
20
|
+
# GET (unknown slug) 404 as a page listing the hosted bundles — a stale
|
|
21
|
+
# bookmark after a rename gets a way home, not bare text
|
|
22
|
+
#
|
|
23
|
+
# +bundles+ is an ordered array of Hub::Bundle (slug, folder, title). Apps are
|
|
24
|
+
# built up front, each carrying the *other* bundles as siblings so the in-page
|
|
25
|
+
# switcher can jump between them; static `okf render` files get no siblings and
|
|
26
|
+
# so cannot switch.
|
|
27
|
+
class Hub
|
|
28
|
+
MOUNT = "/b"
|
|
29
|
+
|
|
30
|
+
# One hosted bundle: its +slug+ (unique mount key), the on-disk +folder+, and
|
|
31
|
+
# its display +title+.
|
|
32
|
+
Bundle = Struct.new(:slug, :folder, :title)
|
|
33
|
+
|
|
34
|
+
# Shared style for the hub's own pages (empty landing, /b/ index, 404) —
|
|
35
|
+
# self-contained and theme-aware, no external requests, in keeping with the
|
|
36
|
+
# graph page's own no-CDN-at-rest rule.
|
|
37
|
+
STYLE = <<~CSS
|
|
38
|
+
body{margin:0;min-height:100vh;display:grid;place-items:center;background:#f4f5f7;color:#1f2328;font:15px/1.5 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}
|
|
39
|
+
main{max-width:34rem;width:calc(100% - 4rem);padding:2rem}h1{font-size:1.3rem;margin:0 0 .5rem}
|
|
40
|
+
code{background:#e6e8eb;padding:.15rem .4rem;border-radius:.35rem}
|
|
41
|
+
ul.bundles{list-style:none;margin:1rem 0 0;padding:0}
|
|
42
|
+
ul.bundles li{padding:.45rem 0;border-top:1px solid #e6e8eb;display:flex;justify-content:space-between;gap:1rem;align-items:baseline}
|
|
43
|
+
ul.bundles a{color:inherit;font-weight:600;text-decoration:none}ul.bundles a:hover{text-decoration:underline}
|
|
44
|
+
.meta{color:#63697a;font-size:.85rem;white-space:nowrap}
|
|
45
|
+
.def{margin-left:.5rem;padding:.05rem .45rem;border-radius:99px;background:#e6e8eb;font-size:.75rem}
|
|
46
|
+
@media(prefers-color-scheme:dark){body{background:#111318;color:#eceef1}code,.def{background:#232833}
|
|
47
|
+
ul.bundles li{border-color:#2a2e36}.meta{color:#9aa0aa}}
|
|
48
|
+
CSS
|
|
49
|
+
|
|
50
|
+
# The hosted bundles in mount order, and the one `/` redirects to — so a
|
|
51
|
+
# caller printing the mount table asks the hub instead of re-deriving the
|
|
52
|
+
# rule and drifting from it.
|
|
53
|
+
attr_reader :bundles, :default
|
|
54
|
+
|
|
55
|
+
# The first bundle is the one `/` redirects to — the registry hands them over
|
|
56
|
+
# in its own order, where first *is* the default (`okf registry default`
|
|
57
|
+
# moves an entry to the front), and an ephemeral run takes the dirs as typed.
|
|
58
|
+
def initialize(bundles, layout: "cose")
|
|
59
|
+
@bundles = bundles
|
|
60
|
+
@default = bundles.first
|
|
61
|
+
@apps = build_apps(layout)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def call(env)
|
|
65
|
+
request = Rack::Request.new(env)
|
|
66
|
+
return not_found unless request.get?
|
|
67
|
+
|
|
68
|
+
path = request.path_info
|
|
69
|
+
query = request.query_string.to_s
|
|
70
|
+
# Everything this class *emits* must carry the prefix a host mounted it
|
|
71
|
+
# under; PATH_INFO is already relative to it.
|
|
72
|
+
base = env["SCRIPT_NAME"].to_s
|
|
73
|
+
return landing(base, query) if [ "", "/" ].include?(path)
|
|
74
|
+
return html(200, index_page(base)) if [ MOUNT, "#{MOUNT}/" ].include?(path)
|
|
75
|
+
|
|
76
|
+
slug, rest = split(path)
|
|
77
|
+
app = slug && @apps[slug]
|
|
78
|
+
return html(404, missing_page(base, path)) unless app
|
|
79
|
+
return redirect("#{base}#{MOUNT}/#{slug}/", 301, query) if rest.empty?
|
|
80
|
+
|
|
81
|
+
app.call(mounted(env, slug, rest))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# Split "/b/<slug>/rest" into [ "<slug>", "/rest" ] (rest "" for just
|
|
87
|
+
# "/b/<slug>"). A path outside the mount prefix, or an empty slug, is [ nil, nil ].
|
|
88
|
+
def split(path)
|
|
89
|
+
prefix = "#{MOUNT}/"
|
|
90
|
+
return [ nil, nil ] unless path.start_with?(prefix)
|
|
91
|
+
|
|
92
|
+
slug, slash, rest = path[prefix.length..-1].partition("/")
|
|
93
|
+
return [ nil, nil ] if slug.empty?
|
|
94
|
+
|
|
95
|
+
[ slug, slash + rest ]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Concept counts for the listing pages, computed once. Bundle#graph is not
|
|
99
|
+
# memoized (App memoizes its own), and /b/ and every stray 404 render this
|
|
100
|
+
# list — without the memo a 404 flood reparses every hosted bundle.
|
|
101
|
+
def counts
|
|
102
|
+
@counts ||= @bundles.each_with_object({}) do |bundle, memo|
|
|
103
|
+
memo[bundle.slug] = bundle.folder.graph(minimal: true).nodes.size
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# A copy of env aimed at the bundle's App: the /b/<slug> prefix moves from
|
|
108
|
+
# PATH_INFO to SCRIPT_NAME. The App ignores SCRIPT_NAME (its endpoints are
|
|
109
|
+
# relative), but keeping the split correct leaves the env well-formed.
|
|
110
|
+
def mounted(env, slug, rest)
|
|
111
|
+
env.merge(
|
|
112
|
+
"SCRIPT_NAME" => "#{env["SCRIPT_NAME"]}#{MOUNT}/#{slug}",
|
|
113
|
+
"PATH_INFO" => rest
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def build_apps(layout)
|
|
118
|
+
@bundles.each_with_object({}) do |bundle, apps|
|
|
119
|
+
apps[bundle.slug] = App.new(
|
|
120
|
+
bundle.folder,
|
|
121
|
+
title: bundle.title,
|
|
122
|
+
layout: layout,
|
|
123
|
+
siblings: siblings_of(bundle),
|
|
124
|
+
self_slug: bundle.slug,
|
|
125
|
+
hub_path: "/"
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Every other bundle, as { slug:, title:, path:, default: } — what the
|
|
131
|
+
# switcher lists; default marks the bundle `/` opens. The path is
|
|
132
|
+
# *relative* because these are baked into each App at boot, before any
|
|
133
|
+
# request names a SCRIPT_NAME: every page lives at <prefix>/b/<slug>/, so
|
|
134
|
+
# "../<other>/" reaches its sibling under any mount and needs no prefix.
|
|
135
|
+
def siblings_of(bundle)
|
|
136
|
+
@bundles.reject { |other| other.slug == bundle.slug }
|
|
137
|
+
.map { |other| { slug: other.slug, title: other.title, path: "../#{other.slug}/", default: other.equal?(@default) } }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def landing(base, query = "")
|
|
141
|
+
return redirect("#{base}#{MOUNT}/#{@default.slug}/", 302, query) if @default
|
|
142
|
+
|
|
143
|
+
html(200, page("OKF · no bundles", <<~BODY))
|
|
144
|
+
<h1>No bundles registered</h1>
|
|
145
|
+
<p>Register one with <code>okf registry set <dir></code>, then restart <code>okf server</code>.</p>
|
|
146
|
+
BODY
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The /b/ index — every hosted bundle with its mount link, concept count,
|
|
150
|
+
# and the default marked. The browser counterpart of `okf registry`.
|
|
151
|
+
def index_page(base)
|
|
152
|
+
page("OKF · bundles", "<h1>Bundles</h1>#{bundle_list(base)}")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# The 404 for a slug the hub does not host: name what was asked for, then
|
|
156
|
+
# list what exists — a stale bookmark after a rename gets a way home.
|
|
157
|
+
def missing_page(base, path)
|
|
158
|
+
body = "<h1>No bundle here</h1><p><code>#{escape(path)}</code> does not match a hosted bundle.</p>"
|
|
159
|
+
body += bundle_list(base) unless @bundles.empty?
|
|
160
|
+
page("OKF · not found", body)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def bundle_list(base)
|
|
164
|
+
rows = @bundles.map do |bundle|
|
|
165
|
+
count = counts[bundle.slug]
|
|
166
|
+
badge = bundle.equal?(@default) ? %(<span class="def">default</span>) : ""
|
|
167
|
+
%(<li><a href="#{escape(base)}#{MOUNT}/#{escape(bundle.slug)}/">#{escape(bundle.title)}</a>) +
|
|
168
|
+
%(<span class="meta">#{escape(bundle.slug)} · #{count} concepts#{badge}</span></li>)
|
|
169
|
+
end
|
|
170
|
+
%(<ul class="bundles">#{rows.join}</ul>)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def page(title, body)
|
|
174
|
+
<<~HTML
|
|
175
|
+
<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
176
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
177
|
+
<title>#{escape(title)}</title>
|
|
178
|
+
<style>#{STYLE}</style>
|
|
179
|
+
</head><body><main>#{body}</main></body></html>
|
|
180
|
+
HTML
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def html(status, body)
|
|
184
|
+
[ status, { "content-type" => "text/html; charset=utf-8" }, [ body ] ]
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Keep the query string across redirects — `/b/notes?view=files` must land
|
|
188
|
+
# on the Files view, not reset to the default graph.
|
|
189
|
+
def redirect(location, status, query = "")
|
|
190
|
+
location += "?#{query}" unless query.empty?
|
|
191
|
+
[ status, { "location" => location, "content-type" => "text/plain; charset=utf-8" }, [ "" ] ]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def not_found
|
|
195
|
+
OKF::Server::App.not_found
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Rack's, not a fourth hand-rolled one: the server layer had three, each
|
|
199
|
+
# escaping a different set — App's left `"` alone, which is safe only
|
|
200
|
+
# while nothing interpolates it into an attribute. Rack::Utils covers
|
|
201
|
+
# & " ' < > and ships with the dependency we already have.
|
|
202
|
+
def escape(str)
|
|
203
|
+
Rack::Utils.escape_html(str.to_s)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
data/lib/okf/skill/SKILL.md
CHANGED
|
@@ -150,7 +150,7 @@ Read the referenced playbook before executing — it *is* the procedure.
|
|
|
150
150
|
| `consume` | Use | use the bundle as context for a task | [playbooks/consume.md](playbooks/consume.md) |
|
|
151
151
|
| `curate` | Curate | structural upkeep as it stands: validate + lint + loose | [playbooks/curate.md](playbooks/curate.md) |
|
|
152
152
|
| `doctor` | Setup | install and verify the CLI, then doctor the bundle | [playbooks/doctor.md](playbooks/doctor.md) |
|
|
153
|
-
| `<okf-cli-verb>` | Read | validate, lint, loose, index, catalog, files, tags, types, stats, graph, server, render, skill | `okf <verb> --help` + [reference/cli.md](reference/cli.md) |
|
|
153
|
+
| `<okf-cli-verb>` | Read | validate, lint, loose, index, catalog, files, tags, types, stats, graph, server, render, registry, skill | `okf <verb> --help` + [reference/cli.md](reference/cli.md) |
|
|
154
154
|
|
|
155
155
|
Two boundaries worth keeping sharp: `curate` is structural upkeep only — when
|
|
156
156
|
the *content* no longer matches reality, that is `maintain` — and `doctor` is
|
|
@@ -21,6 +21,9 @@ reads, and full bodies are read last, and only the winners.
|
|
|
21
21
|
for patterns like `err_[a-z]+_409`). Scope it with what the map taught you:
|
|
22
22
|
`--area billing`, `--type Decision`, `--tag idempotency`, `--in body`.
|
|
23
23
|
Matches rank by where they hit, and the snippet often *is* the answer.
|
|
24
|
+
When the answer may live in another registered bundle, span them — leading
|
|
25
|
+
@slugs (`okf search @handbook @notes <terms>`) or `@all` for every registered
|
|
26
|
+
one — and read the per-row bundle slug before following an id home.
|
|
24
27
|
4. **Read only the winners.** A match row's `id` is its file: `<dir>/<id>.md`.
|
|
25
28
|
Read that file — not its folder, never the whole tree. Follow its links (§5)
|
|
26
29
|
one hop at a time; check `log.md` when freshness matters.
|
|
@@ -38,9 +38,36 @@ difference between a few hundred bytes and hundreds of KB, since the per-item ro
|
|
|
38
38
|
(`listing`) dominate at scale. `okf index --no-body` is shorthand for dropping just
|
|
39
39
|
`body`.
|
|
40
40
|
|
|
41
|
+
**Every output names its bundle.** Two keys, one meaning each: `bundle` is
|
|
42
|
+
always a directory, `slug` always a registry slug. Name a bundle by `@slug` and
|
|
43
|
+
the answer comes back in that identity — `OKF lint — @handbook (/path/to/one)`,
|
|
44
|
+
and `{ "bundle": "/path/to/one", "slug": "handbook", … }` — so an agent holding
|
|
45
|
+
several bundles never has to remember which invocation produced which output.
|
|
46
|
+
A bundle named by path carries no `slug`: it may not have one, and inventing a
|
|
47
|
+
name it was never given would imply a registration that does not exist.
|
|
48
|
+
|
|
49
|
+
**@slug — point any verb at a registered bundle.** Wherever a `<dir>` goes,
|
|
50
|
+
`@slug` names a bundle registered via `okf registry set`, and bare `@` the
|
|
51
|
+
registry's default. They resolve through `$OKF_HOME` (default `~/.okf`) — the
|
|
52
|
+
single lever on which registry *any* verb reads, and it names exactly one, with
|
|
53
|
+
no fallback behind it. The slug is normalized as registration
|
|
54
|
+
normalized it — `@One` finds the bundle from dir `One` — but never to a
|
|
55
|
+
placeholder: `@***` names nothing, not a bundle. An unknown slug, a
|
|
56
|
+
registered-but-gone directory, or a malformed registry file is a usage error
|
|
57
|
+
(exit 2) whose message names the registry file consulted and the next move —
|
|
58
|
+
an explicit ask fails hard, never silently skipped. So `okf lint @handbook`
|
|
59
|
+
or `okf index @` work from any directory, no path recall needed.
|
|
60
|
+
|
|
41
61
|
**Exit codes:** `0` success · `1` non-conformant bundle (or a `lint --fail-on`
|
|
42
62
|
threshold crossed) · `2` usage error. `graph`, `server`, and `render` are best-effort
|
|
43
|
-
(§9): a file
|
|
63
|
+
(§9): a file the reader cannot use — frontmatter that will not parse, or a file it
|
|
64
|
+
cannot open at all — is skipped and noted on stderr, never fatal. The note counts;
|
|
65
|
+
`validate` names each file and why.
|
|
66
|
+
|
|
67
|
+
**One bundle per verb, except two.** Only `search` merges several bundles and only
|
|
68
|
+
`server` mounts them; hand a second bundle to any other verb — two dirs, two refs,
|
|
69
|
+
or a mix — and it is a usage error (exit 2), never a silent answer about the first.
|
|
70
|
+
To ask the same question of several bundles, ask `search`, or ask each in turn.
|
|
44
71
|
|
|
45
72
|
## validate — the hard gate (§9)
|
|
46
73
|
|
|
@@ -119,16 +146,55 @@ expressions with `--regexp`/`-e` (an invalid pattern is a usage error, exit 2).
|
|
|
119
146
|
body); the shared `--type/--area/--tag` filters narrow the candidates *first*,
|
|
120
147
|
so a search scoped by what `index` taught you stays surgical.
|
|
121
148
|
|
|
149
|
+
**Search spans bundles.** Leading @refs pick several registered bundles
|
|
150
|
+
(`okf search @handbook @notes auth`); **`@all`** is the ref that means every one.
|
|
151
|
+
The per-bundle rankings merge — scores are absolute term weights, so they
|
|
152
|
+
compare across bundles — and each row carries its bundle's slug. This is the
|
|
153
|
+
cross-bundle retrieval the in-page search does not have: one question, every
|
|
154
|
+
bundle you keep. <!-- rule:okf-search-all -->
|
|
155
|
+
|
|
156
|
+
`@all` is a ref, not a flag, which is what keeps the grammar single: slot 1 is
|
|
157
|
+
always a bundle identity, so a directory there is a directory and nothing can
|
|
158
|
+
flip it into a term. Being a ref, it is normalized like one — `@ALL` and `@All`
|
|
159
|
+
name every bundle just as `@One` names the bundle registered from dir `One`. It composes accordingly — `@all @docs` expands and dedupes
|
|
160
|
+
(all ⊇ docs), needing no diagnostic. **Asking for everything tolerates gaps;
|
|
161
|
+
naming one bundle demands it**: `@all` skips a bundle whose directory has
|
|
162
|
+
vanished with a note on stderr, while `@docs` fails hard. `@all` is only
|
|
163
|
+
`search`'s: every other verb answers about one bundle, so it refuses `@all` by
|
|
164
|
+
name rather than letting the answer depend on how many bundles you happen to
|
|
165
|
+
have registered. `all` is reserved as a slug — a directory named `all/` registers
|
|
166
|
+
as `all-2`, `--as all` is refused, and an `all` row already in the registry file
|
|
167
|
+
(hand-typed, or written before the name was reserved) is read as `all-2` rather
|
|
168
|
+
than taken as grounds to reject the file — so `@all` is never ambiguous, and the
|
|
169
|
+
reservation never strands a registry it inherited. **The read normalizes every
|
|
170
|
+
slug** the same way registration would, so a hand-typed `"slug": "My Docs"` lists
|
|
171
|
+
and resolves as `my-docs`; an entry the listing shows is always an entry `@slug`,
|
|
172
|
+
`rename`, and `default` can name.
|
|
173
|
+
|
|
174
|
+
`--fields` projects the shape the mode actually emits: `slug` is available in
|
|
175
|
+
registry mode, and a usage error naming the real fields on a path-named search,
|
|
176
|
+
which has no slug to give. Two sharp edges: every *leading* @-arg is taken as a ref, so a literal @-term
|
|
177
|
+
(`@babel/core`, a Ruby `@ivar`) needs a non-@ term before it or `-e '\@term'` —
|
|
178
|
+
the CLI notes both traps on stderr — and any ref, even one, switches the JSON
|
|
179
|
+
envelope (next paragraph).
|
|
180
|
+
|
|
122
181
|
Rows rank by **where** they hit — title 5, id 4, tags 3, type/description 2,
|
|
123
182
|
body 1, summed over matched fields — and carry one bounded context snippet from
|
|
124
183
|
the strongest match that needs context (description or body). Deliberately not
|
|
125
184
|
fuzzy: the consuming agent is the fuzzy layer — when terms miss, learn the
|
|
126
185
|
bundle's vocabulary from `tags`/`types` and re-ask in its own words, rather
|
|
127
186
|
than hammering synonyms. Advisory read: **exit 0 even with zero matches**.
|
|
128
|
-
JSON: `{ bundle, query, count, matches: [{ id, title, type,
|
|
129
|
-
matched, score, snippet }] }
|
|
130
|
-
|
|
131
|
-
|
|
187
|
+
JSON, plain-dir mode: `{ bundle, query, count, matches: [{ id, title, type,
|
|
188
|
+
area, tags, matched, score, snippet }] }`. Registry mode — any leading @ref,
|
|
189
|
+
`@all` among them — swaps the envelope: `{ bundles: [{ slug, dir }, …],
|
|
190
|
+
query, count, matches: [{ slug, id, … }] }`; a parser must branch on which form
|
|
191
|
+
it called. The head maps each slug to its dir once, so a row resolves to
|
|
192
|
+
`<dir>/<id>.md` without a second lookup and without repeating a path per row.
|
|
193
|
+
Both are projectable with `--fields/--except`, and projection is literal — when
|
|
194
|
+
merging bundles, put `slug` in your `--fields` list or the row label drops and
|
|
195
|
+
same-id concepts from different bundles become indistinguishable. The retrieval procedure that puts this verb in sequence —
|
|
196
|
+
map first, finder second, bodies last — is the
|
|
197
|
+
[search playbook](../playbooks/search.md).
|
|
132
198
|
|
|
133
199
|
## index — the progressive-disclosure map (§6)
|
|
134
200
|
|
|
@@ -216,6 +282,56 @@ just-appended entry shows without a restart. `?view=index` jumps straight to
|
|
|
216
282
|
the Indexes tab. It is a Rack app, so the same server can be mounted in a
|
|
217
283
|
host app (e.g. Rails).
|
|
218
284
|
|
|
285
|
+
**Hosting many bundles (the hub).** `okf server` takes zero or more dirs.
|
|
286
|
+
One dir is the classic single bundle at `/`. Two or more mounts each under
|
|
287
|
+
`/b/<slug>/` behind a hub, `/` redirects to the default, and `/b/` is a
|
|
288
|
+
self-contained **bundle index** (every hosted bundle, concept counts, default
|
|
289
|
+
marked — the browser counterpart of `okf registry`). An unknown slug 404s as a
|
|
290
|
+
page listing the hosted bundles, so a stale bookmark after a rename gets a way
|
|
291
|
+
home. With **no** dir it serves the *persistent registry*, a plain JSON file
|
|
292
|
+
under `$OKF_HOME` (default `~/.okf`), managed by the
|
|
293
|
+
`okf registry` umbrella — like git's `remote` family, and split by what each
|
|
294
|
+
verb keys on. **Entry verbs** take a path: `okf registry set <dir>` adds it
|
|
295
|
+
(slug from the basename, or `--as`, which errors on a collision; `--default`
|
|
296
|
+
puts it first), and because the entry is keyed by path, `set` on an
|
|
297
|
+
already-registered dir updates it in place — refreshing its title, and renaming
|
|
298
|
+
it when `--as` is given. `okf registry del <dir|@slug>` removes one — by name, so an entry whose
|
|
299
|
+
directory is already gone still deletes. Slug *or* dir, never both readings at
|
|
300
|
+
once: an argument with a `/` in it names a location and only a location, so
|
|
301
|
+
`del ./notes` refuses when no entry points there rather than stripping to the
|
|
302
|
+
slug `notes` and deleting a bundle somewhere else entirely.
|
|
303
|
+
<!-- rule:okf-registry-del-path-or-slug -->
|
|
304
|
+
**Slug verbs** take the name — bare, or as an `@slug`: `okf registry default <@slug>`
|
|
305
|
+
chooses which bundle `/` opens **by moving that entry to the front**, and
|
|
306
|
+
`okf registry rename <@slug> <new>` renames a slug (mount path and switcher
|
|
307
|
+
name) — `<new>` is a name being minted, so it is never a ref. The registry is ordered and **the first entry still on disk is the
|
|
308
|
+
default** — that is the whole rule, so the first bundle you register is the
|
|
309
|
+
default until you move another one, a rename keeps its position, and a `del`
|
|
310
|
+
promotes whatever is next. A vanished directory is stepped over (the server
|
|
311
|
+
cannot open one, so starring it would name a bundle `/` never serves), and
|
|
312
|
+
`registry default @slug` refuses one outright — the same refusal `registry set`
|
|
313
|
+
gives a directory that is not there. The file is hand-editable and reorders
|
|
314
|
+
visibly, which is the point: there is no stored slug that can dangle.
|
|
315
|
+
<!-- rule:okf-registry-default-position -->
|
|
316
|
+
`okf registry list` (or a bare
|
|
317
|
+
`okf registry`) stars the default and flags vanished dirs `(missing)` — the
|
|
318
|
+
server skips those with a note; `--json` answers
|
|
319
|
+
`{ registry: <file>, count, bundles: [{ slug, title, dir, mount, default,
|
|
320
|
+
missing }] }`, naming the file it read so a `$OKF_HOME` mismatch is visible. The hub roster is a
|
|
321
|
+
**boot-time snapshot**: restart `okf server` after registry changes. Behind a
|
|
322
|
+
hub the page gains a **bundle switcher** (⌘/Ctrl-K, or the rail button with its
|
|
323
|
+
bundle-count badge): the current bundle is pinned, the default chipped; ⏎
|
|
324
|
+
opens, ⌘/Ctrl-⏎ opens a new tab, and the current view carries over. Switching
|
|
325
|
+
is a server-only affordance — a static `render` file has no siblings and shows
|
|
326
|
+
none.
|
|
327
|
+
|
|
328
|
+
**Bundle-less run.** Register bundles once, then `okf server` (no dir) hosts
|
|
329
|
+
them all with the registry's first entry still on disk at `/` — the way to keep
|
|
330
|
+
several bundles a keystroke apart without re-passing paths.
|
|
331
|
+
`okf server @a @b` serves a registry subset, each mounted under its registered
|
|
332
|
+
slug — but as with any dirs-given run, the *first argument* lands at `/`; the
|
|
333
|
+
registry's own order applies only to the bundle-less run.
|
|
334
|
+
|
|
219
335
|
**Trust boundary:** the page renders each fetched markdown body through
|
|
220
336
|
DOMPurify and escapes everything it inlines (every `<` in the graph data is
|
|
221
337
|
escaped, so it cannot break out of its `<script>`), but it still loads its
|
|
@@ -242,10 +358,12 @@ you trust.
|
|
|
242
358
|
|
|
243
359
|
## graph — the raw structure
|
|
244
360
|
|
|
245
|
-
Prints the node/edge graph. `--json` emits a machine-readable dump
|
|
361
|
+
Prints the node/edge graph. `--json` emits a machine-readable dump — the
|
|
362
|
+
`bundle`/`slug` head every view carries, then `nodes` (with
|
|
246
363
|
`id`/`type`/`title`/`description`/`tags` **and, by default, every `body`** — the
|
|
247
|
-
part that dominates the bytes on a real bundle
|
|
248
|
-
other analysis.
|
|
364
|
+
part that dominates the bytes on a real bundle) plus `edges` — you can pipe into
|
|
365
|
+
other analysis. A concept with a missing *or blank* `type` indexes under
|
|
366
|
+
`Untyped`: §9.2 rejects both identically, so both land in one bucket. To *plan* a traversal, structure is all you need: `--no-body`
|
|
249
367
|
drops each node's body, and `--minimal` ships only `id`/`title` plus the type/tag
|
|
250
368
|
indexes — the lean shape the `server` page boots from. Reach for the full dump
|
|
251
369
|
only when the task truly consumes every body; for one question, the
|
data/lib/okf/version.rb
CHANGED
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.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rodrigo Serradura
|
|
@@ -76,9 +76,11 @@ files:
|
|
|
76
76
|
- lib/okf/markdown/frontmatter.rb
|
|
77
77
|
- lib/okf/markdown/links.rb
|
|
78
78
|
- lib/okf/path.rb
|
|
79
|
+
- lib/okf/registry.rb
|
|
79
80
|
- lib/okf/server/app.rb
|
|
80
81
|
- lib/okf/server/graph.rb
|
|
81
82
|
- lib/okf/server/graph/template.html.erb
|
|
83
|
+
- lib/okf/server/hub.rb
|
|
82
84
|
- lib/okf/server/runner.rb
|
|
83
85
|
- lib/okf/skill.rb
|
|
84
86
|
- lib/okf/skill/SKILL.md
|