okf 1.10.0 → 1.11.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 +203 -0
- data/README.md +238 -413
- data/lib/okf/bundle/folder.rb +20 -5
- data/lib/okf/bundle/linter.rb +1 -1
- data/lib/okf/bundle/search/index.rb +13 -3
- data/lib/okf/bundle/search.rb +91 -11
- data/lib/okf/bundle.rb +1 -1
- data/lib/okf/cli/catalog.rb +1 -1
- data/lib/okf/cli/command.rb +169 -7
- data/lib/okf/cli/dirs.rb +118 -0
- data/lib/okf/cli/files.rb +2 -2
- data/lib/okf/cli/index.rb +67 -25
- data/lib/okf/cli/loose.rb +2 -2
- data/lib/okf/cli/search.rb +1 -1
- data/lib/okf/cli/server.rb +8 -1
- data/lib/okf/cli/stats.rb +33 -8
- data/lib/okf/cli/tags.rb +29 -7
- data/lib/okf/cli/types.rb +1 -1
- data/lib/okf/cli.rb +9 -5
- data/lib/okf/render/graph/template.html.erb +293 -96
- data/lib/okf/server/app.rb +61 -0
- data/lib/okf/server/hub.rb +35 -27
- data/lib/okf/skill/SKILL.md +12 -9
- 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 +1 -1
- data/lib/okf/skill/playbooks/refine.md +4 -3
- data/lib/okf/skill/playbooks/search.md +7 -7
- data/lib/okf/skill/reference/cli.md +100 -18
- data/lib/okf/version.rb +1 -1
- data/lib/okf.rb +9 -0
- metadata +20 -8
data/lib/okf/server/app.rb
CHANGED
|
@@ -30,12 +30,57 @@ 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
85
|
search_endpoint: nil, manage_root: nil, manage_token: nil)
|
|
41
86
|
@folder = folder
|
|
@@ -50,6 +95,14 @@ module OKF
|
|
|
50
95
|
@manage_token = manage_token
|
|
51
96
|
end
|
|
52
97
|
|
|
98
|
+
# Build the search index now rather than on the first reader's keystroke.
|
|
99
|
+
# `okf server` calls this after the bundle is loaded, so the cost lands in
|
|
100
|
+
# boot — where it is expected and attributable — instead of in a request.
|
|
101
|
+
def warm_search
|
|
102
|
+
search_corpus
|
|
103
|
+
self
|
|
104
|
+
end
|
|
105
|
+
|
|
53
106
|
def call(env)
|
|
54
107
|
request = Rack::Request.new(env)
|
|
55
108
|
return not_found unless request.get?
|
|
@@ -63,6 +116,7 @@ module OKF
|
|
|
63
116
|
when "/types" then respond_json(graph.type_index)
|
|
64
117
|
when "/index" then respond_json(directory_index)
|
|
65
118
|
when "/log" then respond_json(logs)
|
|
119
|
+
when "/search" then respond_json(self.class.search_payload(search_corpus, request.params["q"]))
|
|
66
120
|
else not_found
|
|
67
121
|
end
|
|
68
122
|
end
|
|
@@ -147,6 +201,13 @@ module OKF
|
|
|
147
201
|
[ 200, { "content-type" => content_type }, [ body.to_s ] ]
|
|
148
202
|
end
|
|
149
203
|
|
|
204
|
+
# Built on the first search and held for the life of the app, the way the
|
|
205
|
+
# graph is. `okf server` warms it at boot so the first reader does not pay
|
|
206
|
+
# for the whole corpus.
|
|
207
|
+
def search_corpus
|
|
208
|
+
@search_corpus ||= OKF::Bundle::Search.prepare([ [ nil, @folder.bundle ] ], engine: SEARCH_ENGINE)
|
|
209
|
+
end
|
|
210
|
+
|
|
150
211
|
def respond_json(object)
|
|
151
212
|
respond("application/json; charset=utf-8", JSON.generate(object))
|
|
152
213
|
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+.
|
|
@@ -205,6 +192,12 @@ module OKF
|
|
|
205
192
|
end
|
|
206
193
|
private_class_method :load_entry
|
|
207
194
|
|
|
195
|
+
# See App#warm_search — same reason, one corpus over every hosted bundle.
|
|
196
|
+
def warm_search
|
|
197
|
+
search_corpus
|
|
198
|
+
self
|
|
199
|
+
end
|
|
200
|
+
|
|
208
201
|
def call(env)
|
|
209
202
|
request = Rack::Request.new(env)
|
|
210
203
|
# Everything this class *emits* must carry the prefix a host mounted it
|
|
@@ -339,6 +332,9 @@ module OKF
|
|
|
339
332
|
@apps = build_apps(@layout)
|
|
340
333
|
@counts = nil
|
|
341
334
|
@health = nil
|
|
335
|
+
# The corpus is a snapshot of the set, so a write that changes the set
|
|
336
|
+
# invalidates it. Without this a removed bundle keeps answering /search.
|
|
337
|
+
@search_corpus = nil
|
|
342
338
|
end
|
|
343
339
|
|
|
344
340
|
# Same-origin *and* the token. Neither alone is enough: the token lives in
|
|
@@ -374,14 +370,14 @@ module OKF
|
|
|
374
370
|
# keystroke, and the box starts empty. `fuzzy: true` matches both the TUI
|
|
375
371
|
# and the page's own MiniSearch, so all three forgive the same typos.
|
|
376
372
|
def search(query)
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
)
|
|
373
|
+
json(App.search_payload(search_corpus, query))
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# One index over every hosted bundle — one corpus, so BM25 weighs a term
|
|
377
|
+
# against the whole set exactly as .across intends. Dropped whenever the
|
|
378
|
+
# bundle set changes, which is the only thing that can invalidate it.
|
|
379
|
+
def search_corpus
|
|
380
|
+
@search_corpus ||= OKF::Bundle::Search.prepare(pairs, engine: App::SEARCH_ENGINE)
|
|
385
381
|
end
|
|
386
382
|
|
|
387
383
|
# The /b/ manager's own rows, as JSON — what the graph page's Bundles panel
|
|
@@ -533,14 +529,26 @@ module OKF
|
|
|
533
529
|
count: nil, health: "missing", word: word, default: false }
|
|
534
530
|
end
|
|
535
531
|
|
|
532
|
+
# A bundle is addressed by its slug — `@orders` on the CLI, `/b/orders/`
|
|
533
|
+
# here — so the slug is its name and the folder is a fact about it. The row
|
|
534
|
+
# led with Folder.label instead, which put the address where the name goes;
|
|
535
|
+
# in a real registry that label is `…/.okf` on nearly every line, so the
|
|
536
|
+
# loudest column repeated the one word that tells no two bundles apart.
|
|
537
|
+
#
|
|
538
|
+
# The short label is gone rather than demoted: it only ever stood in for the
|
|
539
|
+
# path, and the path is right here on the row's second line.
|
|
540
|
+
#
|
|
541
|
+
# The name keeps its `@`, which the ref line used to carry: it is the exact
|
|
542
|
+
# spelling `okf lint @orders` takes, so the row teaches the CLI for free —
|
|
543
|
+
# and one element now does the whole job two were splitting.
|
|
536
544
|
def manager_row(base, row)
|
|
537
545
|
name = if row[:mount]
|
|
538
|
-
%(<a class="name" href="#{escape(base)}#{MOUNT}/#{escape(row[:mount])}/"
|
|
546
|
+
%(<a class="name" href="#{escape(base)}#{MOUNT}/#{escape(row[:mount])}/">@#{escape(row[:slug])}</a>)
|
|
539
547
|
else
|
|
540
|
-
%(<span class="name off"
|
|
548
|
+
%(<span class="name off">@#{escape(row[:slug])}</span>)
|
|
541
549
|
end
|
|
542
550
|
%(<li class="row" data-health="#{escape(row[:health])}">) +
|
|
543
|
-
%(<div class="who">#{name}<div class="ref"
|
|
551
|
+
%(<div class="who">#{name}<div class="ref">) +
|
|
544
552
|
# The row shows the tail; the tooltip is where the whole path stays
|
|
545
553
|
# reachable, since nothing else on the page carries it.
|
|
546
554
|
%(<span class="dir" title="#{escape(row[:dir])}"><bdi>#{escape(row[:dir])}</bdi></span></div></div>) +
|
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`).
|
|
@@ -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,7 +34,7 @@ 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
|
|
37
|
+
`okf stats`, tags spread thin across dirs in `okf tags --by dir`, hubs
|
|
38
38
|
whose inbound links are mostly foreign in `okf graph --hubs` → offer
|
|
39
39
|
**`refine`** (evidence-driven restructuring; it proposes before it
|
|
40
40
|
touches anything).
|
|
@@ -19,14 +19,15 @@ 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
|
|
@@ -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
|
|
@@ -150,7 +150,7 @@ as Ruby regular expressions with `--regexp`/`-e` (an invalid pattern is a usage
|
|
|
150
150
|
error, exit 2). `--fuzzy` forgives typos; pairing it with `-e` is a usage error,
|
|
151
151
|
since a pattern is matched literally rather than by edit distance.
|
|
152
152
|
`--in a,b` restricts the searched fields (title, id, tags, type, description,
|
|
153
|
-
body); the shared `--type/--
|
|
153
|
+
body); the shared `--type/--dir/--tag` filters narrow the candidates *first*,
|
|
154
154
|
so a search scoped by what `index` taught you stays surgical.
|
|
155
155
|
|
|
156
156
|
**The default is exact, so an exact query means what it looks like.** A phrase in
|
|
@@ -273,8 +273,30 @@ there, its child directories, and the concept listing. Run it first when picking
|
|
|
273
273
|
an existing bundle: it is the cheapest high-signal orientation, and it surfaces
|
|
274
274
|
enumeration drift a grep can't (you can't grep for a listing entry that is *missing*).
|
|
275
275
|
|
|
276
|
-
`--
|
|
277
|
-
format` shows both; `root`
|
|
276
|
+
`--dir PATH` narrows to a directory **and everything below it**, and is
|
|
277
|
+
**repeatable** — `--dir model --dir format` shows both; `root` (or `.`) names the
|
|
278
|
+
bundle root. A `--dir` also brings the **chain from the root down to it**, so a branch is
|
|
279
|
+
never shown adrift of the authored context that says what it is — the root
|
|
280
|
+
`index.md`'s prose first among it. Those rows print with a leading `↑` and carry
|
|
281
|
+
`ancestor: true`; `--no-ancestors` drops them. Ascent and descent are separate
|
|
282
|
+
axes, so `--depth` never bounds the chain: `--dir X --depth 0` is X alone, plus
|
|
283
|
+
how you get to X. A `--dir` that names nothing gains no chain — a lone root row
|
|
284
|
+
would read as a partial answer to a query that matched nothing.
|
|
285
|
+
|
|
286
|
+
`--depth N` bounds how far below the starting point the map reaches
|
|
287
|
+
(the `--dir` when one is given, else the bundle root), counted **relatively**:
|
|
288
|
+
`--depth 1` is the top of the tree, `--dir X --depth 1` is one branch of it, and
|
|
289
|
+
the pair walks down a level at a time.
|
|
290
|
+
|
|
291
|
+
**On a bundle of any size the map is unreadable whole** — every directory is a
|
|
292
|
+
section, and even `--no-body` keeps one listing row per *concept* — so narrow
|
|
293
|
+
rather than paging it: `okf dirs` is the orientation, `--dir <branch> --depth 1`
|
|
294
|
+
is the step down into it,
|
|
295
|
+
and `--except body,listing` on top of either is the lean JSON skeleton. Full
|
|
296
|
+
`index` output on a few hundred concepts runs to hundreds of KB; the same map at
|
|
297
|
+
`--depth 1` is a couple of KB.
|
|
298
|
+
|
|
299
|
+
`--no-body` drops the prose to a
|
|
278
300
|
skeleton (headers, rollups, child pointers). For a directory that has concepts but
|
|
279
301
|
**no `index.md`**, the listing is **synthesized** from the concepts' descriptions
|
|
280
302
|
and tagged `(no index.md)` — §6 explicitly permits synthesizing a map on the fly.
|
|
@@ -283,7 +305,49 @@ It is a **read view**: advisory, always exit 0. A synthesized directory is a
|
|
|
283
305
|
*signal* (a map worth writing), never a defect — `index` emits no lint findings and
|
|
284
306
|
never fails a bundle. JSON: `{ bundle, count, directories: [{ dir, index_path,
|
|
285
307
|
present, synthesized, count, types, tags, subdirs, body, listing: [{ id, title,
|
|
286
|
-
description, type, tags }] }] }
|
|
308
|
+
description, type, tags }] }] }` — `ancestor` marks a row that is there to place
|
|
309
|
+
the branch rather than to answer about it.
|
|
310
|
+
|
|
311
|
+
## dirs — the bundle's clusters and their sizes
|
|
312
|
+
|
|
313
|
+
`okf dirs <dir>` lists every directory the bundle has — the ones holding
|
|
314
|
+
concepts, the ones carrying an `index.md`, and the empty intermediates that only
|
|
315
|
+
exist to connect the tree — with the number of concepts living **directly** in
|
|
316
|
+
each and the number in its **subtree**. A cluster *is* a directory here, so this
|
|
317
|
+
is the view that tells you what `--dir` can be pointed at and how much sits
|
|
318
|
+
behind each choice.
|
|
319
|
+
|
|
320
|
+
Two numbers, because one cannot answer the question. `count` is direct, so the
|
|
321
|
+
column sums to the bundle's concept total and a dir holding only sub-directories
|
|
322
|
+
reads `0` rather than a hidden rollup. `subtree` is defined as *exactly what
|
|
323
|
+
`--dir <that row>` returns*, so the row and the flag can never disagree — which
|
|
324
|
+
is also why the root's subtree is its own direct count (`.` is a prefix of
|
|
325
|
+
nothing). Without it a truncated listing is all zeroes at the top of a deep tree,
|
|
326
|
+
which is where you most need to know where the mass is. The human table shows the
|
|
327
|
+
second column only where some dir actually nests.
|
|
328
|
+
|
|
329
|
+
`--dir PATH` (repeatable) narrows to a directory and its subtree, and brings the
|
|
330
|
+
**chain up to the root** with it so the branch is placed rather than shown
|
|
331
|
+
adrift — those rows are marked `↑`, carry `ancestor: true`, and stay out of
|
|
332
|
+
`total` (`--no-ancestors` drops them). `--depth N` keeps only N levels below the
|
|
333
|
+
starting point — the `--dir` when one is given,
|
|
334
|
+
the bundle root otherwise. Relative, not absolute, so `--dir a/b --depth 1`
|
|
335
|
+
reads "a/b and one level under it" without your first working out how deep `a/b`
|
|
336
|
+
is. `--depth 0` is the starting point alone. A `--depth` that is not a whole
|
|
337
|
+
number is a usage error (exit 2).
|
|
338
|
+
|
|
339
|
+
**This is the first command to run on a bundle you do not know** — the same first
|
|
340
|
+
move [SKILL.md](../SKILL.md) prescribes. `okf dirs <dir>` is one row per
|
|
341
|
+
directory, so its size tracks the tree rather than the concept count: it tells
|
|
342
|
+
you the shape and where the weight sits, `--depth 1` trims it further on a deep
|
|
343
|
+
bundle, and you then descend with `okf index --dir`, one level at a time.
|
|
344
|
+
|
|
345
|
+
The root prints `(root)` and stores `.` — the split every grouped view keeps, so
|
|
346
|
+
a table and its `--json` never disagree about which spelling is the data. JSON:
|
|
347
|
+
`{ bundle, total, count, dirs: [{ dir, ancestor, count, subtree, subdirs }] }`,
|
|
348
|
+
root first. `count` is rows printed, chain included; `total` sums the direct
|
|
349
|
+
counts of the rows you actually asked for, which is what keeps a row's `subtree`
|
|
350
|
+
equal to the `total` that `--dir` on that row returns.
|
|
287
351
|
|
|
288
352
|
## catalog / files / tags / types / stats — the server views, as text
|
|
289
353
|
|
|
@@ -293,7 +357,8 @@ All are advisory reads (exit 0) sharing one data source (per-concept metadata pl
|
|
|
293
357
|
in/out link degree). Add `--json` to any for a machine substrate.
|
|
294
358
|
|
|
295
359
|
- **`catalog`** — every concept with its metadata (type, status, tags, timestamp,
|
|
296
|
-
in/out link degree, description), grouped by top-level area
|
|
360
|
+
in/out link degree, description), grouped by top-level area (`dir` on every row
|
|
361
|
+
carries the full path). The "what's here, in
|
|
297
362
|
detail" view. JSON: `{ bundle, count, concepts: [{ id, title, type, description,
|
|
298
363
|
tags, timestamp, status, backlog_ref, dir, area, links_out, links_in }] }`.
|
|
299
364
|
- **`files`** — the folder tree: each concept's filename + title, grouped by
|
|
@@ -301,7 +366,7 @@ in/out link degree). Add `--json` to any for a machine substrate.
|
|
|
301
366
|
id, dir, type, title, description }] }`.
|
|
302
367
|
- **`tags`** — every tag with the concepts that carry it, ordered by count
|
|
303
368
|
descending. The "what themes dominate" view. JSON: `{ bundle, count, tags: [{ tag,
|
|
304
|
-
count, concepts: [id, …] }] }`. `--by type|
|
|
369
|
+
count, concepts: [id, …] }] }`. `--by type|dir` regroups the list per concept
|
|
305
370
|
dimension with **within-group** counts (a tag spanning groups appears in each);
|
|
306
371
|
each row also carries the tag's **total** across the narrowed set, printed
|
|
307
372
|
`count/total` when they differ — so a tag's locality reads per row (a plain
|
|
@@ -313,18 +378,32 @@ in/out link degree). Add `--json` to any for a machine substrate.
|
|
|
313
378
|
- **`types`** — every type with the concepts that carry it, ordered by count
|
|
314
379
|
descending. The "what kinds of knowledge" view. JSON: `{ bundle, count, types:
|
|
315
380
|
[{ type, count, concepts: [id, …] }] }`.
|
|
316
|
-
- **`stats`** — bundle rollups: concept /
|
|
317
|
-
totals plus per-type and per-
|
|
318
|
-
`{ bundle, concepts, areas, concept_types, cross_links, distinct_tags,
|
|
381
|
+
- **`stats`** — bundle rollups: concept / dir / type / cross-link / distinct-tag
|
|
382
|
+
totals plus per-type and per-dir breakdowns. The "shape at a glance" view. JSON:
|
|
383
|
+
`{ bundle, concepts, dirs, areas, concept_types, cross_links, distinct_tags,
|
|
384
|
+
by_type, by_dir, by_area }` (`areas`/`by_area` are the deprecated first-segment
|
|
385
|
+
cut, kept for one release). `dirs`/`by_dir` cover every directory `okf dirs`
|
|
386
|
+
lists — counts are direct, so a directory holding nothing itself is present at
|
|
387
|
+
`0` rather than missing, and `by_dir.keys` is a complete list of what `--dir`
|
|
388
|
+
can address.
|
|
319
389
|
|
|
320
390
|
The four list views narrow with the same filters the browser panels offer —
|
|
321
|
-
`--type TYPE`, `--
|
|
322
|
-
itself (`tags` can't filter by tag). Matching is case-insensitive and
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
391
|
+
`--type TYPE`, `--dir PATH`, `--tag TAG`; each takes the ones orthogonal to
|
|
392
|
+
itself (`tags` can't filter by tag). Matching is case-insensitive; `--type` and
|
|
393
|
+
`--tag` are exact, `--dir` takes the named directory **and everything below it**
|
|
394
|
+
(`--dir platform` reaches `platform/services/api`). A concept at the bundle root
|
|
395
|
+
lives in `.`, which `--dir` also accepts as plain `root` (no shell quoting). A
|
|
396
|
+
filter that matches nothing is an empty view, not an error: `okf tags <dir> --dir
|
|
397
|
+
billing --json` answers "which tags does the billing cluster use?",
|
|
398
|
+
`okf catalog <dir> --tag auth` answers "what carries the auth tag?".
|
|
399
|
+
|
|
400
|
+
**`--area` is deprecated.** It still works — matching the *first path segment*
|
|
401
|
+
only, its old behavior unchanged — and prints `warning: --area is deprecated, use
|
|
402
|
+
--dir` on stderr (stdout stays clean, so a `--json` consumer is unaffected). Same
|
|
403
|
+
for `tags --by area`. Both go in a later release; write `--dir` in anything new.
|
|
404
|
+
On `index` it combines with neither `--depth` nor `--dir` — exit 2, because it is
|
|
405
|
+
*exact* and both of those select a range, so the pair used to return the area
|
|
406
|
+
plus whatever the other flag selected: an answer to neither question.
|
|
328
407
|
|
|
329
408
|
Reach for `stats` first to size a bundle, `catalog`/`files` to enumerate it, `tags`
|
|
330
409
|
to find thematic clusters — all without standing up the server.
|
|
@@ -340,10 +419,13 @@ in a body render as diagrams, and a click (or tap) opens the diagram full
|
|
|
340
419
|
screen with drag-to-pan and wheel/pinch zoom. Concepts render as nodes
|
|
341
420
|
coloured by `type` and sized by degree, links as edges, with a detail panel
|
|
342
421
|
(rendered markdown, "Links to" / "Linked from" backlinks), layout switching,
|
|
343
|
-
type/
|
|
422
|
+
type/dir/tag filters on every view (the dir chips take a directory *and* its
|
|
423
|
+
subtree, the same rule `--dir` uses), and search. Cluster mode groups the
|
|
424
|
+
concepts into one box per directory, nested to a depth picked beside the layout
|
|
425
|
+
select — depth 1 is the flat view, and a flat bundle is offered no control. The authored layer is in the
|
|
344
426
|
UI too: the Files view carries **Files | Indexes** tabs — the Indexes tab
|
|
345
427
|
lists the log first (the chronological index), then every `index.md` — and
|
|
346
|
-
folder nodes in file-tree mode and
|
|
428
|
+
folder nodes in file-tree mode and directory boxes in cluster mode open a
|
|
347
429
|
directory's §6 map in the inspector (authored, or synthesized when none
|
|
348
430
|
exists). Links to an `index.md`, `log.md`, or bare directory navigate instead
|
|
349
431
|
of dead-ending, and the log is fetched fresh on every read, so a
|
data/lib/okf/version.rb
CHANGED
data/lib/okf.rb
CHANGED
|
@@ -25,6 +25,15 @@ module OKF
|
|
|
25
25
|
value.respond_to?(:empty?) ? value.empty? : false
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# The directory a concept lives in, derived from its §2 id: the id *is* the
|
|
29
|
+
# path minus `.md`, so putting the suffix back and taking the dirname is the
|
|
30
|
+
# definition rather than a parse of it. One home for it because three views
|
|
31
|
+
# answer with a `dir` — the catalog, the search rows, the linter's per-directory
|
|
32
|
+
# checks — and a rule spelled three times is three things to keep in step.
|
|
33
|
+
def self.dir_of(id)
|
|
34
|
+
File.dirname("#{id}.md")
|
|
35
|
+
end
|
|
36
|
+
|
|
28
37
|
require "okf/version"
|
|
29
38
|
|
|
30
39
|
# ── kernel: cross-cutting primitives ──
|
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.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rodrigo Serradura
|
|
@@ -54,11 +54,22 @@ dependencies:
|
|
|
54
54
|
description: |
|
|
55
55
|
OKF (Open Knowledge Format) is portable knowledge: Markdown files with YAML
|
|
56
56
|
frontmatter that both humans and agents read from one source. This gem is the
|
|
57
|
-
Ruby-native way to work with it.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
Ruby-native way to work with it.
|
|
58
|
+
|
|
59
|
+
Its companion agent skill authors and curates a bundle. The `okf` command-line
|
|
60
|
+
tool validates the result for v0.1 (§9) conformance, lints its curation
|
|
61
|
+
quality, and answers questions about it: ranked full-text search, and a
|
|
62
|
+
progressive-disclosure map that reads a large bundle a directory at a time
|
|
63
|
+
rather than loading it whole. `okf server` opens it as an interactive
|
|
64
|
+
knowledge graph and `okf render` bakes that same page into one self-contained
|
|
65
|
+
HTML file you can host anywhere. A per-user registry names your bundles, so
|
|
66
|
+
every verb reaches them by @slug from any directory and one search can span
|
|
67
|
+
them all.
|
|
68
|
+
|
|
69
|
+
Everything the CLI does also runs in-process through a library API
|
|
70
|
+
(OKF::Bundle and friends), and the graph server is a mountable Rack app. It
|
|
71
|
+
adds no service to your stack: rack, webrick and minifts are the only runtime
|
|
72
|
+
dependencies, and it runs on every Ruby since 2.4.
|
|
62
73
|
email:
|
|
63
74
|
- rodrigo.serradura@gmail.com
|
|
64
75
|
executables:
|
|
@@ -88,6 +99,7 @@ files:
|
|
|
88
99
|
- lib/okf/cli.rb
|
|
89
100
|
- lib/okf/cli/catalog.rb
|
|
90
101
|
- lib/okf/cli/command.rb
|
|
102
|
+
- lib/okf/cli/dirs.rb
|
|
91
103
|
- lib/okf/cli/files.rb
|
|
92
104
|
- lib/okf/cli/graph.rb
|
|
93
105
|
- lib/okf/cli/index.rb
|
|
@@ -160,6 +172,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
160
172
|
requirements: []
|
|
161
173
|
rubygems_version: 4.0.16
|
|
162
174
|
specification_version: 4
|
|
163
|
-
summary: 'The complete
|
|
164
|
-
|
|
175
|
+
summary: 'The complete Open Knowledge Format toolkit: an agent skill, a CLI and library,
|
|
176
|
+
ranked search, and a live knowledge graph. 100% local.'
|
|
165
177
|
test_files: []
|