okf 1.10.0 → 1.12.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 +294 -0
- data/README.md +310 -445
- data/lib/okf/bundle/folder.rb +24 -5
- data/lib/okf/bundle/linter.rb +1 -1
- data/lib/okf/bundle/search/index.rb +13 -3
- data/lib/okf/bundle/search.rb +93 -13
- data/lib/okf/bundle/skeleton.rb +241 -0
- data/lib/okf/bundle.rb +19 -14
- data/lib/okf/cli/catalog.rb +6 -6
- data/lib/okf/cli/command.rb +241 -14
- data/lib/okf/cli/dirs.rb +118 -0
- data/lib/okf/cli/files.rb +2 -2
- data/lib/okf/cli/graph.rb +115 -9
- data/lib/okf/cli/index.rb +67 -25
- data/lib/okf/cli/loose.rb +2 -2
- data/lib/okf/cli/registry.rb +152 -15
- data/lib/okf/cli/render.rb +3 -2
- data/lib/okf/cli/search.rb +33 -2
- data/lib/okf/cli/server.rb +16 -5
- data/lib/okf/cli/stats.rb +36 -11
- data/lib/okf/cli/tags.rb +29 -7
- data/lib/okf/cli/types.rb +1 -1
- data/lib/okf/cli.rb +10 -6
- data/lib/okf/registry.rb +351 -20
- data/lib/okf/render/graph/template.html.erb +504 -103
- data/lib/okf/render/graph.rb +27 -3
- data/lib/okf/server/app.rb +74 -3
- data/lib/okf/server/hub.rb +40 -30
- data/lib/okf/skill/SKILL.md +17 -10
- 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 +3 -2
- data/lib/okf/skill/playbooks/refine.md +30 -3
- data/lib/okf/skill/playbooks/search.md +7 -7
- data/lib/okf/skill/reference/cli.md +171 -32
- data/lib/okf/version.rb +1 -1
- data/lib/okf.rb +10 -0
- metadata +21 -8
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
|
|
|
@@ -41,44 +41,69 @@ module OKF
|
|
|
41
41
|
graph = folder.graph(minimal: true)
|
|
42
42
|
entries = folder.catalog
|
|
43
43
|
by_type = graph.type_index.transform_values(&:size).sort_by { |_, n| -n }.to_h
|
|
44
|
-
|
|
44
|
+
by_top_dir = entries.group_by { |entry| entry[:top_dir] }.transform_values(&:size).sort_by { |_, n| -n }.to_h
|
|
45
|
+
by_dir = directory_counts(folder)
|
|
45
46
|
{
|
|
46
47
|
concepts: entries.size,
|
|
47
|
-
|
|
48
|
+
dirs: by_dir.size,
|
|
49
|
+
top_dirs: by_top_dir.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,
|
|
52
|
-
|
|
54
|
+
by_dir: by_dir,
|
|
55
|
+
by_top_dir: by_top_dir
|
|
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_top_dir` stays in --json (the
|
|
87
|
+
# first-segment rollup) but a screen that printed both it and `by_dir`
|
|
88
|
+
# would double up on one idea, so the human view shows the full-path cut.
|
|
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], "
|
|
104
|
+
"concepts" => stats[:concepts], "dirs" => stats[:dirs], "top_dirs" => stats[:top_dirs],
|
|
80
105
|
"concept_types" => stats[:types], "cross_links" => stats[:cross_links], "distinct_tags" => stats[:tags],
|
|
81
|
-
"by_type" => stats[:by_type], "
|
|
106
|
+
"by_type" => stats[:by_type], "by_dir" => stats[:by_dir], "by_top_dir" => stats[:by_top_dir]
|
|
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[:top_dir]
|
|
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
|
|
35
|
+
"matches" => %w[id title type dir top_dir 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
|
|
42
|
-
"concepts" => %w[id title type description tags timestamp status backlog_ref dir
|
|
41
|
+
"matches_by_ref" => %w[slug id title type dir top_dir tags matched score snippet],
|
|
42
|
+
"concepts" => %w[id title type description tags timestamp status backlog_ref dir top_dir 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"
|