@untangle-code/complexity 0.0.0-stage → 0.1.1

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 (105) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +90 -0
  3. package/README.md +235 -2
  4. package/THIRD_PARTY_LICENSES.md +24 -0
  5. package/dist/aggregator.d.ts +28 -0
  6. package/dist/aggregator.d.ts.map +1 -0
  7. package/dist/aggregator.js +77 -0
  8. package/dist/aggregator.js.map +1 -0
  9. package/dist/assemble.d.ts +50 -0
  10. package/dist/assemble.d.ts.map +1 -0
  11. package/dist/assemble.js +43 -0
  12. package/dist/assemble.js.map +1 -0
  13. package/dist/cognitive.d.ts +62 -0
  14. package/dist/cognitive.d.ts.map +1 -0
  15. package/dist/cognitive.js +62 -0
  16. package/dist/cognitive.js.map +1 -0
  17. package/dist/cyclomatic.d.ts +29 -0
  18. package/dist/cyclomatic.d.ts.map +1 -0
  19. package/dist/cyclomatic.js +52 -0
  20. package/dist/cyclomatic.js.map +1 -0
  21. package/dist/entropy.d.ts +5 -0
  22. package/dist/entropy.d.ts.map +1 -0
  23. package/dist/entropy.js +29 -0
  24. package/dist/entropy.js.map +1 -0
  25. package/dist/explain.d.ts +22 -0
  26. package/dist/explain.d.ts.map +1 -0
  27. package/dist/explain.js +35 -0
  28. package/dist/explain.js.map +1 -0
  29. package/dist/halstead.d.ts +23 -0
  30. package/dist/halstead.d.ts.map +1 -0
  31. package/dist/halstead.js +24 -0
  32. package/dist/halstead.js.map +1 -0
  33. package/dist/index.d.ts +35 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +34 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/ir/build-ir.d.ts +105 -0
  38. package/dist/ir/build-ir.d.ts.map +1 -0
  39. package/dist/ir/build-ir.js +267 -0
  40. package/dist/ir/build-ir.js.map +1 -0
  41. package/dist/maintainability.d.ts +15 -0
  42. package/dist/maintainability.d.ts.map +1 -0
  43. package/dist/maintainability.js +10 -0
  44. package/dist/maintainability.js.map +1 -0
  45. package/dist/node-kind-heuristics.d.ts +24 -0
  46. package/dist/node-kind-heuristics.d.ts.map +1 -0
  47. package/dist/node-kind-heuristics.js +37 -0
  48. package/dist/node-kind-heuristics.js.map +1 -0
  49. package/dist/prose/metrics.d.ts +81 -0
  50. package/dist/prose/metrics.d.ts.map +1 -0
  51. package/dist/prose/metrics.js +103 -0
  52. package/dist/prose/metrics.js.map +1 -0
  53. package/dist/scope-aggregate.d.ts +229 -0
  54. package/dist/scope-aggregate.d.ts.map +1 -0
  55. package/dist/scope-aggregate.js +588 -0
  56. package/dist/scope-aggregate.js.map +1 -0
  57. package/dist/scope-identity.d.ts +85 -0
  58. package/dist/scope-identity.d.ts.map +1 -0
  59. package/dist/scope-identity.js +144 -0
  60. package/dist/scope-identity.js.map +1 -0
  61. package/dist/scope.d.ts +36 -0
  62. package/dist/scope.d.ts.map +1 -0
  63. package/dist/scope.js +60 -0
  64. package/dist/scope.js.map +1 -0
  65. package/dist/structural.d.ts +30 -0
  66. package/dist/structural.d.ts.map +1 -0
  67. package/dist/structural.js +98 -0
  68. package/dist/structural.js.map +1 -0
  69. package/dist/syntactic.d.ts +35 -0
  70. package/dist/syntactic.d.ts.map +1 -0
  71. package/dist/syntactic.js +143 -0
  72. package/dist/syntactic.js.map +1 -0
  73. package/dist/text-lines.d.ts +34 -0
  74. package/dist/text-lines.d.ts.map +1 -0
  75. package/dist/text-lines.js +60 -0
  76. package/dist/text-lines.js.map +1 -0
  77. package/dist/textual.d.ts +28 -0
  78. package/dist/textual.d.ts.map +1 -0
  79. package/dist/textual.js +75 -0
  80. package/dist/textual.js.map +1 -0
  81. package/dist/types.d.ts +112 -0
  82. package/dist/types.d.ts.map +1 -0
  83. package/dist/types.js +2 -0
  84. package/dist/types.js.map +1 -0
  85. package/package.json +60 -3
  86. package/src/aggregator.ts +105 -0
  87. package/src/assemble.ts +109 -0
  88. package/src/cognitive.ts +127 -0
  89. package/src/cyclomatic.ts +78 -0
  90. package/src/entropy.ts +30 -0
  91. package/src/explain.ts +64 -0
  92. package/src/halstead.ts +54 -0
  93. package/src/index.ts +85 -0
  94. package/src/ir/build-ir.ts +411 -0
  95. package/src/maintainability.ts +29 -0
  96. package/src/node-kind-heuristics.ts +55 -0
  97. package/src/prose/metrics.ts +201 -0
  98. package/src/scope-aggregate.ts +775 -0
  99. package/src/scope-identity.ts +240 -0
  100. package/src/scope.ts +77 -0
  101. package/src/structural.ts +132 -0
  102. package/src/syntactic.ts +183 -0
  103. package/src/text-lines.ts +90 -0
  104. package/src/textual.ts +119 -0
  105. package/src/types.ts +121 -0
