okf 1.8.0 → 1.10.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 +615 -38
  3. data/README.md +109 -15
  4. data/lib/okf/bundle/folder.rb +20 -0
  5. data/lib/okf/bundle/search/index.rb +65 -0
  6. data/lib/okf/bundle/search/scan.rb +89 -0
  7. data/lib/okf/bundle/search.rb +262 -66
  8. data/lib/okf/bundle.rb +27 -3
  9. data/lib/okf/cli/catalog.rb +66 -0
  10. data/lib/okf/cli/command.rb +495 -0
  11. data/lib/okf/cli/files.rb +68 -0
  12. data/lib/okf/cli/graph.rb +82 -0
  13. data/lib/okf/cli/index.rb +127 -0
  14. data/lib/okf/cli/lint.rb +139 -0
  15. data/lib/okf/cli/loose.rb +78 -0
  16. data/lib/okf/cli/registry.rb +229 -0
  17. data/lib/okf/cli/render.rb +66 -0
  18. data/lib/okf/cli/search.rb +285 -0
  19. data/lib/okf/cli/server.rb +179 -0
  20. data/lib/okf/cli/skill.rb +57 -0
  21. data/lib/okf/cli/stats.rb +88 -0
  22. data/lib/okf/cli/tags.rb +122 -0
  23. data/lib/okf/cli/types.rb +37 -0
  24. data/lib/okf/cli/validate.rb +66 -0
  25. data/lib/okf/cli.rb +418 -1633
  26. data/lib/okf/{server → render}/graph/template.html.erb +1553 -175
  27. data/lib/okf/{server → render}/graph.rb +85 -9
  28. data/lib/okf/server/app.rb +17 -48
  29. data/lib/okf/server/hub/not_found.rb +663 -0
  30. data/lib/okf/server/hub.rb +504 -38
  31. data/lib/okf/skill/SKILL.md +41 -26
  32. data/lib/okf/skill/playbooks/consume.md +5 -3
  33. data/lib/okf/skill/playbooks/curate.md +3 -1
  34. data/lib/okf/skill/playbooks/maintain.md +4 -3
  35. data/lib/okf/skill/playbooks/menu.md +5 -0
  36. data/lib/okf/skill/playbooks/refine.md +92 -0
  37. data/lib/okf/skill/playbooks/search.md +47 -7
  38. data/lib/okf/skill/reference/authoring.md +3 -2
  39. data/lib/okf/skill/reference/cli.md +98 -21
  40. data/lib/okf/version.rb +1 -1
  41. data/lib/okf.rb +8 -0
  42. metadata +37 -3
@@ -3,10 +3,14 @@
3
3
  require "rack/utils"
4
4
 
5
5
  module OKF
6
- module Server
7
- # Renders an OKF::Bundle::Graph as the interactive graph page served by
8
- # OKF::Server::App. The markup lives in graph/template.html.erb; #render
9
- # returns the HTML string.
6
+ # The view layer: turns a bundle into the interactive graph page. Pairs with the
7
+ # pure OKF::Bundle::Graph (the data model) — Bundle builds the graph, Render
8
+ # draws it. A shell (reads the template, escapes with rack/utils), but knows
9
+ # nothing about HTTP: OKF::Server::App serves what this produces, and `okf
10
+ # render` writes it to a file, from the one class.
11
+ module Render
12
+ # Renders an OKF::Bundle::Graph as the interactive graph page. The markup lives
13
+ # in graph/template.html.erb; #render returns the HTML string.
10
14
  #
11
15
  # The page boots from a *minimal* payload — nodes carry only id + title, plus
12
16
  # compact TYPES/TAGS inverted indexes for colouring and filtering. It has two
@@ -16,9 +20,9 @@ module OKF
16
20
  # metadata, catalog, index and log are pulled from OKF::Server::App on
17
21
  # demand via fetch, so the initial payload stays small and bodies read
18
22
  # live from disk (edits show without a restart). Nothing extra embedded.
19
- # render mode (embed: payload) — `okf render` bakes the whole bundle in:
20
- # the same fetch getters resolve from the injected payload instead, so
21
- # the single file needs no server (e.g. hosting on GitHub Pages).
23
+ # render mode (embed: payload) — `okf render` bakes the whole bundle in via
24
+ # .static below: the same fetch getters resolve from the injected payload
25
+ # instead, so the single file needs no server (e.g. GitHub Pages).
22
26
  #
