hashira 0.2.0 → 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.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +141 -0
  3. data/README.md +53 -5
  4. data/lib/hashira/analysis/catalog.rb +27 -0
  5. data/lib/hashira/analysis/census.rb +42 -35
  6. data/lib/hashira/analysis/constant_registry.rb +43 -0
  7. data/lib/hashira/analysis/cycle_findings.rb +34 -29
  8. data/lib/hashira/analysis/cycle_search.rb +28 -35
  9. data/lib/hashira/analysis/cycles.rb +14 -0
  10. data/lib/hashira/analysis/definition.rb +22 -0
  11. data/lib/hashira/analysis/definitions.rb +22 -18
  12. data/lib/hashira/analysis/edge.rb +4 -3
  13. data/lib/hashira/analysis/edge_map.rb +23 -26
  14. data/lib/hashira/analysis/finding.rb +7 -6
  15. data/lib/hashira/analysis/folder_placement.rb +17 -0
  16. data/lib/hashira/analysis/folding.rb +54 -0
  17. data/lib/hashira/analysis/graph.rb +35 -38
  18. data/lib/hashira/analysis/metric.rb +8 -7
  19. data/lib/hashira/analysis/namespace_placement.rb +28 -0
  20. data/lib/hashira/analysis/namespace_prefix.rb +32 -0
  21. data/lib/hashira/analysis/naming.rb +14 -0
  22. data/lib/hashira/analysis/no_folding.rb +13 -0
  23. data/lib/hashira/analysis/node_walk.rb +3 -3
  24. data/lib/hashira/analysis/placement.rb +19 -0
  25. data/lib/hashira/analysis/references.rb +14 -13
  26. data/lib/hashira/analysis/resolver.rb +28 -0
  27. data/lib/hashira/analysis/roster.rb +25 -0
  28. data/lib/hashira/analysis/rule.rb +10 -14
  29. data/lib/hashira/analysis/sdp_check.rb +8 -12
  30. data/lib/hashira/analysis/sdp_violation_findings.rb +20 -18
  31. data/lib/hashira/analysis/syntax.rb +21 -6
  32. data/lib/hashira/analysis/type_walk.rb +7 -9
  33. data/lib/hashira/churn.rb +11 -13
  34. data/lib/hashira/ci/accepted.rb +27 -31
  35. data/lib/hashira/ci/baseline.rb +22 -24
  36. data/lib/hashira/ci/diff.rb +8 -7
  37. data/lib/hashira/ci/edge_diff_report.rb +13 -17
  38. data/lib/hashira/ci/finding_diff_report.rb +14 -18
  39. data/lib/hashira/ci/gate.rb +19 -23
  40. data/lib/hashira/ci/improvement.rb +9 -14
  41. data/lib/hashira/ci/ratchet.rb +36 -27
  42. data/lib/hashira/ci/ratchet_report.rb +26 -32
  43. data/lib/hashira/cli/arguments.rb +23 -0
  44. data/lib/hashira/cli/command_line.rb +55 -71
  45. data/lib/hashira/cli/fail_on.rb +15 -20
  46. data/lib/hashira/cli/options.rb +9 -4
  47. data/lib/hashira/cli/package_by.rb +14 -0
  48. data/lib/hashira/cli/run.rb +29 -27
  49. data/lib/hashira/cli/skip.rb +14 -20
  50. data/lib/hashira/cli/usage.rb +41 -41
  51. data/lib/hashira/cli.rb +17 -19
  52. data/lib/hashira/complexity/analyzer.rb +30 -32
  53. data/lib/hashira/complexity/boolean_run.rb +11 -15
  54. data/lib/hashira/complexity/cognitive_score.rb +61 -66
  55. data/lib/hashira/complexity/if_chain.rb +33 -38
  56. data/lib/hashira/complexity/method_finding.rb +43 -48
  57. data/lib/hashira/complexity/method_score.rb +9 -7
  58. data/lib/hashira/complexity/rescue_scan.rb +16 -21
  59. data/lib/hashira/complexity/rollup.rb +13 -15
  60. data/lib/hashira/diagram/dot.rb +8 -11
  61. data/lib/hashira/diagram/mermaid.rb +11 -16
  62. data/lib/hashira/diagram/renderer.rb +10 -15
  63. data/lib/hashira/duplication/analyzer.rb +10 -14
  64. data/lib/hashira/duplication/cluster.rb +15 -12
  65. data/lib/hashira/duplication/clusterer.rb +31 -29
  66. data/lib/hashira/duplication/delta.rb +29 -30
  67. data/lib/hashira/duplication/duplication_finding.rb +19 -21
  68. data/lib/hashira/duplication/extractor.rb +15 -19
  69. data/lib/hashira/duplication/fragment.rb +19 -23
  70. data/lib/hashira/duplication/grouping.rb +12 -16
  71. data/lib/hashira/duplication/index.rb +20 -24
  72. data/lib/hashira/duplication/maximal.rb +11 -15
  73. data/lib/hashira/duplication/near_miss.rb +19 -22
  74. data/lib/hashira/duplication/sequence.rb +20 -22
  75. data/lib/hashira/duplication/similarity.rb +28 -35
  76. data/lib/hashira/duplication/union_find.rb +10 -14
  77. data/lib/hashira/duplication/variance.rb +36 -44
  78. data/lib/hashira/error.rb +4 -2
  79. data/lib/hashira/hotspots/file_cost.rb +8 -7
  80. data/lib/hashira/hotspots/rollup.rb +19 -21
  81. data/lib/hashira/pipeline.rb +36 -31
  82. data/lib/hashira/project.rb +53 -32
  83. data/lib/hashira/report/complexity_table.rb +27 -31
  84. data/lib/hashira/report/dependency_map.rb +17 -22
  85. data/lib/hashira/report/finding_lines.rb +15 -19
  86. data/lib/hashira/report/graph_payload.rb +14 -20
  87. data/lib/hashira/report/hotspot_table.rb +32 -37
  88. data/lib/hashira/report/json.rb +30 -32
  89. data/lib/hashira/report/metrics_table.rb +46 -33
  90. data/lib/hashira/report/text.rb +59 -52
  91. data/lib/hashira/version.rb +1 -1
  92. data/lib/hashira.rb +92 -55
  93. metadata +16 -2
  94. data/lib/hashira/analysis/root_namespace.rb +0 -14
