sqa-tai 0.1.1 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c708d46e3d535588af3a14a4d209e32e79a34fa11733c3a20cec8e8a25ddf8f
4
- data.tar.gz: 665274f5b878d3165f50edfc78a6c6903c95f3696483357d07b12b929f2a00fb
3
+ metadata.gz: ae6d7d288fb0bea0c3664ce0daf6cb02d73fb0d9590637431cd276c0b00e9efc
4
+ data.tar.gz: 8c10225300c771ace26256f9724f37ca84261bb718ebe9e8b061fbc51c7ad1dd
5
5
  SHA512:
6
- metadata.gz: 72e84001d6624688c24800916e2d9651a13d4047e3dc10e6dd53725b1ea464748890df15e4c35b74e1def91429dc2819750d87529cdb86f2d4fa3a8295fca706
7
- data.tar.gz: b0a2341c3d7777326cddd41f50254f9ff5a290c73b63a6782495fc5c94a29759daa7d0d752cc82e1ea2cd3f1f749bb866ed69f76ba89d12cb1afc13959851a66
6
+ metadata.gz: de3f11e6ca8fdf99034d951a74a35d8930585325a0cd91098330e6765cae71d1600c1b7fae05e79a7a4f50f9a1e2f164e4998520037179fca6ff5ea7ca03a479
7
+ data.tar.gz: 8f6c5bff1286e2833ec9c846b7b6243f9d313b9d17fe49da857567ae97adcfff70b1ab3adf50a17ef5425d24c79840d658563b600de77b3332879515432f2f9f
@@ -0,0 +1,33 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(find:*)",
5
+ "Bash(bundle exec rake:*)",
6
+ "Bash(ruby -e:*)",
7
+ "Bash(gem which:*)",
8
+ "Bash(xargs dirname:*)",
9
+ "Bash(xargs:*)",
10
+ "Bash(gem info:*)",
11
+ "Bash(ruby analyze_indicators.rb:*)",
12
+ "Bash(bundle exec rspec:*)",
13
+ "Bash(ruby count_indicators.rb:*)",
14
+ "Bash(ruby test_new_indicators.rb:*)",
15
+ "Bash(ruby verify_count.rb:*)",
16
+ "Bash(ruby generate_docs.rb:*)",
17
+ "Bash(mkdocs build:*)",
18
+ "Bash(test:*)",
19
+ "Bash(for file in cdl_counterattack.md cdl_darkcloudcover.md cdl_dojistar.md cdl_dragonflydoji.md cdl_eveningdojistar.md cdl_eveningstar.md cdl_gapsidesidewhite.md cdl_gravestonedoji.md cdl_hammer.md cdl_hangingman.md)",
20
+ "Bash(done)",
21
+ "Bash(wc:*)",
22
+ "Bash(for file in cdl_sticksandwich cdl_matchinglow cdl_identical3crows cdl_unique3river cdl_tristar cdl_stalledpattern cdl_tasukigap cdl_xsidegap3methods cdl_upsidegap2crows cdl_longline cdl_shortline)",
23
+ "Bash(do echo \"Processing $file\")",
24
+ "Bash(ls:*)",
25
+ "Bash(ruby:*)",
26
+ "Bash(git commit:*)",
27
+ "Bash(mkdir:*)",
28
+ "Bash(bundle install:*)"
29
+ ],
30
+ "deny": [],
31
+ "ask": []
32
+ }
33
+ }
data/.envrc CHANGED
@@ -1 +1,6 @@
1
- export RR=`pwd`
1
+ # sqa_project/sqa-tai/.envrc
2
+
3
+ source_up
4
+
5
+ export RR=$(pwd)
6
+ # BUNDLE_GEMFILE is inherited from the project root .envrc (asgard dev|prod)
data/.loki ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+ # sqa-tai — Ruby wrapper around TA-Lib, 136+ technical indicators.
3
+
4
+ import_up "repo_dev.loki"
5
+
6
+ class Tasks
7
+ @@gem_name ||= "sqa-tai".freeze
8
+
9
+ header "sqa-tai v#{gem_version} — TA-Lib technical indicators"
10
+ end
File without changes
data/.rubocop.yml ADDED
@@ -0,0 +1,221 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ SuggestExtensions: false
4
+ TargetRubyVersion: 4.0
5
+ Exclude:
6
+ - 'examples/**/*'
7
+ - 'docs/**/*'
8
+ - 'site/**/*'
9
+ - 'pkg/**/*'
10
+ - 'coverage/**/*'
11
+ # Untracked scratch/dev scripts left at repo root in sqa-tai (not part of the shipped gem)
12
+ - 'add_array_order_note.rb'
13
+ - 'complete_all_patterns.rb'
14
+ - 'generate_remaining_patterns.rb'
15
+
16
+ # ── Style: disabled cops ───────────────────────────────────────────────────
17
+ Style/StringLiterals:
18
+ Enabled: false
19
+
20
+ Style/StringLiteralsInInterpolation:
21
+ Enabled: false
22
+
23
+ Style/Documentation:
24
+ Enabled: false
25
+
26
+ # Ruby 4.0 freezes string literals by default
27
+ Style/FrozenStringLiteralComment:
28
+ Enabled: false
29
+
30
+ Style/IfUnlessModifier:
31
+ Enabled: false
32
+
33
+ Style/RescueModifier:
34
+ Enabled: false
35
+
36
+ Style/TrivialAccessors:
37
+ Enabled: false
38
+
39
+ Style/MultilineTernaryOperator:
40
+ Enabled: false
41
+
42
+ Style/SafeNavigation:
43
+ Enabled: false
44
+
45
+ Style/EmptyClassDefinition:
46
+ Enabled: false
47
+
48
+ Style/ClassAndModuleChildren:
49
+ Enabled: false
50
+
51
+ Style/RescueStandardError:
52
+ Enabled: false
53
+
54
+ Style/OneClassPerFile:
55
+ Enabled: false
56
+
57
+ # Both % and format/sprintf are acceptable
58
+ Style/FormatString:
59
+ Enabled: false
60
+
61
+ # String concatenation and interpolation are both acceptable
62
+ Style/StringConcatenation:
63
+ Enabled: false
64
+
65
+ # OpenStruct is a deliberate architectural choice throughout this ecosystem —
66
+ # strategy/backtest/stream vectors are built as OpenStructs on purpose (see
67
+ # sqa/CLAUDE.md). Not a candidate for a Struct/class/test-double rewrite.
68
+ Style/OpenStructUse:
69
+ Enabled: false
70
+
71
+ # $DEBUG_ME (debug_me gem convention) and test fixture globals like $data are
72
+ # established patterns here, not accidental global-variable leaks.
73
+ Style/GlobalVars:
74
+ Enabled: false
75
+
76
+ # ── Layout ─────────────────────────────────────────────────────────────────
77
+ Layout/LineLength:
78
+ Max: 140
79
+
80
+ Layout/ExtraSpacing:
81
+ Enabled: false
82
+
83
+ Layout/HashAlignment:
84
+ Enabled: false
85
+
86
+ Layout/FirstHashElementIndentation:
87
+ Enabled: false
88
+
89
+ Layout/EmptyLineAfterGuardClause:
90
+ Enabled: false
91
+
92
+ # ── Naming ─────────────────────────────────────────────────────────────────
93
+ # Single-char params (c, e, n) are acceptable throughout
94
+ Naming/MethodParameterName:
95
+ Enabled: false
96
+
97
+ # sma_20, percentile_95, and similar parameterized names are more readable
98
+ # than the normalcase default (sma20, percentile95). alpha_vantage_api.rb is
99
+ # excluded entirely: it mirrors Alpha Vantage's literal API function/parameter
100
+ # names (t3, timeperiod1/2/3, ...) verbatim, which we can't rename. Same
101
+ # reasoning for sqa-tai's overlap_studies.rb (t3) and momentum_indicators.rb
102
+ # (ultosc's time_period1/2/3) — TA-Lib's own C function/parameter names.
103
+ Naming/VariableNumber:
104
+ EnforcedStyle: snake_case
105
+ Exclude:
106
+ - 'test/**/*'
107
+ - 'lib/api/alpha_vantage_api.rb'
108
+ - 'lib/sqa/tai/overlap_studies.rb'
109
+ - 'lib/sqa/tai/momentum_indicators.rb'
110
+ - 'lib/sqa/data_frame/yahoo_finance.rb' # period1/period2 mirror Yahoo's own chart API query params verbatim
111
+
112
+ # sqa-cli's gem entry point must be lib/sqa-cli.rb (hyphen) so that
113
+ # `require 'sqa-cli'` resolves by exact match on the gem name; renaming to
114
+ # snake_case would break the require path. (Only sqa-cli has such a file.)
115
+ Naming/FileName:
116
+ Exclude:
117
+ - 'lib/sqa-cli.rb'
118
+
119
+ Naming/RescuedExceptionsVariableName:
120
+ Enabled: false
121
+
122
+ # set_results and similar explicit setters are clear and conventional
123
+ Naming/AccessorMethodName:
124
+ Enabled: false
125
+
126
+ # has_tool_calls? and similar are clear and conventional
127
+ Naming/PredicatePrefix:
128
+ Enabled: false
129
+
130
+ # Test helper methods don't need to follow predicate naming rules
131
+ Naming/PredicateMethod:
132
+ Exclude:
133
+ - 'test/**/*'
134
+
135
+ # ── Lint: relax noisy cops on intentional patterns ─────────────────────────
136
+ Lint/EmptyBlock:
137
+ Exclude:
138
+ - 'test/**/*'
139
+
140
+ # Library and framework methods commonly accept args for API/documentation purposes
141
+ Lint/UnusedMethodArgument:
142
+ Enabled: false
143
+
144
+ Lint/ConstantDefinitionInBlock:
145
+ Exclude:
146
+ - 'Rakefile'
147
+ - 'test/**/*'
148
+
149
+ # ── Gemspec ────────────────────────────────────────────────────────────────
150
+ # All 5 sqa gem repos declare dev deps directly in the gemspec (not a Gemfile
151
+ # group) — the opposite convention from robot_lab_project, so match reality.
152
+ Gemspec/DevelopmentDependencies:
153
+ EnforcedStyle: gemspec
154
+
155
+ Gemspec/RequiredRubyVersion:
156
+ Enabled: false
157
+
158
+ Gemspec/OrderedDependencies:
159
+ Enabled: false
160
+
161
+ # ── Metrics ────────────────────────────────────────────────────────────────
162
+ # Flog is the primary complexity gate (see repo_dev.loki flog_check) — these
163
+ # RuboCop thresholds catch only egregious outliers without duplicating flog's
164
+ # more precise per-method scoring.
165
+
166
+ Metrics/MethodLength:
167
+ Max: 35
168
+ CountAsOne:
169
+ - heredoc
170
+ - array
171
+ - hash
172
+ Exclude:
173
+ - 'test/**/*'
174
+ - 'bin/**/*'
175
+
176
+ Metrics/AbcSize:
177
+ Max: 40
178
+ Exclude:
179
+ - 'test/**/*'
180
+ - 'bin/**/*'
181
+
182
+ Metrics/ClassLength:
183
+ Max: 650 # matches robot_lab_project: central orchestrator classes are legitimately large
184
+ Exclude:
185
+ - 'test/**/*'
186
+ - 'bin/**/*'
187
+
188
+ Metrics/ModuleLength:
189
+ Max: 200
190
+ Exclude:
191
+ - 'test/**/*'
192
+ - 'bin/**/*'
193
+ # Many independent thin TA-Lib indicator wrappers — naturally long by breadth, not complexity
194
+ - 'lib/sqa/tai/pattern_recognition.rb'
195
+ - 'lib/sqa/tai/momentum_indicators.rb'
196
+ # Cohesive groups of route helpers extracted during the flog complexity pass
197
+ - 'lib/sqa_demo/sinatra/helpers/api_helpers.rb'
198
+ - 'lib/sqa_demo/sinatra/helpers/stock_loader.rb'
199
+
200
+ Metrics/CyclomaticComplexity:
201
+ Max: 20
202
+ Exclude:
203
+ - 'test/**/*'
204
+ - 'bin/**/*'
205
+
206
+ Metrics/PerceivedComplexity:
207
+ Max: 20
208
+ Exclude:
209
+ - 'test/**/*'
210
+ - 'bin/**/*'
211
+
212
+ # Long method signatures with keyword args are common in this codebase
213
+ Metrics/ParameterLists:
214
+ Enabled: false
215
+
216
+ Metrics/BlockLength:
217
+ Exclude:
218
+ - 'Rakefile'
219
+ - '*.gemspec'
220
+ - 'test/**/*'
221
+ - 'bin/**/*'
data/CHANGELOG.md CHANGED
@@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2026-09-18
11
+ ### Changed
12
+ - Dropped the unmaintained `ta_lib_ffi` gem dependency. `sqa-tai` now talks
13
+ to the installed TA-Lib C library directly via a vendored Fiddle binding
14
+ (`SQA::TAI::Native`), using TA-Lib's own abstract interface
15
+ (`TA_GetFuncInfo`/`TA_Get{Input,OptInput,Output}ParameterInfo`) to
16
+ discover each function's signature at runtime instead of depending on
17
+ hand-written, unmaintained bindings.
18
+ ### Fixed
19
+ - Fixed an ABI mismatch where `ta_lib_ffi`'s `TA_FuncInfo` struct declared
20
+ a `camelCaseName` field that doesn't exist in current TA-Lib builds
21
+ (Homebrew's 0.8.x community fork), which misaligned every subsequent
22
+ struct field and caused any indicator call to hang while allocating
23
+ unbounded memory.
24
+ - Fixed indicators whose TA-Lib input is `TA_Input_Price` (all OHLC(V)
25
+ indicators and all 61 candlestick pattern functions) to bundle their
26
+ price arrays into a single argument (e.g. `atr([high, low, close], ...)`)
27
+ as the C API requires, instead of passing them as separate positional
28
+ arguments — this had previously been worked around with a monkey patch
29
+ (`lib/extensions/ta_lib_ffi.rb`, now removed) that patched the wrong
30
+ layer instead of fixing the call sites.
31
+
32
+ ## [0.3.0] - 2026-07-02
33
+ - Coordinated version bump to v0.3.0 across the SQA workspace.
34
+
35
+ ## [0.1.2] 2025-11-13
36
+ ### Added
37
+ - Help system for accessing indicator documentation (`SQA::TAI.help`)
38
+ - Returns URLs to online documentation for any indicator
39
+ - Supports searching by name, filtering by category, and listing all indicators
40
+ - Auto-generated from mkdocs documentation files via `rake help:generate`
41
+ - Flexible API returns Help::Resource objects with formatted output
42
+ - Optional browser integration to open documentation directly
43
+ - Zeitwerk-compatible namespace structure with `help/resource.rb` and `help/data.json`
44
+ - Lazy-loaded JSON data for improved gem load performance
45
+ - Formatted `to_s` output displaying indicator name, category, and website URL
46
+
10
47
  ## [0.1.1] 2025-11-13
11
48
  ### Added
12
49
  - Intraday Momentum Index (IMI) indicator
data/Gemfile CHANGED
@@ -4,14 +4,6 @@ source "https://rubygems.org"
4
4
 
5
5
  gemspec
6
6
 
7
- group :development, :test do
8
- gem "rake", "~> 13.0"
9
- gem "minitest", "~> 5.0"
10
- gem "minitest-reporters"
11
- gem "simplecov", require: false
12
- gem "debug"
13
- end
14
-
15
7
  group :documentation do
16
8
  # MkDocs is Python-based, managed separately
17
9
  # See docs/README.md for setup instructions
data/Gemfile.local ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Local cross-repo development Gemfile.
4
+ #
5
+ # sqa-tai is the leaf of the SQA dependency chain (it has no sibling
6
+ # SQA-ecosystem dependencies), so this file is identical to Gemfile.
7
+ # It exists so that `BUNDLE_GEMFILE=Gemfile.local` works uniformly in
8
+ # every repo of the workspace.
9
+ #
10
+ # Activate with: BUNDLE_GEMFILE=Gemfile.local bundle <command>
11
+
12
+ source "https://rubygems.org"
13
+
14
+ gemspec
15
+
16
+ group :documentation do
17
+ # MkDocs is Python-based, managed separately
18
+ # See docs/README.md for setup instructions
19
+ end
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <div align="center">
2
2
  <h1>SQA::TAI - Technical Analysis Indicators</h1>
3
3
 
4
- <p>Ruby wrapper around [TA-Lib](https://ta-lib.org/) providing **136 technical analysis indicators** for stock market analysis.<br/>
4
+ <p>Ruby wrapper around [TA-Lib](https://ta-lib.org/) providing **134 technical analysis indicators** for stock market analysis.<br/>
5
5
  Part of the [SQA](https://github.com/MadBomber/sqa) (Simple Qualitative Analysis) ecosystem.</p>
6
6
  </div>
7
7
 
@@ -15,14 +15,13 @@
15
15
 
16
16
  ## Features
17
17
 
18
- - 🚀 **136 Indicators** - Comprehensive coverage with 94% of trading-relevant TA-Lib indicators
19
- - ⚡ **Blazing Fast** - C library performance with Ruby convenience
18
+ - 🚀 **134 Indicators** - Comprehensive coverage with 94% of trading-relevant TA-Lib indicators
19
+ - ⚡ **Blazing Fast** - Talks directly to the TA-Lib C library via a vendored Fiddle binding, no unmaintained FFI gem in between
20
20
  - 🎯 **Clean API** - Simple, intuitive Ruby interface with keyword arguments
21
21
  - 📊 **Comprehensive** - Overlap studies, momentum, volatility, volume, cycles, stats, patterns
22
22
  - 🔌 **Easy Integration** - Works seamlessly with existing Ruby trading tools
23
23
  - ✅ **Well Tested** - 73 tests, 332 assertions, 100% passing
24
24
  - 📚 **Documented** - Full documentation at [madbomber.github.io/sqa-tai](https://madbomber.github.io/sqa-tai)
25
- - 🔧 **Bug Fixed** - Includes monkey patch for ta_lib_ffi 0.3.0 multi-array parameter bug
26
25
 
27
26
  </td>
28
27
  </tr>
@@ -86,17 +85,45 @@ if SQA::TAI.available?
86
85
  end
87
86
  ```
88
87
 
89
- ## Available Indicators (136 Total)
88
+ ## Getting Help on Indicators
90
89
 
91
- ### Overlap Studies (15)
92
- - **SMA, EMA, WMA** - Moving Averages
90
+ Access comprehensive documentation for any indicator directly from your code:
91
+
92
+ ```ruby
93
+ # Get documentation URL for an indicator
94
+ help = SQA::TAI.help(:sma)
95
+ puts help.url
96
+ # => https://madbomber.github.io/sqa-tai/indicators/overlap/sma/
97
+
98
+ # Open documentation in browser
99
+ SQA::TAI.help(:rsi, open: true)
100
+
101
+ # Search for indicators
102
+ SQA::TAI.help(search: "momentum")
103
+ # => {mom: "https://...", cmo: "https://...", ...}
104
+
105
+ # List all indicators in a category
106
+ SQA::TAI.help(category: :momentum_indicators)
107
+ # => {rsi: "https://...", macd: "https://...", ...}
108
+
109
+ # Get all available indicators
110
+ SQA::TAI.help(:all)
111
+ # => {sma: "https://...", ema: "https://...", ...}
112
+ ```
113
+
114
+ See the [Help System Guide](https://madbomber.github.io/sqa-tai/getting-started/help/) for more details.
115
+
116
+ ## Available Indicators (134 Total)
117
+
118
+ ### Overlap Studies (17)
119
+ - **SMA, EMA, WMA, MA** - Moving Averages
93
120
  - **DEMA, TEMA, TRIMA** - Advanced Moving Averages
94
121
  - **KAMA, T3, MAMA** - Adaptive Moving Averages
95
- - **BBANDS** - Bollinger Bands
122
+ - **BBANDS, ACCBANDS** - Bands
96
123
  - **SAREXT, HT_TRENDLINE** - Trend indicators
97
124
  - **MIDPOINT, MIDPRICE, MAVP** - Price calculations
98
125
 
99
- ### Momentum Indicators (30)
126
+ ### Momentum Indicators (31)
100
127
  - **RSI** - Relative Strength Index
101
128
  - **MACD, MACDEXT, MACDFIX** - MACD variants
102
129
  - **STOCH, STOCHF, STOCHRSI** - Stochastic variants
@@ -110,6 +137,7 @@ end
110
137
  - **BOP** - Balance of Power
111
138
  - **CMO** - Chande Momentum Oscillator
112
139
  - **MFI** - Money Flow Index
140
+ - **IMI** - Intraday Momentum Index
113
141
  - **PLUS_DI, MINUS_DI, PLUS_DM, MINUS_DM** - Directional indicators
114
142
  - **TRIX** - Triple Smooth EMA
115
143
  - **ULTOSC** - Ultimate Oscillator
@@ -282,7 +310,6 @@ The gem is available as open source under the terms of the [MIT License](LICENSE
282
310
  ## Acknowledgments
283
311
 
284
312
  - [TA-Lib](https://ta-lib.org/) - The underlying C library
285
- - [ta_lib_ffi](https://github.com/TA-Lib/ta-lib-ruby) - Ruby FFI wrapper
286
313
 
287
314
  ## Support
288
315
 
data/Rakefile CHANGED
@@ -11,3 +11,228 @@ Rake::TestTask.new(:test) do |t|
11
11
  end
12
12
 
13
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
171
+
172
+ namespace :help do
173
+ desc "Generate help mappings from documentation files"
174
+ task :generate do
175
+ require "fileutils"
176
+
177
+ indicators = {}
178
+ category_mapping = {
179
+ "overlap" => :overlap_studies,
180
+ "momentum" => :momentum_indicators,
181
+ "volatility" => :volatility_indicators,
182
+ "volume" => :volume_indicators,
183
+ "price_transform" => :price_transform,
184
+ "statistical" => :statistical_functions,
185
+ "cycle" => :cycle_indicators,
186
+ "patterns" => :pattern_recognition
187
+ }
188
+
189
+ # Scan docs directory
190
+ Dir.glob("docs/indicators/**/*.md").each do |file|
191
+ next if file.include?("index.md")
192
+
193
+ # Extract indicator name from filename
194
+ indicator = File.basename(file, ".md").to_sym
195
+
196
+ # Extract category from path
197
+ path_category = File.dirname(file).split("/").last
198
+ category = category_mapping[path_category] || path_category.to_sym
199
+
200
+ # Read first heading for name
201
+ name = nil
202
+ File.open(file, "r") do |f|
203
+ f.each_line do |line|
204
+ next unless line.start_with?("# ")
205
+ name = line.gsub(/^#\s*/, "").strip
206
+ # Remove content after parentheses if present
207
+ name = name.split("(").first.strip if name.include?("(")
208
+ break
209
+ end
210
+ end
211
+
212
+ # Build path relative to docs/
213
+ path = file.gsub("docs/", "").gsub(".md", "")
214
+
215
+ indicators[indicator] = {
216
+ name: name || indicator.to_s.upcase,
217
+ category: category,
218
+ path: path
219
+ }
220
+ end
221
+
222
+ # Convert to JSON-friendly format (strings instead of symbols)
223
+ json_data = indicators.transform_keys(&:to_s).transform_values do |meta|
224
+ {
225
+ "name" => meta[:name],
226
+ "category" => meta[:category].to_s,
227
+ "path" => meta[:path]
228
+ }
229
+ end
230
+
231
+ # Write to JSON file
232
+ require "json"
233
+ json_output = JSON.pretty_generate(json_data)
234
+ File.write("lib/sqa/tai/help/data.json", json_output)
235
+ puts "✓ Generated help data for #{indicators.size} indicators"
236
+ puts " File: lib/sqa/tai/help/data.json"
237
+ end
238
+ end