@lifeaitools/rdc-skills 0.34.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.claude-plugin/plugin.json +284 -1
  2. package/VALIDATOR-ARCHITECTURE.md +534 -0
  3. package/commands/analyze-tests.md +11 -0
  4. package/commands/check-clean-code.md +11 -0
  5. package/commands/check-packages.md +10 -0
  6. package/commands/compare-compliance.md +14 -0
  7. package/commands/full-analysis.md +50 -0
  8. package/commands/get-refactoring-plan.md +13 -0
  9. package/commands/quick-check.md +13 -0
  10. package/commands/recover.md +149 -0
  11. package/commands/review-arch.md +12 -0
  12. package/commands/review.md +12 -113
  13. package/commands/suggest-patterns.md +11 -0
  14. package/commands/validate-solid.md +11 -0
  15. package/package.json +14 -2
  16. package/scripts/architecture-score.mjs +157 -0
  17. package/scripts/clean-code-score.mjs +177 -0
  18. package/scripts/duplication-score.mjs +66 -0
  19. package/scripts/lib/architecture-scoring.mjs +695 -0
  20. package/scripts/lib/clean-code-scoring.mjs +258 -0
  21. package/scripts/lib/duplication-scoring.mjs +238 -0
  22. package/scripts/lib/language-plugin.mjs +82 -0
  23. package/scripts/lib/package-metrics.mjs +439 -0
  24. package/scripts/lib/pattern-scoring.mjs +351 -0
  25. package/scripts/lib/plugins/treesitter.mjs +1182 -0
  26. package/scripts/lib/plugins/typescript.mjs +672 -0
  27. package/scripts/lib/refactoring-scoring.mjs +307 -0
  28. package/scripts/lib/solid-scoring.mjs +101 -0
  29. package/scripts/lib/test-smell-scoring.mjs +581 -0
  30. package/scripts/lib/vendor/codeflow-parser/.source-commit +1 -0
  31. package/scripts/lib/vendor/codeflow-parser/grammars.d.ts +23 -0
  32. package/scripts/lib/vendor/codeflow-parser/grammars.js +57 -0
  33. package/scripts/lib/vendor/codeflow-parser/memberFacts.d.ts +274 -0
  34. package/scripts/lib/vendor/codeflow-parser/memberFacts.js +1117 -0
  35. package/scripts/lib/vendor/codeflow-parser/nativeParser.d.ts +115 -0
  36. package/scripts/lib/vendor/codeflow-parser/nativeParser.js +759 -0
  37. package/scripts/lib/vendor/codeflow-parser/package.json +3 -0
  38. package/scripts/lib/vendor/codeflow-parser/xmlParser.d.ts +77 -0
  39. package/scripts/lib/vendor/codeflow-parser/xmlParser.js +400 -0
  40. package/scripts/package-metrics-cli.mjs +112 -0
  41. package/scripts/pattern-score.mjs +143 -0
  42. package/scripts/refactoring-score.mjs +253 -0
  43. package/scripts/solid-score.mjs +337 -0
  44. package/skills/architecture-reviewer/SKILL.md +287 -0
  45. package/skills/clean-code-analyzer/SKILL.md +147 -0
  46. package/skills/package-design/SKILL.md +118 -0
  47. package/skills/pattern-advisor/SKILL.md +237 -0
  48. package/skills/pattern-refactoring-guide/SKILL.md +262 -0
  49. package/skills/review/SKILL.md +29 -0
  50. package/skills/solid-validator/SKILL.md +92 -0
  51. package/skills/testing-strategy/SKILL.md +132 -0
  52. package/tests/lib/architecture-scoring.test.mjs +335 -0
  53. package/tests/lib/clean-code-scoring.test.mjs +241 -0
  54. package/tests/lib/duplication-scoring.test.mjs +144 -0
  55. package/tests/lib/fixtures.mjs +58 -0
  56. package/tests/lib/package-metrics.test.mjs +241 -0
  57. package/tests/lib/pattern-scoring.test.mjs +251 -0
  58. package/tests/lib/refactoring-scoring.test.mjs +264 -0
  59. package/tests/lib/solid-scoring.test.mjs +291 -0
  60. package/tests/lib/test-smell-scoring.test.mjs +281 -0