@@ -7,19 +7,17 @@ module Hashira
7
7
  module TypeWalk
8
8
  module_function
9
9
 
10
- def each_definition(node, prefix = [], &)
11
- return each_child_definition(node, prefix, &) unless type_node?(node)
12
-
13
- full = prefix + Syntax.path_segments(node.constant_path)
10
+ def each(node, stack = [], roots: nil, &)
11
+ return descend(node, stack, roots, &) unless type?(node)
12
+ full = Syntax.anchor(stack, Syntax.segments(node.constant_path), roots)
14
13
  yield(node, full)
15
- each_child_definition(node.body, full, &)
14
+ descend(node.body, stack + [full], roots, &)
16
15
  end
17
16
 
18
- def type_node?(node) = node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)
17
+ def type?(node) = node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)
19
18
 
20
- def each_child_definition(node, prefix, &)
21
- children = node ? node.compact_child_nodes : []
22
- children.each { each_definition(it, prefix, &) }
19
+ def descend(node, stack, roots, &)
20
+ (node ? node.compact_child_nodes : []).each { each(it, stack, roots:, &) }
23
21
  end
24
22
  end
25
23
  end
data/lib/hashira/churn.rb CHANGED
@@ -1,22 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- class Churn
5
- LOG = "git log --name-only --format= 2>/dev/null"
6
- SITES_THAT_DRIFT_APART = 2
3
+ class Hashira::Churn
4
+ LOG = "git log --name-only --format= 2>/dev/null"
5
+ SITES_THAT_DRIFT_APART = 2
7
6
 
8
- def self.from_git = new(tally(`#{LOG}`))
7
+ def self.scan = new(tally(`#{LOG}`))
9
8
 
10
- def self.tally(output) = output.split("\n").map(&:strip).reject(&:empty?).tally
9
+ def self.tally(output) = output.split("\n").map(&:strip).reject(&:empty?).tally
11
10
 
12
- def initialize(counts)
13
- @counts = counts
14
- end
11
+ def initialize(counts)
12
+ @counts = counts
13
+ end
15
14
 
16
- def hits(file) = @counts.select { |path, _| path.end_with?(file) }.values.max || 0
15
+ def hits(file) = @counts.select { |path, _| path.end_with?(file) }.values.max || 0
17
16
 
18
- def hot?(members) = changing(members) >= SITES_THAT_DRIFT_APART
17
+ def hot?(members) = changing(members) >= SITES_THAT_DRIFT_APART
19
18
 
