sqa-bi 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/.github/workflows/docs.yml +55 -0
- data/.quality/reek_baseline.txt +5 -0
- data/.rubocop.yml +224 -0
- data/CHANGELOG.md +33 -0
- data/CLAUDE.md +128 -0
- data/COMMITS.md +196 -0
- data/LICENSE.txt +21 -0
- data/README.md +229 -0
- data/Rakefile +170 -0
- data/decision_support_techniques.md +391 -0
- data/docs/EXPLORATION.md +128 -0
- data/docs/api/index.md +66 -0
- data/docs/api/likelihood.md +78 -0
- data/docs/api/llm-elicitors.md +119 -0
- data/docs/api/llm-support.md +136 -0
- data/docs/api/posterior.md +106 -0
- data/docs/api/prior.md +81 -0
- data/docs/api/time-series-predictor.md +96 -0
- data/docs/assets/css/custom.css +25 -0
- data/docs/assets/diagrams/architecture.svg +75 -0
- data/docs/assets/diagrams/bayes-pipeline.svg +48 -0
- data/docs/assets/diagrams/kde.svg +52 -0
- data/docs/assets/diagrams/llm-bayes-loop.svg +58 -0
- data/docs/assets/diagrams/provider-resolution.svg +77 -0
- data/docs/assets/js/mathjax.js +18 -0
- data/docs/development.md +164 -0
- data/docs/examples/index.md +198 -0
- data/docs/getting-started/core-concepts.md +119 -0
- data/docs/getting-started/installation.md +112 -0
- data/docs/getting-started/quick-start.md +143 -0
- data/docs/guide/likelihood.md +132 -0
- data/docs/guide/posterior.md +135 -0
- data/docs/guide/predictor.md +191 -0
- data/docs/guide/prior.md +141 -0
- data/docs/guide/tuning.md +156 -0
- data/docs/guide/uncertainty.md +148 -0
- data/docs/index.md +110 -0
- data/docs/llm/index.md +124 -0
- data/docs/llm/likelihood-estimation.md +161 -0
- data/docs/llm/prior-elicitation.md +172 -0
- data/docs/llm/providers.md +184 -0
- data/docs/requirements.txt +8 -0
- data/lib/sqa/bi/likelihood.rb +167 -0
- data/lib/sqa/bi/llm_likelihood_estimator.rb +110 -0
- data/lib/sqa/bi/llm_prior_elicitor.rb +110 -0
- data/lib/sqa/bi/llm_support.rb +299 -0
- data/lib/sqa/bi/posterior.rb +189 -0
- data/lib/sqa/bi/prior.rb +135 -0
- data/lib/sqa/bi/time_series_predictor.rb +219 -0
- data/lib/sqa/bi/version.rb +7 -0
- data/lib/sqa/bi.rb +57 -0
- data/mkdocs.yml +174 -0
- metadata +272 -0
data/README.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# SQA::BI
|
|
2
|
+
|
|
3
|
+
Bayesian inference over discrete outcomes from time series data.
|
|
4
|
+
|
|
5
|
+
`SQA::BI` answers one question: **given this feature vector, what is the
|
|
6
|
+
probability distribution over outcomes?** — where "outcomes" is a small
|
|
7
|
+
ordered set such as `[-2, -1, 0, 1, 2]`, read as strong downtrend through
|
|
8
|
+
strong uptrend.
|
|
9
|
+
|
|
10
|
+
It gives you a full posterior rather than a point estimate, so entropy,
|
|
11
|
+
confidence, and information gain come out of the math instead of being
|
|
12
|
+
bolted on afterward.
|
|
13
|
+
|
|
14
|
+
📖 **[Full documentation](https://madbomber.github.io/sqa-bi)** — guide, API
|
|
15
|
+
reference, LLM integration, and runnable examples.
|
|
16
|
+
|
|
17
|
+
**⚠️ WARNING:** This is a learning tool, not production software. DO NOT use
|
|
18
|
+
this library when real money is at stake. The probability distributions it
|
|
19
|
+
produces should not be taken seriously. If you lose your shirt playing in
|
|
20
|
+
the stock market, don't come crying to me. Playing in the market is like
|
|
21
|
+
playing in the street — you're going to get run over.
|
|
22
|
+
|
|
23
|
+
## Where it sits in the workspace
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
sqa-tai ─┐
|
|
27
|
+
sqa-bi ─┴→ sqa → sqa-cli / sqa-advisor / sqa-rails / sqa-sinatra
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`sqa-bi` is a **leaf** gem with no runtime dependencies. The inference math
|
|
31
|
+
is domain-agnostic — it knows nothing about markets — so `sqa` can depend on
|
|
32
|
+
it the same way it depends on `sqa-tai`. The market-facing demo
|
|
33
|
+
(`examples/03_stock_market_prediction_v2.rb`) reaches the other direction
|
|
34
|
+
and needs `sqa`, which is why `Gemfile.local` resolves `sqa` from the
|
|
35
|
+
sibling checkout for development only.
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
require "sqa/bi"
|
|
41
|
+
|
|
42
|
+
predictor = SQA::BI.predictor(outcomes: [-2, -1, 0, 1, 2], bandwidth: 1.0)
|
|
43
|
+
|
|
44
|
+
predictor.train([1.0, 2.0, 3.0], outcome: 1)
|
|
45
|
+
predictor.train([1.1, 2.1, 2.9], outcome: 1)
|
|
46
|
+
predictor.train([-1.0, -2.0, 0.5], outcome: -2)
|
|
47
|
+
|
|
48
|
+
posterior = predictor.predict([1.05, 2.05, 3.0])
|
|
49
|
+
|
|
50
|
+
posterior.max_outcome # => 1 (MAP estimate)
|
|
51
|
+
posterior.probability(1)# => 0.74
|
|
52
|
+
posterior.confidence # => 0.41 (1 - entropy / max entropy)
|
|
53
|
+
posterior.entropy # => 1.37 bits
|
|
54
|
+
puts posterior.summary
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Components
|
|
58
|
+
|
|
59
|
+
| Class | Role |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `Prior` | `P(outcome)` — uniform or custom, Laplace-smoothed updates from observed frequencies, weighted combination, entropy |
|
|
62
|
+
| `Likelihood` | `P(features \| outcome)` — Gaussian kernel density estimation over historical observations |
|
|
63
|
+
| `Posterior` | `P(outcome \| features)` — Bayes' theorem, plus entropy, confidence, KL divergence from the prior, MAP, sampling |
|
|
64
|
+
| `TimeSeriesPredictor` | the `train` / `predict` interface tying the three together |
|
|
65
|
+
| `LlmPriorElicitor` | an LLM supplies the prior from a natural-language description |
|
|
66
|
+
| `LlmLikelihoodEstimator` | an LLM acts as a likelihood function over textual evidence |
|
|
67
|
+
| `LlmSupport` | provider resolution, JSON extraction, re-keying, normalization, clamping |
|
|
68
|
+
|
|
69
|
+
**Bayes' theorem:** `P(outcome | data) = P(data | outcome) × P(outcome) / P(data)`
|
|
70
|
+
|
|
71
|
+
**Kernel density estimate for the likelihood:**
|
|
72
|
+
`P(x | outcome) = (1/n) × Σ K((x - xᵢ) / h)`, with `K` Gaussian and `h` the
|
|
73
|
+
bandwidth. Smaller bandwidth tracks local patterns; larger smooths.
|
|
74
|
+
|
|
75
|
+
## LLM integration
|
|
76
|
+
|
|
77
|
+
The design rule, stated once and applied everywhere:
|
|
78
|
+
|
|
79
|
+
> **The LLM judges. Ruby computes.**
|
|
80
|
+
> Ask the LLM only for isolated, independent judgments (a weight, a
|
|
81
|
+
> conditional probability). Never ask it to accumulate, normalize, or update
|
|
82
|
+
> beliefs — that is Bayes' job, done in Ruby.
|
|
83
|
+
|
|
84
|
+
LLMs and Bayes' theorem have exactly complementary failure modes: an LLM is
|
|
85
|
+
good at "how surprising is this log line if the database were down?" and bad
|
|
86
|
+
at combining five such judgments without anchoring or double-counting. The
|
|
87
|
+
posterior is good at exactly the second thing and has no opinion about the
|
|
88
|
+
first. See [`docs/EXPLORATION.md`](docs/EXPLORATION.md) for the verified
|
|
89
|
+
results behind both patterns.
|
|
90
|
+
|
|
91
|
+
**Prior elicitation** answers the standing objection to Bayesian methods —
|
|
92
|
+
where does the prior come from? — by treating the LLM as a queryable
|
|
93
|
+
compression of domain knowledge:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
elicitor = SQA::BI::LlmPriorElicitor.new(
|
|
97
|
+
outcomes: [-2, -1, 0, 1, 2],
|
|
98
|
+
outcome_descriptions: {
|
|
99
|
+
-2 => "strong downtrend", -1 => "mild downtrend", 0 => "sideways",
|
|
100
|
+
1 => "mild uptrend", 2 => "strong uptrend"
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
prior = elicitor.elicit("The Fed unexpectedly cut rates by 50bp...")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Likelihood estimation** handles evidence that is text rather than numbers:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
estimator = SQA::BI::LlmLikelihoodEstimator.new(
|
|
110
|
+
hypotheses: {
|
|
111
|
+
bad_deploy: "The 14:02 deploy introduced a bug",
|
|
112
|
+
database: "The primary database is degraded",
|
|
113
|
+
network: "There is a network partition between AZs"
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
estimator.likelihoods("Error rate spiked 2 minutes after deploy")
|
|
117
|
+
# => { bad_deploy: 0.9, database: 0.2, network: 0.15 }
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Likelihoods are clamped to `[0.001, 0.999]` so an overconfident model can
|
|
121
|
+
never zero out a hypothesis in a single step (Cromwell's rule) — which is
|
|
122
|
+
what keeps later contradicting evidence able to reverse the belief.
|
|
123
|
+
|
|
124
|
+
`ruby_llm` is required lazily inside `LlmSupport.build_chat`, so the core
|
|
125
|
+
math loads and runs without it. Both LLM classes accept an injectable
|
|
126
|
+
`chat:` object; the test suite makes zero network calls.
|
|
127
|
+
|
|
128
|
+
### Provider selection — local first
|
|
129
|
+
|
|
130
|
+
Auto-detected in order: LM Studio via `ruby_llm-providers-lms`
|
|
131
|
+
(`http://localhost:1234/v1`), then Apfel / Apple Foundation Models via
|
|
132
|
+
`ruby_llm-providers-apfel` (`http://127.0.0.1:11434/v1`), then cloud.
|
|
133
|
+
|
|
134
|
+
Detection probes each server, so **a local provider is only used when its
|
|
135
|
+
server is actually running with a chat model available**. For LM Studio
|
|
136
|
+
that means two things, neither of which the desktop app does by itself:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
lms status # Server: ON / OFF
|
|
140
|
+
lms server start # start the HTTP server on :1234
|
|
141
|
+
lms ps # which models are loaded
|
|
142
|
+
lms load qwen/qwen3.8-27b # load one (choose_local_model prefers qwen)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
With the server off, or on but offering only embedding models, resolution
|
|
146
|
+
falls through to cloud — silently, by design. That is how an expected
|
|
147
|
+
local run turns into an unexpected cloud bill or a 401 from a stale key.
|
|
148
|
+
The LLM demos therefore print where they are actually sending each
|
|
149
|
+
question before the first call:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
LLM: lms — qwen/qwen3.8-27b at http://localhost:1234/v1
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`SQA::BI::LlmSupport.current_resolution` returns that line if you want the
|
|
156
|
+
same check in your own code.
|
|
157
|
+
|
|
158
|
+
| Variable | Effect |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `SQA_BI_LLM_PROVIDER` | force `lms`, `apfel`, or `cloud` |
|
|
161
|
+
| `SQA_BI_LLM_MODEL` | force a model id |
|
|
162
|
+
| `LMS_API_BASE`, `APFEL_API_BASE` | override the local server URLs |
|
|
163
|
+
|
|
164
|
+
The unprefixed `BI_LLM_PROVIDER` / `BI_LLM_MODEL` names this library used
|
|
165
|
+
before it moved into the SQA workspace are still honored as a fallback.
|
|
166
|
+
|
|
167
|
+
For local servers, `choose_local_model` prefers qwen models — they honor
|
|
168
|
+
JSON prompts most reliably in LM Studio — then gpt-oss, skipping embedding
|
|
169
|
+
and OCR models. Model quality shows up directly as prior sharpness: qwen3.8-27b
|
|
170
|
+
via LM Studio produced a bullish prior peaked at +1, while Apple's 3B
|
|
171
|
+
on-device model was more cautious and peaked at 0.
|
|
172
|
+
|
|
173
|
+
## Examples
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
ruby examples/01_coin_flip.rb # the mechanics, minimal
|
|
177
|
+
ruby examples/02_time_series_prediction.rb # synthetic trends
|
|
178
|
+
ruby examples/03_stock_market_prediction_v2.rb # real OHLCV via sqa
|
|
179
|
+
ruby examples/04_llm_elicited_prior.rb # LLM as prior elicitor
|
|
180
|
+
ruby examples/05_llm_likelihood_diagnosis.rb # LLM as likelihood function
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Example 03 needs the `sqa` gem, so run it in dev bundle mode
|
|
184
|
+
(`asgard dev` from the workspace root, which points `BUNDLE_GEMFILE` at
|
|
185
|
+
`Gemfile.local`). `examples/pure_ruby_indicators.rb` carries dependency-free
|
|
186
|
+
SMA/EMA/RSI so the other examples stay standalone — production code should
|
|
187
|
+
use `sqa-tai` instead.
|
|
188
|
+
|
|
189
|
+
## Documentation
|
|
190
|
+
|
|
191
|
+
The detailed docs are an MkDocs site under `docs/`:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
pip install -r docs/requirements.txt
|
|
195
|
+
mkdocs serve # live reload at http://127.0.0.1:8000
|
|
196
|
+
mkdocs build --strict
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`--strict` turns the link and anchor validation configured in `mkdocs.yml` into
|
|
200
|
+
build errors, so a broken cross-reference fails the build rather than shipping.
|
|
201
|
+
`.github/workflows/docs.yml` publishes to GitHub Pages on push to `main`.
|
|
202
|
+
|
|
203
|
+
Diagrams are hand-authored SVG in `docs/assets/diagrams/` — dark theme,
|
|
204
|
+
transparent background, colour used to distinguish function.
|
|
205
|
+
|
|
206
|
+
## Development
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
asgard test # or: bundle exec rake test
|
|
210
|
+
asgard quality # tests + coverage, RuboCop, Flog, Flay, Reek
|
|
211
|
+
asgard rubocop
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Reek runs baseline-aware against `.quality/reek_baseline.txt`; ratchet the
|
|
215
|
+
floor down with `asgard reek_baseline` after a genuine improvement. The
|
|
216
|
+
RuboCop config is generated from the workspace's `.rubocop.yml.common` —
|
|
217
|
+
edit that file and run `asgard sync_rubocop`, not this repo's copy.
|
|
218
|
+
|
|
219
|
+
## History
|
|
220
|
+
|
|
221
|
+
Ported from the `bayesian_inference` prototype in
|
|
222
|
+
`~/sandbox/git_repos/madbomber/experiments/ai_misc/`. See
|
|
223
|
+
[`decision_support_techniques.md`](decision_support_techniques.md) for the
|
|
224
|
+
investigation log that led here, including why a tree-ensemble forecaster
|
|
225
|
+
and the Laya typed-decision model were each evaluated and set aside.
|
|
226
|
+
|
|
227
|
+
## License
|
|
228
|
+
|
|
229
|
+
MIT. See [LICENSE.txt](LICENSE.txt).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rake/testtask"
|
|
5
|
+
|
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
|
7
|
+
t.libs << "test"
|
|
8
|
+
t.libs << "lib"
|
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
10
|
+
t.warning = false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
task default: :test
|
|
14
|
+
|
|
15
|
+
desc "Check code style with RuboCop"
|
|
16
|
+
task :rubocop do
|
|
17
|
+
sh "bundle exec rubocop --format simple"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
desc "Auto-correct RuboCop offenses"
|
|
21
|
+
task :rubocop_fix do
|
|
22
|
+
sh "bundle exec rubocop -A"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
desc "Check code complexity with Flog (warn >=20, fail >=50)"
|
|
26
|
+
task :flog_check do
|
|
27
|
+
require "flog"
|
|
28
|
+
|
|
29
|
+
warn_threshold = 20.0
|
|
30
|
+
fail_threshold = 50.0
|
|
31
|
+
|
|
32
|
+
flogger = Flog.new(all: true)
|
|
33
|
+
flogger.flog(*Dir.glob("lib/**/*.rb"))
|
|
34
|
+
|
|
35
|
+
warnings = []
|
|
36
|
+
failures = []
|
|
37
|
+
|
|
38
|
+
flogger.each_by_score do |method, score|
|
|
39
|
+
next if method.end_with?("#none")
|
|
40
|
+
|
|
41
|
+
if score > fail_threshold
|
|
42
|
+
failures << "#{format("%.1f", score)}: #{method}"
|
|
43
|
+
elsif score > warn_threshold
|
|
44
|
+
warnings << "#{format("%.1f", score)}: #{method}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
unless warnings.empty?
|
|
49
|
+
puts "\nFlog warnings (#{warn_threshold}–#{fail_threshold}) — target for future refactoring:"
|
|
50
|
+
warnings.each { |v| puts " #{v}" }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if failures.empty?
|
|
54
|
+
puts "\nFlog: no methods exceed the failure threshold (>=#{fail_threshold})"
|
|
55
|
+
else
|
|
56
|
+
puts "\nFlog failures (>=#{fail_threshold}) — must be refactored:"
|
|
57
|
+
failures.each { |v| puts " #{v}" }
|
|
58
|
+
abort "\nFlog quality gate failed: #{failures.size} method(s) exceed #{fail_threshold}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
desc "Check for structural code duplication with Flay (mass >= 50)"
|
|
63
|
+
task :flay_check do
|
|
64
|
+
require "flay"
|
|
65
|
+
|
|
66
|
+
mass_threshold = 50
|
|
67
|
+
|
|
68
|
+
flay = Flay.new(mass: mass_threshold, diff: false, verbose: false, summary: false, timeout: 60)
|
|
69
|
+
flay.process(*Dir.glob("lib/**/*.rb"))
|
|
70
|
+
flay.analyze
|
|
71
|
+
|
|
72
|
+
if flay.hashes.empty?
|
|
73
|
+
puts "\nFlay: no structural duplication detected (mass >= #{mass_threshold})"
|
|
74
|
+
else
|
|
75
|
+
puts "\nFlay found structural duplication (mass >= #{mass_threshold}):"
|
|
76
|
+
flay.report
|
|
77
|
+
abort "\nFlay quality gate failed: #{flay.hashes.length} pattern(s) detected"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
desc "Check code smells with Reek (fails only on new/worsened files vs .quality/reek_baseline.txt)"
|
|
82
|
+
task :reek_check do
|
|
83
|
+
require "reek"
|
|
84
|
+
require "reek/configuration/app_configuration"
|
|
85
|
+
|
|
86
|
+
# Resolve .reek.yml by walking up from cwd: a gem-level config wins,
|
|
87
|
+
# otherwise the shared workspace-level one is used.
|
|
88
|
+
config_file = nil
|
|
89
|
+
dir = Pathname.new(Dir.pwd).expand_path
|
|
90
|
+
loop do
|
|
91
|
+
candidate = dir.join(".reek.yml")
|
|
92
|
+
if candidate.exist?
|
|
93
|
+
config_file = candidate.to_s
|
|
94
|
+
break
|
|
95
|
+
end
|
|
96
|
+
break if dir.root?
|
|
97
|
+
|
|
98
|
+
dir = dir.parent
|
|
99
|
+
end
|
|
100
|
+
config = config_file ? Reek::Configuration::AppConfiguration.from_path(Pathname.new(config_file)) : nil
|
|
101
|
+
|
|
102
|
+
# Smell count per file (only files with at least one smell).
|
|
103
|
+
current = Dir.glob("lib/**/*.rb").each_with_object({}) do |file, acc|
|
|
104
|
+
count = Reek::Examiner.new(File.read(file), configuration: config).smells.size
|
|
105
|
+
acc[file] = count if count.positive?
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Grandfathered smell counts from .quality/reek_baseline.txt ("count\tfile").
|
|
109
|
+
baseline_path = File.join(".quality", "reek_baseline.txt")
|
|
110
|
+
baseline = {}
|
|
111
|
+
if File.exist?(baseline_path)
|
|
112
|
+
File.readlines(baseline_path).each do |line|
|
|
113
|
+
count, file = line.strip.split("\t", 2)
|
|
114
|
+
baseline[file] = count.to_i if file && !file.empty?
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
new_files = current.reject { |file, _| baseline.key?(file) }
|
|
119
|
+
worsened = current.select { |file, count| baseline.key?(file) && count > baseline[file] }
|
|
120
|
+
|
|
121
|
+
puts "\nReek: #{current.values.sum} warning(s) across #{current.size} file(s) " \
|
|
122
|
+
"(#{baseline.values.sum} grandfathered across #{baseline.size} file(s))."
|
|
123
|
+
|
|
124
|
+
if new_files.empty? && worsened.empty?
|
|
125
|
+
puts "Reek: no new or worsened files (quality gate passed)"
|
|
126
|
+
else
|
|
127
|
+
puts "\nReek quality gate failed:"
|
|
128
|
+
new_files.each { |file, count| puts " NEW #{count.to_s.rjust(3)} warning(s): #{file}" }
|
|
129
|
+
worsened.each { |file, count| puts " WORSENED #{baseline[file].to_s.rjust(3)} -> #{count.to_s.rjust(3)}: #{file}" }
|
|
130
|
+
abort "\nReek quality gate failed: #{new_files.size + worsened.size} file(s) regressed. " \
|
|
131
|
+
"Fix smells, or run `asgard reek_baseline` if intentional."
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
desc "Run all quality checks: tests (with coverage), Flog, Flay, and Reek"
|
|
136
|
+
task :quality do
|
|
137
|
+
results = {}
|
|
138
|
+
|
|
139
|
+
puts "\n#{"=" * 60}"
|
|
140
|
+
puts "Quality Gate: Tests + Coverage"
|
|
141
|
+
puts "=" * 60
|
|
142
|
+
results[:tests] = system("bundle exec rake test") ? :pass : :fail
|
|
143
|
+
|
|
144
|
+
puts "\n#{"=" * 60}"
|
|
145
|
+
puts "Quality Gate: Flog Complexity"
|
|
146
|
+
puts "=" * 60
|
|
147
|
+
results[:flog] = system("bundle exec rake flog_check") ? :pass : :fail
|
|
148
|
+
|
|
149
|
+
puts "\n#{"=" * 60}"
|
|
150
|
+
puts "Quality Gate: Flay Duplication"
|
|
151
|
+
puts "=" * 60
|
|
152
|
+
results[:flay] = system("bundle exec rake flay_check") ? :pass : :fail
|
|
153
|
+
|
|
154
|
+
puts "\n#{"=" * 60}"
|
|
155
|
+
puts "Quality Gate: Reek Smells"
|
|
156
|
+
puts "=" * 60
|
|
157
|
+
results[:reek] = system("bundle exec rake reek_check") ? :pass : :fail
|
|
158
|
+
|
|
159
|
+
puts "\n#{"=" * 60}"
|
|
160
|
+
puts "Quality Summary"
|
|
161
|
+
puts "=" * 60
|
|
162
|
+
results.each do |gate, status|
|
|
163
|
+
icon = status == :pass ? "PASS" : "FAIL"
|
|
164
|
+
puts " [#{icon}] #{gate}"
|
|
165
|
+
end
|
|
166
|
+
puts "=" * 60
|
|
167
|
+
|
|
168
|
+
abort "\nQuality gate failed" if results.values.any?(:fail)
|
|
169
|
+
puts "\nAll quality gates passed."
|
|
170
|
+
end
|