@@ -0,0 +1,1182 @@
1
+ /**
2
+ * Tree-sitter language plugin — the multi-language-capable, in-process AST
3
+ * backend for the LanguagePlugin contract (see ../language-plugin.mjs).
4
+ *
5
+ * Direct operator instruction (Dave, 2026-08-20): stop depending on a
6
+ * third-party TS-only parser (ts-morph, scripts/lib/plugins/typescript.mjs)
7
+ * when the fleet already owns a standalone, in-process, multi-language
8
+ * tree-sitter parser — CodeFlow's own `packages/codeflow-parser/src/
9
+ * nativeParser.ts` in the separate `regen-root` monorepo.
10
+ *
11
+ * UPDATE (2026-08-20/21, same night): `nativeParser.ts` shipped a real
12
+ * `members[]`/`units[]` surface (via the new `src/memberFacts.ts`) that
13
+ * walks EVERY callable body — not just top-level exported functions — and
14
+ * computes almost exactly the per-member/per-unit facts this file used to
15
+ * duplicate from scratch. This plugin now VENDORS the compiled
16
+ * `memberFacts.js` (plus `nativeParser.js`/`grammars.js`/`xmlParser.js`, for
17
+ * fidelity and any future use) into `../vendor/codeflow-parser/` and calls
18
+ * its exported `extractMembers(rootNode, language)` DIRECTLY against this
19
+ * plugin's OWN tree-sitter parse of the file — not through
20
+ * `createNativeParser().parse()`'s async service wrapper. That wrapper's
21
+ * `parse()` is `async` (it exists to front an XML branch and a batch
22
+ * override/reference pass this plugin doesn't use) and therefore cannot be
23
+ * called from `extractUnits()`, which the LanguagePlugin contract
24
+ * (../language-plugin.mjs) requires to stay SYNCHRONOUS. `extractMembers`
25
+ * itself has no `await` anywhere in it — it is a plain, pure, synchronous
26
+ * function of `(rootNode, language)` — so calling it directly is both the
27
+ * correct fix for the sync/async mismatch and a smaller surface to vendor.
28
+ *
29
+ * WHAT THIS PLUGIN NOW GETS FROM THE VENDORED EXTRACTOR (native-sourced,
30
+ * per member, when a correlated entry is found — see `nativeMemberKey`
31
+ * below): `paramCount`, `fieldAccess`, `calleeNames`, `deepChainCallCount`,
32
+ * `constructorNewCallTargets`, `branchHits`, `statementCount`,
33
+ * `declaredNames` (destructured-pattern entries filtered — see
34
+ * `filterDeclaredNames`), `magicNumbers` (value coerced string→number). Per
35
+ * unit (class only — the vendored extractor has no "module" unit concept,
36
+ * see below): `concreteInstantiations`, `totalDependencies`,
37
+ * `staticPropertyNames`, `hasGetInstanceMethod`, `hasBaseClass`.
38
+ *
39
+ * WHAT STAYS LOCALLY COMPUTED, AND WHY — this is a real, disclosed residual,
40
+ * not a "small gap": the vendored `ParsedMember` shape reports several
41
+ * clean-code/refactoring facts as either a bare COUNT (`nullChecks`,
42
+ * `emptyCatches`, `deadConditionals` are `number`, not an array) or as
43
+ * LINE-LESS text (`statementTexts`, `complexConditionals` are `string[]`,
44
+ * not `{text/length, line}[]`). This repo's clean-code-scoring.mjs and
45
+ * refactoring-scoring.mjs read `.line` (and, for `deadConditionals`,
46
+ * `.kind`; for `complexConditionals`, `.length`) on every one of those to
47
+ * build a findable location — so all five stay a local per-item CST walk on
48
+ * the member's own `bodyNode`, same functions as before this change.
49
+ * `switchStatements[].hasBehaviorCall/hasTypeCreation`,
50
+ * `switchBehaviorCallLine`, and `conditionalFeatureCallLine` also stay
51
+ * local: the vendored `SwitchFact.behaviorDispatch`/`.typeConstruction` use
52
+ * a DIFFERENT, broader test (any call-or-return in a case; any `new` in a
53
+ * type-discriminant-named switch) than this repo's specific
54
+ * architecture-toolkit-derived word lists
55
+ * (calculate/process/validate/format, and separately
56
+ * calculate/process/execute/validate/format for the pattern-advisor's
57
+ * Strategy signal, and wrap/add/extend/enhance for Decorator) — reusing the
58
+ * vendored flags would silently change which findings fire. `calls` also
59
+ * stays local: this repo's contract keeps a receiver-qualified-except-`this.`
60
+ * form (`obj.method()` → `"obj.method"`) for solid-scoring.mjs's SRP
61
+ * same-component test; the vendored `calleeNames` strips EVERY receiver,
62
+ * which is the right fact for pattern-scoring.mjs's keyword scans but the
63
+ * wrong one for SRP's sibling-method-call detection. `isPublic` stays
64
+ * local: the vendored `ParsedMember.exported` is the OWNING CLASS's export
65
+ * status propagated to every member, not an accessibility check — it has no
66
+ * `private`/`protected`/`#`-prefix signal at all, so this plugin still reads
67
+ * `accessibility_modifier` off the located CST node itself. `override`
68
+ * (LSP base-method drift) stays local for a different reason: the vendored
69
+ * resolver (`resolveOverrideShapes`) is BATCH-scoped over one `parse()` call
70
+ * across every file handed to it at once, but this plugin's `extractUnits`
71
+ * is called incrementally, one file at a time — re-running a whole-project
72
+ * batch parse on every single-file call would be a real performance
73
+ * regression, so cross-file override resolution keeps using this plugin's
74
+ * own existing `fileCache`-backed base-class lookup (unchanged).
75
+ * `deadExportsOf`/`referenceSitesOf` (cross-file dead-export / reference-site
76
+ * scanning) are UNCHANGED and fully local for the same reason, plus SOLID
77
+ * itself never calls either.
78
+ *
79
+ * CORRELATION: `extractMembers` returns a FLAT `members[]` array that
80
+ * includes every nested closure as its own entry with `owner: null` — not
81
+ * just top-level/class members — because a callback passed to `.map()` is
82
+ * genuinely its own member with its own facts (see memberFacts.js's own
83
+ * header comment). This plugin still needs to know WHICH callable node is a
84
+ * "member" per its own NormalizedUnit contract (a class's own methods, or a
85
+ * file's own top-level declarations — never an inner closure folded into
86
+ * one of those). So this plugin keeps its OWN existing identity walk
87
+ * (`memberEntriesOf` for a class body, the top-level declaration scan for a
88
+ * module) to decide what counts as a member and to locate each one's
89
+ * `bodyNode` for the local residual facts above, and uses that identity
90
+ * walk's own `(owner, name, start_line)` to look up the matching entry in
91
+ * the vendored extractor's flat list (`nativeMemberKey`/
92
+ * `buildNativeMemberIndex`) for the native-sourced fields. A correlation
93
+ * miss (should not happen for a real class method or top-level declaration;
94
+ * kept as a safety net for a grammar edge case this plugin's own detection
95
+ * and memberFacts.js's disagree on) falls back to this plugin's original,
96
+ * fully-local computation for that one member's mappable fields, unchanged
97
+ * from before this pass — a miss degrades to old-but-correct, never to a
98
+ * silently dropped or wrong fact.
99
+ *
100
+ * nativeParser.ts's top-level CST-walking functions (`extractTsJsSymbols`,
101
+ * `extractCallsFromBody`, `extractTsJsImports`) were the ORIGINAL cited
102
+ * foundation for this file's now-superseded from-scratch member/unit
103
+ * assembly and remain the model for `importsOf`/`deadExportsOf`/
104
+ * `referenceSitesOf` below, which this plugin still computes itself.
105
+ *
106
+ * Every node-type name and field name used below (e.g. `public_field_
107
+ * definition`, `method_definition`'s `parameters`/`body`/`return_type`
108
+ * fields, `if_statement`'s `condition`/`consequence`/`alternative` fields,
109
+ * `else_clause` wrapping an `else if` as a nested `if_statement`, `super()`
110
+ * as function-field type `super` vs `super.method()` as a `member_expression`
111
+ * whose `object` field is `super`, single-param parenless arrow functions
112
+ * exposing a bare `parameter` field instead of `formal_parameters`, TS
113
+ * `enum_assignment` nodes) was VERIFIED empirically this session by parsing
114
+ * representative TypeScript source with the installed
115
+ * `tree-sitter-wasms@0.1.13` grammar and inspecting the resulting CST —
116
+ * not guessed from memory of the grammar.
117
+ *
118
+ * WASM grammar resolution mirrors `packages/codeflow-parser/src/
119
+ * grammars.ts`'s approach for THIS plugin's own tree-sitter Parser/Language
120
+ * setup below (kept separate from the vendored `grammars.js`, which backs
121
+ * only the unused async `createNativeParser()` wrapper): `require.resolve(
122
+ * 'tree-sitter-wasms/out/tree-sitter-<lang>.wasm')`, confirmed against the
123
+ * real installed package layout (`node_modules/tree-sitter-wasms/out/*.wasm`).
124
+ *
125
+ * Scope this pass: TypeScript, TSX, and JavaScript — parity with
126
+ * typescript.mjs, the only two source extensions that plugin ever handled
127
+ * (`.tsx` is new; ts-morph's single plugin already accepted `.tsx` files but
128
+ * parsed them with the same TS-family checker, so this plugin routes `.tsx`
129
+ * to the dedicated `tree-sitter-tsx` grammar for correctness rather than
130
+ * matching that shortcut). Python is explicitly NOT implemented here.
131
+ * nativeParser.ts has real, working Python extraction logic
132
+ * (`extractPythonSymbols`) that could be ported in a follow-up pass, but no
133
+ * scoring CLI in this repo has ever targeted Python — shipping an unproven,
134
+ * un-dogfooded third language in the same pass as the TS/JS swap is scope
135
+ * creep this task explicitly called out as optional and skippable. C/C++/C#
136
+ * are out of scope entirely for the same reason.
137
+ */
138
+
139
+ import { extractMembers } from '../vendor/codeflow-parser/memberFacts.js';
140
+
141
+ import { createRequire } from 'node:module';
142
+ import { readFileSync } from 'node:fs';
143
+ import { basename } from 'node:path';
144
+
145
+ const require = createRequire(import.meta.url);
146
+
147
+ // ── Runtime init ────────────────────────────────────────────────────────
148
+ // web-tree-sitter's Parser.init() and Language.load() are inherently async
149
+ // (WASM instantiation). The LanguagePlugin contract's extractUnits/importsOf
150
+ // are synchronous (see the JSDoc in ../language-plugin.mjs). Top-level await
151
+ // closes that gap: ESM blocks the whole module graph on this module's own
152
+ // top-level await before ANY importer's code can run — so by the time
153
+ // solid-score.mjs's `main()` calls `plugin.extractUnits(...)`, both the
154
+ // runtime and both grammars below are already loaded, and every exported
155
+ // method is genuinely synchronous. No call site anywhere else changes.
156
+
157
+ const mod = await import('web-tree-sitter');
158
+ const Parser = mod.default ?? mod.Parser;
159
+ await Parser.init();
160
+ const Language = mod.Language ?? Parser.Language;
161
+
162
+ const GRAMMAR_FILES = {
163
+ typescript: 'tree-sitter-typescript.wasm',
164
+ tsx: 'tree-sitter-tsx.wasm',
165
+ javascript: 'tree-sitter-javascript.wasm',
166
+ };
167
+
168
+ async function loadGrammar(name) {
169
+ const wasmPath = require.resolve(`tree-sitter-wasms/out/${GRAMMAR_FILES[name]}`);
170
+ return Language.load(wasmPath);
171
+ }
172
+
173
+ const TS_LANG = await loadGrammar('typescript');
174
+ const TSX_LANG = await loadGrammar('tsx');
175
+ const JS_LANG = await loadGrammar('javascript');
176
+
177
+ function grammarFor(filePath) {
178
+ if (/\.tsx$/.test(filePath)) return TSX_LANG;
179
+ if (/\.(ts|mjs|cjs)$/.test(filePath)) return TS_LANG; // TS grammar syntactically supersets plain JS — same "one plugin, both languages" shape as typescript.mjs's ts-morph `allowJs`
180
+ return JS_LANG; // plain .js
181
+ }
182
+
183
+ /**
184
+ * memberFacts.js's `LANGUAGE_PROFILES` table (vendored) has no separate
185
+ * 'tsx' entry — only 'typescript'/'javascript'/'python'/'csharp'/'c'/'cpp'.
186
+ * A `.tsx` file's node-type vocabulary for class/method/call/etc. is the
187
+ * SAME TS-family grammar as `.ts` (only JSX-specific node types like
188
+ * `jsx_element` differ, none of which this plugin's facts touch), so
189
+ * passing 'tsx' through would silently fall back to the C-like default
190
+ * profile (whose `klass`/`method` node types are wrong for TS) rather than
191
+ * erroring — 'typescript' is the correct language string for both.
192
+ */
193
+ function nativeLanguageFor(filePath) {
194
+ if (/\.(ts|tsx|mjs|cjs)$/.test(filePath)) return 'typescript';
195
+ return 'javascript';
196
+ }
197
+
198
+ /**
199
+ * Correlation key between this plugin's own member-identity walk
200
+ * (`memberEntriesOf` / the module-level top-declaration scan) and
201
+ * `extractMembers`'s flat `members[]` output. `owner` is the class name (or
202
+ * `null` at module scope); `startLine` is the callable node's OWN start
203
+ * line — for a `method_definition` that's the method node itself, for an
204
+ * arrow-valued class field or a `const foo = () => {}` it's the
205
+ * `arrow_function`/`function_expression` node's start line, not the
206
+ * enclosing field-definition/declarator's — matching exactly what
207
+ * `extractMembers`'s own `visit()` uses as `node.startPosition.row + 1`
208
+ * (see memberFacts.js's `buildMember`). This plugin's own entries already
209
+ * carry that exact node as `entry.typeSourceNode` for every entry shape
210
+ * (method, arrow field, top-level function, top-level arrow const) — see
211
+ * `memberEntriesOf` and `unitsFromModuleLevel` below.
212
+ */
213
+ function nativeMemberKey(owner, name, startLine) {
214
+ return `${owner ?? ''}::${name}::${startLine}`;
215
+ }
216
+
217
+ /**
218
+ * A get/set pair declared on the exact same source line (same owner, same
219
+ * name, same start line) would collide in this index — the second one wins
220
+ * and the first falls back to local computation via the correlation-miss
221
+ * path in `normalizedMember`. Real code essentially never writes get/set
222
+ * accessors on one line, so this is an accepted, disclosed edge case rather
223
+ * than a bug worth a more expensive per-kind key.
224
+ */
225
+ function buildNativeMemberIndex(nativeMembers) {
226
+ const map = new Map();
227
+ for (const nm of nativeMembers) {
228
+ map.set(nativeMemberKey(nm.owner, nm.name, nm.start_line), nm);
229
+ }
230
+ return map;
231
+ }
232
+
233
+ const parserCache = new Map(); // grammar object -> reusable Parser instance
234
+ function parserFor(grammar) {
235
+ let p = parserCache.get(grammar);
236
+ if (!p) {
237
+ p = new Parser();
238
+ p.setLanguage(grammar);
239
+ parserCache.set(grammar, p);
240
+ }
241
+ return p;
242
+ }
243
+
244
+ function parseText(filePath, sourceText) {
245
+ return parserFor(grammarFor(filePath)).parse(sourceText);
246
+ }
247
+
248
+ // ── File cache (mirrors typescript.mjs's `sharedProject()`) ───────────────
249
+ // One shared cache per process. A file is added to it the first time
250
+ // extractUnits/importsOf/deadExportsOf/referenceSitesOf touches it, exactly
251
+ // mirroring ts-morph's incremental Project: solid-score.mjs's own directory
252
+ // walk calls extractUnits() on every file before the boundary/dead-export
253
+ // checks run, so by the time cross-file resolution (base-class lookup,
254
+ // local-class-name set, reference counting) is needed, every file already
255
+ // scanned this run is present.
256
+
257
+ const fileCache = new Map(); // absolute path -> { rootNode, sourceText }
258
+
259
+ function cacheEntryFor(filePath, sourceText) {
260
+ if (sourceText !== undefined) {
261
+ // Ephemeral parse (e.g. a file's content at a git ref via `--diff`) —
262
+ // its own one-shot parse, never mixed into the shared cross-file cache,
263
+ // matching typescript.mjs's ephemeral in-memory Project branch.
264
+ const tree = parseText(filePath, sourceText);
265
+ return { rootNode: tree.rootNode, sourceText };
266
+ }
267
+ const cached = fileCache.get(filePath);
268
+ if (cached) return cached;
269
+ const text = readFileSync(filePath, 'utf8');
270
+ const tree = parseText(filePath, text);
271
+ const entry = { rootNode: tree.rootNode, sourceText: text };
272
+ fileCache.set(filePath, entry);
273
+ return entry;
274
+ }
275
+
276
+ function ensureCached(paths) {
277
+ for (const p of paths) {
278
+ if (fileCache.has(p)) continue;
279
+ try {
280
+ const text = readFileSync(p, 'utf8');
281
+ const tree = parseText(p, text);
282
+ fileCache.set(p, { rootNode: tree.rootNode, sourceText: text });
283
+ } catch {
284
+ // unreadable/binary/non-source file — not a TS/JS file, skip (matches
285
+ // typescript.mjs's identical try/catch around addSourceFileAtPath)
286
+ }
287
+ }
288
+ }
289
+
290
+ // ── Generic CST walk helpers ────────────────────────────────────────────
291
+
292
+ function lineOf(node) {
293
+ return node.startPosition.row + 1;
294
+ }
295
+
296
+ /**
297
+ * Depth-first, self-INCLUSIVE, ALL nesting levels (no boundary stop at
298
+ * nested function/class) — matches ts-morph's getDescendantsOfKind
299
+ * semantics for the "all nesting depths" part (the NormalizedMember JSDoc
300
+ * calls this out explicitly), but is deliberately SELF-inclusive where
301
+ * ts-morph's own getDescendantsOfKind is not.
302
+ *
303
+ * Real bug found and fixed this session: a concise-body arrow class
304
+ * property — `model = () => new PhaseModel({...})` (verified against
305
+ * `packages/phases/test/phase-model.test.mjs:21` in rdc-harness, found via
306
+ * a `constructorNewCallTargets` parity diff against ts-morph) — has its
307
+ * ENTIRE body AS the `new_expression`/`call_expression`/`member_expression`
308
+ * node itself (arrow_function's `body` field for a concise body is the
309
+ * expression directly, not a `statement_block` wrapping it; verified by
310
+ * parsing `x => x * 2` and inspecting the CST). A self-exclusive walk
311
+ * starting at that body node only visits ITS CHILDREN (the `new` call's
312
+ * arguments), never testing the body node's own type — so `new PhaseModel`
313
+ * itself was invisible to constructorNewCallTargetsOf/callsOf/fieldsOf/
314
+ * branchHitsOf/calleeNamesOf/deepChainCallCountOf for every concise-body
315
+ * arrow member. ts-morph's typescript.mjs does not hit this because
316
+ * ts-morph's own body/expression node distinction differs internally; the
317
+ * gap here is a tree-sitter-CST-specific consequence of `body` sometimes
318
+ * being a bare expression rather than a block. Self-inclusion is safe
319
+ * everywhere ELSE in this file: every other target type this file searches
320
+ * for (if_statement, switch_statement, catch_clause, variable_declarator,
321
+ * class_declaration, identifier, …) can never structurally BE the root node
322
+ * passed in (a `statement_block`, `class_declaration`, or `program`), so
323
+ * testing the root is a harmless no-op there and a real fix here.
324
+ */
325
+ function walkSelfAndDescendants(node, visit) {
326
+ visit(node);
327
+ for (let i = 0; i < node.namedChildCount; i++) {
328
+ const c = node.namedChild(i);
329
+ if (c) walkSelfAndDescendants(c, visit);
330
+ }
331
+ }
332
+
333
+ function descendantsOfType(root, types) {
334
+ const set = types instanceof Set ? types : new Set(types);
335
+ const out = [];
336
+ walkSelfAndDescendants(root, (n) => { if (set.has(n.type)) out.push(n); });
337
+ return out;
338
+ }
339
+
340
+ function hasAnonChild(node, text) {
341
+ for (let i = 0; i < node.childCount; i++) {
342
+ const c = node.child(i);
343
+ if (c && !c.isNamed && c.text === text) return true;
344
+ }
345
+ return false;
346
+ }
347
+
348
+ function isStaticNode(node) {
349
+ return hasAnonChild(node, 'static');
350
+ }
351
+
352
+ function accessibilityOf(node) {
353
+ for (let i = 0; i < node.namedChildCount; i++) {
354
+ const c = node.namedChild(i);
355
+ if (c && c.type === 'accessibility_modifier') return c.text; // 'private' | 'protected' | 'public'
356
+ }
357
+ return null;
358
+ }
359
+
360
+ function unwrapParen(node) {
361
+ return node && node.type === 'parenthesized_expression' ? node.namedChild(0) : node;
362
+ }
363
+
364
+ function normalizeText(t) {
365
+ return t.replace(/\s+/g, ' ').trim();
366
+ }
367
+
368
+ /** `required_parameter`/`optional_parameter` wrap the binding in a `pattern` field; a parenless single-param arrow's own parameter node IS the binding. */
369
+ function patternOf(paramNode) {
370
+ if (paramNode.type === 'required_parameter' || paramNode.type === 'optional_parameter') {
371
+ return paramNode.childForFieldName('pattern');
372
+ }
373
+ return paramNode;
374
+ }
375
+
376
+ /** `formal_parameters` holds 0+ params as named children; a parenless single-param arrow's `parameter` field IS the one param, not a list. */
377
+ function paramListOf(paramsNode) {
378
+ if (!paramsNode) return [];
379
+ if (paramsNode.type === 'formal_parameters') {
380
+ const out = [];
381
+ for (let i = 0; i < paramsNode.namedChildCount; i++) out.push(paramsNode.namedChild(i));
382
+ return out;
383
+ }
384
+ return [paramsNode];
385
+ }
386
+
387
+ // ── Clean Code / Refactoring / Pattern-advisor facts ───────────────────
388
+ // Same detection intent as typescript.mjs's block of the same name (real
389
+ // logic ported from architecture-toolkit, MIT, explicit reuse approval —
390
+ // see typescript.mjs's own citations for the original source); only the
391
+ // AST-walk mechanics differ (tree-sitter CST vs ts-morph wrapper API).
392
+
393
+ const STATEMENT_TYPES = new Set([
394
+ 'expression_statement', 'lexical_declaration', 'variable_declaration',
395
+ 'if_statement', 'for_statement', 'for_in_statement', 'while_statement',
396
+ 'do_statement', 'switch_statement', 'return_statement', 'throw_statement',
397
+ 'try_statement', 'break_statement', 'continue_statement', 'labeled_statement',
398
+ ]);
399
+ // Tree-sitter's grammar unifies `for...in` and `for...of` into ONE node type
400
+ // (`for_in_statement`, disambiguated only by an anonymous `in`/`of` child
401
+ // token) — verified by parsing both forms and inspecting the CST. ts-morph
402
+ // keeps ForInStatement/ForOfStatement as two SyntaxKinds; STATEMENT_TYPES
403
+ // above counts `for_in_statement` once, which already covers both source
404
+ // forms — no separate entry needed or possible.
405
+
406
+ function statementCountOf(bodyNode) {
407
+ return descendantsOfType(bodyNode, STATEMENT_TYPES).length;
408
+ }
409
+
410
+ function fieldsOf(bodyNode) {
411
+ const out = [];
412
+ walkSelfAndDescendants(bodyNode, (n) => {
413
+ if (n.type !== 'member_expression') return;
414
+ const obj = n.childForFieldName('object');
415
+ if (obj && obj.type === 'this') {
416
+ const prop = n.childForFieldName('property');
417
+ if (prop) out.push(prop.text);
418
+ }
419
+ });
420
+ return out;
421
+ }
422
+
423
+ function callsOf(bodyNode) {
424
+ const out = [];
425
+ walkSelfAndDescendants(bodyNode, (n) => {
426
+ if (n.type !== 'call_expression') return;
427
+ const fn = n.childForFieldName('function');
428
+ if (fn) out.push(fn.text.replace(/^this\./, ''));
429
+ });
430
+ return out;
431
+ }
432
+
433
+ /** `else if` is `else_clause` wrapping a nested `if_statement` as its sole child — verified by parsing an `if/else if/else` chain and inspecting the CST (NOT assumed from the plain-JS/TS spec, which some grammars implement differently). */
434
+ function isElseIfChain(ifStmt) {
435
+ const alt = ifStmt.childForFieldName('alternative');
436
+ if (!alt) return false;
437
+ if (alt.type === 'if_statement') return true;
438
+ if (alt.type === 'else_clause') return alt.namedChild(0)?.type === 'if_statement';
439
+ return false;
440
+ }
441
+
442
+ function branchHitsOf(bodyNode) {
443
+ let n = 0;
444
+ walkSelfAndDescendants(bodyNode, (node) => {
445
+ if (node.type === 'switch_statement') {
446
+ const body = node.childForFieldName('body');
447
+ if (body) {
448
+ for (let i = 0; i < body.namedChildCount; i++) {
449
+ if (body.namedChild(i).type === 'switch_case') n++;
450
+ }
451
+ }
452
+ } else if (node.type === 'binary_expression') {
453
+ if (node.childForFieldName('operator')?.text === 'instanceof') n++;
454
+ } else if (node.type === 'unary_expression') {
455
+ if (node.childForFieldName('operator')?.text === 'typeof') n++;
456
+ } else if (node.type === 'if_statement' && isElseIfChain(node)) {
457
+ n++;
458
+ }
459
+ });
460
+ return n;
461
+ }
462
+
463
+ function declaredNamesOf(paramsNode, bodyNode) {
464
+ const names = [];
465
+ for (const p of paramListOf(paramsNode)) {
466
+ const pat = patternOf(p);
467
+ if (pat && pat.type === 'identifier') names.push({ name: pat.text, line: lineOf(p) });
468
+ }
469
+ for (const decl of descendantsOfType(bodyNode, ['variable_declarator'])) {
470
+ const nameNode = decl.childForFieldName('name');
471
+ if (nameNode && nameNode.type === 'identifier') names.push({ name: nameNode.text, line: lineOf(decl) });
472
+ }
473
+ return names;
474
+ }
475
+
476
+ function magicNumbersOf(bodyNode) {
477
+ const found = [];
478
+ for (const lit of descendantsOfType(bodyNode, ['number'])) {
479
+ let node = lit;
480
+ let value = Number(lit.text);
481
+ const parent = lit.parent;
482
+ if (parent?.type === 'unary_expression' && parent.childForFieldName('operator')?.text === '-') {
483
+ value = -value;
484
+ node = parent;
485
+ }
486
+ if (value === 0 || value === 1 || value === -1) continue;
487
+
488
+ const initParent = node.parent;
489
+ let excluded = false;
490
+ if (initParent?.type === 'variable_declarator' && initParent.childForFieldName('value') === node) {
491
+ const declList = initParent.parent;
492
+ if (declList?.type === 'lexical_declaration' && declList.child(0)?.type === 'const') excluded = true;
493
+ }
494
+ // TS enum members (`enum_assignment`, e.g. `Red = 1`) — verified node
495
+ // type by parsing a real `enum` declaration and inspecting the CST.
496
+ if (initParent?.type === 'enum_assignment') excluded = true;
497
+ if (excluded) continue;
498
+
499
+ found.push({ value, line: lineOf(lit) });
500
+ }
501
+ return found;
502
+ }
503
+
504
+ function emptyCatchesOf(bodyNode) {
505
+ const found = [];
506
+ for (const cc of descendantsOfType(bodyNode, ['catch_clause'])) {
507
+ const block = cc.childForFieldName('body');
508
+ if (block && block.namedChildCount === 0) found.push({ line: lineOf(cc) });
509
+ }
510
+ return found;
511
+ }
512
+
513
+ function deadConditionalsOf(bodyNode) {
514
+ const found = [];
515
+ for (const ifStmt of descendantsOfType(bodyNode, ['if_statement'])) {
516
+ const cond = unwrapParen(ifStmt.childForFieldName('condition'));
517
+ if (cond?.type === 'true') found.push({ line: lineOf(ifStmt), kind: 'if-true' });
518
+ else if (cond?.type === 'false') found.push({ line: lineOf(ifStmt), kind: 'if-false' });
519
+ }
520
+ for (const whileStmt of descendantsOfType(bodyNode, ['while_statement'])) {
521
+ const cond = unwrapParen(whileStmt.childForFieldName('condition'));
522
+ if (cond?.type === 'false') found.push({ line: lineOf(whileStmt), kind: 'while-false' });
523
+ }
524
+ return found;
525
+ }
526
+
527
+ function statementTextsOf(bodyNode) {
528
+ const found = [];
529
+ for (const node of descendantsOfType(bodyNode, STATEMENT_TYPES)) {
530
+ const text = normalizeText(node.text);
531
+ if (text.length > 10) found.push({ text, line: lineOf(node) });
532
+ }
533
+ return found;
534
+ }
535
+
536
+ function nullChecksOf(bodyNode) {
537
+ const found = [];
538
+ for (const ifStmt of descendantsOfType(bodyNode, ['if_statement'])) {
539
+ const cond = ifStmt.childForFieldName('condition');
540
+ if (!cond) continue;
541
+ for (const b of descendantsOfType(cond, ['binary_expression'])) {
542
+ const op = b.childForFieldName('operator')?.text;
543
+ if (op !== '===' && op !== '!==') continue;
544
+ const left = b.childForFieldName('left');
545
+ const right = b.childForFieldName('right');
546
+ if (left?.type === 'null' || right?.type === 'null') { found.push({ line: lineOf(ifStmt) }); break; }
547
+ }
548
+ }
549
+ return found;
550
+ }
551
+
552
+ const STRATEGY_BEHAVIOR_RE = /(calculate|process|validate|format)/i;
553
+ function switchStatementsOf(bodyNode) {
554
+ const found = [];
555
+ for (const sw of descendantsOfType(bodyNode, ['switch_statement'])) {
556
+ const discriminant = sw.childForFieldName('value');
557
+ const discriminantText = discriminant ? discriminant.text : '';
558
+ const swText = sw.text;
559
+ found.push({
560
+ line: lineOf(sw),
561
+ hasBehaviorCall: STRATEGY_BEHAVIOR_RE.test(swText),
562
+ hasTypeCreation: /type/i.test(discriminantText) && /\bnew\s+/.test(swText),
563
+ });
564
+ }
565
+ return found;
566
+ }
567
+
568
+ function complexConditionalsOf(bodyNode) {
569
+ const found = [];
570
+ for (const ifStmt of descendantsOfType(bodyNode, ['if_statement'])) {
571
+ const inner = unwrapParen(ifStmt.childForFieldName('condition'));
572
+ const condText = inner ? inner.text : '';
573
+ if (condText.length >= 50) found.push({ line: lineOf(ifStmt), length: condText.length });
574
+ }
575
+ return found;
576
+ }
577
+
578
+ const PATTERN_ADVISOR_BEHAVIOR_RE = /(calculate|process|execute|validate|format)/i;
579
+ function switchBehaviorCallLineOf(bodyNode) {
580
+ for (const sw of descendantsOfType(bodyNode, ['switch_statement'])) {
581
+ if (PATTERN_ADVISOR_BEHAVIOR_RE.test(sw.text)) return lineOf(sw);
582
+ }
583
+ return null;
584
+ }
585
+
586
+ function constructorNewCallTargetsOf(bodyNode) {
587
+ const out = [];
588
+ for (const n of descendantsOfType(bodyNode, ['new_expression'])) {
589
+ const ctor = n.childForFieldName('constructor');
590
+ if (ctor) out.push(ctor.text);
591
+ }
592
+ return out;
593
+ }
594
+
595
+ const FEATURE_CALL_RE = /(wrap|add|extend|enhance)/i;
596
+ function trailingCallName(callExpr) {
597
+ const fn = callExpr.childForFieldName('function');
598
+ if (!fn) return '';
599
+ if (fn.type === 'member_expression') {
600
+ const prop = fn.childForFieldName('property');
601
+ return prop ? prop.text : fn.text;
602
+ }
603
+ return fn.text.replace(/^this\./, '');
604
+ }
605
+
606
+ function conditionalFeatureCallLineOf(bodyNode) {
607
+ for (const ifStmt of descendantsOfType(bodyNode, ['if_statement'])) {
608
+ const then = ifStmt.childForFieldName('consequence');
609
+ if (!then) continue;
610
+ const calls = descendantsOfType(then, ['call_expression']);
611
+ if (calls.some((c) => FEATURE_CALL_RE.test(trailingCallName(c)))) return lineOf(ifStmt);
612
+ }
613
+ return null;
614
+ }
615
+
616
+ function deepChainCallCountOf(bodyNode) {
617
+ let n = 0;
618
+ for (const c of descendantsOfType(bodyNode, ['call_expression'])) {
619
+ const fn = c.childForFieldName('function');
620
+ if (fn?.type === 'member_expression' && fn.childForFieldName('object')?.type === 'member_expression') n++;
621
+ }
622
+ return n;
623
+ }
624
+
625
+ function calleeNamesOf(bodyNode) {
626
+ return descendantsOfType(bodyNode, ['call_expression']).map(trailingCallName);
627
+ }
628
+
629
+ // ── Member/unit assembly ────────────────────────────────────────────────
630
+
631
+ const STDLIB_WHITELIST = new Set([
632
+ 'Map', 'Set', 'WeakMap', 'WeakSet', 'Array', 'Object', 'Date', 'Error',
633
+ 'TypeError', 'RangeError', 'RegExp', 'Promise', 'URL', 'URLSearchParams',
634
+ 'AbortController', 'Buffer', 'Headers', 'Request', 'Response',
635
+ ]);
636
+ const PRIMITIVE_TYPES = new Set(['string', 'number', 'boolean', 'any', 'unknown']);
637
+
638
+ function isPublicOf(isPublicNode, name) {
639
+ if (!isPublicNode) return false;
640
+ const access = accessibilityOf(isPublicNode);
641
+ if (access === 'private' || access === 'protected') return false;
642
+ if ((name ?? '').startsWith('#')) return false;
643
+ return true;
644
+ }
645
+
646
+ /**
647
+ * `cls.getMethods()`-equivalent alone would miss constructors, get/set
648
+ * accessors, and arrow-function class properties ("class ArrowGod { greet =
649
+ * () => {} }") — a mainstream TS/JS style. Mirrors typescript.mjs's
650
+ * `memberEntries()` for the same reason: a class scored on methods alone
651
+ * with none present reads as `members.length === 0`, a silent perfect score
652
+ * on a class the scorer never actually looked inside.
653
+ */
654
+ function memberEntriesOf(classBody) {
655
+ const entries = [];
656
+ for (let i = 0; i < classBody.namedChildCount; i++) {
657
+ const node = classBody.namedChild(i);
658
+ if (!node) continue;
659
+ if (node.type === 'method_definition') {
660
+ const nameNode = node.childForFieldName('name');
661
+ const name = nameNode ? nameNode.text : '(anonymous)';
662
+ const bodyNode = node.childForFieldName('body');
663
+ if (!bodyNode) continue; // abstract/overload signature — no body to analyze
664
+ const isCtor = name === 'constructor';
665
+ entries.push({
666
+ name,
667
+ paramsNode: node.childForFieldName('parameters'),
668
+ bodyNode,
669
+ isPublicNode: isCtor ? null : node, // constructor: not counted toward ISP's public behavioral surface, matching typescript.mjs
670
+ isStatic: isStaticNode(node),
671
+ typeSourceNode: node,
672
+ defNode: node,
673
+ });
674
+ } else if (node.type === 'public_field_definition') {
675
+ const valueNode = node.childForFieldName('value');
676
+ if (valueNode && (valueNode.type === 'arrow_function' || valueNode.type === 'function_expression' || valueNode.type === 'generator_function')) {
677
+ const nameNode = node.childForFieldName('name');
678
+ const name = nameNode ? nameNode.text : '(anonymous)';
679
+ entries.push({
680
+ name,
681
+ paramsNode: valueNode.childForFieldName('parameters') ?? valueNode.childForFieldName('parameter'),
682
+ bodyNode: valueNode.childForFieldName('body'),
683
+ isPublicNode: node,
684
+ isStatic: isStaticNode(node),
685
+ typeSourceNode: valueNode,
686
+ defNode: node,
687
+ });
688
+ }
689
+ }
690
+ }
691
+ return entries;
692
+ }
693
+
694
+ function findConstructorNode(bodyNode) {
695
+ if (!bodyNode) return null;
696
+ for (let i = 0; i < bodyNode.namedChildCount; i++) {
697
+ const c = bodyNode.namedChild(i);
698
+ if (c && c.type === 'method_definition' && c.childForFieldName('name')?.text === 'constructor') return c;
699
+ }
700
+ return null;
701
+ }
702
+
703
+ function callsSuperIn(bodyNode) {
704
+ return descendantsOfType(bodyNode, ['call_expression']).some((c) => {
705
+ const fn = c.childForFieldName('function');
706
+ if (!fn) return false;
707
+ // `super(...)` — function field IS a `super` node (verified: distinct
708
+ // from `super.method(...)`, whose function field is a `member_expression`
709
+ // with `object` field of type `super`).
710
+ if (fn.type === 'super') return true;
711
+ if (fn.type === 'member_expression') return fn.childForFieldName('object')?.type === 'super';
712
+ return false;
713
+ });
714
+ }
715
+
716
+ function returnTypeTextOf(typeSourceNode) {
717
+ const n = typeSourceNode?.childForFieldName?.('return_type');
718
+ return n ? n.text.replace(/^:\s*/, '') : null;
719
+ }
720
+
721
+ /**
722
+ * `declaredNames` entries whose name contains a destructuring bracket are a
723
+ * known wrinkle in the vendored extractor: a destructured parameter
724
+ * (`{ handle, target, snapshot }`) or local (`const { a, b } = x`) has no
725
+ * single bound identifier at the grammar's `name`/`pattern` field, so
726
+ * `memberFacts.js`'s `parameterName()`/declarator handling falls back to
727
+ * the whole pattern's normalized source text as ONE combined "name" — verified
728
+ * empirically this session (`{ handle, target, snapshot }` as a single
729
+ * `declaredNames` entry). This repo's N1/N2 naming rules (clean-code-
730
+ * scoring.mjs) want simple, non-destructured bindings only, so any entry
731
+ * shaped like a pattern is dropped rather than reported as a single
732
+ * cryptic/noise-word "name".
733
+ */
734
+ function filterDeclaredNames(declaredNames) {
735
+ const out = [];
736
+ for (const d of declaredNames) {
737
+ if (/[{}[\]]/.test(d.name)) continue;
738
+ out.push({ name: d.name, line: d.line });
739
+ }
740
+ return out;
741
+ }
742
+
743
+ function normalizedMember(entry, baseMethodsMap, nativeMember) {
744
+ const { name, paramsNode, bodyNode, isPublicNode, typeSourceNode } = entry;
745
+ const isPublic = isPublicOf(isPublicNode, name);
746
+ const base = name && baseMethodsMap ? baseMethodsMap.get(name) : null;
747
+ let override = null;
748
+ if (base) {
749
+ const baseParamCount = paramListOf(base.childForFieldName('parameters')).length;
750
+ override = {
751
+ baseParamCount,
752
+ callsSuper: callsSuperIn(bodyNode),
753
+ returnType: returnTypeTextOf(typeSourceNode),
754
+ baseReturnType: returnTypeTextOf(base),
755
+ };
756
+ }
757
+
758
+ // Facts that stay local regardless of correlation — see the file header
759
+ // ("WHAT STAYS LOCALLY COMPUTED, AND WHY") for why each of these cannot
760
+ // be sourced from the vendored extractor's ParsedMember shape.
761
+ const localFacts = {
762
+ calls: callsOf(bodyNode),
763
+ emptyCatches: emptyCatchesOf(bodyNode),
764
+ deadConditionals: deadConditionalsOf(bodyNode),
765
+ statementTexts: statementTextsOf(bodyNode),
766
+ nullChecks: nullChecksOf(bodyNode),
767
+ switchStatements: switchStatementsOf(bodyNode),
768
+ complexConditionals: complexConditionalsOf(bodyNode),
769
+ switchBehaviorCallLine: switchBehaviorCallLineOf(bodyNode),
770
+ conditionalFeatureCallLine: conditionalFeatureCallLineOf(bodyNode),
771
+ };
772
+
773
+ if (nativeMember) {
774
+ return {
775
+ name: name ?? '(anonymous)',
776
+ paramCount: nativeMember.paramCount,
777
+ fieldAccess: nativeMember.fieldAccess,
778
+ branchHits: nativeMember.branchHits,
779
+ isPublic,
780
+ override,
781
+ statementCount: nativeMember.statementCount,
782
+ declaredNames: filterDeclaredNames(nativeMember.declaredNames),
783
+ magicNumbers: nativeMember.magicNumbers.map((n) => ({ value: Number(n.value), line: n.line })),
784
+ constructorNewCallTargets: nativeMember.constructorNewCallTargets,
785
+ deepChainCallCount: nativeMember.deepChainCallCount,
786
+ calleeNames: nativeMember.calleeNames,
787
+ ...localFacts,
788
+ };
789
+ }
790
+
791
+ // Correlation miss — see the file header's CORRELATION section. Compute
792
+ // every field locally, exactly as this plugin did before the native
793
+ // extractor existed, so a miss degrades to old-but-correct rather than a
794
+ // dropped or wrong fact.
795
+ return {
796
+ name: name ?? '(anonymous)',
797
+ paramCount: paramListOf(paramsNode).length,
798
+ fieldAccess: fieldsOf(bodyNode),
799
+ branchHits: branchHitsOf(bodyNode),
800
+ isPublic,
801
+ override,
802
+ statementCount: statementCountOf(bodyNode),
803
+ declaredNames: declaredNamesOf(paramsNode, bodyNode),
804
+ magicNumbers: magicNumbersOf(bodyNode),
805
+ constructorNewCallTargets: constructorNewCallTargetsOf(bodyNode),
806
+ deepChainCallCount: deepChainCallCountOf(bodyNode),
807
+ calleeNames: calleeNamesOf(bodyNode),
808
+ ...localFacts,
809
+ };
810
+ }
811
+
812
+ function namedImportSpecifiersOf(rootNode) {
813
+ const out = [];
814
+ for (let i = 0; i < rootNode.namedChildCount; i++) {
815
+ const node = rootNode.namedChild(i);
816
+ if (!node || node.type !== 'import_statement') continue;
817
+ for (let j = 0; j < node.namedChildCount; j++) {
818
+ const clause = node.namedChild(j);
819
+ if (!clause || clause.type !== 'import_clause') continue;
820
+ for (let k = 0; k < clause.namedChildCount; k++) {
821
+ const cc = clause.namedChild(k);
822
+ if (cc && cc.type === 'named_imports') {
823
+ for (let m = 0; m < cc.namedChildCount; m++) {
824
+ const spec = cc.namedChild(m);
825
+ if (spec && spec.type === 'import_specifier') {
826
+ const nameNode = spec.childForFieldName('name');
827
+ if (nameNode) out.push(nameNode.text);
828
+ }
829
+ }
830
+ }
831
+ }
832
+ }
833
+ }
834
+ return out;
835
+ }
836
+
837
+ function concreteDependencyCounts(scopeNode, rootNode, localClassNames, constructorNode) {
838
+ let concrete = 0;
839
+ for (const n of descendantsOfType(scopeNode, ['new_expression'])) {
840
+ const ctor = n.childForFieldName('constructor');
841
+ const name = ctor ? ctor.text : '';
842
+ if (STDLIB_WHITELIST.has(name)) continue;
843
+ if (localClassNames.has(name)) concrete++;
844
+ }
845
+ const importNames = namedImportSpecifiersOf(rootNode);
846
+ let injected = 0;
847
+ if (constructorNode) {
848
+ for (const p of paramListOf(constructorNode.childForFieldName('parameters'))) {
849
+ if (p.type !== 'required_parameter' && p.type !== 'optional_parameter') continue;
850
+ const typeNode = p.childForFieldName('type');
851
+ const t = typeNode ? typeNode.text.replace(/^:\s*/, '').trim() : null;
852
+ if (t && !PRIMITIVE_TYPES.has(t)) injected++;
853
+ }
854
+ }
855
+ return { concreteInstantiations: concrete, totalDependencies: concrete + importNames.length + injected };
856
+ }
857
+
858
+ function getDeclarationNode(topNode) {
859
+ if (topNode.type === 'export_statement') {
860
+ for (let i = 0; i < topNode.namedChildCount; i++) {
861
+ const child = topNode.namedChild(i);
862
+ if (child && child.type !== 'comment') return child;
863
+ }
864
+ return null;
865
+ }
866
+ return topNode;
867
+ }
868
+
869
+ function classHeritageBaseName(cls) {
870
+ for (let i = 0; i < cls.namedChildCount; i++) {
871
+ const c = cls.namedChild(i);
872
+ if (c && c.type === 'class_heritage') {
873
+ for (let j = 0; j < c.namedChildCount; j++) {
874
+ const cc = c.namedChild(j);
875
+ if (cc && cc.type === 'extends_clause') {
876
+ const target = cc.namedChild(0);
877
+ return target ? target.text : null;
878
+ }
879
+ }
880
+ }
881
+ }
882
+ return null;
883
+ }
884
+
885
+ function allClassDeclarationsAcrossCache(extraRoot) {
886
+ const out = [];
887
+ const roots = new Set([...[...fileCache.values()].map((e) => e.rootNode), extraRoot]);
888
+ for (const root of roots) {
889
+ for (const cls of descendantsOfType(root, ['class_declaration'])) out.push(cls);
890
+ }
891
+ return out;
892
+ }
893
+
894
+ function findClassDeclByName(name, extraRoot) {
895
+ for (const cls of allClassDeclarationsAcrossCache(extraRoot)) {
896
+ if (cls.childForFieldName('name')?.text === name) return cls;
897
+ }
898
+ return null;
899
+ }
900
+
901
+ function unitFromClass(cls, rootNode, localClassNames, nativeUnitsByName, nativeIndex) {
902
+ const nameNode = cls.childForFieldName('name');
903
+ const bodyNode = cls.childForFieldName('body');
904
+ const baseName = classHeritageBaseName(cls);
905
+ const baseDecl = baseName ? findClassDeclByName(baseName, rootNode) : null;
906
+ const className = nameNode ? nameNode.text : '(anonymous)';
907
+
908
+ let baseMethodsMap = null;
909
+ if (baseDecl) {
910
+ const baseBody = baseDecl.childForFieldName('body');
911
+ baseMethodsMap = new Map();
912
+ if (baseBody) {
913
+ for (let i = 0; i < baseBody.namedChildCount; i++) {
914
+ const m = baseBody.namedChild(i);
915
+ if (m && m.type === 'method_definition') {
916
+ const mn = m.childForFieldName('name');
917
+ if (mn) baseMethodsMap.set(mn.text, m);
918
+ }
919
+ }
920
+ }
921
+ }
922
+
923
+ const entries = bodyNode ? memberEntriesOf(bodyNode) : [];
924
+ const members = entries.map((e) => {
925
+ const nativeStartLine = e.typeSourceNode.startPosition.row + 1;
926
+ const nm = nativeIndex.get(nativeMemberKey(className, e.name, nativeStartLine));
927
+ return normalizedMember(e, baseMethodsMap, nm);
928
+ });
929
+
930
+ const nativeUnit = nativeUnitsByName.get(className);
931
+
932
+ let dep;
933
+ let staticPropertyNames;
934
+ let hasBaseClassFlag;
935
+ if (nativeUnit) {
936
+ dep = { concreteInstantiations: nativeUnit.concreteInstantiations, totalDependencies: nativeUnit.totalDependencies };
937
+ staticPropertyNames = nativeUnit.staticPropertyNames;
938
+ hasBaseClassFlag = nativeUnit.hasBaseClass;
939
+ } else {
940
+ // Correlation miss (e.g. two classes sharing a name in one file) — fall
941
+ // back to this plugin's original local computation, unchanged.
942
+ const ctorNode = findConstructorNode(bodyNode);
943
+ dep = concreteDependencyCounts(cls, rootNode, localClassNames, ctorNode);
944
+ staticPropertyNames = [];
945
+ if (bodyNode) {
946
+ for (let i = 0; i < bodyNode.namedChildCount; i++) {
947
+ const c = bodyNode.namedChild(i);
948
+ if (c && c.type === 'public_field_definition' && isStaticNode(c)) {
949
+ const n = c.childForFieldName('name');
950
+ if (n) staticPropertyNames.push(n.text);
951
+ }
952
+ }
953
+ }
954
+ hasBaseClassFlag = Boolean(baseName);
955
+ }
956
+
957
+ return {
958
+ name: className,
959
+ kind: 'class',
960
+ members,
961
+ hasBaseClass: hasBaseClassFlag,
962
+ ...dep,
963
+ staticPropertyNames,
964
+ hasGetInstanceMethod: nativeUnit ? nativeUnit.hasGetInstanceMethod : members.some((m) => m.name === 'getInstance'),
965
+ };
966
+ }
967
+
968
+ function unitsFromModuleLevel(rootNode, localClassNames, filePath, nativeIndex) {
969
+ const fnDecls = [];
970
+ const arrowFns = [];
971
+ for (let i = 0; i < rootNode.namedChildCount; i++) {
972
+ const top = rootNode.namedChild(i);
973
+ const exported = top.type === 'export_statement';
974
+ const decl = getDeclarationNode(top);
975
+ if (!decl) continue;
976
+ if (decl.type === 'function_declaration') {
977
+ fnDecls.push({ decl, exported });
978
+ } else if (decl.type === 'lexical_declaration' || decl.type === 'variable_declaration') {
979
+ for (let j = 0; j < decl.namedChildCount; j++) {
980
+ const declarator = decl.namedChild(j);
981
+ if (!declarator || declarator.type !== 'variable_declarator') continue;
982
+ const nameNode = declarator.childForFieldName('name');
983
+ const valueNode = declarator.childForFieldName('value');
984
+ if (!nameNode || nameNode.type !== 'identifier') continue;
985
+ if (valueNode && (valueNode.type === 'arrow_function' || valueNode.type === 'function_expression')) {
986
+ arrowFns.push({ name: nameNode.text, node: valueNode, exported });
987
+ }
988
+ }
989
+ }
990
+ }
991
+
992
+ if (!fnDecls.length && !arrowFns.length) return [];
993
+
994
+ const members = [
995
+ ...fnDecls.map(({ decl, exported }) => {
996
+ const entry = {
997
+ name: decl.childForFieldName('name')?.text ?? '(anonymous)',
998
+ paramsNode: decl.childForFieldName('parameters'),
999
+ bodyNode: decl.childForFieldName('body'),
1000
+ isPublicNode: exported ? decl : null,
1001
+ typeSourceNode: decl,
1002
+ };
1003
+ const nativeStartLine = decl.startPosition.row + 1;
1004
+ const nm = nativeIndex.get(nativeMemberKey(null, entry.name, nativeStartLine));
1005
+ return normalizedMember(entry, null, nm);
1006
+ }),
1007
+ ...arrowFns.map(({ name, node, exported }) => {
1008
+ const entry = {
1009
+ name,
1010
+ paramsNode: node.childForFieldName('parameters') ?? node.childForFieldName('parameter'),
1011
+ bodyNode: node.childForFieldName('body'),
1012
+ isPublicNode: exported ? node : null,
1013
+ typeSourceNode: node,
1014
+ };
1015
+ const nativeStartLine = node.startPosition.row + 1;
1016
+ const nm = nativeIndex.get(nativeMemberKey(null, name, nativeStartLine));
1017
+ return normalizedMember(entry, null, nm);
1018
+ }),
1019
+ ];
1020
+
1021
+ const dep = concreteDependencyCounts(rootNode, rootNode, localClassNames, null);
1022
+ return [{
1023
+ name: basename(filePath),
1024
+ kind: 'module',
1025
+ members,
1026
+ hasBaseClass: false,
1027
+ ...dep,
1028
+ staticPropertyNames: [],
1029
+ hasGetInstanceMethod: members.some((m) => m.name === 'getInstance'),
1030
+ }];
1031
+ }
1032
+
1033
+ function unitsFromParsedFile(rootNode, filePath) {
1034
+ const localClassNames = new Set();
1035
+ for (const cls of allClassDeclarationsAcrossCache(rootNode)) {
1036
+ const n = cls.childForFieldName('name');
1037
+ if (n) localClassNames.add(n.text);
1038
+ }
1039
+
1040
+ const classDecls = [];
1041
+ for (let i = 0; i < rootNode.namedChildCount; i++) {
1042
+ const top = rootNode.namedChild(i);
1043
+ const decl = getDeclarationNode(top);
1044
+ if (decl && decl.type === 'class_declaration') classDecls.push(decl);
1045
+ }
1046
+
1047
+ // One synchronous call to the vendored extractor per file. `extractMembers`
1048
+ // is a pure function of (rootNode, language) — see the file header — so
1049
+ // this is safe to call from this plugin's synchronous extractUnits(). A
1050
+ // throw here (grammar/extractor mismatch this plugin has not seen) must
1051
+ // never take down the whole scorer: every member/unit falls back to fully
1052
+ // local computation via an empty index, same as a correlation miss.
1053
+ let native;
1054
+ try {
1055
+ native = extractMembers(rootNode, nativeLanguageFor(filePath));
1056
+ } catch {
1057
+ native = { members: [], units: [] };
1058
+ }
1059
+ const nativeIndex = buildNativeMemberIndex(native.members);
1060
+ const nativeUnitsByName = new Map(native.units.map((u) => [u.name, u]));
1061
+
1062
+ if (classDecls.length) {
1063
+ return classDecls.map((cls) => unitFromClass(cls, rootNode, localClassNames, nativeUnitsByName, nativeIndex));
1064
+ }
1065
+ return unitsFromModuleLevel(rootNode, localClassNames, filePath, nativeIndex);
1066
+ }
1067
+
1068
+ // ── Cross-file dead-export / reference-site scanning ──────────────────
1069
+ // tree-sitter has no language-service `findReferencesAsNodes()` — this is a
1070
+ // real identifier-text walk over every cached parsed file (declaration's
1071
+ // own name-node occurrence excluded), not a semantic resolver. It will
1072
+ // count a same-named unrelated identifier in another file as a "reference"
1073
+ // where ts-morph's type-aware resolver would not — a real, disclosed
1074
+ // precision tradeoff of the syntactic approach, not a bug. See
1075
+ // VALIDATOR-ARCHITECTURE.md's parity write-up for the practical impact
1076
+ // (SOLID does not call either of these two functions, so it does not affect
1077
+ // the parity numbers reported there).
1078
+
1079
+ function exportedDeclarationsOf(rootNode) {
1080
+ const out = [];
1081
+ for (let i = 0; i < rootNode.namedChildCount; i++) {
1082
+ const top = rootNode.namedChild(i);
1083
+ if (top.type !== 'export_statement') continue;
1084
+ const decl = getDeclarationNode(top);
1085
+ if (decl) {
1086
+ if (decl.type === 'function_declaration' || decl.type === 'class_declaration'
1087
+ || decl.type === 'interface_declaration' || decl.type === 'type_alias_declaration') {
1088
+ const n = decl.childForFieldName('name');
1089
+ if (n) out.push({ name: n.text, line: lineOf(decl), kind: decl.type, nameNode: n });
1090
+ } else if (decl.type === 'lexical_declaration' || decl.type === 'variable_declaration') {
1091
+ for (const d of descendantsOfType(decl, ['variable_declarator'])) {
1092
+ const n = d.childForFieldName('name');
1093
+ if (n && n.type === 'identifier') out.push({ name: n.text, line: lineOf(d), kind: 'variable_declarator', nameNode: n });
1094
+ }
1095
+ }
1096
+ }
1097
+ // `export { a, b }` named-export-list form
1098
+ for (let j = 0; j < top.namedChildCount; j++) {
1099
+ const c = top.namedChild(j);
1100
+ if (c.type === 'export_clause') {
1101
+ for (let k = 0; k < c.namedChildCount; k++) {
1102
+ const spec = c.namedChild(k);
1103
+ if (spec.type === 'export_specifier') {
1104
+ const n = spec.childForFieldName('name');
1105
+ if (n) out.push({ name: n.text, line: lineOf(spec), kind: 'export_specifier', nameNode: n });
1106
+ }
1107
+ }
1108
+ }
1109
+ }
1110
+ }
1111
+ return out;
1112
+ }
1113
+
1114
+ function countReferencesAcrossFiles(name, declNode, declFilePath) {
1115
+ let count = 0;
1116
+ const files = [];
1117
+ for (const [path, entry] of fileCache.entries()) {
1118
+ walkSelfAndDescendants(entry.rootNode, (n) => {
1119
+ if (n.type !== 'identifier' && n.type !== 'type_identifier') return;
1120
+ if (n.text !== name) return;
1121
+ if (path === declFilePath && n.startIndex === declNode.startIndex) return; // exclude the declaration's own name occurrence
1122
+ count++;
1123
+ if (!files.includes(path)) files.push(path);
1124
+ });
1125
+ }
1126
+ return { count, files };
1127
+ }
1128
+
1129
+ /**
1130
+ * G9 export-usage half — cross-file identifier scan (see comment block
1131
+ * above). `projectFilePaths` MUST include every file a real usage could
1132
+ * live in; a name matching zero files in that set reports `referenceCount:
1133
+ * 0`, same as a name with no callers at all.
1134
+ */
1135
+ export function deadExportsOf(filePath, projectFilePaths = []) {
1136
+ ensureCached(new Set([filePath, ...projectFilePaths]));
1137
+ const entry = fileCache.get(filePath);
1138
+ if (!entry) return [];
1139
+ return exportedDeclarationsOf(entry.rootNode).map(({ name, line, kind, nameNode }) => {
1140
+ const { count } = countReferencesAcrossFiles(name, nameNode, filePath);
1141
+ return { name, line, referenceCount: count, kind };
1142
+ });
1143
+ }
1144
+
1145
+ /**
1146
+ * refactoring effort estimation's call-site-count / package-boundary
1147
+ * criteria — same cross-file identifier scan as deadExportsOf above,
1148
+ * targeted at ONE named export, also returning the deduplicated file list.
1149
+ */
1150
+ export function referenceSitesOf(filePath, exportName, projectFilePaths = []) {
1151
+ ensureCached(new Set([filePath, ...projectFilePaths]));
1152
+ const entry = fileCache.get(filePath);
1153
+ if (!entry) return { referenceCount: -1, files: [], kind: null };
1154
+ const exported = exportedDeclarationsOf(entry.rootNode).find((e) => e.name === exportName);
1155
+ if (!exported) return { referenceCount: -1, files: [], kind: null };
1156
+ const { count, files } = countReferencesAcrossFiles(exportName, exported.nameNode, filePath);
1157
+ return { referenceCount: count, files, kind: exported.kind };
1158
+ }
1159
+
1160
+ // ── Plugin surface ──────────────────────────────────────────────────────
1161
+
1162
+ export const treesitterPlugin = {
1163
+ id: 'tree-sitter',
1164
+ canHandle: (filePath) => /\.(mjs|ts|tsx|js|cjs)$/.test(filePath),
1165
+ extractUnits(filePath, sourceText) {
1166
+ const entry = cacheEntryFor(filePath, sourceText);
1167
+ return unitsFromParsedFile(entry.rootNode, filePath);
1168
+ },
1169
+ importsOf(filePath, sourceText) {
1170
+ const entry = cacheEntryFor(filePath, sourceText);
1171
+ const out = [];
1172
+ for (let i = 0; i < entry.rootNode.namedChildCount; i++) {
1173
+ const node = entry.rootNode.namedChild(i);
1174
+ if (node.type !== 'import_statement') continue;
1175
+ const source = node.childForFieldName('source');
1176
+ if (source) out.push(source.text.replace(/^['"]|['"]$/g, ''));
1177
+ }
1178
+ return out;
1179
+ },
1180
+ deadExportsOf,
1181
+ referenceSitesOf,
1182
+ };