20
- def changing(members) = members.count { |member| hits(member.file).positive? }
21
- end
19
+ def changing(members) = members.count { |member| hits(member.file).positive? }
22
20
  end
@@ -2,47 +2,43 @@
2
2
 
3
3
  require "json"
4
4
 
5
- module Hashira
6
- module CI
7
- class Accepted
8
- Screened = Data.define(:all, :accepted)
5
+ class Hashira::CI::Accepted
6
+ Screened = Data.define(:all, :accepted)
9
7
 
10
- Entry = Data.define(:kind, :package, :digest, :reason) do
11
- def self.from(hash)
12
- new(kind: hash["kind"], package: hash["package"], digest: hash["digest"], reason: hash["reason"])
13
- end
14
-
15
- def matches?(finding) = kind == finding.kind && identity == finding.identity
8
+ Entry =
9
+ Data.define(:kind, :package, :digest, :reason) do
10
+ def self.from(hash)
11
+ new(kind: hash["kind"], package: hash["package"], digest: hash["digest"], reason: hash["reason"])
12
+ end
16
13
 
17
- def identity = digest || package
14
+ def matches?(finding) = kind == finding.kind && identity == finding.identity
18
15
 
19
- def label = reason || "accepted (no reason recorded)"
16
+ def identity = digest || package
20
17
 
21
- def to_h = { kind:, package:, digest:, reason: }.compact
22
- end
18
+ def label = reason || "accepted (no reason recorded)"
23
19
 
24
- def self.load(path)
25
- return new([]) unless path && File.exist?(path)
20
+ def to_h = { kind:, package:, digest:, reason: }.compact
21
+ end
26
22
 
27
- new(JSON.parse(File.read(path)).fetch("accepted", []))
28
- end
23
+ def self.load(path)
24
+ return new([]) unless path && File.exist?(path)
25
+ new(JSON.parse(File.read(path)).fetch("accepted", []))
26
+ end
29
27
 
30
- def initialize(entries)
31
- @entries = entries.map { Entry.from(it) }
32
- end
28
+ def initialize(entries)
29
+ @entries = entries.map { Entry.from(it) }
30
+ end
33
31
 
34
- def entries = @entries.map(&:to_h)
32
+ def entries = @entries.map(&:to_h)
35
33
 
36
- def screen(findings)
37
- accepted, live = findings.map { [it, reason_for(it)] }.partition(&:last)
38
- Screened.new(all: live.map(&:first), accepted:)
39
- end
34
+ def screen(findings)
35
+ accepted, live = findings.map { [it, reason(it)] }.partition(&:last)
36
+ Screened.new(all: live.map(&:first), accepted:)
37
+ end
40
38
 
41
- private
39
+ private
42
40
 
43
- def reason_for(finding)
44
- @entries.find { it.matches?(finding) }&.label
45
- end
46
- end
41
+ def reason(finding)
42
+ @entries.find { it.matches?(finding) }&.label
47
43
  end
48
44
  end
@@ -2,39 +2,37 @@
2
2
 
3
3
  require "json"
4
4
 
5
- module Hashira
6
- module CI
7
- class Baseline
8
- SCHEMA_VERSION = 2
5
+ class Hashira::CI::Baseline
6
+ SCHEMA_VERSION = 3
9
7
 
10
- def self.load(path) = new(path, File.exist?(path) ? JSON.parse(File.read(path)) : {})
8
+ def self.load(path) = new(path, File.exist?(path) ? JSON.parse(File.read(path)) : {})
11
9
 
12
- def initialize(path, recorded)
13
- @path = path
14
- @recorded = recorded
15
- end
10
+ def initialize(path, recorded)
11
+ @path = path
12
+ @recorded = recorded
13
+ end
14
+
15
+ attr_reader :path
16
16
 
17
- attr_reader :path
17
+ def exist? = File.exist?(@path)
18
18
 
19
- def exist? = File.exist?(@path)
19
+ def edges = @recorded.fetch("edges", [])
20
20
 
21
- def edges = @recorded.fetch("edges", [])
21
+ def findings = @recorded.fetch("findings", [])
22
22
 
23
- def findings = @recorded.fetch("findings", [])
23
+ def findings? = @recorded.key?("findings")
24
24
 
25
- def ratchets_findings? = @recorded.key?("findings")
25
+ def packaging = @recorded.fetch("packaging", "folder")
26
26
 