@@ -0,0 +1,143 @@
1
+ import { frequenciesOf, shannonEntropy } from "./entropy.js";
2
+ import { classifyNodeKind, isAsyncNodeKind, isExceptionNodeKind } from "./node-kind-heuristics.js";
3
+ import { rangeKey } from "./scope.js";
4
+ function mean(values) {
5
+ if (values.length === 0)
6
+ return 0;
7
+ return values.reduce((s, v) => s + v, 0) / values.length;
8
+ }
9
+ /**
10
+ * Exact per-node child counts from a DFS pre-order depth sequence (see the
11
+ * module doc): a stack of "open" ancestor indices is maintained; each new
12
+ * node closes every open node with depth >= its own, and the remaining top
13
+ * (nearest preceding smaller depth) is its parent. Works on scope-filtered
14
+ * sequences too — a scope's facts form contiguous pre-order fragments whose
15
+ * missing outer ancestors simply make their subtree roots forest roots, and
16
+ * child counts then cover exactly the scope's OWN nodes, consistent with
17
+ * every other metric computed from scope-narrowed facts.
18
+ */
19
+ function computeBranchingFromPreorderDepths(nodeKinds) {
20
+ const childCounts = new Array(nodeKinds.length).fill(0);
21
+ const openAncestors = [];
22
+ for (let i = 0; i < nodeKinds.length; i += 1) {
23
+ const depth = nodeKinds[i].depth;
24
+ while (openAncestors.length > 0 &&
25
+ nodeKinds[openAncestors[openAncestors.length - 1]].depth >= depth) {
26
+ openAncestors.pop();
27
+ }
28
+ const parentIndex = openAncestors[openAncestors.length - 1];
29
+ if (parentIndex !== undefined) {
30
+ childCounts[parentIndex] = childCounts[parentIndex] + 1;
31
+ }
32
+ openAncestors.push(i);
33
+ }
34
+ let max = 0;
35
+ for (const count of childCounts) {
36
+ if (count > max)
37
+ max = count;
38
+ }
39
+ return { avg: mean(childCounts), max };
40
+ }
41
+ function computeBranching(root) {
42
+ const branchCounts = [];
43
+ const stack = [root];
44
+ while (stack.length > 0) {
45
+ const node = stack.pop();
46
+ if (!node)
47
+ continue;
48
+ branchCounts.push(node.children.length);
49
+ for (const child of node.children)
50
+ stack.push(child);
51
+ }
52
+ return {
53
+ avg: mean(branchCounts),
54
+ max: branchCounts.length > 0 ? Math.max(...branchCounts) : 0,
55
+ };
56
+ }
57
+ /**
58
+ * @param facts Facts already narrowed to the target scope.
59
+ * @param sloc The scope's own SLOC, as computed by `computeStructural` —
60
+ * passed in rather than recomputed so `ast_nodes_per_sloc` is consistent
61
+ * with the structural snapshot instead of a second, possibly divergent,
62
+ * line-scan.
63
+ * @param irRoot Optional IR subtree for the same scope; only used as the
64
+ * branching-factor source when `facts.nodeKinds` is empty (the pre-order
65
+ * reconstruction over real node-kind facts is exact and takes precedence).
66
+ */
67
+ export function computeSyntactic(facts, sloc, irRoot, algorithmVersion = "untangle-syntactic-v1") {
68
+ const depths = facts.nodeKinds.map((n) => n.depth);
69
+ const astNodeCount = facts.nodeKinds.length;
70
+ const controlFlowKeys = new Set();
71
+ for (const d of facts.decisions)
72
+ controlFlowKeys.add(rangeKey(d.range));
73
+ for (const n of facts.nestingEvents)
74
+ controlFlowKeys.add(rangeKey(n.range));
75
+ const controlFlowNodeCount = controlFlowKeys.size;
76
+ // Ranged exception constructs (catch clauses) are deduplicated by range,
77
+ // since adapters may emit both a DecisionFact and a NestingFact for the
78
+ // same `catch`. Node-kind-only signals (e.g. a bare `throw_statement`,
79
+ // which carries no dedicated fact type) have no range to dedupe by and are
80
+ // simply counted in addition.
81
+ const exceptionRangeKeys = new Set();
82
+ for (const n of facts.nestingEvents) {
83
+ if (n.construct === "catch")
84
+ exceptionRangeKeys.add(rangeKey(n.range));
85
+ }
86
+ for (const d of facts.decisions) {
87
+ if (d.kind === "catch")
88
+ exceptionRangeKeys.add(rangeKey(d.range));
89
+ }
90
+ const throwLikeNodeCount = facts.nodeKinds.filter((n) => isExceptionNodeKind(n.kind) && !n.kind.toLowerCase().includes("catch")).length;
91
+ const exceptionConstructCount = exceptionRangeKeys.size + throwLikeNodeCount;
92
+ const nodeKindFrequencies = frequenciesOf(facts.nodeKinds, (n) => n.kind);
93
+ const metrics = {
94
+ ast_node_count: astNodeCount,
95
+ ast_max_depth: depths.length > 0 ? Math.max(...depths) : 0,
96
+ ast_avg_depth: mean(depths),
97
+ statement_count: facts.nodeKinds.filter((n) => classifyNodeKind(n.kind) === "statement").length,
98
+ expression_count: facts.nodeKinds.filter((n) => classifyNodeKind(n.kind) === "expression")
99
+ .length,
100
+ declaration_count: facts.nodeKinds.filter((n) => classifyNodeKind(n.kind) === "declaration")
101
+ .length,
102
+ control_flow_node_count: controlFlowNodeCount,
103
+ control_flow_density: astNodeCount > 0 ? controlFlowNodeCount / astNodeCount : 0,
104
+ lambda_count: facts.nestingEvents.filter((n) => n.construct === "lambda").length,
105
+ async_construct_count: facts.nodeKinds.filter((n) => isAsyncNodeKind(n.kind)).length,
106
+ exception_construct_count: exceptionConstructCount,
107
+ pattern_match_count: facts.decisions.filter((d) => d.kind === "pattern_alternative").length,
108
+ node_kind_entropy: shannonEntropy(nodeKindFrequencies.values()),
109
+ ast_nodes_per_sloc: astNodeCount / Math.max(sloc, 1),
110
+ parse_error_node_count: facts.parseDiagnostics.filter((d) => d.kind === "error_node").length,
111
+ missing_node_count: facts.parseDiagnostics.filter((d) => d.kind === "missing_node").length,
112
+ // WP-185: DIE ZWEI ZAHLEN OBEN SIND NICHT DIE GANZE WAHRHEIT.
113
+ //
114
+ // `ParseDiagnostic.kind` kennt vier Werte (`error_node`, `missing_node`,
115
+ // `fallback`, `other`). Die beiden Kennzahlen oben zaehlen genau zwei
116
+ // davon — und ein Adapter, dessen Parser keine tree-sitter-Fehlerknoten
117
+ // kennt, meldet seine Syntaxfehler als `"other"`. Gemessen am eigenen
118
+ // Python-Adapter: eine Datei mit `parseQuality: "partial"` und
119
+ // `ParseResult.errorNodeCount: 5` erzeugt fuenf Diagnosen der Art
120
+ // `"other"` — und `parse_error_node_count` stand trotzdem auf 0. Zwei
121
+ // Zeugen desselben Laufs widersprachen einander, und der, der das Haus
122
+ // verlaesst, war der falsche. Wer 0 liest, schliesst auf einen heilen
123
+ // Baum; das ist die Sorte stiller Fehlaussage, die E4 verbietet.
124
+ //
125
+ // Die beiden vorhandenen Kennzahlen bleiben unveraendert — sie bedeuten
126
+ // weiterhin genau, was ihr Name sagt. Ergaenzt werden zwei, die den Rest
127
+ // sichtbar machen, statt ihn unter einer 0 verschwinden zu lassen.
128
+ parse_diagnostic_count: facts.parseDiagnostics.length,
129
+ unclassified_parse_error_count: facts.parseDiagnostics.filter((d) => d.severity === "error" && d.kind !== "error_node" && d.kind !== "missing_node").length,
130
+ };
131
+ if (facts.nodeKinds.length > 0) {
132
+ const branching = computeBranchingFromPreorderDepths(facts.nodeKinds);
133
+ metrics["ast_branching_factor_avg"] = branching.avg;
134
+ metrics["ast_branching_factor_max"] = branching.max;
135
+ }
136
+ else if (irRoot) {
137
+ const branching = computeBranching(irRoot);
138
+ metrics["ast_branching_factor_avg"] = branching.avg;
139
+ metrics["ast_branching_factor_max"] = branching.max;
140
+ }
141
+ return { metrics, algorithmVersion };
142
+ }
143
+ //# sourceMappingURL=syntactic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"syntactic.js","sourceRoot":"","sources":["../src/syntactic.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Shared, pure line/comment classification used by both StructuralCalculator
3
+ * and TextualCalculator (spec.md §16.5, §16.6). Not a calculator itself —
4
+ * just the byte/line bookkeeping both of them need, factored out so neither
5
+ * calculator duplicates it.
6
+ */
7
+ import type { ComplexityFacts, SourceRange } from "@untangle-code/domain";
8
+ export interface LineClassification {
9
+ /** Physical line count of the scope (`endLine - startLine + 1`, or 0 for an empty range). */
10
+ physicalLoc: number;
11
+ /** Set of absolute physical line numbers touched by any comment range. */
12
+ commentLines: Set<number>;
13
+ /** Subset of `commentLines` whose owning comment looks like a doc comment. */
14
+ docLines: Set<number>;
15
+ /** Absolute line numbers that are blank (whitespace-only) and not comment lines. */
16
+ blankLines: Set<number>;
17
+ }
18
+ /**
19
+ * Classifies every physical line of `scopeRange` within `sourceText` as
20
+ * blank, comment, and/or doc-comment. `comments` is expected already
21
+ * narrowed to the scope (e.g. via `selectSymbolFacts`).
22
+ *
23
+ * Documented simplification (README "Entscheidungen" #3): a line touched by
24
+ * *any part* of a comment range counts as a comment line in full, even for a
25
+ * trailing `code(); // comment` line — `ComplexityFacts` gives byte ranges,
26
+ * not a column split between code and comment, so a line can't be split
27
+ * further without raw-text heuristics beyond this package's scope.
28
+ */
29
+ export declare function classifyLines(sourceText: string, scopeRange: SourceRange, comments: readonly SourceRange[]): LineClassification;
30
+ /** Re-exported for calculators that need to iterate raw scope lines directly. */
31
+ export declare function scopeLines(sourceText: string, scopeRange: SourceRange): string[];
32
+ /** Convenience: comments already narrowed to one scope (see `selectSymbolFacts`). */
33
+ export declare function commentsOf(facts: ComplexityFacts): readonly SourceRange[];
34
+ //# sourceMappingURL=text-lines.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-lines.d.ts","sourceRoot":"","sources":["../src/text-lines.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAY1E,MAAM,WAAW,kBAAkB;IACjC,6FAA6F;IAC7F,WAAW,EAAE,MAAM,CAAC;IACpB,0EAA0E;IAC1E,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1B,8EAA8E;IAC9E,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACtB,oFAAoF;IACpF,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,WAAW,EACvB,QAAQ,EAAE,SAAS,WAAW,EAAE,GAC/B,kBAAkB,CA+BpB;AAED,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,WAAW,GAAG,MAAM,EAAE,CAMhF;AAED,qFAAqF;AACrF,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,SAAS,WAAW,EAAE,CAEzE"}
@@ -0,0 +1,60 @@
1
+ import { sliceByRange, splitLines } from "./scope.js";
2
+ /** Leading markers recognized as "doc comment" (Untangle decision — see README "Entscheidungen" #3). */
3
+ const DOC_COMMENT_MARKERS = ["/**", "///", '"""', "'''", "##!", "//!", "/// "];
4
+ function isDocCommentText(commentText) {
5
+ const trimmed = commentText.trimStart();
6
+ return DOC_COMMENT_MARKERS.some((marker) => trimmed.startsWith(marker));
7
+ }
8
+ /**
9
+ * Classifies every physical line of `scopeRange` within `sourceText` as
10
+ * blank, comment, and/or doc-comment. `comments` is expected already
11
+ * narrowed to the scope (e.g. via `selectSymbolFacts`).
12
+ *
13
+ * Documented simplification (README "Entscheidungen" #3): a line touched by
14
+ * *any part* of a comment range counts as a comment line in full, even for a
15
+ * trailing `code(); // comment` line — `ComplexityFacts` gives byte ranges,
16
+ * not a column split between code and comment, so a line can't be split
17
+ * further without raw-text heuristics beyond this package's scope.
18
+ */
19
+ export function classifyLines(sourceText, scopeRange, comments) {
20
+ const scopeText = sliceByRange(sourceText, scopeRange);
21
+ const lines = splitLines(scopeText);
22
+ // A trailing newline at the end of the range produces one extra empty
23
+ // element from split("\n"); drop it so physicalLoc reflects real lines.
24
+ const physicalLines = lines.length > 1 && lines[lines.length - 1] === "" && scopeText.endsWith("\n")
25
+ ? lines.slice(0, -1)
26
+ : lines;
27
+ const physicalLoc = physicalLines.length;
28
+ const commentLines = new Set();
29
+ const docLines = new Set();
30
+ for (const range of comments) {
31
+ const isDoc = isDocCommentText(sliceByRange(sourceText, range));
32
+ for (let line = range.startLine; line <= range.endLine; line++) {
33
+ commentLines.add(line);
34
+ if (isDoc)
35
+ docLines.add(line);
36
+ }
37
+ }
38
+ const blankLines = new Set();
39
+ for (let i = 0; i < physicalLines.length; i++) {
40
+ const lineNo = scopeRange.startLine + i;
41
+ const text = physicalLines[i] ?? "";
42
+ if (text.trim().length === 0 && !commentLines.has(lineNo)) {
43
+ blankLines.add(lineNo);
44
+ }
45
+ }
46
+ return { physicalLoc, commentLines, docLines, blankLines };
47
+ }
48
+ /** Re-exported for calculators that need to iterate raw scope lines directly. */
49
+ export function scopeLines(sourceText, scopeRange) {
50
+ const scopeText = sliceByRange(sourceText, scopeRange);
51
+ const lines = splitLines(scopeText);
52
+ return lines.length > 1 && lines[lines.length - 1] === "" && scopeText.endsWith("\n")
53
+ ? lines.slice(0, -1)
54
+ : lines;
55
+ }
56
+ /** Convenience: comments already narrowed to one scope (see `selectSymbolFacts`). */
57
+ export function commentsOf(facts) {
58
+ return facts.comments;
59
+ }
60
+ //# sourceMappingURL=text-lines.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-lines.js","sourceRoot":"","sources":["../src/text-lines.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEtD,wGAAwG;AACxG,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;AAE/E,SAAS,gBAAgB,CAAC,WAAmB;IAC3C,MAAM,OAAO,GAAG,WAAW,CAAC,SAAS,EAAE,CAAC;IACxC,OAAO,mBAAmB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;AAC1E,CAAC;AAaD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa,CAC3B,UAAkB,EAClB,UAAuB,EACvB,QAAgC;IAEhC,MAAM,SAAS,GAAG,YAAY,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;IACvD,MAAM,KAAK,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;IACpC,sEAAsE;IACtE,wEAAwE;IACxE,MAAM,aAAa,GACjB,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,IAAI,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC;QAC5E,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACpB,CAAC,CAAC,KAAK,CAAC;IACZ,MAAM,WAAW,GAAG,aAAa,CAAC,MAAM,CAAC;IAEzC,MAAM,YAAY,GAAG,IAAI,GAAG,EAAU,CAAC;IACvC,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IACnC,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,gBAAgB,CAAC,YAAY,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC;QAChE,KAAK,IAAI,IAAI,GAAG,KAAK,CAAC,SAAS,EAAE,IAAI,IAAI,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC;YAC/D,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACvB,IAAI,KAAK;gBAAE,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAChC,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU,CAAC;IACrC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,aAAa,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC9C,MAAM,MAAM,GAAG,UAAU,CAAC,SAAS,GAAG,CAAC,CAAC;QACxC,MAAM,IAAI,GAAG,aAAa,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACpC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YAC1D,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACzB,CAAC;IACH,CAAC;IAED,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;AAC7D,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,UAAU,CAAC,UAAkB,EAAE,UAAuB;IACpE,MAAM,SAAS,GAAG,YAAY,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;IACvD,MAAM,KAAK,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;IACpC,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,IAAI,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC;QACnF,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACpB,CAAC,CAAC,KAAK,CAAC;AACZ,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,UAAU,CAAC,KAAsB;IAC/C,OAAO,KAAK,CAAC,QAAQ,CAAC;AACxB,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * TextualCalculator (spec.md §16.6).
3
+ *
4
+ * `identifier_entropy`: Shannon entropy in bits computed over the multiset of
5
+ * *characters* across all identifier occurrences in scope (not per-token
6
+ * entropy over distinct identifier names) — Untangle decision, documented in
7
+ * README "Entscheidungen" #6: character-level entropy reacts to naming style
8
+ * (e.g. heavy abbreviation vs. verbose camelCase) rather than being dominated
9
+ * by how many times short common names like `i`/`x` repeat.
10
+ *
11
+ * `magic_literal_count`: counts `number`/`string` literals only (booleans,
12
+ * `null`, regexes, templates are never "magic" by definition here), excluding
13
+ * a small, documented allowlist (see `EXCLUDED_NUMERIC_LITERALS`/
14
+ * `EXCLUDED_STRING_LITERALS` below).
15
+ *
16
+ * `todo_count`/`fixme_count`/`hack_count`: case-insensitive whole-word
17
+ * occurrence counts of `TODO`/`FIXME`/`HACK` inside comment text, sliced from
18
+ * `sourceText` via each comment's byte range.
19
+ */
20
+ import type { ComplexityFacts, SourceRange } from "@untangle-code/domain";
21
+ import type { MetricsResult } from "./types.js";
22
+ /**
23
+ * @param facts Facts already narrowed to the target scope.
24
+ * @param sourceText Full original file content (UTF-8).
25
+ * @param scopeRange The scope's own range.
26
+ */
27
+ export declare function computeTextual(facts: ComplexityFacts, sourceText: string, scopeRange: SourceRange, algorithmVersion?: string): MetricsResult;
28
+ //# sourceMappingURL=textual.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"textual.d.ts","sourceRoot":"","sources":["../src/textual.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAK1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAqBhD;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,eAAe,EACtB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,WAAW,EACvB,gBAAgB,SAAwB,GACvC,aAAa,CA+Df"}
@@ -0,0 +1,75 @@
1
+ import { frequenciesOf, shannonEntropy } from "./entropy.js";
2
+ import { sliceByRange } from "./scope.js";
3
+ import { classifyLines, scopeLines } from "./text-lines.js";
4
+ const EXCLUDED_NUMERIC_LITERALS = new Set(["0", "1", "-1"]);
5
+ const EXCLUDED_STRING_LITERALS = new Set(['""', "''", "``", ""]);
6
+ function isMagicLiteral(literalKind, raw) {
7
+ const trimmed = raw.trim();
8
+ if (literalKind === "number")
9
+ return !EXCLUDED_NUMERIC_LITERALS.has(trimmed);
10
+ if (literalKind === "string")
11
+ return !EXCLUDED_STRING_LITERALS.has(trimmed);
12
+ return false;
13
+ }
14
+ function countWholeWordOccurrences(text, word) {
15
+ const pattern = new RegExp(`\\b${word}\\b`, "gi");
16
+ return text.match(pattern)?.length ?? 0;
17
+ }
18
+ function codepointLength(text) {
19
+ return Array.from(text).length;
20
+ }
21
+ /**
22
+ * @param facts Facts already narrowed to the target scope.
23
+ * @param sourceText Full original file content (UTF-8).
24
+ * @param scopeRange The scope's own range.
25
+ */
26
+ export function computeTextual(facts, sourceText, scopeRange, algorithmVersion = "untangle-textual-v1") {
27
+ const scopeText = sliceByRange(sourceText, scopeRange);
28
+ const { physicalLoc, commentLines, docLines } = classifyLines(sourceText, scopeRange, facts.comments);
29
+ const lines = scopeLines(sourceText, scopeRange);
30
+ const lineLengths = lines.map(codepointLength);
31
+ const avgLineLength = lineLengths.length > 0 ? lineLengths.reduce((s, v) => s + v, 0) / lineLengths.length : 0;
32
+ const maxLineLength = lineLengths.length > 0 ? Math.max(...lineLengths) : 0;
33
+ const identifierNames = facts.identifiers.map((id) => id.name);
34
+ const uniqueIdentifiers = new Set(identifierNames);
35
+ const identifierLengths = identifierNames.map(codepointLength);
36
+ const avgIdentifierLength = identifierLengths.length > 0
37
+ ? identifierLengths.reduce((s, v) => s + v, 0) / identifierLengths.length
38
+ : 0;
39
+ const identifierCharFrequencies = frequenciesOf(identifierNames.join("").split(""), (ch) => ch);
40
+ const magicLiteralCount = facts.literals.filter((lit) => isMagicLiteral(lit.literalKind, lit.raw)).length;
41
+ let todoCount = 0;
42
+ let fixmeCount = 0;
43
+ let hackCount = 0;
44
+ for (const commentRange of facts.comments) {
45
+ const text = sliceByRange(sourceText, commentRange);
46
+ todoCount += countWholeWordOccurrences(text, "todo");
47
+ fixmeCount += countWholeWordOccurrences(text, "fixme");
48
+ hackCount += countWholeWordOccurrences(text, "hack");
49
+ }
50
+ const operatorTexts = facts.operators.map((t) => t.text);
51
+ const operandTexts = facts.operands.map((t) => t.text);
52
+ const metrics = {
53
+ character_count: codepointLength(scopeText),
54
+ byte_count: scopeRange.endByte - scopeRange.startByte,
55
+ token_count: operatorTexts.length + operandTexts.length,
56
+ avg_line_length: avgLineLength,
57
+ max_line_length: maxLineLength,
58
+ comment_ratio: physicalLoc > 0 ? commentLines.size / physicalLoc : 0,
59
+ doc_ratio: physicalLoc > 0 ? docLines.size / physicalLoc : 0,
60
+ identifier_count: identifierNames.length,
61
+ unique_identifier_count: uniqueIdentifiers.size,
62
+ avg_identifier_length: avgIdentifierLength,
63
+ identifier_entropy: shannonEntropy(identifierCharFrequencies.values()),
64
+ magic_literal_count: magicLiteralCount,
65
+ todo_count: todoCount,
66
+ fixme_count: fixmeCount,
67
+ hack_count: hackCount,
68
+ operator_count: operatorTexts.length,
69
+ operand_count: operandTexts.length,
70
+ unique_operator_count: new Set(operatorTexts).size,
71
+ unique_operand_count: new Set(operandTexts).size,
72
+ };
73
+ return { metrics, algorithmVersion };
74
+ }
75
+ //# sourceMappingURL=textual.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"textual.js","sourceRoot":"","sources":["../src/textual.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Local result contracts for the complexity calculators (spec.md §16,
3
+ * architecture.md §15.2). These are NOT domain types: domain fixes the
4
+ * *stored* shapes (`ComplexitySnapshotV1`, `ComplexitySummaryV1`,
5
+ * `CognitiveContributionV1`, `MetricContribution`); this module fixes the
6
+ * richer *calculator output* shapes that `assemble.ts` folds into those
7
+ * stored shapes. See README "Entscheidungen" for the mapping rules.
8
+ */
9
+ import type { CognitiveContributionV1, MetricContribution } from "@untangle-code/domain";
10
+ /**
11
+ * One cyclomatic decision contribution. Structurally a `MetricContribution`
12
+ * (so it can be stored directly in `ComplexitySnapshotV1.cyclomatic.contributors`)
13
+ * plus the two spec.md §16.2/§16.3 membership flags that tell which of the two
14
+ * totals (classic/extended) this contribution feeds.
15
+ */
16
+ export interface CyclomaticContribution extends MetricContribution {
17
+ countsForClassic: boolean;
18
+ countsForExtended: boolean;
19
+ }
20
+ /** Output of `computeCyclomatic` (spec.md §16.2, §16.3). */
21
+ export interface CyclomaticResult {
22
+ classic: number;
23
+ extended: number;
24
+ /** Superset list (extended ⊇ classic); filter by the membership flags. */
25
+ contributions: CyclomaticContribution[];
26
+ algorithmVersion: string;
27
+ }
28
+ /** Output of `computeCognitive` (spec.md §16.4, algorithm `untangle-cognitive-v1`). */
29
+ export interface CognitiveResult {
30
+ value: number;
31
+ algorithm: string;
32
+ contributions: CognitiveContributionV1[];
33
+ }
34
+ /**
35
+ * Coupling and API-surface inputs that require whole-repository graph or
36
+ * export/visibility knowledge the facts-only calculators do not have. Every
37
+ * field is optional and is only ever included in the output when supplied
38
+ * here — never guessed or defaulted (per the work order).
39
+ */
40
+ export interface StructuralExternalInput {
41
+ fanIn?: number;
42
+ fanOut?: number;
43
+ afferentCoupling?: number;
44
+ efferentCoupling?: number;
45
+ /** Used as-is when given; otherwise derived from afferent/efferent when both are present. */
46
+ instability?: number;
47
+ importInDegree?: number;
48
+ importOutDegree?: number;
49
+ inheritanceDepth?: number;
50
+ publicApiCount?: number;
51
+ }
52
+ /** Envelope every non-cyclomatic/cognitive calculator returns (uniform API, spec.md §16.5–§16.8). */
53
+ export interface MetricsResult {
54
+ metrics: Record<string, number>;
55
+ algorithmVersion: string;
56
+ }
57
+ /** Output of `computeHalstead` (spec.md §16.7). */
58
+ export interface HalsteadResult {
59
+ metrics: Record<string, number>;
60
+ /** Present only when `options.includeEstimates` is set; explicitly labeled as estimates. */
61
+ estimates?: {
62
+ /** Estimated time to implement, in seconds (Halstead's Stroud-number-18 heuristic). */
63
+ timeSeconds: number;
64
+ /** Estimated defect count (`volume / 3000`, Halstead's heuristic). */
65
+ estimatedBugs: number;
66
+ };
67
+ algorithmVersion: string;
68
+ }
69
+ /** Output of `computeMaintainability` (spec.md §16.9). */
70
+ export interface MaintainabilityResult {
71
+ /** Clamped to [0, 100]. */
72
+ value: number;
73
+ /** Unclamped `MI_raw` before the `* 100 / 171` rescale, for debugging/explain. */
74
+ raw: number;
75
+ /** The exact formula used, as required by the work order. */
76
+ formula: string;
77
+ algorithmVersion: string;
78
+ }
79
+ /** One value to aggregate (spec.md §16.10). */
80
+ export interface AggregationEntry {
81
+ /** Dedupe key — duplicate ids are folded to a single sample (first occurrence wins). */
82
+ metricsScopeId: string;
83
+ value: number;
84
+ /** SLOC of the scope this value came from, for the SLOC-weighted mean. */
85
+ sloc?: number;
86
+ }
87
+ /** Result of `aggregate()` (spec.md §16.10). */
88
+ export interface AggregationResult {
89
+ count: number;
90
+ sum: number;
91
+ mean: number;
92
+ median: number;
93
+ max: number;
94
+ p90: number;
95
+ p95: number;
96
+ /** Undefined when no entry carried an `sloc`. */
97
+ slocWeightedMean?: number;
98
+ /** Number of (deduplicated) entries whose value is strictly greater than each configured threshold, keyed by the threshold's decimal string. */
99
+ thresholdCounts: Record<string, number>;
100
+ /** Number of raw entries dropped because their `metricsScopeId` repeated an earlier one. */
101
+ duplicatesDropped: number;
102
+ }
103
+ /** One line of `explainSymbol`'s ranked, human-readable breakdown. */
104
+ export interface ExplainEntry {
105
+ rank: number;
106
+ metric: "cyclomatic" | "cognitive";
107
+ kind: string;
108
+ line: number;
109
+ value: number;
110
+ text: string;
111
+ }
112
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEzF;;;;;GAKG;AACH,MAAM,WAAW,sBAAuB,SAAQ,kBAAkB;IAChE,gBAAgB,EAAE,OAAO,CAAC;IAC1B,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAED,4DAA4D;AAC5D,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,aAAa,EAAE,sBAAsB,EAAE,CAAC;IACxC,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,uFAAuF;AACvF,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,uBAAuB,EAAE,CAAC;CAC1C;AAED;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6FAA6F;IAC7F,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,qGAAqG;AACrG,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,mDAAmD;AACnD,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,4FAA4F;IAC5F,SAAS,CAAC,EAAE;QACV,uFAAuF;QACvF,WAAW,EAAE,MAAM,CAAC;QACpB,sEAAsE;QACtE,aAAa,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,0DAA0D;AAC1D,MAAM,WAAW,qBAAqB;IACpC,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,kFAAkF;IAClF,GAAG,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,+CAA+C;AAC/C,MAAM,WAAW,gBAAgB;IAC/B,wFAAwF;IACxF,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,gDAAgD;AAChD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,iDAAiD;IACjD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gJAAgJ;IAChJ,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,4FAA4F;IAC5F,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,sEAAsE;AACtE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,YAAY,GAAG,WAAW,CAAC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,63 @@
1
1
  {
2
2
  "name": "@untangle-code/complexity",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Untangle complexity engine: pure, deterministic calculators for cyclomatic, cognitive, structural, textual, syntactic, Halstead and maintainability metrics, plus aggregation and explainability (spec.md §16).",
7
+ "license": "AGPL-3.0-only",
8
+ "homepage": "https://github.com/lboel/untangle",
9
+ "bugs": {
10
+ "url": "https://github.com/lboel/untangle/issues"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/lboel/untangle.git",
15
+ "directory": "packages/complexity"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "engines": {
21
+ "node": "^22.13.0 || >=24.0.0"
22
+ },
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "default": "./dist/index.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "!dist/**/*.test.*",
32
+ "!dist/**/test-helpers.*",
33
+ "!dist/**/test-support.*",
34
+ "!dist/**/test-support/**",
35
+ "!dist/**/testing/**",
36
+ "!dist/**/__tests__/**",
37
+ "src",
38
+ "!src/**/*.test.ts",
39
+ "!src/**/test-helpers.ts",
40
+ "!src/**/test-support.ts",
41
+ "!src/**/test-support/**",
42
+ "!src/**/testing/**",
43
+ "LICENSE",
44
+ "NOTICE",
45
+ "README.md",
46
+ "THIRD_PARTY_LICENSES.md"
47
+ ],
48
+ "dependencies": {
49
+ "@untangle-code/domain": "0.1.1"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "22.19.7",
53
+ "typescript": "5.9.3",
54
+ "vitest": "3.2.7",
55
+ "@untangle-code/parser-registry": "0.1.1"
56
+ },
57
+ "scripts": {
58
+ "build": "node ../../scripts/tsc-build.mjs tsconfig.json",
59
+ "typecheck": "tsc -p tsconfig.typecheck.json",
60
+ "test": "vitest run",
61
+ "clean": "rm -rf dist *.tsbuildinfo"
62
+ }
6
63
  }