@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,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();