snoot 0.1.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.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +20 -0
  3. data/LICENSE +21 -0
  4. data/README.md +49 -0
  5. data/data/reek_docs/API.md +174 -0
  6. data/data/reek_docs/Attribute.md +39 -0
  7. data/data/reek_docs/Basic-Smell-Options.md +85 -0
  8. data/data/reek_docs/Boolean-Parameter.md +54 -0
  9. data/data/reek_docs/Class-Variable.md +40 -0
  10. data/data/reek_docs/Code-Smells.md +39 -0
  11. data/data/reek_docs/Command-Line-Options.md +119 -0
  12. data/data/reek_docs/Control-Couple.md +26 -0
  13. data/data/reek_docs/Control-Parameter.md +32 -0
  14. data/data/reek_docs/Data-Clump.md +46 -0
  15. data/data/reek_docs/Duplicate-Method-Call.md +264 -0
  16. data/data/reek_docs/Feature-Envy.md +93 -0
  17. data/data/reek_docs/How-To-Write-New-Detectors.md +144 -0
  18. data/data/reek_docs/How-reek-works-internally.md +114 -0
  19. data/data/reek_docs/Instance-Variable-Assumption.md +163 -0
  20. data/data/reek_docs/Irresponsible-Module.md +47 -0
  21. data/data/reek_docs/LICENSE +20 -0
  22. data/data/reek_docs/Large-Class.md +16 -0
  23. data/data/reek_docs/Long-Parameter-List.md +39 -0
  24. data/data/reek_docs/Long-Yield-List.md +37 -0
  25. data/data/reek_docs/Manual-Dispatch.md +30 -0
  26. data/data/reek_docs/Missing-Safe-Method.md +92 -0
  27. data/data/reek_docs/Module-Initialize.md +62 -0
  28. data/data/reek_docs/Nested-Iterators.md +59 -0
  29. data/data/reek_docs/Nil-Check.md +47 -0
  30. data/data/reek_docs/RSpec-matchers.md +129 -0
  31. data/data/reek_docs/Rake-Task.md +66 -0
  32. data/data/reek_docs/Reek-4-to-Reek-5-migration.md +188 -0
  33. data/data/reek_docs/Reek-Driven-Development.md +46 -0
  34. data/data/reek_docs/Repeated-Conditional.md +47 -0
  35. data/data/reek_docs/Simulated-Polymorphism.md +16 -0
  36. data/data/reek_docs/Smell-Suppression.md +96 -0
  37. data/data/reek_docs/Style-Guide.md +19 -0
  38. data/data/reek_docs/Subclassed-From-Core-Class.md +79 -0
  39. data/data/reek_docs/Too-Many-Constants.md +37 -0
  40. data/data/reek_docs/Too-Many-Instance-Variables.md +43 -0
  41. data/data/reek_docs/Too-Many-Methods.md +56 -0
  42. data/data/reek_docs/Too-Many-Statements.md +54 -0
  43. data/data/reek_docs/Uncommunicative-Method-Name.md +94 -0
  44. data/data/reek_docs/Uncommunicative-Module-Name.md +92 -0
  45. data/data/reek_docs/Uncommunicative-Name.md +18 -0
  46. data/data/reek_docs/Uncommunicative-Parameter-Name.md +90 -0
  47. data/data/reek_docs/Uncommunicative-Variable-Name.md +96 -0
  48. data/data/reek_docs/Unused-Parameters.md +28 -0
  49. data/data/reek_docs/Unused-Private-Method.md +101 -0
  50. data/data/reek_docs/Utility-Function.md +57 -0
  51. data/data/reek_docs/Versioning-Policy.md +7 -0
  52. data/data/reek_docs/YAML-Reports.md +93 -0
  53. data/exe/snoot +5 -0
  54. data/lib/snoot/analyse_run/decision.rb +62 -0
  55. data/lib/snoot/analyse_run/result.rb +12 -0
  56. data/lib/snoot/analyse_run.rb +70 -0
  57. data/lib/snoot/analyser_orchestration/default.rb +149 -0
  58. data/lib/snoot/analyser_orchestration/result_mapping.rb +52 -0
  59. data/lib/snoot/analyser_orchestration.rb +21 -0
  60. data/lib/snoot/analyser_result.rb +14 -0
  61. data/lib/snoot/cli/event.rb +13 -0
  62. data/lib/snoot/cli/pipeline.rb +14 -0
  63. data/lib/snoot/cli.rb +147 -0
  64. data/lib/snoot/findings.rb +23 -0
  65. data/lib/snoot/render_report.rb +82 -0
  66. data/lib/snoot/run.rb +35 -0
  67. data/lib/snoot/state_error.rb +9 -0
  68. data/lib/snoot/value_types.rb +20 -0
  69. data/lib/snoot/version.rb +5 -0
  70. data/lib/snoot.rb +21 -0
  71. data/snoot.allium +482 -0
  72. metadata +160 -0
