tina4ruby 3.13.99 → 3.13.101
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 +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +12 -0
- data/lib/tina4/ai_client.rb +310 -0
- data/lib/tina4/cli.rb +0 -106
- data/lib/tina4/dev_admin.rb +0 -2
- data/lib/tina4/frond.rb +253 -25
- data/lib/tina4/metrics.rb +65 -374
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4.rb +1 -0
- metadata +3 -2
data/lib/tina4/metrics.rb
CHANGED
|
@@ -1,426 +1,117 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Tina4 Code Metrics — the native engine (ADR-0002) plus an instant file census.
|
|
4
|
-
#
|
|
5
|
-
# Two-tier analysis:
|
|
6
|
-
# 1. Quick metrics (instant): LOC, file counts, class/function counts
|
|
7
|
-
# 2. Full analysis (on-demand, cached): cyclomatic complexity, maintainability
|
|
8
|
-
# index, coupling, Halstead metrics, offenders
|
|
9
|
-
#
|
|
10
|
-
# Zero dependencies. The census is pure Ruby; the analysis is `tina4 metrics --json`.
|
|
11
|
-
|
|
12
3
|
require 'json'
|
|
13
|
-
require '
|
|
4
|
+
require 'open3'
|
|
14
5
|
require 'pathname'
|
|
15
6
|
|
|
16
7
|
module Tina4
|
|
17
|
-
# The native metrics engine could not produce a payload.
|
|
18
|
-
#
|
|
19
|
-
# Raised instead of falling back to a second implementation: two engines is
|
|
20
|
-
# exactly the condition that made the four frameworks' numbers incomparable.
|
|
21
8
|
class MetricsEngineError < StandardError; end
|
|
22
9
|
|
|
10
|
+
# Thin dev-admin adapter for the native `tina4 metrics` engine (ADR-0054).
|
|
23
11
|
module Metrics
|
|
24
|
-
|
|
25
|
-
@full_cache_hash = ""
|
|
26
|
-
@full_cache_data = nil
|
|
27
|
-
@full_cache_time = 0
|
|
28
|
-
CACHE_TTL = 60
|
|
29
|
-
|
|
30
|
-
# Stores the resolved scan root so file_detail can locate framework files.
|
|
31
|
-
@last_scan_root = ""
|
|
32
|
-
|
|
33
|
-
# ── Root Resolution ──────────────────────────────────────────
|
|
34
|
-
|
|
35
|
-
# Pick the right directory to scan.
|
|
36
|
-
#
|
|
37
|
-
# If the root dir has Ruby files, scan the user's project code.
|
|
38
|
-
# Otherwise, scan the framework itself — so the bubble chart is never empty.
|
|
39
|
-
def self._resolve_root(root = 'src')
|
|
40
|
-
root_path = Pathname.new(root)
|
|
41
|
-
if root_path.directory? && !Dir.glob(root_path.join('**', '*.rb')).empty?
|
|
42
|
-
@last_scan_root = File.expand_path(root)
|
|
43
|
-
return root
|
|
44
|
-
end
|
|
45
|
-
# Fallback: scan the framework package itself
|
|
46
|
-
fw_dir = File.dirname(__FILE__)
|
|
47
|
-
@last_scan_root = fw_dir
|
|
48
|
-
fw_dir
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def self.last_scan_root
|
|
52
|
-
@last_scan_root
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Return [directory to scan, scan_mode] for any metrics producer.
|
|
56
|
-
#
|
|
57
|
-
# The CLI engine is language-agnostic and cannot know which directory holds a
|
|
58
|
-
# framework package, so root resolution and the "framework" label stay here,
|
|
59
|
-
# shared by the census and the engine adapter so the two never disagree about
|
|
60
|
-
# what was measured.
|
|
61
|
-
def self.resolve_scan_target(root = 'src')
|
|
62
|
-
resolved = _resolve_root(root)
|
|
63
|
-
framework_dir = File.dirname(__FILE__)
|
|
64
|
-
resolved_real = File.expand_path(resolved)
|
|
65
|
-
scanning_framework = resolved_real == framework_dir || resolved_real.start_with?(framework_dir)
|
|
66
|
-
[resolved, scanning_framework ? 'framework' : 'project']
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# ── Quick Metrics ───────────────────────────────────────────
|
|
70
|
-
|
|
71
|
-
def self.quick_metrics(root = 'src')
|
|
72
|
-
# Check if the requested directory exists before falling back
|
|
73
|
-
root_path = Pathname.new(root)
|
|
74
|
-
return { "error" => "Directory not found: #{root}" } unless root_path.directory?
|
|
75
|
-
|
|
76
|
-
root = _resolve_root(root)
|
|
77
|
-
root_path = Pathname.new(root)
|
|
78
|
-
|
|
79
|
-
rb_files = Dir.glob(root_path.join('**', '*.rb'))
|
|
80
|
-
twig_files = Dir.glob(root_path.join('**', '*.twig')) + Dir.glob(root_path.join('**', '*.erb'))
|
|
81
|
-
|
|
82
|
-
migrations_path = Pathname.new('migrations')
|
|
83
|
-
sql_files = if migrations_path.directory?
|
|
84
|
-
Dir.glob(migrations_path.join('**', '*.sql')) + Dir.glob(migrations_path.join('**', '*.rb'))
|
|
85
|
-
else
|
|
86
|
-
[]
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
scss_files = Dir.glob(root_path.join('**', '*.scss')) + Dir.glob(root_path.join('**', '*.css'))
|
|
90
|
-
|
|
91
|
-
total_loc = 0
|
|
92
|
-
total_blank = 0
|
|
93
|
-
total_comment = 0
|
|
94
|
-
total_classes = 0
|
|
95
|
-
total_functions = 0
|
|
96
|
-
file_details = []
|
|
97
|
-
|
|
98
|
-
rb_files.each do |f|
|
|
99
|
-
source = begin
|
|
100
|
-
File.read(f, encoding: 'utf-8')
|
|
101
|
-
rescue StandardError
|
|
102
|
-
next
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
lines = source.lines.map(&:chomp)
|
|
106
|
-
loc = 0
|
|
107
|
-
blank = 0
|
|
108
|
-
comment = 0
|
|
109
|
-
in_heredoc = false
|
|
110
|
-
heredoc_id = nil
|
|
111
|
-
in_block_comment = false
|
|
112
|
-
|
|
113
|
-
lines.each do |line|
|
|
114
|
-
stripped = line.strip
|
|
115
|
-
|
|
116
|
-
if stripped.empty?
|
|
117
|
-
blank += 1
|
|
118
|
-
next
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
# =begin/=end block comments
|
|
122
|
-
if in_block_comment
|
|
123
|
-
comment += 1
|
|
124
|
-
in_block_comment = false if stripped.start_with?('=end')
|
|
125
|
-
next
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
if stripped.start_with?('=begin')
|
|
129
|
-
comment += 1
|
|
130
|
-
in_block_comment = true
|
|
131
|
-
next
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
# Heredoc tracking (simplified)
|
|
135
|
-
if in_heredoc
|
|
136
|
-
if stripped == heredoc_id
|
|
137
|
-
in_heredoc = false
|
|
138
|
-
end
|
|
139
|
-
loc += 1
|
|
140
|
-
next
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
if stripped.match?(/<<[~-]?['"]?(\w+)['"]?/)
|
|
144
|
-
m = stripped.match(/<<[~-]?['"]?(\w+)['"]?/)
|
|
145
|
-
heredoc_id = m[1]
|
|
146
|
-
in_heredoc = true unless stripped.include?(heredoc_id + stripped[-1].to_s)
|
|
147
|
-
loc += 1
|
|
148
|
-
next
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
if stripped.start_with?('#')
|
|
152
|
-
comment += 1
|
|
153
|
-
next
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
loc += 1
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
# Count classes and methods via simple pattern matching
|
|
160
|
-
classes = lines.count { |l| l.strip.match?(/\A(class|module)\s+/) }
|
|
161
|
-
functions = lines.count { |l| l.strip.match?(/\Adef\s+/) }
|
|
162
|
-
|
|
163
|
-
total_loc += loc
|
|
164
|
-
total_blank += blank
|
|
165
|
-
total_comment += comment
|
|
166
|
-
total_classes += classes
|
|
167
|
-
total_functions += functions
|
|
168
|
-
|
|
169
|
-
rel_path = begin
|
|
170
|
-
Pathname.new(f).relative_path_from(root_path).to_s
|
|
171
|
-
rescue ArgumentError
|
|
172
|
-
f
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
file_details << {
|
|
176
|
-
"path" => rel_path,
|
|
177
|
-
"loc" => loc,
|
|
178
|
-
"blank" => blank,
|
|
179
|
-
"comment" => comment,
|
|
180
|
-
"classes" => classes,
|
|
181
|
-
"functions" => functions
|
|
182
|
-
}
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
file_details.sort_by! { |d| -d["loc"] }
|
|
186
|
-
|
|
187
|
-
# Route and ORM counts
|
|
188
|
-
route_count = 0
|
|
189
|
-
orm_count = 0
|
|
190
|
-
begin
|
|
191
|
-
if defined?(Tina4::Router) && Tina4::Router.respond_to?(:routes)
|
|
192
|
-
route_count = Tina4::Router.routes.length
|
|
193
|
-
elsif defined?(Tina4::Router) && Tina4::Router.instance_variable_defined?(:@routes)
|
|
194
|
-
route_count = Tina4::Router.instance_variable_get(:@routes).length
|
|
195
|
-
end
|
|
196
|
-
rescue StandardError
|
|
197
|
-
# ignore
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
begin
|
|
201
|
-
if defined?(Tina4::ORM)
|
|
202
|
-
orm_count = ObjectSpace.each_object(Class).count { |c| c < Tina4::ORM }
|
|
203
|
-
end
|
|
204
|
-
rescue StandardError
|
|
205
|
-
# ignore
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
breakdown = {
|
|
209
|
-
"ruby" => rb_files.length,
|
|
210
|
-
"templates" => twig_files.length,
|
|
211
|
-
"migrations" => sql_files.length,
|
|
212
|
-
"stylesheets" => scss_files.length
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
{
|
|
216
|
-
"file_count" => rb_files.length,
|
|
217
|
-
"total_loc" => total_loc,
|
|
218
|
-
"total_blank" => total_blank,
|
|
219
|
-
"total_comment" => total_comment,
|
|
220
|
-
"lloc" => total_loc,
|
|
221
|
-
"classes" => total_classes,
|
|
222
|
-
"functions" => total_functions,
|
|
223
|
-
"route_count" => route_count,
|
|
224
|
-
"orm_count" => orm_count,
|
|
225
|
-
"template_count" => twig_files.length,
|
|
226
|
-
"migration_count" => sql_files.length,
|
|
227
|
-
"avg_file_size" => rb_files.empty? ? 0 : (total_loc.to_f / rb_files.length).round(1),
|
|
228
|
-
"largest_files" => file_details.first(10),
|
|
229
|
-
"breakdown" => breakdown
|
|
230
|
-
}
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
# ── Full Analysis (Ripper-based) ────────────────────────────
|
|
234
|
-
# ── The native engine (ADR-0002) ─────────────────────────────
|
|
235
|
-
#
|
|
236
|
-
# The Ripper-based analyzer that used to live below here is gone. Everything
|
|
237
|
-
# except the instant file census now comes from `tina4 metrics --json`, so a
|
|
238
|
-
# number measured in Ruby is comparable with the same number measured in
|
|
239
|
-
# Python, PHP or Node. There is deliberately NO fallback: a second engine is
|
|
240
|
-
# exactly the condition that made the four frameworks' numbers incomparable.
|
|
241
|
-
|
|
242
|
-
TIMEOUT_SECONDS = 60
|
|
243
|
-
|
|
244
|
-
INSTALL_HINT = <<~HINT.strip
|
|
245
|
-
the tina4 CLI provides the metrics engine (ADR-0002). Install it with
|
|
246
|
-
curl -fsSL https://tina4.com/install.sh | sh
|
|
247
|
-
or see https://tina4.com/cli
|
|
248
|
-
HINT
|
|
249
|
-
|
|
250
|
-
# Fields the dashboard renders. Checking for the DATA is honest where checking
|
|
251
|
-
# a version string is not: a user may run any CLI build, and the payload is
|
|
252
|
-
# what tells us what that build can actually do.
|
|
12
|
+
INSTALL_HINT = 'update the native tina4 CLI: https://tina4.com/cli'
|
|
253
13
|
SUMMARY_KEYS = %w[files_analyzed total_functions avg_complexity avg_maintainability].freeze
|
|
254
14
|
FILE_KEYS = %w[path loc avg_complexity maintainability has_tests].freeze
|
|
255
15
|
FUNCTION_KEYS = %w[name file line complexity loc].freeze
|
|
256
16
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
next if dir.empty?
|
|
17
|
+
def self.resolve_target(root = 'src')
|
|
18
|
+
source = Pathname.new(root)
|
|
19
|
+
resolved, mode = if source.directory? && !Dir.glob(source.join('**/*.rb').to_s).empty?
|
|
20
|
+
[source.expand_path, 'project']
|
|
21
|
+
else
|
|
22
|
+
[Pathname.new(__dir__).expand_path, 'framework']
|
|
23
|
+
end
|
|
24
|
+
@last_scan_root = resolved.to_s
|
|
25
|
+
[resolved.to_s, mode]
|
|
26
|
+
end
|
|
268
27
|
|
|
28
|
+
def self.engine_path
|
|
29
|
+
ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).each do |directory|
|
|
269
30
|
%w[tina4 tina4.exe].each do |name|
|
|
270
|
-
candidate = File.join(
|
|
31
|
+
candidate = File.join(directory, name)
|
|
271
32
|
next unless File.file?(candidate) && File.executable?(candidate)
|
|
272
|
-
next if
|
|
33
|
+
next if File.binread(candidate, 2) == '#!'
|
|
273
34
|
|
|
274
35
|
return candidate
|
|
36
|
+
rescue StandardError
|
|
37
|
+
next
|
|
275
38
|
end
|
|
276
39
|
end
|
|
277
40
|
nil
|
|
278
41
|
end
|
|
279
42
|
|
|
280
|
-
|
|
281
|
-
def self._shebang_script?(path)
|
|
282
|
-
File.binread(path, 2) == '#!'
|
|
283
|
-
rescue StandardError
|
|
284
|
-
false
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
# Run `tina4 metrics --json` over path and return the raw payload.
|
|
288
|
-
#
|
|
289
|
-
# Raises MetricsEngineError naming the actual cause: a caller that cannot get
|
|
290
|
-
# metrics needs to know whether the binary is missing, the run failed, or the
|
|
291
|
-
# output was unreadable.
|
|
292
|
-
def self._run_engine(path)
|
|
43
|
+
def self.run_engine(path)
|
|
293
44
|
binary = engine_path
|
|
294
|
-
raise MetricsEngineError, "tina4 not found on PATH - #{INSTALL_HINT}"
|
|
295
|
-
|
|
296
|
-
stdout = nil
|
|
297
|
-
status = nil
|
|
298
|
-
stderr = nil
|
|
299
|
-
begin
|
|
300
|
-
require 'open3'
|
|
301
|
-
stdout, stderr, status = Open3.capture3(
|
|
302
|
-
binary, 'metrics', '--path', path.to_s, '--json'
|
|
303
|
-
)
|
|
304
|
-
# capture3 tags the output with the LOCALE's encoding, so under a
|
|
305
|
-
# minimal locale (LANG=C / LANG unset, common on CI runners and in slim
|
|
306
|
-
# containers) the engine's UTF-8 JSON arrives labelled US-ASCII and the
|
|
307
|
-
# first String#strip raises Encoding::CompatibilityError. The bytes were
|
|
308
|
-
# always UTF-8; only the label was wrong.
|
|
309
|
-
stdout = stdout.to_s.dup.force_encoding(Encoding::UTF_8)
|
|
310
|
-
stderr = stderr.to_s.dup.force_encoding(Encoding::UTF_8)
|
|
311
|
-
rescue StandardError => e
|
|
312
|
-
raise MetricsEngineError, "could not run #{binary}: #{e.message}"
|
|
313
|
-
end
|
|
45
|
+
raise MetricsEngineError, "tina4 not found on PATH - #{INSTALL_HINT}" unless binary
|
|
314
46
|
|
|
47
|
+
stdout, stderr, status = Open3.capture3(binary, 'metrics', '--path', path.to_s, '--json')
|
|
315
48
|
unless status.success?
|
|
316
|
-
detail = (stderr.
|
|
317
|
-
|
|
318
|
-
raise MetricsEngineError, "tina4 metrics failed on #{path}: #{first}"
|
|
49
|
+
detail = (stderr.empty? ? stdout : stderr).strip.lines.first || "exit code #{status.exitstatus}"
|
|
50
|
+
raise MetricsEngineError, "tina4 metrics failed on #{path}: #{detail.strip}"
|
|
319
51
|
end
|
|
320
|
-
|
|
321
|
-
raise MetricsEngineError, "tina4 metrics produced no output for #{path}" if stdout.to_s.strip.empty?
|
|
322
|
-
|
|
323
|
-
begin
|
|
324
|
-
payload = JSON.parse(stdout)
|
|
325
|
-
rescue JSON::ParserError => e
|
|
326
|
-
raise MetricsEngineError, "tina4 metrics returned unreadable JSON: #{e.message}"
|
|
327
|
-
end
|
|
328
|
-
|
|
52
|
+
payload = JSON.parse(stdout)
|
|
329
53
|
raise MetricsEngineError, 'tina4 metrics returned a non-object payload' unless payload.is_a?(Hash)
|
|
330
54
|
|
|
331
55
|
payload
|
|
56
|
+
rescue JSON::ParserError => error
|
|
57
|
+
raise MetricsEngineError, "tina4 metrics returned unreadable JSON: #{error.message}"
|
|
58
|
+
rescue SystemCallError => error
|
|
59
|
+
raise MetricsEngineError, "could not run #{binary}: #{error.message}"
|
|
332
60
|
end
|
|
333
61
|
|
|
334
|
-
|
|
335
|
-
def self._require(payload, key, kind)
|
|
62
|
+
def self.require_array(payload, key)
|
|
336
63
|
value = payload[key]
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
"a field the dashboard renders. Update it: #{INSTALL_HINT}"
|
|
341
|
-
end
|
|
342
|
-
value
|
|
64
|
+
return value if value.is_a?(Array)
|
|
65
|
+
|
|
66
|
+
raise MetricsEngineError, "engine payload has no usable '#{key}' - #{INSTALL_HINT}"
|
|
343
67
|
end
|
|
344
68
|
|
|
345
|
-
# Full code analysis from the native engine, shaped for the dashboard.
|
|
346
69
|
def self.full_analysis(root = 'src')
|
|
347
|
-
resolved, scan_mode =
|
|
348
|
-
payload =
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
missing = SUMMARY_KEYS.reject { |
|
|
355
|
-
unless missing.empty?
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
unless file_metrics.empty?
|
|
360
|
-
absent = FILE_KEYS.reject { |k| file_metrics.first.key?(k) }
|
|
361
|
-
raise MetricsEngineError, "engine file_metrics is missing #{absent.join(', ')}" unless absent.empty?
|
|
70
|
+
resolved, scan_mode = resolve_target(root)
|
|
71
|
+
payload = run_engine(resolved)
|
|
72
|
+
summary = payload['summary']
|
|
73
|
+
raise MetricsEngineError, "engine payload has no usable 'summary' - #{INSTALL_HINT}" unless summary.is_a?(Hash)
|
|
74
|
+
|
|
75
|
+
files = require_array(payload, 'file_metrics')
|
|
76
|
+
functions = require_array(payload, 'most_complex_functions')
|
|
77
|
+
missing = SUMMARY_KEYS.reject { |key| summary.key?(key) }
|
|
78
|
+
raise MetricsEngineError, "engine summary is missing #{missing.join(', ')}" unless missing.empty?
|
|
79
|
+
unless files.empty?
|
|
80
|
+
missing = FILE_KEYS.reject { |key| files.first.key?(key) }
|
|
81
|
+
raise MetricsEngineError, "engine file_metrics is missing #{missing.join(', ')}" unless missing.empty?
|
|
362
82
|
end
|
|
363
83
|
unless functions.empty?
|
|
364
|
-
|
|
365
|
-
raise MetricsEngineError, "engine function metrics are missing #{
|
|
84
|
+
missing = FUNCTION_KEYS.reject { |key| functions.first.key?(key) }
|
|
85
|
+
raise MetricsEngineError, "engine function metrics are missing #{missing.join(', ')}" unless missing.empty?
|
|
366
86
|
end
|
|
367
87
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
result['scan_mode'] = scan_mode
|
|
377
|
-
result['scan_root'] = File.expand_path(resolved)
|
|
378
|
-
result['engine'] = 'tina4-cli'
|
|
379
|
-
result
|
|
88
|
+
SUMMARY_KEYS.to_h { |key| [key, summary[key]] }.merge(
|
|
89
|
+
'file_metrics' => files,
|
|
90
|
+
'most_complex_functions' => functions.first(15),
|
|
91
|
+
'dependency_graph' => payload['dependency_graph'] || {},
|
|
92
|
+
'scan_mode' => scan_mode,
|
|
93
|
+
'scan_root' => resolved,
|
|
94
|
+
'engine' => 'tina4-cli'
|
|
95
|
+
)
|
|
380
96
|
end
|
|
381
97
|
|
|
382
|
-
# Top code-health offenders from the native engine.
|
|
383
|
-
#
|
|
384
|
-
# The engine ranks and severity-tags them, and its own --fail-on gate reads
|
|
385
|
-
# the same list, so the CLI and the dashboard can never disagree about what
|
|
386
|
-
# counts as an offender.
|
|
387
|
-
def self.offenders(root = 'src', top = 20)
|
|
388
|
-
resolved, scan_mode = resolve_scan_target(root)
|
|
389
|
-
payload = _run_engine(resolved)
|
|
390
|
-
|
|
391
|
-
found = _require(payload, 'offenders', Array)
|
|
392
|
-
summary = _require(payload, 'summary', Hash).dup
|
|
393
|
-
summary['scan_mode'] = scan_mode
|
|
394
|
-
summary['scan_root'] = File.expand_path(resolved)
|
|
395
|
-
summary['engine'] = 'tina4-cli'
|
|
396
|
-
summary['total_offenders'] ||= found.length
|
|
397
|
-
{ 'offenders' => found.first(top), 'summary' => summary }
|
|
398
|
-
end
|
|
399
|
-
|
|
400
|
-
# Per-file metrics from the native engine.
|
|
401
|
-
#
|
|
402
|
-
# The engine accepts a single file for --path, so one code path serves both
|
|
403
|
-
# the whole-tree scan and one file.
|
|
404
98
|
def self.file_detail(file_path)
|
|
405
|
-
raise MetricsEngineError, 'file_detail needs a path' if file_path.
|
|
99
|
+
raise MetricsEngineError, 'file_detail needs a path' if file_path.to_s.empty?
|
|
406
100
|
|
|
407
101
|
target = Pathname.new(file_path.to_s)
|
|
408
|
-
|
|
409
|
-
# Try it relative to whatever the census last resolved, so the dashboard
|
|
410
|
-
# can pass a path taken straight out of file_metrics.
|
|
411
|
-
unless @last_scan_root.to_s.empty?
|
|
412
|
-
candidate = Pathname.new(@last_scan_root).join(file_path.to_s)
|
|
413
|
-
target = candidate if candidate.exist?
|
|
414
|
-
end
|
|
415
|
-
end
|
|
102
|
+
target = Pathname.new(@last_scan_root).join(file_path.to_s) if !target.exist? && @last_scan_root
|
|
416
103
|
raise MetricsEngineError, "no such file: #{file_path}" unless target.exist?
|
|
417
104
|
raise MetricsEngineError, "not a file: #{file_path}" if target.directory?
|
|
418
105
|
|
|
419
|
-
payload =
|
|
420
|
-
|
|
421
|
-
raise MetricsEngineError, "engine reported no metrics for #{file_path}" if
|
|
106
|
+
payload = run_engine(target.to_s)
|
|
107
|
+
files = require_array(payload, 'file_metrics')
|
|
108
|
+
raise MetricsEngineError, "engine reported no metrics for #{file_path}" if files.empty?
|
|
422
109
|
|
|
423
|
-
|
|
110
|
+
files.first.merge(
|
|
111
|
+
'function_count' => files.first.fetch('functions', 0),
|
|
112
|
+
'functions' => require_array(payload, 'most_complex_functions'),
|
|
113
|
+
'engine' => 'tina4-cli'
|
|
114
|
+
)
|
|
424
115
|
end
|
|
425
116
|
end
|
|
426
117
|
end
|
data/lib/tina4/version.rb
CHANGED
data/lib/tina4.rb
CHANGED
|
@@ -42,6 +42,7 @@ require_relative "tina4/dev_admin"
|
|
|
42
42
|
require_relative "tina4/feedback"
|
|
43
43
|
require_relative "tina4/dev_mailbox"
|
|
44
44
|
require_relative "tina4/ai"
|
|
45
|
+
require_relative "tina4/ai_client"
|
|
45
46
|
require_relative "tina4/cache"
|
|
46
47
|
require_relative "tina4/sql_translator"
|
|
47
48
|
require_relative "tina4/cache_backends"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tina4ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.13.
|
|
4
|
+
version: 3.13.101
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tina4 Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|
|
@@ -321,6 +321,7 @@ files:
|
|
|
321
321
|
- exe/tina4ruby
|
|
322
322
|
- lib/tina4.rb
|
|
323
323
|
- lib/tina4/ai.rb
|
|
324
|
+
- lib/tina4/ai_client.rb
|
|
324
325
|
- lib/tina4/api.rb
|
|
325
326
|
- lib/tina4/auth.rb
|
|
326
327
|
- lib/tina4/auto_crud.rb
|