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 +4 -4
- data/CHANGELOG.md +13 -0
- data/lib/moult/duplication.rb +1 -0
- data/lib/moult/duplication_report.rb +5 -2
- data/lib/moult/gate/evaluation.rb +14 -9
- data/lib/moult/gate.rb +11 -8
- data/lib/moult/version.rb +1 -1
- data/schema/duplication.schema.json +4 -0
- data/schema/gate.schema.json +4 -0
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5ad87ca8d76d09c7808a172aa24b42bf8c2031c4c5e05604f1c98e2a1120aacc
|
|
4
|
+
data.tar.gz: 683a307fa930966927516e37f6b1445b7b0e08c31da996d2c250d8ace63b2e67
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/moult/duplication.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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
|
|
131
|
-
#
|
|
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.
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
@@ -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",
|
data/schema/gate.schema.json
CHANGED
|
@@ -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.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
7
|
+
- Brandyn Britton
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
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
|
-
-
|
|
61
|
+
- brandynbb96@gmail.com
|
|
62
62
|
executables:
|
|
63
63
|
- moult
|
|
64
64
|
extensions: []
|