data/snoot.allium ADDED
@@ -0,0 +1,482 @@
1
+ -- allium: 3
2
+ -- snoot.allium
3
+
4
+ -- Scope: Runtime behaviour of a Ruby gem that runs reek, flog and flay over a
5
+ -- configured path set and emits a single agent-targeted report
6
+ -- describing one finding.
7
+ -- Includes: invocation, finding collection, finding selection, report emission,
8
+ -- run outcomes (FindingRendered, NothingToReport, AnalysisFailed).
9
+ -- Excludes:
10
+ -- - Build-time vendored-doc workflow (sync, packaging, version pinning)
11
+ -- - Gem packaging, dependency management, CLI argument parsing internals
12
+ -- - CI gating policy (operator decisions about which exit codes
13
+ -- fail builds, retry behaviour, etc.) -- exit-code semantics
14
+ -- themselves are in scope, see CLI surface
15
+ -- - Diff-aware analysis, JSON output, MCP, pre-commit hooks (non-goals
16
+ -- enumerated in the design doc)
17
+ -- Centred on: the LLM agent reading the emitted report.
18
+
19
+ ------------------------------------------------------------
20
+ -- External Entities
21
+ ------------------------------------------------------------
22
+
23
+ -- The three analysers the gem orchestrates. Their internal behaviour is out
24
+ -- of scope; their findings are surfaced via black-box analyse_* functions.
25
+
26
+ external entity Reek {
27
+ -- Reek detects code smells in Ruby source. A run yields zero or more Smell
28
+ -- value instances, each tagged with a SmellType.
29
+ }
30
+
31
+ external entity Flog {
32
+ -- Flog assigns a complexity score to methods and classes. A run yields
33
+ -- zero or more ComplexityHit instances.
34
+ }
35
+
36
+ external entity Flay {
37
+ -- Flay detects structural duplication across the analysed source. A run
38
+ -- yields zero or more DuplicationCluster instances.
39
+ }
40
+
41
+ ------------------------------------------------------------
42
+ -- Value Types
43
+ ------------------------------------------------------------
44
+
45
+ value Path {
46
+ raw: String
47
+ }
48
+
49
+ value Location {
50
+ path: Path
51
+ line_start: Integer
52
+ line_end: Integer
53
+ }
54
+
55
+ -- A reek smell type identifier (e.g. "FeatureEnvy", "TooManyMethods"). The
56
+ -- set of valid values is owned by reek.
57
+ value SmellType {
58
+ name: String
59
+ }
60
+
61
+ ------------------------------------------------------------
62
+ -- Analyser Result
63
+ ------------------------------------------------------------
64
+
65
+ -- The two-shaped outcome of AnalyserOrchestration#analyse. AnalyserResult is
66
+ -- a sum-type entity so the rule body can narrow on `kind`. Both variants
67
+ -- are short-lived: an instance is created by analyse() and consumed within
68
+ -- a single AnalyseRun execution; neither persists beyond the rule.
69
+ entity AnalyserResult {
70
+ kind: Sources | AnalyserFailure
71
+ }
72
+
73
+ -- The bundle of analyser outputs produced by a successful invocation of
74
+ -- AnalyserOrchestration#analyse. Carries the per-analyser finding sets
75
+ -- so AnalyseRun can derive significance and candidate views without
76
+ -- re-invoking the analysers.
77
+ variant Sources : AnalyserResult {
78
+ smells: Set<Smell>
79
+ complexities: Set<ComplexityHit>
80
+ duplications: Set<DuplicationCluster>
81
+ }
82
+
83
+ -- An analyser failure surfaced to the operator on outcome = analysis_failed.
84
+ -- The analyser field identifies which of the three analysers (Reek, Flog,
85
+ -- Flay) produced the failure; message is the human-readable error detail
86
+ -- carried on stderr. Only the first failed analyser in canonical order
87
+ -- (Reek -> Flog -> Flay) is recorded; the orchestration aborts on first
88
+ -- failure and does not invoke subsequent analysers.
89
+ variant AnalyserFailure : AnalyserResult {
90
+ analyser: Reek | Flog | Flay
91
+ message: String
92
+ }
93
+
94
+ ------------------------------------------------------------
95
+ -- Contracts
96
+ ------------------------------------------------------------
97
+
98
+ -- The analyser-orchestration contract is the gem's integration shape with
99
+ -- the three external analysers and the vendored-docs corpus. The CLI
100
+ -- surface demands this contract: an implementation must supply each
101
+ -- signature for the spec's rules to be satisfiable. The contract names the
102
+ -- analyser entities (Reek, Flog, Flay) explicitly so the spec records that
103
+ -- it depends on those tools, even though their internal behaviour is out
104
+ -- of scope.
105
+ contract AnalyserOrchestration {
106
+ -- adapter_label maps an external analyser to its short identifier
107
+ -- (e.g. "reek", "flog", "flay"). The signature exists to record the
108
+ -- per-tool dependency on Reek, Flog and Flay at the contract
109
+ -- boundary; rule bodies do not invoke it.
110
+ adapter_label: (analyser: Reek | Flog | Flay) -> String
111
+
112
+ -- vendored_doc maps a smell type to its bundled markdown doc, or null
113
+ -- when no doc is bundled for that smell type. Consulted both to filter
114
+ -- the smell candidate pool and to render the doc section. Location
115
+ -- rendering for the FindingContext section is carried by Location
116
+ -- itself, not the orchestration contract.
117
+ vendored_doc: (smell_type: SmellType) -> String?
118
+
119
+ -- Significance pre-filters. Each returns the subset of its input deemed
120
+ -- significant enough to warrant attention from the agent reading the
121
+ -- report. Numeric
122
+ -- thresholds are an implementation policy each adapter owns; the
123
+ -- contract requires only that the result is a subset of the input and
124
+ -- is deterministic per @invariant Determinism. This is the gate that
125
+ -- filters findings that don't warrant addressing before category
126
+ -- selection in AnalyseRun.
127
+ significant_smells: (smells: Set<Smell>) -> Set<Smell>
128
+ significant_complexities: (complexities: Set<ComplexityHit>) -> Set<ComplexityHit>
129
+ significant_duplications: (duplications: Set<DuplicationCluster>) -> Set<DuplicationCluster>
130
+
131
+ -- analyse runs the three analysers in canonical order (Reek -> Flog
132
+ -- -> Flay), capturing each output as it succeeds. On the first
133
+ -- failure it returns an AnalyserFailure tagged with that analyser
134
+ -- and does not invoke the remaining ones; on full success it
135
+ -- returns a Sources bundling the three result sets. This signature
136
+ -- respects @invariant Determinism: for the same path set, the same
137
+ -- outcome (including which analyser fails first, if any, and its
138
+ -- message) is produced within a single CLI invocation.
139
+ analyse: (paths: Set<Path>) -> AnalyserResult
140
+
141
+ @invariant Determinism
142
+ -- Each signature is pure within a single CLI invocation: the same
143
+ -- inputs produce the same outputs. Outputs may differ between
144
+ -- invocations as the source under analysis changes; that is not a
145
+ -- violation of determinism within a run.
146
+ }
147
+
148
+ ------------------------------------------------------------
149
+ -- Entities and Variants
150
+ ------------------------------------------------------------
151
+
152
+ -- A Finding is the unit a Run can render. It is one of three variants,
153
+ -- corresponding to the three analysers the gem orchestrates. Findings exist
154
+ -- only within the scope of the Run that produced them; they have no
155
+ -- persistence beyond the run.
156
+
157
+ entity Finding {
158
+ kind: Smell | ComplexityHit | DuplicationCluster
159
+ }
160
+
161
+ variant Smell : Finding {
162
+ smell_type: SmellType
163
+ location: Location
164
+ message: String
165
+ }
166
+
167
+ variant ComplexityHit : Finding {
168
+ location: Location
169
+ method_name: String?
170
+ score: Decimal
171
+ }
172
+
173
+ variant DuplicationCluster : Finding {
174
+ signature: String
175
+ locations: Set<Location>
176
+ }
177
+
178
+ -- A Run represents one CLI invocation. Its lifecycle terminates in exactly
179
+ -- one of three outcomes.
180
+
181
+ entity Run {
182
+ paths: Set<Path>
183
+ outcome: pending | finding_rendered | nothing_to_report | analysis_failed
184
+
185
+ selected_finding: Finding when outcome = finding_rendered
186
+
187
+ -- The analyser failure detail surfaced when the run aborts. Present
188
+ -- only on outcome = analysis_failed; carries the first analyser to fail
189
+ -- in canonical order (Reek -> Flog -> Flay) and its error message.
190
+ failure: AnalyserFailure when outcome = analysis_failed
191
+
192
+ transitions outcome {
193
+ pending -> finding_rendered
194
+ pending -> nothing_to_report
195
+ pending -> analysis_failed
196
+ terminal: finding_rendered, nothing_to_report, analysis_failed
197
+ }
198
+ }
199
+
200
+ ------------------------------------------------------------
201
+ -- Rules
202
+ ------------------------------------------------------------
203
+
204
+ -- The pipeline is two rules. AnalyseRun spans collection through selection
205
+ -- (it produces the terminal outcome). RenderReport reacts to a selected
206
+ -- finding by emitting the report.
207
+
208
+ rule AnalyseRun {
209
+ when: RunInvoked(paths)
210
+
211
+ ensures:
212
+ let run = Run.created(paths: paths, outcome: pending)
213
+
214
+ -- analyse runs the three analysers in canonical order (Reek ->
215
+ -- Flog -> Flay) and returns either the captured Sources bundle
216
+ -- or, on the first analyser error, an AnalyserFailure. A
217
+ -- single invocation produces both the failure-detection result
218
+ -- and the per-analyser outputs, so the analysers are not
219
+ -- re-invoked along the success branch.
220
+ let result = analyse(run.paths)
221
+
222
+ if result.kind = AnalyserFailure:
223
+ run.outcome = analysis_failed
224
+ run.failure = result
225
+ else if result.kind = Sources:
226
+ -- Significance is the "warrants addressing" pre-filter: each
227
+ -- analyser adapter discards its own noise (per-analyser
228
+ -- numeric thresholds are the implementation's concern) before
229
+ -- category selection runs. The doc-less filter for smells
230
+ -- sits on top of significance, so only significant doc-less
231
+ -- smells reach SkippedDocLessSmellWarned. Doc-less detection
232
+ -- only matters for smells -- non-smell findings have no
233
+ -- vendored doc concept -- and because select_top_finding's
234
+ -- category priority puts Smell strictly above the other
235
+ -- variants, the top smell among significant smells is
236
+ -- exactly the would-be-top-finding whenever it would have
237
+ -- been a smell. Hence top_smell_overall is computed over the
238
+ -- significant smell set rather than the cross-category union.
239
+ let significant_smell_findings = significant_smells(result.smells)
240
+ let significant_complexity_findings = significant_complexities(result.complexities)
241
+ let significant_duplication_findings = significant_duplications(result.duplications)
242
+
243
+ -- A reek smell is excluded from the candidate pool when no
244
+ -- vendored doc exists for its type. If the would-have-been-
245
+ -- selected finding (i.e. the top-scoring finding before this
246
+ -- filter) is one of these, a stderr warning is emitted (see
247
+ -- SkippedDocLessSmellWarned trigger below).
248
+ let documented_smell_findings =
249
+ filter(significant_smell_findings, s => vendored_doc(s.smell_type) != null)
250
+
251
+ let candidate_findings =
252
+ documented_smell_findings + significant_complexity_findings + significant_duplication_findings
253
+
254
+ let top_smell_overall = top_smell(significant_smell_findings)
255
+
256
+ -- select_top_finding orders findings by category priority
257
+ -- first, then by an analyser-natural primary key, then by
258
+ -- deterministic secondary keys to break ties. Category
259
+ -- priority: Smell > DuplicationCluster > ComplexityHit.
260
+ -- Within a category the primary key is the analyser's
261
+ -- natural ranking (smell-type instance count for Smells;
262
+ -- locations.size for DuplicationClusters; score for
263
+ -- ComplexityHits). Ties are broken by:
264
+ -- - Smell: smell_type.name ascending, then path ascending,
265
+ -- then line_start ascending.
266
+ -- - DuplicationCluster: signature ascending, then
267
+ -- min(location.path) ascending, then
268
+ -- min(location.line_start) ascending.
269
+ -- - ComplexityHit: path ascending, then line_start
270
+ -- ascending.
271
+ let selected = select_top_finding(candidate_findings)
272
+
273
+ if candidate_findings.count = 0:
274
+ run.outcome = nothing_to_report
275
+ else:
276
+ run.outcome = finding_rendered
277
+ run.selected_finding = selected
278
+
279
+ -- Stderr warning fires when the top significant smell has
280
+ -- no vendored doc. top_smell_overall is already Smell-or-
281
+ -- null, so a single null-check plus the doc check is enough
282
+ -- to gate the trigger; no variant narrowing is needed.
283
+ if top_smell_overall != null:
284
+ if vendored_doc(top_smell_overall.smell_type) = null:
285
+ SkippedDocLessSmellWarned(
286
+ run: run,
287
+ smell_type: top_smell_overall.smell_type
288
+ )
289
+ }
290
+
291
+ rule RenderReport {
292
+ when: run: Run.outcome becomes finding_rendered
293
+ requires: run.selected_finding != null
294
+
295
+ -- Section composition is variant-conditional:
296
+ --
297
+ -- For Smell findings the report is two sections:
298
+ -- - doc: vendored_doc(smell_type) (always non-null here, per the
299
+ -- SelectedFindingsAreRenderable invariant)
300
+ -- - instances: every Smell from the analysed sources whose smell_type
301
+ -- matches the selected_finding's smell_type, grouped by
302
+ -- file, with files ordered by descending instance count
303
+ -- and alphabetical tie-break. The header / finding_context
304
+ -- sections are absent because the doc + instances pair
305
+ -- is sufficient context for an agent acting on a
306
+ -- multi-instance smell type.
307
+ --
308
+ -- For ComplexityHit and DuplicationCluster findings the report is the
309
+ -- three-section structure (header, finding_context, doc):
310
+ -- - ComplexityHit: hand-written generic prose for high complexity
311
+ -- - DuplicationCluster: hand-written generic prose for high duplication
312
+ -- The exact prose is static implementation policy of the gem (one
313
+ -- string per non-Smell variant), not an orchestration-contract
314
+ -- responsibility. This mirrors the threshold-policy delegation in
315
+ -- AnalyserOrchestration: behavioural variability is contracted, but
316
+ -- static report copy is owned by the implementation.
317
+ ensures:
318
+ if run.selected_finding.kind = Smell:
319
+ ReportEmitted(
320
+ run: run,
321
+ finding: run.selected_finding,
322
+ sections: { doc, instances }
323
+ )
324
+ else:
325
+ ReportEmitted(
326
+ run: run,
327
+ finding: run.selected_finding,
328
+ sections: { header, finding_context, doc }
329
+ )
330
+
331
+ @guarantee SmellReportShape
332
+ -- For Smell findings, a rendered report contains, in order:
333
+ -- 1. Doc -- the vendored markdown for the selected smell type
334
+ -- 2. Instances -- '## Instances' heading followed by per-file
335
+ -- groupings of every Smell from the analysed sources
336
+ -- matching the selected smell_type. Each file group
337
+ -- is the raw path on its own line, then 2-space-
338
+ -- indented 'Line N: <message>' lines. File groups
339
+ -- are ordered by descending instance count, with
340
+ -- alphabetical path as the tie-break.
341
+
342
+ @guarantee NonSmellReportShape
343
+ -- For ComplexityHit and DuplicationCluster findings, a rendered
344
+ -- report contains, in order:
345
+ -- 1. Header -- one-line summary (analyser-implicit, finding
346
+ -- name, location)
347
+ -- 2. FindingContext -- file path(s), line range(s), the analyser's
348
+ -- message; for DuplicationCluster, all cluster
349
+ -- locations are enumerated
350
+ -- 3. Doc -- one inlined hand-written prose constant
351
+
352
+ @guarantee AnalyserProvenanceImplicit
353
+ -- The report does not label the originating analyser. The shape of
354
+ -- the report (doc + instances for Smell vs header + finding_context
355
+ -- + doc for non-Smell) and the finding's wording (smell name vs
356
+ -- complexity language vs duplication enumeration) are sufficient
357
+ -- for the agent to infer provenance.
358
+
359
+ @guidance
360
+ -- Section ordering within each variant shape is structural.
361
+ -- Implementations should emit sections in the declared order so an
362
+ -- agent / parser can rely on predictable landmarks. The agent
363
+ -- reading the report should treat the inlined doc as primary
364
+ -- context for acting on the finding. For Smell findings, the
365
+ -- Instances list enumerates every site the agent must consider.
366
+ -- For non-Smell findings, the header and finding_context sections
367
+ -- locate the single finding so the agent can read the doc against
368
+ -- it.
369
+ }
370
+
371
+ ------------------------------------------------------------
372
+ -- Invariants
373
+ ------------------------------------------------------------
374
+
375
+ -- A rendered run references exactly one finding. There is no concept of a
376
+ -- top-tier of multiple findings in this spec.
377
+ invariant SingleFindingPerRun {
378
+ for r in Runs:
379
+ r.outcome = finding_rendered implies r.selected_finding != null
380
+ }
381
+
382
+ -- A failed run carries a failure record describing which analyser aborted
383
+ -- and the message it produced. The structural `when outcome = analysis_failed`
384
+ -- obligation already pins the field's presence; this invariant makes the
385
+ -- guarantee explicit so consumers can rely on r.failure being non-null
386
+ -- whenever they observe r.outcome = analysis_failed.
387
+ invariant FailurePresentOnFailedRuns {
388
+ for r in Runs:
389
+ r.outcome = analysis_failed implies r.failure != null
390
+ }
391
+
392
+ -- The selection filter excludes doc-less smells. Therefore: when a run's
393
+ -- selected_finding is a Smell, that smell type has a vendored doc
394
+ -- available. The quantification ranges over Smell instances directly so
395
+ -- variant-specific access (smell_type) is unambiguous; the equality with
396
+ -- selected_finding pins the relationship.
397
+ invariant SelectedFindingsAreRenderable {
398
+ for r in Runs:
399
+ for s in Smells:
400
+ (r.outcome = finding_rendered and r.selected_finding = s)
401
+ implies vendored_doc(s.smell_type) != null
402
+ }
403
+
404
+ -- The selected finding must be significant: it must survive the
405
+ -- significance pre-filter for its variant. This is the "warrants
406
+ -- addressing" gate, complementary to SelectedFindingsAreRenderable's
407
+ -- documented-doc gate. Quantification ranges over each variant; the
408
+ -- implementation in AnalyserOrchestration owns the numeric thresholds.
409
+ -- The membership test's input set differs by variant. The smell branch
410
+ -- ranges over the smell set produced by analyse(r.paths) because an
411
+ -- adapter may apply a collection-relative policy -- the production
412
+ -- Default adapter filters by smell-type recurrence count, so a
413
+ -- singleton {s} input would be incorrectly empty. Complexity and
414
+ -- duplication adapters are per-instance (score floor and identity
415
+ -- respectively), so the singleton {f} suffices for those.
416
+ invariant SignificantFindingsOnly {
417
+ for r in Runs:
418
+ for s in Smells:
419
+ (r.outcome = finding_rendered and r.selected_finding = s)
420
+ implies s in significant_smells(analyse(r.paths).smells)
421
+ for c in ComplexityHits:
422
+ (r.outcome = finding_rendered and r.selected_finding = c)
423
+ implies c in significant_complexities({c})
424
+ for d in DuplicationClusters:
425
+ (r.outcome = finding_rendered and r.selected_finding = d)
426
+ implies d in significant_duplications({d})
427
+ }
428
+
429
+ ------------------------------------------------------------
430
+ -- Surfaces
431
+ ------------------------------------------------------------
432
+
433
+ surface CLI {
434
+ -- The CLI's only domain input is the path set. Other flags (verbosity,
435
+ -- output destination) are out of scope at the spec level.
436
+ provides:
437
+ RunInvoked(paths)
438
+
439
+ contracts:
440
+ demands AnalyserOrchestration
441
+
442
+ @guarantee TerminatesInOneOutcome
443
+ -- Every accepted invocation produces exactly one of the three
444
+ -- terminal outcomes (FindingRendered, NothingToReport,
445
+ -- AnalysisFailed). Exit code by outcome:
446
+ -- nothing_to_report -> 0
447
+ -- finding_rendered -> 1
448
+ -- analysis_failed -> 2
449
+
450
+ @guarantee UsageErrorExit
451
+ -- An invocation whose argv shape the CLI rejects (unknown flag,
452
+ -- malformed switch) is not an accepted invocation: RunInvoked
453
+ -- does not fire, no Run is created, and TerminatesInOneOutcome
454
+ -- does not apply. The CLI exits 64 (POSIX EX_USAGE) and writes
455
+ -- the usage banner to stderr. Exit code 64 is reserved for this
456
+ -- pre-domain failure mode so an operator (or CI) can distinguish
457
+ -- a usage error from finding_rendered, both of which would
458
+ -- otherwise share an exit code.
459
+
460
+ @guarantee EmptyPathsDefault
461
+ -- An operator invocation that supplies no paths is treated as if
462
+ -- Path(".") had been supplied: the CLI surface normalises the
463
+ -- empty path set to {Path(".")} before RunInvoked fires.
464
+ -- RunInvoked is therefore always invoked with a non-empty path
465
+ -- set; downstream rules (including AnalyseRun) never observe an
466
+ -- empty paths argument.
467
+
468
+ @guarantee StdoutMutuallyExclusive
469
+ -- A run produces stdout content corresponding to its outcome:
470
+ -- finding_rendered -> stdout = formatted report (variant-
471
+ -- specific shape, see the RenderReport
472
+ -- rule's @guarantee blocks)
473
+ -- nothing_to_report -> stdout = single-line acknowledgement
474
+ -- (see the NothingToReport resolution at
475
+ -- the bottom for the exact format)
476
+ -- analysis_failed -> stdout = empty
477
+ -- Stderr carries warnings (missing vendored docs) at all outcomes.
478
+ -- On outcome = analysis_failed, stderr additionally carries the
479
+ -- failed analyser's message (run.failure.message): one analyser's
480
+ -- error, not aggregated across analysers and not multi-line per
481
+ -- analyser. These stderr contents are independent of stdout.
482
+ }
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: snoot
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Benjamin Kudria
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: flay
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.14'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.14'
26
+ - !ruby/object:Gem::Dependency
27
+ name: flog
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '4.9'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '4.9'
40
+ - !ruby/object:Gem::Dependency
41
+ name: reek
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '6.5'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '6.5'
54
+ description: |
55
+ snoot orchestrates reek, flog and flay over a configured path set
56
+ and emits a single agent-targeted report describing one finding.
57
+ Designed for an LLM coding agent as the reader.
58
+ email:
59
+ - ben@kudria.net
60
+ executables:
61
+ - snoot
62
+ extensions: []
63
+ extra_rdoc_files: []
64
+ files:
65
+ - CHANGELOG.md
66
+ - LICENSE
67
+ - README.md
68
+ - data/reek_docs/API.md
69
+ - data/reek_docs/Attribute.md
70
+ - data/reek_docs/Basic-Smell-Options.md
71
+ - data/reek_docs/Boolean-Parameter.md
72
+ - data/reek_docs/Class-Variable.md
73
+ - data/reek_docs/Code-Smells.md
74
+ - data/reek_docs/Command-Line-Options.md
75
+ - data/reek_docs/Control-Couple.md
76
+ - data/reek_docs/Control-Parameter.md
77
+ - data/reek_docs/Data-Clump.md
78
+ - data/reek_docs/Duplicate-Method-Call.md
79
+ - data/reek_docs/Feature-Envy.md
80
+ - data/reek_docs/How-To-Write-New-Detectors.md
81
+ - data/reek_docs/How-reek-works-internally.md
82
+ - data/reek_docs/Instance-Variable-Assumption.md
83
+ - data/reek_docs/Irresponsible-Module.md
84
+ - data/reek_docs/LICENSE
85
+ - data/reek_docs/Large-Class.md
86
+ - data/reek_docs/Long-Parameter-List.md
87
+ - data/reek_docs/Long-Yield-List.md
88
+ - data/reek_docs/Manual-Dispatch.md
89
+ - data/reek_docs/Missing-Safe-Method.md
90
+ - data/reek_docs/Module-Initialize.md
91
+ - data/reek_docs/Nested-Iterators.md
92
+ - data/reek_docs/Nil-Check.md
93
+ - data/reek_docs/RSpec-matchers.md
94
+ - data/reek_docs/Rake-Task.md
95
+ - data/reek_docs/Reek-4-to-Reek-5-migration.md
96
+ - data/reek_docs/Reek-Driven-Development.md
97
+ - data/reek_docs/Repeated-Conditional.md
98
+ - data/reek_docs/Simulated-Polymorphism.md
99
+ - data/reek_docs/Smell-Suppression.md
100
+ - data/reek_docs/Style-Guide.md
101
+ - data/reek_docs/Subclassed-From-Core-Class.md
102
+ - data/reek_docs/Too-Many-Constants.md
103
+ - data/reek_docs/Too-Many-Instance-Variables.md
104
+ - data/reek_docs/Too-Many-Methods.md
105
+ - data/reek_docs/Too-Many-Statements.md
106
+ - data/reek_docs/Uncommunicative-Method-Name.md
107
+ - data/reek_docs/Uncommunicative-Module-Name.md
108
+ - data/reek_docs/Uncommunicative-Name.md
109
+ - data/reek_docs/Uncommunicative-Parameter-Name.md
110
+ - data/reek_docs/Uncommunicative-Variable-Name.md
111
+ - data/reek_docs/Unused-Parameters.md
112
+ - data/reek_docs/Unused-Private-Method.md
113
+ - data/reek_docs/Utility-Function.md
114
+ - data/reek_docs/Versioning-Policy.md
115
+ - data/reek_docs/YAML-Reports.md
116
+ - exe/snoot
117
+ - lib/snoot.rb
118
+ - lib/snoot/analyse_run.rb
119
+ - lib/snoot/analyse_run/decision.rb
120
+ - lib/snoot/analyse_run/result.rb
121
+ - lib/snoot/analyser_orchestration.rb
122
+ - lib/snoot/analyser_orchestration/default.rb
123
+ - lib/snoot/analyser_orchestration/result_mapping.rb
124
+ - lib/snoot/analyser_result.rb
125
+ - lib/snoot/cli.rb
126
+ - lib/snoot/cli/event.rb
127
+ - lib/snoot/cli/pipeline.rb
128
+ - lib/snoot/findings.rb
129
+ - lib/snoot/render_report.rb
130
+ - lib/snoot/run.rb
131
+ - lib/snoot/state_error.rb
132
+ - lib/snoot/value_types.rb
133
+ - lib/snoot/version.rb
134
+ - snoot.allium
135
+ homepage: https://github.com/bkudria/snoot
136
+ licenses:
137
+ - MIT
138
+ metadata:
139
+ source_code_uri: https://github.com/bkudria/snoot
140
+ bug_tracker_uri: https://github.com/bkudria/snoot/issues
141
+ changelog_uri: https://github.com/bkudria/snoot/blob/main/CHANGELOG.md
142
+ rubygems_mfa_required: 'true'
143
+ rdoc_options: []
144
+ require_paths:
145
+ - lib
146
+ required_ruby_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: '4.0'
151
+ required_rubygems_version: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ">="
154
+ - !ruby/object:Gem::Version
155
+ version: '0'
156
+ requirements: []
157
+ rubygems_version: 4.0.6
158
+ specification_version: 4
159
+ summary: Single-finding agent-targeted reek/flog/flay reporter.
160
+ test_files: []