@lifeaitools/rdc-skills 0.34.1 → 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.
- package/.claude-plugin/plugin.json +284 -1
- package/VALIDATOR-ARCHITECTURE.md +534 -0
- package/commands/analyze-tests.md +11 -0
- package/commands/check-clean-code.md +11 -0
- package/commands/check-packages.md +10 -0
- package/commands/compare-compliance.md +14 -0
- package/commands/full-analysis.md +50 -0
- package/commands/get-refactoring-plan.md +13 -0
- package/commands/quick-check.md +13 -0
- package/commands/recover.md +149 -0
- package/commands/review-arch.md +12 -0
- package/commands/review.md +12 -113
- package/commands/suggest-patterns.md +11 -0
- package/commands/validate-solid.md +11 -0
- package/package.json +14 -2
- package/scripts/architecture-score.mjs +157 -0
- package/scripts/clean-code-score.mjs +177 -0
- package/scripts/duplication-score.mjs +66 -0
- package/scripts/lib/architecture-scoring.mjs +695 -0
- package/scripts/lib/clean-code-scoring.mjs +258 -0
- package/scripts/lib/duplication-scoring.mjs +238 -0
- package/scripts/lib/language-plugin.mjs +82 -0
- package/scripts/lib/package-metrics.mjs +439 -0
- package/scripts/lib/pattern-scoring.mjs +351 -0
- package/scripts/lib/plugins/treesitter.mjs +1182 -0
- package/scripts/lib/plugins/typescript.mjs +672 -0
- package/scripts/lib/refactoring-scoring.mjs +307 -0
- package/scripts/lib/solid-scoring.mjs +101 -0
- package/scripts/lib/test-smell-scoring.mjs +581 -0
- package/scripts/lib/vendor/codeflow-parser/.source-commit +1 -0
- package/scripts/lib/vendor/codeflow-parser/grammars.d.ts +23 -0
- package/scripts/lib/vendor/codeflow-parser/grammars.js +57 -0
- package/scripts/lib/vendor/codeflow-parser/memberFacts.d.ts +274 -0
- package/scripts/lib/vendor/codeflow-parser/memberFacts.js +1117 -0
- package/scripts/lib/vendor/codeflow-parser/nativeParser.d.ts +115 -0
- package/scripts/lib/vendor/codeflow-parser/nativeParser.js +759 -0
- package/scripts/lib/vendor/codeflow-parser/package.json +3 -0
- package/scripts/lib/vendor/codeflow-parser/xmlParser.d.ts +77 -0
- package/scripts/lib/vendor/codeflow-parser/xmlParser.js +400 -0
- package/scripts/package-metrics-cli.mjs +112 -0
- package/scripts/pattern-score.mjs +143 -0
- package/scripts/refactoring-score.mjs +253 -0
- package/scripts/solid-score.mjs +337 -0
- package/skills/architecture-reviewer/SKILL.md +287 -0
- package/skills/clean-code-analyzer/SKILL.md +147 -0
- package/skills/package-design/SKILL.md +118 -0
- package/skills/pattern-advisor/SKILL.md +237 -0
- package/skills/pattern-refactoring-guide/SKILL.md +262 -0
- package/skills/review/SKILL.md +29 -0
- package/skills/solid-validator/SKILL.md +92 -0
- package/skills/testing-strategy/SKILL.md +132 -0
- package/tests/lib/architecture-scoring.test.mjs +335 -0
- package/tests/lib/clean-code-scoring.test.mjs +241 -0
- package/tests/lib/duplication-scoring.test.mjs +144 -0
- package/tests/lib/fixtures.mjs +58 -0
- package/tests/lib/package-metrics.test.mjs +241 -0
- package/tests/lib/pattern-scoring.test.mjs +251 -0
- package/tests/lib/refactoring-scoring.test.mjs +264 -0
- package/tests/lib/solid-scoring.test.mjs +291 -0
- 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); });
|