@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,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 };
|