@lifeaitools/rdc-skills 0.34.0 → 0.35.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 (60) hide show
  1. package/.claude-plugin/plugin.json +284 -1
  2. package/VALIDATOR-ARCHITECTURE.md +534 -0
  3. package/commands/analyze-tests.md +11 -0
  4. package/commands/check-clean-code.md +11 -0
  5. package/commands/check-packages.md +10 -0
  6. package/commands/compare-compliance.md +14 -0
  7. package/commands/full-analysis.md +50 -0
  8. package/commands/get-refactoring-plan.md +13 -0
  9. package/commands/quick-check.md +13 -0
  10. package/commands/recover.md +149 -0
  11. package/commands/review-arch.md +12 -0
  12. package/commands/review.md +12 -113
  13. package/commands/suggest-patterns.md +11 -0
  14. package/commands/validate-solid.md +11 -0
  15. package/package.json +14 -2
  16. package/scripts/architecture-score.mjs +157 -0
  17. package/scripts/clean-code-score.mjs +177 -0
  18. package/scripts/duplication-score.mjs +66 -0
  19. package/scripts/lib/architecture-scoring.mjs +695 -0
  20. package/scripts/lib/clean-code-scoring.mjs +258 -0
  21. package/scripts/lib/duplication-scoring.mjs +238 -0
  22. package/scripts/lib/language-plugin.mjs +82 -0
  23. package/scripts/lib/package-metrics.mjs +439 -0
  24. package/scripts/lib/pattern-scoring.mjs +351 -0
  25. package/scripts/lib/plugins/treesitter.mjs +1182 -0
  26. package/scripts/lib/plugins/typescript.mjs +672 -0
  27. package/scripts/lib/refactoring-scoring.mjs +307 -0
  28. package/scripts/lib/solid-scoring.mjs +101 -0
  29. package/scripts/lib/test-smell-scoring.mjs +581 -0
  30. package/scripts/lib/vendor/codeflow-parser/.source-commit +1 -0
  31. package/scripts/lib/vendor/codeflow-parser/grammars.d.ts +23 -0
  32. package/scripts/lib/vendor/codeflow-parser/grammars.js +57 -0
  33. package/scripts/lib/vendor/codeflow-parser/memberFacts.d.ts +274 -0
  34. package/scripts/lib/vendor/codeflow-parser/memberFacts.js +1117 -0
  35. package/scripts/lib/vendor/codeflow-parser/nativeParser.d.ts +115 -0
  36. package/scripts/lib/vendor/codeflow-parser/nativeParser.js +759 -0
  37. package/scripts/lib/vendor/codeflow-parser/package.json +3 -0
  38. package/scripts/lib/vendor/codeflow-parser/xmlParser.d.ts +77 -0
  39. package/scripts/lib/vendor/codeflow-parser/xmlParser.js +400 -0
  40. package/scripts/package-metrics-cli.mjs +112 -0
  41. package/scripts/pattern-score.mjs +143 -0
  42. package/scripts/refactoring-score.mjs +253 -0
  43. package/scripts/solid-score.mjs +337 -0
  44. package/skills/architecture-reviewer/SKILL.md +287 -0
  45. package/skills/clean-code-analyzer/SKILL.md +147 -0
  46. package/skills/package-design/SKILL.md +118 -0
  47. package/skills/pattern-advisor/SKILL.md +237 -0
  48. package/skills/pattern-refactoring-guide/SKILL.md +262 -0
  49. package/skills/review/SKILL.md +29 -0
  50. package/skills/solid-validator/SKILL.md +92 -0
  51. package/skills/testing-strategy/SKILL.md +132 -0
  52. package/tests/lib/architecture-scoring.test.mjs +335 -0
  53. package/tests/lib/clean-code-scoring.test.mjs +241 -0
  54. package/tests/lib/duplication-scoring.test.mjs +144 -0
  55. package/tests/lib/fixtures.mjs +58 -0
  56. package/tests/lib/package-metrics.test.mjs +241 -0
  57. package/tests/lib/pattern-scoring.test.mjs +251 -0
  58. package/tests/lib/refactoring-scoring.test.mjs +264 -0
  59. package/tests/lib/solid-scoring.test.mjs +291 -0
  60. package/tests/lib/test-smell-scoring.test.mjs +281 -0
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * pattern-score — mechanical design-pattern-opportunity scorer, language-
4
+ * plugin-based, same architecture as solid-score.mjs / clean-code-score.mjs:
5
+ * this CLI owns argv parsing, file walking, and output formatting. It does
6
+ * NOT know what an AST node is — every fact comes from
7
+ * `lib/plugins/typescript.mjs` via the `lib/language-plugin.mjs` contract,
8
+ * and every detector is a pure function in `lib/pattern-scoring.mjs`.
9
+ *
10
+ * Detects 9 patterns — Factory Method, Builder, Singleton (creational),
11
+ * Decorator, Adapter, Facade (structural), Strategy, Observer, Command,
12
+ * Template Method (behavioral) — ported from architecture-toolkit's real
13
+ * `src/agents/pattern-advisor/tools/*-pattern-analyzer.ts` (MIT,
14
+ * github.com/OnSightTeam/architecture-toolkit). See
15
+ * lib/pattern-scoring.mjs's header for the full citation and porting
16
+ * rationale, and skills/pattern-advisor/SKILL.md for the confidence
17
+ * calibration this scorer's hard-coded numbers were checked against.
18
+ *
19
+ * ATF-compatibility: `--format json` output is byte-identical across repeat
20
+ * runs on unchanged input — no timestamps, no absolute paths (file paths are
21
+ * relative to the scanned root), and both the file list and each unit's
22
+ * per-pattern findings are explicitly sorted (file path; then unit name;
23
+ * then, within a pattern, line-then-location — see pattern-scoring.mjs's
24
+ * `patternScore`).
25
+ *
26
+ * Usage:
27
+ * node pattern-score.mjs <path> [--format text|json] [--help]
28
+ */
29
+
30
+ import { readdirSync, statSync, existsSync, realpathSync } from 'node:fs';
31
+ import { join, dirname, relative, resolve, sep } from 'node:path';
32
+ import { pathToFileURL } from 'node:url';
33
+
34
+ import { registerPlugin, pluginFor } from './lib/language-plugin.mjs';
35
+ import { typescriptPlugin } from './lib/plugins/typescript.mjs';
36
+ import { patternScore, PATTERN_NAMES } from './lib/pattern-scoring.mjs';
37
+
38
+ registerPlugin(typescriptPlugin);
39
+ // A future Python plugin registers here too — nothing else in this file changes.
40
+
41
+ const EXCLUDE_DIRS = new Set(['node_modules', '.git']);
42
+
43
+ /** Recursive file walk, directory entries sorted so traversal order (and
44
+ * therefore the resulting file list) is stable across runs and platforms —
45
+ * `readdirSync`'s own order is filesystem-dependent, not a language
46
+ * guarantee. */
47
+ function walk(dir, out = []) {
48
+ for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
49
+ if (EXCLUDE_DIRS.has(entry.name)) continue;
50
+ const full = join(dir, entry.name);
51
+ if (entry.isDirectory()) walk(full, out);
52
+ else out.push(full);
53
+ }
54
+ return out;
55
+ }
56
+
57
+ /**
58
+ * Bash-tool / MSYS argv can hand this native-Windows process a POSIX-shaped
59
+ * path (`/c/Dev/...`) — normalize before use. Mirrors solid-score.mjs's
60
+ * normalizePath() exactly (same failure mode, same fix).
61
+ */
62
+ function normalizePath(p) {
63
+ const m = /^\/([A-Za-z])\/(.*)$/.exec(p);
64
+ const windowsShaped = m ? `${m[1].toUpperCase()}:/${m[2]}` : p;
65
+ const abs = resolve(process.cwd(), windowsShaped);
66
+ try { return realpathSync(abs); } catch { return abs; }
67
+ }
68
+
69
+ function arg(name, fallback = null) { const i = process.argv.indexOf(name); return i !== -1 ? process.argv[i + 1] : fallback; }
70
+ function flag(name) { return process.argv.includes(name); }
71
+
72
+ const HELP = `pattern-score — mechanical design-pattern-opportunity scorer
73
+
74
+ Usage:
75
+ node pattern-score.mjs <path> [--format text|json]
76
+
77
+ Detects 9 patterns ported from architecture-toolkit's real pattern-advisor
78
+ analyzers (MIT, github.com/OnSightTeam/architecture-toolkit):
79
+ Creational: Factory Method, Builder, Singleton
80
+ Structural: Decorator, Adapter, Facade
81
+ Behavioral: Strategy, Observer, Command, Template Method
82
+
83
+ Options:
84
+ --format text|json Output format (default: text)
85
+ --help, -h Show this help
86
+ `;
87
+
88
+ async function main() {
89
+ if (flag('--help') || flag('-h')) { console.log(HELP); process.exit(0); }
90
+
91
+ const rawTarget = process.argv[2]?.startsWith('--') ? process.cwd() : (process.argv[2] ?? process.cwd());
92
+ const targetPath = normalizePath(rawTarget);
93
+ const format = arg('--format', 'text');
94
+
95
+ if (!existsSync(targetPath)) throw new Error(`target path does not exist: ${targetPath}`);
96
+ const isFile = statSync(targetPath).isFile();
97
+ const rootForRelative = isFile ? dirname(targetPath) : targetPath;
98
+ const targetFiles = isFile ? [targetPath] : walk(targetPath);
99
+ const scannedFiles = targetFiles.filter((f) => pluginFor(f)).sort();
100
+ const unresolvedLanguages = targetFiles.filter((f) => !pluginFor(f)).sort();
101
+
102
+ const results = [];
103
+ for (const f of scannedFiles) {
104
+ const plugin = pluginFor(f);
105
+ const units = plugin.extractUnits(f);
106
+ const scored = units.map((u) => patternScore(u)).filter((s) => s.totalFindings > 0);
107
+ if (scored.length) {
108
+ scored.sort((a, b) => a.unit.localeCompare(b.unit));
109
+ results.push({ file: relative(rootForRelative, f).split(sep).join('/'), units: scored });
110
+ }
111
+ }
112
+ results.sort((a, b) => a.file.localeCompare(b.file));
113
+
114
+ const output = { results, unresolvedLanguages, patternCatalog: PATTERN_NAMES };
115
+
116
+ if (format === 'json') {
117
+ console.log(JSON.stringify(output, null, 2));
118
+ } else {
119
+ for (const r of results) {
120
+ for (const u of r.units) {
121
+ console.log(`${r.file} :: ${u.unit} (${u.kind}) — ${u.totalFindings} pattern finding(s)`);
122
+ for (const [patternName, det] of Object.entries(u.patterns)) {
123
+ for (const f of det.findings) {
124
+ console.log(` [${patternName}] [${f.confidence}%/${f.priority}] ${f.location} — ${f.problem}`);
125
+ }
126
+ }
127
+ }
128
+ }
129
+ if (!results.length) console.log('No pattern findings.');
130
+ if (unresolvedLanguages.length) {
131
+ console.log(`\n${unresolvedLanguages.length} file(s) matched no registered language plugin — skipped, not silently passed.`);
132
+ }
133
+ console.log(`\nPatterns checked: ${PATTERN_NAMES.join(', ')}`);
134
+ }
135
+
136
+ process.exit(0);
137
+ }
138
+
139
+ function realFileURL(p) {
140
+ try { return pathToFileURL(realpathSync(p)).href; } catch { return pathToFileURL(p).href; }
141
+ }
142
+ const isMain = process.argv[1] && import.meta.url === realFileURL(process.argv[1]);
143
+ if (isMain) main().catch((err) => { console.error(err); process.exit(2); });
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * refactoring-score — mechanical refactoring-opportunity catalog
4
+ * (extract-method / extract-class / introduce-parameter-object /
5
+ * replace-magic-number / consolidate-duplicate-code / decompose-conditional /
6
+ * strategy-transform / factory-transform / null-object-transform),
7
+ * language-plugin-based, same architecture as clean-code-score.mjs and
8
+ * solid-score.mjs: this CLI owns argv parsing, file walking, and output
9
+ * formatting; it does NOT know what an AST node is — every fact comes from
10
+ * `lib/plugins/typescript.mjs` via the `lib/language-plugin.mjs` contract,
11
+ * and every rule is a pure function in `lib/refactoring-scoring.mjs`.
12
+ *
13
+ * Detection thresholds are ported/corroborated from architecture-toolkit's
14
+ * REAL implementation (MIT, github.com/OnSightTeam/architecture-toolkit) —
15
+ * see refactoring-scoring.mjs's file header for the full citation and for
16
+ * why two thresholds here deliberately differ from this repo's own
17
+ * clean-code-scoring.mjs (extract-method >25 vs. F1's >20; introduce-
18
+ * parameter-object >4 vs. F2's >3).
19
+ *
20
+ * Effort estimation (low/medium/high) needs a real cross-file call-site
21
+ * count, which is genuinely project-wide — it cannot be answered from one
22
+ * file in isolation. This CLI always builds the reference-scan project
23
+ * scope from the FULL walk (not just the requested target), same pattern as
24
+ * clean-code-score.mjs's dead-export scope, and runs the SAME kind of
25
+ * positive control before trusting any effort estimate: a KNOWN-used
26
+ * exported symbol (`refactoringScore` from this package's own
27
+ * lib/refactoring-scoring.mjs, genuinely imported and called by this very
28
+ * file) must scan back with a non-zero reference count, or effort
29
+ * estimation is skipped entirely and every finding reports
30
+ * `effort: null, confidence: 'unmeasured'` rather than guessing.
31
+ *
32
+ * Usage:
33
+ * node refactoring-score.mjs <path> [--project-root <dir>] [--format text|json] [--no-effort]
34
+ */
35
+
36
+ import { readdirSync, statSync, existsSync } from 'node:fs';
37
+ import { join, dirname, relative, resolve, sep } from 'node:path';
38
+ import { pathToFileURL } from 'node:url';
39
+ import { realpathSync } from 'node:fs';
40
+ import { execFileSync } from 'node:child_process';
41
+
42
+ import { registerPlugin, pluginFor } from './lib/language-plugin.mjs';
43
+ import { typescriptPlugin } from './lib/plugins/typescript.mjs';
44
+ import { refactoringScore, estimateEffort } from './lib/refactoring-scoring.mjs';
45
+
46
+ registerPlugin(typescriptPlugin);
47
+
48
+ const EXCLUDE_DIRS = new Set(['node_modules', '.git']);
49
+
50
+ function walk(dir, out = []) {
51
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
52
+ if (EXCLUDE_DIRS.has(entry.name)) continue;
53
+ const full = join(dir, entry.name);
54
+ if (entry.isDirectory()) walk(full, out);
55
+ else out.push(full);
56
+ }
57
+ return out;
58
+ }
59
+
60
+ /**
61
+ * Bash-tool / MSYS argv can hand this native-Windows process a POSIX-shaped
62
+ * path (`/c/Dev/...`) — normalize before use. Mirrors clean-code-score.mjs's
63
+ * / solid-score.mjs's normalizePath() exactly (same failure mode, same fix).
64
+ */
65
+ function normalizePath(p) {
66
+ const m = /^\/([A-Za-z])\/(.*)$/.exec(p);
67
+ const windowsShaped = m ? `${m[1].toUpperCase()}:/${m[2]}` : p;
68
+ const abs = resolve(process.cwd(), windowsShaped);
69
+ try { return realpathSync(abs); } catch { return abs; }
70
+ }
71
+
72
+ function arg(name, fallback = null) { const i = process.argv.indexOf(name); return i !== -1 ? process.argv[i + 1] : fallback; }
73
+ function flag(name) { return process.argv.includes(name); }
74
+
75
+ function printHelp() {
76
+ console.log(`refactoring-score — mechanical refactoring-opportunity catalog
77
+
78
+ Usage:
79
+ node refactoring-score.mjs <path> [--project-root <dir>] [--format text|json] [--no-effort]
80
+
81
+ Options:
82
+ --project-root <dir> Directory that bounds the cross-file call-site scan
83
+ used for effort estimation (default: the containing
84
+ git repo, or the target's own dir if not a repo).
85
+ --format text|json Output format (default: text).
86
+ --no-effort Skip the cross-file call-site scan entirely — every
87
+ finding reports effort: null, confidence: 'unmeasured'.
88
+ --help Show this message.
89
+
90
+ Refactoring types detected: extract-method, extract-class,
91
+ introduce-parameter-object, replace-magic-number, consolidate-duplicate-code,
92
+ decompose-conditional, strategy-transform, factory-transform,
93
+ null-object-transform. Detection logic: scripts/lib/refactoring-scoring.mjs.
94
+ `);
95
+ }
96
+
97
+ /**
98
+ * Derive the target's own top-level package/app segment (e.g.
99
+ * "packages/core", "apps/prt") from an absolute file path, for the
100
+ * package-boundary-crossing effort criterion. Returns null when the file
101
+ * isn't under a recognized `packages/*` or `apps/*` root — effort
102
+ * estimation then falls back to call-site count alone.
103
+ */
104
+ function packageOf(filePath) {
105
+ const norm = filePath.replace(/\\/g, '/');
106
+ const m = /\/(packages|apps|sites|models)\/([^/]+)\//.exec(norm);
107
+ return m ? `${m[1]}/${m[2]}` : null;
108
+ }
109
+
110
+ /**
111
+ * Prove the reference-site scan itself works before trusting any effort
112
+ * estimate from it. `known` is {filePath, exportName} — a symbol the caller
113
+ * KNOWS is referenced elsewhere in `projectFiles` (this CLI uses its own
114
+ * `refactoringScore`, which `isMain` below genuinely imports and calls).
115
+ */
116
+ function runPositiveControl(plugin, known, projectFiles) {
117
+ if (typeof plugin.referenceSitesOf !== 'function') {
118
+ return { ok: false, reason: 'plugin has no referenceSitesOf — effort estimation unmeasured for this language' };
119
+ }
120
+ const result = plugin.referenceSitesOf(known.filePath, known.exportName, projectFiles);
121
+ if (result.referenceCount <= 0) return { ok: false, reason: `positive control export '${known.exportName}' scanned as referenceCount=${result.referenceCount}, expected >0 — scan is broken, not the project` };
122
+ return { ok: true, referenceCount: result.referenceCount };
123
+ }
124
+
125
+ async function main() {
126
+ if (flag('--help') || flag('-h')) { printHelp(); process.exit(0); }
127
+
128
+ const rawTarget = process.argv[2]?.startsWith('--') ? process.cwd() : (process.argv[2] ?? process.cwd());
129
+ const targetPath = normalizePath(rawTarget);
130
+ const format = arg('--format', 'text');
131
+ const projectRootArg = arg('--project-root');
132
+ const skipEffort = flag('--no-effort');
133
+
134
+ if (!existsSync(targetPath)) throw new Error(`target path does not exist: ${targetPath}`);
135
+ const isFile = statSync(targetPath).isFile();
136
+ const targetFiles = isFile ? [targetPath] : walk(targetPath);
137
+ const scannedFiles = targetFiles.filter((f) => pluginFor(f));
138
+
139
+ // Effort-scan project scope: --project-root if given, else the WHOLE
140
+ // containing repo (never just the scanned target/dir) — same rationale as
141
+ // clean-code-score.mjs's dead-export scope: a real caller of a class/
142
+ // function scanned under a directory target routinely lives OUTSIDE that
143
+ // directory.
144
+ let projectFiles = scannedFiles;
145
+ if (!skipEffort) {
146
+ let scopeDir = projectRootArg ? normalizePath(projectRootArg) : (isFile ? dirname(targetPath) : targetPath);
147
+ if (!projectRootArg) {
148
+ try { scopeDir = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd: scopeDir, encoding: 'utf8' }).trim(); } catch { /* not a repo — fall back to the target's own dir */ }
149
+ }
150
+ projectFiles = walk(scopeDir).filter((f) => pluginFor(f));
151
+ }
152
+
153
+ // Positive control target: `refactoringScore`, exported from
154
+ // lib/refactoring-scoring.mjs and genuinely called a few lines below in
155
+ // THIS file — a real, known-used symbol, not a synthetic one.
156
+ const scoringLibPath = normalizePath(join(dirname(process.argv[1]), 'lib', 'refactoring-scoring.mjs'));
157
+ const plugin = pluginFor(scoringLibPath);
158
+ let effortAvailable = false;
159
+ let positiveControl = null;
160
+ if (!skipEffort && plugin) {
161
+ const controlScope = [...new Set([scoringLibPath, normalizePath(process.argv[1]), ...projectFiles])];
162
+ positiveControl = runPositiveControl(plugin, { filePath: scoringLibPath, exportName: 'refactoringScore' }, controlScope);
163
+ effortAvailable = positiveControl.ok;
164
+ }
165
+
166
+ const results = [];
167
+ const unresolvedLanguages = [];
168
+
169
+ for (const f of scannedFiles) {
170
+ const p = pluginFor(f);
171
+ if (!p) { unresolvedLanguages.push(f); continue; }
172
+ const units = p.extractUnits(f);
173
+ const unitPackage = packageOf(f);
174
+ const unitScores = units.map((u) => {
175
+ const score = refactoringScore(u);
176
+ // Effort estimation only applies to CLASS units: `referenceSitesOf`
177
+ // resolves ONE named export per call, and a class's own name IS the
178
+ // export name it would be imported by. A 'module' unit's `u.name` is
179
+ // the file's base name (e.g. "acceptance.mjs"), not an exported
180
+ // symbol — there is no single call-site count for "this file" the
181
+ // same way. Per-member (per-method/per-function) call-site counts are
182
+ // NOT computed here either — see refactoring-score.mjs's own header
183
+ // and the task report for why that's a documented scoping choice, not
184
+ // an oversight.
185
+ let effort = null;
186
+ if (effortAvailable && u.kind === 'class' && p.referenceSitesOf) {
187
+ const referenceSites = p.referenceSitesOf(f, u.name, projectFiles);
188
+ if (referenceSites.referenceCount >= 0) {
189
+ const estimate = estimateEffort({ unitPackage, referenceSites });
190
+ effort = {
191
+ ...estimate,
192
+ files: referenceSites.files.map((rf) => relative(process.cwd(), rf).split(sep).join('/')).sort(),
193
+ };
194
+ }
195
+ }
196
+ return { ...score, effort };
197
+ });
198
+ // Stable finding order — sort each rule's findings by location so
199
+ // re-running on unchanged input is byte-identical regardless of any
200
+ // incidental AST-walk-order variance (ATF golden-capture requirement).
201
+ for (const u of unitScores) {
202
+ for (const rule of Object.values(u.rules)) {
203
+ rule.findings.sort((a, b) => a.location.localeCompare(b.location));
204
+ }
205
+ }
206
+ if (unitScores.some((u) => u.totalFindings > 0)) {
207
+ results.push({ file: relative(process.cwd(), f).split(sep).join('/'), unitPackage, units: unitScores });
208
+ }
209
+ }
210
+ results.sort((a, b) => a.file.localeCompare(b.file));
211
+
212
+ const output = {
213
+ results,
214
+ unresolvedLanguages: unresolvedLanguages.map((f) => relative(process.cwd(), f).split(sep).join('/')).sort(),
215
+ effortScope: skipEffort ? null : { fileCount: projectFiles.length, positiveControlOk: effortAvailable, positiveControlReason: positiveControl?.reason ?? null },
216
+ refactoringTypes: [
217
+ 'extract-method', 'extract-class', 'introduce-parameter-object',
218
+ 'replace-magic-number', 'consolidate-duplicate-code', 'decompose-conditional',
219
+ 'strategy-transform', 'factory-transform', 'null-object-transform',
220
+ ],
221
+ };
222
+
223
+ if (format === 'json') {
224
+ console.log(JSON.stringify(output, null, 2));
225
+ } else {
226
+ for (const r of results) {
227
+ for (const u of r.units) {
228
+ if (u.totalFindings === 0) continue;
229
+ const effortLine = u.effort ? ` — effort: ${u.effort.effort ?? 'unmeasured'} (${u.effort.criterion}, ${u.effort.callSites} call site(s))` : '';
230
+ console.log(`${r.file} :: ${u.unit} (${u.kind}) — ${u.totalFindings} finding(s)${effortLine}`);
231
+ for (const rule of Object.values(u.rules)) {
232
+ for (const f of rule.findings) console.log(` [${rule.refactoringType}] [${rule.confidence}] ${f.location} — ${f.detail}`);
233
+ }
234
+ }
235
+ }
236
+ if (!skipEffort) {
237
+ console.log(effortAvailable
238
+ ? `\nEffort scan: positive control OK (${positiveControl.referenceCount} reference(s) found for a known-used symbol) — ${projectFiles.length} file(s) in scope.`
239
+ : `\nEffort scan: SKIPPED — positive control failed (${positiveControl?.reason ?? 'unknown'}). Every finding reports effort as unmeasured.`);
240
+ }
241
+ if (unresolvedLanguages.length) {
242
+ console.log(`\n${unresolvedLanguages.length} file(s) matched no registered language plugin — skipped, not silently passed.`);
243
+ }
244
+ }
245
+
246
+ process.exit(0);
247
+ }
248
+
249
+ function realFileURL(p) {
250
+ try { return pathToFileURL(realpathSync(p)).href; } catch { return pathToFileURL(p).href; }
251
+ }
252
+ const isMain = process.argv[1] && import.meta.url === realFileURL(process.argv[1]);
253
+ if (isMain) main().catch((err) => { console.error(err); process.exit(2); });