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.
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, types, areas, links, tags — in one screen.
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, types, areas, links, tags)" ]
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
- by_area = entries.group_by { |entry| entry[:area] }.transform_values(&:size).sort_by { |_, n| -n }.to_h
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
- areas: by_area.size,
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
- by_area: by_area
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 " areas #{stats[:areas]}"
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
- print_stat_breakdown("By area", stats[:by_area])
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
- width = counts.keys.map(&:length).max
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.each { |label, count| @out.puts " #{label.ljust(width)} #{count}" }
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], "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], "by_area" => stats[:by_area]
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|area] [--type T] [--area A] [--json]"
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 | area)") { |v| options[:by] = v.to_sym }
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|area`: the tag index re-cut per concept type or top-level
42
- # area, with within-group counts — the curation view. A tag confined to one
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/--area filters narrow the concepts first, then the grouping cuts.
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] == :type ? entry_type(entry) : entry[:area]
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 = dim == :area && key != "(root)" ? "#{key}/" : key
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> [--area A] [--tag T] [--json]"
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 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 area tags matched score snippet],
42
- "concepts" => %w[id title type description tags timestamp status backlog_ref dir area links_out links_in],
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, --area AREA, --tag TAG
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
- tags --by DIM regroups the tags per concept dimension — type or area — with
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"