okf 1.10.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 +203 -0
- data/README.md +238 -413
- data/lib/okf/bundle/folder.rb +20 -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 +1 -1
- data/lib/okf/cli/catalog.rb +1 -1
- data/lib/okf/cli/command.rb +169 -7
- data/lib/okf/cli/dirs.rb +118 -0
- data/lib/okf/cli/files.rb +2 -2
- data/lib/okf/cli/index.rb +67 -25
- data/lib/okf/cli/loose.rb +2 -2
- data/lib/okf/cli/search.rb +1 -1
- data/lib/okf/cli/server.rb +8 -1
- data/lib/okf/cli/stats.rb +33 -8
- data/lib/okf/cli/tags.rb +29 -7
- data/lib/okf/cli/types.rb +1 -1
- data/lib/okf/cli.rb +9 -5
- data/lib/okf/render/graph/template.html.erb +293 -96
- data/lib/okf/server/app.rb +61 -0
- data/lib/okf/server/hub.rb +35 -27
- data/lib/okf/skill/SKILL.md +12 -9
- data/lib/okf/skill/playbooks/consume.md +3 -3
- data/lib/okf/skill/playbooks/maintain.md +4 -3
- data/lib/okf/skill/playbooks/menu.md +1 -1
- data/lib/okf/skill/playbooks/refine.md +4 -3
- data/lib/okf/skill/playbooks/search.md +7 -7
- data/lib/okf/skill/reference/cli.md +100 -18
- data/lib/okf/version.rb +1 -1
- data/lib/okf.rb +9 -0
- metadata +20 -8
data/lib/okf/cli/index.rb
CHANGED
|
@@ -5,9 +5,15 @@ module OKF
|
|
|
5
5
|
# The progressive-disclosure map (spec §6): every directory that holds concepts
|
|
6
6
|
# or carries an index.md, with its authored index body, a type/tag rollup, its
|
|
7
7
|
# child directories, and — for a directory with no index.md — the listing
|
|
8
|
-
# synthesized from the concepts there. The "orient before you read" view. `--
|
|
9
|
-
# is repeatable
|
|
10
|
-
#
|
|
8
|
+
# synthesized from the concepts there. The "orient before you read" view. `--dir`
|
|
9
|
+
# is repeatable and selects a directory *and its subtree* (`root` is the bundle
|
|
10
|
+
# root), `--depth N` bounds how far below the starting point that goes, and
|
|
11
|
+
# `--no-body` drops the prose to a skeleton; advisory, exit 0.
|
|
12
|
+
#
|
|
13
|
+
# The two narrowings are what make the map usable on a deep bundle: every
|
|
14
|
+
# directory is a section, so a few hundred concepts is a map nobody reads at
|
|
15
|
+
# once. `--depth 1` is the top of the tree, `--dir X --depth 1` is one branch
|
|
16
|
+
# of it, and the pair walks down a level at a time.
|
|
11
17
|
class Index < Command
|
|
12
18
|
def self.id
|
|
13
19
|
:index
|
|
@@ -19,26 +25,47 @@ module OKF
|
|
|
19
25
|
|
|
20
26
|
def self.help_rows
|
|
21
27
|
[
|
|
22
|
-
[ "index <dir|@slug> [--
|
|
28
|
+
[ "index <dir|@slug> [--dir D] [--depth N] [--no-body]", "the index map: dirs, their listings and rollups" ]
|
|
23
29
|
]
|
|
24
30
|
end
|
|
25
31
|
|
|
26
32
|
def call(argv)
|
|
27
|
-
options = { json: false, body: true, areas: nil }
|
|
33
|
+
options = { json: false, body: true, dirs: nil, areas: nil, depth: nil, ancestors: true }
|
|
28
34
|
parser = OptionParser.new do |o|
|
|
29
|
-
o.banner = "Usage: okf index <dir|@slug> [--
|
|
35
|
+
o.banner = "Usage: okf index <dir|@slug> [--dir PATH] [--depth N] [--no-body] [--json]"
|
|
30
36
|
json_flags(o, options, "emit the index map as JSON")
|
|
31
37
|
projection_flags(o, options)
|
|
32
|
-
o.on("--
|
|
38
|
+
o.on("--dir PATH", "only this directory and the ones below it",
|
|
39
|
+
"(repeatable; `root` for the bundle root)") { |v| (options[:dirs] ||= []) << v }
|
|
40
|
+
depth_flag(o, options)
|
|
41
|
+
ancestors_flag(o, options)
|
|
42
|
+
o.on("--area AREA", "deprecated: use --dir (this directory exactly)") do |v|
|
|
43
|
+
(options[:areas] ||= []) << v
|
|
44
|
+
deprecated("--area", "--dir")
|
|
45
|
+
end
|
|
33
46
|
o.on("--[no-]body", "include each index's prose body (default: yes)") { |v| options[:body] = v }
|
|
34
47
|
help_flag(o)
|
|
35
48
|
end
|
|
36
49
|
dir = positional_dir(parser, argv) or return 2
|
|
50
|
+
bad_depth = depth_error(options)
|
|
51
|
+
return bad_depth if bad_depth
|
|
52
|
+
# --area is exact and names no starting point, so --depth has nothing to
|
|
53
|
+
# be relative *to*: the pair used to union the area with every directory
|
|
54
|
+
# at that depth from the root. Refusing beats answering with more.
|
|
55
|
+
#
|
|
56
|
+
# --dir is refused for the same reason and not a weaker one: one flag is
|
|
57
|
+
# exact and the other a prefix, so the pair came back with the area *and*
|
|
58
|
+
# the subtree — an answer to neither question, from a combination only
|
|
59
|
+
# someone mid-migration would type. A deprecated flag that quietly widens
|
|
60
|
+
# is worse than one that is merely old.
|
|
61
|
+
if options[:areas] && (options[:depth] || options[:dirs])
|
|
62
|
+
return usage_error("--area and #{options[:dirs] ? "--dir" : "--depth"} do not combine: use --dir")
|
|
63
|
+
end
|
|
37
64
|
|
|
38
65
|
folder = OKF::Bundle::Folder.load(dir)
|
|
39
66
|
report_skipped(folder)
|
|
40
67
|
entries = folder.directory_index
|
|
41
|
-
selected = select_directories(entries, options
|
|
68
|
+
selected, chain = select_directories(entries, options)
|
|
42
69
|
if options[:json]
|
|
43
70
|
# --no-body is shorthand for --except body, so asking for the body by
|
|
44
71
|
# name in the same breath is a contradiction. Letting --fields quietly
|
|
@@ -48,30 +75,44 @@ module OKF
|
|
|
48
75
|
end
|
|
49
76
|
|
|
50
77
|
options[:except] = Array(options[:except]) + [ "body" ] unless options[:body] || options[:fields]
|
|
51
|
-
return print_index_map_json(dir, selected, options)
|
|
78
|
+
return print_index_map_json(dir, selected, chain, options)
|
|
52
79
|
end
|
|
53
|
-
print_index_map(dir, selected, options[:body])
|
|
80
|
+
print_index_map(dir, selected, chain, options[:body])
|
|
54
81
|
0
|
|
55
82
|
end
|
|
56
83
|
|
|
57
84
|
private
|
|
58
85
|
|
|
59
|
-
# Narrow the map
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
86
|
+
# Narrow the map — case-insensitive, `root` matching the bundle root (".").
|
|
87
|
+
# --dir takes the named directory *and its subtree* and --depth bounds how
|
|
88
|
+
# far below the starting point that reaches, both through the shared
|
|
89
|
+
# select_dirs so the whole CLI answers "which directories?" one way. The
|
|
90
|
+
# deprecated --area keeps its old exact match beside them, because a
|
|
91
|
+
# deprecated flag that quietly widens is worse than one that is merely old.
|
|
92
|
+
# Nothing passed keeps the whole map.
|
|
93
|
+
def select_directories(entries, options)
|
|
94
|
+
areas = Array(options[:areas]).map { |area| fold_dir(area) }
|
|
95
|
+
scoped = !options[:dirs].nil? || !options[:depth].nil?
|
|
96
|
+
return [ entries, [] ] if areas.empty? && !scoped
|
|
97
|
+
|
|
98
|
+
all_dirs = entries.map { |entry| entry[:dir] }
|
|
99
|
+
wanted = scoped ? select_dirs(all_dirs, options) : []
|
|
100
|
+
chain = ancestor_dirs(options, all_dirs) - wanted
|
|
101
|
+
selected = entries.select do |entry|
|
|
102
|
+
areas.include?(fold(entry[:dir])) || wanted.include?(entry[:dir]) || chain.include?(entry[:dir])
|
|
103
|
+
end
|
|
104
|
+
[ selected, chain ]
|
|
67
105
|
end
|
|
68
106
|
|
|
69
|
-
def print_index_map(dir, entries, body)
|
|
107
|
+
def print_index_map(dir, entries, chain, body)
|
|
70
108
|
noun = entries.size == 1 ? "directory" : "directories"
|
|
71
109
|
@out.puts "Index map — #{bundle_label(dir)} (#{entries.size} #{noun})"
|
|
72
110
|
entries.each do |entry|
|
|
73
111
|
@out.puts
|
|
74
|
-
|
|
112
|
+
# ↑ marks a row the reader did not ask for: it is here to place the
|
|
113
|
+
# branch, not to answer about it.
|
|
114
|
+
up = chain.include?(entry[:dir]) ? "↑ " : ""
|
|
115
|
+
@out.puts " #{up}#{index_dir_label(entry)}#{index_dir_meta(entry)}"
|
|
75
116
|
subdirs = entry[:subdirs]
|
|
76
117
|
@out.puts " → #{subdirs.map { |sub| "#{File.basename(sub)}/" }.join(" ")}" unless subdirs.empty?
|
|
77
118
|
if entry[:present]
|
|
@@ -83,7 +124,7 @@ module OKF
|
|
|
83
124
|
end
|
|
84
125
|
|
|
85
126
|
def index_dir_label(entry)
|
|
86
|
-
base = entry[:dir]
|
|
127
|
+
base = dir_label(entry[:dir], slash: true)
|
|
87
128
|
entry[:present] ? base : "#{base} (no index.md)"
|
|
88
129
|
end
|
|
89
130
|
|
|
@@ -107,13 +148,14 @@ module OKF
|
|
|
107
148
|
end
|
|
108
149
|
end
|
|
109
150
|
|
|
110
|
-
def print_index_map_json(dir, entries, options)
|
|
111
|
-
|
|
151
|
+
def print_index_map_json(dir, entries, chain, options)
|
|
152
|
+
rows = entries.map { |entry| index_map_entry_json(entry, chain.include?(entry[:dir])) }
|
|
153
|
+
emit_list_json(dir, "directories", rows, options)
|
|
112
154
|
end
|
|
113
155
|
|
|
114
|
-
def index_map_entry_json(entry)
|
|
156
|
+
def index_map_entry_json(entry, ancestor)
|
|
115
157
|
{
|
|
116
|
-
"dir" => entry[:dir], "index_path" => entry[:index_path],
|
|
158
|
+
"dir" => entry[:dir], "ancestor" => ancestor, "index_path" => entry[:index_path],
|
|
117
159
|
"present" => entry[:present], "synthesized" => entry[:synthesized],
|
|
118
160
|
"count" => entry[:count], "types" => entry[:types], "tags" => entry[:tags],
|
|
119
161
|
"subdirs" => entry[:subdirs], "body" => entry[:body],
|
data/lib/okf/cli/loose.rb
CHANGED
|
@@ -44,7 +44,7 @@ module OKF
|
|
|
44
44
|
def loose_files(graph)
|
|
45
45
|
titles = graph.nodes.map { |node| [ node[:id], node[:title] ] }.to_h
|
|
46
46
|
graph.unlinked_ids
|
|
47
|
-
.map { |id| { id: id, title: titles[id], dir:
|
|
47
|
+
.map { |id| { id: id, title: titles[id], dir: OKF.dir_of(id) } }
|
|
48
48
|
.sort_by { |file| file[:id] }
|
|
49
49
|
end
|
|
50
50
|
|
|
@@ -58,7 +58,7 @@ module OKF
|
|
|
58
58
|
files.group_by { |file| file[:dir] }.sort_by(&:first).each do |folder, group|
|
|
59
59
|
width = group.map { |file| File.basename("#{file[:id]}.md").length }.max
|
|
60
60
|
@out.puts
|
|
61
|
-
@out.puts " #{folder
|
|
61
|
+
@out.puts " #{dir_label(folder, slash: true)}"
|
|
62
62
|
group.each do |file|
|
|
63
63
|
@out.puts " #{File.basename("#{file[:id]}.md").ljust(width)} #{file[:title]}"
|
|
64
64
|
end
|
data/lib/okf/cli/search.rb
CHANGED
|
@@ -34,7 +34,7 @@ module OKF
|
|
|
34
34
|
def call(argv)
|
|
35
35
|
options = { json: false, regexp: false, fuzzy: false, engine: nil }
|
|
36
36
|
parser = OptionParser.new do |o|
|
|
37
|
-
o.banner = "Usage: okf search <dir|@slug…|@all> <term…> [--engine NAME] [--regexp|--fuzzy] [--in FIELDS] [--type T] [--
|
|
37
|
+
o.banner = "Usage: okf search <dir|@slug…|@all> <term…> [--engine NAME] [--regexp|--fuzzy] [--in FIELDS] [--type T] [--dir D] [--tag T] [--json]"
|
|
38
38
|
search_engine_note(o)
|
|
39
39
|
json_flags(o, options, "emit the matches as JSON")
|
|
40
40
|
projection_flags(o, options)
|
data/lib/okf/cli/server.rb
CHANGED
|
@@ -59,10 +59,16 @@ module OKF
|
|
|
59
59
|
# Build the single-bundle Rack app and hand it to the runner (WEBrick by
|
|
60
60
|
# default, injected so tests drive this without a socket).
|
|
61
61
|
def run_server(folder, options)
|
|
62
|
-
|
|
62
|
+
# search_endpoint is named here rather than defaulted in App: the page
|
|
63
|
+
# resolves it against the URL the reader is on, and this is the layer that
|
|
64
|
+
# knows the app is mounted at the root. An embedding host mounting App
|
|
65
|
+
# elsewhere passes its own.
|
|
66
|
+
app = OKF::Server::App.new(folder, title: options[:title] || folder.name, link: options[:link],
|
|
67
|
+
layout: options[:layout], search_endpoint: "search")
|
|
63
68
|
# minimal: the banner wants a count, not bodies — and Folder#graph is not
|
|
64
69
|
# memoized, so a full build here parses every concept a second time (the
|
|
65
70
|
# App builds its own) purely to print one number.
|
|
71
|
+
app.warm_search
|
|
66
72
|
count = folder.graph(minimal: true).nodes.size
|
|
67
73
|
@out.puts "serving #{count} #{pluralize(count, "concept")} at http://#{options[:bind]}:#{options[:port]} (Ctrl-C to stop)"
|
|
68
74
|
serve(app, options)
|
|
@@ -92,6 +98,7 @@ module OKF
|
|
|
92
98
|
# could not host — a folder deleted out from under one is the question
|
|
93
99
|
# "where did my bundle go?", and only the registry can answer it.
|
|
94
100
|
hub = OKF::Server::Hub.new(bundles, layout: options[:layout], registry: reg, writable: writable?(options))
|
|
101
|
+
hub.warm_search
|
|
95
102
|
concepts = bundles.inject(0) { |sum, bundle| sum + bundle.folder.graph(minimal: true).nodes.size }
|
|
96
103
|
@out.puts "serving #{bundles.size} #{pluralize(bundles.size,
|
|
97
104
|
"bundle")}, #{concepts} #{pluralize(concepts, "concept")} at http://#{options[:bind]}:#{options[:port]} (Ctrl-C to stop)"
|
data/lib/okf/cli/stats.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module OKF
|
|
4
4
|
class CLI
|
|
5
|
-
# Bundle rollups — concepts,
|
|
5
|
+
# Bundle rollups — concepts, dirs, types, links, tags — in one screen.
|
|
6
6
|
class Stats < Command
|
|
7
7
|
def self.id
|
|
8
8
|
:stats
|
|
@@ -14,7 +14,7 @@ module OKF
|
|
|
14
14
|
|
|
15
15
|
def self.help_rows
|
|
16
16
|
[
|
|
17
|
-
[ "stats <dir|@slug> [--json]", "bundle rollups (concepts,
|
|
17
|
+
[ "stats <dir|@slug> [--json]", "bundle rollups (concepts, dirs, types, links, tags)" ]
|
|
18
18
|
]
|
|
19
19
|
end
|
|
20
20
|
|
|
@@ -42,43 +42,68 @@ module OKF
|
|
|
42
42
|
entries = folder.catalog
|
|
43
43
|
by_type = graph.type_index.transform_values(&:size).sort_by { |_, n| -n }.to_h
|
|
44
44
|
by_area = entries.group_by { |entry| entry[:area] }.transform_values(&:size).sort_by { |_, n| -n }.to_h
|
|
45
|
+
by_dir = directory_counts(folder)
|
|
45
46
|
{
|
|
46
47
|
concepts: entries.size,
|
|
48
|
+
dirs: by_dir.size,
|
|
47
49
|
areas: by_area.size,
|
|
48
50
|
types: by_type.size,
|
|
49
51
|
cross_links: graph.edges.size,
|
|
50
52
|
tags: graph.tag_index.size,
|
|
51
53
|
by_type: by_type,
|
|
54
|
+
by_dir: by_dir,
|
|
52
55
|
by_area: by_area
|
|
53
56
|
}
|
|
54
57
|
end
|
|
55
58
|
|
|
59
|
+
# Every directory the bundle has, with the concepts that live *directly* in
|
|
60
|
+
# it. Read off Bundle#directory_index — the same map `okf dirs` lists and
|
|
61
|
+
# `--dir` is answered against — rather than off the catalog, which knows
|
|
62
|
+
# only the directories that happen to hold a concept. Grouping the catalog
|
|
63
|
+
# made `stats` and `dirs` report different totals for one bundle, and left
|
|
64
|
+
# an addressable directory out of by_dir entirely: `--dir deeply` answers,
|
|
65
|
+
# but nothing in `stats` said `deeply` was there to ask about.
|
|
66
|
+
#
|
|
67
|
+
# A directory holding nothing directly therefore appears at 0. That is the
|
|
68
|
+
# honest reading — it is the same zero `okf dirs` prints in its Concepts
|
|
69
|
+
# column — and it keeps `dirs` equal to `by_dir.size`. Ties break by path so
|
|
70
|
+
# the order is total, not whatever the sort happened to leave.
|
|
71
|
+
def directory_counts(folder)
|
|
72
|
+
folder.directory_index
|
|
73
|
+
.map { |entry| [ entry[:dir], entry[:count] ] }
|
|
74
|
+
.sort_by { |dir, count| [ -count, dir ] }.to_h
|
|
75
|
+
end
|
|
76
|
+
|
|
56
77
|
def print_stats(dir, stats)
|
|
57
78
|
@out.puts "Stats — #{bundle_label(dir)}"
|
|
58
79
|
@out.puts
|
|
59
80
|
@out.puts " concepts #{stats[:concepts]}"
|
|
60
|
-
@out.puts "
|
|
81
|
+
@out.puts " dirs #{stats[:dirs]}"
|
|
61
82
|
@out.puts " concept types #{stats[:types]}"
|
|
62
83
|
@out.puts " cross-links #{stats[:cross_links]}"
|
|
63
84
|
@out.puts " distinct tags #{stats[:tags]}"
|
|
64
85
|
print_stat_breakdown("By type", stats[:by_type])
|
|
65
|
-
|
|
86
|
+
# One grouping word in the human view: `by_area` stays in --json for the
|
|
87
|
+
# deprecation window, but a screen that printed both would be teaching the
|
|
88
|
+
# vocabulary the rest of this change is retiring.
|
|
89
|
+
print_stat_breakdown("By dir", stats[:by_dir]) { |label| dir_label(label) }
|
|
66
90
|
end
|
|
67
91
|
|
|
68
92
|
def print_stat_breakdown(title, counts)
|
|
69
93
|
return if counts.empty?
|
|
70
94
|
|
|
71
|
-
|
|
95
|
+
labels = counts.keys.map { |key| block_given? ? yield(key) : key }
|
|
96
|
+
width = labels.map(&:length).max
|
|
72
97
|
@out.puts
|
|
73
98
|
@out.puts " #{title}"
|
|
74
|
-
counts.
|
|
99
|
+
counts.each_with_index { |(_, count), i| @out.puts " #{labels[i].ljust(width)} #{count}" }
|
|
75
100
|
end
|
|
76
101
|
|
|
77
102
|
def print_stats_json(dir, stats)
|
|
78
103
|
emit_json(bundle_head(dir).merge(
|
|
79
|
-
"concepts" => stats[:concepts], "areas" => stats[:areas],
|
|
104
|
+
"concepts" => stats[:concepts], "dirs" => stats[:dirs], "areas" => stats[:areas],
|
|
80
105
|
"concept_types" => stats[:types], "cross_links" => stats[:cross_links], "distinct_tags" => stats[:tags],
|
|
81
|
-
"by_type" => stats[:by_type], "by_area" => stats[:by_area]
|
|
106
|
+
"by_type" => stats[:by_type], "by_dir" => stats[:by_dir], "by_area" => stats[:by_area]
|
|
82
107
|
))
|
|
83
108
|
end
|
|
84
109
|
end
|
data/lib/okf/cli/tags.rb
CHANGED
|
@@ -23,9 +23,12 @@ module OKF
|
|
|
23
23
|
def call(argv)
|
|
24
24
|
options = { json: false, by: nil }
|
|
25
25
|
parser = OptionParser.new do |o|
|
|
26
|
-
o.banner = "Usage: okf tags <dir|@slug> [--by type|
|
|
26
|
+
o.banner = "Usage: okf tags <dir|@slug> [--by type|dir] [--type T] [--dir D] [--json]"
|
|
27
27
|
json_flags(o, options, "emit the tag index as JSON")
|
|
28
|
-
o.on("--by DIM", %w[type area], "group the tags by a concept dimension (type |
|
|
28
|
+
o.on("--by DIM", %w[type dir area], "group the tags by a concept dimension (type | dir)") do |v|
|
|
29
|
+
options[:by] = v.to_sym
|
|
30
|
+
deprecated("--by area", "--by dir") if options[:by] == :area
|
|
31
|
+
end
|
|
29
32
|
filter_flags(o, options, :type, :area)
|
|
30
33
|
help_flag(o)
|
|
31
34
|
end
|
|
@@ -38,10 +41,10 @@ module OKF
|
|
|
38
41
|
|
|
39
42
|
private
|
|
40
43
|
|
|
41
|
-
# `tags --by type|
|
|
42
|
-
#
|
|
44
|
+
# `tags --by type|dir`: the tag index re-cut per concept type or directory,
|
|
45
|
+
# with within-group counts — the curation view. A tag confined to one
|
|
43
46
|
# group at count 1 is scattered; one recurring across groups is connective.
|
|
44
|
-
# The --type/--
|
|
47
|
+
# The --type/--dir filters narrow the concepts first, then the grouping cuts.
|
|
45
48
|
def grouped_tags(dir, options)
|
|
46
49
|
folder = OKF::Bundle::Folder.load(dir)
|
|
47
50
|
report_skipped(folder)
|
|
@@ -66,7 +69,7 @@ module OKF
|
|
|
66
69
|
entry = by_id[id]
|
|
67
70
|
next if entry.nil?
|
|
68
71
|
|
|
69
|
-
key = options[:by]
|
|
72
|
+
key = group_key(entry, options[:by])
|
|
70
73
|
((groups[key] ||= {})[tag] ||= []) << id
|
|
71
74
|
totals[tag] += 1
|
|
72
75
|
end
|
|
@@ -83,10 +86,29 @@ module OKF
|
|
|
83
86
|
OKF.blank?(entry[:type]) ? "Untyped" : entry[:type]
|
|
84
87
|
end
|
|
85
88
|
|
|
89
|
+
# The group a concept falls in, in its *stored* spelling — `.` for the root
|
|
90
|
+
# under --by dir, never "(root)". The human label is applied at print time,
|
|
91
|
+
# so the JSON and the table cannot disagree about which one is the data.
|
|
92
|
+
def group_key(entry, dim)
|
|
93
|
+
case dim
|
|
94
|
+
when :type then entry_type(entry)
|
|
95
|
+
when :dir then entry[:dir]
|
|
96
|
+
else entry[:area]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# `.` prints "(root)" bare; every other dir carries the trailing slash that
|
|
101
|
+
# says it is one. The deprecated --by area already stores "(root)" itself.
|
|
102
|
+
def group_label(key, dim)
|
|
103
|
+
return key if dim == :type
|
|
104
|
+
|
|
105
|
+
dir_label(key, slash: true)
|
|
106
|
+
end
|
|
107
|
+
|
|
86
108
|
def print_grouped_tags(dir, dim, groups, titles)
|
|
87
109
|
@out.puts "Tags — #{bundle_label(dir)} (#{distinct_tags(groups)} distinct, by #{dim})"
|
|
88
110
|
groups.each do |key, rows|
|
|
89
|
-
label =
|
|
111
|
+
label = group_label(key, dim)
|
|
90
112
|
@out.puts
|
|
91
113
|
@out.puts " #{label} (#{rows.size} #{pluralize(rows.size, "tag")})"
|
|
92
114
|
width = rows.map { |row| row[:tag].length }.max || 0
|
data/lib/okf/cli/types.rb
CHANGED
|
@@ -21,7 +21,7 @@ module OKF
|
|
|
21
21
|
def call(argv)
|
|
22
22
|
options = { json: false }
|
|
23
23
|
parser = OptionParser.new do |o|
|
|
24
|
-
o.banner = "Usage: okf types <dir|@slug> [--
|
|
24
|
+
o.banner = "Usage: okf types <dir|@slug> [--dir D] [--tag T] [--json]"
|
|
25
25
|
json_flags(o, options, "emit the type index as JSON")
|
|
26
26
|
filter_flags(o, options, :area, :tag)
|
|
27
27
|
help_flag(o)
|
data/lib/okf/cli.rb
CHANGED
|
@@ -32,16 +32,17 @@ module OKF
|
|
|
32
32
|
# Declared in emission order, so the "available:" list a typo prints reads
|
|
33
33
|
# the same as the rows themselves.
|
|
34
34
|
ROW_FIELDS = {
|
|
35
|
-
"matches" => %w[id title type area tags matched score snippet],
|
|
35
|
+
"matches" => %w[id title type dir area tags matched score snippet],
|
|
36
36
|
# Registry mode labels every row with the bundle it came from; a plain-dir
|
|
37
37
|
# search has one bundle and no slug to carry. Two shapes, because the typo
|
|
38
38
|
# guard checks against the *declared* one — a single shape covering both
|
|
39
39
|
# would let `--fields slug` pass on a search whose rows have none, and hand
|
|
40
40
|
# back an empty object per match under a count that says otherwise.
|
|
41
|
-
"matches_by_ref" => %w[slug id title type area tags matched score snippet],
|
|
41
|
+
"matches_by_ref" => %w[slug id title type dir area tags matched score snippet],
|
|
42
42
|
"concepts" => %w[id title type description tags timestamp status backlog_ref dir area links_out links_in],
|
|
43
43
|
"files" => %w[path id dir type title description],
|
|
44
|
-
"directories" => %w[dir index_path present synthesized count types tags subdirs body listing],
|
|
44
|
+
"directories" => %w[dir ancestor index_path present synthesized count types tags subdirs body listing],
|
|
45
|
+
"dirs" => %w[dir ancestor count subtree subdirs],
|
|
45
46
|
"bundles" => %w[slug title dir mount default missing]
|
|
46
47
|
}.freeze
|
|
47
48
|
|
|
@@ -103,9 +104,11 @@ module OKF
|
|
|
103
104
|
search spans bundles: several leading @slugs, or @all for every registered one
|
|
104
105
|
(@all skips a bundle whose directory is gone; a named @slug insists on it).
|
|
105
106
|
|
|
106
|
-
[filters] narrow a view to matching concepts: --type TYPE, --
|
|
107
|
+
[filters] narrow a view to matching concepts: --type TYPE, --dir PATH, --tag TAG
|
|
107
108
|
(each view takes the ones orthogonal to it; matching is case-insensitive).
|
|
108
|
-
|
|
109
|
+
--dir takes a directory and everything below it; `root` (or `.`) is the bundle
|
|
110
|
+
root. It replaces --area, which still works, warns, and matches one segment.
|
|
111
|
+
tags --by DIM regroups the tags per concept dimension — type or dir — with
|
|
109
112
|
within-group counts, the view for curating a tag vocabulary.
|
|
110
113
|
--json emits compact JSON (the machine substrate); add --pretty to indent it.
|
|
111
114
|
--fields / --except project the JSON to the properties you want (search/index/catalog/files).
|
|
@@ -497,6 +500,7 @@ require "okf/cli/loose"
|
|
|
497
500
|
require "okf/cli/validate"
|
|
498
501
|
require "okf/cli/search"
|
|
499
502
|
require "okf/cli/index"
|
|
503
|
+
require "okf/cli/dirs"
|
|
500
504
|
require "okf/cli/stats"
|
|
501
505
|
require "okf/cli/types"
|
|
502
506
|
require "okf/cli/tags"
|