moult 0.3.0 → 0.4.1

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: 5da8cf8ae9bf6dae1b9a05494fdf1280d1b3c62742cd8700e24a60ca9fc66eeb
4
- data.tar.gz: f487eadae0717dc3f221612234699ea175c005a0b3c1801af57f71b34aa9cfc1
3
+ metadata.gz: 5ad87ca8d76d09c7808a172aa24b42bf8c2031c4c5e05604f1c98e2a1120aacc
4
+ data.tar.gz: 683a307fa930966927516e37f6b1445b7b0e08c31da996d2c250d8ace63b2e67
5
5
  SHA512:
6
- metadata.gz: 136f27d5540be7e6fb4d2174b198703ec44dbab7c3bff8735f8f6cfb40217af2df230fd48e70965a4680740c3fdf8f2325c9c3c31b589b88a56939d2bfcf5b06
7
- data.tar.gz: 677b2b5fdbb133f49046baa84e4a4a286e587c9da8904e44397a01d6ef116f5d7e4480bba88b73cfbaae45a37318e36b02d1981dc8ec6d6c44be912bcea20329
6
+ metadata.gz: 04d0d5fb568b8135a854d36a89e4b1134a8666007e033465f7e5d56c7566e80dce839a383b333c4dad72cb02054b4b5e87b88954003f8a8d29555e545d5d276f
7
+ data.tar.gz: 82836397a09f0314f00ae108c339c2a119beb9c0a648323805bbd1a1d9c87b2bbf8191c94e6815837fa1627eb535d9e7c2511df21e8e12ea78ca570ec564dd61
data/CHANGELOG.md CHANGED
@@ -11,6 +11,19 @@ follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ### Added
15
+ - `clone_group` on duplication findings and gate contributions — a
16
+ `"<kind>:<structural-hash>"` join key (kind `identical`|`similar`) shared by
17
+ every occurrence of a clone group, so consumers can link exact twins. Null on
18
+ non-duplication contributions. Additive/optional in both schemas, so
19
+ `schema_version` stays 1.
20
+
21
+ ### Changed
22
+ - The gate now emits one duplication contribution per in-diff occurrence
23
+ (previously one per clone group, attributed to its first in-diff
24
+ occurrence), so every site of a clone is visible downstream. Verdicts are
25
+ unchanged, and rule reasons still count clone groups.
26
+
14
27
  ## [0.3.0] - 2026-07-02
15
28
 
16
29
  (v0.2.0 was tagged in git but never published as a gem — its changes are
@@ -63,6 +63,7 @@ module Moult
63
63
  kind: set.kind,
64
64
  node_type: set.node_type,
65
65
  mass: set.mass,
66
+ clone_group: "#{set.kind}:#{set.structural_hash}",
66
67
  reasons: assessment.reasons,
67
68
  occurrences: occurrences
68
69
  )
@@ -21,8 +21,10 @@ module Moult
21
21
  end
22
22
 
23
23
  # A confidence-graded clone group. Carries its reasons so no claim is made
24
- # without a recorded justification.
25
- Finding = Struct.new(:confidence, :kind, :node_type, :mass, :reasons, :occurrences) do
24
+ # without a recorded justification. +clone_group+ ("<kind>:<structural-hash>")
25
+ # is the group's join key, shared by every occurrence; stable within a report
26
+ # only (the hash comes from the detector backend).
27
+ Finding = Struct.new(:confidence, :kind, :node_type, :mass, :clone_group, :reasons, :occurrences) do
26
28
  def to_h
27
29
  {
28
30
  category: Duplication::Confidence::CATEGORY,
@@ -30,6 +32,7 @@ module Moult
30
32
  kind: kind.to_s,
31
33
  node_type: node_type,
32
34
  mass: mass,
35
+ clone_group: clone_group,
33
36
  reasons: reasons.map(&:to_h),
34
37
  occurrences: occurrences.map(&:to_h)
35
38
  }
@@ -19,7 +19,7 @@ module Moult
19
19
  DeadCodeObs = Struct.new(:symbol_id, :path, :line, :confidence)
20
20
  BoundaryObs = Struct.new(:symbol_id, :path, :line, :severity, :violation_type)
21
21
  ComplexityObs = Struct.new(:symbol_id, :path, :line, :abc)
22
- DuplicationObs = Struct.new(:symbol_id, :path, :line, :mass)
22
+ DuplicationObs = Struct.new(:symbol_id, :path, :line, :mass, :clone_group)
23
23
 
24
24
  # The full input. Each analysis's list is nil when that analysis was skipped
25
25
  # (errored, or not applicable — e.g. a non-packwerk repo for boundaries);
@@ -39,10 +39,11 @@ module Moult
39
39
 
40
40
  # One contributing finding behind a rule outcome. Stays confidence-graded:
41
41
  # +value+ is the observed signal (confidence/abc/mass/severity), never a
42
- # claim of certainty.
43
- Contribution = Struct.new(:category, :path, :symbol_id, :line, :value) do
42
+ # claim of certainty. +clone_group+ links occurrences of the same clone
43
+ # group ("<kind>:<structural-hash>"); null outside duplication.
44
+ Contribution = Struct.new(:category, :path, :symbol_id, :line, :value, :clone_group) do
44
45
  def to_h
45
- {category: category, path: path, symbol_id: symbol_id, line: line, value: value}
46
+ {category: category, path: path, symbol_id: symbol_id, line: line, value: value, clone_group: clone_group}
46
47
  end
