rigortype 0.2.1 → 0.2.3

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.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -14
  3. data/docs/handbook/01-getting-started.md +311 -0
  4. data/docs/handbook/02-everyday-types.md +337 -0
  5. data/docs/handbook/03-narrowing.md +359 -0
  6. data/docs/handbook/04-tuples-and-shapes.md +321 -0
  7. data/docs/handbook/05-methods-and-blocks.md +339 -0
  8. data/docs/handbook/06-classes.md +305 -0
  9. data/docs/handbook/07-rbs-and-extended.md +427 -0
  10. data/docs/handbook/08-understanding-errors.md +373 -0
  11. data/docs/handbook/09-plugins.md +241 -0
  12. data/docs/handbook/10-sorbet.md +347 -0
  13. data/docs/handbook/11-sig-gen.md +312 -0
  14. data/docs/handbook/12-lightweight-hkt.md +333 -0
  15. data/docs/handbook/README.md +275 -0
  16. data/docs/handbook/appendix-elixir.md +370 -0
  17. data/docs/handbook/appendix-go.md +399 -0
  18. data/docs/handbook/appendix-java-csharp.md +470 -0
  19. data/docs/handbook/appendix-liskov.md +580 -0
  20. data/docs/handbook/appendix-mypy.md +370 -0
  21. data/docs/handbook/appendix-phpstan.md +338 -0
  22. data/docs/handbook/appendix-protocols-and-structural-typing.md +292 -0
  23. data/docs/handbook/appendix-rust.md +446 -0
  24. data/docs/handbook/appendix-steep.md +336 -0
  25. data/docs/handbook/appendix-type-theory.md +1662 -0
  26. data/docs/handbook/appendix-typeprof.md +416 -0
  27. data/docs/handbook/appendix-typescript.md +332 -0
  28. data/docs/install.md +189 -0
  29. data/docs/llms.txt +72 -0
  30. data/docs/manual/01-installation.md +342 -0
  31. data/docs/manual/02-cli-reference.md +569 -0
  32. data/docs/manual/03-configuration.md +152 -0
  33. data/docs/manual/04-diagnostics.md +206 -0
  34. data/docs/manual/05-inspecting-types.md +109 -0
  35. data/docs/manual/06-baseline.md +104 -0
  36. data/docs/manual/07-plugins.md +92 -0
  37. data/docs/manual/08-skills.md +143 -0
  38. data/docs/manual/09-editor-integration.md +245 -0
  39. data/docs/manual/10-mcp-server.md +539 -0
  40. data/docs/manual/11-ci.md +274 -0
  41. data/docs/manual/12-caching.md +116 -0
  42. data/docs/manual/13-troubleshooting.md +120 -0
  43. data/docs/manual/14-rails-quickstart.md +332 -0
  44. data/docs/manual/15-type-protection-coverage.md +204 -0
  45. data/docs/manual/16-rbs-extended-annotations.md +190 -0
  46. data/docs/manual/17-driving-improvement.md +160 -0
  47. data/docs/manual/README.md +87 -0
  48. data/docs/manual/ci-templates/README.md +58 -0
  49. data/docs/manual/plugins/README.md +86 -0
  50. data/docs/manual/plugins/rigor-actioncable.md +78 -0
  51. data/docs/manual/plugins/rigor-actionmailer.md +74 -0
  52. data/docs/manual/plugins/rigor-actionpack.md +80 -0
  53. data/docs/manual/plugins/rigor-activejob.md +58 -0
  54. data/docs/manual/plugins/rigor-activerecord.md +102 -0
  55. data/docs/manual/plugins/rigor-activestorage.md +74 -0
  56. data/docs/manual/plugins/rigor-activesupport-core-ext.md +86 -0
  57. data/docs/manual/plugins/rigor-devise.md +70 -0
  58. data/docs/manual/plugins/rigor-dry-schema.md +56 -0
  59. data/docs/manual/plugins/rigor-dry-struct.md +60 -0
  60. data/docs/manual/plugins/rigor-dry-types.md +59 -0
  61. data/docs/manual/plugins/rigor-dry-validation.md +62 -0
  62. data/docs/manual/plugins/rigor-factorybot.md +76 -0
  63. data/docs/manual/plugins/rigor-graphql.md +89 -0
  64. data/docs/manual/plugins/rigor-hanami.md +83 -0
  65. data/docs/manual/plugins/rigor-mangrove.md +73 -0
  66. data/docs/manual/plugins/rigor-minitest.md +86 -0
  67. data/docs/manual/plugins/rigor-pundit.md +72 -0
  68. data/docs/manual/plugins/rigor-rails-i18n.md +92 -0
  69. data/docs/manual/plugins/rigor-rails-routes.md +94 -0
  70. data/docs/manual/plugins/rigor-rails.md +44 -0
  71. data/docs/manual/plugins/rigor-rbs-inline.md +83 -0
  72. data/docs/manual/plugins/rigor-rspec-rails.md +72 -0
  73. data/docs/manual/plugins/rigor-rspec.md +86 -0
  74. data/docs/manual/plugins/rigor-shoulda-matchers.md +78 -0
  75. data/docs/manual/plugins/rigor-sidekiq.md +78 -0
  76. data/docs/manual/plugins/rigor-sinatra.md +61 -0
  77. data/docs/manual/plugins/rigor-sorbet.md +63 -0
  78. data/docs/manual/plugins/rigor-statesman.md +75 -0
  79. data/docs/manual/plugins/rigor-typescript-utility-types.md +71 -0
  80. data/exe/rigor +1 -1
  81. data/lib/rigor/analysis/incremental_session.rb +4 -2
  82. data/lib/rigor/analysis/run_stats.rb +13 -1
  83. data/lib/rigor/analysis/runner.rb +54 -12
  84. data/lib/rigor/cli/check_command.rb +1 -1
  85. data/lib/rigor/cli/docs_command.rb +248 -0
  86. data/lib/rigor/cli/skill_command.rb +103 -41
  87. data/lib/rigor/cli/skill_describe.rb +346 -0
  88. data/lib/rigor/cli/triage_command.rb +8 -2
  89. data/lib/rigor/cli/triage_renderer.rb +4 -0
  90. data/lib/rigor/cli.rb +25 -3
  91. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +124 -32
  92. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
  93. data/lib/rigor/inference/scope_indexer.rb +87 -89
  94. data/lib/rigor/plugin/isolation.rb +5 -5
  95. data/lib/rigor/plugin/loader.rb +4 -2
  96. data/lib/rigor/triage/catalogue.rb +16 -1
  97. data/lib/rigor/triage.rb +30 -7
  98. data/lib/rigor/version.rb +1 -1
  99. data/skills/rigor-ask/SKILL.md +172 -0
  100. data/skills/rigor-doctor/SKILL.md +87 -0
  101. data/skills/rigor-editor-setup/SKILL.md +114 -0
  102. data/skills/rigor-mcp-setup/SKILL.md +117 -0
  103. data/skills/rigor-monkeypatch-resolve/SKILL.md +79 -0
  104. data/skills/rigor-next-steps/SKILL.md +113 -0
  105. data/skills/rigor-plugin-tune/SKILL.md +79 -0
  106. data/skills/rigor-protection-uplift/SKILL.md +133 -0
  107. data/skills/rigor-rbs-setup/SKILL.md +128 -0
  108. data/skills/rigor-upgrade/SKILL.md +79 -0
  109. metadata +90 -1
