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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +20 -0
- data/LICENSE +21 -0
- data/README.md +49 -0
- data/data/reek_docs/API.md +174 -0
- data/data/reek_docs/Attribute.md +39 -0
- data/data/reek_docs/Basic-Smell-Options.md +85 -0
- data/data/reek_docs/Boolean-Parameter.md +54 -0
- data/data/reek_docs/Class-Variable.md +40 -0
- data/data/reek_docs/Code-Smells.md +39 -0
- data/data/reek_docs/Command-Line-Options.md +119 -0
- data/data/reek_docs/Control-Couple.md +26 -0
- data/data/reek_docs/Control-Parameter.md +32 -0
- data/data/reek_docs/Data-Clump.md +46 -0
- data/data/reek_docs/Duplicate-Method-Call.md +264 -0
- data/data/reek_docs/Feature-Envy.md +93 -0
- data/data/reek_docs/How-To-Write-New-Detectors.md +144 -0
- data/data/reek_docs/How-reek-works-internally.md +114 -0
- data/data/reek_docs/Instance-Variable-Assumption.md +163 -0
- data/data/reek_docs/Irresponsible-Module.md +47 -0
- data/data/reek_docs/LICENSE +20 -0
- data/data/reek_docs/Large-Class.md +16 -0
- data/data/reek_docs/Long-Parameter-List.md +39 -0
- data/data/reek_docs/Long-Yield-List.md +37 -0
- data/data/reek_docs/Manual-Dispatch.md +30 -0
- data/data/reek_docs/Missing-Safe-Method.md +92 -0
- data/data/reek_docs/Module-Initialize.md +62 -0
- data/data/reek_docs/Nested-Iterators.md +59 -0
- data/data/reek_docs/Nil-Check.md +47 -0
- data/data/reek_docs/RSpec-matchers.md +129 -0
- data/data/reek_docs/Rake-Task.md +66 -0
- data/data/reek_docs/Reek-4-to-Reek-5-migration.md +188 -0
- data/data/reek_docs/Reek-Driven-Development.md +46 -0
- data/data/reek_docs/Repeated-Conditional.md +47 -0
- data/data/reek_docs/Simulated-Polymorphism.md +16 -0
- data/data/reek_docs/Smell-Suppression.md +96 -0
- data/data/reek_docs/Style-Guide.md +19 -0
- data/data/reek_docs/Subclassed-From-Core-Class.md +79 -0
- data/data/reek_docs/Too-Many-Constants.md +37 -0
- data/data/reek_docs/Too-Many-Instance-Variables.md +43 -0
- data/data/reek_docs/Too-Many-Methods.md +56 -0
- data/data/reek_docs/Too-Many-Statements.md +54 -0
- data/data/reek_docs/Uncommunicative-Method-Name.md +94 -0
- data/data/reek_docs/Uncommunicative-Module-Name.md +92 -0
- data/data/reek_docs/Uncommunicative-Name.md +18 -0
- data/data/reek_docs/Uncommunicative-Parameter-Name.md +90 -0
- data/data/reek_docs/Uncommunicative-Variable-Name.md +96 -0
- data/data/reek_docs/Unused-Parameters.md +28 -0
- data/data/reek_docs/Unused-Private-Method.md +101 -0
- data/data/reek_docs/Utility-Function.md +57 -0
- data/data/reek_docs/Versioning-Policy.md +7 -0
- data/data/reek_docs/YAML-Reports.md +93 -0
- data/exe/snoot +5 -0
- data/lib/snoot/analyse_run/decision.rb +62 -0
- data/lib/snoot/analyse_run/result.rb +12 -0
- data/lib/snoot/analyse_run.rb +70 -0
- data/lib/snoot/analyser_orchestration/default.rb +149 -0
- data/lib/snoot/analyser_orchestration/result_mapping.rb +52 -0
- data/lib/snoot/analyser_orchestration.rb +21 -0
- data/lib/snoot/analyser_result.rb +14 -0
- data/lib/snoot/cli/event.rb +13 -0
- data/lib/snoot/cli/pipeline.rb +14 -0
- data/lib/snoot/cli.rb +147 -0
- data/lib/snoot/findings.rb +23 -0
- data/lib/snoot/render_report.rb +82 -0
- data/lib/snoot/run.rb +35 -0
- data/lib/snoot/state_error.rb +9 -0
- data/lib/snoot/value_types.rb +20 -0
- data/lib/snoot/version.rb +5 -0
- data/lib/snoot.rb +21 -0
- data/snoot.allium +482 -0
- 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: []
|