47
48
  end
48
49
 
@@ -123,8 +124,10 @@ module Moult
123
124
  t = policy.duplication_max_mass
124
125
  outcome("new_code_duplication_ceiling", "<= #{t}", obs, diagnostic, "structural_duplication") do |list|
125
126
  violating = list.select { |o| o.mass > t }
127
+ # Observations are per occurrence, but the reason still counts GROUPS.
126
128
  detail = phrase(violating, "no clone group touching the diff exceeds mass #{t}",
127
- "clone group(s) touching the diff exceed mass #{t}")
129
+ "clone group(s) touching the diff exceed mass #{t}",
130
+ count: violating.map(&:clone_group).uniq.size)
128
131
  [violating, list.map(&:mass).max, detail, ->(o) { o.mass }]
129
132
  end
130
133
  end
@@ -142,7 +145,7 @@ module Moult
142
145
  rule: rule, evaluated: true, observed: observed, threshold: threshold,
143
146
  passed: violating.empty?,
144
147
  reasons: [Reason.new(rule: rule.to_sym, detail: detail)],
145
- findings: violating.map { |o| Contribution.new(category: category, path: o.path, symbol_id: o.symbol_id, line: o.line, value: value.call(o)) }
148
+ findings: violating.map { |o| Contribution.new(category: category, path: o.path, symbol_id: o.symbol_id, line: o.line, value: value.call(o), clone_group: (o.clone_group if o.respond_to?(:clone_group))) }
146
149
  )
147
150
  end
148
151
 
@@ -155,9 +158,11 @@ module Moult
155
158
  )
156
159
  end
157
160
 
158
- # "no X ..." when nothing violates, "<n> X ..." otherwise.
159
- def phrase(violating, none, some)
160
- violating.empty? ? none : "#{violating.size} #{some}"
161
+ # "no X ..." when nothing violates, "<n> X ..." otherwise. +count+ overrides
162
+ # the rendered n where one violation spans several observations (duplication
163
+ # counts groups, not occurrences).
164
+ def phrase(violating, none, some, count: violating.size)
165
+ violating.empty? ? none : "#{count} #{some}"
161
166
  end
162
167
 
163
168
  def verdict_reasons(verdict, failed)
data/lib/moult/gate.rb CHANGED
@@ -127,18 +127,21 @@ module Moult
127
127
  end
128
128
  end
129
129
 
130
- # One observation per clone GROUP touching the diff (mass is a group property);
131
- # attributed to its first in-diff occurrence.
130
+ # One observation per in-diff OCCURRENCE, so every site of a clone is visible
131
+ # downstream. Mass stays a group property shared by all of them, and the
132
+ # occurrences of one group share its clone_group join key.
132
133
  def scope_duplication(run, diff)
133
134
  return nil unless run.ok?
134
135
 
135
- run.value.findings.filter_map do |finding|
136
- occ = finding.occurrences.find { |o| diff.in_diff?(path: o.path, start_line: o.line, end_line: o.line) }
137
- next unless occ
136
+ run.value.findings.flat_map do |finding|
137
+ finding.occurrences.filter_map do |occ|
138
+ next unless diff.in_diff?(path: occ.path, start_line: occ.line, end_line: occ.line)
138
139
 
139
- Evaluation::DuplicationObs.new(
140
- symbol_id: occ.symbol_id, path: occ.path, line: occ.line, mass: finding.mass
141
- )
140
+ Evaluation::DuplicationObs.new(
141
+ symbol_id: occ.symbol_id, path: occ.path, line: occ.line,
142
+ mass: finding.mass, clone_group: finding.clone_group
143
+ )
144
+ end
142
145
  end
143
146
  end
144
147
 
data/lib/moult/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moult
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.1"
5
5
  end
@@ -111,6 +111,10 @@
111
111
  "type": "integer",
112
112
  "minimum": 0
113
113
  },
114
+ "clone_group": {
115
+ "description": "Clone-group join key, \"<kind>:<structural-hash>\" where kind is identical|similar. Every occurrence of this finding shares it. Stable within a report only (the hash comes from the detector backend).",
116
+ "type": ["string", "null"]
117
+ },
114
118
  "reasons": {
115
119
  "description": "The factors that raised or lowered the confidence, in application order.",
116
120
  "type": "array",
@@ -130,6 +130,10 @@
130
130
  "value": {
131
131
  "description": "The observed signal that tripped the rule (a confidence, an ABC score, a clone mass, or a severity). A graded/classified value, never a certainty.",
132
132
  "type": ["number", "string", "null"]
133
+ },
134
+ "clone_group": {
135
+ "description": "Clone-group join key, \"<kind>:<structural-hash>\" where kind is identical|similar. Contributions sharing a value are occurrences of the same clone group. Stable within a report only. Null for non-duplication findings.",
136
+ "type": ["string", "null"]
133
137
  }
134
138
  }
135
139
  },
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moult
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
- - The Moult authors
7
+ - Brandyn Britton
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-02 00:00:00.000000000 Z
11
+ date: 2026-07-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: prism
@@ -58,7 +58,7 @@ description: Moult sheds dead code. `moult hotspots` ranks files by a complexity
58
58
  a rubydex definition graph, with Rails entrypoint awareness. Every finding carries
59
59
  a confidence and its reasons, never a claim of certain death.
60
60
  email:
61
- - contact@moult.dev
61
+ - brandynbb96@gmail.com
62
62
  executables:
63
63
  - moult
64
64
  extensions: []