@@ -0,0 +1,346 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Rigor
6
+ class CLI
7
+ # The presence-only project-state probe behind `rigor skill describe`
8
+ # (ADR-73 WD2). It stats files and opens only config / CI / lockfiles
9
+ # — it runs no analysis — so it is cheap and side-effect-free, and an
10
+ # agent can run it freely at any point. Split from {SkillDescribe} so
11
+ # the routing/rendering class stays focused.
12
+ class ProjectStateProbe
13
+ # Config / baseline filenames the probe stats for. `CONFIG_FILENAMES`
14
+ # mirrors `Configuration::DISCOVERY_ORDER` (developer-local override
15
+ # first, committed default second) so the probe agrees with what
16
+ # `rigor check` would auto-discover.
17
+ CONFIG_FILENAMES = %w[.rigor.yml .rigor.dist.yml].freeze
18
+ BASELINE_FILENAME = ".rigor-baseline.yml"
19
+ # `rbs collection install`'s lockfile — Rigor auto-detects it at the
20
+ # project root and feeds each gem's community RBS into the signature
21
+ # paths, so its presence is the signal that the gem-RBS gap has been
22
+ # addressed.
23
+ RBS_COLLECTION_LOCKFILE = "rbs_collection.lock.yaml"
24
+
25
+ # `Gemfile.lock` substrings that mark a Rails app, and the bundled
26
+ # Rails-family plugin ids — used to spot a configured Rails project
27
+ # that has not enabled any Rails plugin (a `rigor-plugin-tune` cue).
28
+ RAILS_LOCK_MARKERS = %w[railties actionpack activerecord actioncable].freeze
29
+ RAILS_PLUGIN_MARKERS = %w[
30
+ rigor-activerecord rigor-actionpack rigor-actionmailer rigor-activejob
31
+ rigor-rails-routes rigor-rails-i18n rigor-actioncable rigor-activestorage rigor-rails
32
+ ].freeze
33
+
34
+ def initialize(root)
35
+ @root = root
36
+ end
37
+
38
+ # @return [Hash] the presence-only state the recommendation routes on.
39
+ def to_h
40
+ config = CONFIG_FILENAMES.find { |name| File.file?(File.join(@root, name)) }
41
+ {
42
+ config: config,
43
+ baseline: File.file?(File.join(@root, BASELINE_FILENAME)),
44
+ sig: File.directory?(File.join(@root, "sig")),
45
+ gems: File.file?(File.join(@root, "Gemfile.lock")),
46
+ rbs_collection: File.file?(File.join(@root, RBS_COLLECTION_LOCKFILE)),
47
+ rails_unconfigured: rails_unconfigured?(config),
48
+ ci: ci_state,
49
+ editor: editor_state,
50
+ mcp: mcp_state
51
+ }
52
+ end
53
+
54
+ private
55
+
56
+ # True when Rails is in `Gemfile.lock` but the (present) config
57
+ # enables no Rails plugin — so `rigor-plugin-tune` (wiring the
58
+ # ActiveRecord / routes / i18n plugins) buys more than community RBS
59
+ # would (the 20260620 field trial's strap case). Only fires on an
60
+ # already-configured project; an un-configured Rails app routes to
61
+ # `rigor-project-init`, which selects the plugins itself.
62
+ def rails_unconfigured?(config)
63
+ return false if config.nil?
64
+
65
+ lock = File.join(@root, "Gemfile.lock")
66
+ return false unless File.file?(lock) && file_mentions_any?(lock, RAILS_LOCK_MARKERS)
67
+
68
+ !file_mentions_any?(File.join(@root, config), RAILS_PLUGIN_MARKERS)
69
+ end
70
+
71
+ def file_mentions_any?(path, markers)
72
+ content = File.read(path)
73
+ markers.any? { |marker| content.include?(marker) }
74
+ rescue StandardError
75
+ false
76
+ end
77
+
78
+ # `:wired` (a CI config mentions `rigor`), `:unwired` (a CI config
79
+ # exists but does not), or `:none`.
80
+ def ci_state
81
+ files = ci_config_files
82
+ return :none if files.empty?
83
+
84
+ files.any? { |path| file_mentions_rigor?(path) } ? :wired : :unwired
85
+ end
86
+
87
+ # Editor-LSP signal — `:wired` (a committed `.vscode/` config names
88
+ # `rigor`), `:unwired` (a `.vscode/` is present but does not), or
89
+ # `:none`. Only VS Code is detectable here: Neovim / Emacs / Helix
90
+ # configs live in the user's home, not the repo, so editor-setup is
91
+ # otherwise a catalogue-only destination.
92
+ def editor_state
93
+ vscode = File.join(@root, ".vscode")
94
+ return :none unless File.directory?(vscode)
95
+
96
+ files = Dir.glob(File.join(vscode, "*.json"))
97
+ return :unwired if files.empty?
98
+
99
+ files.any? { |path| file_mentions_rigor?(path) } ? :wired : :unwired
100
+ end
101
+
102
+ # MCP-client signal — `:wired` (a committed project MCP config names
103
+ # `rigor`), `:unwired` (one is present but does not), or `:none`.
104
+ # Only the project-scoped configs are detectable (`.mcp.json`,
105
+ # `.cursor/mcp.json`); user-level client configs live in $HOME, so
106
+ # mcp-setup is otherwise a catalogue-only destination.
107
+ def mcp_state
108
+ files = [".mcp.json", File.join(".cursor", "mcp.json")]
109
+ .map { |rel| File.join(@root, rel) }
110
+ .select { |path| File.file?(path) }
111
+ return :none if files.empty?
112
+
113
+ files.any? { |path| file_mentions_rigor?(path) } ? :wired : :unwired
114
+ end
115
+
116
+ def file_mentions_rigor?(path)
117
+ File.read(path).include?("rigor")
118
+ rescue StandardError
119
+ false
120
+ end
121
+
122
+ def ci_config_files
123
+ files = Dir.glob(File.join(@root, ".github", "workflows", "*.{yml,yaml}"))
124
+ gitlab = File.join(@root, ".gitlab-ci.yml")
125
+ files << gitlab if File.file?(gitlab)
126
+ files
127
+ end
128
+ end
129
+
130
+ # Builds the `rigor skill describe` report (ADR-73): a {ProjectStateProbe}
131
+ # snapshot, a recommended next skill, and the live catalogue of bundled
132
+ # skills with their current frontmatter descriptions. Extracted from
133
+ # {SkillCommand} so the command stays a thin dispatcher and this — the
134
+ # "live brain" — owns the routing.
135
+ class SkillDescribe
136
+ # The entry-point SKILL itself — excluded from the catalogue because
137
+ # it is the skill being run, not a destination.
138
+ ENTRY_POINT_SKILL = "rigor-next-steps"
139
+
140
+ # Adoption-journey order for the catalogue and the order the
141
+ # recommendation decision tree walks. `rigor-ask` sits last: it is
142
+ # the journey-agnostic "answer a question about Rigor" companion the
143
+ # agent can offer at any point, never a presence-recommended step.
144
+ CATALOG_ORDER = %w[
145
+ rigor-project-init
146
+ rigor-rbs-setup
147
+ rigor-ci-setup
148
+ rigor-baseline-reduce
149
+ rigor-monkeypatch-resolve
150
+ rigor-editor-setup
151
+ rigor-mcp-setup
152
+ rigor-protection-uplift
153
+ rigor-plugin-tune
154
+ rigor-plugin-author
155
+ rigor-upgrade
156
+ rigor-doctor
157
+ rigor-ask
158
+ ].freeze
159
+
160
+ # @param skills [Array<Hash>] discovered skills, each `{name:, path:}`.
161
+ # @param root [String] project root to probe (defaults to the cwd).
162
+ def initialize(skills:, root: Dir.pwd)
163
+ @skills = skills
164
+ @root = root
165
+ end
166
+
167
+ # @return [String] the full describe report.
168
+ def render
169
+ catalog = catalog_skills
170
+ state = ProjectStateProbe.new(@root).to_h
171
+ recommendation = recommend(state, catalog)
172
+ [
173
+ title,
174
+ state_section(state),
175
+ recommendation_section(recommendation),
176
+ catalog_section(catalog),
177
+ agent_prompt(recommendation)
178
+ ].join("\n")
179
+ end
180
+
181
+ private
182
+
183
+ # The skills offered as "what to do next", in adoption-journey order.
184
+ # The entry-point skill is excluded, and unknown skills sort after
185
+ # the known journey, alphabetically.
186
+ def catalog_skills
187
+ @skills
188
+ .reject { |skill| skill.fetch(:name) == ENTRY_POINT_SKILL }
189
+ .sort_by { |skill| [CATALOG_ORDER.index(skill.fetch(:name)) || CATALOG_ORDER.size, skill.fetch(:name)] }
190
+ end
191
+
192
+ # The decision tree (ADR-73 WD2). Returns `{ skill:, reason: }` for
193
+ # the recommended next step, or nil when no catalogue skill matches.
194
+ def recommend(state, catalog)
195
+ name, reason = recommended_name_and_reason(state)
196
+ skill = catalog.find { |candidate| candidate.fetch(:name) == name }
197
+ skill.nil? ? nil : { skill: skill, reason: reason }
198
+ end
199
+
200
+ def recommended_name_and_reason(state)
201
+ if state.fetch(:config).nil?
202
+ ["rigor-project-init", "this project has no Rigor configuration yet — start here."]
203
+ elsif state.fetch(:rails_unconfigured)
204
+ ["rigor-plugin-tune",
205
+ "Rails is in your Gemfile.lock but no Rails plugins are enabled — wire them so " \
206
+ "ActiveRecord / routes / i18n calls resolve (a bigger win here than community RBS)."]
207
+ elsif state.fetch(:gems) && !state.fetch(:rbs_collection)
208
+ ["rigor-rbs-setup", "your gems ship no community RBS yet — install it so Rigor stops typing them as Dynamic."]
209
+ elsif state.fetch(:ci) != :wired
210
+ ["rigor-ci-setup", "Rigor is configured but not wired into CI — lock in the regression guard."]
211
+ elsif state.fetch(:baseline)
212
+ ["rigor-baseline-reduce", "a baseline is in place — work it down rule by rule."]
213
+ elsif state.fetch(:editor) == :unwired
214
+ ["rigor-editor-setup",
215
+ "you have an editor config but no Rigor LSP — wire `rigor lsp` for live diagnostics and hover types."]
216
+ elsif state.fetch(:mcp) == :unwired
217
+ ["rigor-mcp-setup",
218
+ "an MCP client config is present without Rigor — wire `rigor mcp` so your AI agent can call Rigor's tools."]
219
+ else
220
+ ["rigor-protection-uplift", "the basics are in place — raise how much of your code Rigor can catch bugs in."]
221
+ end
222
+ end
223
+
224
+ def title
225
+ <<~TITLE
226
+ # Rigor — next steps for this project
227
+ #
228
+ # Generated live by rigortype #{Rigor::VERSION}; this guidance always
229
+ # reflects your installed version and the project's current state.
230
+ TITLE
231
+ end
232
+
233
+ def state_section(state)
234
+ ci = case state.fetch(:ci)
235
+ when :wired then "Rigor wired in"
236
+ when :unwired then "CI present, Rigor not wired"
237
+ else "not detected"
238
+ end
239
+ rbs = if state.fetch(:gems)
240
+ state.fetch(:rbs_collection) ? "collection installed" : "gems present, no collection"
241
+ else
242
+ "no Gemfile.lock"
243
+ end
244
+ editor = case state.fetch(:editor)
245
+ when :wired then "Rigor LSP wired (.vscode)"
246
+ when :unwired then ".vscode present, Rigor LSP not wired"
247
+ else "not detected"
248
+ end
249
+ mcp = case state.fetch(:mcp)
250
+ when :wired then "Rigor MCP wired"
251
+ when :unwired then "MCP config present, Rigor not wired"
252
+ else "not detected"
253
+ end
254
+ <<~STATE
255
+ ## Project state
256
+ - Config file: #{state.fetch(:config) || 'none (no .rigor.yml / .rigor.dist.yml)'}
257
+ - Baseline: #{state.fetch(:baseline) ? '.rigor-baseline.yml' : 'none'}
258
+ - Project sig/: #{state.fetch(:sig) ? 'present' : 'none'}
259
+ - Community RBS: #{rbs}
260
+ - CI integration: #{ci}
261
+ - Editor LSP: #{editor}
262
+ - MCP server: #{mcp}
263
+ STATE
264
+ end
265
+
266
+ def recommendation_section(recommendation)
267
+ return "## Recommended next step\n- (no bundled skill matched the current state)\n" if recommendation.nil?
268
+
269
+ name = recommendation.fetch(:skill).fetch(:name)
270
+ <<~REC
271
+ ## Recommended next step
272
+ → #{name} — #{recommendation.fetch(:reason)}
273
+ Load it: rigor skill #{name}
274
+ REC
275
+ end
276
+
277
+ def catalog_section(catalog)
278
+ lines = catalog.map do |skill|
279
+ name = skill.fetch(:name)
280
+ "- #{name} — #{catalog_blurb(skill.fetch(:path))}\n rigor skill #{name}"
281
+ end
282
+ "## All skills you can run next\n#{lines.join("\n")}\n"
283
+ end
284
+
285
+ def agent_prompt(recommendation)
286
+ opener =
287
+ if recommendation.nil?
288
+ "Ask the user what they would like to do next"
289
+ else
290
+ "Present the recommended step above (or, if the user has a different goal, ask which they want)"
291
+ end
292
+ <<~PROMPT
293
+ ## For the agent
294
+ #{opener}, then run `rigor skill <name>` for the chosen skill and
295
+ follow its body top to bottom.
296
+
297
+ The recommendation above is from a presence-only probe — it does not run
298
+ `rigor check`. If you have run (or now run) `rigor check`, let its findings
299
+ refine the choice:
300
+ - errors present and no baseline yet → rigor-baseline-reduce
301
+ - a `call.unresolved-toplevel` / `call.undefined-method` cluster on the
302
+ project's own monkey-patches → rigor-monkeypatch-resolve
303
+ - framework calls (ActiveRecord, routes, i18n …) typing as Dynamic with no
304
+ matching plugins enabled → rigor-plugin-tune
305
+ - `RBS classes available: 0` or a `configuration-error` diagnostic → rigor-doctor
306
+
307
+ Re-run `rigor skill describe` whenever you need the next step — it always
308
+ reflects the project's current state.
309
+ PROMPT
310
+ end
311
+
312
+ # The one-line essence of a skill's frontmatter `description`, read
313
+ # live from the SKILL.md so the catalogue can never go stale relative
314
+ # to the shipped skill. Drops the `Triggers:` / `NOT for` tail and
315
+ # caps the length.
316
+ def catalog_blurb(path)
317
+ text = frontmatter(path).fetch("description", "").to_s.strip.tr("\n", " ").squeeze(" ")
318
+ head = text.split(/\s*Triggers?:/, 2).first.to_s.strip
319
+ head = text if head.empty?
320
+ truncate(head, 200)
321
+ end
322
+
323
+ def truncate(text, limit)
324
+ return text if text.length <= limit
325
+
326
+ "#{(text[0, limit] || '').rstrip}…"
327
+ end
328
+
329
+ # Parse a SKILL.md's leading `---`-delimited YAML frontmatter. Returns
330
+ # {} on any missing or malformed block so the catalogue degrades to a
331
+ # name-only entry rather than raising.
332
+ def frontmatter(path)
333
+ text = File.read(path)
334
+ return {} unless text.start_with?("---\n")
335
+
336
+ closing = text.index("\n---\n", 4)
337
+ return {} if closing.nil?
338
+
339
+ data = YAML.safe_load(text[4...closing])
340
+ data.is_a?(Hash) ? data : {}
341
+ rescue StandardError
342
+ {}
343
+ end
344
+ end
345
+ end
346
+ end
@@ -31,7 +31,8 @@ module Rigor
31
31
  diagnostics = analyze(configuration)
