okf 1.9.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 +696 -133
- data/README.md +250 -334
- 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 +91 -11
- data/lib/okf/bundle.rb +26 -2
- data/lib/okf/cli/catalog.rb +66 -0
- data/lib/okf/cli/command.rb +657 -0
- data/lib/okf/cli/dirs.rb +118 -0
- data/lib/okf/cli/files.rb +68 -0
- data/lib/okf/cli/graph.rb +82 -0
- data/lib/okf/cli/index.rb +169 -0
- data/lib/okf/cli/lint.rb +139 -0
- data/lib/okf/cli/loose.rb +78 -0
- data/lib/okf/cli/registry.rb +229 -0
- data/lib/okf/cli/render.rb +66 -0
- data/lib/okf/cli/search.rb +285 -0
- data/lib/okf/cli/server.rb +186 -0
- data/lib/okf/cli/skill.rb +57 -0
- data/lib/okf/cli/stats.rb +113 -0
- data/lib/okf/cli/tags.rb +144 -0
- data/lib/okf/cli/types.rb +37 -0
- data/lib/okf/cli/validate.rb +66 -0
- data/lib/okf/cli.rb +425 -1706
- data/lib/okf/render/graph/template.html.erb +1285 -129
- data/lib/okf/render/graph.rb +46 -2
- data/lib/okf/server/app.rb +71 -4
- data/lib/okf/server/hub/not_found.rb +663 -0
- data/lib/okf/server/hub.rb +512 -38
- data/lib/okf/skill/SKILL.md +26 -19
- data/lib/okf/skill/playbooks/consume.md +3 -3
- data/lib/okf/skill/playbooks/curate.md +3 -1
- data/lib/okf/skill/playbooks/maintain.md +7 -5
- data/lib/okf/skill/playbooks/menu.md +5 -0
- data/lib/okf/skill/playbooks/refine.md +93 -0
- data/lib/okf/skill/playbooks/search.md +7 -7
- data/lib/okf/skill/reference/cli.md +122 -22
- data/lib/okf/version.rb +1 -1
- data/lib/okf.rb +9 -0
- metadata +38 -8
data/lib/okf/render/graph.rb
CHANGED
|
@@ -71,9 +71,11 @@ module OKF
|
|
|
71
71
|
# +siblings+/+self_slug+/+hub_path+ carry the hub's bundle switcher into the
|
|
72
72
|
# page (server mode only). nil — the standalone-server and `okf render`
|
|
73
73
|
# default — injects an empty SIBLINGS, so the switcher never appears in a
|
|
74
|
-
# single bundle or a static file.
|
|
74
|
+
# single bundle or a static file. +search_endpoint+ rides along with them:
|
|
75
|
+
# the hub's cross-bundle /search, which only a hub can answer.
|
|
75
76
|
def initialize(graph, title: nil, link: nil, layout: "cose", node_endpoint: "node", meta_endpoint: "node/meta", embed: nil,
|
|
76
|
-
siblings: nil, self_slug: nil, hub_path: nil
|
|
77
|
+
siblings: nil, self_slug: nil, hub_path: nil, search_endpoint: nil,
|
|
78
|
+
manage_root: nil, manage_token: nil)
|
|
77
79
|
@graph = graph
|
|
78
80
|
@title = title
|
|
79
81
|
@link = link
|
|
@@ -84,6 +86,9 @@ module OKF
|
|
|
84
86
|
@siblings = siblings
|
|
85
87
|
@self_slug = self_slug
|
|
86
88
|
@hub_path = hub_path
|
|
89
|
+
@search_endpoint = search_endpoint
|
|
90
|
+
@manage_root = manage_root
|
|
91
|
+
@manage_token = manage_token
|
|
87
92
|
end
|
|
88
93
|
|
|
89
94
|
def render
|
|
@@ -158,6 +163,45 @@ module OKF
|
|
|
158
163
|
json_for_script(@hub_path)
|
|
159
164
|
end
|
|
160
165
|
|
|
166
|
+
# The hub's cross-bundle /search, mount-relative — null everywhere else, and
|
|
167
|
+
# that null is the gate: a standalone server and a static file have no set
|
|
168
|
+
# of bundles to search, so the palette never offers concepts there.
|
|
169
|
+
def search_endpoint_json
|
|
170
|
+
json_for_script(@search_endpoint)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Behind a hub the mark is a link back to the bundle list — "../" reaches
|
|
174
|
+
# it under any mount, because every page lives at <prefix>/b/<slug>/.
|
|
175
|
+
# Standalone and static have nowhere to go, so there it stays the plain
|
|
176
|
+
# identity badge it has always been, and an <a href> to nothing is worse
|
|
177
|
+
# than no <a> at all.
|
|
178
|
+
def rail_brand_open
|
|
179
|
+
return %(<span class="rail-brand" title=") unless @manage_root
|
|
180
|
+
|
|
181
|
+
%(<a class="rail-brand" href="../" aria-label="All bundles" title=")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def rail_brand_close
|
|
185
|
+
@manage_root ? "</a>" : "</span>"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# The hub root, mount-relative — where the Bundles panel reads /bundles and
|
|
189
|
+
# posts /registry/<verb>. Null everywhere else, and that null is the gate:
|
|
190
|
+
# a standalone server and a static file have no registry behind them, so
|
|
191
|
+
# the panel never appears there.
|
|
192
|
+
def manage_root_json
|
|
193
|
+
json_for_script(@manage_root)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# This boot's CSRF token, and only where a write could be honoured — a
|
|
197
|
+
# read-only hub bakes null, so the page holds no credential it may not use.
|
|
198
|
+
# It is no wider than the /b/ manager, which any script on this origin
|
|
199
|
+
# could already read; keeping it out of the page where it is useless is
|
|
200
|
+
# tidiness, not a boundary.
|
|
201
|
+
def manage_token_json
|
|
202
|
+
json_for_script(@manage_token)
|
|
203
|
+
end
|
|
204
|
+
|
|
161
205
|
# JSON-encode for safe embedding in an inline <script>: escaping every `<` to
|
|
162
206
|
# its JSON unicode escape neutralizes </script>, <!-- and <script in one
|
|
163
207
|
# stroke, and the result stays valid JSON *and* JavaScript.
|
data/lib/okf/server/app.rb
CHANGED
|
@@ -30,12 +30,59 @@ 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
|
-
# bundle's own mount slug,
|
|
37
|
-
# server and for `okf render`, so a
|
|
38
|
-
|
|
73
|
+
# bundle's own mount slug, the hub root, and the hub's cross-bundle search
|
|
74
|
+
# route. They stay nil for a standalone server and for `okf render`, so a
|
|
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.
|
|
84
|
+
def initialize(folder, title: nil, link: nil, layout: "cose", siblings: nil, self_slug: nil, hub_path: nil,
|
|
85
|
+
search_endpoint: nil, manage_root: nil, manage_token: nil)
|
|
39
86
|
@folder = folder
|
|
40
87
|
@title = title
|
|
41
88
|
@link = link
|
|
@@ -43,6 +90,17 @@ module OKF
|
|
|
43
90
|
@siblings = siblings
|
|
44
91
|
@self_slug = self_slug
|
|
45
92
|
@hub_path = hub_path
|
|
93
|
+
@search_endpoint = search_endpoint
|
|
94
|
+
@manage_root = manage_root
|
|
95
|
+
@manage_token = manage_token
|
|
96
|
+
end
|
|
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
|
|
46
104
|
end
|
|
47
105
|
|
|
48
106
|
def call(env)
|
|
@@ -58,6 +116,7 @@ module OKF
|
|
|
58
116
|
when "/types" then respond_json(graph.type_index)
|
|
59
117
|
when "/index" then respond_json(directory_index)
|
|
60
118
|
when "/log" then respond_json(logs)
|
|
119
|
+
when "/search" then respond_json(self.class.search_payload(search_corpus, request.params["q"]))
|
|
61
120
|
else not_found
|
|
62
121
|
end
|
|
63
122
|
end
|
|
@@ -100,7 +159,8 @@ module OKF
|
|
|
100
159
|
def page
|
|
101
160
|
@page ||= OKF::Render::Graph.new(
|
|
102
161
|
graph, title: @title || @folder.name, link: @link, layout: @layout,
|
|
103
|
-
siblings: @siblings, self_slug: @self_slug, hub_path: @hub_path
|
|
162
|
+
siblings: @siblings, self_slug: @self_slug, hub_path: @hub_path, search_endpoint: @search_endpoint,
|
|
163
|
+
manage_root: @manage_root, manage_token: @manage_token
|
|
104
164
|
).render
|
|
105
165
|
end
|
|
106
166
|
|
|
@@ -141,6 +201,13 @@ module OKF
|
|
|
141
201
|
[ 200, { "content-type" => content_type }, [ body.to_s ] ]
|
|
142
202
|
end
|
|
143
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
|
+
|
|
144
211
|
def respond_json(object)
|
|
145
212
|
respond("application/json; charset=utf-8", JSON.generate(object))
|
|
146
213
|
end
|