@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.
- 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,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* clean-code-score — mechanical Clean Code rule catalog (N1/N2/N4/N7/F1/F2/
|
|
4
|
+
* E1/G9), language-plugin-based, same architecture as solid-score.mjs: this
|
|
5
|
+
* CLI owns argv parsing, file walking, and output formatting; it does NOT
|
|
6
|
+
* know what an AST node is — every fact comes from `lib/plugins/typescript.mjs`
|
|
7
|
+
* via the `lib/language-plugin.mjs` contract, and every rule is a pure
|
|
8
|
+
* function in `lib/clean-code-scoring.mjs`.
|
|
9
|
+
*
|
|
10
|
+
* G9's unused-export half needs a cross-file reference scan
|
|
11
|
+
* (`plugin.deadExportsOf`), which is genuinely project-wide — it cannot be
|
|
12
|
+
* answered from one file in isolation. This CLI always builds the export-
|
|
13
|
+
* usage project scope from the FULL walk (not just the requested target),
|
|
14
|
+
* so a single-file target still gets real cross-file dead-export answers
|
|
15
|
+
* (the `--project-root` flag only widens that scope further, e.g. to
|
|
16
|
+
* include a sibling `tests/` directory outside the scanned tree).
|
|
17
|
+
*
|
|
18
|
+
* Before trusting any `referenceCount === 0` finding, this CLI runs the SAME
|
|
19
|
+
* scan against a KNOWN-used export (`cleanCodeScore`, exported from
|
|
20
|
+
* lib/clean-code-scoring.mjs and genuinely called a few lines below in this
|
|
21
|
+
* very file) and refuses to report dead-export findings if that positive
|
|
22
|
+
* control itself comes back at 0 — see `runPositiveControl()`. That is the
|
|
23
|
+
* rule required by .claude/rules/prove-absence-positive-control.md and by
|
|
24
|
+
* this task's own instruction: an unverified "zero callers" is a guess, not
|
|
25
|
+
* a finding.
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* node clean-code-score.mjs <path> [--project-root <dir>] [--format text|json] [--no-dead-exports]
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { readdirSync, statSync, existsSync } from 'node:fs';
|
|
32
|
+
import { join, dirname, relative, resolve, sep } from 'node:path';
|
|
33
|
+
import { pathToFileURL } from 'node:url';
|
|
34
|
+
import { realpathSync } from 'node:fs';
|
|
35
|
+
import { execFileSync } from 'node:child_process';
|
|
36
|
+
|
|
37
|
+
import { registerPlugin, pluginFor } from './lib/language-plugin.mjs';
|
|
38
|
+
import { typescriptPlugin } from './lib/plugins/typescript.mjs';
|
|
39
|
+
import { cleanCodeScore } from './lib/clean-code-scoring.mjs';
|
|
40
|
+
|
|
41
|
+
registerPlugin(typescriptPlugin);
|
|
42
|
+
|
|
43
|
+
const EXCLUDE_DIRS = new Set(['node_modules', '.git']);
|
|
44
|
+
|
|
45
|
+
function walk(dir, out = []) {
|
|
46
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
47
|
+
if (EXCLUDE_DIRS.has(entry.name)) continue;
|
|
48
|
+
const full = join(dir, entry.name);
|
|
49
|
+
if (entry.isDirectory()) walk(full, out);
|
|
50
|
+
else out.push(full);
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Bash-tool / MSYS argv can hand this native-Windows process a POSIX-shaped
|
|
57
|
+
* path (`/c/Dev/...`) — normalize before use. Mirrors solid-score.mjs's
|
|
58
|
+
* normalizePath() exactly (same failure mode, same fix).
|
|
59
|
+
*/
|
|
60
|
+
function normalizePath(p) {
|
|
61
|
+
const m = /^\/([A-Za-z])\/(.*)$/.exec(p);
|
|
62
|
+
const windowsShaped = m ? `${m[1].toUpperCase()}:/${m[2]}` : p;
|
|
63
|
+
const abs = resolve(process.cwd(), windowsShaped);
|
|
64
|
+
try { return realpathSync(abs); } catch { return abs; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function arg(name, fallback = null) { const i = process.argv.indexOf(name); return i !== -1 ? process.argv[i + 1] : fallback; }
|
|
68
|
+
function flag(name) { return process.argv.includes(name); }
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Prove the dead-export scan itself works before trusting any zero result
|
|
72
|
+
* from it. `known` is {filePath, exportName} — a symbol the caller KNOWS is
|
|
73
|
+
* referenced elsewhere in `projectFiles` (this CLI uses its own `main`,
|
|
74
|
+
* which `isMain` below genuinely calls).
|
|
75
|
+
*/
|
|
76
|
+
function runPositiveControl(plugin, known, projectFiles) {
|
|
77
|
+
if (typeof plugin.deadExportsOf !== 'function') {
|
|
78
|
+
return { ok: false, reason: 'plugin has no deadExportsOf — G9 export-usage half unmeasured for this language' };
|
|
79
|
+
}
|
|
80
|
+
const facts = plugin.deadExportsOf(known.filePath, projectFiles);
|
|
81
|
+
const hit = facts.find((f) => f.name === known.exportName);
|
|
82
|
+
if (!hit) return { ok: false, reason: `positive control export '${known.exportName}' not found in scan results at all — scan is broken` };
|
|
83
|
+
if (hit.referenceCount <= 0) return { ok: false, reason: `positive control export '${known.exportName}' scanned as referenceCount=${hit.referenceCount}, expected >0 — scan is broken, not the project` };
|
|
84
|
+
return { ok: true, referenceCount: hit.referenceCount };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function main() {
|
|
88
|
+
const rawTarget = process.argv[2]?.startsWith('--') ? process.cwd() : (process.argv[2] ?? process.cwd());
|
|
89
|
+
const targetPath = normalizePath(rawTarget);
|
|
90
|
+
const format = arg('--format', 'text');
|
|
91
|
+
const projectRootArg = arg('--project-root');
|
|
92
|
+
const skipDeadExports = flag('--no-dead-exports');
|
|
93
|
+
|
|
94
|
+
if (!existsSync(targetPath)) throw new Error(`target path does not exist: ${targetPath}`);
|
|
95
|
+
const isFile = statSync(targetPath).isFile();
|
|
96
|
+
const targetFiles = isFile ? [targetPath] : walk(targetPath);
|
|
97
|
+
const scannedFiles = targetFiles.filter((f) => pluginFor(f));
|
|
98
|
+
|
|
99
|
+
// Export-usage project scope: --project-root if given, else the WHOLE
|
|
100
|
+
// containing repo (never just the scanned target/dir) — a real caller of
|
|
101
|
+
// an export scanned under a directory target routinely lives OUTSIDE that
|
|
102
|
+
// directory (confirmed live: scanning only `scripts/lib` reported
|
|
103
|
+
// `loadAllManifests` as a dead export at referenceCount 0, because its two
|
|
104
|
+
// real callers — scripts/acceptance.mjs, scripts/self-test.mjs — are
|
|
105
|
+
// siblings of `lib/`, not inside it). Defaulting to the target dir instead
|
|
106
|
+
// of the repo root is a false-positive generator, not a narrower-but-valid
|
|
107
|
+
// scope.
|
|
108
|
+
let projectFiles = scannedFiles;
|
|
109
|
+
if (!skipDeadExports) {
|
|
110
|
+
let scopeDir = projectRootArg ? normalizePath(projectRootArg) : (isFile ? dirname(targetPath) : targetPath);
|
|
111
|
+
if (!projectRootArg) {
|
|
112
|
+
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 */ }
|
|
113
|
+
}
|
|
114
|
+
projectFiles = walk(scopeDir).filter((f) => pluginFor(f));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Positive control target: `cleanCodeScore`, exported from
|
|
118
|
+
// lib/clean-code-scoring.mjs and genuinely called a few lines below in
|
|
119
|
+
// THIS file — a real, known-used symbol, not a synthetic one.
|
|
120
|
+
const scoringLibPath = normalizePath(join(dirname(process.argv[1]), 'lib', 'clean-code-scoring.mjs'));
|
|
121
|
+
const plugin = pluginFor(scoringLibPath);
|
|
122
|
+
let deadExportsAvailable = false;
|
|
123
|
+
let positiveControl = null;
|
|
124
|
+
if (!skipDeadExports && plugin) {
|
|
125
|
+
const controlScope = [...new Set([scoringLibPath, normalizePath(process.argv[1]), ...projectFiles])];
|
|
126
|
+
positiveControl = runPositiveControl(plugin, { filePath: scoringLibPath, exportName: 'cleanCodeScore' }, controlScope);
|
|
127
|
+
deadExportsAvailable = positiveControl.ok;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const results = [];
|
|
131
|
+
const unresolvedLanguages = [];
|
|
132
|
+
|
|
133
|
+
for (const f of scannedFiles) {
|
|
134
|
+
const p = pluginFor(f);
|
|
135
|
+
if (!p) { unresolvedLanguages.push(f); continue; }
|
|
136
|
+
const units = p.extractUnits(f);
|
|
137
|
+
const deadExportsFacts = deadExportsAvailable ? p.deadExportsOf(f, projectFiles) : null;
|
|
138
|
+
results.push({ file: f, units: units.map((u) => cleanCodeScore(u, deadExportsFacts)) });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const output = {
|
|
142
|
+
results, unresolvedLanguages,
|
|
143
|
+
deadExportsScope: skipDeadExports ? null : { fileCount: projectFiles.length, positiveControlOk: deadExportsAvailable, positiveControl },
|
|
144
|
+
notImplemented: ['N3', 'N5', 'N6', 'C1', 'C2', 'C3', 'C4', 'C5', 'G5', 'G14', 'G16', 'G28'],
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
if (format === 'json') {
|
|
148
|
+
console.log(JSON.stringify(output, null, 2));
|
|
149
|
+
} else {
|
|
150
|
+
for (const r of results) {
|
|
151
|
+
for (const u of r.units) {
|
|
152
|
+
if (u.totalFindings === 0) continue;
|
|
153
|
+
console.log(`${relative(process.cwd(), r.file).split(sep).join('/')} :: ${u.unit} (${u.kind}) — ${u.totalFindings} finding(s)`);
|
|
154
|
+
for (const rule of Object.values(u.rules)) {
|
|
155
|
+
for (const f of rule.findings) console.log(` [${rule.ruleId}] [${rule.confidence}] ${f.location} — ${f.detail}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (!skipDeadExports) {
|
|
160
|
+
console.log(deadExportsAvailable
|
|
161
|
+
? `\nDead-export scan: positive control OK (${positiveControl.referenceCount} reference(s) found for a known-used symbol) — ${projectFiles.length} file(s) in scope.`
|
|
162
|
+
: `\nDead-export scan: SKIPPED — positive control failed (${positiveControl?.reason ?? 'unknown'}). G9 export-usage findings are NOT reported; only the unreachable-code half ran.`);
|
|
163
|
+
}
|
|
164
|
+
if (unresolvedLanguages.length) {
|
|
165
|
+
console.log(`\n${unresolvedLanguages.length} file(s) matched no registered language plugin — skipped, not silently passed.`);
|
|
166
|
+
}
|
|
167
|
+
console.log(`\nNot implemented (heuristic quality too low / needs a semantic model): ${output.notImplemented.join(', ')}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
process.exit(0);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function realFileURL(p) {
|
|
174
|
+
try { return pathToFileURL(realpathSync(p)).href; } catch { return pathToFileURL(p).href; }
|
|
175
|
+
}
|
|
176
|
+
const isMain = process.argv[1] && import.meta.url === realFileURL(process.argv[1]);
|
|
177
|
+
if (isMain) main().catch((err) => { console.error(err); process.exit(2); });
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* rdc-duplication-score — G5 Clean Code rule, CLI.
|
|
4
|
+
* See scripts/lib/duplication-scoring.mjs for the algorithm and provenance.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync, readdirSync, statSync, realpathSync } from 'node:fs';
|
|
7
|
+
import { resolve, extname, relative } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { findDuplicates } from './lib/duplication-scoring.mjs';
|
|
10
|
+
|
|
11
|
+
function walkFiles(dir, exts, out = []) {
|
|
12
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
13
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
|
|
14
|
+
const full = resolve(dir, entry.name);
|
|
15
|
+
if (entry.isDirectory()) walkFiles(full, exts, out);
|
|
16
|
+
else if (exts.includes(extname(entry.name))) out.push(full);
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function printHelp() {
|
|
22
|
+
console.log(`rdc-duplication-score <path> [--min-tokens <n>] [--format text|json]
|
|
23
|
+
|
|
24
|
+
Detects G5 (Duplicate Code) via token-shingle Rabin-Karp matching across all
|
|
25
|
+
files under <path>. Default --min-tokens 50 (matches PMD CPD's default).`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function main() {
|
|
29
|
+
const args = process.argv.slice(2);
|
|
30
|
+
if (args.includes('--help') || args.includes('-h') || args.length === 0) {
|
|
31
|
+
printHelp();
|
|
32
|
+
process.exit(args.length === 0 ? 1 : 0);
|
|
33
|
+
}
|
|
34
|
+
const positional = args.filter((a) => !a.startsWith('--'));
|
|
35
|
+
const target = resolve(positional[0]);
|
|
36
|
+
const minTokensIdx = args.indexOf('--min-tokens');
|
|
37
|
+
const minTokens = minTokensIdx !== -1 ? Number(args[minTokensIdx + 1]) : 50;
|
|
38
|
+
const format = args.includes('--format') ? args[args.indexOf('--format') + 1] : 'text';
|
|
39
|
+
|
|
40
|
+
const exts = ['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.py'];
|
|
41
|
+
const st = statSync(target);
|
|
42
|
+
const files = st.isDirectory() ? walkFiles(target, exts) : [target];
|
|
43
|
+
|
|
44
|
+
const fileRecords = files.map((f) => ({
|
|
45
|
+
file: relative(process.cwd(), f).split('\\').join('/'),
|
|
46
|
+
text: readFileSync(f, 'utf8'),
|
|
47
|
+
ext: extname(f),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
const result = findDuplicates(fileRecords, minTokens);
|
|
51
|
+
|
|
52
|
+
if (format === 'json') {
|
|
53
|
+
console.log(JSON.stringify(result, null, 2));
|
|
54
|
+
} else {
|
|
55
|
+
console.log(`G5 Duplicate Code — ${result.filesScanned} file(s) scanned, min ${minTokens} tokens`);
|
|
56
|
+
for (const d of result.duplicates) {
|
|
57
|
+
console.log(`\n${d.tokenCount}+ token block found in ${d.occurrences.length} location(s):`);
|
|
58
|
+
for (const o of d.occurrences) console.log(` ${o.file}:${o.startLine}-${o.endLine}`);
|
|
59
|
+
}
|
|
60
|
+
console.log(`\n${result.duplicates.length} duplicate block(s) found.`);
|
|
61
|
+
}
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const isMain = process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
66
|
+
if (isMain) main();
|