@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,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * xmlParser.ts — XML, BPMN and DMN extraction, without a tree-sitter grammar.
3
+ *
4
+ * WHY NOT tree-sitter
5
+ * -------------------
6
+ * The grammar set shipped here has no XML. The choice is therefore between
7
+ * adding a WASM grammar (a new binary artifact, a new load path, a new failure
8
+ * mode at boot) and tokenizing a format whose entire syntax is angle brackets
9
+ * and quoted attributes. XML is regular enough at the tag level that the second
10
+ * is smaller, has no runtime dependency, and cannot fail to load.
11
+ *
12
+ * The limits of that choice, stated rather than discovered later: this reads
13
+ * TAGS and ATTRIBUTES. It does not validate, resolve namespaces, expand
14
+ * entities, or parse DTDs. It is a structural index, not an XML processor, and
15
+ * anything that needs real XML semantics must not use it.
16
+ *
17
+ * WHY BPMN GETS ITS OWN TREATMENT
18
+ * -------------------------------
19
+ * A BPMN file is not decoration — it is a program, and it already has the exact
20
+ * shape the rest of this parser emits:
21
+ *
22
+ * `bpmn:process` → a unit, like a class
23
+ * `userTask` / `serviceTask` / … → members, like methods
24
+ * `sequenceFlow src → tgt` → a CALL edge, precisely
25
+ *
26
+ * Indexing one as generic XML would record 32 elements with ids and lose all 36
27
+ * edges between them — the part that makes it a program rather than a list. The
28
+ * repository's own onramp BPMN is 36 sequence flows over 32 flow nodes; as
29
+ * generic XML that graph is invisible.
30
+ *
31
+ * DETERMINISM: source order throughout, line numbers from the byte offset, no
32
+ * timestamps, no path resolution.
33
+ */
34
+ import type { ParsedCall, ParsedImport, ParsedInterface, ParsedSymbol } from './nativeParser.js';
35
+ import type { ParsedMember, ParsedUnit } from './memberFacts.js';
36
+ /** Languages this module handles, keyed by the caller's language string. */
37
+ export declare const XML_LANGUAGES: readonly ["xml", "bpmn", "dmn", "xsd", "svg"];
38
+ export declare function isXmlLanguage(language: string): boolean;
39
+ /** One tag occurrence, with its attributes and 1-based line. */
40
+ interface Tag {
41
+ /** Local name with any namespace prefix stripped. */
42
+ name: string;
43
+ /** Name exactly as written, prefix included. */
44
+ raw: string;
45
+ attrs: Record<string, string>;
46
+ line: number;
47
+ selfClosing: boolean;
48
+ closing: boolean;
49
+ }
50
+ /**
51
+ * Tokenize tags out of an XML document.
52
+ *
53
+ * Comments, CDATA, processing instructions and the prolog are skipped as spans
54
+ * rather than parsed — a `<` inside a comment is not a tag, and treating it as
55
+ * one is the classic way a regex "XML parser" invents structure that is not
56
+ * there.
57
+ */
58
+ export declare function tokenizeTags(content: string): Tag[];
59
+ export interface XmlExtraction {
60
+ symbols: ParsedSymbol[];
61
+ interfaces: ParsedInterface[];
62
+ calls: ParsedCall[];
63
+ imports: ParsedImport[];
64
+ members: ParsedMember[];
65
+ units: ParsedUnit[];
66
+ }
67
+ /**
68
+ * Decide which dialect a document is, from its CONTENT rather than its
69
+ * extension.
70
+ *
71
+ * The repository's own BPMN ships as `.bpmn20.xml`, so an extension test would
72
+ * classify the one real BPMN file here as generic XML and drop every edge in
73
+ * it. The root element is the thing that actually says what a document is.
74
+ */
75
+ export declare function detectXmlDialect(tags: Tag[], language: string): 'bpmn' | 'dmn' | 'xml';
76
+ export declare function extractXml(content: string, language: string): XmlExtraction;
77
+ export {};
@@ -0,0 +1,400 @@
1
+ /**
2
+ * xmlParser.ts — XML, BPMN and DMN extraction, without a tree-sitter grammar.
3
+ *
4
+ * WHY NOT tree-sitter
5
+ * -------------------
6
+ * The grammar set shipped here has no XML. The choice is therefore between
7
+ * adding a WASM grammar (a new binary artifact, a new load path, a new failure
8
+ * mode at boot) and tokenizing a format whose entire syntax is angle brackets
9
+ * and quoted attributes. XML is regular enough at the tag level that the second
10
+ * is smaller, has no runtime dependency, and cannot fail to load.
11
+ *
12
+ * The limits of that choice, stated rather than discovered later: this reads
13
+ * TAGS and ATTRIBUTES. It does not validate, resolve namespaces, expand
14
+ * entities, or parse DTDs. It is a structural index, not an XML processor, and
15
+ * anything that needs real XML semantics must not use it.
16
+ *
17
+ * WHY BPMN GETS ITS OWN TREATMENT
18
+ * -------------------------------
19
+ * A BPMN file is not decoration — it is a program, and it already has the exact
20
+ * shape the rest of this parser emits:
21
+ *
22
+ * `bpmn:process` → a unit, like a class
23
+ * `userTask` / `serviceTask` / … → members, like methods
24
+ * `sequenceFlow src → tgt` → a CALL edge, precisely
25
+ *
26
+ * Indexing one as generic XML would record 32 elements with ids and lose all 36
27
+ * edges between them — the part that makes it a program rather than a list. The
28
+ * repository's own onramp BPMN is 36 sequence flows over 32 flow nodes; as
29
+ * generic XML that graph is invisible.
30
+ *
31
+ * DETERMINISM: source order throughout, line numbers from the byte offset, no
32
+ * timestamps, no path resolution.
33
+ */
34
+ /** Languages this module handles, keyed by the caller's language string. */
35
+ export const XML_LANGUAGES = ['xml', 'bpmn', 'dmn', 'xsd', 'svg'];
36
+ export function isXmlLanguage(language) {
37
+ return XML_LANGUAGES.includes(language);
38
+ }
39
+ const ATTR_RE = /([\w:.-]+)\s*=\s*("([^"]*)"|'([^']*)')/g;
40
+ /**
41
+ * Tokenize tags out of an XML document.
42
+ *
43
+ * Comments, CDATA, processing instructions and the prolog are skipped as spans
44
+ * rather than parsed — a `<` inside a comment is not a tag, and treating it as
45
+ * one is the classic way a regex "XML parser" invents structure that is not
46
+ * there.
47
+ */
48
+ export function tokenizeTags(content) {
49
+ const tags = [];
50
+ // Precompute line starts once; per-tag line lookup is then a binary search
51
+ // instead of counting newlines from the top for every tag, which is what
52
+ // turns a large document from linear into quadratic.
53
+ const lineStarts = [0];
54
+ for (let i = 0; i < content.length; i++) {
55
+ if (content.charCodeAt(i) === 10)
56
+ lineStarts.push(i + 1);
57
+ }
58
+ const lineAt = (offset) => {
59
+ let lo = 0;
60
+ let hi = lineStarts.length - 1;
61
+ while (lo < hi) {
62
+ const mid = (lo + hi + 1) >> 1;
63
+ if (lineStarts[mid] <= offset)
64
+ lo = mid;
65
+ else
66
+ hi = mid - 1;
67
+ }
68
+ return lo + 1;
69
+ };
70
+ let i = 0;
71
+ while (i < content.length) {
72
+ const lt = content.indexOf('<', i);
73
+ if (lt === -1)
74
+ break;
75
+ if (content.startsWith('<!--', lt)) {
76
+ const end = content.indexOf('-->', lt + 4);
77
+ i = end === -1 ? content.length : end + 3;
78
+ continue;
79
+ }
80
+ if (content.startsWith('<![CDATA[', lt)) {
81
+ const end = content.indexOf(']]>', lt + 9);
82
+ i = end === -1 ? content.length : end + 3;
83
+ continue;
84
+ }
85
+ if (content.startsWith('<?', lt) || content.startsWith('<!', lt)) {
86
+ const end = content.indexOf('>', lt + 2);
87
+ i = end === -1 ? content.length : end + 1;
88
+ continue;
89
+ }
90
+ const gt = content.indexOf('>', lt);
91
+ if (gt === -1)
92
+ break;
93
+ const inner = content.slice(lt + 1, gt);
94
+ i = gt + 1;
95
+ if (inner.length === 0)
96
+ continue;
97
+ const closing = inner.startsWith('/');
98
+ const selfClosing = inner.endsWith('/');
99
+ const body = inner.replace(/^\//, '').replace(/\/$/, '');
100
+ const nameMatch = /^([\w:.-]+)/.exec(body);
101
+ if (!nameMatch)
102
+ continue;
103
+ const raw = nameMatch[1];
104
+ const attrs = {};
105
+ if (!closing) {
106
+ ATTR_RE.lastIndex = nameMatch[0].length;
107
+ for (let m = ATTR_RE.exec(body); m !== null; m = ATTR_RE.exec(body)) {
108
+ attrs[m[1]] = m[3] ?? m[4] ?? '';
109
+ }
110
+ }
111
+ tags.push({
112
+ name: raw.includes(':') ? raw.slice(raw.indexOf(':') + 1) : raw,
113
+ raw,
114
+ attrs,
115
+ line: lineAt(lt),
116
+ selfClosing,
117
+ closing,
118
+ });
119
+ }
120
+ return tags;
121
+ }
122
+ /** BPMN element names that are flow NODES — the members of a process. */
123
+ const BPMN_FLOW_NODES = new Set([
124
+ 'startEvent', 'endEvent', 'intermediateCatchEvent', 'intermediateThrowEvent',
125
+ 'boundaryEvent', 'task', 'userTask', 'serviceTask', 'scriptTask', 'manualTask',
126
+ 'businessRuleTask', 'sendTask', 'receiveTask', 'callActivity', 'subProcess',
127
+ 'transaction', 'exclusiveGateway', 'parallelGateway', 'inclusiveGateway',
128
+ 'eventBasedGateway', 'complexGateway',
129
+ ]);
130
+ /** BPMN element names that own a set of flow nodes — the units. */
131
+ const BPMN_UNITS = new Set(['process', 'subProcess', 'collaboration', 'transaction']);
132
+ /** DMN elements that behave as members and as edge endpoints. */
133
+ const DMN_NODES = new Set(['decision', 'inputData', 'businessKnowledgeModel', 'knowledgeSource']);
134
+ function emptyMember(name, owner, kind, line) {
135
+ return {
136
+ name,
137
+ owner,
138
+ kind,
139
+ exported: true,
140
+ isStatic: false,
141
+ start_line: line,
142
+ end_line: line,
143
+ return_type: null,
144
+ paramCount: 0,
145
+ declaredNames: [],
146
+ statementCount: 0,
147
+ statementTexts: [],
148
+ fieldAccess: [],
149
+ calleeNames: [],
150
+ deepChainCallCount: 0,
151
+ constructorNewCallTargets: [],
152
+ branchHits: 0,
153
+ switchStatements: [],
154
+ complexConditionals: [],
155
+ nullChecks: 0,
156
+ magicNumbers: [],
157
+ emptyCatches: 0,
158
+ deadConditionals: 0,
159
+ override: {
160
+ callsSuper: false,
161
+ baseClass: null,
162
+ baseParamCount: null,
163
+ paramCountDrift: null,
164
+ baseReturnType: null,
165
+ returnTypeDrift: null,
166
+ },
167
+ };
168
+ }
169
+ /**
170
+ * Decide which dialect a document is, from its CONTENT rather than its
171
+ * extension.
172
+ *
173
+ * The repository's own BPMN ships as `.bpmn20.xml`, so an extension test would
174
+ * classify the one real BPMN file here as generic XML and drop every edge in
175
+ * it. The root element is the thing that actually says what a document is.
176
+ */
177
+ export function detectXmlDialect(tags, language) {
178
+ if (language === 'bpmn')
179
+ return 'bpmn';
180
+ if (language === 'dmn')
181
+ return 'dmn';
182
+ for (const tag of tags) {
183
+ if (tag.closing)
184
+ continue;
185
+ if (tag.name === 'definitions') {
186
+ const ns = Object.entries(tag.attrs).find(([k]) => k.startsWith('xmlns'));
187
+ const value = ns?.[1] ?? '';
188
+ if (/BPMN/i.test(value) || tag.raw.startsWith('bpmn'))
189
+ return 'bpmn';
190
+ if (/DMN/i.test(value) || tag.raw.startsWith('dmn'))
191
+ return 'dmn';
192
+ }
193
+ if (tag.name === 'process' || tag.name === 'collaboration')
194
+ return 'bpmn';
195
+ if (tag.name === 'decision')
196
+ return 'dmn';
197
+ break;
198
+ }
199
+ return 'xml';
200
+ }
201
+ export function extractXml(content, language) {
202
+ const tags = tokenizeTags(content);
203
+ const dialect = detectXmlDialect(tags, language);
204
+ const symbols = [];
205
+ const interfaces = [];
206
+ const calls = [];
207
+ const imports = [];
208
+ const members = [];
209
+ const units = [];
210
+ // A flow node's id is what edges reference, but its NAME is what a human
211
+ // reads. Both are kept: symbols are indexed under the readable name, and this
212
+ // map resolves an edge's id reference back to it.
213
+ const idToName = new Map();
214
+ const unitStack = [];
215
+ // Read through the stack rather than mirroring its top in a separate
216
+ // variable: one source of truth for "which unit are we inside", and no way
217
+ // for the mirror to drift out of step with a push or pop.
218
+ const currentUnit = () => unitStack[unitStack.length - 1] ?? null;
219
+ // Namespace declarations are this format's imports — they say which
220
+ // vocabularies the document depends on, which is exactly what an import is.
221
+ for (const tag of tags) {
222
+ if (tag.closing)
223
+ continue;
224
+ for (const [key, value] of Object.entries(tag.attrs)) {
225
+ if (key === 'xmlns' || key.startsWith('xmlns:')) {
226
+ const alias = key === 'xmlns' ? '*' : key.slice('xmlns:'.length);
227
+ if (!imports.some(imp => imp.source === value)) {
228
+ imports.push({ source: value, specifiers: [alias], line: tag.line });
229
+ }
230
+ }
231
+ }
232
+ }
233
+ // Pass 1: ids → readable names, so an edge declared before its target still
234
+ // resolves. Source order cannot be relied on for references in XML.
235
+ for (const tag of tags) {
236
+ if (tag.closing)
237
+ continue;
238
+ const id = tag.attrs.id;
239
+ if (id)
240
+ idToName.set(id, tag.attrs.name || id);
241
+ }
242
+ /**
243
+ * `emitSymbol` is false when the caller has already recorded this element —
244
+ * the generic-XML branch indexes an element and THEN opens it as a unit, and
245
+ * pushing from both sites listed the document root twice.
246
+ */
247
+ const openUnit = (tag, emitSymbol = true) => {
248
+ const name = tag.attrs.id || tag.attrs.name || tag.name;
249
+ const unit = {
250
+ name,
251
+ kind: 'class',
252
+ exported: true,
253
+ start_line: tag.line,
254
+ end_line: tag.line,
255
+ baseClass: null,
256
+ hasBaseClass: false,
257
+ concreteInstantiations: 0,
258
+ totalDependencies: 0,
259
+ staticPropertyNames: [],
260
+ hasGetInstanceMethod: false,
261
+ memberNames: [],
262
+ };
263
+ units.push(unit);
264
+ if (emitSymbol) {
265
+ symbols.push({
266
+ name,
267
+ kind: dialect === 'bpmn' ? 'process' : 'element',
268
+ exported: true,
269
+ start_line: tag.line,
270
+ end_line: tag.line,
271
+ });
272
+ }
273
+ unitStack.push(unit);
274
+ };
275
+ const closeUnit = (tag) => {
276
+ const closed = unitStack.pop();
277
+ if (closed)
278
+ closed.end_line = tag.line;
279
+ };
280
+ for (const tag of tags) {
281
+ if (tag.closing) {
282
+ if ((dialect === 'bpmn' && BPMN_UNITS.has(tag.name))
283
+ || (dialect === 'dmn' && tag.name === 'definitions')
284
+ || (dialect === 'xml' && unitStack.length > 0 && unitStack[unitStack.length - 1].name === tag.name)) {
285
+ closeUnit(tag);
286
+ }
287
+ continue;
288
+ }
289
+ if (dialect === 'bpmn') {
290
+ if (BPMN_UNITS.has(tag.name)) {
291
+ // A subProcess is both a member of its parent and a unit of its own.
292
+ if (tag.name === 'subProcess' && currentUnit()) {
293
+ const memberName = tag.attrs.name || tag.attrs.id || tag.name;
294
+ members.push(emptyMember(memberName, currentUnit()?.name ?? null, 'method', tag.line));
295
+ currentUnit()?.memberNames.push(memberName);
296
+ }
297
+ if (!tag.selfClosing)
298
+ openUnit(tag);
299
+ continue;
300
+ }
301
+ if (BPMN_FLOW_NODES.has(tag.name)) {
302
+ const name = tag.attrs.name || tag.attrs.id || tag.name;
303
+ symbols.push({
304
+ name,
305
+ kind: tag.name,
306
+ exported: true,
307
+ start_line: tag.line,
308
+ end_line: tag.line,
309
+ });
310
+ const member = emptyMember(name, currentUnit()?.name ?? null, 'method', tag.line);
311
+ // A gateway IS a branch. Recording it as one lets the same complexity
312
+ // signal cover a process definition and the code it compiles to.
313
+ if (/Gateway$/.test(tag.name))
314
+ member.branchHits = 1;
315
+ members.push(member);
316
+ currentUnit()?.memberNames.push(name);
317
+ continue;
318
+ }
319
+ if (tag.name === 'sequenceFlow') {
320
+ const from = tag.attrs.sourceRef;
321
+ const to = tag.attrs.targetRef;
322
+ if (from && to) {
323
+ calls.push({
324
+ caller: idToName.get(from) ?? from,
325
+ callee: idToName.get(to) ?? to,
326
+ call_line: tag.line,
327
+ resolved: idToName.has(to),
328
+ resolution_confidence: idToName.has(to) ? 0.9 : 0.0,
329
+ arity: 0,
330
+ count: 1,
331
+ });
332
+ }
333
+ continue;
334
+ }
335
+ continue;
336
+ }
337
+ if (dialect === 'dmn') {
338
+ if (tag.name === 'definitions' && !tag.selfClosing) {
339
+ openUnit(tag);
340
+ continue;
341
+ }
342
+ if (DMN_NODES.has(tag.name)) {
343
+ const name = tag.attrs.name || tag.attrs.id || tag.name;
344
+ symbols.push({
345
+ name, kind: tag.name, exported: true, start_line: tag.line, end_line: tag.line,
346
+ });
347
+ members.push(emptyMember(name, currentUnit()?.name ?? null, 'method', tag.line));
348
+ currentUnit()?.memberNames.push(name);
349
+ continue;
350
+ }
351
+ // A requirement edge names its source in an href like `#decision_id`.
352
+ // There was a `continue` here for exactly these element names, which
353
+ // skipped the href read below and discarded every DMN edge in the
354
+ // document — the one thing this branch exists to capture.
355
+ const href = tag.attrs.href;
356
+ if (href?.startsWith('#')) {
357
+ const target = href.slice(1);
358
+ const owner = currentUnit()?.name;
359
+ if (owner) {
360
+ calls.push({
361
+ caller: owner,
362
+ callee: idToName.get(target) ?? target,
363
+ call_line: tag.line,
364
+ resolved: idToName.has(target),
365
+ resolution_confidence: idToName.has(target) ? 0.9 : 0.0,
366
+ arity: 0,
367
+ count: 1,
368
+ });
369
+ }
370
+ }
371
+ continue;
372
+ }
373
+ // Generic XML: an element carrying an id or a name is a thing worth
374
+ // indexing. An element carrying neither is structure, not content — the
375
+ // 30,000 anonymous `<g>` elements in an SVG are not symbols, and recording
376
+ // them would bury the ones that are.
377
+ const identity = tag.attrs.id || tag.attrs.name;
378
+ if (identity) {
379
+ symbols.push({
380
+ name: identity,
381
+ kind: tag.name,
382
+ exported: true,
383
+ start_line: tag.line,
384
+ end_line: tag.line,
385
+ });
386
+ if (unitStack.length === 0 && !tag.selfClosing) {
387
+ openUnit(tag, false);
388
+ }
389
+ else {
390
+ members.push(emptyMember(identity, currentUnit()?.name ?? null, 'method', tag.line));
391
+ currentUnit()?.memberNames.push(identity);
392
+ }
393
+ }
394
+ }
395
+ for (const unit of units) {
396
+ unit.totalDependencies = new Set(calls.filter(c => unit.memberNames.includes(c.caller)).map(c => c.callee)).size;
397
+ }
398
+ return { symbols, interfaces, calls, imports, members, units };
399
+ }
400
+ //# sourceMappingURL=xmlParser.js.map
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * package-metrics-cli — thin CLI wrapper over lib/package-metrics.mjs.
4
+ *
5
+ * Discovers sibling package directories under a monorepo `packages/` root
6
+ * (or a caller-supplied explicit list) and prints Ca/Ce/instability/
7
+ * abstractness/distance-from-main-sequence/cycles/zone for each.
8
+ *
9
+ * Usage:
10
+ * node package-metrics-cli.mjs <packagesRoot> [--format text|json]
11
+ * node package-metrics-cli.mjs --dirs <dir1,dir2,...> [--format text|json]
12
+ *
13
+ * Exit code is always 0 — this is a reporting tool, not a gate. A future
14
+ * `--fail-on zone-of-pain,adp` could add gating; not built tonight because
15
+ * nothing asked for it and a fabricated gate threshold would be exactly the
16
+ * kind of number this whole file exists to avoid inventing.
17
+ */
18
+
19
+ import { readdirSync, statSync, existsSync } from 'node:fs';
20
+ import path from 'node:path';
21
+
22
+ import { packageMetricsAll } from './lib/package-metrics.mjs';
23
+
24
+ function parseArgs(argv) {
25
+ const args = { format: 'text', root: null, dirs: null };
26
+ const rest = [];
27
+ for (let i = 0; i < argv.length; i++) {
28
+ const a = argv[i];
29
+ if (a === '--format') args.format = argv[++i];
30
+ else if (a === '--dirs') args.dirs = argv[++i].split(',').map((s) => s.trim()).filter(Boolean);
31
+ else rest.push(a);
32
+ }
33
+ if (rest.length) args.root = rest[0];
34
+ return args;
35
+ }
36
+
37
+ function discoverPackageDirs(root) {
38
+ if (!existsSync(root)) throw new Error(`packages root does not exist: ${root}`);
39
+ return readdirSync(root)
40
+ .map((name) => path.join(root, name))
41
+ .filter((p) => {
42
+ try {
43
+ return statSync(p).isDirectory();
44
+ } catch {
45
+ return false;
46
+ }
47
+ });
48
+ }
49
+
50
+ function fmtNum(n, digits = 3) {
51
+ return n === null ? 'null' : n.toFixed(digits);
52
+ }
53
+
54
+ function printText(results) {
55
+ const rows = results.map((r) => ({
56
+ name: r.name,
57
+ ca: String(r.ca),
58
+ ce: String(r.ce),
59
+ I: fmtNum(r.instability),
60
+ A: fmtNum(r.abstractness),
61
+ basis: r.abstractnessBasis,
62
+ D: fmtNum(r.distanceFromMainSequence),
63
+ zone: r.zone,
64
+ cycles: r.cycles.length ? r.cycles.map((c) => c.join(' -> ')).join(' | ') : '-',
65
+ }));
66
+ const cols = ['name', 'ca', 'ce', 'I', 'A', 'basis', 'D', 'zone', 'cycles'];
67
+ const widths = Object.fromEntries(
68
+ cols.map((c) => [c, Math.max(c.length, ...rows.map((r) => String(r[c]).length))]),
69
+ );
70
+ const line = (r) => cols.map((c) => String(r[c]).padEnd(widths[c])).join(' ');
71
+ console.log(line(Object.fromEntries(cols.map((c) => [c, c]))));
72
+ console.log(cols.map((c) => '-'.repeat(widths[c])).join(' '));
73
+ for (const r of rows) console.log(line(r));
74
+
75
+ const cyclesFound = results.filter((r) => r.cycles.length);
76
+ if (cyclesFound.length) {
77
+ console.log('');
78
+ console.log('ADP violations (real cycle paths):');
79
+ for (const r of cyclesFound) {
80
+ for (const c of r.cycles) console.log(` ${c.join(' -> ')}`);
81
+ }
82
+ }
83
+ }
84
+
85
+ async function main() {
86
+ const args = parseArgs(process.argv.slice(2));
87
+ let dirs;
88
+ if (args.dirs) dirs = args.dirs.map((d) => path.resolve(d));
89
+ else if (args.root) dirs = discoverPackageDirs(path.resolve(args.root));
90
+ else {
91
+ console.error('Usage: node package-metrics-cli.mjs <packagesRoot> [--format text|json]');
92
+ console.error(' node package-metrics-cli.mjs --dirs <dir1,dir2,...> [--format text|json]');
93
+ process.exit(1);
94
+ }
95
+
96
+ const results = packageMetricsAll(dirs);
97
+ if (args.format === 'json') {
98
+ console.log(JSON.stringify(results, null, 2));
99
+ } else {
100
+ printText(results);
101
+ }
102
+ }
103
+
104
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1'));
105
+ if (isMain) {
106
+ main().catch((err) => {
107
+ console.error(err.stack || String(err));
108
+ process.exit(1);
109
+ });
110
+ }
111
+
112
+ export { discoverPackageDirs, parseArgs };