circle-ir-ai 2.42.0 → 2.44.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.
@@ -0,0 +1,127 @@
1
+ /**
2
+ * cognium-ai#198 — per-entity design-smell detection.
3
+ *
4
+ * The quality passes historically keyed on **directory-averaged** circle-ir
5
+ * metrics (avgComplexity, avgCBO, avgLCOM, MI). Averages are refactor-
6
+ * invariant for design smells: splitting one god-class into three small
7
+ * classes, or one high-CC method into three simple ones, barely moves the
8
+ * average because you divide by more entities. Result (#198): across the 18
9
+ * contrastive quality-regression pairs only 1 discriminated smelly-from-
10
+ * refactored; 6 patterns produced no signal at all (McCabe CC, god-class,
11
+ * …), and 10 fired on both twins.
12
+ *
13
+ * The circle-ir metric stream already carries the per-entity data the
14
+ * averages threw away:
15
+ * - one `cyclomatic_complexity` entry per method, tagged
16
+ * `description: "method: <name>"`;
17
+ * - per-type `RFC` / `LCOM` / `CBO` / `DIT` / `NOC` entries, tagged
18
+ * `description: "type: <name>"`.
19
+ *
20
+ * This module consumes that stream and emits **per-entity** smells with
21
+ * per-entity thresholds, so a smell fires on the offending method/class and
22
+ * *not* on its refactored replacements. It is a pure function of the metric
23
+ * array (no I/O, no circle-ir re-analysis) so it is unit-testable against
24
+ * synthetic contrastive pairs in isolation.
25
+ */
26
+ export const DEFAULT_SMELL_THRESHOLDS = {
27
+ methodComplexity: 10,
28
+ classRfc: 12,
29
+ };
30
+ /** Parse `"method: foo"` / `"type: Bar"` → `foo` / `Bar`; else undefined. */
31
+ function entityName(description, prefix) {
32
+ if (!description)
33
+ return undefined;
34
+ const m = new RegExp(`^${prefix}:\\s*(.+)$`).exec(description.trim());
35
+ return m ? m[1].trim() : undefined;
36
+ }
37
+ /**
38
+ * Detect per-entity smells for a single file's circle-ir metric array.
39
+ *
40
+ * - **high-complexity**: any method whose cyclomatic complexity exceeds
41
+ * `methodComplexity`. Discriminates because a refactor lowers the *peak*
42
+ * method's CC, even when total/average CC is unchanged.
43
+ * - **god-class**: a type with a large response surface (`RFC ≥ classRfc`).
44
+ * RFC drops when the class is split across collaborators, so the smell
45
+ * clears on the refactored twin. `LCOM ≥ classLcom` (when available)
46
+ * escalates severity but is not required — see {@link SmellThresholds}.
47
+ */
48
+ export function detectEntitySmells(file, metrics, thresholds = DEFAULT_SMELL_THRESHOLDS) {
49
+ const smells = [];
50
+ // -- Per-method cyclomatic complexity -------------------------------------
51
+ for (const m of metrics) {
52
+ if (m.name !== 'cyclomatic_complexity')
53
+ continue;
54
+ const method = entityName(m.description, 'method');
55
+ if (!method)
56
+ continue; // aggregate/module-level entry — skip
57
+ if (m.value > thresholds.methodComplexity) {
58
+ smells.push({
59
+ file,
60
+ entity: method,
61
+ entityKind: 'method',
62
+ smell: 'high-complexity',
63
+ metric: 'cyclomatic_complexity',
64
+ value: m.value,
65
+ threshold: thresholds.methodComplexity,
66
+ });
67
+ }
68
+ }
69
+ // -- Per-class god-class / low-cohesion -----------------------------------
70
+ // Group the per-type metrics (RFC, LCOM, …) by their type name.
71
+ const byType = new Map();
72
+ for (const m of metrics) {
73
+ const type = entityName(m.description, 'type');
74
+ if (!type)
75
+ continue;
76
+ let bucket = byType.get(type);
77
+ if (!bucket) {
78
+ bucket = new Map();
79
+ byType.set(type, bucket);
80
+ }
81
+ // keep the max if a metric repeats for the same type
82
+ bucket.set(m.name, Math.max(bucket.get(m.name) ?? -Infinity, m.value));
83
+ }
84
+ for (const [type, bucket] of byType) {
85
+ const rfc = bucket.get('RFC');
86
+ if (rfc === undefined || rfc < thresholds.classRfc)
87
+ continue;
88
+ smells.push({
89
+ file,
90
+ entity: type,
91
+ entityKind: 'class',
92
+ smell: 'god-class',
93
+ metric: 'RFC',
94
+ value: rfc,
95
+ threshold: thresholds.classRfc,
96
+ });
97
+ }
98
+ return smells;
99
+ }
100
+ /** Map a smell to a stable quality ruleId. */
101
+ export function smellRuleId(smell) {
102
+ switch (smell.smell) {
103
+ case 'high-complexity':
104
+ return 'code-complexity-high-mccabe';
105
+ case 'god-class':
106
+ return 'code-complexity-god-class';
107
+ }
108
+ }
109
+ /** Map a smell to a severity by how far past the threshold it sits. */
110
+ export function smellSeverity(smell) {
111
+ const ratio = smell.value / smell.threshold;
112
+ if (ratio >= 2)
113
+ return 'high';
114
+ if (ratio >= 1.5)
115
+ return 'medium';
116
+ return 'low';
117
+ }
118
+ /** Human-readable message for a smell finding. */
119
+ export function smellMessage(smell) {
120
+ switch (smell.smell) {
121
+ case 'high-complexity':
122
+ return `Method '${smell.entity}' has cyclomatic complexity ${smell.value} (> ${smell.threshold})`;
123
+ case 'god-class':
124
+ return `Class '${smell.entity}' has a large response surface (${smell.metric} ${smell.value} ≥ ${smell.threshold}) — god-class / large-class smell`;
125
+ }
126
+ }
127
+ //# sourceMappingURL=entity-smells.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity-smells.js","sourceRoot":"","sources":["../../src/quality/entity-smells.ts"],"names":[],"mappings":"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"}
@@ -12,4 +12,5 @@
12
12
  export { type QualityFinding, type QualityPassResult, type QualityTier, type QualityScoreResult, getQualityTier, QUALITY_TIER_COLORS, } from './types.js';