23
27
  # NOTE (trust boundary): the page loads Cytoscape + marked from a CDN, so it
24
28
  # needs network for those libraries even in render mode. Fetched/embedded
@@ -38,6 +42,28 @@ module OKF
38
42
  # built from the backslash code point so no literal escape appears here.
39
43
  LT_ESCAPE = (92.chr(Encoding::UTF_8) + "u003c").freeze
40
44
 
45
+ # `okf render`: the whole page as one self-contained file, the bundle baked
46
+ # in, so it hosts where no server answers a fetch. Takes any bundle handle
47
+ # (an OKF::Bundle::Folder) and returns the HTML string.
48
+ def self.static(folder, title: nil, link: nil, layout: "cose")
49
+ new(folder.graph(minimal: true), title: title || folder.name, link: link, layout: layout, embed: payload(folder)).render
50
+ end
51
+
52
+ # What the baked page carries in place of the endpoints a live server would
53
+ # answer. Every key here is data a client getter reads from EMBED instead of
54
+ # fetching — and each derives from the *same* folder method the matching
55
+ # OKF::Server::App endpoint uses, so the bake and the live server cannot
56
+ # drift (/node/meta is the exception: the fragment is derived on the client
57
+ # from the catalog's raw description, so no map is baked for it).
58
+ def self.payload(folder)
59
+ {
60
+ catalog: folder.catalog,
61
+ index: folder.directory_index,
62
+ logs: folder.log_entries,
63
+ bodies: folder.concepts.each_with_object({}) { |concept, map| map[concept.id] = concept.body.to_s }
64
+ }
65
+ end
66
+
41
67
  # +node_endpoint+/+meta_endpoint+ are the (mount-relative) URLs the page
42
68
  # fetches a concept's raw markdown and metadata fragment from — relative so
43
69
  # the page works whether served at "/" or mounted under a Rails prefix.
@@ -45,9 +71,11 @@ module OKF
45
71
  # +siblings+/+self_slug+/+hub_path+ carry the hub's bundle switcher into the
46
72
  # page (server mode only). nil — the standalone-server and `okf render`
47
73
  # default — injects an empty SIBLINGS, so the switcher never appears in a
48
- # 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.
49
76
  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)
77
+ siblings: nil, self_slug: nil, hub_path: nil, search_endpoint: nil,
78
+ manage_root: nil, manage_token: nil)
51
79
  @graph = graph
52
80
  @title = title
53
81
  @link = link
@@ -58,6 +86,9 @@ module OKF
58
86
  @siblings = siblings
59
87
  @self_slug = self_slug
60
88
  @hub_path = hub_path
89
+ @search_endpoint = search_endpoint
90
+ @manage_root = manage_root
91
+ @manage_token = manage_token
61
92
  end
62
93
 
63
94
  def render
@@ -74,6 +105,12 @@ module OKF
74
105
  html_escape(graph_name)
75
106
  end
76
107
 
108
+ # The bundle's own name, for the client: what the header already shows, so
109
+ # the page can label the root with it instead of `(root)` or `/`.
110
+ def name_json
111
+ json_for_script(graph_name)
112
+ end
113
+
77
114
  def og_title
78
115
  html_escape("OKF · #{graph_name}")
79
116
  end
@@ -126,6 +163,45 @@ module OKF
126
163
  json_for_script(@hub_path)
127
164
  end
128
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
+
129
205
  # JSON-encode for safe embedding in an inline <script>: escaping every `<` to
130
206
  # its JSON unicode escape neutralizes </script>, <!-- and <script in one
131
207
  # stroke, and the result stays valid JSON *and* JavaScript.
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "rack"
4
4
 
5
- require "okf/server/graph"
5
+ require "okf/render/graph"
6
6
 
7
7
  module OKF
8
8
  module Server
@@ -11,7 +11,7 @@ module OKF
11
11
  #
12
12
  # mount OKF::Server::App.new(folder) => "/knowledge"
13
13
  #
14
- # The page (OKF::Server::Graph) boots from a *minimal* graph (id + title + edges
14
+ # The page (OKF::Render::Graph) boots from a *minimal* graph (id + title + edges
15
15
  # + type/tag indexes) and pulls each concept's markdown body and description from
16
16
  # here on demand, so the initial payload stays small and bodies are read live
