@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
package/NOTICE ADDED
@@ -0,0 +1,90 @@
1
+ # NOTICE
2
+
3
+ Copyright (C) 2026 Luke Bölling and contributors.
4
+
5
+ This attribution covers original Untangle work. Contributors retain copyright in
6
+ their contributions. Third-party packages, grammars and other materials retain
7
+ their own copyright holders, licenses and notices; this line does not replace them.
8
+
9
+ ## License — binding decision
10
+
11
+ Untangle is licensed under the **GNU Affero General Public License, Version 3
12
+ (AGPL-3.0-only)**. See [`LICENSE`](./LICENSE).
13
+
14
+ This decision is **final, not provisional**, and was explicitly instructed by
15
+ the project owner. No contributor, agent or tool may change it without a new
16
+ explicit instruction.
17
+
18
+ The repository-wide license covers Untangle's first-party source code, scripts,
19
+ tests, fixtures, configuration, documentation, website and brand artwork, including
20
+ the logo and 3D yarn illustrations. Every workspace `package.json` declares
21
+ `"license": "AGPL-3.0-only"`. There is no separately licensed enterprise component
22
+ or separate permissive license for documentation or brand assets.
23
+
24
+ Third-party code, embedded grammars, dependency assets and their notices retain
25
+ their original licenses. Their inclusion does not relicense those works. The
26
+ repository license does not grant trademark rights or change the license of a
27
+ repository analyzed with Untangle. See the repository's `docs/LICENSING.md` for
28
+ scope and verification details.
29
+
30
+ This resolves the license question left open in `vision.md` §14 and completes
31
+ `product.md` §16, item 2.
32
+
33
+ ## Rationale
34
+
35
+ - **Network copyleft fits the intended product model.** AGPL §13 sets source
36
+ availability requirements for modified versions used through a computer network.
37
+ The current local CLI/stdio-MCP entry and future shared-server modes share the
38
+ same license.
39
+ - **Local-first analysis has a clear boundary.** Running Untangle against a codebase
40
+ does not replace that codebase's own license with Untangle's license.
41
+ - **First-party components share one license.** Implemented components and future
42
+ official plugins use this repository's licensing policy. Planned integrations do
43
+ not establish implemented or published products.
44
+
45
+ ## Deliberately accepted consequence
46
+
47
+ The project deliberately chooses AGPL rather than a permissive embedding license.
48
+ Redistribution, modifications and combined works must be assessed against the
49
+ actual terms in `LICENSE`, including applicable Corresponding Source requirements.
50
+ No permissive exception, commercial dual license or special contract-only package
51
+ license is granted or promised by this notice.
52
+
53
+ ## Dependencies
54
+
55
+ Third-party runtime dependencies are checked against the allowlist in
56
+ `scripts/check-licenses.mjs`. Their licenses and copyright notices remain unchanged
57
+ and traceable through package `THIRD_PARTY_LICENSES.md` files, grammar provenance,
58
+ the lockfile and release SBOM. The allowlist includes MIT / Apache-2.0 / BSD / ISC
59
+ and other explicitly listed licenses; it is not a license grant for dependencies.
60
+
61
+ The allowlist in `scripts/check-licenses.mjs` applies to **dependencies**, not
62
+ workspace packages. Untangle's own packages carry AGPL-3.0-only and are excluded
63
+ from that check.
64
+
65
+ ## Clean room
66
+
67
+ Untangle is an independent implementation with its own data models, APIs, tool
68
+ names, tests and sources. Feature ideas may be derived from publicly observable
69
+ behavior and generally known techniques. Third-party source code is not copied,
70
+ including from AGPL projects even where the license would permit it.
71
+
72
+ ## Contribution terms
73
+
74
+ Pull requests are welcome under the repository's existing AGPL-3.0-only license.
75
+ No separate CLA, copyright assignment or DCO sign-off is required. Contributors
76
+ must have the right to submit their work under those terms. See CONTRIBUTING.md.
77
+
78
+ ## Release decisions
79
+
80
+ - [x] Copyright attribution: Luke Bölling, with contributors retaining their rights.
81
+ - [x] Contributions use AGPL-3.0-only without an additional CLA or DCO gate.
82
+
83
+ ## Open legal items before public beta
84
+
85
+ - [ ] Trademark search for the product name `Untangle`.
86
+ - [ ] Registration and legal review of the npm scope `@untangle-code`.
87
+ - [ ] Verify source availability and notices for each released network daemon/server
88
+ mode (WP-59). The current stdio MCP `untangle_about` already reports the license,
89
+ source repository and build commit when supplied; metadata alone does not prove
90
+ that a concrete deployed version's source is available.
package/README.md CHANGED
@@ -1,3 +1,236 @@
1
- # Temporary Holding Version
1
+ # @untangle-code/complexity
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Untangle's complexity engine (spec.md §16, architecture.md §6.7, §15): pure deterministic
4
+ calculators over domain `ComplexityFacts`/`ComplexityIRNode`. No I/O, parser-registry, embeddings,
5
+ `Date.now()` or `Math.random()`. Every calculator is a pure function of facts/IR and algorithm/
6
+ metric versions, returning snapshots and explainable contributions.
7
+
8
+ Normative sources: [`spec.md`](../../spec.md) §16 (takes precedence),
9
+ [`architecture.md`](../../architecture.md) §6.7, §15.
10
+
11
+ <a id="modulübersicht"></a>
12
+
13
+ ## Modules
14
+
15
+ | Module | Contents | Normative source |
16
+ |---|---|---|
17
+ | `scope.ts` | `selectSymbolFacts()` narrows file facts to a symbol; `containsRange`/`strictlyContainsRange`, `rangeKey`, `sliceByRange`, `splitLines` | arch §15.1 |
18
+ | `cyclomatic.ts` | `computeCyclomatic()`: classic/extended, independent `logicalGroupId` groups counted once | spec §16.2–§16.3 |
19
+ | `cognitive.ts` | `computeCognitive()`: explicit-depth-stack IR traversal, `untangle-cognitive-v1`; `assertCognitiveInvariant()` | spec §16.4; arch §15.3 |
20
+ | `ir/build-ir.ts` | `buildSymbolIR()`: facts→IR, flat else-if siblings, logical-condition hoisting, lambda nesting, optional recursion marking from `RawCall` | arch §6.7; spec §16.4 |
21
+ | `structural.ts` | `computeStructural()`: all §16.5 metrics; coupling/API metrics only with explicit `StructuralExternalInput` | spec §16.5 |
22
+ | `textual.ts` | `computeTextual()`: all §16.6 metrics including `identifier_entropy`, `magic_literal_count`, TODO/FIXME/HACK | spec §16.6 |
23
+ | `syntactic.ts` | `computeSyntactic()`: all §16.8 metrics; exact branching factors from `nodeKinds` preorder depths, IR root only as fallback | spec §16.8 |
24
+ | `halstead.ts` | `computeHalstead()`: exact formulas including `max(n,1)`/`max(n2,1)` guards; time/bugs labeled estimates | spec §16.7 |
25
+ | `maintainability.ts` | `computeMaintainability()`: exact default formula, clamped [0,100], formula returned as text | spec §16.9 |
26
+ | `aggregator.ts` | `aggregate()`: count/sum/mean/median/max/p90/p95/SLOC-weighted mean/thresholds, deduplication by `metricsScopeId` | spec §16.10 |
27
+ | `scope-aggregate.ts` | `aggregateScopeTree()`, `checkScopeTreeInvariants()`, `checkScopeCoverage()`/`checkSelectionCoverage()`, `assertScopeTreeInvariants()`, `selectScopeEntries()`: scope-TREE aggregation **without double counting**, three readings, value-bound/count checks (WP-82) | spec §16.10 |
28
+ | `scope-identity.ts` | `withUniqueScopeIds()`: converts per-file **nonunique** adapter `symbolScopeId` values to byte-range IDs and reports every collision (WP-82) | spec §16.10; E4 |
29
+ | `explain.ts` | `explainSymbol()`: ordered human-readable explanations of largest contributions | Task |
30
+ | `assemble.ts` | `assembleComplexitySnapshot()`: folds calculator results into domain `ComplexitySnapshotV1` | spec §16; arch §10.5 |
31
+ | `entropy.ts` | Shared `shannonEntropy()`, `frequenciesOf()` | spec §16.6, §16.8 |
32
+ | `node-kind-heuristics.ts` | Best-effort `classifyNodeKind()`, `isAsyncNodeKind()`, `isExceptionNodeKind()` for raw kind strings | Decision 5 |
33
+ | `text-lines.ts` | Shared `classifyLines()`, `scopeLines()` for structural/textual metrics | Decision 3 |
34
+ | `types.ts` | Local calculator result types (`CyclomaticResult`, `CognitiveResult`, `MetricsResult`, `AggregationResult`, `ExplainEntry`, …), **not** domain types | Decision 2 |
35
+
36
+ Public symbols are re-exported only through `src/index.ts`.
37
+
38
+ <a id="architekturentscheidung-scope-filterung-liegt-beim-aufrufer"></a>
39
+
40
+ ## Architecture decision: callers own scope filtering
41
+
42
+ No calculator knows `symbolScopeId`. `selectSymbolFacts(facts, symbolScopeId)` narrows file-wide
43
+ `ComplexityFacts` to one symbol; for `scope: "file"`, callers pass unfiltered facts. Every
44
+ calculator stays easily testable and scope-agnostic; the same code calculates symbol/file metrics.
45
+
46
+ <a id="entscheidungen"></a>
47
+
48
+ ## Decisions
49
+
50
+ Reasoned choices left open by spec.md/architecture.md (task rule 9); downstream consumers should
51
+ extend additively rather than redefine contracts locally.
52
+
53
+ 1. **Fact attribution is flat, not recursive.** `SymbolScopeFact.symbolScopeId` denotes exact
54
+ membership: facts belong to that symbol, not its nested named children, which receive their
55
+ own `SymbolScopeFact` and metrics. File aggregation instead sums across every file scope.
56
+
57
+ 2. **IR branching chains use flat siblings.** Architecture.md §15.3 does not fix their shape.
58
+ Every IR producer maps `if`/`else if`/`else` to sibling nodes under one parent, **not** raw AST
59
+ nested `alternate` structure. Else-if then receives **no** extra nesting penalty relative to
60
+ its if because both share ambient depth, satisfying spec.md §16.4 without special cases.
61
+ `ir/build-ir.ts` now implements this itself: `buildSymbolIR()` derives flat siblings from fact
62
+ byte ranges, reparenting continuation events to their enclosing if's final parent. Language
63
+ adapters therefore need not know IR shape.
64
+
65
+ Since the §16.4 correction, `ComplexitySnapshotV1.cognitive.contributors` uses full
66
+ `CognitiveContributionV1[]`: every persisted contribution requires structured position, kind,
67
+ base AND nesting. Former conversion to `MetricContribution` with free-form `note`
68
+ `base`/`nestingPenalty` violated this and was removed. Snapshots also carry
69
+ `maintainabilityFormula` because §16.9 requires the actual formula explicitly.
70
+
71
+ 3. **Documentation-comment heuristic.** Facts do not distinguish ordinary/documentation
72
+ comments. After byte-range slicing, `text-lines.ts` treats prefixes `/**`, `///`, `"""`, `'''`,
73
+ `##!`, `//!` as documentation.
74
+
75
+ 4. **`instability` uses explicit values only.** Supplied values pass through unchanged. Otherwise
76
+ it is calculated only with BOTH `afferentCoupling` and `efferentCoupling` as
77
+ `efferent / (afferent + efferent)`, Martin's instability metric. It is never guessed from one
78
+ value or defaulted.
79
+
80
+ 5. **Node-kind-dependent counts are best effort:** `parameter_count`/`return_count`/
81
+ `exit_point_count`/`statement_count`/`expression_count`/`declaration_count`/
82
+ `async_construct_count`/`exception_construct_count`. `NodeKindFact.kind` is an opaque adapter
83
+ string, not a domain-normalized taxonomy. `node-kind-heuristics.ts` matches common grammar
84
+ conventions (`"return_statement"`, `"required_parameter"`, `"throw_statement"`,
85
+ `"async_function"`, …), a documented versionable heuristic and **candidate for additive domain
86
+ extension** (see `openIssues`).
87
+
88
+ 6. **`identifier_entropy` is character entropy, not token entropy.** Shannon entropy uses
89
+ concatenated characters from all identifier occurrences, not frequencies of distinct names.
90
+ It responds to naming style, such as abbreviations versus descriptive camelCase, rather than
91
+ being dominated by repeated short frequent names (`i`, `x`).
92
+
93
+ 7. **`ast_branching_factor_avg`/`_max` reconstruct exactly from preorder `nodeKinds` depths.**
94
+ Adapters emit DFS preorder (the input contract in `syntactic.ts`). Each node's parent is the
95
+ nearest preceding entry with smaller depth, determining child counts and branching exactly
96
+ without pointers or guesses. This is real AST branching, not sparse control-flow IR branching;
97
+ optional IR roots are fallback only for empty `nodeKinds`, such as handwritten IR fixtures.
98
+
99
+ 8. **Percentiles use linear interpolation**, following NumPy `linear`/Excel `PERCENTILE.INC`:
100
+ `index = p * (n - 1)`, interpolating `sorted[floor(index)]` and `sorted[ceil(index)]`. Median
101
+ uses the same formula at `p = 0.5`, without a separate even/odd branch.
102
+
103
+ 9. **`magic_literal_count` exclusions:** numbers `{0, 1, -1}`, strings `{"", ''}` using trimmed
104
+ raw text. Only `number`/`string` literals can be magic, never
105
+ `boolean`/`null`/`regex`/`template`/`other`.
106
+
107
+ 10. **`comment_lines` counts every touched line fully**, including `code(); // comment` lines.
108
+ `SourceRange` supplies byte rather than column boundaries; reliably separating code/comment
109
+ line portions would require extra raw-text heuristics.
110
+
111
+ 11. **Calculators return `{ metrics: Record<string, number>; algorithmVersion: string }`**, except
112
+ cyclomatic/cognitive, which have dedicated domain-compatible results. This provides a shared
113
+ API across all seven calculators, even where `ComplexitySnapshotV1` has no per-family version slot.
114
+
115
+ <a id="öffentliche-api-auszug-siehe-srcindexts"></a>
116
+
117
+ ## Public API (excerpt; see `src/index.ts`)
118
+
119
+ ```ts
120
+ buildSymbolIR(facts: ComplexityFacts, symbolScopeId: string, options?: BuildSymbolIROptions): ComplexityIRNode | undefined
121
+ computeCyclomatic(facts: ComplexityFacts, baseLine: number, algorithmVersion: string): CyclomaticResult
122
+ computeCognitive(symbolRoot: ComplexityIRNode, algorithm: string): CognitiveResult
123
+ computeStructural(facts, sourceText, scopeRange, external?, algorithmVersion?): MetricsResult
124
+ computeTextual(facts, sourceText, scopeRange, algorithmVersion?): MetricsResult
125
+ computeSyntactic(facts, sloc, irRoot?, algorithmVersion?): MetricsResult
126
+ computeHalstead(tokens: HalsteadTokens, options?, algorithmVersion?): HalsteadResult
127
+ computeMaintainability(input: MaintainabilityInput, algorithmVersion?): MaintainabilityResult
128
+ aggregate(entries: AggregationEntry[], thresholds?: number[]): AggregationResult
129
+ aggregateScopeTree(nodes: ScopeMetricNodeV1[], options?: ScopeTreeOptions): ScopeTreeAggregateV1
130
+ checkScopeTreeInvariants(nodes: ScopeMetricNodeV1[], options?: ScopeTreeOptions): ScopeInvariantViolationV1[]
131
+ assertScopeTreeInvariants(nodes: ScopeMetricNodeV1[], options?: ScopeTreeOptions): void
132
+ selectScopeEntries(nodes: ScopeMetricNodeV1[], reading: ScopeSumReading): AggregationEntry[]
133
+ withUniqueScopeIds(facts: ComplexityFacts, calls: readonly RawCall[], idPrefix: string): UniqueScopeIdsResultV1
134
+ explainSymbol(snapshot: ComplexitySnapshotV1, top?: number): ExplainEntry[]
135
+ assembleComplexitySnapshot(context: SnapshotContext, parts: SnapshotParts): ComplexitySnapshotV1
136
+ selectSymbolFacts(facts: ComplexityFacts, symbolScopeId: string): ComplexityFacts
137
+ ```
138
+
139
+ <a id="aggregation-ohne-doppelzählung-wp-82"></a>
140
+
141
+ ## Aggregation without double counting (WP-82)
142
+
143
+ `aggregate()` deduplicates `metricsScopeId`, handling duplicate input fragments, **not** nested
144
+ scopes. `buildSymbolIR()` deliberately includes every descendant in target IR (spec.md §16.4's
145
+ documented nesting context): a `file` scope carries all class, method and closure control flow.
146
+ Each has its own ID, so naive summation counts source once per enclosing scope. Measured on this
147
+ repository, the naive sum is approximately three times larger.
148
+
149
+ `scope-aggregate.ts` defines **three readings**, each counting every scope once while differing
150
+ in included nesting context:
151
+
152
+ | Reading | Counted values | Question answered |
153
+ |---|---|---|
154
+ | `top_level` | Root `inclusiveValue` | Difficulty of reading a file **as one whole**; every enclosing function contributes nesting |
155
+ | `execution_unit` (**default**) | Outermost execution-unit `inclusiveValue` plus ancestors' `selfValue` (file/class/interface) | Difficulty of each **function by itself**; nesting resets at each outermost function, keeping nested closures inside it. The only reading that withstands comparison with an established tool (X8) |
156
+ | `self` | Every scope's `selfValue` | Complexity in each scope's **own body**; lower bound, with differences exactly the nesting penalty |
157
+
158
+ There is deliberately no **leaf-only reading**: direct control flow in a parent with children
159
+ belongs to no leaf and would disappear, the silent loss E4 forbids. `leafOnlySum` still appears as
160
+ a diagnostic alongside `leafOnlyDroppedAmount`, the omitted amount. The same applies to
161
+ `naiveSum` and `doubleCountedAmount`.
162
+
163
+ **Invariant checks are mandatory.** `aggregateScopeTree()` **throws** `UntangleError` with
164
+ `ANALYSIS_INCOMPLETE` on any scope violation. Its two stages serve different purposes:
165
+
166
+ 1. **Value bound** (`checkScopeTreeInvariants`):
167
+ `selfValue(S) + Σ inclusiveValue(direct children) <= inclusiveValue(S)`, plus leaf equality,
168
+ non-negativity, known parents and no cycles. This **one-sided** bound catches too-small parent
169
+ values, hence **missing** content. Too-large parents—including double counting—also satisfy
170
+ it and remain undetected. Values alone cannot distinguish inflation from a scope with much
171
+ own control flow. This stage therefore does **not** guarantee no double counting; earlier
172
+ documentation incorrectly claimed that.
173
+ 2. **Counting check** (`checkScopeCoverage` per reading, `checkAllScopeCoverage` for all three):
174
+ every scope contributes to **exactly one** selected summand, not two (`scope_counted_twice`)
175
+ and not zero (`scope_uncounted`). **This** guarantees the three selection sums. It checks
176
+ selection structure; internal double counting within a value remains the calculator's concern.
177
+ `checkSelectionCoverage(nodes, selection, label)` runs the same check on arbitrary selections,
178
+ making alarm cases demonstrable rather than merely asserted.
179
+
180
+ Metrics with a per-scope base, such as cyclomatic `M = 1 + decision_points`, use `perScopeBase`
181
+ normalization for checks. Their three sums reverse order because `self` counts the base per scope
182
+ and `top_level` per file.
183
+
184
+ `duplicatesDropped` is **not** merely operational: repeated `metricsScopeId` with equal values
185
+ is deduplicated without violation, as §16.10 requires for repeated retrieval fragments. In real
186
+ scope trees where each ID must denote one scope, any positive `duplicatesDropped` means lost
187
+ content. `scripts/complexity-report.mjs` aborts on it.
188
+
189
+ All this requires IDs to identify scopes uniquely. Adapter `symbolScopeId` currently does
190
+ **not**: per-parent ordinals plus qualified names collide for anonymous callbacks under anonymous
191
+ parents, affecting roughly one fifth of scopes here. `withUniqueScopeIds()` rewrites IDs to byte
192
+ ranges and **reports** every collision rather than hiding it. The adapter owns the actual fix.
193
+
194
+ Run through `scripts/complexity-report.mjs` (`pnpm complexity:report`).
195
+
196
+ ## Tests
197
+
198
+ 105 co-located Vitest cases under `src/**/*.test.ts`. Notable coverage:
199
+
200
+ - `golden-real-source.test.ts` exercises real TypeScript/JavaScript through real adapters, not
201
+ handwritten facts/IR. Hand-calculated cognitive values are **17** for TS triple nesting,
202
+ else-if, `&&`, lambda, and **13** for JS switch/case, `||`, loop, catch and direct recursion.
203
+ Complete contribution lists, hand-calculated cyclomatic values,
204
+ `sum(contributions.total) === total`, real-file branching factors and formula/structure
205
+ assertions verify persisted snapshots.
206
+ - `cognitive.test.ts` has a handwritten golden case (triple nesting, else-if, logical sequence,
207
+ lambda), total **17**, full contributions, and a corrupted-list test requiring
208
+ `assertCognitiveInvariant` to throw. `ir/build-ir.test.ts` isolates chain flattening, condition
209
+ hoisting, lambda context, recursion marking and switch/case modeling.
210
+ - Every calculator has a hand-calculated case: the spec §16.2 eight-point cyclomatic example
211
+ plus extended grouping; Halstead n1/n2/N1/N2/volume/difficulty/effort; full MI formula;
212
+ percentiles against reference set 1..10.
213
+ - `sum(contributions) === total` is explicitly tested for cyclomatic and cognitive.
214
+
215
+ ```bash
216
+ pnpm --filter @untangle-code/complexity run typecheck # tsc --noEmit, including tests
217
+ pnpm --filter @untangle-code/complexity run test # vitest run
218
+ pnpm --filter @untangle-code/complexity run build # tsc -p tsconfig.json -> dist/
219
+ ```
220
+
221
+ <a id="lizenz"></a>
222
+
223
+ ## License
224
+
225
+
226
+ **AGPL-3.0-only.** This package includes the complete license in [`LICENSE`](./LICENSE), the
227
+ license-choice rationale in [`NOTICE`](./NOTICE), and licenses for all third-party production
228
+ dependencies in [`THIRD_PARTY_LICENSES.md`](./THIRD_PARTY_LICENSES.md). All three ship in the npm
229
+ tarball: AGPL-3.0-only §4 and §5 require recipients to receive the license; a README alone is not
230
+ a copy of the license.
231
+
232
+ Local use alone does not trigger an obligation. Copyleft obligations apply to redistribution and
233
+ providing a network service (AGPL §13).
234
+
235
+ Corresponding TypeScript source ships under `src/` in the same tarball; the complete repository
236
+ is available at <https://github.com/lboel/untangle>.
@@ -0,0 +1,24 @@
1
+ # Third-party licenses — @untangle-code/complexity
2
+
3
+ Generated by `pnpm third-party-licenses` from this package's installed
4
+ production dependency tree: transitive `dependencies`, across
5
+ workspace edges, with no `devDependencies` included. The generator reads
6
+ each resolved package's `package.json` in `node_modules`; nothing is maintained by hand
7
+ or estimated.
8
+
9
+ Do not edit manually: `pnpm third-party-licenses --check` fails on drift,
10
+ and `pnpm check-publishable` rejects packages without a current file.
11
+
12
+ This file exists because `docs/BUILD_PLAN.md`, risk R6, makes
13
+ per-package `THIRD_PARTY_LICENSES.md` a release gate for npm publication.
14
+
15
+ Untangle itself is **AGPL-3.0-only** (see `LICENSE` and `NOTICE` in
16
+ this package). The packages below are third-party works; their own
17
+ licenses and copyright notices continue to apply unchanged. Workspace
18
+ packages (`@untangle-code/*`) are not listed here; they belong to the same
19
+ work and use the same AGPL-3.0-only license.
20
+
21
+ ## Result: no third-party dependencies
22
+
23
+ This package has no production dependencies outside the workspace.
24
+ The list is empty because none exist, not because they were not checked.
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Aggregator (spec.md §16.10). Aggregates one metric's values across many
3
+ * scopes (a file's symbols, a repo's files, ...).
4
+ *
5
+ * Percentile method (Untangle decision, documented per the work order):
6
+ * linear interpolation between closest ranks, i.e. for a sorted array `s` of
7
+ * length `n` and percentile `p` in [0, 1], `index = p * (n - 1)`,
8
+ * `result = s[floor(index)] + (s[ceil(index)] - s[floor(index)]) * frac(index)`.
9
+ * This is the same convention as NumPy's default `linear` interpolation and
10
+ * Excel's `PERCENTILE.INC`. `median` uses the same formula at `p = 0.5`, so
11
+ * it agrees with `p90`/`p95` on the same interpolation rule rather than using
12
+ * a different odd/even-length special case.
13
+ *
14
+ * Deduplication: entries are deduplicated by `metricsScopeId` (first
15
+ * occurrence wins) before any statistic is computed, per spec.md §16.10
16
+ * ("Doppelte Retrieval-Fragmente MÜSSEN über metrics_scope_id dedupliziert
17
+ * werden").
18
+ */
19
+ import type { AggregationEntry, AggregationResult } from "./types.js";
20
+ /**
21
+ * @param entries Raw values, one per scope. Duplicate `metricsScopeId`s are
22
+ * dropped (first occurrence wins) before aggregating.
23
+ * @param thresholds Threshold values; `thresholdCounts` counts entries whose
24
+ * (deduplicated) value is strictly greater than each threshold, keyed by
25
+ * the threshold's own decimal string (e.g. `"10"`, `"15.5"`).
26
+ */
27
+ export declare function aggregate(entries: readonly AggregationEntry[], thresholds?: readonly number[]): AggregationResult;
28
+ //# sourceMappingURL=aggregator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aggregator.d.ts","sourceRoot":"","sources":["../src/aggregator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAiCtE;;;;;;GAMG;AACH,wBAAgB,SAAS,CACvB,OAAO,EAAE,SAAS,gBAAgB,EAAE,EACpC,UAAU,GAAE,SAAS,MAAM,EAAO,GACjC,iBAAiB,CA2CnB"}
@@ -0,0 +1,77 @@
1
+ function percentile(sorted, p) {
2
+ if (sorted.length === 0)
3
+ return 0;
4
+ if (sorted.length === 1)
5
+ return sorted[0];
6
+ const index = p * (sorted.length - 1);
7
+ const lower = Math.floor(index);
8
+ const upper = Math.ceil(index);
9
+ const lowerValue = sorted[lower];
10
+ const upperValue = sorted[upper];
11
+ if (lower === upper)
12
+ return lowerValue;
13
+ const fraction = index - lower;
14
+ return lowerValue + (upperValue - lowerValue) * fraction;
15
+ }
16
+ function dedupeByScopeId(entries) {
17
+ const seen = new Set();
18
+ const deduped = [];
19
+ let duplicatesDropped = 0;
20
+ for (const entry of entries) {
21
+ if (seen.has(entry.metricsScopeId)) {
22
+ duplicatesDropped++;
23
+ continue;
24
+ }
25
+ seen.add(entry.metricsScopeId);
26
+ deduped.push(entry);
27
+ }
28
+ return { deduped, duplicatesDropped };
29
+ }
30
+ /**
31
+ * @param entries Raw values, one per scope. Duplicate `metricsScopeId`s are
32
+ * dropped (first occurrence wins) before aggregating.
33
+ * @param thresholds Threshold values; `thresholdCounts` counts entries whose
34
+ * (deduplicated) value is strictly greater than each threshold, keyed by
35
+ * the threshold's own decimal string (e.g. `"10"`, `"15.5"`).
36
+ */
37
+ export function aggregate(entries, thresholds = []) {
38
+ const { deduped, duplicatesDropped } = dedupeByScopeId(entries);
39
+ const values = deduped.map((e) => e.value);
40
+ const sorted = [...values].sort((a, b) => a - b);
41
+ const count = sorted.length;
42
+ const sum = sorted.reduce((s, v) => s + v, 0);
43
+ const mean = count > 0 ? sum / count : 0;
44
+ const median = percentile(sorted, 0.5);
45
+ const max = count > 0 ? sorted[count - 1] : 0;
46
+ const p90 = percentile(sorted, 0.9);
47
+ const p95 = percentile(sorted, 0.95);
48
+ const weighted = deduped.filter((e) => e.sloc !== undefined);
49
+ let slocWeightedMean;
50
+ if (weighted.length > 0) {
51
+ const totalSloc = weighted.reduce((s, e) => s + e.sloc, 0);
52
+ slocWeightedMean =
53
+ totalSloc > 0
54
+ ? weighted.reduce((s, e) => s + e.value * e.sloc, 0) / totalSloc
55
+ : 0;
56
+ }
57
+ const thresholdCounts = {};
58
+ for (const threshold of thresholds) {
59
+ thresholdCounts[String(threshold)] = values.filter((v) => v > threshold).length;
60
+ }
61
+ const result = {
62
+ count,
63
+ sum,
64
+ mean,
65
+ median,
66
+ max,
67
+ p90,
68
+ p95,
69
+ thresholdCounts,
70
+ duplicatesDropped,
71
+ };
72
+ if (slocWeightedMean !== undefined) {
73
+ result.slocWeightedMean = slocWeightedMean;
74
+ }
75
+ return result;
76
+ }
77
+ //# sourceMappingURL=aggregator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aggregator.js","sourceRoot":"","sources":["../src/aggregator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Folds calculator outputs into the domain-stored `ComplexitySnapshotV1`
3
+ * shape (spec.md §16, architecture.md §10.5). Pure function of its inputs —
4
+ * `measuredAt`/`snapshotId` etc. are supplied by the caller (who owns the
5
+ * `Clock`/`Random`/identity ports); this package never calls `Date.now()` or
6
+ * generates ids itself.
7
+ *
8
+ * Mapping decisions (README "Entscheidungen" #2):
9
+ * - `cyclomatic.contributors` takes the calculator's superset contribution
10
+ * list as-is (each `CyclomaticContribution` is structurally a
11
+ * `MetricContribution` plus the two membership flags, which are dropped —
12
+ * TypeScript's structural typing lets extra fields pass through, but they
13
+ * are stripped explicitly here so the stored payload never leaks
14
+ * calculator-internal fields).
15
+ * - `cognitive.contributors` stores the calculator's
16
+ * `CognitiveContributionV1[]` UNCHANGED: spec.md §16.4 requires every
17
+ * persisted contribution to carry source position, kind, base and nesting
18
+ * share as structured fields ("jeder Beitrag MUSS Quellposition, Art,
19
+ * Basis und Nesting-Anteil enthalten"), so `ComplexitySnapshotV1` keeps
20
+ * the full shape instead of flattening `base`/`nestingPenalty` into a
21
+ * free-text note.
22
+ * - `maintainabilityFormula` records the exact formula string the
23
+ * maintainability calculator used (spec.md §16.9: "Untangle MUSS die
24
+ * verwendete Formel explizit ausgeben").
25
+ */
26
+ import type { ComplexityScope, ComplexitySnapshotV1, ParseQuality } from "@untangle-code/domain";
27
+ import type { CognitiveResult, CyclomaticResult, MaintainabilityResult } from "./types.js";
28
+ export interface SnapshotContext {
29
+ snapshotId: string;
30
+ scope: ComplexityScope;
31
+ scopeId: string;
32
+ repoId: string;
33
+ refId: string;
34
+ commitSha?: string;
35
+ metricsVersion: string;
36
+ /** ISO-8601, obtained by the caller via the `Clock` port. */
37
+ measuredAt: string;
38
+ parseQuality: ParseQuality;
39
+ }
40
+ export interface SnapshotParts {
41
+ cyclomatic: CyclomaticResult;
42
+ cognitive: CognitiveResult;
43
+ structural: Record<string, number>;
44
+ textual: Record<string, number>;
45
+ syntactic: Record<string, number>;
46
+ halstead: Record<string, number>;
47
+ maintainability: MaintainabilityResult;
48
+ }
49
+ export declare function assembleComplexitySnapshot(context: SnapshotContext, parts: SnapshotParts): ComplexitySnapshotV1;
50
+ //# sourceMappingURL=assemble.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assemble.d.ts","sourceRoot":"","sources":["../src/assemble.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,EACV,eAAe,EACf,oBAAoB,EAEpB,YAAY,EACb,MAAM,uBAAuB,CAAC;AAE/B,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAE3F,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,eAAe,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,YAAY,CAAC;CAC5B;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,gBAAgB,CAAC;IAC7B,SAAS,EAAE,eAAe,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,eAAe,EAAE,qBAAqB,CAAC;CACxC;AAaD,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,eAAe,EACxB,KAAK,EAAE,aAAa,GACnB,oBAAoB,CAqCtB"}
@@ -0,0 +1,43 @@
1
+ function toMetricContribution(line, kind, value, note) {
2
+ const contribution = { line, kind, value };
3
+ if (note !== undefined)
4
+ contribution.note = note;
5
+ return contribution;
6
+ }
7
+ export function assembleComplexitySnapshot(context, parts) {
8
+ const snapshot = {
9
+ snapshotId: context.snapshotId,
10
+ scope: context.scope,
11
+ scopeId: context.scopeId,
12
+ repoId: context.repoId,
13
+ refId: context.refId,
14
+ metricsVersion: context.metricsVersion,
15
+ measuredAt: context.measuredAt,
16
+ parseQuality: context.parseQuality,
17
+ cyclomatic: {
18
+ classic: parts.cyclomatic.classic,
19
+ extended: parts.cyclomatic.extended,
20
+ contributors: parts.cyclomatic.contributions.map((c) => toMetricContribution(c.line, c.kind, c.value, c.note)),
21
+ },
22
+ cognitive: {
23
+ value: parts.cognitive.value,
24
+ algorithm: parts.cognitive.algorithm,
25
+ // Stored structurally, spec.md §16.4 (base/nestingPenalty/total are
26
+ // required fields of every persisted contribution). Copied so the
27
+ // snapshot never aliases the calculator's arrays.
28
+ contributors: parts.cognitive.contributions.map((c) => ({ ...c })),
29
+ },
30
+ structural: parts.structural,
31
+ textual: parts.textual,
32
+ syntactic: parts.syntactic,
33
+ halstead: parts.halstead,
34
+ maintainabilityIndex: parts.maintainability.value,
35
+ // spec.md §16.9: the formula actually used MUST be output explicitly.
36
+ maintainabilityFormula: parts.maintainability.formula,
37
+ };
38
+ if (context.commitSha !== undefined) {
39
+ snapshot.commitSha = context.commitSha;
40
+ }
41
+ return snapshot;
42
+ }
43
+ //# sourceMappingURL=assemble.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assemble.js","sourceRoot":"","sources":["../src/assemble.ts"],"names":[],"mappings":"AAyDA,SAAS,oBAAoB,CAC3B,IAAY,EACZ,IAAY,EACZ,KAAa,EACb,IAAa;IAEb,MAAM,YAAY,GAAuB,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAC/D,IAAI,IAAI,KAAK,SAAS;QAAE,YAAY,CAAC,IAAI,GAAG,IAAI,CAAC;IACjD,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,MAAM,UAAU,0BAA0B,CACxC,OAAwB,EACxB,KAAoB;IAEpB,MAAM,QAAQ,GAAyB;QACrC,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,cAAc,EAAE,OAAO,CAAC,cAAc;QACtC,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,YAAY,EAAE,OAAO,CAAC,YAAY;QAClC,UAAU,EAAE;YACV,OAAO,EAAE,KAAK,CAAC,UAAU,CAAC,OAAO;YACjC,QAAQ,EAAE,KAAK,CAAC,UAAU,CAAC,QAAQ;YACnC,YAAY,EAAE,KAAK,CAAC,UAAU,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CACrD,oBAAoB,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,CACtD;SACF;QACD,SAAS,EAAE;YACT,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC,KAAK;YAC5B,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,SAA2D;YACtF,oEAAoE;YACpE,kEAAkE;YAClE,kDAAkD;YAClD,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;SACnE;QACD,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,oBAAoB,EAAE,KAAK,CAAC,eAAe,CAAC,KAAK;QACjD,sEAAsE;QACtE,sBAAsB,EAAE,KAAK,CAAC,eAAe,CAAC,OAAO;KACtD,CAAC;IACF,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QACpC,QAAQ,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;IACzC,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * CognitiveCalculator, algorithm id `untangle-cognitive-v1` (spec.md §16.4,
3
+ * architecture.md §15.3).
4
+ *
5
+ * Traverses a `ComplexityIRNode` subtree (rooted at a `"symbol"` node) with
6
+ * an explicit depth-tracking stack, exactly as architecture.md §15.3
7
+ * prescribes ("current symbol, nesting depth, logical sequence state,
8
+ * recursion state"). Every contribution is `base + nestingPenalty = total`,
9
+ * and `value` is defined as `sum(contributions[].total)` — so the
10
+ * "sum of contributions equals the total" invariant (required by the work
11
+ * order) holds *by construction*; `assertCognitiveInvariant` is the
12
+ * belt-and-suspenders runtime guard the work order also asks for, exercised
13
+ * both indirectly (every golden test) and directly (a deliberately corrupted
14
+ * contribution list in cognitive.test.ts).
15
+ *
16
+ * REQUIRED INPUT CONTRACT (Untangle decision — architecture.md §15.3 leaves
17
+ * the exact IR shape for if/else-if/else chains unspecified; see README
18
+ * "Entscheidungen" #2 for the full rationale): a language adapter/IR builder
19
+ * feeding this calculator MUST represent an `if` / `else if` / `else` chain
20
+ * as a flat run of *sibling* IR nodes (one per branch) under the same
21
+ * parent, each carrying its own consequent block as `children` — never as
22
+ * the raw AST's nested "alternate" shape. This is what makes "`else if` gets
23
+ * no full new nesting increment" fall out of the traversal for free: siblings
24
+ * share the same ambient depth, so an `else if` scores exactly what its
25
+ * originating `if` scores, never more.
26
+ *
27
+ * Nesting rules (this calculator's own, documented decisions):
28
+ * - A node contributes when it carries a `decision` fact, or is itself kind
29
+ * `"logical_sequence"` (a pre-grouped `&&`/`||` run) or `"recursion_marker"`.
30
+ * - `base` is always 1. `nestingPenalty` is the ambient depth *before*
31
+ * entering the node (0 for a top-level construct), except recursion, whose
32
+ * penalty is always 0 (recursion is flagged flatly, not nesting-scaled —
33
+ * spec.md §16.4 only says it "KANN einen Beitrag erzeugen").
34
+ * - A node with `nestingBoundary: true` increments depth by exactly 1 for
35
+ * its own children (this covers `if`/`else_if`/loops/`catch`/`ternary`/
36
+ * `switch`/`case`/lambdas alike — lambdas use this same mechanism for the
37
+ * "documented nesting context" spec.md §16.4 asks for). `logical_sequence`
38
+ * and `recursion_marker` nodes are not nesting boundaries.
39
+ * - Decisions that share a `logicalGroupId` (an independent `&&`/`||` group
40
+ * spanning multiple facts) contribute once, at the first occurrence
41
+ * encountered during traversal — mirrors the cyclomatic-extended grouping
42
+ * rule in cyclomatic.ts.
43
+ */
44
+ import type { CognitiveContributionV1, ComplexityIRNode } from "@untangle-code/domain";
45
+ import type { CognitiveResult } from "./types.js";
46
+ /**
47
+ * @param symbolRoot The `ComplexityIRNode` (kind `"symbol"`) rooting the
48
+ * symbol's own subtree; traversal starts at depth 0.
49
+ * @param algorithm Caller-supplied algorithm id — pass domain's
50
+ * `COGNITIVE_ALGORITHM_ID` in production; kept as a plain parameter (not
51
+ * hardcoded) so this stays a pure function of its inputs.
52
+ */
53
+ export declare function computeCognitive(symbolRoot: ComplexityIRNode, algorithm: string): CognitiveResult;
54
+ /**
55
+ * Throws when `sum(contributions[].total) !== total`. `computeCognitive`
56
+ * cannot actually violate this (value is defined as that very sum), but the
57
+ * work order asks for an explicit, independently-testable guard — this is
58
+ * also the function a caller re-assembling a snapshot from stored
59
+ * contributions should re-run to catch data corruption.
60
+ */
61
+ export declare function assertCognitiveInvariant(contributions: readonly CognitiveContributionV1[], total: number): void;
62
+ //# sourceMappingURL=cognitive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cognitive.d.ts","sourceRoot":"","sources":["../src/cognitive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,OAAO,KAAK,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEvF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAiDlD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,GAAG,eAAe,CAMjG;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,aAAa,EAAE,SAAS,uBAAuB,EAAE,EACjD,KAAK,EAAE,MAAM,GACZ,IAAI,CAON"}