13
13
  export { calculateQualityScore, formatQualityReport, DEFAULT_QUALITY_WEIGHTS, type QualityWeights, type QualityScoreOptions, } from './calculator.js';
14
14
  export { generateQualityBadge, generateQualityBadgeFromScore, } from './badge.js';
15
+ export { detectEntitySmells, smellRuleId, smellSeverity, smellMessage, DEFAULT_SMELL_THRESHOLDS, type EntitySmell, type SmellKind, type SmellThresholds, type MetricEntryLike, } from './entity-smells.js';
15
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/quality/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,cAAc,EACd,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EACnB,uBAAuB,EACvB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,oBAAoB,EACpB,6BAA6B,GAC9B,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/quality/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,cAAc,EACd,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EACnB,uBAAuB,EACvB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,oBAAoB,EACpB,6BAA6B,GAC9B,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,aAAa,EACb,YAAY,EACZ,wBAAwB,EACxB,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,eAAe,GACrB,MAAM,oBAAoB,CAAC"}
@@ -15,4 +15,6 @@ export { getQualityTier, QUALITY_TIER_COLORS, } from './types.js';
15
15
  export { calculateQualityScore, formatQualityReport, DEFAULT_QUALITY_WEIGHTS, } from './calculator.js';
16
16
  // Badge
17
17
  export { generateQualityBadge, generateQualityBadgeFromScore, } from './badge.js';
18
+ // cognium-ai#198 — per-entity design-smell detection
19
+ export { detectEntitySmells, smellRuleId, smellSeverity, smellMessage, DEFAULT_SMELL_THRESHOLDS, } from './entity-smells.js';
18
20
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/quality/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,QAAQ;AACR,OAAO,EAKL,cAAc,EACd,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAEpB,aAAa;AACb,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EACnB,uBAAuB,GAGxB,MAAM,iBAAiB,CAAC;AAEzB,QAAQ;AACR,OAAO,EACL,oBAAoB,EACpB,6BAA6B,GAC9B,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/quality/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,QAAQ;AACR,OAAO,EAKL,cAAc,EACd,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAEpB,aAAa;AACb,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EACnB,uBAAuB,GAGxB,MAAM,iBAAiB,CAAC;AAEzB,QAAQ;AACR,OAAO,EACL,oBAAoB,EACpB,6BAA6B,GAC9B,MAAM,YAAY,CAAC;AAEpB,qDAAqD;AACrD,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,aAAa,EACb,YAAY,EACZ,wBAAwB,GAKzB,MAAM,oBAAoB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circle-ir-ai",
3
- "version": "2.42.0",
3
+ "version": "2.44.0",
4
4
  "description": "LLM-enhanced SAST analysis built on circle-ir",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",