okf 1.4.0 → 1.6.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 +71 -0
- data/README.md +88 -43
- data/lib/okf/bundle/search.rb +155 -0
- data/lib/okf/bundle.rb +2 -2
- data/lib/okf/cli.rb +98 -4
- data/lib/okf/server/app.rb +59 -0
- data/lib/okf/server/graph/template.html.erb +271 -45
- data/lib/okf/server/graph.rb +25 -9
- data/lib/okf/skill/SKILL.md +23 -21
- data/lib/okf/skill/playbooks/consume.md +4 -2
- data/lib/okf/skill/playbooks/maintain.md +6 -4
- data/lib/okf/skill/playbooks/menu.md +10 -6
- data/lib/okf/skill/playbooks/search.md +42 -0
- data/lib/okf/skill/reference/authoring.md +2 -2
- data/lib/okf/skill/reference/cli.md +73 -18
- data/lib/okf/version.rb +1 -1
- data/lib/okf.rb +1 -0
- metadata +5 -28
- data/.okf/capabilities/agent-skill.md +0 -46
- data/.okf/capabilities/graph-server.md +0 -63
- data/.okf/capabilities/index.md +0 -20
- data/.okf/capabilities/library-api.md +0 -72
- data/.okf/capabilities/linter.md +0 -49
- data/.okf/capabilities/read-views.md +0 -84
- data/.okf/capabilities/validator.md +0 -40
- data/.okf/cli.md +0 -52
- data/.okf/design/core-shell-split.md +0 -58
- data/.okf/design/index.md +0 -10
- data/.okf/design/ruby-floor.md +0 -45
- data/.okf/design/runtime-dependencies.md +0 -44
- data/.okf/design/server-trust-boundary.md +0 -40
- data/.okf/format/citations.md +0 -33
- data/.okf/format/cross-links.md +0 -52
- data/.okf/format/frontmatter.md +0 -38
- data/.okf/format/index.md +0 -9
- data/.okf/format/okf-format.md +0 -43
- data/.okf/index.md +0 -18
- data/.okf/log.md +0 -10
- data/.okf/model/bundle.md +0 -38
- data/.okf/model/concept.md +0 -44
- data/.okf/model/graph.md +0 -44
- data/.okf/model/index.md +0 -8
- data/.okf/overview.md +0 -67
data/lib/okf/cli.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "optparse"
|
|
4
4
|
|
|
5
5
|
module OKF
|
|
6
|
-
# Command-line front end: `okf graph|validate|lint|loose|index|catalog|files|tags|types|stats|server <dir>`.
|
|
6
|
+
# Command-line front end: `okf graph|validate|lint|loose|search|index|catalog|files|tags|types|stats|server <dir>`.
|
|
7
7
|
# This is the
|
|
8
8
|
# only layer that parses argv, prints, writes files, and decides exit codes — the
|
|
9
9
|
# lib classes below it just return data. Streams are injectable for testing.
|
|
@@ -47,6 +47,7 @@ module OKF
|
|
|
47
47
|
when "validate" then validate(argv)
|
|
48
48
|
when "lint" then lint(argv)
|
|
49
49
|
when "loose" then loose(argv)
|
|
50
|
+
when "search" then search(argv)
|
|
50
51
|
when "index" then index(argv)
|
|
51
52
|
when "catalog" then catalog(argv)
|
|
52
53
|
when "files" then files(argv)
|
|
@@ -54,6 +55,7 @@ module OKF
|
|
|
54
55
|
when "types" then types(argv)
|
|
55
56
|
when "stats" then stats(argv)
|
|
56
57
|
when "server" then server(argv)
|
|
58
|
+
when "render" then render(argv)
|
|
57
59
|
when "skill" then skill(argv)
|
|
58
60
|
when "version", "--version", "-v" then @out.puts(OKF::VERSION); 0
|
|
59
61
|
when "help", "--help", "-h" then usage(@out); 0
|
|
@@ -131,6 +133,65 @@ module OKF
|
|
|
131
133
|
0
|
|
132
134
|
end
|
|
133
135
|
|
|
136
|
+
# Deterministic text retrieval — the browser page's search brought to the CLI
|
|
137
|
+
# and extended to bodies. Terms after the directory are ANDed case-insensitive
|
|
138
|
+
# substrings (Ruby regexps with --regexp); rows rank by where they hit (title >
|
|
139
|
+
# id > tags > type/description > body) and carry one bounded context snippet,
|
|
140
|
+
# so "which concept covers X?" costs a few rows, not a body read. Advisory
|
|
141
|
+
# read: exit 0 even with no matches. Deliberately not fuzzy — the consuming
|
|
142
|
+
# agent is the fuzzy layer.
|
|
143
|
+
def search(argv)
|
|
144
|
+
options = { json: false, regexp: false }
|
|
145
|
+
parser = OptionParser.new do |o|
|
|
146
|
+
o.banner = "Usage: okf search <bundle-dir> <term> [term ...] [--regexp] [--in FIELDS] [--type T] [--area A] [--tag T] [--json]"
|
|
147
|
+
json_flags(o, options, "emit the matches as JSON")
|
|
148
|
+
projection_flags(o, options)
|
|
149
|
+
o.on("-e", "--regexp", "treat each term as a Ruby regular expression (case-insensitive)") { options[:regexp] = true }
|
|
150
|
+
o.on("--in LIST", Array, "search only these fields (#{OKF::Bundle::Search::FIELDS.join(", ")})") { |v| options[:in] = v.map(&:downcase) }
|
|
151
|
+
filter_flags(o, options, :type, :area, :tag)
|
|
152
|
+
end
|
|
153
|
+
dir = positional_dir(parser, argv) or return 2
|
|
154
|
+
terms = argv
|
|
155
|
+
if terms.empty?
|
|
156
|
+
@err.puts parser.banner
|
|
157
|
+
return 2
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
unknown = Array(options[:in]) - OKF::Bundle::Search::FIELDS
|
|
161
|
+
return usage_error("unknown field(s): #{unknown.join(", ")} (searchable: #{OKF::Bundle::Search::FIELDS.join(", ")})") unless unknown.empty?
|
|
162
|
+
|
|
163
|
+
folder = OKF::Bundle::Folder.load(dir)
|
|
164
|
+
report_skipped(folder)
|
|
165
|
+
rows = OKF::Bundle::Search.call(folder.bundle, terms, fields: options[:in], regexp: options[:regexp])
|
|
166
|
+
keep = filter_ids(folder, options)
|
|
167
|
+
rows = rows.select { |row| keep.include?(row[:id]) } unless keep.nil?
|
|
168
|
+
return print_search_json(dir, terms, rows, options) if options[:json]
|
|
169
|
+
|
|
170
|
+
print_search(dir, terms, rows, folder.bundle.concepts.size)
|
|
171
|
+
0
|
|
172
|
+
rescue RegexpError => e
|
|
173
|
+
usage_error("invalid pattern: #{e.message}")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def print_search(dir, terms, rows, total)
|
|
177
|
+
@out.puts "Search — #{dir} · #{terms.join(" ")} (#{counted(rows.size, total, "concepts")})"
|
|
178
|
+
if rows.empty?
|
|
179
|
+
@out.puts " no matches — fewer or broader terms, or scan `okf tags #{dir}` for the vocabulary"
|
|
180
|
+
return
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
width = rows.map { |row| row[:id].length }.max
|
|
184
|
+
rows.each do |row|
|
|
185
|
+
@out.puts
|
|
186
|
+
@out.puts " #{row[:id].ljust(width)} #{row[:title]} · #{row[:type]} · #{row[:matched].join("+")}"
|
|
187
|
+
@out.puts " #{truncate(row[:snippet], 100)}" unless row[:snippet].empty?
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def print_search_json(dir, terms, rows, options)
|
|
192
|
+
emit_list_json(dir, "matches", rows.map { |row| stringify(row) }, options, "query" => terms)
|
|
193
|
+
end
|
|
194
|
+
|
|
134
195
|
def server(argv)
|
|
135
196
|
require "okf/server/app"
|
|
136
197
|
|
|
@@ -159,6 +220,34 @@ module OKF
|
|
|
159
220
|
@runner.call(app, options[:bind], options[:port])
|
|
160
221
|
end
|
|
161
222
|
|
|
223
|
+
# The static counterpart to `server`: bake the whole bundle into one
|
|
224
|
+
# self-contained HTML file (bodies, catalog, index, logs baked in, no server
|
|
225
|
+
# needed — e.g. hosting on GitHub Pages). Prints to stdout unless -o is given.
|
|
226
|
+
def render(argv)
|
|
227
|
+
require "okf/server/app"
|
|
228
|
+
|
|
229
|
+
options = { output: nil, title: nil, link: nil, layout: "cose" }
|
|
230
|
+
parser = OptionParser.new do |o|
|
|
231
|
+
o.banner = "Usage: okf render <bundle-dir> [-o FILE] [--layout NAME] [-t title] [-l url]"
|
|
232
|
+
o.on("-o", "--output FILE", "write to FILE instead of stdout") { |v| options[:output] = v }
|
|
233
|
+
o.on("-t", "--title TITLE", "graph title (default: parent/bundle dir name)") { |v| options[:title] = v }
|
|
234
|
+
o.on("-l", "--link URL", "source URL shown in the header") { |v| options[:link] = v }
|
|
235
|
+
o.on("--layout NAME", OKF::Server::Graph::LAYOUTS, "initial layout (#{OKF::Server::Graph::LAYOUTS.join(", ")})") { |v| options[:layout] = v }
|
|
236
|
+
end
|
|
237
|
+
dir = positional_dir(parser, argv) or return 2
|
|
238
|
+
|
|
239
|
+
folder = OKF::Bundle::Folder.load(dir)
|
|
240
|
+
report_skipped(folder)
|
|
241
|
+
html = OKF::Server::App.new(folder, title: options[:title] || folder.name, link: options[:link], layout: options[:layout]).render_static
|
|
242
|
+
if options[:output]
|
|
243
|
+
File.write(options[:output], html)
|
|
244
|
+
@out.puts "wrote #{folder.graph(minimal: true).nodes.size} concepts to #{options[:output]}"
|
|
245
|
+
else
|
|
246
|
+
@out.print html
|
|
247
|
+
end
|
|
248
|
+
0
|
|
249
|
+
end
|
|
250
|
+
|
|
162
251
|
def graph(argv)
|
|
163
252
|
options = { json: false, minimal: false, body: true }
|
|
164
253
|
parser = OptionParser.new do |o|
|
|
@@ -801,13 +890,16 @@ module OKF
|
|
|
801
890
|
# Emit a list view's JSON envelope with --fields/--except projection applied to
|
|
802
891
|
# each item. Returns the verb's exit code (0, or 2 on a bad projection request —
|
|
803
892
|
# both flags at once, or a field name no item carries).
|
|
804
|
-
def emit_list_json(dir, key, items, options)
|
|
893
|
+
def emit_list_json(dir, key, items, options, extra = {})
|
|
805
894
|
return usage_error("--fields and --except are mutually exclusive") if options[:fields] && options[:except]
|
|
806
895
|
|
|
807
896
|
unknown = unknown_fields(items, options)
|
|
808
897
|
return usage_error("unknown field(s): #{unknown.join(", ")} (available: #{available_fields(items).join(", ")})") unless unknown.empty?
|
|
809
898
|
|
|
810
|
-
|
|
899
|
+
payload = { "bundle" => dir }.merge(extra)
|
|
900
|
+
payload["count"] = items.size
|
|
901
|
+
payload[key] = project(items, options)
|
|
902
|
+
emit_json(payload)
|
|
811
903
|
0
|
|
812
904
|
end
|
|
813
905
|
|
|
@@ -884,11 +976,13 @@ module OKF
|
|
|
884
976
|
|
|
885
977
|
skill <dest> [--here] [--force] install the companion agent skill
|
|
886
978
|
server <dir> [-p PORT] [--bind ADDR] [...] serve an interactive HTML graph
|
|
979
|
+
render <dir> [-o FILE] [--layout NAME] [...] write a static, self-contained HTML graph
|
|
887
980
|
|
|
888
981
|
lint <dir> [--json] [--fail-on warn] [...] report curation-quality issues
|
|
889
982
|
loose <dir> [--json] list files with no graph links, by folder
|
|
890
983
|
validate <dir> [--json] check OKF v0.1 conformance
|
|
891
984
|
|
|
985
|
+
search <dir> <term…> [-e] [--in FIELDS] [...] find concepts by text or regexp, ranked
|
|
892
986
|
index <dir> [--json] [--area A] [--no-body] the index map: dirs, their listings and rollups
|
|
893
987
|
stats <dir> [--json] bundle rollups (concepts, types, areas, links, tags)
|
|
894
988
|
types <dir> [--json] [filters] list types with their concepts, by count
|
|
@@ -903,7 +997,7 @@ module OKF
|
|
|
903
997
|
tags --by DIM regroups the tags per concept dimension — type or area — with
|
|
904
998
|
within-group counts, the view for curating a tag vocabulary.
|
|
905
999
|
--json emits compact JSON (the machine substrate); add --pretty to indent it.
|
|
906
|
-
--fields / --except project the JSON to the properties you want (index/catalog/files).
|
|
1000
|
+
--fields / --except project the JSON to the properties you want (search/index/catalog/files).
|
|
907
1001
|
|
|
908
1002
|
okf --version
|
|
909
1003
|
USAGE
|
data/lib/okf/server/app.rb
CHANGED
|
@@ -24,6 +24,12 @@ module OKF
|
|
|
24
24
|
# tags, timestamp, status, area, dir, links_*} ] } (JSON)
|
|
25
25
|
# GET /tags the tag index { tag => [id, …] } (JSON)
|
|
26
26
|
# GET /types the type index { type => [id, …] } (JSON)
|
|
27
|
+
# GET /index the §6 progressive-disclosure map for the Index panel:
|
|
28
|
+
# { directories: [ …okf-index rows… ] } (JSON, from the
|
|
29
|
+
# boot snapshot — authored maps are structure)
|
|
30
|
+
# GET /log the §7 history for the Log panel: { logs: [ {path,
|
|
31
|
+
# dir, content} ] } (JSON; content read live from disk,
|
|
32
|
+
# like a body — the log is the file that changes most)
|
|
27
33
|
class App
|
|
28
34
|
def initialize(folder, title: nil, link: nil, layout: "cose")
|
|
29
35
|
@folder = folder
|
|
@@ -43,10 +49,19 @@ module OKF
|
|
|
43
49
|
when "/catalog" then respond_json(catalog)
|
|
44
50
|
when "/tags" then respond_json(graph.tag_index)
|
|
45
51
|
when "/types" then respond_json(graph.type_index)
|
|
52
|
+
when "/index" then respond_json(directory_index)
|
|
53
|
+
when "/log" then respond_json(logs)
|
|
46
54
|
else not_found
|
|
47
55
|
end
|
|
48
56
|
end
|
|
49
57
|
|
|
58
|
+
# The same interactive page, but with the whole bundle baked in — bodies,
|
|
59
|
+
# catalog, index and logs — so it needs no server. This is what `okf render`
|
|
60
|
+
# writes: the fetch getters resolve from the embedded payload, not from here.
|
|
61
|
+
def render_static
|
|
62
|
+
Graph.new(graph, title: @title || @folder.name, link: @link, layout: @layout, embed: embed_payload).render
|
|
63
|
+
end
|
|
64
|
+
|
|
50
65
|
private
|
|
51
66
|
|
|
52
67
|
# The minimal graph snapshot taken at boot — drives the page and the indexes.
|
|
@@ -63,10 +78,54 @@ module OKF
|
|
|
63
78
|
{ concepts: @folder.catalog }
|
|
64
79
|
end
|
|
65
80
|
|
|
81
|
+
# The §6 map the Index panel renders — the same rows `okf index` prints,
|
|
82
|
+
# built by the pure OKF::Bundle#directory_index over the boot snapshot
|
|
83
|
+
# (authored index bodies are structure, read at load like the graph).
|
|
84
|
+
def directory_index
|
|
85
|
+
{ directories: @folder.directory_index }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Every log.md with its content, root scope first. Content is read live
|
|
89
|
+
# from disk so a just-appended entry shows without a restart; paths come
|
|
90
|
+
# from the loaded bundle, never from the request.
|
|
91
|
+
def logs
|
|
92
|
+
entries = @folder.bundle.log_files.sort_by { |path| [ path == "log.md" ? 0 : 1, path ] }
|
|
93
|
+
{ logs: entries.map { |path| { path: path, dir: File.dirname(path), content: log_content(path) } } }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def log_content(path)
|
|
97
|
+
File.read(File.join(@folder.root, path), encoding: "UTF-8")
|
|
98
|
+
rescue SystemCallError
|
|
99
|
+
@folder.bundle.reserved_content(path)
|
|
100
|
+
end
|
|
101
|
+
|
|
66
102
|
def page
|
|
67
103
|
@page ||= Graph.new(graph, title: @title || @folder.name, link: @link, layout: @layout).render
|
|
68
104
|
end
|
|
69
105
|
|
|
106
|
+
# Everything the on-demand endpoints would serve, baked for render mode. The
|
|
107
|
+
# arrays match what each client getter extracts from the JSON envelope; the
|
|
108
|
+
# per-concept maps mirror /node (raw, unstripped body) and /node/meta (the
|
|
109
|
+
# same escaped fragment). Read from the in-memory bundle — no live disk read,
|
|
110
|
+
# since a static file is a snapshot, not a window on edits.
|
|
111
|
+
def embed_payload
|
|
112
|
+
{
|
|
113
|
+
catalog: @folder.catalog,
|
|
114
|
+
index: @folder.directory_index,
|
|
115
|
+
logs: logs[:logs],
|
|
116
|
+
bodies: bodies,
|
|
117
|
+
meta: meta
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def bodies
|
|
122
|
+
@folder.bundle.concepts.each_with_object({}) { |concept, map| map[concept.id] = concept.body.to_s }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def meta
|
|
126
|
+
@folder.bundle.concepts.each_with_object({}) { |concept, map| map[concept.id] = description_fragment(concept) }
|
|
127
|
+
end
|
|
128
|
+
|
|
70
129
|
def node_body(id)
|
|
71
130
|
concept = concept_for(id)
|
|
72
131
|
return not_found if concept.nil?
|