@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.
- package/LICENSE +661 -0
- package/NOTICE +90 -0
- package/README.md +235 -2
- package/THIRD_PARTY_LICENSES.md +24 -0
- package/dist/aggregator.d.ts +28 -0
- package/dist/aggregator.d.ts.map +1 -0
- package/dist/aggregator.js +77 -0
- package/dist/aggregator.js.map +1 -0
- package/dist/assemble.d.ts +50 -0
- package/dist/assemble.d.ts.map +1 -0
- package/dist/assemble.js +43 -0
- package/dist/assemble.js.map +1 -0
- package/dist/cognitive.d.ts +62 -0
- package/dist/cognitive.d.ts.map +1 -0
- package/dist/cognitive.js +62 -0
- package/dist/cognitive.js.map +1 -0
- package/dist/cyclomatic.d.ts +29 -0
- package/dist/cyclomatic.d.ts.map +1 -0
- package/dist/cyclomatic.js +52 -0
- package/dist/cyclomatic.js.map +1 -0
- package/dist/entropy.d.ts +5 -0
- package/dist/entropy.d.ts.map +1 -0
- package/dist/entropy.js +29 -0
- package/dist/entropy.js.map +1 -0
- package/dist/explain.d.ts +22 -0
- package/dist/explain.d.ts.map +1 -0
- package/dist/explain.js +35 -0
- package/dist/explain.js.map +1 -0
- package/dist/halstead.d.ts +23 -0
- package/dist/halstead.d.ts.map +1 -0
- package/dist/halstead.js +24 -0
- package/dist/halstead.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/ir/build-ir.d.ts +105 -0
- package/dist/ir/build-ir.d.ts.map +1 -0
- package/dist/ir/build-ir.js +267 -0
- package/dist/ir/build-ir.js.map +1 -0
- package/dist/maintainability.d.ts +15 -0
- package/dist/maintainability.d.ts.map +1 -0
- package/dist/maintainability.js +10 -0
- package/dist/maintainability.js.map +1 -0
- package/dist/node-kind-heuristics.d.ts +24 -0
- package/dist/node-kind-heuristics.d.ts.map +1 -0
- package/dist/node-kind-heuristics.js +37 -0
- package/dist/node-kind-heuristics.js.map +1 -0
- package/dist/prose/metrics.d.ts +81 -0
- package/dist/prose/metrics.d.ts.map +1 -0
- package/dist/prose/metrics.js +103 -0
- package/dist/prose/metrics.js.map +1 -0
- package/dist/scope-aggregate.d.ts +229 -0
- package/dist/scope-aggregate.d.ts.map +1 -0
- package/dist/scope-aggregate.js +588 -0
- package/dist/scope-aggregate.js.map +1 -0
- package/dist/scope-identity.d.ts +85 -0
- package/dist/scope-identity.d.ts.map +1 -0
- package/dist/scope-identity.js +144 -0
- package/dist/scope-identity.js.map +1 -0
- package/dist/scope.d.ts +36 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +60 -0
- package/dist/scope.js.map +1 -0
- package/dist/structural.d.ts +30 -0
- package/dist/structural.d.ts.map +1 -0
- package/dist/structural.js +98 -0
- package/dist/structural.js.map +1 -0
- package/dist/syntactic.d.ts +35 -0
- package/dist/syntactic.d.ts.map +1 -0
- package/dist/syntactic.js +143 -0
- package/dist/syntactic.js.map +1 -0
- package/dist/text-lines.d.ts +34 -0
- package/dist/text-lines.d.ts.map +1 -0
- package/dist/text-lines.js +60 -0
- package/dist/text-lines.js.map +1 -0
- package/dist/textual.d.ts +28 -0
- package/dist/textual.d.ts.map +1 -0
- package/dist/textual.js +75 -0
- package/dist/textual.js.map +1 -0
- package/dist/types.d.ts +112 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +60 -3
- package/src/aggregator.ts +105 -0
- package/src/assemble.ts +109 -0
- package/src/cognitive.ts +127 -0
- package/src/cyclomatic.ts +78 -0
- package/src/entropy.ts +30 -0
- package/src/explain.ts +64 -0
- package/src/halstead.ts +54 -0
- package/src/index.ts +85 -0
- package/src/ir/build-ir.ts +411 -0
- package/src/maintainability.ts +29 -0
- package/src/node-kind-heuristics.ts +55 -0
- package/src/prose/metrics.ts +201 -0
- package/src/scope-aggregate.ts +775 -0
- package/src/scope-identity.ts +240 -0
- package/src/scope.ts +77 -0
- package/src/structural.ts +132 -0
- package/src/syntactic.ts +183 -0
- package/src/text-lines.ts +90 -0
- package/src/textual.ts +119 -0
- 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
|
-
#
|
|
1
|
+
# @untangle-code/complexity
|
|
2
2
|
|
|
3
|
-
|
|
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"}
|
package/dist/assemble.js
ADDED
|
@@ -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"}
|