okf 1.10.0 → 1.12.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 +294 -0
- data/README.md +310 -445
- data/lib/okf/bundle/folder.rb +24 -5
- data/lib/okf/bundle/linter.rb +1 -1
- data/lib/okf/bundle/search/index.rb +13 -3
- data/lib/okf/bundle/search.rb +93 -13
- data/lib/okf/bundle/skeleton.rb +241 -0
- data/lib/okf/bundle.rb +19 -14
- data/lib/okf/cli/catalog.rb +6 -6
- data/lib/okf/cli/command.rb +241 -14
- data/lib/okf/cli/dirs.rb +118 -0
- data/lib/okf/cli/files.rb +2 -2
- data/lib/okf/cli/graph.rb +115 -9
- data/lib/okf/cli/index.rb +67 -25
- data/lib/okf/cli/loose.rb +2 -2
- data/lib/okf/cli/registry.rb +152 -15
- data/lib/okf/cli/render.rb +3 -2
- data/lib/okf/cli/search.rb +33 -2
- data/lib/okf/cli/server.rb +16 -5
- data/lib/okf/cli/stats.rb +36 -11
- data/lib/okf/cli/tags.rb +29 -7
- data/lib/okf/cli/types.rb +1 -1
- data/lib/okf/cli.rb +10 -6
- data/lib/okf/registry.rb +351 -20
- data/lib/okf/render/graph/template.html.erb +504 -103
- data/lib/okf/render/graph.rb +27 -3
- data/lib/okf/server/app.rb +74 -3
- data/lib/okf/server/hub.rb +40 -30
- data/lib/okf/skill/SKILL.md +17 -10
- data/lib/okf/skill/playbooks/consume.md +3 -3
- data/lib/okf/skill/playbooks/maintain.md +4 -3
- data/lib/okf/skill/playbooks/menu.md +3 -2
- data/lib/okf/skill/playbooks/refine.md +30 -3
- data/lib/okf/skill/playbooks/search.md +7 -7
- data/lib/okf/skill/reference/cli.md +171 -32
- data/lib/okf/version.rb +1 -1
- data/lib/okf.rb +10 -0
- metadata +21 -8
data/lib/okf/render/graph.rb
CHANGED
|
@@ -45,8 +45,10 @@ module OKF
|
|
|
45
45
|
# `okf render`: the whole page as one self-contained file, the bundle baked
|
|
46
46
|
# in, so it hosts where no server answers a fetch. Takes any bundle handle
|
|
47
47
|
# (an OKF::Bundle::Folder) and returns the HTML string.
|
|
48
|
-
def self.static(folder, title: nil, link: nil, layout: "cose")
|
|
49
|
-
|
|
48
|
+
def self.static(folder, title: nil, link: nil, layout: "cose", map: false)
|
|
49
|
+
graph = folder.graph(minimal: true)
|
|
50
|
+
new(graph, title: title || folder.name, link: link, layout: layout, embed: payload(folder),
|
|
51
|
+
cuts: folder.skeleton.cuts_for(graph.edges), map: map).render
|
|
50
52
|
end
|
|
51
53
|
|
|
52
54
|
# What the baked page carries in place of the endpoints a live server would
|
|
@@ -73,10 +75,17 @@ module OKF
|
|
|
73
75
|
# default — injects an empty SIBLINGS, so the switcher never appears in a
|
|
74
76
|
# single bundle or a static file. +search_endpoint+ rides along with them:
|
|
75
77
|
# the hub's cross-bundle /search, which only a hub can answer.
|
|
78
|
+
# +cuts+ is one integer per edge, in @graph.edges order: the cut that edge
|
|
79
|
+
# survives (OKF::Bundle::Skeleton). It rides inline rather than being
|
|
80
|
+
# fetched because its only consumer needs it *before* the first layout
|
|
81
|
+
# runs, which is earlier than any request could answer. nil is allowed and
|
|
82
|
+
# simply turns the reduced first layout off.
|
|
76
83
|
def initialize(graph, title: nil, link: nil, layout: "cose", node_endpoint: "node", meta_endpoint: "node/meta", embed: nil,
|
|
77
84
|
siblings: nil, self_slug: nil, hub_path: nil, search_endpoint: nil,
|
|
78
|
-
manage_root: nil, manage_token: nil)
|
|
85
|
+
manage_root: nil, manage_token: nil, cuts: nil, map: false)
|
|
79
86
|
@graph = graph
|
|
87
|
+
@cuts = cuts
|
|
88
|
+
@map = map
|
|
80
89
|
@title = title
|
|
81
90
|
@link = link
|
|
82
91
|
@layout = layout
|
|
@@ -133,6 +142,21 @@ module OKF
|
|
|
133
142
|
json_for_script(@graph.edges)
|
|
134
143
|
end
|
|
135
144
|
|
|
145
|
+
# Index-aligned with EDGES: `EDGE_CUT[i]` is the cut `EDGES[i]` survives.
|
|
146
|
+
# An array of small integers rather than a keyed map, because keying it by
|
|
147
|
+
# "source target" would cost more bytes than the edge list it annotates.
|
|
148
|
+
def edge_cuts_json
|
|
149
|
+
json_for_script(@cuts)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Whether the page opens in the Map view — every concept, boxed by the
|
|
153
|
+
# directory it lives in, with the cross-links undrawn until one is
|
|
154
|
+
# selected. A boot state rather than a different page: the toggle is the
|
|
155
|
+
# same one the reader can press, so `--map` only decides where they start.
|
|
156
|
+
def map_json
|
|
157
|
+
json_for_script(@map ? true : false)
|
|
158
|
+
end
|
|
159
|
+
|
|
136
160
|
# { type => [id, …] } — the client builds an id→type map for node colour.
|
|
137
161
|
def types_json
|
|
138
162
|
json_for_script(@graph.type_index)
|
data/lib/okf/server/app.rb
CHANGED
|
@@ -21,7 +21,7 @@ module OKF
|
|
|
21
21
|
# GET /node/meta?id=… its description, as an escaped HTML fragment
|
|
22
22
|
# GET /catalog rich per-concept metadata for the catalog/files/stats
|
|
23
23
|
# views: { concepts: [ {id, title, type, description,
|
|
24
|
-
# tags, timestamp, status,
|
|
24
|
+
# tags, timestamp, status, top_dir, dir, links_*} ] } (JSON)
|
|
25
25
|
# GET /tags the tag index { tag => [id, …] } (JSON)
|
|
26
26
|
# GET /types the type index { type => [id, …] } (JSON)
|
|
27
27
|
# GET /index the §6 progressive-disclosure map for the Index panel:
|
|
@@ -30,18 +30,64 @@ module OKF
|
|
|
30
30
|
# GET /log the §7 history for the Log panel: { logs: [ {path,
|
|
31
31
|
# dir, content} ] } (JSON; content read live from disk,
|
|
32
32
|
# like a body — the log is the file that changes most)
|
|
33
|
+
# GET /search?q=… ranked concepts in this bundle, for the ⌘K palette:
|
|
34
|
+
# { query, total, truncated, results: [ …rows… ] } (JSON)
|
|
33
35
|
class App
|
|
36
|
+
# How many rows /search answers with. The palette shows a handful and the
|
|
37
|
+
# rest is scroll nobody reaches, but the count is reported alongside so a
|
|
38
|
+
# capped answer never reads as a complete one.
|
|
39
|
+
SEARCH_LIMIT = 50
|
|
40
|
+
|
|
41
|
+
# The engine /search runs on, named rather than inferred. `fuzzy: true`
|
|
42
|
+
# would route here on its own today — the index is the only registered
|
|
43
|
+
# engine that offers it — but that is correctness by coincidence, and an
|
|
44
|
+
# addon declaring :fuzzy would silently take the route.
|
|
45
|
+
#
|
|
46
|
+
# It is also the *right* engine here for a reason the CLI's default does
|
|
47
|
+
# not share: `okf search` is one-shot and cannot amortize an index build,
|
|
48
|
+
# while this is a long-lived server answering keystroke after keystroke.
|
|
49
|
+
# And the page's own MiniSearch is what minifts is a port of, so a palette
|
|
50
|
+
# hit and an in-page search rank alike instead of nearly alike.
|
|
51
|
+
SEARCH_ENGINE = :index
|
|
52
|
+
|
|
53
|
+
# The /search payload, defined once because two hosts answer it: this one
|
|
54
|
+
# bundle, or every bundle the hub hosts. The only difference is the corpus
|
|
55
|
+
# handed in — and a nil slug drops the `slug` key from a row, so a
|
|
56
|
+
# standalone server never answers as if it were a set.
|
|
57
|
+
#
|
|
58
|
+
# It takes a prepared corpus rather than the pairs, because that is what
|
|
59
|
+
# makes the index survive the request that built it.
|
|
60
|
+
def self.search_payload(corpus, query)
|
|
61
|
+
terms = query.to_s.split(/\s+/).reject(&:empty?)
|
|
62
|
+
rows = terms.empty? ? [] : OKF::Bundle::Search.with(corpus, terms, fuzzy: true, engine: SEARCH_ENGINE)
|
|
63
|
+
{
|
|
64
|
+
"query" => query.to_s.strip,
|
|
65
|
+
"total" => rows.length,
|
|
66
|
+
"truncated" => rows.length > SEARCH_LIMIT,
|
|
67
|
+
"results" => rows.first(SEARCH_LIMIT)
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
34
71
|
# +siblings+/+self_slug+/+hub_path+ are set only when this app is hosted under
|
|
35
72
|
# a hub (OKF::Server::Hub): the other bundles the in-page switcher offers, this
|
|
36
73
|
# bundle's own mount slug, the hub root, and the hub's cross-bundle search
|
|
37
74
|
# route. They stay nil for a standalone server and for `okf render`, so a
|
|
38
75
|
# static file never advertises a switcher or a search it cannot answer.
|
|
76
|
+
#
|
|
77
|
+
# +search_endpoint+ belongs to that group for the same reason, even though
|
|
78
|
+
# this app now answers /search itself: the page resolves it *relative to the
|
|
79
|
+
# URL the reader is on*, so only whoever mounted the app knows what to call
|
|
80
|
+
# it. `okf server` mounts at the root and passes "search"; a host doing
|
|
81
|
+
# `mount App.new(folder) => "/knowledge"` needs its own spelling, and a
|
|
82
|
+
# default would have pointed its palette at the host's root instead. The
|
|
83
|
+
# route answers either way — advertising it is the caller's call.
|
|
39
84
|
def initialize(folder, title: nil, link: nil, layout: "cose", siblings: nil, self_slug: nil, hub_path: nil,
|
|
40
|
-
search_endpoint: nil, manage_root: nil, manage_token: nil)
|
|
85
|
+
search_endpoint: nil, manage_root: nil, manage_token: nil, map: false)
|
|
41
86
|
@folder = folder
|
|
42
87
|
@title = title
|
|
43
88
|
@link = link
|
|
44
89
|
@layout = layout
|
|
90
|
+
@map = map
|
|
45
91
|
@siblings = siblings
|
|
46
92
|
@self_slug = self_slug
|
|
47
93
|
@hub_path = hub_path
|
|
@@ -50,6 +96,14 @@ module OKF
|
|
|
50
96
|
@manage_token = manage_token
|
|
51
97
|
end
|
|
52
98
|
|
|
99
|
+
# Build the search index now rather than on the first reader's keystroke.
|
|
100
|
+
# `okf server` calls this after the bundle is loaded, so the cost lands in
|
|
101
|
+
# boot — where it is expected and attributable — instead of in a request.
|
|
102
|
+
def warm_search
|
|
103
|
+
search_corpus
|
|
104
|
+
self
|
|
105
|
+
end
|
|
106
|
+
|
|
53
107
|
def call(env)
|
|
54
108
|
request = Rack::Request.new(env)
|
|
55
109
|
return not_found unless request.get?
|
|
@@ -63,6 +117,7 @@ module OKF
|
|
|
63
117
|
when "/types" then respond_json(graph.type_index)
|
|
64
118
|
when "/index" then respond_json(directory_index)
|
|
65
119
|
when "/log" then respond_json(logs)
|
|
120
|
+
when "/search" then respond_json(self.class.search_payload(search_corpus, request.params["q"]))
|
|
66
121
|
else not_found
|
|
67
122
|
end
|
|
68
123
|
end
|
|
@@ -102,11 +157,20 @@ module OKF
|
|
|
102
157
|
{ logs: @folder.log_entries }
|
|
103
158
|
end
|
|
104
159
|
|
|
160
|
+
# Only one thing is read off it here — #cuts_for, the per-link cut the page
|
|
161
|
+
# lays a large bundle out on. No endpoint serves it: the page needs those
|
|
162
|
+
# numbers before its first layout, which is earlier than a request could
|
|
163
|
+
# answer, so they ride inline with the graph instead. Built over the boot
|
|
164
|
+
# snapshot and held, like the graph itself.
|
|
165
|
+
def skeleton
|
|
166
|
+
@skeleton ||= @folder.skeleton
|
|
167
|
+
end
|
|
168
|
+
|
|
105
169
|
def page
|
|
106
170
|
@page ||= OKF::Render::Graph.new(
|
|
107
171
|
graph, title: @title || @folder.name, link: @link, layout: @layout,
|
|
108
172
|
siblings: @siblings, self_slug: @self_slug, hub_path: @hub_path, search_endpoint: @search_endpoint,
|
|
109
|
-
manage_root: @manage_root, manage_token: @manage_token
|
|
173
|
+
manage_root: @manage_root, manage_token: @manage_token, cuts: skeleton.cuts_for(graph.edges), map: @map
|
|
110
174
|
).render
|
|
111
175
|
end
|
|
112
176
|
|
|
@@ -147,6 +211,13 @@ module OKF
|
|
|
147
211
|
[ 200, { "content-type" => content_type }, [ body.to_s ] ]
|
|
148
212
|
end
|
|
149
213
|
|
|
214
|
+
# Built on the first search and held for the life of the app, the way the
|
|
215
|
+
# graph is. `okf server` warms it at boot so the first reader does not pay
|
|
216
|
+
# for the whole corpus.
|
|
217
|
+
def search_corpus
|
|
218
|
+
@search_corpus ||= OKF::Bundle::Search.prepare([ [ nil, @folder.bundle ] ], engine: SEARCH_ENGINE)
|
|
219
|
+
end
|
|
220
|
+
|
|
150
221
|
def respond_json(object)
|
|
151
222
|
respond("application/json; charset=utf-8", JSON.generate(object))
|
|
152
223
|
end
|
data/lib/okf/server/hub.rb
CHANGED
|
@@ -45,22 +45,9 @@ module OKF
|
|
|
45
45
|
# names" is how a router becomes an eval.
|
|
46
46
|
WRITES = %w[default rename remove add].freeze
|
|
47
47
|
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
SEARCH_LIMIT = 50
|
|
52
|
-
|
|
53
|
-
# The engine /search runs on, named rather than inferred. `fuzzy: true`
|
|
54
|
-
# would route here on its own today — the index is the only registered
|
|
55
|
-
# engine that offers it — but that is correctness by coincidence, and an
|
|
56
|
-
# addon declaring :fuzzy would silently take the route.
|
|
57
|
-
#
|
|
58
|
-
# It is also the *right* engine here for a reason the CLI's default does
|
|
59
|
-
# not share: `okf search` is one-shot and cannot amortize an index build,
|
|
60
|
-
# while this is a long-lived server answering keystroke after keystroke.
|
|
61
|
-
# And the page's own MiniSearch is what minifts is a port of, so a palette
|
|
62
|
-
# hit and an in-page search rank alike instead of nearly alike.
|
|
63
|
-
SEARCH_ENGINE = :index
|
|
48
|
+
# The /search cap and engine live on App, which now defines the payload both
|
|
49
|
+
# hosts answer with (App.search_payload). Two copies of a constant is two
|
|
50
|
+
# places to raise the cap and one of them silently losing.
|
|
64
51
|
|
|
65
52
|
# One hosted bundle: its +slug+ (unique mount key), the on-disk +folder+, and
|
|
66
53
|
# its display +title+.
|
|
@@ -171,12 +158,13 @@ module OKF
|
|
|
171
158
|
# is refused outright with no flag that says otherwise — `--bind 0.0.0.0`
|
|
172
159
|
# turns a personal tool into a public one, and the write surface does not
|
|
173
160
|
# follow it there at all.
|
|
174
|
-
def initialize(bundles, layout: "cose", registry: nil, writable: false)
|
|
161
|
+
def initialize(bundles, layout: "cose", registry: nil, writable: false, map: false)
|
|
175
162
|
@bundles = bundles
|
|
176
163
|
@default = bundles.first
|
|
177
164
|
@boot_registry = registry
|
|
178
165
|
@layout = layout
|
|
179
166
|
@writable = writable
|
|
167
|
+
@map = map
|
|
180
168
|
@apps = build_apps(layout)
|
|
181
169
|
end
|
|
182
170
|
|
|
@@ -205,6 +193,12 @@ module OKF
|
|
|
205
193
|
end
|
|
206
194
|
private_class_method :load_entry
|
|
207
195
|
|
|
196
|
+
# See App#warm_search — same reason, one corpus over every hosted bundle.
|
|
197
|
+
def warm_search
|
|
198
|
+
search_corpus
|
|
199
|
+
self
|
|
200
|
+
end
|
|
201
|
+
|
|
208
202
|
def call(env)
|
|
209
203
|
request = Rack::Request.new(env)
|
|
210
204
|
# Everything this class *emits* must carry the prefix a host mounted it
|
|
@@ -259,7 +253,7 @@ module OKF
|
|
|
259
253
|
end
|
|
260
254
|
|
|
261
255
|
def apply(verb, params)
|
|
262
|
-
registry =
|
|
256
|
+
registry = @boot_registry.reopen
|
|
263
257
|
message = mutate(verb, registry, params)
|
|
264
258
|
reload(registry)
|
|
265
259
|
json("ok" => true, "message" => message)
|
|
@@ -339,6 +333,9 @@ module OKF
|
|
|
339
333
|
@apps = build_apps(@layout)
|
|
340
334
|
@counts = nil
|
|
341
335
|
@health = nil
|
|
336
|
+
# The corpus is a snapshot of the set, so a write that changes the set
|
|
337
|
+
# invalidates it. Without this a removed bundle keeps answering /search.
|
|
338
|
+
@search_corpus = nil
|
|
342
339
|
end
|
|
343
340
|
|
|
344
341
|
# Same-origin *and* the token. Neither alone is enough: the token lives in
|
|
@@ -374,14 +371,14 @@ module OKF
|
|
|
374
371
|
# keystroke, and the box starts empty. `fuzzy: true` matches both the TUI
|
|
375
372
|
# and the page's own MiniSearch, so all three forgive the same typos.
|
|
376
373
|
def search(query)
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
)
|
|
374
|
+
json(App.search_payload(search_corpus, query))
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# One index over every hosted bundle — one corpus, so BM25 weighs a term
|
|
378
|
+
# against the whole set exactly as .across intends. Dropped whenever the
|
|
379
|
+
# bundle set changes, which is the only thing that can invalidate it.
|
|
380
|
+
def search_corpus
|
|
381
|
+
@search_corpus ||= OKF::Bundle::Search.prepare(pairs, engine: App::SEARCH_ENGINE)
|
|
385
382
|
end
|
|
386
383
|
|
|
387
384
|
# The /b/ manager's own rows, as JSON — what the graph page's Bundles panel
|
|
@@ -452,6 +449,7 @@ module OKF
|
|
|
452
449
|
bundle.folder,
|
|
453
450
|
title: bundle.title,
|
|
454
451
|
layout: layout,
|
|
452
|
+
map: @map,
|
|
455
453
|
siblings: siblings_of(bundle),
|
|
456
454
|
self_slug: bundle.slug,
|
|
457
455
|
hub_path: "/",
|
|
@@ -533,14 +531,26 @@ module OKF
|
|
|
533
531
|
count: nil, health: "missing", word: word, default: false }
|
|
534
532
|
end
|
|
535
533
|
|
|
534
|
+
# A bundle is addressed by its slug — `@orders` on the CLI, `/b/orders/`
|
|
535
|
+
# here — so the slug is its name and the folder is a fact about it. The row
|
|
536
|
+
# led with Folder.label instead, which put the address where the name goes;
|
|
537
|
+
# in a real registry that label is `…/.okf` on nearly every line, so the
|
|
538
|
+
# loudest column repeated the one word that tells no two bundles apart.
|
|
539
|
+
#
|
|
540
|
+
# The short label is gone rather than demoted: it only ever stood in for the
|
|
541
|
+
# path, and the path is right here on the row's second line.
|
|
542
|
+
#
|
|
543
|
+
# The name keeps its `@`, which the ref line used to carry: it is the exact
|
|
544
|
+
# spelling `okf lint @orders` takes, so the row teaches the CLI for free —
|
|
545
|
+
# and one element now does the whole job two were splitting.
|
|
536
546
|
def manager_row(base, row)
|
|
537
547
|
name = if row[:mount]
|
|
538
|
-
%(<a class="name" href="#{escape(base)}#{MOUNT}/#{escape(row[:mount])}/"
|
|
548
|
+
%(<a class="name" href="#{escape(base)}#{MOUNT}/#{escape(row[:mount])}/">@#{escape(row[:slug])}</a>)
|
|
539
549
|
else
|
|
540
|
-
%(<span class="name off"
|
|
550
|
+
%(<span class="name off">@#{escape(row[:slug])}</span>)
|
|
541
551
|
end
|
|
542
552
|
%(<li class="row" data-health="#{escape(row[:health])}">) +
|
|
543
|
-
%(<div class="who">#{name}<div class="ref"
|
|
553
|
+
%(<div class="who">#{name}<div class="ref">) +
|
|
544
554
|
# The row shows the tail; the tooltip is where the whole path stays
|
|
545
555
|
# reachable, since nothing else on the page carries it.
|
|
546
556
|
%(<span class="dir" title="#{escape(row[:dir])}"><bdi>#{escape(row[:dir])}</bdi></span></div></div>) +
|
|
@@ -585,7 +595,7 @@ module OKF
|
|
|
585
595
|
# `okf registry rename` in another terminal shows on the next refresh
|
|
586
596
|
# instead of waiting for a restart.
|
|
587
597
|
def registry
|
|
588
|
-
@boot_registry
|
|
598
|
+
@boot_registry&.reopen
|
|
589
599
|
end
|
|
590
600
|
|
|
591
601
|
# ok / warn / error, with the word that carries the same message for a
|
data/lib/okf/skill/SKILL.md
CHANGED
|
@@ -83,10 +83,10 @@ flags. The division of labour is the whole game:
|
|
|
83
83
|
|
|
84
84
|
- **Shell out — never eyeball —** anything a verb computes: conformance (§9), what
|
|
85
85
|
exists, what links where, where a term lives, what's stale, the map. Every read
|
|
86
|
-
verb takes `--json` and the list views filter by type/
|
|
86
|
+
verb takes `--json` and the list views filter by type/dir/tag, so ask the narrow
|
|
87
87
|
question instead of paging the bundle.
|
|
88
|
-
- **Skeleton first, bodies last.** `
|
|
89
|
-
|
|
88
|
+
- **Skeleton first, bodies last.** `dirs`, `search`, `graph --minimal`, and
|
|
89
|
+
`--fields` projections each answer for a fraction of a dump's bytes; full
|
|
90
90
|
bodies are the final step of a retrieval, never the first. <!-- rule:okf-skeleton-first -->
|
|
91
91
|
- **You judge — the CLI can't —** meaning: contradictions, semantic staleness
|
|
92
92
|
(parses fine, no longer true), whether a loose file is terminal-by-design, whether
|
|
@@ -102,12 +102,15 @@ shapes, the tag-curation views, the server's trust boundary.
|
|
|
102
102
|
|
|
103
103
|
## Orient before you touch anything
|
|
104
104
|
|
|
105
|
-
Picking up a bundle you don't already know — to consume or maintain —
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
105
|
+
Picking up a bundle you don't already know — to consume or maintain — start with
|
|
106
|
+
`okf dirs <dir|@slug>`: one row per *directory*, so it stays small on a bundle of
|
|
107
|
+
any size and it names the branches every other view narrows to. Then open the one
|
|
108
|
+
you want with `okf index <dir|@slug> --dir <branch>` (the §6 map: that directory's
|
|
109
|
+
index body, rollups, and listing), and read `log.md` (the §7 baseline of what
|
|
110
|
+
changed last) — all of it **before** greping or opening leaves. Reach for `index`
|
|
111
|
+
rather than grep for the one reason that outranks convenience: **grep cannot find
|
|
112
|
+
an index entry that is missing**, so enumeration drift is invisible to it — you
|
|
113
|
+
can't search for the word that should be there but isn't. <!-- rule:okf-orient-index -->
|
|
111
114
|
Per-verb steps are in the
|
|
112
115
|
playbooks (the Commands table below; no `okf` installed? read the root
|
|
113
116
|
`index.md` plus each area's `index.md`).
|
|
@@ -133,7 +136,11 @@ ambiguous, ask.
|
|
|
133
136
|
**Which target?** A leading `@` is a *registry ref*, not a path: `@slug` names a
|
|
134
137
|
bundle registered with `okf registry set`, bare `@` the default — route it
|
|
135
138
|
straight to `okf <verb> @slug` and skip the directory hunt (`okf search` spans
|
|
136
|
-
several: `@a @b`, or `@all`). A
|
|
139
|
+
several: `@a @b`, or `@all`). A `@slug` may instead name a **group** — a saved set
|
|
140
|
+
of bundles (`okf registry group backend @a @b`, members nest); it resolves like
|
|
141
|
+
any ref for the two set-taking verbs (`okf search @backend`, `okf server
|
|
142
|
+
@backend`) and every single-bundle verb refuses it with exit 2, the message
|
|
143
|
+
saying which two take a group. A plain path is used as given. Given no target and a
|
|
137
144
|
cwd that carries no bundle, `okf registry list` is the next move, not a hunt
|
|
138
145
|
across sibling directories. Producing a *new* bundle with no path? Default to
|
|
139
146
|
`.okf/` at the repo root, but first detect whether the project already keeps its
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Playbook: consume — use a bundle as context
|
|
2
2
|
|
|
3
|
-
1. **Orient first** (the [SKILL.md](../SKILL.md) reflex): `okf
|
|
4
|
-
the
|
|
5
|
-
and `log.md` gives recent history. Address a registered bundle by `@slug` (bare
|
|
3
|
+
1. **Orient first** (the [SKILL.md](../SKILL.md) reflex): `okf dirs <dir|@slug>`
|
|
4
|
+
gives the shape, `okf index <dir|@slug> --dir <branch>` opens the branch you
|
|
5
|
+
want — its index body, rollups and listing — and `log.md` gives recent history. Address a registered bundle by `@slug` (bare
|
|
6
6
|
`@` = the default); if the cwd carries no bundle, `okf registry list` finds one
|
|
7
7
|
instead of a directory hunt. Then follow links only into the concepts the
|
|
8
8
|
task needs. For a *pointed question* rather than broad context, switch to the
|
|
@@ -5,8 +5,9 @@ up. Restructuring the bundle itself — moving concepts, adding areas — is
|
|
|
5
5
|
[refine](refine.md), not maintain. The modelling craft behind steps 3 and 7
|
|
6
6
|
lives in [authoring.md](../reference/authoring.md).
|
|
7
7
|
|
|
8
|
-
1. **Orient before hunting.** Run `okf
|
|
9
|
-
|
|
8
|
+
1. **Orient before hunting.** Run `okf dirs <dir>` (the shape), then `okf index
|
|
9
|
+
<dir> --dir <branch>` on the branch the change touches (the §6 map: its index
|
|
10
|
+
body, rollups and listing), read `log.md` (the §7 baseline: what changed
|
|
10
11
|
last), and `okf stats <dir>` (size and shape) *before* you grep. It is the
|
|
11
12
|
cheapest context and it primes the hunt — and it is the only reliable way to
|
|
12
13
|
catch enumeration drift, because **grep cannot find an index entry that is
|
|
@@ -45,7 +46,7 @@ lives in [authoring.md](../reference/authoring.md).
|
|
|
45
46
|
defect.** Loose ≠ orphan: an index listing makes a file *reachable* (not an
|
|
46
47
|
orphan) but is not a graph edge, so an indexed file can still float here.
|
|
47
48
|
7. **Curate the tag vocabulary** when the pass
|
|
48
|
-
touched tags, or when `okf tags <dir>` shows a long tail of singletons. Run `okf tags <dir> --by
|
|
49
|
+
touched tags, or when `okf tags <dir>` shows a long tail of singletons. Run `okf tags <dir> --by dir` and
|
|
49
50
|
`--by type` — the grouped view is the analysis; read each group top-down:
|
|
50
51
|
- **twins** — two tags riding the exact same concepts (equal counts sort them
|
|
51
52
|
adjacent). Merge into one unless each genuinely names a different theme.
|
|
@@ -34,8 +34,9 @@ is the lede.
|
|
|
34
34
|
(`git status`), prefer **`maintain`**: that is exactly the drift it exists
|
|
35
35
|
to close.
|
|
36
36
|
- **clean, but the shape strains** — one area dwarfing the rest in
|
|
37
|
-
`okf stats`, tags spread thin across
|
|
38
|
-
whose inbound links are mostly foreign in `okf graph --hubs
|
|
37
|
+
`okf stats`, tags spread thin across dirs in `okf tags --by dir`, hubs
|
|
38
|
+
whose inbound links are mostly foreign in `okf graph --hubs`, a directory
|
|
39
|
+
with almost no internal traffic in `okf graph --traffic` → offer
|
|
39
40
|
**`refine`** (evidence-driven restructuring; it proposes before it
|
|
40
41
|
touches anything).
|
|
41
42
|
4. **Freshness is off by default.** If the bundle carries timestamps, note that a
|
|
@@ -19,18 +19,25 @@ relationship rides links and tags, never new directories. And cohesion outranks
|
|
|
19
19
|
balance — a move has semantic cost, so balance is a tiebreaker and a fatness
|
|
20
20
|
alarm, never the objective. <!-- rule:okf-cohesion-over-balance -->
|
|
21
21
|
|
|
22
|
-
1. **Orient.** `okf
|
|
23
|
-
`log.md` (how the bundle grew), `okf
|
|
22
|
+
1. **Orient.** `okf dirs <dir|@slug>` (directories, fan-out, depth — and
|
|
23
|
+
`subtree` says where the weight sits), `log.md` (how the bundle grew), `okf
|
|
24
|
+
stats` (totals). Additive growth
|
|
24
25
|
optimizes each pass locally, never the whole — that is the drift this
|
|
25
26
|
playbook corrects.
|
|
26
27
|
2. **Measure — the CLI is the evidence.** Baseline `validate` / `lint
|
|
27
28
|
--stale-after` / `loose` first: refine assumes a sound bundle, and hard
|
|
28
29
|
errors are [curate](curate.md)'s job. Then the two structural reads:
|
|
29
|
-
- `okf tags <dir> --by
|
|
30
|
+
- `okf tags <dir> --by dir` — each row carries `count/total`, so a tag's
|
|
30
31
|
**locality** reads directly: a tag wholly inside one area names a *domain*
|
|
31
32
|
(the directories are right); one spread across areas names a *concern*.
|
|
32
33
|
- `okf graph <dir> --hubs` — concepts ranked by inbound links, each with
|
|
33
34
|
the areas those links come from: the **origin test** for every hub.
|
|
35
|
+
- `okf graph <dir> --traffic` — the same question one level up. Every
|
|
36
|
+
directory's link traffic split three ways (internal / out / in) with the
|
|
37
|
+
**cohesion** ratio, and the weighted arcs between directories. `--hubs`
|
|
38
|
+
measures concepts; step 3 decides about *directories*, and this is the
|
|
39
|
+
only read at that grain. Rows lead with the lowest cohesion, so the
|
|
40
|
+
directories with a case to answer are at the top.
|
|
34
41
|
3. **Diagnose — you are the judgment.** The measurements are evidence, never
|
|
35
42
|
verdicts:
|
|
36
43
|
- **Concerns never become containers.** A directory built around a spread
|
|
@@ -42,6 +49,26 @@ alarm, never the objective. <!-- rule:okf-cohesion-over-balance -->
|
|
|
42
49
|
exclusive, and roughly comparable in size. And small is not merge-worthy
|
|
43
50
|
on its own — a two-concept area that is a genuinely distinct domain
|
|
44
51
|
stays. <!-- rule:okf-directory-prunes -->
|
|
52
|
+
- **Cohesion discriminates the low rows, which are the ones the view floats
|
|
53
|
+
to the top.** High cohesion sorts to the bottom and means the directory
|
|
54
|
+
holds together — the shape the tree exists to express, nothing to do. The
|
|
55
|
+
rows that need reading are the near-zero ones, and two shapes land there
|
|
56
|
+
together. Heavy *outbound* with almost nothing coming back is the finding: a
|
|
57
|
+
directory that points at the bundle rather than holding a part of it is
|
|
58
|
+
behaving like a projection, and this playbook's own frame says a projection
|
|
59
|
+
rides an `index.md` or a tag, never a directory — ask what would be lost if
|
|
60
|
+
its concepts moved to the areas they point at and its listing became a map.
|
|
61
|
+
Heavy *inbound* with nothing outbound is its benign twin, a **shared
|
|
62
|
+
vocabulary**: everyone cites it, it cites nobody, a reference area doing its
|
|
63
|
+
job, not a mis-homing. Same low number, opposite verdicts — the direction of
|
|
64
|
+
the traffic is which one you are looking at.
|
|
65
|
+
<!-- rule:okf-cohesion-reads-the-container -->
|
|
66
|
+
Two cautions, both of which will otherwise generate false findings. A
|
|
67
|
+
directory holding one or two concepts has too little traffic for a ratio
|
|
68
|
+
to mean anything — read the raw counts, not the percentage. And in a
|
|
69
|
+
design bundle the central `decisions/` will read low for exactly the
|
|
70
|
+
reason its concepts fail the hub origin test: that is centrality, and the
|
|
71
|
+
measurement is confirming the bundle works, not that it is broken.
|
|
45
72
|
- **The hub origin test.** Inbound majority from the hub's own area:
|
|
46
73
|
well-homed, leave it. A dominant *foreign* area: that area is the better
|
|
47
74
|
home. Foreign majority with *no* dominant area: a shared primitive — the
|
|
@@ -12,12 +12,12 @@ reads, and full bodies are read last, and only the winners.
|
|
|
12
12
|
the root `index.md` then each relevant area's `index.md` by hand. No bundle in
|
|
13
13
|
the cwd? `okf registry list` names the registered ones — address them by
|
|
14
14
|
`@slug`, don't hunt sibling directories.
|
|
15
|
-
2. **Ingest the map and decide where to look.** `okf
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
2. **Ingest the map and decide where to look.** `okf dirs <dir|@slug>` is the
|
|
16
|
+
skeleton: every directory with what lives under it. *You* do the semantic
|
|
17
|
+
matching here — the question names a meaning, the map names directories;
|
|
18
|
+
connect them by judgment, not string equality. When one looks right, `okf
|
|
19
|
+
index <dir|@slug> --dir <name>` buys its authored index body and listing
|
|
20
|
+
(titles + descriptions) for the price of one directory.
|
|
21
21
|
<!-- rule:okf-search-map-first -->
|
|
22
22
|
3. **Cut across with the finder when the question is lexical.** An exact
|
|
23
23
|
symbol, an error code, a column name, a phrase — things structure won't
|
|
@@ -52,7 +52,7 @@ reads, and full bodies are read last, and only the winners.
|
|
|
52
52
|
default when you can. <!-- rule:okf-search-fuzzy-is-a-switch -->
|
|
53
53
|
|
|
54
54
|
Scope any of them with what the map taught you:
|
|
55
|
-
`--
|
|
55
|
+
`--dir billing`, `--type Decision`, `--tag idempotency`, `--in body`.
|
|
56
56
|
Matches rank by where they hit, and the snippet often *is* the answer.
|
|
57
57
|
When the answer may live in another registered bundle, span them — leading
|
|
58
58
|
@slugs (`okf search @handbook @notes <terms>`) or `@all` for every registered
|