@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,337 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* solid-score — SOLID + Clean Architecture scoring, language-plugin-based.
|
|
4
|
+
*
|
|
5
|
+
* This CLI is deliberately language-agnostic. It owns: argv parsing, config
|
|
6
|
+
* loading, git-diff orchestration, the boundary-rule check, and output
|
|
7
|
+
* formatting. It does NOT know what an AST node is — every fact about source
|
|
8
|
+
* code comes from whichever plugin in `lib/plugins/` claims the file
|
|
9
|
+
* (see lib/language-plugin.mjs for the contract). Day 1 ships one plugin
|
|
10
|
+
* (TypeScript/JavaScript, via ts-morph). Adding Python later means writing
|
|
11
|
+
* `lib/plugins/python.mjs` against the same contract — nothing in this file
|
|
12
|
+
* changes.
|
|
13
|
+
*
|
|
14
|
+
* It does not persist a baseline file. `--diff <ref>` scores each changed
|
|
15
|
+
* unit TWICE — once at <ref> via `git show`, once against the CURRENT
|
|
16
|
+
* WORKING TREE (uncommitted and untracked changes included, not just what's
|
|
17
|
+
* been committed) — and gates on the delta. `git diff --name-only <ref>`
|
|
18
|
+
* (no `...HEAD`) compares the ref directly against the working directory,
|
|
19
|
+
* which also means the "before" side (`git show <ref>:<path>`) and the file
|
|
20
|
+
* list are read from the exact same commit — no separate merge-base to fall
|
|
21
|
+
* out of sync with it.
|
|
22
|
+
*
|
|
23
|
+
* A SEPARATE, non-SOLID check rides alongside the five letters: Clean
|
|
24
|
+
* Architecture's dependency rule — an orchestrator module declared to own a
|
|
25
|
+
* set of ports must actually import and delegate to them, not reimplement
|
|
26
|
+
* their logic inline. Dogfooded proof this matters: `rdc-harness`'s `Harness`
|
|
27
|
+
* god-object scores 68.5/100 on the plain weighted SOLID sum (DIP alone
|
|
28
|
+
* doesn't catch it — a class with almost no dependencies of ANY kind scores
|
|
29
|
+
* fine on concrete-instantiation ratio) but fails the boundary check outright
|
|
30
|
+
* on all six of its declared ports.
|
|
31
|
+
*
|
|
32
|
+
* A per-consumer-repo config lives at `<repo>/.solid-score.yml` — this
|
|
33
|
+
* package ships no default boundary rules of its own; each repo declares
|
|
34
|
+
* its own orchestrator/port pairs.
|
|
35
|
+
*
|
|
36
|
+
* Usage:
|
|
37
|
+
* node solid-score.mjs <path> [--config <file>] [--diff <ref>]
|
|
38
|
+
* [--format text|json] [--parser tree-sitter|ts-morph]
|
|
39
|
+
*
|
|
40
|
+
* `--parser` selects the AST backend (default `tree-sitter`, see the
|
|
41
|
+
* PARSER_PLUGINS block below for why).
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { execFileSync } from 'node:child_process';
|
|
45
|
+
import { readFileSync, existsSync, readdirSync, statSync, realpathSync } from 'node:fs';
|
|
46
|
+
import { join, dirname, relative, resolve, sep } from 'node:path';
|
|
47
|
+
import { pathToFileURL } from 'node:url';
|
|
48
|
+
import { parse as parseYaml } from 'yaml';
|
|
49
|
+
|
|
50
|
+
import { registerPlugin, pluginFor } from './lib/language-plugin.mjs';
|
|
51
|
+
import { typescriptPlugin } from './lib/plugins/typescript.mjs';
|
|
52
|
+
import { treesitterPlugin } from './lib/plugins/treesitter.mjs';
|
|
53
|
+
import { scoreUnit } from './lib/solid-scoring.mjs';
|
|
54
|
+
|
|
55
|
+
// AST backend selection — `--parser tree-sitter|ts-morph`, default
|
|
56
|
+
// `tree-sitter` per direct operator instruction (Dave, 2026-08-20): the
|
|
57
|
+
// fleet's own standalone, in-process, multi-language tree-sitter parser
|
|
58
|
+
// (ported from CodeFlow's `packages/codeflow-parser/src/nativeParser.ts`,
|
|
59
|
+
// see scripts/lib/plugins/treesitter.mjs's own header for the full port
|
|
60
|
+
// citation) replaces the third-party TS-only ts-morph backend as the
|
|
61
|
+
// default for this CLI. `--parser ts-morph` stays available as an escape
|
|
62
|
+
// hatch/regression-comparison lever, not a silent removal — ts-morph
|
|
63
|
+
// remains the backend the other 3 ts-morph tools (clean-code-score,
|
|
64
|
+
// pattern-score, refactoring-score) use this pass; only SOLID moves.
|
|
65
|
+
const PARSER_PLUGINS = { 'tree-sitter': treesitterPlugin, 'ts-morph': typescriptPlugin };
|
|
66
|
+
|
|
67
|
+
const parserArgIndex = process.argv.indexOf('--parser');
|
|
68
|
+
const parserName = parserArgIndex !== -1 ? process.argv[parserArgIndex + 1] : 'tree-sitter';
|
|
69
|
+
if (!PARSER_PLUGINS[parserName]) {
|
|
70
|
+
throw new Error(`--parser must be one of ${Object.keys(PARSER_PLUGINS).join('|')}, got "${parserName}"`);
|
|
71
|
+
}
|
|
72
|
+
// Register only the SELECTED plugin — typescriptPlugin and treesitterPlugin
|
|
73
|
+
// both claim the identical file-extension set (canHandle), so registering
|
|
74
|
+
// both would make `pluginFor()` always resolve to whichever was registered
|
|
75
|
+
// first regardless of `--parser`, silently ignoring the flag.
|
|
76
|
+
registerPlugin(PARSER_PLUGINS[parserName]);
|
|
77
|
+
// A future Python plugin registers here too — nothing else in this file changes.
|
|
78
|
+
|
|
79
|
+
export const DEFAULT_CONFIG = {
|
|
80
|
+
weights: { srp: 0.20, ocp: 0.15, lsp: 0.15, isp: 0.20, dip: 0.30 },
|
|
81
|
+
thresholds: null, // not implemented — see skills/solid-validator/SKILL.md "Known gaps"; left null so a config setting it is visibly ignored rather than silently accepted
|
|
82
|
+
diff: { maxDecrease: 0, newUnitMin: 0 },
|
|
83
|
+
exclude: ['**/test/**', '**/*.test.*', '**/node_modules/**'],
|
|
84
|
+
boundaries: [], // [{ orchestrator: glob, requiredPorts: [glob, ...] }]
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A config path that was explicitly PASSED and does not exist is a user
|
|
89
|
+
* error, not "use defaults" — an absent `--config` flag (no path at all)
|
|
90
|
+
* legitimately means "no config, use defaults". Before this fix the two
|
|
91
|
+
* were indistinguishable: a typo'd path silently produced `boundaries: []`
|
|
92
|
+
* and a clean exit 0, and every SKILL.md-documented invocation of this tool
|
|
93
|
+
* pointed at `.solid-score.yml`, a filename that existed in no repo tonight
|
|
94
|
+
* — so the Clean Architecture gate ran with zero configured rules on every
|
|
95
|
+
* documented call site.
|
|
96
|
+
*/
|
|
97
|
+
export function loadConfig(path) {
|
|
98
|
+
if (!path) return { config: structuredClone(DEFAULT_CONFIG), configPath: null, configLoaded: false };
|
|
99
|
+
if (!existsSync(path)) throw new Error(`--config ${path} does not exist`);
|
|
100
|
+
const raw = parseYaml(readFileSync(path, 'utf8'));
|
|
101
|
+
const config = {
|
|
102
|
+
...structuredClone(DEFAULT_CONFIG), ...raw,
|
|
103
|
+
weights: { ...DEFAULT_CONFIG.weights, ...(raw.weights ?? {}) },
|
|
104
|
+
diff: { ...DEFAULT_CONFIG.diff, ...(raw.diff ?? {}) },
|
|
105
|
+
};
|
|
106
|
+
return { config, configPath: path, configLoaded: true };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const GLOB_STAR_TOKEN = '\u0001GLOBSTAR\u0001';
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Real glob-to-regex, not a globstar-strip substring check — a single `*`
|
|
113
|
+
* in a user config previously survived as a literal character and matched
|
|
114
|
+
* nothing. Built with string split/join rather than regex-literal replace
|
|
115
|
+
* to sidestep a tool encoding issue that corrupted a prior version of this
|
|
116
|
+
* function with embedded NUL bytes at the globstar/space substitution
|
|
117
|
+
* points — confirmed byte-for-byte with a raw scan before rewriting.
|
|
118
|
+
*/
|
|
119
|
+
function globToRegExp(glob) {
|
|
120
|
+
const specialChars = ['.', '+', '^', '$', '{', '}', '(', ')', '|', '[', ']'];
|
|
121
|
+
let escaped = glob;
|
|
122
|
+
for (const ch of specialChars) escaped = escaped.split(ch).join(`\\${ch}`);
|
|
123
|
+
const withGlobstar = escaped.split('**').join(GLOB_STAR_TOKEN);
|
|
124
|
+
const withStar = withGlobstar.split('*').join('[^/]*');
|
|
125
|
+
const final = withStar.split(GLOB_STAR_TOKEN).join('.*');
|
|
126
|
+
return new RegExp(`(^|/)${final}$|(^|/)${final}(/|$)`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isExcluded(relPath, exclude) {
|
|
130
|
+
return exclude.some((p) => globToRegExp(p).test(relPath));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Recursive file walk — no glob library, so no more languages than plugins to add here either. */
|
|
134
|
+
function walk(dir, exclude, out = []) {
|
|
135
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
136
|
+
const full = join(dir, entry.name);
|
|
137
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
138
|
+
if (entry.isDirectory()) walk(full, exclude, out);
|
|
139
|
+
else if (!isExcluded(full.split(sep).join('/'), exclude)) out.push(full);
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Score a file's text as it existed at a given git ref, without touching the working tree. */
|
|
145
|
+
function scoreAtRef(repoRoot, filePath, ref, weights) {
|
|
146
|
+
const relPath = relative(repoRoot, filePath).split('\\').join('/');
|
|
147
|
+
let text;
|
|
148
|
+
try {
|
|
149
|
+
text = execFileSync('git', ['show', `${ref}:${relPath}`], { cwd: repoRoot, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
150
|
+
} catch (err) {
|
|
151
|
+
// `git show` fails both when the file is genuinely new at `ref` AND for
|
|
152
|
+
// unrelated reasons (a path-form mismatch, a bad ref). Collapsing both
|
|
153
|
+
// into "it's new" hides the second case — the exact class of silent
|
|
154
|
+
// path-form bug found three times in this file already tonight. Only a
|
|
155
|
+
// "does not exist" message is treated as new; anything else is rethrown.
|
|
156
|
+
const msg = String(err.stderr ?? err.message ?? '');
|
|
157
|
+
if (msg.includes('does not exist in') || msg.includes('fatal: path') && msg.includes('does not exist')) return null;
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
const plugin = pluginFor(filePath);
|
|
161
|
+
if (!plugin) return null;
|
|
162
|
+
return plugin.extractUnits(filePath, text).map((u) => scoreUnit(u, weights));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Does `orchestrator` import from every `requiredPorts` glob it's declared
|
|
167
|
+
* to own? Both fields are documented as globs; a rule that matches zero
|
|
168
|
+
* files is a config error (typo'd path, moved file) and MUST be reported —
|
|
169
|
+
* silently passing zero matches makes a misconfigured rule indistinguishable
|
|
170
|
+
* from a satisfied one.
|
|
171
|
+
*/
|
|
172
|
+
function checkBoundaries(files, boundaries, repoRoot) {
|
|
173
|
+
const findings = [];
|
|
174
|
+
const misconfigured = [];
|
|
175
|
+
for (const rule of boundaries) {
|
|
176
|
+
const orchestratorRe = globToRegExp(rule.orchestrator.split('\\').join('/'));
|
|
177
|
+
const matched = files.filter((f) => orchestratorRe.test(relative(repoRoot, f).split('\\').join('/')));
|
|
178
|
+
if (!matched.length) { misconfigured.push({ rule: rule.orchestrator, reason: 'matched zero files in the scanned set' }); continue; }
|
|
179
|
+
for (const f of matched) {
|
|
180
|
+
const plugin = pluginFor(f);
|
|
181
|
+
if (!plugin) continue;
|
|
182
|
+
const importPaths = plugin.importsOf(f);
|
|
183
|
+
for (const port of rule.requiredPorts) {
|
|
184
|
+
const portRe = globToRegExp(port);
|
|
185
|
+
const substringFallback = port.split('**')[0].replace(/^\.\//, '');
|
|
186
|
+
findings.push({ file: relative(repoRoot, f), requiredPort: port, satisfied: importPaths.some((p) => portRe.test(p) || p.includes(substringFallback)) });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return { findings, misconfigured };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── CLI ──────────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
function arg(name, fallback = null) { const i = process.argv.indexOf(name); return i !== -1 ? process.argv[i + 1] : fallback; }
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Bash-tool / MSYS argv can hand this process a POSIX-shaped path
|
|
199
|
+
* (`/c/Dev/...`) even though this is native Windows node. The filesystem
|
|
200
|
+
* layer only resolves Windows-shaped paths — a POSIX path fails silently
|
|
201
|
+
* upstream of any error message. Normalize before use.
|
|
202
|
+
*/
|
|
203
|
+
function normalizePath(p) {
|
|
204
|
+
const m = /^\/([A-Za-z])\/(.*)$/.exec(p);
|
|
205
|
+
const windowsShaped = m ? `${m[1].toUpperCase()}:/${m[2]}` : p;
|
|
206
|
+
// A relative arg ("packages/core/src/index.mjs") must become absolute
|
|
207
|
+
// BEFORE any downstream comparison — the boundary check always builds its
|
|
208
|
+
// side of the match from repoRoot-absolute paths, so a relative target
|
|
209
|
+
// silently matches nothing.
|
|
210
|
+
const abs = resolve(process.cwd(), windowsShaped);
|
|
211
|
+
// realpathSync so a path reached through a symlink/junction compares
|
|
212
|
+
// equal to repoRoot (derived from `git rev-parse`, which reports the real
|
|
213
|
+
// path) — the same symlink-vs-original mismatch that broke the isMain
|
|
214
|
+
// check under `npm link` also applies to any target path reached that way.
|
|
215
|
+
try { return realpathSync(abs); } catch { return abs; }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function main() {
|
|
219
|
+
const rawTarget = process.argv[2]?.startsWith('--') ? process.cwd() : (process.argv[2] ?? process.cwd());
|
|
220
|
+
const targetPath = normalizePath(rawTarget);
|
|
221
|
+
const configArg = arg('--config');
|
|
222
|
+
const { config, configPath, configLoaded } = loadConfig(configArg ? normalizePath(configArg) : null);
|
|
223
|
+
const diffRef = arg('--diff');
|
|
224
|
+
const format = arg('--format', 'text');
|
|
225
|
+
|
|
226
|
+
if (!existsSync(targetPath)) throw new Error(`target path does not exist: ${targetPath}`);
|
|
227
|
+
const isFile = statSync(targetPath).isFile();
|
|
228
|
+
|
|
229
|
+
// A plain (non-diff) score does not need a git repo at all — only resolve
|
|
230
|
+
// repoRoot when it's actually needed (diff mode, or a boundary check that
|
|
231
|
+
// needs repo-root-relative paths).
|
|
232
|
+
const needsGit = Boolean(diffRef) || (config.boundaries ?? []).length > 0;
|
|
233
|
+
let repoRoot = isFile ? dirname(targetPath) : targetPath;
|
|
234
|
+
if (needsGit) {
|
|
235
|
+
const cwdForGit = isFile ? dirname(targetPath) : targetPath;
|
|
236
|
+
repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd: cwdForGit, encoding: 'utf8' }).trim();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let files;
|
|
240
|
+
if (diffRef) {
|
|
241
|
+
// `git diff --name-only <ref> -- <path>` (no `...HEAD`) compares `<ref>`
|
|
242
|
+
// directly against the WORKING TREE — committed history AND uncommitted
|
|
243
|
+
// changes both show up. Untracked new files show up in neither `git
|
|
244
|
+
// diff` form, so they're unioned in separately. Before this fix, only
|
|
245
|
+
// `<ref>...HEAD` (committed history alone) was scored — a dirty tree
|
|
246
|
+
// with real regressions scored zero files and exited 0.
|
|
247
|
+
const committed = execFileSync('git', ['diff', '--name-only', diffRef, '--', targetPath], { cwd: repoRoot, encoding: 'utf8' })
|
|
248
|
+
.split('\n').filter(Boolean);
|
|
249
|
+
const untracked = execFileSync('git', ['ls-files', '--others', '--exclude-standard', '--', targetPath], { cwd: repoRoot, encoding: 'utf8' })
|
|
250
|
+
.split('\n').filter(Boolean);
|
|
251
|
+
files = [...new Set([...committed, ...untracked])]
|
|
252
|
+
.map((f) => join(repoRoot, f))
|
|
253
|
+
.filter((f) => !isExcluded(f.split(sep).join('/'), config.exclude));
|
|
254
|
+
} else if (isFile) {
|
|
255
|
+
files = [targetPath];
|
|
256
|
+
} else {
|
|
257
|
+
files = walk(targetPath, config.exclude);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const results = [];
|
|
261
|
+
const regressions = [];
|
|
262
|
+
const unresolvedLanguages = [];
|
|
263
|
+
|
|
264
|
+
for (const f of files) {
|
|
265
|
+
const plugin = pluginFor(f);
|
|
266
|
+
if (!plugin) { unresolvedLanguages.push(f); continue; } // reachable now — files are no longer pre-filtered by pluginFor before reaching this loop
|
|
267
|
+
const current = plugin.extractUnits(f).map((u) => scoreUnit(u, config.weights));
|
|
268
|
+
|
|
269
|
+
if (diffRef) {
|
|
270
|
+
const before = scoreAtRef(repoRoot, f, diffRef, config.weights);
|
|
271
|
+
for (const unit of current) {
|
|
272
|
+
if (unit.total === null) continue; // fully unmeasured — nothing to compare
|
|
273
|
+
const prior = before?.find((b) => b.unit === unit.unit);
|
|
274
|
+
if (!prior || prior.total === null) {
|
|
275
|
+
if (unit.total < config.diff.newUnitMin) regressions.push({ file: f, unit: unit.unit, reason: 'new_unit_below_min', total: unit.total, min: config.diff.newUnitMin });
|
|
276
|
+
} else if (prior.total - unit.total > config.diff.maxDecrease) {
|
|
277
|
+
regressions.push({ file: f, unit: unit.unit, reason: 'regression', before: prior.total, after: unit.total });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
results.push({ file: relative(repoRoot, f).split('\\').join('/'), units: current });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const { findings: boundaryFindings, misconfigured: boundaryMisconfigured } = checkBoundaries(files, config.boundaries ?? [], repoRoot);
|
|
285
|
+
const boundaryViolations = boundaryFindings.filter((f) => !f.satisfied);
|
|
286
|
+
|
|
287
|
+
const output = {
|
|
288
|
+
results, regressions, boundaryFindings, boundaryViolations, boundaryMisconfigured, unresolvedLanguages,
|
|
289
|
+
config: { weights: config.weights, diff: config.diff, boundaryRuleCount: (config.boundaries ?? []).length },
|
|
290
|
+
configPath, configLoaded,
|
|
291
|
+
parser: parserName,
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
if (format === 'json') {
|
|
295
|
+
console.log(JSON.stringify(output, null, 2));
|
|
296
|
+
} else {
|
|
297
|
+
for (const r of results) {
|
|
298
|
+
for (const u of r.units) {
|
|
299
|
+
console.log(`${r.file} :: ${u.unit} (${u.kind}) total=${u.total ?? 'UNMEASURED'}`);
|
|
300
|
+
for (const [k, v] of Object.entries(u.criteria)) console.log(` ${k.toUpperCase()}: ${v.score.toString().padStart(3)} [${v.confidence}] ${v.detail}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (boundaryViolations.length) {
|
|
304
|
+
console.log('\nCLEAN ARCHITECTURE BOUNDARY VIOLATIONS:');
|
|
305
|
+
for (const v of boundaryViolations) console.log(` ${v.file}: missing required port import '${v.requiredPort}'`);
|
|
306
|
+
}
|
|
307
|
+
if (boundaryMisconfigured.length) {
|
|
308
|
+
console.log('\nBOUNDARY RULES MATCHING ZERO FILES (config error, not a pass):');
|
|
309
|
+
for (const m of boundaryMisconfigured) console.log(` orchestrator '${m.rule}' — ${m.reason}`);
|
|
310
|
+
}
|
|
311
|
+
if (regressions.length) {
|
|
312
|
+
console.log('\nREGRESSIONS:');
|
|
313
|
+
for (const r of regressions) console.log(` ${r.file} :: ${r.unit} — ${r.reason} ${JSON.stringify(r)}`);
|
|
314
|
+
}
|
|
315
|
+
if (unresolvedLanguages.length) {
|
|
316
|
+
console.log(`\n${unresolvedLanguages.length} file(s) matched no registered language plugin — skipped, not silently passed:`);
|
|
317
|
+
for (const f of unresolvedLanguages) console.log(` ${relative(repoRoot, f)}`);
|
|
318
|
+
}
|
|
319
|
+
if ((config.boundaries ?? []).length === 0) {
|
|
320
|
+
console.log(configLoaded ? '\nNo boundary rules configured — Clean Architecture check has nothing to verify.' : '\nNo --config given — running with defaults, zero boundary rules. Pass --config <repo>/.solid-score.yml to enable the Clean Architecture check.');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const failed = regressions.length > 0 || boundaryViolations.length > 0 || boundaryMisconfigured.length > 0;
|
|
325
|
+
process.exit(failed ? 1 : 0);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// `import.meta.url` resolves through symlinks to the real path; a plain
|
|
329
|
+
// `pathToFileURL(process.argv[1])` does not. Under `npm link` (a symlinked
|
|
330
|
+
// global bin — exactly how this tool got installed) the two disagreed and
|
|
331
|
+
// main() silently never ran, on every invocation, with exit 0 and no output.
|
|
332
|
+
// realpathSync on both sides closes that gap.
|
|
333
|
+
function realFileURL(p) {
|
|
334
|
+
try { return pathToFileURL(realpathSync(p)).href; } catch { return pathToFileURL(p).href; }
|
|
335
|
+
}
|
|
336
|
+
const isMain = process.argv[1] && import.meta.url === realFileURL(process.argv[1]);
|
|
337
|
+
if (isMain) main().catch((err) => { console.error(err); process.exit(2); });
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: architecture-reviewer
|
|
3
|
+
description: >-
|
|
4
|
+
Usage `rdc:architecture-reviewer <path> [--config <file>] [--diff <ref>]` —
|
|
5
|
+
Clean Architecture boundary/dependency-direction/layer-separation review.
|
|
6
|
+
Runs TWO mechanical scorers first — `rdc-architecture-score` (layer
|
|
7
|
+
classification, inward-dependency rule, framework coupling, missing
|
|
8
|
+
repository/port abstractions, HTTP/DB leaks into Use Cases, circular
|
|
9
|
+
layer dependencies) and `rdc-solid-score`'s orchestrator/port boundary
|
|
10
|
+
check — then dispatches judgment-level review for the ONE thing neither
|
|
11
|
+
can see: whether an abstraction boundary makes architectural SENSE for
|
|
12
|
+
this domain, not just whether it has the right shape. Call from
|
|
13
|
+
rdc:review step 8b+, or standalone before merging a new package/module.
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
> **⚠️ OUTPUT CONTRACT (READ FIRST):** `guides/output-contract.md`
|
|
17
|
+
> Checklist-only output. No tool-call narration. No raw MCP/JSON/log dumps.
|
|
18
|
+
> One checklist upfront, updated in place, shown again at end with a 1-line verdict.
|
|
19
|
+
|
|
20
|
+
# architecture-reviewer — Layering & Dependency-Direction Review
|
|
21
|
+
|
|
22
|
+
## Why this is now mostly mechanical, not judgment-dispatch
|
|
23
|
+
|
|
24
|
+
This skill used to run ONE mechanical check (`rdc-solid-score`'s named
|
|
25
|
+
orchestrator/port boundary rule) and push everything else — framework
|
|
26
|
+
imports leaking into inner layers, DB/HTTP access inside Use Cases, missing
|
|
27
|
+
repository interfaces, layer-mixing, circular layer dependencies — into a
|
|
28
|
+
dispatched judgment pass, on the theory that "mechanical facts don't exist
|
|
29
|
+
yet for this domain."
|
|
30
|
+
|
|
31
|
+
That premise was checked against the actual source it was adapted from and
|
|
32
|
+
found wrong. `architecture-toolkit`'s own
|
|
33
|
+
[`.claude/skills/review-arch.md`](https://github.com/OnSightTeam/architecture-toolkit)
|
|
34
|
+
does not dispatch judgment for any of this — it shells out to
|
|
35
|
+
`node dist/cli.js --agents=architecture <paths>`, a fully deterministic CLI.
|
|
36
|
+
There is no LLM judgment step in the source project for Clean Architecture
|
|
37
|
+
boundary/dependency/layer detection at all: it is glob-based layer
|
|
38
|
+
classification plus regex import/text matching, because that is what the
|
|
39
|
+
Dependency Rule actually needs — WHICH FILE, WHICH FOLDER, WHICH IMPORT
|
|
40
|
+
TARGET — not an AST, and not an opinion.
|
|
41
|
+
|
|
42
|
+
`scripts/lib/architecture-scoring.mjs` ports that detection logic (see its
|
|
43
|
+
header for full provenance — three files fetched and read in full from
|
|
44
|
+
`github.com/OnSightTeam/architecture-toolkit`, MIT). It runs as
|
|
45
|
+
`rdc-architecture-score` alongside the pre-existing `rdc-solid-score`
|
|
46
|
+
boundary check, which is a DIFFERENT, complementary mechanism (an
|
|
47
|
+
explicitly-declared `{orchestrator, requiredPorts}` rule per repo, not
|
|
48
|
+
layer classification) and stays exactly as it was.
|
|
49
|
+
|
|
50
|
+
What is genuinely LEFT as judgment — the one thing neither scorer can do —
|
|
51
|
+
is deciding whether an abstraction boundary makes architectural SENSE for
|
|
52
|
+
this specific domain: a `Harness`-shaped class that reimplements logic its
|
|
53
|
+
own sibling packages already own, a `shared`/`utils` module quietly
|
|
54
|
+
accumulating business logic, a port whose shape leaks an implementation
|
|
55
|
+
detail, a package importing more siblings than its stated job justifies.
|
|
56
|
+
None of those are a shape a glob/regex scanner can match — they require
|
|
57
|
+
reading INTENT against the code that implements it. That is Step 2 below,
|
|
58
|
+
and it is now the ONLY judgment step.
|
|
59
|
+
|
|
60
|
+
## When to Use
|
|
61
|
+
|
|
62
|
+
- Any PR that adds or restructures a package boundary
|
|
63
|
+
- Before merging a new orchestrator/use-case class
|
|
64
|
+
- Called from `rdc:review` step 8b+ alongside the SOLID score gate
|
|
65
|
+
- When a reviewer suspects "this could have just called the existing thing"
|
|
66
|
+
|
|
67
|
+
## Arguments
|
|
68
|
+
|
|
69
|
+
- `rdc:architecture-reviewer <path>` — full review of the target
|
|
70
|
+
- `rdc:architecture-reviewer <path> --config <file>` — non-default layer
|
|
71
|
+
classification (see `lib/architecture-scoring.mjs`'s `DEFAULT_LAYERS` for
|
|
72
|
+
the shape; a repo that doesn't name its folders `entities`/`use-cases`/
|
|
73
|
+
`adapters`/`frameworks` needs its own `.architecture-score.yml`)
|
|
74
|
+
- `rdc:architecture-reviewer <path> --diff <ref>` — new/changed code only
|
|
75
|
+
(scope the file list to the diff before invoking either scorer)
|
|
76
|
+
|
|
77
|
+
## Mechanical Rule Catalog — `rdc-architecture-score`
|
|
78
|
+
|
|
79
|
+
Every row cites the exact `architecture-toolkit` file:line it is ported or
|
|
80
|
+
adapted from (full citations live in `lib/architecture-scoring.mjs`'s
|
|
81
|
+
per-rule comments — read them before trusting a finding's severity).
|
|
82
|
+
|
|
83
|
+
| Rule ID | What it detects | Severity | Confidence basis |
|
|
84
|
+
|---|---|---|---|
|
|
85
|
+
| `dependency-direction` | An inner-layer file imports a more-outer layer (Entities importing UseCases, UseCases importing Frameworks, etc.) | Critical | `high` when the import RESOLVES to a real scanned file with a known layer; `low` when only the import specifier text is keyword-matched (the toolkit's own, weaker, method) |
|
|
86
|
+
| `framework-coupling` | A non-Frameworks-layer file imports a known framework/library (express, react, mongoose, prisma, aws-sdk, pg, …) | Critical (Entities) / High (everything else non-Frameworks) | `high`, except `medium` for the three generic-word indicators (`http`, `fetch`, `request`) the toolkit's own list carries, which real false-positive on an unrelated local variable or Node's builtin `node:http` |
|
|
87
|
+
| `missing-abstraction` (5 subtypes) | `missing-repository-interface` (UseCase `new`s a concrete Repository/Gateway/DataSource) · `missing-input-port` (Controller `new`s a concrete UseCase, no Port interface declared) · `http-request-in-usecase` (`request.`/`req.` referenced in a UseCase file) · `direct-db-access-in-usecase` (`db.`/raw SQL in a UseCase/Interactor/Service) · `data-structure-leak` (a Controller returns/types a value as an Entity) | High/Critical per subtype | `high` for the first three (unambiguous regex shape); `medium`/`low` for db-access and data-structure-leak (broader textual patterns, real false-positive risk — disclosed per-subtype) |
|
|
88
|
+
| `mixed-concerns` (bonus, beyond the task's 4 required categories) | Business-rule vocabulary and infrastructure vocabulary both appear in one file | High | `low` — whole-file word-list co-occurrence, same heuristic class as `clean-code-analyzer`'s N2 |
|
|
89
|
+
| `ui-business-logic-mixing` (bonus) | A 100+ char `calculate`/`validate`/`process` function body alongside UI indicators | Medium | `medium` |
|
|
90
|
+
| `mixed-layer-imports` (bonus) | A file imports from 3+ distinct layers | Medium | `medium` |
|
|
91
|
+
| `circular-layer-dependency` | A REAL cycle in the layer-import graph, built from RESOLVED file imports and walked with the SAME ADP algorithm `package-metrics.mjs` already uses for package-level cycles (`findCycles`, reused not reimplemented) — this is STRONGER than `architecture-toolkit`'s own circular check, which is a `"../../.."`-depth proxy that is neither necessary nor sufficient for an actual cycle (see `lib/architecture-scoring.mjs`'s header for why that half was deliberately NOT ported) | High | `high` — built only from resolved edges, not keyword guesses |
|
|
92
|
+
|
|
93
|
+
**Layer classification is config-driven, not hardcoded.** `DEFAULT_LAYERS`
|
|
94
|
+
covers the canonical four rings (`entities`/`domain`, `use-cases`/
|
|
95
|
+
`usecases`/`application`, `adapters`/`controllers`/`presenters`/`gateways`,
|
|
96
|
+
`frameworks`/`infrastructure`) by path glob, with a class/interface-name
|
|
97
|
+
regex fallback (ported from the toolkit's own `detectLayer`). A file
|
|
98
|
+
matching NEITHER is reported `layer: null` / unclassified — it is never
|
|
99
|
+
silently defaulted to any specific layer (the toolkit's own `detectLayer`
|
|
100
|
+
defaults an unmatched file to `Frameworks`; this port deliberately does
|
|
101
|
+
not, because "we don't know" and "this is definitely the outermost ring"
|
|
102
|
+
are different claims). A repo with different folder names supplies its own
|
|
103
|
+
`layers:` in `--config <file>`.
|
|
104
|
+
|
|
105
|
+
## Procedure
|
|
106
|
+
|
|
107
|
+
1. **Run both mechanical scorers first — cheap, exhaustive, never skip:**
|
|
108
|
+
```bash
|
|
109
|
+
rdc-architecture-score <path> [--config <file>] --format json
|
|
110
|
+
rdc-solid-score <path> --diff <ref> --config <repo>/.solid-score.yml --format json
|
|
111
|
+
```
|
|
112
|
+
Read `results[].rules` from `rdc-architecture-score` for every finding
|
|
113
|
+
above; read `boundaryFindings` from `rdc-solid-score` for the
|
|
114
|
+
orchestrator/port rule — a DIFFERENT, complementary check (an
|
|
115
|
+
explicitly-declared `{orchestrator, requiredPorts}` rule per repo, not
|
|
116
|
+
layer classification). Report `unclassifiedFiles` honestly — a file
|
|
117
|
+
with no glob/name-hint match got NO layer-aware findings run against it,
|
|
118
|
+
which is not the same as "clean."
|
|
119
|
+
|
|
120
|
+
If the target repo has no `.solid-score.yml` `boundaries` section, or no
|
|
121
|
+
`.architecture-score.yml` `layers` override where the default four rings
|
|
122
|
+
don't apply, say so plainly — a review that silently skipped a
|
|
123
|
+
configurable check is incomplete, not clean.
|
|
124
|
+
|
|
125
|
+
2. **Dispatch the ONE remaining judgment step — architectural SENSE, not
|
|
126
|
+
shape:**
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
Agent({
|
|
130
|
+
subagent_type: "pr-review-toolkit:code-reviewer",
|
|
131
|
+
description: "architecture-reviewer judgment pass",
|
|
132
|
+
prompt: "Review `git diff <ref>...HEAD` (or the full target if no
|
|
133
|
+
--diff) for architectural boundary choices that are
|
|
134
|
+
technically LEGAL — no mechanical rule catches them — but
|
|
135
|
+
conceptually WRONG for this domain. The mechanical scorers
|
|
136
|
+
(rdc-architecture-score, rdc-solid-score) already caught
|
|
137
|
+
layer-direction violations, framework coupling, missing
|
|
138
|
+
repository/port abstractions, HTTP/DB leaks into Use Cases,
|
|
139
|
+
layer-mixing, and circular layer dependencies — do NOT
|
|
140
|
+
re-litigate those. Check ONLY these four, which require
|
|
141
|
+
reading INTENT against the implementation, not matching a
|
|
142
|
+
shape:
|
|
143
|
+
(1) a module that reimplements logic a sibling package
|
|
144
|
+
already owns instead of importing and delegating to it — the
|
|
145
|
+
`Harness`-shaped failure: a class with almost no external
|
|
146
|
+
dependencies of ANY kind can score fine on a generic
|
|
147
|
+
DIP/coupling metric while still being exactly this;
|
|
148
|
+
(2) a 'shared'/'common'/'utils' module accumulating actual
|
|
149
|
+
business logic rather than genuinely-shared primitives;
|
|
150
|
+
(3) a port/interface whose own shape leaks a concrete
|
|
151
|
+
implementation detail (a parameter that only makes sense for
|
|
152
|
+
one adapter);
|
|
153
|
+
(4) a new package importing more siblings than its stated
|
|
154
|
+
responsibility justifies.
|
|
155
|
+
Report each finding with severity (critical/high/medium/low),
|
|
156
|
+
file:line, the concrete violation, and what delegation/
|
|
157
|
+
boundary should exist instead. Return
|
|
158
|
+
ARCHITECTURE_REVIEW_COMPLETE with:
|
|
159
|
+
{ critical_count, high_count, medium_count, low_count,
|
|
160
|
+
findings: [{severity, file:line, issue, suggested_boundary}] }."
|
|
161
|
+
})
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
3. **Merge all three into one report.** The two mechanical scorers'
|
|
165
|
+
findings are certain (a named rule failed, or a real graph cycle
|
|
166
|
+
exists); the judgment findings carry the subagent's confidence — do
|
|
167
|
+
not present them with equal certainty. Within the mechanical findings
|
|
168
|
+
themselves, honor each finding's own `confidence` field (see the
|
|
169
|
+
catalog table) — a `low`-confidence mechanical finding is real evidence
|
|
170
|
+
worth a look, not a certainty either.
|
|
171
|
+
|
|
172
|
+
4. **Severity gate**, same shape as `rdc:review` step 8b:
|
|
173
|
+
- Any `critical`/`high` mechanical finding, `rdc-solid-score` boundary
|
|
174
|
+
violation, or `critical`/`high` judgment finding → verdict cannot be
|
|
175
|
+
CLEAN.
|
|
176
|
+
- `medium`/`low` findings from either source → recorded, verdict can
|
|
177
|
+
still be CLEAN.
|
|
178
|
+
- Zero findings across all three → `ARCHITECTURE_REVIEW: CLEAN`.
|
|
179
|
+
|
|
180
|
+
5. **Report:**
|
|
181
|
+
```
|
|
182
|
+
## Architecture Review
|
|
183
|
+
### Mechanical — rdc-architecture-score (layer/dependency/boundary)
|
|
184
|
+
| Rule | File | Severity | Confidence | Detail |
|
|
185
|
+
### Mechanical — rdc-solid-score (orchestrator/port boundary)
|
|
186
|
+
| File | Required port | Satisfied |
|
|
187
|
+
### Unclassified files (no layer-aware rule ran)
|
|
188
|
+
### Judgment findings — architectural sense
|
|
189
|
+
| Severity | File:Line | Issue | Suggested boundary |
|
|
190
|
+
## Verdict: CLEAN / HAS ISSUES
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Rules
|
|
194
|
+
|
|
195
|
+
- Never skip step 1 to save time — both scorers are cheap and certain, and
|
|
196
|
+
each has independently dogfooded a real finding (see below).
|
|
197
|
+
- The judgment pass is advisory-plus-severity, not a rubber stamp on the
|
|
198
|
+
mechanical passes; a clean mechanical result with high-severity judgment
|
|
199
|
+
findings is still HAS ISSUES.
|
|
200
|
+
- Do not report a mechanical finding without its `confidence` field — a
|
|
201
|
+
`low`-confidence heuristic hit (e.g. `data-structure-leak`'s broad
|
|
202
|
+
`Entity`-substring match) is worth a look, never a certainty.
|
|
203
|
+
- If neither the target repo nor `--diff` is given, refuse rather than
|
|
204
|
+
guess scope — an architecture review with an unstated scope is
|
|
205
|
+
unfalsifiable.
|
|
206
|
+
|
|
207
|
+
## Dogfood evidence — real, not hypothetical
|
|
208
|
+
|
|
209
|
+
Two DIFFERENT real targets confirm the mechanism works and is honest about
|
|
210
|
+
its own limits:
|
|
211
|
+
|
|
212
|
+
**A constructed positive-control fixture** (per
|
|
213
|
+
`.claude/rules/prove-absence-positive-control.md`) — a 4-file Clean
|
|
214
|
+
Architecture layout with a KNOWN violation of each required category —
|
|
215
|
+
correctly produced ALL of: 2 `dependency-direction` criticals (Entities and
|
|
216
|
+
UseCases each importing the Frameworks-layer file), 1 `framework-coupling`
|
|
217
|
+
critical (`express` imported into Entities), 5 `missing-abstraction`
|
|
218
|
+
findings across all 5 subtypes, and a real `circular-layer-dependency`
|
|
219
|
+
finding (`Frameworks -> UseCases -> Frameworks`, from two files that
|
|
220
|
+
genuinely import each other). This run also caught a real bug in the
|
|
221
|
+
scorer's own glob matcher — `**/frameworks/**` failed to match a TOP-LEVEL
|
|
222
|
+
`frameworks/db-client.mjs` path (no parent directory to supply the literal
|
|
223
|
+
slash the naive translation required) — fixed before ship, see
|
|
224
|
+
`lib/architecture-scoring.mjs`'s `globToRegExp` header for the full story.
|
|
225
|
+
|
|
226
|
+
**Two real fleets that do NOT use Clean-Architecture folder/class-naming
|
|
227
|
+
conventions** (`rdc-harness/packages`, 85 files; this repo's own `scripts/`,
|
|
228
|
+
36 files) correctly report ZERO `dependency-direction`/`framework-coupling`/
|
|
229
|
+
`missing-abstraction` findings — confirmed as a GENUINE absence, not a
|
|
230
|
+
broken scanner, by an independent `grep -rE 'class\s+\w+(UseCase|Interactor|
|
|
231
|
+
Controller)'` across both trees (zero hits, matching the scorer's own
|
|
232
|
+
report). Both correctly report every scanned file as `unclassified` — real
|
|
233
|
+
honesty, not a silent false-positive from defaulting to a layer. The
|
|
234
|
+
layer-independent `mixed-concerns` heuristic still fires (34/54 hits
|
|
235
|
+
respectively) at its disclosed `low` confidence, the same character as
|
|
236
|
+
`clean-code-analyzer`'s N2 — worth a look, not a certainty.
|
|
237
|
+
|
|
238
|
+
## Worked Example — `rdc-harness`'s `Harness` class, THREE sources now
|
|
239
|
+
|
|
240
|
+
Real target, read in full: [`C:/Dev/rdc-harness/packages/core/src/index.mjs`](file:///C:/Dev/rdc-harness/packages/core/src/index.mjs)
|
|
241
|
+
(398 lines, one exported class). This is now a THREE-source report, and it
|
|
242
|
+
is a genuinely instructive case: `Harness`'s failure is NOT a Clean
|
|
243
|
+
Architecture layer violation (the file uses no `entities`/`use-cases`/
|
|
244
|
+
`adapters`/`frameworks` folder or class-naming convention at all — that's
|
|
245
|
+
a different codebase style, package-oriented rather than layer-oriented),
|
|
246
|
+
so `rdc-architecture-score` correctly abstains rather than fabricating a
|
|
247
|
+
layer-based finding:
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
## Architecture Review — rdc-harness/packages/core/src/index.mjs
|
|
251
|
+
|
|
252
|
+
### Mechanical — rdc-architecture-score
|
|
253
|
+
UNCLASSIFIED (no layer glob or name-hint matched `index.mjs`) — zero
|
|
254
|
+
layer-aware findings ran. This is an honest abstention, not a clean result:
|
|
255
|
+
`Harness`'s failure mode is package-level (see below), not layer-level.
|
|
256
|
+
|
|
257
|
+
### Mechanical — rdc-solid-score (orchestrator/port boundary)
|
|
258
|
+
| File | Required port | Satisfied |
|
|
259
|
+
| index.mjs (Harness) | 6/6 ports (transaction, delivery, deploy, orchestration,
|
|
260
|
+
boundary, work — one per sibling package) | false — 0/6 satisfied |
|
|
261
|
+
|
|
262
|
+
### Judgment findings — architectural sense
|
|
263
|
+
| Severity | File:Line | Issue | Suggested boundary |
|
|
264
|
+
| high (judgment #1) | index.mjs:49-394 (whole class) | `Harness` reimplements
|
|
265
|
+
transaction handling (`#block`, `transaction.blocked` events), delivery
|
|
266
|
+
(`shipDev`), deploy (`deploy`, `requestProduction`, `recordDecision`), and
|
|
267
|
+
orchestration (`createRun`, `createTarget`) all in one class — the exact
|
|
268
|
+
`Harness`-shaped failure: 21 sibling packages exist (`packages/transaction`,
|
|
269
|
+
`packages/delivery`, `packages/deploy`, `packages/orchestration`,
|
|
270
|
+
`packages/boundary`, …) and `Harness` currently imports from none of them;
|
|
271
|
+
it reimplements their responsibilities inline instead of delegating. It
|
|
272
|
+
would still pass a generic SOLID/coupling metric because its only external
|
|
273
|
+
imports are `node:crypto` and `node:fs` — a narrow dependency *count*
|
|
274
|
+
hiding a wide responsibility *surface*. | Split into a thin `Harness`
|
|
275
|
+
facade that composes injected ports: `TransactionPort`, `DeliveryPort`,
|
|
276
|
+
`DeployPort`, `OrchestrationPort`, each backed by its already-existing
|
|
277
|
+
sibling package. |
|
|
278
|
+
|
|
279
|
+
## Verdict: HAS ISSUES (0/6 ports satisfied, 1 high judgment finding)
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
This is a real result, not a hypothetical: `Harness` genuinely has zero
|
|
283
|
+
imports from any of its 21 sibling packages (confirmed via
|
|
284
|
+
`grep -r "from '.*packages/(transaction|delivery|deploy|orchestration|boundary)"`
|
|
285
|
+
against `index.mjs` — zero hits), `rdc-solid-score` genuinely reports 0/6
|
|
286
|
+
ports satisfied, and `rdc-architecture-score` genuinely reports the file
|
|
287
|
+
unclassified — three independently-verified facts, not one narrative.
|