32
32
 
33
33
  report = Triage.analyze(diagnostics, top: options.fetch(:top),
34
- hints: options.fetch(:sections).include?(:hints))
34
+ hints: options.fetch(:sections).include?(:hints),
35
+ include_info: options.fetch(:include_info))
35
36
  renderer = TriageRenderer.new(report, sections: options.fetch(:sections))
36
37
  @out.puts(options.fetch(:format) == "json" ? renderer.json : renderer.text)
37
38
  0
@@ -40,12 +41,17 @@ module Rigor
40
41
  private
41
42
 
42
43
  def parse_options
43
- options = { config: nil, format: "text", top: 10, sections: DEFAULT_SECTIONS }
44
+ options = { config: nil, format: "text", top: 10, sections: DEFAULT_SECTIONS, include_info: false }
44
45
  OptionParser.new do |opts|
45
46
  opts.banner = USAGE
46
47
  Options.add_config(opts, options)
47
48
  opts.on("--format=FORMAT", "Output format: text (default) or json") { |v| options[:format] = v }
48
49
  opts.on("--top=N", Integer, "Hotspot-file count (default 10)") { |v| options[:top] = v }
50
+ opts.on("--include-info",
51
+ "Route info diagnostics into distribution / selectors / hotspots " \
52
+ "(excluded by default — mostly plugin recognition trace)") do
53
+ options[:include_info] = true
54
+ end
49
55
  opts.on("--hints-only", "Print only the heuristic-hints section") { options[:sections] = %i[hints] }
