@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,534 @@
|
|
|
1
|
+
# Validator — Architecture
|
|
2
|
+
|
|
3
|
+
## Third-party attribution
|
|
4
|
+
|
|
5
|
+
| Package | License (verified) | What we took from it |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| [OnSightTeam/architecture-toolkit](https://github.com/OnSightTeam/architecture-toolkit) | MIT (verified via its `package.json` `license` field + repo README `## License` section — the repo's root `LICENSE` file itself was not independently fetched this session) | Real detection logic, thresholds, and rule IDs for Steps 1–7 below, ported/adapted from its actual TypeScript source under `src/agents/*/tools/*.ts` (not reimplemented from a description) |
|
|
8
|
+
| [ts-morph](https://github.com/dsherret/ts-morph) `24.0.0` | MIT (verified: `node_modules/ts-morph/package.json`) | The AST layer for the TypeScript/JavaScript `LanguagePlugin` (`scripts/lib/plugins/typescript.mjs`) — still the backend for Clean Code, Patterns, and Refactoring this pass; see "AST parser — CLOSED" below for the SOLID swap to tree-sitter |
|
|
9
|
+
| [web-tree-sitter](https://www.npmjs.com/package/web-tree-sitter) `0.24.7` | MIT (verified: `node_modules/web-tree-sitter/package.json`) | The tree-sitter WASM runtime bindings for `scripts/lib/plugins/treesitter.mjs` — SOLID's default AST layer as of this pass |
|
|
10
|
+
| [tree-sitter-wasms](https://www.npmjs.com/package/tree-sitter-wasms) `0.1.13` | **Unlicense** (verified: `node_modules/tree-sitter-wasms/package.json` — public domain, NOT MIT) | Pre-built WASM grammar assets (TypeScript, TSX, JavaScript) consumed by `treesitter.mjs` |
|
|
11
|
+
| [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk) `1.30.0` | MIT (verified: `node_modules/@modelcontextprotocol/sdk/package.json`) | MCP server transport, unrelated to the scoring logic itself |
|
|
12
|
+
| [zod](https://github.com/colinhacks/zod) `3.25.76` | MIT (verified) | Input validation elsewhere in this repo's skill tooling |
|
|
13
|
+
| [express](https://github.com/expressjs/express) `5.2.1` | MIT (verified) | HTTP transport for `rdc-skills-mcp`, unrelated to the scoring logic |
|
|
14
|
+
| [yaml](https://github.com/eemeli/yaml) `2.9.0` | **ISC** (verified — NOT MIT, corrected here) | YAML parsing elsewhere in this repo's skill tooling |
|
|
15
|
+
|
|
16
|
+
**Explicitly evaluated but NOT used** (their algorithms/prior-art informed design decisions below; no code or dependency taken):
|
|
17
|
+
[jscpd](https://github.com/kucherenko/jscpd), PMD [CPD](https://pmd.github.io/pmd/pmd_userdocs_cpd.html), [ArchUnitTS](https://github.com/LukasNiessen/ArchUnitTS), [dependency-cruiser](https://github.com/sverweij/dependency-cruiser).
|
|
18
|
+
|
|
19
|
+
## What it is
|
|
20
|
+
|
|
21
|
+
A deterministic code-analysis suite covering SOLID, Clean Code, Clean
|
|
22
|
+
Architecture, package design, testing strategy, design patterns,
|
|
23
|
+
refactoring opportunities, and duplicate-code detection. Eight CLIs, zero
|
|
24
|
+
LLM calls inside any of them. Seven are ported from
|
|
25
|
+
[OnSightTeam/architecture-toolkit](https://github.com/OnSightTeam/architecture-toolkit)
|
|
26
|
+
(MIT) — their own project structure is 7 top-level agent folders under
|
|
27
|
+
`src/agents/`, and their own `.claude/skills/*.md` are thin wrappers around
|
|
28
|
+
`node dist/cli.js --agents=<name>`, no LLM in their loop either. This suite
|
|
29
|
+
matches that shape: one folder of pure functions per domain, one CLI per
|
|
30
|
+
domain, real detection logic ported from their real TypeScript source (not
|
|
31
|
+
reimplemented from a description of it), each independently dogfooded
|
|
32
|
+
against real code before being called done.
|
|
33
|
+
|
|
34
|
+
Four judgment calls remain genuinely un-mechanizable and stay routed to an
|
|
35
|
+
LLM reviewer (`pr-review-toolkit:code-reviewer`, or a Codex peer via
|
|
36
|
+
`rdc:co-develop`) — listed at the bottom of this document, not hidden inside
|
|
37
|
+
the tool tables.
|
|
38
|
+
|
|
39
|
+
## Shared contract
|
|
40
|
+
|
|
41
|
+
Four tools (SOLID, Clean Code, Patterns, Refactoring) read from one shape:
|
|
42
|
+
`NormalizedUnit` / `NormalizedMember`, defined in
|
|
43
|
+
[`scripts/lib/language-plugin.mjs`](scripts/lib/language-plugin.mjs). Two
|
|
44
|
+
plugins produce that shape for TypeScript/JavaScript today:
|
|
45
|
+
[`scripts/lib/plugins/typescript.mjs`](scripts/lib/plugins/typescript.mjs)
|
|
46
|
+
(ts-morph — the ONE file in this repo allowed to import it) and
|
|
47
|
+
[`scripts/lib/plugins/treesitter.mjs`](scripts/lib/plugins/treesitter.mjs)
|
|
48
|
+
(the fleet's own tree-sitter parser, ported from CodeFlow's
|
|
49
|
+
`nativeParser.ts` — see "AST parser — CLOSED" below). SOLID defaults to the
|
|
50
|
+
tree-sitter plugin (`--parser` selects either); Clean Code, Patterns, and
|
|
51
|
+
Refactoring still default to ts-morph this pass. Every scoring file
|
|
52
|
+
downstream of either plugin is pure functions over the shared shape; none of
|
|
53
|
+
them touch an AST directly. This is what "language independent" means in
|
|
54
|
+
practice: adding a Python or Go plugin that emits the same `NormalizedUnit`
|
|
55
|
+
shape makes all four of those tools work on that language with zero changes
|
|
56
|
+
to the scoring logic itself.
|
|
57
|
+
|
|
58
|
+
Three tools (Package Design, Architecture, Duplicate Code) are deliberately
|
|
59
|
+
NOT built on this contract — they operate on file paths, import statements,
|
|
60
|
+
or raw token streams, which a plain text/regex scan answers as correctly as
|
|
61
|
+
an AST would, so they carry no `ts-morph` dependency at all and work on any
|
|
62
|
+
language whose imports look like ES/CJS syntax (Package Design, Architecture)
|
|
63
|
+
or whose source can be tokenized at all (Duplicate Code — see Step 8).
|
|
64
|
+
|
|
65
|
+
All 8 CLIs support `--format json`, are proven deterministic (identical
|
|
66
|
+
output across two runs on the same unchanged input — no timestamps, no
|
|
67
|
+
absolute paths, sorted finding arrays), and are registered as global bins in
|
|
68
|
+
`package.json`.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Step 1 — SOLID
|
|
73
|
+
|
|
74
|
+
| | |
|
|
75
|
+
|---|---|
|
|
76
|
+
| **Call** | `rdc-solid-score <path> [--diff <ref>] [--config <file>]` |
|
|
77
|
+
| **Input** | file or directory path; optional git ref to score a diff; optional boundary-check config |
|
|
78
|
+
| **Script** | [`scripts/lib/solid-scoring.mjs`](scripts/lib/solid-scoring.mjs), CLI in [`scripts/solid-score.mjs`](scripts/solid-score.mjs) |
|
|
79
|
+
| **Algorithm** | SRP: union-find over shared `this.field`/method-to-method access, connected-component count. OCP: switch/instanceof/type-check density per member. LSP: override signature, super-call presence, return-type drift vs. base class. ISP: public-member count + average param count. DIP: ratio of concrete `new X()` instantiation vs. injected/abstract dependency. Plus a separate Clean Architecture boundary check (`{orchestrator, requiredPorts}` config-driven). |
|
|
80
|
+
| **Output** | JSON: per-unit `{srp, ocp, lsp, isp, dip}` scores 0–100 each with a confidence label (`high`/`medium`/`low`/`none`), a weighted `total` renormalized over only the measured criteria (weights: srp .20 / ocp .15 / lsp .15 / isp .20 / dip .30), plus boundary violations if configured. |
|
|
81
|
+
| **Source** | Original build, not ported — this repo's own design, informed by standard SOLID literature (Martin, *Agile Principles, Patterns, and Practices*). `confidence: 'none'` exists because a class with real constructor-injected deps and arrow methods was scoring a false 100 before the AST-completeness fix (2026-08-20) — see the file header. |
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Step 2 — Clean Code
|
|
86
|
+
|
|
87
|
+
| | |
|
|
88
|
+
|---|---|
|
|
89
|
+
| **Call** | `rdc-clean-code-score <path> [--project-root <dir>] [--no-dead-exports]` |
|
|
90
|
+
| **Input** | file or directory path; optional project root for cross-file resolution |
|
|
91
|
+
| **Script** | [`scripts/lib/clean-code-scoring.mjs`](scripts/lib/clean-code-scoring.mjs), CLI in [`scripts/clean-code-score.mjs`](scripts/clean-code-score.mjs) |
|
|
92
|
+
| **Algorithm** | N1/N2/N4/N7: per-declared-name checks (cryptic, noise-word, magic-number-outside-const, generic-suffix) walking the real AST, not a whole-file regex. F1/F2: statement count (>20) / param count (>3) per member. E1: empty catch-block scan. G9a: unreachable constant-conditional branch. G9b: cross-file unused-export scan via `findReferencesAsNodes()`, gated behind a positive-control check. |
|
|
93
|
+
| **Output** | JSON: findings list, each `{rule, file, line, message, confidence}`; G9 findings additionally carry `deadExportsScope.positiveControlOk` — G9b's export-usage findings are withheld entirely if the control fails. |
|
|
94
|
+
| **Source** | `src/agents/clean-code-analyzer/tools/{naming,function,code-smell}-validator.ts`, github.com/OnSightTeam/architecture-toolkit (MIT). Their checks are whole-file text regexes with an occurrence-count threshold to suppress false positives; this port reads the real AST per declared binding instead, so every occurrence is its own finding with no threshold needed. **Not ported**: N3, N5, N6, C1–C5, G14, G16, G28 — named with reasons in `skills/clean-code-analyzer/SKILL.md`. G5 (Duplicate Code) is real but lives in Step 8 below, not here — it's a cross-file, whole-corpus check, the wrong shape for this file's per-unit contract. |
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Step 3 — Package Design
|
|
99
|
+
|
|
100
|
+
| | |
|
|
101
|
+
|---|---|
|
|
102
|
+
| **Call** | `rdc-package-metrics <packagesRoot>` or `--dirs <d1,d2,...>` |
|
|
103
|
+
| **Input** | directory containing `packages/*`, or an explicit list of package dirs |
|
|
104
|
+
| **Script** | [`scripts/lib/package-metrics.mjs`](scripts/lib/package-metrics.mjs), CLI in [`scripts/package-metrics-cli.mjs`](scripts/package-metrics-cli.mjs) |
|
|
105
|
+
| **Algorithm** | Robert C. Martin's package-coupling metrics. Resolves every `import`/`export...from`/`require()`/dynamic `import()` across sibling packages via plain `node:fs` + regex (no AST — deliberately independent of the ts-morph plugin). Ca = distinct importing packages, Ce = distinct imported packages, Instability I = Ce/(Ca+Ce), Abstractness A = exported interface/type ÷ exported total (`.ts` files only), Distance D = \|A+I-1\|, real graph cycle-walk for ADP violations. |
|
|
106
|
+
| **Output** | JSON: per-package `{ca, ce, instability, abstractness, distanceFromMainSequence, zone}` (`zone` ∈ main-sequence / zone-of-pain / zone-of-uselessness / off-main-sequence / unmeasurable), plus named cycle paths (`a → b → c → a`), not just "a cycle exists." |
|
|
107
|
+
| **Source** | Formulas checked against `src/agents/package-design/tools/{stability-metrics-calculator,package-coupling-analyzer}.ts`, github.com/OnSightTeam/architecture-toolkit (MIT) — confirmed independently rather than copied (both short enough to derive from Martin's own published formulas and cross-check). The cycle-detection graph walk is this repo's own implementation, later reused by Step 5. |
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Step 4 — Testing Strategy
|
|
112
|
+
|
|
113
|
+
| | |
|
|
114
|
+
|---|---|
|
|
115
|
+
| **Call** | `rdc-test-smell-score <test-path> [--repo-root <dir>]` |
|
|
116
|
+
| **Input** | test file or directory (`.test.mjs`/`.test.ts`/`*.spec.*`) |
|
|
117
|
+
| **Script** | [`scripts/lib/test-smell-scoring.mjs`](scripts/lib/test-smell-scoring.mjs) (also its own CLI — has a runnable main-guard block) |
|
|
118
|
+
| **Algorithm** | T1: test-block count vs. exported-unit count of the paired source file (via the same `NormalizedUnit` contract, not a regex). T2: `.skip`/`xit`/`xdescribe` scan. T5/T6: assertions-per-block (>10) / lines-per-block (>30), via a brace-balanced block extractor (not a non-greedy regex, which breaks on nested `{}`). T7/T8: literal timer/`Date.now`/`Math.random`/`process.env` calls inside a test body. T9: Jaccard similarity (≥0.75) over normalized 3-token shingles of `beforeEach`/`beforeAll` bodies, across files. FIRST-Independent: a `let` declared outside test() and mutated in ≥2 separate test() blocks. |
|
|
119
|
+
| **Output** | JSON: findings list per rule with file:line and description; T9 findings span multiple files by design. |
|
|
120
|
+
| **Source** | `src/agents/testing-strategy/tools/test-quality-validator.ts`, github.com/OnSightTeam/architecture-toolkit (MIT), lines 47–240 (T1:47-65, T2:67-85, T5:127-145, T6:147-171, T7:173-190, T8:192-219, T9:221-240). The brace-balanced extractor replacing their non-greedy regex is this repo's own fix. **Not ported**: T3 (Test Per Class), T4 (Untested Method), and Fast/Repeatable/SelfValidating/Timely (FIRST's other 4 letters) — named with reasons in `skills/testing-strategy/SKILL.md`. |
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Step 5 — Architecture (Clean Architecture boundaries)
|
|
125
|
+
|
|
126
|
+
| | |
|
|
127
|
+
|---|---|
|
|
128
|
+
| **Call** | `rdc-architecture-score <path> [--config <file>]` |
|
|
129
|
+
| **Input** | directory path; optional layer-classification config (glob → layer name mapping) |
|
|
130
|
+
| **Script** | [`scripts/lib/architecture-scoring.mjs`](scripts/lib/architecture-scoring.mjs), CLI in [`scripts/architecture-score.mjs`](scripts/architecture-score.mjs) |
|
|
131
|
+
| **Algorithm** | Self-contained, no AST — file-path + import-target analysis, same shape as Step 3. Classifies each file into a Clean Architecture layer (Entities/UseCases/Adapters/Frameworks) by configurable path glob, then checks: dependency-direction (inner layer imports outer), framework-coupling (concrete framework import inside Entities/UseCases), missing-abstraction (5 subtypes — a UseCase file importing a concrete DB/HTTP client instead of a port/repository interface), circular-layer-dependency (reuses Step 3's `findCycles` graph walk, applied to layers instead of packages). |
|
|
132
|
+
| **Output** | JSON: findings by violation type, each citing the file(s) and layer(s) involved; low-confidence heuristic findings (mixed-concerns, UI-mixing, mixed-layer-imports) are labeled as such, not reported as certain. |
|
|
133
|
+
| **Source** | `src/agents/architecture-reviewer/tools/{dependency-rule-validator (213 lines), boundary-analysis-validator (169 lines), layer-separation-validator (145 lines)}.ts`, github.com/OnSightTeam/architecture-toolkit (MIT), fetched and read in full 2026-08-20. Their own circular-dependency check (a `"../../.."`-depth proxy) was deliberately NOT ported — replaced with a real graph cycle walk, which is strictly stronger. A real bug was caught by this tool's own positive-control fixture before ship: a `**`-glob-to-regex translation mismatched top-level paths with no parent directory segment — fixed same session. |
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Step 6 — Pattern Advisor
|
|
138
|
+
|
|
139
|
+
| | |
|
|
140
|
+
|---|---|
|
|
141
|
+
| **Call** | `rdc-pattern-score <path>` |
|
|
142
|
+
| **Input** | file or directory path |
|
|
143
|
+
| **Script** | [`scripts/lib/pattern-scoring.mjs`](scripts/lib/pattern-scoring.mjs), CLI in [`scripts/pattern-score.mjs`](scripts/pattern-score.mjs) |
|
|
144
|
+
| **Algorithm** | 10 structural detectors over `NormalizedUnit`/`NormalizedMember` facts (`switchStatements[].hasTypeCreation`, `switchBehaviorCallLine`, `constructorNewCallTargets`, `conditionalFeatureCallLine`, `deepChainCallCount`, `calleeNames`, `hasGetInstanceMethod`, field-access for Command): Factory Method (switch/if-else creating types via `new`), Builder (>4-param constructor), Singleton (`getInstance()` present), Decorator (conditional feature-wrapping call), Adapter (interface-conversion call pattern), Facade (deep call-chain into a subsystem), Strategy (switch/if-else selecting behavior), Observer (manual notify/listener pattern), Command (undo/redo/queue call OR field-access), Template Method (base method calling overridable hooks). |
|
|
145
|
+
| **Output** | JSON: per-pattern findings, each with confidence and the specific structural signal matched, attributed to a real member/unit and line. |
|
|
146
|
+
| **Source** | `src/agents/pattern-advisor/tools/{creational,structural,behavioral}-pattern-analyzer.ts`, github.com/OnSightTeam/architecture-toolkit (MIT), fetched and read in full 2026-08-20. Their checks are whole-file text regexes with zero scoping to which switch/if/call the signal actually came from (their own Factory regex matches if `new` appears anywhere after a type-switch's opening brace, even statements later) — this port walks the real AST per switch-case/if-block/call-expression, so every finding is real-line-attributable. A real undercount was found and fixed during build: Command's original port only scanned call names, missing field-access-only usage (`this.queue`/`this.history` read but never called). |
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Step 7 — Refactoring Guide
|
|
151
|
+
|
|
152
|
+
| | |
|
|
153
|
+
|---|---|
|
|
154
|
+
| **Call** | `rdc-refactoring-score <path> [--project-root <dir>] [--no-effort]` |
|
|
155
|
+
| **Input** | file or directory path; optional project root for cross-file call-site resolution |
|
|
156
|
+
| **Script** | [`scripts/lib/refactoring-scoring.mjs`](scripts/lib/refactoring-scoring.mjs), CLI in [`scripts/refactoring-score.mjs`](scripts/refactoring-score.mjs) |
|
|
157
|
+
| **Algorithm** | 9 rules over `NormalizedUnit`/`NormalizedMember` facts: extract-method (statementCount > 25), extract-class (method count > 15), introduce-parameter-object (params > 4), replace-magic-number (reuses Step 2's N4 signal, reframed as a fix recommendation), consolidate-duplicate-code (repeated statement text > 3× across > 3 patterns), decompose-conditional (> 2 conditions, each ≥ 50 chars), strategy/factory/null-object-transform candidates. `estimateEffort()` reuses the SAME cross-file `findReferencesAsNodes()` reference-graph walk as Step 2's G9 dead-export check to get a real call-site count, not a guess. |
|
|
158
|
+
| **Output** | JSON: findings with `{type, file, line, effort: low\|medium\|high\|null}` — `effort` is `null` for module-level (non-class) findings rather than a fabricated guess, since call-site resolution currently only covers exported class symbols. |
|
|
159
|
+
| **Source** | `src/agents/pattern-refactoring-guide/tools/{refactoring-analyzer, code-smell-refactoring-guide, pattern-transformation-guide}.ts`, github.com/OnSightTeam/architecture-toolkit (MIT), fetched and read in full 2026-08-20; two stale line-citations in the prior skill doc were corrected against the real source during this build (`pattern-transformation-guide.ts:109→110`, `:169→168`). **Deliberately not merged** with Step 2's thresholds even though both measure similar facts: extract-method here fires at statementCount > 25 (the toolkit's real refactoring-domain number, `refactoring-analyzer.ts:49`) vs. Clean Code's F1 at > 20 — same underlying fact, two different real thresholds for two different domains, kept separate on purpose. |
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Step 8 — Duplicate Code (G5)
|
|
164
|
+
|
|
165
|
+
| | |
|
|
166
|
+
|---|---|
|
|
167
|
+
| **Call** | `rdc-duplication-score <path> [--min-tokens <n>] [--format text\|json]` |
|
|
168
|
+
| **Input** | file or directory path; `--min-tokens` default 50 (matches PMD CPD's default token threshold) |
|
|
169
|
+
| **Script** | [`scripts/lib/duplication-scoring.mjs`](scripts/lib/duplication-scoring.mjs), CLI in [`scripts/duplication-score.mjs`](scripts/duplication-score.mjs) |
|
|
170
|
+
| **Algorithm** | Token-shingle Rabin-Karp rolling-hash matching — the same technique jscpd and PMD's CPD both use. Tokenizes each file (comments stripped by extension, string literals kept as real content since a repeated literal is real duplication), computes a rolling hash over every `minTokens`-length window in O(n), buckets windows by hash, then **merges consecutive matching window offsets into maximal contiguous blocks** — the merge step is the part that matters: without it, a single real 60-token duplicate reports as ~40 overlapping findings (one per window slide), which is exactly the bug this tool's own first draft shipped with and caught via its own positive-control test before this doc was written. |
|
|
171
|
+
| **Output** | JSON: `{duplicates: [{tokenCount, occurrences: [{file, startLine, endLine}, ...]}], filesScanned, minTokens}` — each `duplicates[]` entry is one real contiguous duplicate block, not one per window slide. |
|
|
172
|
+
| **Source** | Algorithm choice informed by [jscpd](https://github.com/kucherenko/jscpd) and PMD's [CPD](https://pmd.github.io/pmd/pmd_userdocs_cpd.html) (both real, mature, Rabin-Karp-based — see Research Bibliography below) — **no code taken from either**; this is an independent implementation of the published algorithm, not a port. **Verified against the real toolkit source** (`src/agents/clean-code-analyzer/tools/code-smell-validator.ts`, `checkDuplication()`): their G5 is a same-file repeated-line-text counter (flags a line if it appears >3 times in ONE file) — no cross-file matching, no minimum block length, a one-line coincidental repeat counts the same as a real duplicated block. This implementation is strictly stronger: real cross-file structural matching, a minimum contiguous-block length (not per-line), and reports the actual matched block range on both sides, not just a per-file count. |
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## What's still judgment, not mechanical
|
|
177
|
+
|
|
178
|
+
Four questions no AST/regex fact can answer, still routed to an LLM
|
|
179
|
+
reviewer (`pr-review-toolkit:code-reviewer` via `Agent()`, or a Codex peer
|
|
180
|
+
via `rdc:co-develop`) — named here so nothing is silently hidden inside a
|
|
181
|
+
tool's own "done" claim:
|
|
182
|
+
|
|
183
|
+
| Skill | Judgment call | Why it can't be mechanical |
|
|
184
|
+
|---|---|---|
|
|
185
|
+
| `architecture-reviewer` | Is this abstraction boundary architecturally *right* for the domain, not just shaped right | Shape-correctness (Step 5) is a fact; domain-fit is a design opinion |
|
|
186
|
+
| `pattern-advisor` | Is the detected pattern actually the correct fit here, not just structurally similar | A switch-selecting-behavior IS a Strategy signal (Step 6) whether or not Strategy is the right call for this problem |
|
|
187
|
+
| `clean-code-analyzer` | Does a name lie about what the code does (semantic, not shape) | N1/N2/N7 (Step 2) catch shape; a name that's the right LENGTH and SPECIFICITY but describes the wrong behavior needs reading intent against implementation |
|
|
188
|
+
| `package-design` | Does a package's actual responsibility match its name/README | Ca/Ce/cohesion (Step 3) are structural; "does this package do what it claims" needs reading intent |
|
|
189
|
+
|
|
190
|
+
## Decisions closed this pass (not left open)
|
|
191
|
+
|
|
192
|
+
- **AST parser (SOLID) — closed.** Swapped `solid-score.mjs` from ts-morph
|
|
193
|
+
to the fleet's own tree-sitter parser (`scripts/lib/plugins/treesitter.mjs`,
|
|
194
|
+
ported from CodeFlow's `nativeParser.ts`), default `--parser tree-sitter`.
|
|
195
|
+
Exact parity on `rdc-harness`'s `Harness` (68.5/100, both backends, every
|
|
196
|
+
criterion). One real bug found and fixed (concise-arrow-body blindness);
|
|
197
|
+
one real pre-existing ts-morph defect found and root-caused (shared-project
|
|
198
|
+
degradation at 100+ files — not a tree-sitter issue). Clean Code, Patterns,
|
|
199
|
+
and Refactoring stay on ts-morph this pass — see "AST parser — CLOSED"
|
|
200
|
+
below for full detail.
|
|
201
|
+
- **G5 duplication detection** — closed. Built Step 8 above, real
|
|
202
|
+
Rabin-Karp implementation, positive-control-verified, one real bug (window
|
|
203
|
+
merge) caught and fixed before ship.
|
|
204
|
+
- **Cycle-detection algorithm (Steps 3 and 5) — evaluated, decision: KEEP
|
|
205
|
+
the hand-rolled graph walk, do not adopt ArchUnitTS or dependency-cruiser.**
|
|
206
|
+
Reasoning: (1) our walk is already proven correct — dogfooded against
|
|
207
|
+
`rdc-harness` and this repo's own tree, zero cycles found, independently
|
|
208
|
+
confirmed by hand-verification, not just trusted; (2) it's reused twice
|
|
209
|
+
(Steps 3 and 5) with proven determinism, satisfying the ATF golden-capture
|
|
210
|
+
requirement; (3) both external tools are designed as standalone CLI/CI
|
|
211
|
+
linters with their own config/output format, not as an importable pure
|
|
212
|
+
function returning JSON into another tool's pipeline — adopting either
|
|
213
|
+
would mean wrapping a subprocess or forking their internals, a bigger
|
|
214
|
+
footprint than the ~40-line graph walk already in `package-metrics.mjs`
|
|
215
|
+
for a problem with no identified functional gap. Revisit only if a real
|
|
216
|
+
case surfaces that the hand-rolled walk gets wrong.
|
|
217
|
+
- **Unit tests for all 8 scoring libraries** — closed. 243 tests under
|
|
218
|
+
[`tests/lib/`](tests/lib/), Node's built-in `node:test` (no new
|
|
219
|
+
dependency), one file per library plus a shared `fixtures.mjs`. Every
|
|
220
|
+
rule/detector/threshold has a violation fixture; every numeric threshold
|
|
221
|
+
(F1 >20, F2 >3, Builder >4 params, T5 >10, T6 >30, etc.) is tested at and
|
|
222
|
+
just past the boundary. Re-run independently (`node --test
|
|
223
|
+
tests/lib/*.test.mjs`), not just trusted from the build report: **243
|
|
224
|
+
pass, 0 fail.** No bugs found in the libraries themselves. One real design
|
|
225
|
+
finding, not a bug: `package-metrics.mjs`'s `zone` classifier has an
|
|
226
|
+
`'off-main-sequence'` branch that is mathematically unreachable — since
|
|
227
|
+
`instability`/`abstractness` are both bounded `[0,1]`, `distance > 0.5`
|
|
228
|
+
can only happen when both values fall under 0.5 (zone-of-pain) or both
|
|
229
|
+
over 0.5 (zone-of-uselessness); no input reaches the mixed quadrant with
|
|
230
|
+
distance > 0.5. Left as-is (harmless dead branch, not incorrect output),
|
|
231
|
+
documented in the test file with the proof rather than silently removed.
|
|
232
|
+
|
|
233
|
+
## AST parser — CLOSED (SOLID moved to tree-sitter; 3 tools stay on ts-morph)
|
|
234
|
+
|
|
235
|
+
**Previously disclosed here as an open finding, now closed by direct operator
|
|
236
|
+
instruction (Dave, 2026-08-20):** the AST layer (`scripts/lib/plugins/
|
|
237
|
+
typescript.mjs`) used `ts-morph` (TypeScript/JavaScript only) without first
|
|
238
|
+
checking for existing fleet infrastructure. The monorepo already owns
|
|
239
|
+
[`@regen/codeflow-parser`](https://github.com/LIFEAI/regen-root/tree/e018e119dd22c2b75c9cae243a79230b495c53c7/packages/codeflow-parser)
|
|
240
|
+
([`nativeParser.ts`](https://github.com/LIFEAI/regen-root/blob/e018e119dd22c2b75c9cae243a79230b495c53c7/packages/codeflow-parser/src/nativeParser.ts))
|
|
241
|
+
— a genuinely multi-language, standalone, in-process tree-sitter parser
|
|
242
|
+
(TypeScript, JavaScript, Python, C, C++, C#), confirmed callable in-process
|
|
243
|
+
as a library (`this.parser.parse(files)`, CodeFlow's own ingestion pipeline)
|
|
244
|
+
with the PM2 `server.ts` wrapper as an optional separate deployment this
|
|
245
|
+
validator does not depend on.
|
|
246
|
+
|
|
247
|
+
**Built:** [`scripts/lib/plugins/treesitter.mjs`](scripts/lib/plugins/treesitter.mjs)
|
|
248
|
+
— a new `LanguagePlugin` implementing the full `NormalizedUnit`/
|
|
249
|
+
`NormalizedMember` contract for TypeScript, TSX, and JavaScript on
|
|
250
|
+
`web-tree-sitter` + `tree-sitter-wasms` directly (no dependency on
|
|
251
|
+
`regen-root` at runtime — the port is textual, done once). Foundation ported
|
|
252
|
+
from `nativeParser.ts`'s `extractTsJsSymbols`/`extractCallsFromBody`/
|
|
253
|
+
`extractTsJsImports` (same node-type vocabulary, same top-level-declaration
|
|
254
|
+
walk shape), then extended with every fact `NormalizedUnit`/
|
|
255
|
+
`NormalizedMember` requires that `nativeParser.ts` does not compute
|
|
256
|
+
(statement counts, magic numbers, empty catches, dead conditionals,
|
|
257
|
+
switch-statement shapes, null checks, complex conditionals, callee names,
|
|
258
|
+
constructor `new` targets, deep-chain call counts, `getInstance` detection,
|
|
259
|
+
static property names, LSP override comparison against a resolved base
|
|
260
|
+
class, and cross-file `deadExportsOf`/`referenceSitesOf` via a real
|
|
261
|
+
identifier-text walk over every cached parsed file — tree-sitter has no
|
|
262
|
+
`findReferencesAsNodes()` language service). Every tree-sitter node type and
|
|
263
|
+
field name used (`public_field_definition`, `method_definition`'s
|
|
264
|
+
`parameters`/`body`/`return_type` fields, `if_statement`'s `condition`/
|
|
265
|
+
`consequence`/`alternative`, `else_clause` wrapping an `else if` as a nested
|
|
266
|
+
`if_statement`, `super()` vs `super.method()`'s different function-field
|
|
267
|
+
shapes, parenless single-param arrows exposing a bare `parameter` field, TS
|
|
268
|
+
`enum_assignment`) was verified empirically by parsing representative
|
|
269
|
+
TypeScript with the installed grammar and inspecting the resulting CST —
|
|
270
|
+
not assumed from memory of the grammar.
|
|
271
|
+
|
|
272
|
+
`scripts/solid-score.mjs` now takes `--parser tree-sitter|ts-morph`,
|
|
273
|
+
**defaulting to `tree-sitter`** per the operator's instruction to actually
|
|
274
|
+
use the new plugin, not just build it unused; `ts-morph` stays available as
|
|
275
|
+
an escape hatch/regression-comparison lever.
|
|
276
|
+
|
|
277
|
+
**Parity — real numbers, via the actual CLI, both flags, same target
|
|
278
|
+
(`rdc-harness`'s `Harness` god-object, `packages/core/src/index.mjs`):**
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
=== tree-sitter (default) ===
|
|
282
|
+
Harness (class) total=68.5
|
|
283
|
+
SRP: 40 [high] 3 connected component(s) across 24 member(s)
|
|
284
|
+
OCP: 100 [low] 0 branch/type-check hit(s) across 24 member(s)
|
|
285
|
+
LSP: 100 [low-medium] no base class
|
|
286
|
+
ISP: 73 [medium-high] 18 public member(s), avg 0.8 param(s)
|
|
287
|
+
DIP: 53 [high] 15 concrete instantiation(s) of 32 total dependenc(y/ies)
|
|
288
|
+
|
|
289
|
+
=== ts-morph (--parser ts-morph) ===
|
|
290
|
+
Harness (class) total=68.5 <- byte-identical, all 5 criteria, all details
|
|
291
|
+
```
|
|
292
|
+
Not just in-range: **exactly** `68.5/100` under both backends, every
|
|
293
|
+
per-criterion score and detail string identical. `RefusedError` (the file's
|
|
294
|
+
other class) also scored identically (100/100, both backends). Confirmed
|
|
295
|
+
with a member-level diff across every `NormalizedMember` field
|
|
296
|
+
(`paramCount`, `branchHits`, `statementCount`, `isPublic`, `fieldAccess`,
|
|
297
|
+
`calls`) before wiring the CLI — zero diffs.
|
|
298
|
+
|
|
299
|
+
**A real bug found and fixed during broader dogfooding (not on the Harness
|
|
300
|
+
target — found by scoring 130 files across `rdc-skills` + `rdc-harness`):**
|
|
301
|
+
a concise-body arrow class/module property — `model = () => new
|
|
302
|
+
PhaseModel({...})` (`rdc-harness/packages/phases/test/phase-model.test.mjs:21`)
|
|
303
|
+
— has its ENTIRE body AS the `new_expression`/`call_expression`/
|
|
304
|
+
`member_expression` node itself (an arrow function's concise body is the
|
|
305
|
+
expression directly, not a `statement_block` wrapping it). The self-
|
|
306
|
+
exclusive tree walk (`getDescendantsOfKind`-equivalent, visits children only)
|
|
307
|
+
only tested the body's CHILDREN, never the body node's own type — so
|
|
308
|
+
`new PhaseModel` itself was invisible to `constructorNewCallTargetsOf`/
|
|
309
|
+
`callsOf`/`fieldsOf`/`branchHitsOf`/`calleeNamesOf`/`deepChainCallCountOf`
|
|
310
|
+
for every concise-arrow member. Fixed by making the walk self-inclusive
|
|
311
|
+
(`walkSelfAndDescendants`) — verified safe everywhere else in the file
|
|
312
|
+
(every other target node type this plugin searches for can never
|
|
313
|
+
structurally BE the root node passed in) and confirmed fixed by re-running
|
|
314
|
+
the same 130-file dogfood pass.
|
|
315
|
+
|
|
316
|
+
**A second, larger divergence found and root-caused during the same
|
|
317
|
+
dogfooding — and it is NOT a tree-sitter defect:** scoring all 130 files in
|
|
318
|
+
one process, 9 files showed DIP-score gaps up to 24.3 points, `ts-morph`
|
|
319
|
+
consistently reporting FEWER concrete instantiations than tree-sitter (e.g.
|
|
320
|
+
`phase-model.test.mjs`: ts-morph 0, tree-sitter 7). Root-caused by hand: a
|
|
321
|
+
**fresh, independent ts-morph project** parsing that same file finds **7**
|
|
322
|
+
`new PhaseModel(...)` nodes — matching tree-sitter exactly — and `typescript.mjs`'s
|
|
323
|
+
own `sharedProject()`, warmed up with only the 2 files that actually matter
|
|
324
|
+
(the defining file + the test file), ALSO correctly returns **7**. Only the
|
|
325
|
+
`sharedProject()` instance that had accumulated 113+ prior files in one
|
|
326
|
+
long-running process returns 0. This is a **pre-existing correctness defect
|
|
327
|
+
in ts-morph's incremental shared-`Project` pattern at scale**, not something
|
|
328
|
+
this build introduced — and it is evidence FOR the swap, not against it:
|
|
329
|
+
tree-sitter's plugin holds no incremental language-service state to go
|
|
330
|
+
stale, so it does not reproduce this failure mode at all. `solid-score.mjs`'s
|
|
331
|
+
real invocation pattern (one target directory/file per process) rarely hits
|
|
332
|
+
the file count where ts-morph's degradation appears, but a long-running
|
|
333
|
+
multi-package sweep (exactly what dogfooding this pass did) will.
|
|
334
|
+
|
|
335
|
+
**Determinism** — same requirement as every other tool in this doc: ran
|
|
336
|
+
`solid-score.mjs` twice against the same target, `--format json`, both a
|
|
337
|
+
single-file target and a whole-directory target, diffed the output.
|
|
338
|
+
**Byte-identical both times**, both scopes.
|
|
339
|
+
|
|
340
|
+
**UPDATE (2026-08-20/21, same night) — `treesitter.mjs` now consumes
|
|
341
|
+
`nativeParser.ts`'s own `members[]`/`units[]` extraction directly, instead of
|
|
342
|
+
re-implementing it:** the parity numbers and bug writeups directly above
|
|
343
|
+
describe the FIRST build of `treesitter.mjs`, at a point where
|
|
344
|
+
`nativeParser.ts` only extracted `symbols`/`interfaces`/`calls`/`imports` at a
|
|
345
|
+
coarse, top-level-only granularity — it never walked class method bodies at
|
|
346
|
+
all, which is why this plugin had to independently re-derive every per-member
|
|
347
|
+
fact from scratch. Minutes after that first build shipped, `nativeParser.ts`
|
|
348
|
+
gained a real `members[]`/`units[]`/`references[]` surface (a new
|
|
349
|
+
`src/memberFacts.ts`, commit `1e4e4012b` on `regen-root`'s `develop`) that
|
|
350
|
+
walks EVERY callable body — not just top-level functions — and computes
|
|
351
|
+
almost exactly the same per-member/per-unit facts this plugin was
|
|
352
|
+
duplicating. This plugin was rewritten the same night to consume that surface
|
|
353
|
+
directly rather than continue re-deriving it.
|
|
354
|
+
|
|
355
|
+
**What changed, mechanically:** the compiled `nativeParser.js`/`grammars.js`/
|
|
356
|
+
`memberFacts.js`/`xmlParser.js` (plus `.d.ts`) were re-vendored into
|
|
357
|
+
`scripts/lib/vendor/codeflow-parser/` from `regen-root`'s freshly-built
|
|
358
|
+
`dist/`, and `.source-commit` bumped to `a354d5be2d1db865faea92c1013eb2a62981e271`
|
|
359
|
+
(the `x-claude-sv` worktree HEAD at vendor time). `treesitter.mjs` now imports
|
|
360
|
+
`extractMembers` from the vendored `memberFacts.js` and calls it directly and
|
|
361
|
+
SYNCHRONOUSLY against its own tree-sitter parse of each file — not through
|
|
362
|
+
`createNativeParser().parse()`'s `async` service wrapper, which cannot be
|
|
363
|
+
called from `extractUnits()` (the `LanguagePlugin` contract in
|
|
364
|
+
`../language-plugin.mjs` requires that method to stay synchronous, and
|
|
365
|
+
`parse()` is `async` end-to-end because it also fronts an XML branch and a
|
|
366
|
+
batch override/reference-resolution pass this plugin doesn't use).
|
|
367
|
+
`extractMembers` itself has no `await` in it anywhere — a plain, pure,
|
|
368
|
+
deterministic function of `(rootNode, language)` per its own header contract
|
|
369
|
+
— so calling it directly is the correct fix for the sync/async mismatch, not
|
|
370
|
+
a workaround.
|
|
371
|
+
|
|
372
|
+
**Field mapping — `NormalizedMember`/`NormalizedUnit` (this plugin's
|
|
373
|
+
contract) ← `ParsedMember`/`ParsedUnit` (the vendored extractor's):**
|
|
374
|
+
|
|
375
|
+
| NormalizedMember/Unit field | Source | Note |
|
|
376
|
+
|---|---|---|
|
|
377
|
+
| `paramCount` | `ParsedMember.paramCount` | direct |
|
|
378
|
+
| `fieldAccess` | `ParsedMember.fieldAccess` | direct — now deduped + lexicographically sorted (was unsorted, with duplicate occurrences, before) |
|
|
379
|
+
| `branchHits` | `ParsedMember.branchHits` | direct — semantics broadened: native counts a `switch`'s `default` arm as a branch and counts an if/else-if chain by its full arm count, where this plugin's own prior local walk counted only chain LINKS and never counted `default`. Real, verified difference — see parity re-run below |
|
|
380
|
+
| `statementCount` | `ParsedMember.statementCount` | direct — boundary differs: native's walk stops at a NESTED callable (a closure passed to `.map()` is its own member), where this plugin's prior local walk was self-inclusive across ALL nesting depths including nested closures. Deliberate design in `memberFacts.ts` ("a closure...is its own member...folding its statements into the enclosing method would inflate every complexity signal") |
|
|
381
|
+
| `declaredNames` | `ParsedMember.declaredNames`, filtered | destructured-pattern entries (`{ handle, target, snapshot }` as ONE combined name — verified empirically) are dropped; `kind` field stripped to match this plugin's existing shape |
|
|
382
|
+
| `magicNumbers` | `ParsedMember.magicNumbers`, mapped | `value` coerced `string→number` (native emits `value` as source text, e.g. `"-5"`); native's 0/1/-1 exclusion is a STRING comparison (`"1.0"` would NOT be excluded) where the prior local version excluded by NUMBER comparison — a real, disclosed edge-case difference, not hit in the Harness fixture |
|
|
383
|
+
| `constructorNewCallTargets` | `ParsedMember.constructorNewCallTargets` | direct |
|
|
384
|
+
| `deepChainCallCount` | `ParsedMember.deepChainCallCount` | direct |
|
|
385
|
+
| `calleeNames` | `ParsedMember.calleeNames` | direct |
|
|
386
|
+
| `concreteInstantiations`, `totalDependencies` (unit, class only) | `ParsedUnit.concreteInstantiations`/`.totalDependencies` | direct — `totalDependencies` uses a FUNDAMENTALLY DIFFERENT formula than this plugin's prior local one (native: distinct non-self call receivers + `new`-targets, minus own member names; prior local: concrete instantiations + import-specifier count + constructor-injected-typed-param count) — a real, measured difference, see parity re-run |
|
|
387
|
+
| `staticPropertyNames`, `hasGetInstanceMethod`, `hasBaseClass` (unit, class only) | `ParsedUnit.*` | direct — `hasGetInstanceMethod`/`hasBaseClass` both broaden slightly (more `getInstance`-family names; more heritage-clause node types recognized) |
|
|
388
|
+
| `calls` | **stays local** | this plugin's contract keeps a `this.`-stripped-only, receiver-otherwise-preserved form (`obj.method()` → `"obj.method"`) for `solid-scoring.mjs`'s SRP same-component test; native's `calleeNames` strips EVERY receiver, which is right for pattern-scoring.mjs's keyword scans but wrong for SRP's sibling-call detection — would have created spurious cross-member unions |
|
|
389
|
+
| `isPublic` | **stays local** | native's `ParsedMember.exported` is the OWNING CLASS's export flag propagated to every member — it has no `private`/`protected`/`#`-prefix accessibility signal at all |
|
|
390
|
+
| `override` | **stays local** | native's `resolveOverrideShapes` is BATCH-scoped across one `parse()` call over every file at once; this plugin's `extractUnits` is called per-file, incrementally — re-running a whole-project batch parse on every single-file call would be a real perf regression, so cross-file base-method resolution keeps using this plugin's existing `fileCache`-backed lookup, unchanged |
|
|
391
|
+
| `emptyCatches`, `deadConditionals`, `nullChecks` | **stay local** | native reports these as a bare COUNT (`number`), not an array — `clean-code-scoring.mjs`'s E1/G9 findings and `refactoring-scoring.mjs`'s null-object-transform read `.line` (and, for `deadConditionals`, `.kind`) per occurrence, which a count cannot supply |
|
|
392
|
+
| `statementTexts`, `complexConditionals` | **stay local** | native reports these as `string[]` (text only, no `line`) — `refactoring-scoring.mjs`'s consolidate-duplicate-code and decompose-conditional findings need `.line` (and, for `complexConditionals`, `.length`) to build a locatable finding |
|
|
393
|
+
| `switchStatements[].hasBehaviorCall/.hasTypeCreation`, `switchBehaviorCallLine`, `conditionalFeatureCallLine` | **stay local** | native's `SwitchFact.behaviorDispatch`/`.typeConstruction` use a DIFFERENT, broader test (any call-or-return in a case; any `new` inside a type-named discriminant switch) than this repo's specific architecture-toolkit word lists (calculate/process/validate/format for clean-code and refactoring; calculate/process/execute/validate/format for pattern-advisor's Strategy; wrap/add/extend/enhance for Decorator) — reusing native's flags would silently change which findings fire |
|
|
394
|
+
| `deadExportsOf`/`referenceSitesOf` | **unchanged, fully local** | cross-file identifier-text walk, as before this pass; SOLID never calls either, so this is orthogonal to the parity numbers below |
|
|
395
|
+
|
|
396
|
+
**Correlation.** `extractMembers` returns a FLAT `members[]` including every
|
|
397
|
+
nested closure as its own entry with `owner: null` (a callback is genuinely
|
|
398
|
+
its own member with its own facts, per `memberFacts.ts`'s own design). This
|
|
399
|
+
plugin still needs to decide what counts as a "member" under its OWN contract
|
|
400
|
+
(a class's own methods/arrow-fields, or a file's own top-level declarations —
|
|
401
|
+
never an inner closure folded into one of those), so it keeps its existing
|
|
402
|
+
identity walk (`memberEntriesOf` for a class body; the top-level declaration
|
|
403
|
+
scan for a module) and correlates each locally-identified entry to the
|
|
404
|
+
matching native entry by `` `${owner ?? ''}::${name}::${startLine}` `` (the
|
|
405
|
+
callable node's own start line — verified to match exactly between the two
|
|
406
|
+
walks for method/constructor/arrow-field/top-level-function/top-level-arrow
|
|
407
|
+
shapes). A correlation miss falls back to this plugin's ORIGINAL, fully local
|
|
408
|
+
computation for that one member — unchanged from before this pass — so a miss
|
|
409
|
+
degrades to old-but-correct, never to a dropped or wrong fact.
|
|
410
|
+
|
|
411
|
+
**Re-verified parity, same target (`rdc-harness`'s `Harness`,
|
|
412
|
+
`packages/core/src/index.mjs`), both flags, real CLI output:**
|
|
413
|
+
|
|
414
|
+
```
|
|
415
|
+
=== tree-sitter (post-native-consumption) ===
|
|
416
|
+
Harness (class) total=66.9
|
|
417
|
+
SRP: 40 [high] 3 connected component(s) across 24 member(s)
|
|
418
|
+
OCP: 83 [low] 16 branch/type-check hit(s) across 24 member(s), density 0.67
|
|
419
|
+
LSP: 100 [low-medium] no base class
|
|
420
|
+
ISP: 73 [medium-high] 18 public member(s), avg 0.8 param(s)
|
|
421
|
+
DIP: 56 [high] 15 concrete instantiation(s) of 34 total dependenc(y/ies)
|
|
422
|
+
|
|
423
|
+
=== ts-morph (--parser ts-morph, unchanged) ===
|
|
424
|
+
Harness (class) total=68.5
|
|
425
|
+
OCP: 100 [low] 0 branch/type-check hit(s)
|
|
426
|
+
DIP: 53 [high] 15 concrete instantiation(s) of 32 total dependenc(y/ies)
|
|
427
|
+
(SRP/LSP/ISP unchanged, byte-identical to tree-sitter)
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
**66.9, not 68.5 — a real, explained difference, not a regression:**
|
|
431
|
+
`concreteInstantiations: 15` matches exactly (both backends, unchanged —
|
|
432
|
+
confirms the underlying `new PhaseModel(...)`-class detection is stable). The
|
|
433
|
+
two criteria that moved are exactly the two fields the mapping table above
|
|
434
|
+
flags as using DIFFERENT NATIVE FORMULAS, not local re-derivations:
|
|
435
|
+
- **OCP (83 vs 100, ts-morph's own 0):** `branchHits` rose from ts-morph's 0
|
|
436
|
+
to tree-sitter's 16 because `memberFacts.ts` counts a `switch`'s `default`
|
|
437
|
+
arm as a branch and counts a full if/else-if chain by arm count — both
|
|
438
|
+
MORE COMPLETE than either prior implementation. This is the "real, positive
|
|
439
|
+
finding" case the task called out: the native parser counts branches the
|
|
440
|
+
duplicate logic undercounted (ts-morph's own branchHits equivalent reports
|
|
441
|
+
0 for this same class — a pre-existing gap in the untouched ts-morph path,
|
|
442
|
+
not introduced here).
|
|
443
|
+
- **DIP (56 vs 53):** `totalDependencies` is 34 (native) vs 32 (ts-morph) —
|
|
444
|
+
a 2-dependency gap from two genuinely different counting methodologies
|
|
445
|
+
(native: distinct call-receivers + `new`-targets; ts-morph/prior-local:
|
|
446
|
+
import-specifier count + constructor-injected-typed-param count), not a
|
|
447
|
+
bug in either.
|
|
448
|
+
`RefusedError` (the file's other class) still scores 100/100 identically.
|
|
449
|
+
|
|
450
|
+
**Determinism** — ran `solid-score.mjs --parser tree-sitter` twice against
|
|
451
|
+
the same target, `--format json`, diffed the output: **byte-identical.**
|
|
452
|
+
|
|
453
|
+
**Regression suite** — `node --test tests/lib/*.test.mjs`: **243/243**, no
|
|
454
|
+
change from before this pass (none of the 243 tests exercise the Harness
|
|
455
|
+
fixture's exact score, so the OCP/DIP formula changes above did not trip any
|
|
456
|
+
existing assertion; they are captured here as a disclosed finding instead).
|
|
457
|
+
|
|
458
|
+
**How much duplicate CST-walking code was actually removable — the honest
|
|
459
|
+
number is small, not large:** `treesitter.mjs` grew from 964 to 1,182 lines
|
|
460
|
+
(+218), not shrank. Nothing was DELETED from the local extractor block —
|
|
461
|
+
every local function (`fieldsOf`, `callsOf`, `branchHitsOf`,
|
|
462
|
+
`declaredNamesOf`, `magicNumbersOf`, `statementCountOf`,
|
|
463
|
+
`constructorNewCallTargetsOf`, `deepChainCallCountOf`, `calleeNamesOf`,
|
|
464
|
+
`concreteDependencyCounts`, plus all eleven residual-fact extractors) is
|
|
465
|
+
still present, because it is still needed as the correlation-miss fallback
|
|
466
|
+
path for the nine member fields and three unit fields now primarily sourced
|
|
467
|
+
from native output, in addition to being unconditionally needed for the
|
|
468
|
+
eleven fields that never had a native equivalent to begin with (`calls`,
|
|
469
|
+
`isPublic`, `override`, and the eight clean-code/refactoring/pattern-advisor
|
|
470
|
+
facts requiring per-item line/regex detail the native surface doesn't carry).
|
|
471
|
+
What changed is which VALUES are used at runtime, not which code exists: in
|
|
472
|
+
the common case (correlation succeeds — the normal case for real class/module
|
|
473
|
+
members), 9 of 19 `NormalizedMember` fields and 5 of 9 `NormalizedUnit`
|
|
474
|
+
fields now come from the vendored extractor instead of this plugin's own
|
|
475
|
+
walk, with the local computation demoted to a safety-net fallback rather
|
|
476
|
+
than deleted. The remaining ~10 member facts genuinely cannot be sourced
|
|
477
|
+
from `memberFacts.ts`'s current output shape (missing per-item line numbers,
|
|
478
|
+
different regex/keyword semantics, or a batch-scoping mismatch with this
|
|
479
|
+
plugin's incremental per-file API) and stay local, unconditionally, by
|
|
480
|
+
design — not by oversight.
|
|
481
|
+
|
|
482
|
+
**Scoped explicitly out of this pass:**
|
|
483
|
+
- **Only `solid-score.mjs` was swapped.** `clean-code-score.mjs`,
|
|
484
|
+
`pattern-score.mjs`, and `refactoring-score.mjs` still default to
|
|
485
|
+
`typescript.mjs`/ts-morph. Swapping SOLID first, with the new plugin
|
|
486
|
+
built, dogfooded, and proven, de-risks the other three — a larger
|
|
487
|
+
regression-proof job the operator can direct next.
|
|
488
|
+
- **Python is NOT implemented in `treesitter.mjs`.** `nativeParser.ts` has
|
|
489
|
+
real, working Python extraction logic (`extractPythonSymbols`) that could
|
|
490
|
+
be ported in a follow-up pass, but no scoring CLI in this repo has ever
|
|
491
|
+
targeted Python — shipping an unproven, un-dogfooded third language in
|
|
492
|
+
the same pass as the TS/JS swap is scope creep, not scope discipline.
|
|
493
|
+
C/C++/C# are out of scope entirely for the same reason.
|
|
494
|
+
- **A minor, disclosed `.d.ts`-only gap:** two ambient-declaration-only
|
|
495
|
+
files (`scripts/lib/vendor/codeflow-parser/{grammars,nativeParser}.d.ts`)
|
|
496
|
+
score 1 unit under ts-morph (which recognizes `declare function ...;` as a
|
|
497
|
+
`FunctionDeclaration` via its higher-level API) and 0 under tree-sitter
|
|
498
|
+
(whose grammar wraps an ambient signature as `ambient_declaration`, a
|
|
499
|
+
different node type than plain `function_declaration`, which this
|
|
500
|
+
plugin's top-level walk does not yet match). Both sides of this gap are
|
|
501
|
+
bodyless type declarations with zero SOLID-relevant content either way
|
|
502
|
+
(no statements, no branches, no fields) — disclosed, not chased further
|
|
503
|
+
this pass.
|
|
504
|
+
|
|
505
|
+
**Real dependency versions installed and verified this pass** (added to
|
|
506
|
+
`rdc-skills`' own `package.json` `dependencies` — not the codeflow-parser
|
|
507
|
+
package in the separate `regen-root` monorepo, which this plugin does not
|
|
508
|
+
import from at runtime):
|
|
509
|
+
|
|
510
|
+
| Package | Version installed | License (verified) |
|
|
511
|
+
|---|---|---|
|
|
512
|
+
| [web-tree-sitter](https://www.npmjs.com/package/web-tree-sitter) | `0.24.7` (matches the version confirmed live in `regen-root`'s own `codeflow-parser`; npm `latest` is `0.26.12` — pinned to the version already proven working with this ported logic rather than chasing latest) | MIT (verified: `node_modules/web-tree-sitter/package.json`) |
|
|
513
|
+
| [tree-sitter-wasms](https://www.npmjs.com/package/tree-sitter-wasms) | `0.1.13` (matches npm `latest`) | **Unlicense** (verified: `node_modules/tree-sitter-wasms/package.json` — public domain, NOT MIT) |
|
|
514
|
+
|
|
515
|
+
## Research bibliography
|
|
516
|
+
|
|
517
|
+
Real URLs fetched/searched this session — for the next person picking this
|
|
518
|
+
up, not "I researched online":
|
|
519
|
+
|
|
520
|
+
**Source ported from:**
|
|
521
|
+
- [github.com/OnSightTeam/architecture-toolkit](https://github.com/OnSightTeam/architecture-toolkit) — root repo, `.claude/skills/`, `src/agents/` tree
|
|
522
|
+
- `src/agents/clean-code-analyzer/tools/{naming,function,code-smell}-validator.ts`
|
|
523
|
+
- `src/agents/package-design/tools/{stability-metrics-calculator,package-coupling-analyzer}.ts`
|
|
524
|
+
- `src/agents/testing-strategy/tools/test-quality-validator.ts`
|
|
525
|
+
- `src/agents/architecture-reviewer/tools/{dependency-rule-validator,boundary-analysis-validator,layer-separation-validator}.ts`
|
|
526
|
+
- `src/agents/pattern-advisor/tools/{creational,structural,behavioral}-pattern-analyzer.ts`
|
|
527
|
+
- `src/agents/pattern-refactoring-guide/tools/{refactoring-analyzer,code-smell-refactoring-guide,pattern-transformation-guide}.ts`
|
|
528
|
+
(all fetched via `raw.githubusercontent.com/OnSightTeam/architecture-toolkit/main/<path>`)
|
|
529
|
+
|
|
530
|
+
**Web searches run (query → what it surfaced):**
|
|
531
|
+
- "static analysis tools SOLID principles violation detection without LLM AST-based" → [cycode.com](https://cycode.com/blog/static-code-analysis/), [Sorald (arXiv 2103.12033)](https://arxiv.org/pdf/2103.12033), [AVATAR (arXiv 1812.07270)](https://arxiv.org/pdf/1812.07270), [Mining Fix Patterns for FindBugs (arXiv 1712.03201)](https://arxiv.org/pdf/1712.03201), [datadoghq.com](https://www.datadoghq.com/knowledge-center/static-analysis/), [blog.codacy.com](https://blog.codacy.com/static-code-analysis), [oligo.security](https://www.oligo.security/academy/static-code-analysis)
|
|
532
|
+
- "ArchUnit dependency-cruiser layered architecture boundary enforcement rules" → [archunit.org/userguide](https://www.archunit.org/userguide/html/000_Index.html), [ArchUnitTS](https://github.com/LukasNiessen/ArchUnitTS), [ArchUnitPython](https://github.com/LukasNiessen/ArchUnitPython), [Loiane Groner: Architecture Testing for Java with ArchUnit](https://loiane.com/2026/07/architecture-testing-java-archunit/), [thearchitectsnotebook.substack.com](https://thearchitectsnotebook.substack.com/p/ep-122-the-modular-monolith-part)
|
|
533
|
+
- "design pattern detection static analysis algorithm academic Factory Strategy Observer AST" → [Design pattern detection approaches: a systematic review (Springer, 10.1007/s10462-020-09834-5)](https://link.springer.com/article/10.1007/s10462-020-09834-5), [MARPLE (ScienceDirect S0020025510005955)](https://www.sciencedirect.com/science/article/abs/pii/S0020025510005955), [Identification and Assessment of Software Design Pattern Violations (arXiv 1906.01419)](https://arxiv.org/pdf/1906.01419), [Automatic Design Pattern Detection (Brown CS)](https://vis.cs.brown.edu/docs/pdf/Heuzeroth-2003-ADP.pdf)
|
|
534
|
+
- "jscpd PMD CPD code duplication detection algorithm Rabin-Karp token-based" → [jscpd](https://github.com/kucherenko/jscpd), [PMD CPD](https://pmd.github.io/pmd/pmd_userdocs_cpd.html), [aarongoldenthal.com: GitLab Code Quality with PMD CPD](https://aarongoldenthal.com/posts/gitlab-code-quality-duplication-analysis-with-pmd-cpd/), [dev.to duplicate-checker roundup](https://dev.to/rahulxsingh/13-best-duplicate-code-checker-tools-in-2026-1cnk)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: analyze-tests
|
|
3
|
+
description: >-
|
|
4
|
+
Usage `rdc:analyze-tests <path>` — recommends test level (unit/
|
|
5
|
+
integration/live) and shape (assertion/golden-capture) for a surface
|
|
6
|
+
(FUNCTION corner). See skills/testing-strategy.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# analyze-tests
|
|
10
|
+
|
|
11
|
+
Use Skill tool with skill: "testing-strategy", passing the path as args.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: check-clean-code
|
|
3
|
+
description: >-
|
|
4
|
+
Usage `rdc:check-clean-code <path> [--diff <ref>]` — naming, dead code,
|
|
5
|
+
complexity smells. See skills/clean-code-analyzer.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# check-clean-code
|
|
9
|
+
|
|
10
|
+
Use Skill tool with skill: "clean-code-analyzer", passing the path and any
|
|
11
|
+
`--diff` flag as args.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: compare-compliance
|
|
3
|
+
description: >-
|
|
4
|
+
Usage `rdc:compare-compliance <path> --diff <ref>` — the SOLID/Clean
|
|
5
|
+
Architecture regression gate: did this change make compliance worse than
|
|
6
|
+
`<ref>`? Wraps solid-validator's git-diff mode.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# compare-compliance
|
|
10
|
+
|
|
11
|
+
Use Skill tool with skill: "solid-validator", passing the path and a
|
|
12
|
+
required `--diff <ref>` as args. Read `regressions` and `boundaryViolations`
|
|
13
|
+
from the output — either non-empty means the change made compliance worse
|
|
14
|
+
than the base ref, not merely "still imperfect."
|