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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +615 -38
- data/README.md +109 -15
- data/lib/okf/bundle/folder.rb +20 -0
- data/lib/okf/bundle/search/index.rb +65 -0
- data/lib/okf/bundle/search/scan.rb +89 -0
- data/lib/okf/bundle/search.rb +262 -66
- data/lib/okf/bundle.rb +27 -3
- data/lib/okf/cli/catalog.rb +66 -0
- data/lib/okf/cli/command.rb +495 -0
- data/lib/okf/cli/files.rb +68 -0
- data/lib/okf/cli/graph.rb +82 -0
- data/lib/okf/cli/index.rb +127 -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 +179 -0
- data/lib/okf/cli/skill.rb +57 -0
- data/lib/okf/cli/stats.rb +88 -0
- data/lib/okf/cli/tags.rb +122 -0
- data/lib/okf/cli/types.rb +37 -0
- data/lib/okf/cli/validate.rb +66 -0
- data/lib/okf/cli.rb +418 -1633
- data/lib/okf/{server → render}/graph/template.html.erb +1553 -175
- data/lib/okf/{server → render}/graph.rb +85 -9
- data/lib/okf/server/app.rb +17 -48
- data/lib/okf/server/hub/not_found.rb +663 -0
- data/lib/okf/server/hub.rb +504 -38
- data/lib/okf/skill/SKILL.md +41 -26
- data/lib/okf/skill/playbooks/consume.md +5 -3
- data/lib/okf/skill/playbooks/curate.md +3 -1
- data/lib/okf/skill/playbooks/maintain.md +4 -3
- data/lib/okf/skill/playbooks/menu.md +5 -0
- data/lib/okf/skill/playbooks/refine.md +92 -0
- data/lib/okf/skill/playbooks/search.md +47 -7
- data/lib/okf/skill/reference/authoring.md +3 -2
- data/lib/okf/skill/reference/cli.md +98 -21
- data/lib/okf/version.rb +1 -1
- data/lib/okf.rb +8 -0
- metadata +37 -3
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
require "rack/utils"
|
|
4
4
|
|
|
5
5
|
module OKF
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
21
|
-
# the single file needs no server (e.g.
|
|
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.
|
data/lib/okf/server/app.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "rack"
|
|
4
4
|
|
|
5
|
-
require "okf/
|
|
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::
|
|
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,
|
|
37
|
-
# server and for `okf render`, so a
|
|
38
|
-
|
|
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
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
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
|
-
|
|
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?
|