27
- def write(edges, findings)
28
- File.write(@path, JSON.pretty_generate(payload(edges, findings)) << "\n")
29
- end
27
+ def write(edges, findings, packaging:)
28
+ File.write(@path, JSON.pretty_generate(payload(edges, findings, packaging)) << "\n")
29
+ end
30
30
 
31
- private
31
+ private
32
32
 
33
- def payload(edges, findings)
34
- base = { version: SCHEMA_VERSION, edges:, findings: }
35
- accepted = Accepted.new(@recorded.fetch("accepted", [])).entries
36
- accepted.empty? ? base : base.merge(accepted:)
37
- end
38
- end
33
+ def payload(edges, findings, packaging)
34
+ base = { version: SCHEMA_VERSION, packaging:, edges:, findings: }
35
+ accepted = Hashira::CI::Accepted.new(@recorded.fetch("accepted", [])).entries
36
+ accepted.empty? ? base : base.merge(accepted:)
39
37
  end
40
38
  end
@@ -2,14 +2,15 @@
2
2
 
3
3
  module Hashira
4
4
  module CI
5
- Diff = Data.define(:added, :removed) do
6
- def self.between(current, recorded)
7
- new(added: current - recorded, removed: recorded - current)
8
- end
5
+ Diff =
6
+ Data.define(:added, :removed) do
7
+ def self.between(current, recorded)
8
+ new(added: current - recorded, removed: recorded - current)
9
+ end
9
10
 
10
- def empty? = added.empty? && removed.empty?
11
+ def empty? = added.empty? && removed.empty?
11
12
 
12
- def worse? = !added.empty?
13
- end
13
+ def worse? = !added.empty?
14
+ end
14
15
  end
15
16
  end
@@ -1,24 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module CI
5
- class EdgeDiffReport
6
- def initialize(graph, io: $stdout)
7
- @graph = graph
8
- @io = io
9
- end
3
+ class Hashira::CI::EdgeDiffReport
4
+ def initialize(graph, io: $stdout)
5
+ @graph = graph
6
+ @io = io
7
+ end
10
8
 
11
- def print(diff)
12
- diff.added.each { print_edge(it) }
13
- Improvement.new("Edges removed", io: @io).print(diff.removed)
14
- end
9
+ def print(diff)
10
+ diff.added.each { emit(it) }
11
+ Hashira::CI::Improvement.new("Edges removed", io: @io).print(diff.removed)
12
+ end
15
13
 
16
- private
14
+ private
17
15
 
18
- def print_edge(edge)
19
- @io.puts "NEW EDGE #{edge} — introduced by:"
20
- @graph.evidence_for(edge.from, edge.to).to_a.sort.each { @io.puts " · #{it}" }
21
- end
22
- end
16
+ def emit(edge)
17
+ @io.puts("NEW EDGE #{edge} — introduced by:")
18
+ @graph.evidence(edge.from, edge.to).to_a.sort.each { @io.puts(" · #{it}") }
23
19
  end
24
20
  end
@@ -1,26 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module CI
5
- class FindingDiffReport
6
- def initialize(findings, io: $stdout)
7
- @findings = findings
8
- @io = io
9
- end
3
+ class Hashira::CI::FindingDiffReport
4
+ def initialize(findings, io: $stdout)
5
+ @findings = findings
6
+ @io = io
7
+ end
10
8
 
11
- def print(diff)
12
- introduced(diff.added).each { print_finding(it) }
13
- Improvement.new("Findings resolved", io: @io).print(diff.removed)
14
- end
9
+ def print(diff)
10
+ introduced(diff.added).each { emit(it) }
11
+ Hashira::CI::Improvement.new("Findings resolved", io: @io).print(diff.removed)
12
+ end
15
13
 
16
- private
14
+ private
17
15
 
18
- def introduced(added) = added.filter_map { |signature| @findings.find { it.signature == signature } }
16
+ def introduced(added) = added.filter_map { |signature| @findings.find { it.signature == signature } }
19
17
 
20
- def print_finding(finding)
21
- @io.puts "NEW FINDING:"
22
- Report::FindingLines.new(finding, indent: " ", io: @io).print_with_overflow
23
- end
24
- end
18
+ def emit(finding)
19
+ @io.puts("NEW FINDING:")
20
+ Hashira::Report::FindingLines.new(finding, indent: " ", io: @io).emit
25
21
  end
26
22
  end