17
17
  # from disk (edits show without a restart). Part of the shell — it does I/O.
@@ -33,9 +33,11 @@ module OKF
33
33
  class App
34
34
  # +siblings+/+self_slug+/+hub_path+ are set only when this app is hosted under
35
35
  # 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)
36
+ # bundle's own mount slug, the hub root, and the hub's cross-bundle search
37
+ # route. They stay nil for a standalone server and for `okf render`, so a
38
+ # static file never advertises a switcher or a search it cannot answer.
39
+ 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)
39
41
  @folder = folder
40
42
  @title = title
41
43
  @link = link
@@ -43,6 +45,9 @@ module OKF
43
45
  @siblings = siblings
44
46
  @self_slug = self_slug
45
47
  @hub_path = hub_path
48
+ @search_endpoint = search_endpoint
49
+ @manage_root = manage_root
50
+ @manage_token = manage_token
46
51
  end
47
52
 
48
53
  def call(env)
@@ -62,13 +67,6 @@ module OKF
62
67
  end
63
68
  end
64
69
 
65
- # The same interactive page, but with the whole bundle baked in — bodies,
66
- # catalog, index and logs — so it needs no server. This is what `okf render`
67
- # writes: the fetch getters resolve from the embedded payload, not from here.
68
- def render_static
69
- Graph.new(graph, title: @title || @folder.name, link: @link, layout: @layout, embed: embed_payload).render
70
- end
71
-
72
70
  # The 404 both this app and the Hub answer with, so the two cannot drift.
73
71
  def self.not_found
74
72
  [ 404, { "content-type" => "text/plain; charset=utf-8" }, [ "not found\n" ] ]
@@ -97,50 +95,21 @@ module OKF
97
95
  { directories: @folder.directory_index }
98
96
  end
99
97
 
100
- # Every log.md with its content, root scope first. Content is read live
101
- # from disk so a just-appended entry shows without a restart; paths come
102
- # from the loaded bundle, never from the request.
98
+ # The §7 history the Log panel renders: every log.md with its content, root
99
+ # scope first, read live from disk. Built by OKF::Bundle::Folder#log_entries,
100
+ # shared with `okf render`'s bake so the served and baked logs cannot drift.
103
101
  def logs
104
- entries = @folder.bundle.log_files.sort_by { |path| [ path == "log.md" ? 0 : 1, path ] }
105
- { logs: entries.map { |path| { path: path, dir: File.dirname(path), content: log_content(path) } } }
106
- end
107
-
108
- def log_content(path)
109
- File.read(File.join(@folder.root, path), encoding: "UTF-8")
110
- rescue SystemCallError
111
- @folder.bundle.reserved_content(path)
102
+ { logs: @folder.log_entries }
112
103
  end
113
104
 
114
105
  def page
115
- @page ||= Graph.new(
106
+ @page ||= OKF::Render::Graph.new(
116
107
  graph, title: @title || @folder.name, link: @link, layout: @layout,
117
- siblings: @siblings, self_slug: @self_slug, hub_path: @hub_path
108
+ siblings: @siblings, self_slug: @self_slug, hub_path: @hub_path, search_endpoint: @search_endpoint,
109
+ manage_root: @manage_root, manage_token: @manage_token
118
110
  ).render
119
111
  end
120
112
 
121
- # Everything the on-demand endpoints would serve, baked for render mode. The
122
- # arrays match what each client getter extracts from the JSON envelope; the
123
- # per-concept maps mirror /node (raw, unstripped body) and /node/meta (the
124
- # same escaped fragment). Read from the in-memory bundle — no live disk read,
125
- # since a static file is a snapshot, not a window on edits.
126
- def embed_payload
127
- {
128
- catalog: @folder.catalog,
129
- index: @folder.directory_index,
130
- logs: logs[:logs],
131
- bodies: bodies,
132
- meta: meta
133
- }
134
- end
135
-
136
- def bodies
137
- @folder.bundle.concepts.each_with_object({}) { |concept, map| map[concept.id] = concept.body.to_s }
138
- end
139
-
140
- def meta
141
- @folder.bundle.concepts.each_with_object({}) { |concept, map| map[concept.id] = description_fragment(concept) }
142
- end
143
-
144
113
  def node_body(id)
145
114
  concept = concept_for(id)
146
115
  return not_found if concept.nil?