50
56
  opts.on("--no-hints", "Print distribution + selectors + hotspots only") do
51
57
  options[:sections] = %i[distribution selectors hotspots]
@@ -37,6 +37,10 @@ module Rigor
37
37
  max = @report.distribution.map(&:count).max || 1
38
38
  lines = ["Diagnostic distribution — #{s.total} total " \
39
39
  "(#{s.error} error / #{s.warning} warning#{" / #{s.info} info" if s.info.positive?})"]
40
+ if !@report.include_info && s.info.positive?
41
+ lines << " #{s.info} info diagnostic(s) hidden below " \
42
+ "(mostly plugin recognition trace) — pass --include-info to route them"
43
+ end
40
44
  @report.distribution.each do |row|
41
45
  lines << format(" %<rule>-32s %<count>5d %<bar>s",
42
46
  rule: row.rule, count: row.count, bar: bar(row.count, max))
data/lib/rigor/cli.rb CHANGED
@@ -16,8 +16,10 @@ require_relative "cli/ci_detector"
16
16
  module Rigor
17
17
  # The CLI class is a dispatcher: each `run_*` method delegates to a
18
18
  # command-specific class once the command grows beyond a few lines (see
19
- # {CLI::TypeOfCommand} and {CLI::CheckCommand}).
20
- class CLI
19
+ # {CLI::TypeOfCommand} and {CLI::CheckCommand}). It necessarily grows by
20
+ # one delegator + one help line per command, so — like the command
21
+ # classes it fans out to — it carries an explicit ClassLength exemption.
22
+ class CLI # rubocop:disable Metrics/ClassLength
21
23
  EXIT_USAGE = 64
22
24
 
23
25
  HANDLERS = {
@@ -39,6 +41,8 @@ module Rigor
39
41
  "plugin" => :run_plugin,
40
42
  "playground" => :run_playground,
41
43
  "skill" => :run_skill,
44
+ "describe" => :run_describe,
45
+ "docs" => :run_docs,
42
46
  "show-bleedingedge" => :run_show_bleedingedge
43
47
  }.freeze
44
48
 
@@ -285,6 +289,22 @@ module Rigor
285
289
  CLI::SkillCommand.new(argv: @argv, out: @out, err: @err).run
286
290
  end
287
291
 
292
+ # `rigor describe` — a top-level alias for `rigor skill describe`,
293
+ # the entry point most users reach for first. Surfaced because a
294
+ # bare `rigor describe` is the intuitive guess (the onboarding field
295
+ # trial saw it tried and met "Unknown command").
296
+ def run_describe
297
+ require_relative "cli/skill_command"
298
+
299
+ CLI::SkillCommand.new(argv: ["describe", *@argv], out: @out, err: @err).run
300
+ end
301
+
302
+ def run_docs
303
+ require_relative "cli/docs_command"
304
+
305
+ CLI::DocsCommand.new(argv: @argv, out: @out, err: @err).run
306
+ end
307
+
288
308
  def run_plugin
289
309
  require_relative "cli/plugin_command"
290
310
 
@@ -318,7 +338,9 @@ module Rigor
318
338
  plugins Report activation status of every configured plugin
319
339
  plugin Browse bundled plugin source as worked examples (list/path/print/root)
320
340
  playground Start the browser playground (requires rigor-playground gem)
321
- skill List or print bundled Agent Skills (rigor-project-init, ...)
341
+ describe Recommend the next skill for this project (alias for `skill describe`)
342
+ skill Recommend the next skill + list/print bundled Agent Skills (skill describe, skill <name>)
343
+ docs Print the bundled docs offline (docs <name>, docs --list)
322
344
  show-bleedingedge Show the bleeding-edge overlay + what your config adopts (ADR-50)
323
345
  version Print the Rigor version
324
346
  help Print this help