@@ -1,31 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module CI
5
- class Gate
6
- def initialize(findings, kinds, io: $stdout)
7
- @findings = findings
8
- @kinds = kinds
9
- @io = io
10
- end
3
+ class Hashira::CI::Gate
4
+ def initialize(findings, kinds, io: $stdout)
5
+ @findings = findings
6
+ @kinds = kinds
7
+ @io = io
8
+ end
11
9
 
12
- def check
13
- offending = @findings.all.select { @kinds.include?(it.kind) }
14
- offending.empty? ? report_clean : report_failure(offending)
15
- end
10
+ def check
11
+ offending = @findings.all.select { @kinds.include?(it.kind) }
12
+ offending.empty? ? clean : failure(offending)
13
+ end
16
14
 
17
- private
15
+ private
18
16
 
19
- def report_failure(offending)
20
- offending.each { Report::FindingLines.new(it, io: @io).print }
21
- @io.puts "\nGate FAILED: #{offending.size} finding(s) of kind #{@kinds.join(", ")}."
22
- 1
23
- end
17
+ def failure(offending)
18
+ offending.each { Hashira::Report::FindingLines.new(it, io: @io).print }
19
+ @io.puts("\nGate FAILED: #{offending.size} finding(s) of kind #{@kinds.join(", ")}.")
20
+ 1
21
+ end
24
22
 
25
- def report_clean
26
- @io.puts "Gate OK: no findings of kind #{@kinds.join(", ")}."
27
- 0
28
- end
29
- end
23
+ def clean
24
+ @io.puts("Gate OK: no findings of kind #{@kinds.join(", ")}.")
25
+ 0
30
26
  end
31
27
  end
@@ -1,19 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module CI
5
- class Improvement
6
- def initialize(label, io: $stdout)
7
- @label = label
8
- @io = io
9
- end
10
-
11
- def print(removed)
12
- return if removed.empty?
3
+ class Hashira::CI::Improvement
4
+ def initialize(label, io: $stdout)
5
+ @label = label
6
+ @io = io
7
+ end
13
8
 
14
- @io.puts "#{@label} (improvement!): #{removed.join(", ")}"
15
- @io.puts "Lock it in: hashira --update-baseline"
16
- end
17
- end
9
+ def print(removed)
10
+ return if removed.empty?
11
+ @io.puts("#{@label} (improvement!): #{removed.join(", ")}")
12
+ @io.puts("Lock it in: hashira --update-baseline")
18
13
  end
19
14
  end
@@ -1,40 +1,49 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module CI
5
- class Ratchet
6
- def initialize(graph, findings, baseline_path, io: $stdout)
7
- @graph = graph
8
- @findings = findings
9
- @baseline = Baseline.load(baseline_path)
10
- @io = io
11
- end
3
+ class Hashira::CI::Ratchet
4
+ def initialize(graph, findings, baseline_path, io: $stdout)
5
+ @graph = graph
6
+ @findings = findings
7
+ @baseline = Hashira::CI::Baseline.load(baseline_path)
8
+ @io = io
9
+ end
10
+
11
+ def update
12
+ @baseline.write(edges, digests, packaging:)
13
+ @io.puts("Baseline updated: #{edges.size} edges, #{digests.size} findings.")
14
+ 0
15
+ end
12
16
 
13
- def update
14
- @baseline.write(edge_signatures, finding_signatures)
15
- @io.puts "Baseline updated: #{edge_signatures.size} edges, #{finding_signatures.size} findings."
16
- 0
17
- end
17
+ def check
18
+ raise(Hashira::Error, "no baseline at #{@baseline.path} — run --update-baseline first") unless @baseline.exist?
19
+ vet
20
+ Hashira::CI::RatchetReport.new(@graph, @findings, io: @io).print(drift, delta)
21
+ end
18
22
 
19
- def check
20
- raise Error, "no baseline at #{@baseline.path} — run --update-baseline first" unless @baseline.exist?
23
+ private
21
24
 
22
- RatchetReport.new(@graph, @findings, io: @io).print(edge_diff, finding_diff)
23
- end
25
+ def packaging = @graph.packaging.to_s
24
26
 
25
- private
27
+ def vet
28
+ return if @baseline.packaging == packaging
29
+ raise(Hashira::Error, mismatch)
30
+ end
31
+
32
+ def mismatch
33
+ recorded = @baseline.packaging
34
+ "baseline #{@baseline.path} was recorded with --package-by #{recorded}, but this run " \
35
+ "uses #{packaging} — rerun with --package-by #{recorded}, or refresh it with --update-baseline"
36
+ end
26
37
 
