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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +696 -133
  3. data/README.md +250 -334
  4. data/lib/okf/bundle/folder.rb +24 -5
  5. data/lib/okf/bundle/linter.rb +1 -1
  6. data/lib/okf/bundle/search/index.rb +13 -3
  7. data/lib/okf/bundle/search.rb +91 -11
  8. data/lib/okf/bundle.rb +26 -2
  9. data/lib/okf/cli/catalog.rb +66 -0
  10. data/lib/okf/cli/command.rb +657 -0
  11. data/lib/okf/cli/dirs.rb +118 -0
  12. data/lib/okf/cli/files.rb +68 -0
  13. data/lib/okf/cli/graph.rb +82 -0
  14. data/lib/okf/cli/index.rb +169 -0
  15. data/lib/okf/cli/lint.rb +139 -0
  16. data/lib/okf/cli/loose.rb +78 -0
  17. data/lib/okf/cli/registry.rb +229 -0
  18. data/lib/okf/cli/render.rb +66 -0
  19. data/lib/okf/cli/search.rb +285 -0
  20. data/lib/okf/cli/server.rb +186 -0
  21. data/lib/okf/cli/skill.rb +57 -0
  22. data/lib/okf/cli/stats.rb +113 -0
  23. data/lib/okf/cli/tags.rb +144 -0
  24. data/lib/okf/cli/types.rb +37 -0
  25. data/lib/okf/cli/validate.rb +66 -0
  26. data/lib/okf/cli.rb +425 -1706
  27. data/lib/okf/render/graph/template.html.erb +1285 -129
  28. data/lib/okf/render/graph.rb +46 -2
  29. data/lib/okf/server/app.rb +71 -4
  30. data/lib/okf/server/hub/not_found.rb +663 -0
  31. data/lib/okf/server/hub.rb +512 -38
  32. data/lib/okf/skill/SKILL.md +26 -19
  33. data/lib/okf/skill/playbooks/consume.md +3 -3
  34. data/lib/okf/skill/playbooks/curate.md +3 -1
  35. data/lib/okf/skill/playbooks/maintain.md +7 -5
  36. data/lib/okf/skill/playbooks/menu.md +5 -0
  37. data/lib/okf/skill/playbooks/refine.md +93 -0
  38. data/lib/okf/skill/playbooks/search.md +7 -7
  39. data/lib/okf/skill/reference/cli.md +122 -22
  40. data/lib/okf/version.rb +1 -1
  41. data/lib/okf.rb +9 -0
  42. metadata +38 -8
@@ -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.
@@ -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, and the hub root. They stay nil for a standalone
37
- # server and for `okf render`, so a static file never advertises a switcher.
38
- def initialize(folder, title: nil, link: nil, layout: "cose", siblings: nil, self_slug: nil, hub_path: nil)
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