27
- def edge_signatures = @graph.edge_list.map(&:to_s)
38
+ def edges = @graph.edges.map(&:to_s)
28
39
 
29
- def finding_signatures = @findings.map(&:signature).uniq.sort
40
+ def digests = @findings.map(&:signature).uniq.sort
30
41
 
31
- def edge_diff = Diff.new(added: new_edges, removed: @baseline.edges - edge_signatures)
42
+ def drift = Hashira::CI::Diff.new(added: fresh, removed: @baseline.edges - edges)
32
43
 
33
- def new_edges = @graph.edge_list.reject { @baseline.edges.include?(it.to_s) }
44
+ def fresh = @graph.edges.reject { @baseline.edges.include?(it.to_s) }
34
45
 
35
- def finding_diff
36
- Diff.between(finding_signatures, @baseline.findings) if @baseline.ratchets_findings?
37
- end
38
- end
46
+ def delta
47
+ Hashira::CI::Diff.between(digests, @baseline.findings) if @baseline.findings?
39
48
  end
40
49
  end
@@ -1,42 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module CI
5
- class RatchetReport
6
- def initialize(graph, findings, io: $stdout)
7
- @graph = graph
8
- @findings = findings
9
- @io = io
10
- end
11
-
12
- def print(edges, findings)
13
- return unchanged if quiet?(edges, findings)
14
-
15
- details(edges, findings)
16
- advice(edges, findings)
17
- 1
18
- end
3
+ class Hashira::CI::RatchetReport
4
+ def initialize(graph, findings, io: $stdout)
5
+ @graph = graph
6
+ @findings = findings
7
+ @io = io
8
+ end
19
9
 
20
- private
10
+ def print(edges, findings)
11
+ return unchanged if quiet?(edges, findings)
12
+ details(edges, findings)
13
+ advice(edges, findings)
14
+ 1
15
+ end
21
16
 
22
- def quiet?(*diffs) = diffs.compact.all?(&:empty?)
17
+ private
23
18
 
24
- def unchanged
25
- @io.puts "Ratchet OK: #{@graph.edge_list.size} edges, #{@findings.size} findings, unchanged."
26
- 0
27
- end
19
+ def quiet?(*diffs) = diffs.compact.all?(&:empty?)
28
20
 
29
- def details(edges, findings)
30
- EdgeDiffReport.new(@graph, io: @io).print(edges)
31
- FindingDiffReport.new(@findings, io: @io).print(findings) if findings
32
- end
21
+ def unchanged
22
+ @io.puts("Ratchet OK: #{@graph.edges.size} edges, #{@findings.size} findings, unchanged.")
23
+ 0
24
+ end
33
25
 
34
- def advice(*diffs)
35
- return unless diffs.compact.any?(&:worse?)
26
+ def details(edges, findings)
27
+ Hashira::CI::EdgeDiffReport.new(@graph, io: @io).print(edges)
28
+ Hashira::CI::FindingDiffReport.new(@findings, io: @io).print(findings) if findings
29
+ end
36
30
 
37
- @io.puts "\nRatchet FAILED. Either fix what regressed, or — if it is deliberate —"
38
- @io.puts "record the decision: update the baseline, or accept it with a reason."
39
- end
40
- end
31
+ def advice(*diffs)
32
+ return unless diffs.compact.any?(&:worse?)
33
+ @io.puts("\nRatchet FAILED. Either fix what regressed, or — if it is deliberate —")
34
+ @io.puts("record the decision: update the baseline, or accept it with a reason.")
41
35
  end
42
36
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Hashira::CLI::Arguments
4
+ def initialize(argv)
5
+ @argv = argv.dup
6
+ end
7
+
8
+ def take(flag, default)
9
+ position = @argv.index(flag)
10
+ return default unless position
11
+ _flag, value = @argv.slice!(position, 2)
12
+ raise(Hashira::Error, "#{flag} needs a value") unless value
13
+ value
14
+ end
15
+
16
+ def delete(flag) = @argv.delete(flag)
17
+
18
+ def rest
19
+ stray = @argv.find { it.start_with?("-") }
20
+ raise(Hashira::Error, "unknown option #{stray}") if stray
21
+ @argv
22
+ end
23
+ end