code-gauge 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +97 -108
  2. package/dist/cli.cjs +3 -3
  3. package/dist/cli.cjs.map +1 -1
  4. package/dist/cli.js +3 -3
  5. package/dist/cli.js.map +1 -1
  6. package/dist/cliConfig.cjs +1 -1
  7. package/dist/cliConfig.cjs.map +1 -1
  8. package/dist/cliConfig.d.ts +16 -84
  9. package/dist/cliConfig.js +1 -1
  10. package/dist/cliConfig.js.map +1 -1
  11. package/dist/crossFileDuplication.cjs +1 -1
  12. package/dist/crossFileDuplication.cjs.map +1 -1
  13. package/dist/crossFileDuplication.d.ts +9 -0
  14. package/dist/crossFileDuplication.js +1 -1
  15. package/dist/crossFileDuplication.js.map +1 -1
  16. package/dist/duplication.cjs +1 -1
  17. package/dist/duplication.cjs.map +1 -1
  18. package/dist/duplication.d.ts +13 -2
  19. package/dist/duplication.js +1 -1
  20. package/dist/duplication.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/languages.cjs +1 -1
  23. package/dist/languages.cjs.map +1 -1
  24. package/dist/languages.js +1 -1
  25. package/dist/languages.js.map +1 -1
  26. package/dist/metrics.cjs +1 -2
  27. package/dist/metrics.cjs.map +1 -1
  28. package/dist/metrics.js +1 -2
  29. package/dist/metrics.js.map +1 -1
  30. package/dist/nativeMetrics.cjs +1 -1
  31. package/dist/nativeMetrics.cjs.map +1 -1
  32. package/dist/nativeMetrics.d.ts +2 -2
  33. package/dist/nativeMetrics.js +1 -1
  34. package/dist/nativeMetrics.js.map +1 -1
  35. package/dist/types.d.ts +11 -76
  36. package/package.json +7 -5
  37. package/dist/architectureMetrics.cjs +0 -2
  38. package/dist/architectureMetrics.cjs.map +0 -1
  39. package/dist/architectureMetrics.d.ts +0 -41
  40. package/dist/architectureMetrics.js +0 -2
  41. package/dist/architectureMetrics.js.map +0 -1
  42. package/dist/typescriptProject.cjs +0 -3
  43. package/dist/typescriptProject.cjs.map +0 -1
  44. package/dist/typescriptProject.d.ts +0 -24
  45. package/dist/typescriptProject.js +0 -3
  46. package/dist/typescriptProject.js.map +0 -1
package/README.md CHANGED
@@ -5,7 +5,11 @@
5
5
  [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
6
6
  [![wbfy](https://img.shields.io/badge/wbfy-18.3.0-1e90ff.svg)](https://github.com/WillBooster/shared/tree/main/packages/wbfy)
7
7
 
8
- A command-line tool for measuring code metrics with tree-sitter. It scans a project and reports high-risk files, functions, and React components so you can spot code that is worth refactoring. A [programmatic API](#programmatic-api) is also available.
8
+ A command-line tool that ranks the files of a project by refactoring priority, built for AI-agent
9
+ workflows: an agent asked to "refactor this repository" runs `code-gauge` and starts from the top of
10
+ the list. Measurement uses tree-sitter, and the output is deliberately small — only the metrics that
11
+ tell an agent _what to change_ are measured and reported, so nothing in the output anchors an agent
12
+ toward out-of-scope "improvements". A [programmatic API](#programmatic-api) is also available.
9
13
 
10
14
  ## Getting started
11
15
 
@@ -18,150 +22,135 @@ npm install -g code-gauge
18
22
  code-gauge path/to/project
19
23
  ```
20
24
 
21
- The CLI scans JavaScript, JSX, TypeScript, TSX, Python, Go, Rust, Java, Ruby, C, and C++ files. By default it skips generated, vendor, test, and tool directories, prints a summary, and lists the highest-risk findings. TypeScript project metrics and React component classification turn on automatically when a `tsconfig.json` is found.
25
+ The CLI scans JavaScript, JSX, TypeScript, TSX, Python, Go, Rust, Java, Ruby, C, and C++ files. By
26
+ default it skips generated, vendor, test, and tool directories and prints the top 10 refactoring
27
+ candidates:
22
28
 
23
- ## Options
29
+ ```
30
+ Measured 123 files under /path/to/project (code LOC 45678, NCSS 23456, functions 1789)
31
+
32
+ Refactoring candidates (top 10 of 123):
33
+ 1. src/metrics.ts (score 2.87): worst function measure (L120-310) cognitive 42, NCSS 220, nesting 6; duplicated lines 180 (20%, shared with src/other.ts); file NCSS 1240
34
+ ...
35
+ ```
36
+
37
+ ## Ranking model
24
38
 
25
- | Option | Description |
26
- | -------------------------- | --------------------------------------------------------------------------- |
27
- | `--config <path>` | Use this config file instead of the auto-detected `code-gauge.config.json`. |
28
- | `--include-tests` | Include test files and test directories. |
29
- | `--tsconfig <path>` | Use this `tsconfig.json` instead of the auto-detected one. |
30
- | `--max-findings <n>` | Maximum number of findings to print (default: 20). |
31
- | `--largest-files <n>` | List the `n` largest files by code LOC (config key: `largestFiles`). |
32
- | `--json` | Print machine-readable JSON. |
33
- | `--fail-on-risk` | Exit with code 1 when any high-risk finding is reported. |
34
- | `--fail-on-error` | Exit with code 1 when any file or directory cannot be scanned. |
35
- | `--<metric>-threshold <n>` | Override a risk threshold (see below). |
39
+ Every file gets a score that is the sum of its repository-relative percentile ranks (each in `[0, 1)`)
40
+ over three dimensions:
36
41
 
37
- ## Risk thresholds
42
+ - **Worst-function cognitive complexity** — the SonarSource cognitive-complexity model, the
43
+ measure of understanding effort with the strongest empirical support among structural metrics.
44
+ - **Duplicated lines** — distinct lines covered by within-file duplicate blocks or cross-file
45
+ duplicate occurrences (Type-1/2 clones, gapped and near-miss Type-3 clones).
46
+ - **File NCSS** — non-commenting source statements, a comment- and formatting-independent size
47
+ measure calibrated against PMD's `NcssCount`.
38
48
 
39
- A finding is reported when a measured value is **greater than or equal to** its threshold. Every threshold can be set on the command line (e.g. `--file-loc-threshold 400`) or in a config file; the command line wins over the config file, which wins over the defaults.
49
+ Ranking is relative to the scanned project, so no absolute thresholds are involved: the top of the
50
+ list is worth refactoring first regardless of where any cutoff would sit. Each reported file carries
51
+ the concrete evidence (worst function with location, duplication with partner files, file size) so
52
+ an agent can act on it directly.
40
53
 
41
- `code-gauge` looks for `code-gauge.config.json` by walking up from the target directory (override with `--config`). The following config reproduces every built-in default:
54
+ ## Options
55
+
56
+ | Option | Description |
57
+ | ------------------------------------------ | ------------------------------------------------------------------------------- |
58
+ | `--config <path>` | Use this config file instead of the auto-detected `code-gauge.config.json`. |
59
+ | `--top <n>` | Number of top-ranked files to report (default: 10). |
60
+ | `--include-tests` | Include test files and test directories. |
61
+ | `--json` | Print machine-readable JSON. |
62
+ | `--fail-on-error` | Exit with code 1 when any file or directory cannot be scanned. |
63
+ | `--duplication-min-tokens <n>` | Minimum normalized token count for a duplicate region (default 40). |
64
+ | `--duplication-max-gap-tokens <n>` | Maximum token gap merged into one gapped clone group; 0 disables (default 30). |
65
+ | `--duplication-min-similarity-percent <n>` | Minimum similarity percent for near-miss clones; 100 = exact only (default 70). |
66
+
67
+ ## Configuration
68
+
69
+ `code-gauge` looks for `code-gauge.config.json` by walking up from the target directory (override
70
+ with `--config`). The following config reproduces every built-in default:
42
71
 
43
72
  ```json
44
73
  {
45
- "thresholds": {
46
- "fileLoc": 500,
47
- "functionLoc": 120,
48
- "componentLoc": 350,
49
- "cognitive": 25,
50
- "cyclomatic": 20,
51
- "call": 50,
52
- "import": 25,
53
- "fanOut": 10,
54
- "parameter": 8,
55
- "duplicateBlock": 2,
56
- "duplicationRatioPercent": 30,
57
- "crossFileDuplicateBlock": 2,
58
- "transitiveDependency": 25,
59
- "structuralBreadth": 8,
60
- "structuralCoordination": 300,
61
- "stateMutation": 50,
62
- "duplicateSymbolGroup": 5
63
- },
64
74
  "duplication": {
65
75
  "minTokens": 40,
66
76
  "maxGapTokens": 30,
67
77
  "minSimilarityPercent": 70
68
78
  },
69
- "languageThresholds": {
70
- "python": { "stateMutation": 90, "structuralCoordination": 350 },
71
- "ruby": { "stateMutation": 90, "structuralCoordination": 350 },
72
- "react": { "import": 30 }
73
- },
74
- "maxFindings": 20,
79
+ "rank": { "top": 10 },
75
80
  "includeTests": false,
76
- "failOnRisk": false,
77
81
  "failOnError": false
78
82
  }
79
83
  ```
80
84
 
81
- | Threshold | CLI flag | Reports when a … |
82
- | ------------------------- | ---------------------------------------- | ------------------------------------------------------ |
83
- | `fileLoc` | `--file-loc-threshold` | file's code LOC is large. |
84
- | `functionLoc` | `--function-loc-threshold` | function's physical LOC span is large. |
85
- | `componentLoc` | `--component-loc-threshold` | React component's physical LOC span is large. |
86
- | `cognitive` | `--cognitive-threshold` | function's cognitive complexity is high. |
87
- | `cyclomatic` | `--cyclomatic-threshold` | function's cyclomatic complexity is high. |
88
- | `call` | `--call-threshold` | function makes many calls. |
89
- | `import` | `--import-threshold` | file has many unique import sources. |
90
- | `fanOut` | `--fan-out-threshold` | function calls many other in-file functions. |
91
- | `parameter` | `--parameter-threshold` | function declares many parameters. |
92
- | `duplicateBlock` | `--duplicate-block-threshold` | file contains copy-pasted code blocks. |
93
- | `duplicationRatioPercent` | `--duplication-ratio-percent-threshold` | large percentage of a file's code lines is duplicated. |
94
- | `crossFileDuplicateBlock` | `--cross-file-duplicate-block-threshold` | file shares copy-pasted code blocks with other files. |
95
- | `transitiveDependency` | `--transitive-dependency-threshold` | file transitively reaches many local files. |
96
- | `structuralBreadth` | `--structural-breadth-threshold` | file coordinates many structural concerns. |
97
- | `structuralCoordination` | `--structural-coordination-threshold` | file's structural coordination score is high. |
98
- | `stateMutation` | `--state-mutation-threshold` | file mutates state heavily. |
99
- | `duplicateSymbolGroup` | `--duplicate-symbol-group-threshold` | file shares many duplicated symbols with others. |
100
-
101
- ### Per-language thresholds
102
-
103
- Some metrics distribute very differently by language or file type, so a single global threshold either
104
- over-flags one language or under-flags another. `languageThresholds` overrides individual thresholds for a
105
- profile without repeating the whole set. Each file resolves its thresholds as **base → its language profile →
106
- the `react` profile** (the last applies when the file contains a React component), so later profiles win.
107
-
108
- Valid profile keys are `javascript`, `jsx`, `typescript`, `tsx`, `python`, `go`, `rust`, `java`, `ruby`, `c`,
109
- `cpp`, and `react`. Built-in
110
- overrides raise `stateMutation` and `structuralCoordination` for Python and Ruby (every binding is an
111
- assignment, so these run far higher than in TypeScript) and raise `import` for React files (which pull in
112
- many components).
113
- Anything you specify is merged on top of the built-in overrides, so `{ "python": { "stateMutation": 8 } }`
114
- restores the global value for Python while keeping the other built-in adjustments.
115
-
116
- ```json
117
- {
118
- "languageThresholds": {
119
- "python": { "stateMutation": 120 },
120
- "react": { "componentLoc": 400, "import": 35 }
121
- }
122
- }
123
- ```
124
-
125
- Command-line `--<metric>-threshold` flags set the global base only; use the config file for per-language tuning.
85
+ The command line wins over the config file, which wins over the defaults. Unknown settings are
86
+ rejected so stale configuration fails loudly.
126
87
 
127
88
  ### Duplication detection settings
128
89
 
129
- The `duplication` config section (or the `--duplication-min-tokens`, `--duplication-max-gap-tokens`, and `--duplication-min-similarity-percent` flags) tunes how clones are detected rather than when they are reported:
90
+ The `duplication` section tunes how clones are detected:
130
91
 
131
- - `minTokens` (default 40): minimum normalized token count for a region to count as a duplicate. Raise it to report only substantial copies; lower it to catch small ones.
132
- - `maxGapTokens` (default 30): copies edited in one spot split into two exact matches around the edit; adjacent matches separated by at most this many tokens are merged back into a single gapped (Type-3) clone group. `0` disables merging. Applies to within-file detection and to cross-file matching alike: cross-file candidates are joined by a project-level window index over per-statement fingerprint sequences, and gap-adjacent cross-file matches merge into gapped clone groups the same way. Merging changes only the grouping, not the counting: each matched fragment still counts toward `duplicateBlock` (and each token span counts at most once), so an edited two-fragment pair keeps counting 2.
133
- - `minSimilarityPercent` (default 70): blocks the exact pipeline misses are additionally compared by similarity (n-gram filtration, then token-level longest-common-subsequence verification, following NIL and NiCad), so a near-miss (Type-3) clone with scattered small edits is still reported when both blocks are at least this similar and share more than half of their content-bearing tokens (names and literal values). `100` disables near-miss detection and reports exact (Type-1/2) matches plus gapped merges only. Applies to within-file detection only; literal-dense (data-table) blocks never near-miss match. A near-miss occurrence counts all of its code lines (edited lines included) toward `duplicateLineCount`, because the whole fragment is the clone.
92
+ - `minTokens` (default 40): minimum normalized token count for a region to count as a duplicate.
93
+ Raise it to report only substantial copies; lower it to catch small ones.
94
+ - `maxGapTokens` (default 30): copies edited in one spot split into two exact matches around the
95
+ edit; adjacent matches separated by at most this many tokens are merged back into a single gapped
96
+ (Type-3) clone group. `0` disables merging. Applies to within-file detection and to cross-file
97
+ matching alike.
98
+ - `minSimilarityPercent` (default 70): blocks the exact pipeline misses are additionally compared by
99
+ similarity (n-gram filtration, then token-level longest-common-subsequence verification, following
100
+ NIL and NiCad), so a near-miss (Type-3) clone with scattered small edits is still reported when
101
+ both blocks are at least this similar and share more than half of their content-bearing tokens.
102
+ `100` disables near-miss detection. Applies to within-file detection only.
134
103
 
135
- Custom detection settings are measured by the TypeScript backend; the native backend implements the defaults only.
104
+ Custom detection settings are measured by the TypeScript backend; the native backend implements the
105
+ defaults only.
136
106
 
137
107
  ## Metrics
138
108
 
109
+ `measureCode` reports, per file:
110
+
139
111
  - Physical LOC, code lines, comment-only lines, and blank lines
140
- - Function and class counts
141
- - Cyclomatic and cognitive complexity (per function and maximum), following the SonarSource cognitive-complexity specification (except its recursion increment, which is not counted) and cross-validated against PMD's Java rules
142
- - NCSS (non-commenting source statements, per function and per file), calibrated against PMD's `NcssCount` rule for Java and generalized to every supported language; unlike PMD, package and import declarations count (PMD counts them only with `NcssOption.COUNT_IMPORTS`), and statement-shaped content is counted uniformly in expression positions too (PMD's visitor never descends into lambdas or anonymous classes passed as call arguments, switch-expression bodies, or expression-bodied arrow cases)
143
- - Nesting depth
144
- - Intra-file call graph metrics: call counts, fan-in/fan-out, recursion, call depth, and parameter counts
145
- - Within-file duplication: copy-pasted blocks and statement runs matched on normalized tokens (identifiers anonymized consistently, literals by kind, and literal-dense data tables excluded unless their values also match), with adjacent matches around a small edit merged into gapped (Type-3) clone groups, plus duplicated line count and ratio
146
- - Cross-file duplication: copy-pasted blocks shared between files, matched with the same normalization and reported as groups with their file locations
147
- - File coupling (imports/exports) and cohesion (shared function identifiers)
148
- - Architecture metrics: transitive local dependencies, structural coordination and breadth, state mutation, and cross-file duplicate symbols
149
- - TypeScript type-shape metrics: annotations, aliases, interfaces, generics, unions, intersections, assertions, and conditional types
150
- - Halstead metrics and the maintainability index
112
+ - Per-function cognitive complexity (following the SonarSource specification, except its recursion
113
+ increment, which is not counted; cross-validated against PMD's Java rules), plus the file-level
114
+ total and maximum
115
+ - Per-function and per-file NCSS (non-commenting source statements), calibrated against PMD's
116
+ `NcssCount` rule for Java and generalized to every supported language; unlike PMD, package and
117
+ import declarations count, and statement-shaped content is counted uniformly in expression
118
+ positions too
119
+ - Per-function and file-level nesting depth
120
+ - Per-function parameter counts and locations (name, node type, line span)
121
+ - Within-file duplication: copy-pasted blocks matched on normalized tokens (identifiers anonymized
122
+ consistently, literals by kind, and literal-dense data tables excluded unless their values also
123
+ match), with adjacent matches around a small edit merged into gapped (Type-3) clone groups and
124
+ near-miss (Type-3) clones matched by token-LCS similarity, plus duplicated line count and ratio
125
+ - Cross-file duplication (via `measureCrossFileDuplication`): copy-pasted blocks shared between
126
+ files, matched with the same normalization and reported as groups with their file locations
127
+ - Halstead base counts, vocabulary, length, volume, and effort
128
+
129
+ Metrics that the validation literature shows to be weakly grounded or that invite misdirected
130
+ "improvements" (cyclomatic complexity, call-graph fan-in/fan-out, coupling and cohesion counts,
131
+ maintainability index, and similar) are intentionally not measured; see
132
+ [issue #44](https://github.com/WillBooster/code-gauge/issues/44) for the rationale and references.
151
133
 
152
134
  ## Supported languages
153
135
 
154
- Built-in parsers cover JavaScript, JSX, TypeScript, TSX, Python, Go, Rust, Java, Ruby, C, and C++. Additional tree-sitter grammars can be registered with `TreeMeasurer.registerLanguage`.
136
+ Built-in parsers cover JavaScript, JSX, TypeScript, TSX, Python, Go, Rust, Java, Ruby, C, and C++.
137
+ Additional tree-sitter grammars can be registered with `TreeMeasurer.registerLanguage`.
155
138
 
156
139
  ## Native (Rust) backend
157
140
 
158
- Measurement is also implemented as a Rust addon that produces bit-identical metrics roughly 13x faster than the TypeScript backend (the tree-sitter grammar crates are pinned to the same versions as the npm grammar packages). With a [Rust toolchain](https://rustup.rs) installed, build it once:
141
+ Measurement is also implemented as a Rust addon that produces bit-identical metrics roughly 13x
142
+ faster than the TypeScript backend (the tree-sitter grammar crates are pinned to the same versions
143
+ as the npm grammar packages). With a [Rust toolchain](https://rustup.rs) installed, build it once:
159
144
 
160
145
  ```sh
161
146
  yarn build-native
162
147
  ```
163
148
 
164
- `measureCode` and the CLI pick up `native/code-gauge.node` automatically and fall back to the TypeScript implementation when the addon is missing (for example, on npm installs) or when a custom language has been registered. Set `CODE_GAUGE_NATIVE=0` to force the TypeScript backend, and compare both with `yarn benchmark` (requires `yarn build` first). `isNativeBackendAvailable()` reports which backend is in use.
149
+ `measureCode` and the CLI pick up `native/code-gauge.node` automatically and fall back to the
150
+ TypeScript implementation when the addon is missing (for example, on npm installs) or when a custom
151
+ language has been registered. Set `CODE_GAUGE_NATIVE=0` to force the TypeScript backend, and compare
152
+ both with `yarn benchmark` (requires `yarn build` first). `isNativeBackendAvailable()` reports which
153
+ backend is in use.
165
154
 
166
155
  ## Programmatic API
167
156
 
@@ -180,5 +169,5 @@ function score(value) {
180
169
  { language: 'javascript' }
181
170
  );
182
171
 
183
- console.log(metrics.cyclomaticComplexity);
172
+ console.log(metrics.maxCognitiveComplexity);
184
173
  ```
package/dist/cli.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- "use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./crossFileDuplication.cjs"),n=require("./metrics.cjs"),r=require("./architectureMetrics.cjs"),i=require("./cliConfig.cjs"),a=require("./typescriptProject.cjs");let o=require("node:fs/promises"),s=require("node:os");s=e.__toESM(s,1);let c=require("node:path");c=e.__toESM(c,1);let l=require("commander");const u=new Map([[`.c`,`c`],[`.c++`,`cpp`],[`.cc`,`cpp`],[`.cjs`,`javascript`],[`.cp`,`cpp`],[`.cpp`,`cpp`],[`.tcc`,`cpp`],[`.cts`,`typescript`],[`.cxx`,`cpp`],[`.go`,`go`],[`.h`,`cpp`],[`.hh`,`cpp`],[`.hpp`,`cpp`],[`.hxx`,`cpp`],[`.java`,`java`],[`.js`,`javascript`],[`.jsx`,`jsx`],[`.mjs`,`javascript`],[`.mts`,`typescript`],[`.py`,`python`],[`.rb`,`ruby`],[`.rs`,`rust`],[`.ts`,`typescript`],[`.tsx`,`tsx`]]),d=new Set([`.agents`,`.claude`,`.cursor`,`.git`,`.next`,`.playwright-cli`,`.tox`,`.tmp`,`.turbo`,`.venv`,`.yarn`,`__fixtures__`,`__generated__`,`__pycache__`,`coverage`,`dist`,`fixtures`,`generated`,`node_modules`,`target`,`test-fixtures`,`vendor`,`venv`]),f=new Set([`__tests__`,`test`,`tests`,`spec`]),p=/(?:^test(?:[_-].*)?|\.(?:spec|test)|[_-](?:test|spec))\.[^.]+$/iu,m=/Test\.java$/u;h().catch(e=>{Q(`Error: ${$(e)}\n`),process.exitCode=1});async function h(){let e=new l.Command().name(`code-gauge`).description(`Measure code metrics and list high-risk findings.`).argument(`[target]`,`file or directory to measure`,`.`).option(`--config <path>`,`config file to use instead of the auto-detected ${i.configFileName}`).option(`--file-loc-threshold <number>`,`minimum file code LOC to report`,Y).option(`--function-loc-threshold <number>`,`minimum function physical LOC span to report`,Y).option(`--component-loc-threshold <number>`,`minimum React component physical LOC span to report`,Y).option(`--cognitive-threshold <number>`,`minimum cognitive complexity to report`,Y).option(`--cyclomatic-threshold <number>`,`minimum cyclomatic complexity to report`,Y).option(`--call-threshold <number>`,`minimum function call count to report`,Y).option(`--import-threshold <number>`,`minimum unique import sources per file to report`,Y).option(`--fan-out-threshold <number>`,`minimum intra-file fan-out per function to report`,Y).option(`--parameter-threshold <number>`,`minimum function parameter count to report`,Y).option(`--duplicate-block-threshold <number>`,`minimum count of duplicated code blocks per file to report`,Y).option(`--duplication-ratio-percent-threshold <number>`,`minimum percentage (1-100) of duplicated lines per file to report`,J).option(`--cross-file-duplicate-block-threshold <number>`,`minimum count of cross-file duplicate block groups per file to report`,Y).option(`--duplication-min-tokens <number>`,`minimum normalized token count for a duplicate region (default 40)`,Y).option(`--duplication-max-gap-tokens <number>`,`maximum token gap merged into one gapped clone group; 0 disables merging (default 30)`,ce).option(`--duplication-min-similarity-percent <number>`,`minimum similarity percent (1-100) for near-miss (Type-3) clone blocks; 100 reports exact matches only (default 70)`,J).option(`--transitive-dependency-threshold <number>`,`minimum transitively reachable local files to report`,Y).option(`--structural-breadth-threshold <number>`,`minimum structural breadth score to report`,Y).option(`--structural-coordination-threshold <number>`,`minimum structural coordination score to report`,Y).option(`--state-mutation-threshold <number>`,`minimum state mutation score to report`,Y).option(`--duplicate-symbol-group-threshold <number>`,`minimum duplicate symbol group count to report`,Y).option(`--max-findings <number>`,`maximum number of risk findings to print`,Y).option(`--largest-files <number>`,`number of largest files by code LOC to list`,Y).option(`--include-tests`,`include test files and test directories`).option(`--tsconfig <path>`,`TypeScript project file to use instead of auto-detected tsconfig.json`).option(`--json`,`print JSON output`).option(`--fail-on-error`,`exit with code 1 when files or directories cannot be scanned`).option(`--fail-on-risk`,`exit with code 1 when high-risk findings are found`);e.action(async(e,t)=>{let n=g(e),r=await i.loadConfig(t.config,await _(n)),a=i.resolveOptions(t,r),o=await v(n,a);T(o,a),await ne(o),await ee(o,a,n);let s=ie(o.files,o.architecture,o.crossFileDuplication,o.componentFunctionKeys,o.namedComponentFunctionKeys,a,o.displayRoot);a.json?P(o,s,a):F(n,o,s,a),(o.fatalError||a.failOnError&&o.errors.length>0||a.failOnRisk&&s.length>0)&&(process.exitCode=1)}),await e.parseAsync()}function g(e){return e===`~`?s.default.homedir():e.startsWith(`~/`)?c.default.join(s.default.homedir(),e.slice(2)):c.default.resolve(e)}async function _(e){try{return(await(0,o.stat)(e)).isDirectory()?e:c.default.dirname(e)}catch{return c.default.dirname(e)}}async function v(e,t){let n=[],r=[],i=[],a=e;try{a=await(0,o.realpath)(e)}catch{}let s=c.default.dirname(a),l;try{l=await(0,o.stat)(a)}catch(e){let t=`${X(a,s)}: ${$(e)}`;return{displayRoot:s,files:n,errors:[t],warnings:i,fatalError:t}}if(l.isFile()){let e=c.default.dirname(a),o=q(a,t,!0);if(!o){let t=`${X(a,e)}: unsupported file type`;return{displayRoot:e,files:n,errors:[t],warnings:i,fatalError:t}}let s=y(t,n,r,i,e);return await w(a,o,`single-file`,s,a),{displayRoot:e,files:n,errors:r,warnings:i}}return await S(a,y(t,n,r,i,a)),{displayRoot:a,files:n,errors:r,warnings:i}}function y(e,t,n,r,i){return{options:e,files:t,errors:n,warnings:r,visitedDirectories:new Set,visitedFiles:new Set,rootDirectory:i}}async function ee(e,t,n){if(e.fatalError||e.files.length===0)return;let r=t.tsconfig,i=r!==void 0;if(!i&&!e.files.some(({file:e})=>b(e)))return;let o=r?g(r):await x(n);if(o)try{e.typeScriptProject=await a.measureTypeScriptProject(o,e.files.map(({file:e})=>e)),e.componentFunctionKeys=new Set(e.typeScriptProject.reactComponentFunctions.map(e=>A(e.file,e.startLine,e.startColumn))),e.namedComponentFunctionKeys=new Set(e.typeScriptProject.reactComponentFunctions.flatMap(e=>e.name?[j(e.file,e.name,e.startLine)]:[]))}catch(t){i&&e.errors.push(`${X(o,e.displayRoot)}: ${$(t)}`)}}function b(e){return[`.cjs`,`.cts`,`.js`,`.jsx`,`.mjs`,`.mts`,`.ts`,`.tsx`].includes(c.default.extname(e))}async function x(e){let t=(await(0,o.stat)(e)).isDirectory()?e:c.default.dirname(e);for(;;){let e=c.default.join(t,`tsconfig.json`);if(await te(e))return e;let n=c.default.dirname(t);if(n===t)return;t=n}}async function te(e){try{return(await(0,o.stat)(e)).isFile()}catch{return!1}}async function ne(e){if(!e.fatalError)try{e.architecture=r.measureArchitecture(e.files.map(({file:e,metrics:t})=>({file:e,metrics:t})),e.displayRoot)}catch(t){e.errors.push(`architecture metrics: ${$(t)}`)}}async function S(e,t){let n;try{n=await(0,o.realpath)(e)}catch(n){t.errors.push(`${X(e,t.rootDirectory)}: ${$(n)}`);return}if(!K(n,t.rootDirectory)||t.visitedDirectories.has(n))return;t.visitedDirectories.add(n);let r;try{r=await(0,o.readdir)(e,{withFileTypes:!0})}catch(n){t.errors.push(`${X(e,t.rootDirectory)}: ${$(n)}`);return}for(let n of r){let r=c.default.join(e,n.name);if(n.isSymbolicLink()){await re(n.name,r,t);continue}if(n.isDirectory()){if(G(n.name,t.options))continue;await S(r,t);continue}n.isFile()&&await C(r,t)}}async function re(e,t,n){let r;try{r=await(0,o.realpath)(t)}catch(e){n.errors.push(`${X(t,n.rootDirectory)}: ${$(e)}`);return}if(!K(r,n.rootDirectory))return;let i;try{i=await(0,o.stat)(t)}catch(e){n.errors.push(`${X(t,n.rootDirectory)}: ${$(e)}`);return}if(i.isDirectory()){if(G(e,n.options)||G(c.default.basename(r),n.options))return;await S(t,n);return}i.isFile()&&await C(t,n,r,r)}async function C(e,t,n=e,r){let i=q(n,t.options);i&&await w(e,i,`directory`,t,r)}async function w(e,t,r,i,a){try{let s=a??await(0,o.realpath)(e);if(i.visitedFiles.has(s))return;i.visitedFiles.add(s);let c=await(0,o.readFile)(e,`utf8`),l={language:t,duplication:i.options.duplication},u={file:e,metrics:n.measureCode(c,l)};if(r===`directory`)try{u.duplicationCandidates=n.collectCrossFileDuplicationFileData(c,l)}catch(t){i.warnings.push(`${X(e,i.rootDirectory)}: cross-file duplication candidates unavailable: ${$(t)}`)}i.files.push(u)}catch(t){i.errors.push(`${X(e,i.rootDirectory)}: ${$(t)}`)}}function T(e,n){if(e.fatalError||e.files.length<2)return;let r=e.files.flatMap(({file:t,duplicationCandidates:n})=>n?[{file:X(t,e.displayRoot),...n}]:[]);r.length<2||(e.crossFileDuplication=t.measureCrossFileDuplication(r,n.duplication))}function ie(e,t,n,r,a,o,s){let c=new Map(t?.files.map(e=>[e.file,e])),l=e.flatMap(({file:e,metrics:t})=>{let l=t.functions.some(t=>t.returnsJsx||k(e,t,r,a)),u=i.resolveThresholds(o,t.language,l);return[...ae(e,t,c.get(X(e,s)),n,u,s),...t.functions.flatMap(n=>oe(e,t.language,n,u,s,r,a))]});return l.sort(N),l}function ae(e,t,n,r,i,a){let o=[],s=X(e,a);E(o,`file LOC`,t.lines.code,i.fileLoc),E(o,`import sources`,t.coupling.importSourceCount,i.import);let c=O(t.duplication.duplicateBlockGroups);return E(o,`duplicated blocks`,t.duplication.duplicateBlockCount,i.duplicateBlock,c),E(o,`duplicated lines (%)`,Math.floor(t.duplication.duplicationRatio*100),i.duplicationRatioPercent,c),r&&E(o,`cross-file duplicated blocks`,Object.hasOwn(r.duplicateBlockGroupCountByFile,s)?r.duplicateBlockGroupCountByFile[s]??0:0,i.crossFileDuplicateBlock,D(r,s)),n&&((t.lines.code>=100||n.directLocalDependencyCount>=8)&&E(o,`transitive local dependencies`,n.transitiveLocalDependencyCount,i.transitiveDependency),(o.length>0||n.directLocalDependencyCount>=8||n.structuralCoordination.score>=i.structuralCoordination)&&E(o,`structural breadth`,n.structuralBreadthScore,i.structuralBreadth),E(o,`structural coordination`,n.structuralCoordination.score,i.structuralCoordination),E(o,`state mutation`,n.structuralCoordination.stateMutationScore,i.stateMutation),E(o,`duplicate symbol groups`,n.duplicateSymbolGroupCount,i.duplicateSymbolGroup)),o.length===0?[]:[{file:s,language:t.language,kind:`file`,cyclomaticComplexity:t.cyclomaticComplexity,cognitiveComplexity:t.cognitiveComplexity,triggers:o,score:M(o)}]}function oe(e,t,n,r,i,a,o){let s=n.endLine-n.startLine+1,c=k(e,n,a,o),l=c?`component`:`function`,u=[];return E(u,`cognitive complexity`,n.cognitiveComplexity,r.cognitive),E(u,`cyclomatic complexity`,n.cyclomaticComplexity,r.cyclomatic),E(u,c?`component LOC`:`function LOC`,s,se(c,r)),E(u,`function calls`,n.callCount,r.call),E(u,`fan-out`,n.fanOut,r.fanOut),E(u,`parameters`,n.parameterCount,r.parameter),u.length===0?[]:[{file:X(e,i),language:t,kind:l,name:n.name??`<anonymous>`,startLine:n.startLine,endLine:n.endLine,cyclomaticComplexity:n.cyclomaticComplexity,cognitiveComplexity:n.cognitiveComplexity,triggers:u,score:M(u)}]}function E(e,t,n,r,i){n<r||e.push({metric:t,value:n,threshold:r,score:n/r,detail:i})}function D(e,t){let n=e.groups.filter(e=>e.files.includes(t));if(n.length!==0)return`${n.slice(0,3).map(e=>e.occurrences.map(({file:e,startLine:n,endLine:r})=>e===t?`${n}-${r}`:`${e}:${n}-${r}`).join(` ~ `)).join(`; `)}${n.length>3?`; ...`:``}`}function O(e){if(e.length!==0)return e.map(e=>e.map(({startLine:e,endLine:t})=>`${e}-${t}`).join(` ~ `)).join(`; `)}function k(e,t,n,r){return n?.has(A(e,t.startLine,t.startColumn))||(t.name?r?.has(j(e,t.name,t.startLine)):!1)||!1}function se(e,t){return e?t.componentLoc:t.functionLoc}function A(e,t,n){return`${c.default.resolve(e)}:${t}:${n}`}function j(e,t,n){return`${c.default.resolve(e)}:${t}:${n}`}function M(e){return Math.max(...e.map(e=>e.score))}function N(e,t){return t.score-e.score||e.file.localeCompare(t.file)||(e.startLine??0)-(t.startLine??0)||(e.endLine??0)-(t.endLine??0)||e.kind.localeCompare(t.kind)}function P(e,t,n){let r=W(e.files),i=t.slice(0,n.maxFindings);Z(JSON.stringify({summary:r,thresholds:n.thresholds,profileThresholds:n.profileThresholds,totalRisks:t.length,truncated:i.length<t.length,largestFiles:n.largestFiles>0?I(e.files,n.largestFiles,e.displayRoot):void 0,architecture:e.architecture,crossFileDuplication:e.crossFileDuplication,typeScriptProject:e.typeScriptProject,risks:i,errors:e.errors,warnings:e.warnings},void 0,2)+`
3
- `)}function F(e,t,n,r){if(t.fatalError){Q(`Error: ${t.fatalError}\n`);return}let{thresholds:i}=r,a=W(t.files);Z(`Measured ${a.fileCount} files under ${e}\n`),Z(`LOC ${a.linesOfCode}, NCSS ${a.ncssCount}, functions ${a.functionCount}, max cyclomatic ${a.maxCyclomaticComplexity}, max cognitive ${a.maxCognitiveComplexity}\n`),Z(`Calls ${a.callCount}, internal edges ${a.internalEdgeCount}, max call depth ${a.maxCallDepth}, imports ${a.importSourceCount}, exports ${a.exportCount}\n`),Z(`Type annotations ${a.typeAnnotationCount}, type aliases ${a.typeAliasCount}, interfaces ${a.interfaceCount}, avg cohesion ${a.averageFunctionIdentifierOverlap.toFixed(2)}\n`),t.architecture&&Z(`${H(t.architecture)}\n`),t.typeScriptProject&&Z(`${U(t.typeScriptProject)}\n`),Z(`Risk thresholds: file LOC >= ${i.fileLoc}, function LOC >= ${i.functionLoc}, component LOC >= ${i.componentLoc}, cognitive >= ${i.cognitive}, cyclomatic >= ${i.cyclomatic}, calls >= ${i.call}, imports >= ${i.import}, fan-out >= ${i.fanOut}, parameters >= ${i.parameter}, duplicated blocks >= ${i.duplicateBlock}, duplicated lines (%) >= ${i.duplicationRatioPercent}, cross-file duplicated blocks >= ${i.crossFileDuplicateBlock}\n`);let o=L(r.profileThresholds);if(o&&Z(`Per-language overrides: ${o}\n`),n.length===0)Z(`No high-risk findings found.
4
- `);else{let e=n.slice(0,r.maxFindings),t=n.length>e.length?` of ${n.length}`:``;Z(`\nHigh-risk findings (top ${e.length}${t}):\n`);for(let t of e)Z(`${R(t)} ${z(t)} ${B(t)}\n`)}let s=t.crossFileDuplication?.groups??[];if(s.length>0){let e=s.slice(0,10),t=s.length>e.length?` of ${s.length}`:``;Z(`\nCross-file duplicate blocks (top ${e.length}${t}):\n`);for(let t of e)Z(`${t.tokenCount} tokens: ${t.occurrences.map(({file:e,startLine:t,endLine:n})=>`${e}:${t}-${n}`).join(`, `)}\n`)}let c=t.architecture?.duplicateSymbolGroups??[];if(c.length>0){let e=c.toSorted((e,t)=>t.files.length-e.files.length||e.name.localeCompare(t.name)).slice(0,10),t=c.length>e.length?` of ${c.length}`:``;Z(`\nDuplicate symbols (top ${e.length}${t}):\n`);for(let t of e)Z(`${t.name}: ${t.declarations.map(e=>`${e.file}:${e.line}`).join(`, `)}\n`)}if(r.largestFiles>0){let e=I(t.files,r.largestFiles,t.displayRoot);Z(`\nLargest files by code LOC (top ${e.length}):\n`);for(let{file:t,codeLoc:n}of e)Z(`${t} (code LOC ${n})\n`)}if(t.warnings.length>0){Q(`\nDegraded ${t.warnings.length} files (measured, but excluded from cross-file matching):\n`);for(let e of t.warnings.slice(0,10))Q(`- ${e}\n`);t.warnings.length>10&&Q(`- ... ${t.warnings.length-10} more\n`)}if(t.errors.length>0){Q(`\nSkipped ${t.errors.length} files or directories:\n`);for(let e of t.errors.slice(0,10))Q(`- ${e}\n`);t.errors.length>10&&Q(`- ... ${t.errors.length-10} more\n`)}}function I(e,t,n){return e.map(({file:e,metrics:t})=>({file:X(e,n),codeLoc:t.lines.code})).toSorted((e,t)=>t.codeLoc-e.codeLoc||e.file.localeCompare(t.file)).slice(0,t)}function L(e){return Object.entries(e).map(([e,t])=>`${e} { ${Object.entries(t).map(([e,t])=>`${e} ${t}`).join(`, `)} }`).join(`; `)}function R(e){return e.startLine===void 0||e.endLine===void 0?e.file:`${e.file}:${e.startLine}-${e.endLine}`}function z(e){return e.name?`${e.kind} ${e.name}`:e.kind}function B(e){return`(${e.triggers.map(e=>`${e.metric} ${V(e.value)} >= ${V(e.threshold)}${e.detail?` [${e.detail}]`:``}`).join(`, `)}; cyclomatic ${e.cyclomaticComplexity}, cognitive ${e.cognitiveComplexity})`}function V(e){return Number.isInteger(e)?String(e):e.toFixed(2)}function H(e){let t=Math.max(0,...e.files.map(e=>e.structuralCoordination.stateMutationScore));return`Architecture max reachable files ${e.maxTransitiveLocalDependencyCount}, max structural breadth ${e.maxStructuralBreadthScore}, max structural coordination ${e.maxStructuralCoordinationScore}, max state mutation ${t}, duplicate symbol groups ${e.duplicateSymbolGroups.length}`}function U(e){return`TypeScript project root files ${e.rootFileCount}, measured roots ${e.measuredRootFileCount}, semantic diagnostics ${e.semanticDiagnosticCount}, resolved calls ${e.resolvedCallExpressionCount}/${e.callExpressionCount} (${(e.resolvedCallExpressionRatio*100).toFixed(1)}%)`}function W(e){let t=0,n=0,r=0,i=0,a=0,o=0,s=0,c=0,l=0,u=0,d=0,f=0,p=0,m=0,h=0,g=0,_=0;for(let v of e)t+=v.metrics.functionCount,n+=v.metrics.lines.code,r=Math.max(r,v.metrics.maxCyclomaticComplexity),i=Math.max(i,v.metrics.maxCognitiveComplexity),a+=v.metrics.ncssCount,o+=v.metrics.callGraph.callCount,s+=v.metrics.callGraph.internalEdgeCount,c=Math.max(c,v.metrics.callGraph.maxCallDepth),l+=v.metrics.coupling.importSourceCount,u+=v.metrics.coupling.relativeImportCount,d+=v.metrics.coupling.externalImportCount,f+=v.metrics.coupling.exportCount,p+=v.metrics.cohesion.averageFunctionIdentifierOverlap,m+=v.metrics.typeComplexity.typeAnnotationCount,h+=v.metrics.typeComplexity.typeAliasCount,g+=v.metrics.typeComplexity.interfaceCount,_+=v.metrics.typeComplexity.genericParameterCount;return{fileCount:e.length,functionCount:t,linesOfCode:n,maxCyclomaticComplexity:r,maxCognitiveComplexity:i,ncssCount:a,callCount:o,internalEdgeCount:s,maxCallDepth:c,importSourceCount:l,relativeImportCount:u,externalImportCount:d,exportCount:f,averageFunctionIdentifierOverlap:e.length===0?0:p/e.length,typeAnnotationCount:m,typeAliasCount:h,interfaceCount:g,genericParameterCount:_}}function G(e,t){return d.has(e)?!0:!t.includeTests&&f.has(e)}function K(e,t){let n=c.default.relative(t,e);return n===``||n!==`..`&&!n.startsWith(`..${c.default.sep}`)&&!c.default.isAbsolute(n)}function q(e,t,n=!1){let r=e.toLowerCase();if(!(!n&&(r.endsWith(`.d.ts`)||r.endsWith(`.d.mts`)||r.endsWith(`.d.cts`)||r.endsWith(`.min.js`)||r.endsWith(`.pnp.cjs`)))&&!(!n&&!t.includeTests&&(p.test(c.default.basename(e))||m.test(c.default.basename(e)))))return c.default.extname(e)===`.C`?`cpp`:u.get(c.default.extname(r))}function J(e){let t=Y(e);if(t>100)throw new l.InvalidArgumentError(`Expected an integer between 1 and 100.`);return t}function ce(e){if(!/^\d+$/u.test(e))throw new l.InvalidArgumentError(`Expected a non-negative integer.`);let t=Number(e);if(!Number.isSafeInteger(t)||t<0)throw new l.InvalidArgumentError(`Expected a non-negative integer.`);return t}function Y(e){if(!/^[1-9]\d*$/u.test(e))throw new l.InvalidArgumentError(`Expected a positive integer.`);let t=Number(e);if(!Number.isSafeInteger(t)||t<1)throw new l.InvalidArgumentError(`Expected a positive integer.`);return t}function X(e,t){return c.default.relative(t,e)||c.default.basename(e)}function Z(e){process.stdout.write(e)}function Q(e){process.stderr.write(e)}function $(e){return e instanceof Error?e.message:String(e)}
2
+ "use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./crossFileDuplication.cjs"),n=require("./metrics.cjs"),r=require("./cliConfig.cjs");let i=require("node:fs/promises"),a=require("node:os");a=e.__toESM(a,1);let o=require("node:path");o=e.__toESM(o,1);let s=require("commander");const c=new Map([[`.c`,`c`],[`.c++`,`cpp`],[`.cc`,`cpp`],[`.cjs`,`javascript`],[`.cp`,`cpp`],[`.cpp`,`cpp`],[`.tcc`,`cpp`],[`.cts`,`typescript`],[`.cxx`,`cpp`],[`.go`,`go`],[`.h`,`cpp`],[`.hh`,`cpp`],[`.hpp`,`cpp`],[`.hxx`,`cpp`],[`.java`,`java`],[`.js`,`javascript`],[`.jsx`,`jsx`],[`.mjs`,`javascript`],[`.mts`,`typescript`],[`.py`,`python`],[`.rb`,`ruby`],[`.rs`,`rust`],[`.ts`,`typescript`],[`.tsx`,`tsx`]]),l=new Set([`.agents`,`.claude`,`.cursor`,`.git`,`.next`,`.playwright-cli`,`.tox`,`.tmp`,`.turbo`,`.venv`,`.yarn`,`__fixtures__`,`__generated__`,`__pycache__`,`coverage`,`dist`,`fixtures`,`generated`,`node_modules`,`target`,`test-fixtures`,`vendor`,`venv`]),u=new Set([`__tests__`,`test`,`tests`,`spec`]),d=/(?:^test(?:[_-].*)?|\.(?:spec|test)|[_-](?:test|spec))\.[^.]+$/iu,f=/Test\.java$/u;p().catch(e=>{B(`Error: ${V(e)}\n`),process.exitCode=1});async function p(){let e=new s.Command().name(`code-gauge`).description(`Rank the files of a project by refactoring priority.`).argument(`[target]`,`file or directory to measure`,`.`).option(`--config <path>`,`config file to use instead of the auto-detected ${r.configFileName}`).option(`--top <number>`,`number of top-ranked files to report (default: 10)`,L).option(`--duplication-min-tokens <number>`,`minimum normalized token count for a duplicate region (default 40)`,L).option(`--duplication-max-gap-tokens <number>`,`maximum token gap merged into one gapped clone group; 0 disables merging (default 30)`,I).option(`--duplication-min-similarity-percent <number>`,`minimum similarity percent (1-100) for near-miss (Type-3) clone blocks; 100 reports exact matches only (default 70)`,F).option(`--include-tests`,`include test files and test directories`).option(`--json`,`print JSON output`).option(`--fail-on-error`,`exit with code 1 when files or directories cannot be scanned`);e.action(async(e,t)=>{let n=m(e),i=await r.loadConfig(t.config,await h(n)),a=r.resolveOptions(t,i),o=await g(n,a);S(o,a);let s=C(o,a.top);a.json?O(o,s,a):k(n,o,s,a),(o.fatalError||a.failOnError&&o.errors.length>0)&&(process.exitCode=1)}),await e.parseAsync()}function m(e){return e===`~`?a.default.homedir():e.startsWith(`~/`)?o.default.join(a.default.homedir(),e.slice(2)):o.default.resolve(e)}async function h(e){try{return(await(0,i.stat)(e)).isDirectory()?e:o.default.dirname(e)}catch{return o.default.dirname(e)}}async function g(e,t){let n=[],r=[],a=[],s=e;try{s=await(0,i.realpath)(e)}catch{}let c=o.default.dirname(s),l;try{l=await(0,i.stat)(s)}catch(e){let t=`${R(s,c)}: ${V(e)}`;return{displayRoot:c,files:n,errors:[t],warnings:a,fatalError:t}}if(l.isFile()){let e=o.default.dirname(s),i=P(s,t,!0);if(!i){let t=`${R(s,e)}: unsupported file type`;return{displayRoot:e,files:n,errors:[t],warnings:a,fatalError:t}}let c=_(t,n,r,a,e);return await x(s,i,`single-file`,c,s),{displayRoot:e,files:n,errors:r,warnings:a}}return await v(s,_(t,n,r,a,s)),{displayRoot:s,files:n,errors:r,warnings:a}}function _(e,t,n,r,i){return{options:e,files:t,errors:n,warnings:r,visitedDirectories:new Set,visitedFiles:new Set,rootDirectory:i}}async function v(e,t){let n;try{n=await(0,i.realpath)(e)}catch(n){t.errors.push(`${R(e,t.rootDirectory)}: ${V(n)}`);return}if(!N(n,t.rootDirectory)||t.visitedDirectories.has(n))return;t.visitedDirectories.add(n);let r;try{r=await(0,i.readdir)(e,{withFileTypes:!0})}catch(n){t.errors.push(`${R(e,t.rootDirectory)}: ${V(n)}`);return}for(let n of r){let r=o.default.join(e,n.name);if(n.isSymbolicLink()){await y(n.name,r,t);continue}if(n.isDirectory()){if(M(n.name,t.options))continue;await v(r,t);continue}n.isFile()&&await b(r,t)}}async function y(e,t,n){let r;try{r=await(0,i.realpath)(t)}catch(e){n.errors.push(`${R(t,n.rootDirectory)}: ${V(e)}`);return}if(!N(r,n.rootDirectory))return;let a;try{a=await(0,i.stat)(t)}catch(e){n.errors.push(`${R(t,n.rootDirectory)}: ${V(e)}`);return}if(a.isDirectory()){if(M(e,n.options)||M(o.default.basename(r),n.options))return;await v(t,n);return}a.isFile()&&await b(t,n,r,r)}async function b(e,t,n=e,r){let i=P(n,t.options);i&&await x(e,i,`directory`,t,r)}async function x(e,t,r,a,o){try{let s=o??await(0,i.realpath)(e);if(a.visitedFiles.has(s))return;a.visitedFiles.add(s);let c=await(0,i.readFile)(e,`utf8`),l={language:t,duplication:a.options.duplication},u={file:e,metrics:n.measureCode(c,l)};if(r===`directory`)try{u.duplicationCandidates=n.collectCrossFileDuplicationFileData(c,l)}catch(t){a.warnings.push(`${R(e,a.rootDirectory)}: cross-file duplication candidates unavailable: ${V(t)}`)}a.files.push(u)}catch(t){a.errors.push(`${R(e,a.rootDirectory)}: ${V(t)}`)}}function S(e,n){if(e.fatalError||e.files.length<2)return;let r=e.files.flatMap(({file:t,duplicationCandidates:n})=>n?[{file:R(t,e.displayRoot),...n}]:[]);r.length<2||(e.crossFileDuplication=t.measureCrossFileDuplication(r,n.duplication))}function C(e,t){let n=e.files.map(({file:t,metrics:n})=>{let r=R(t,e.displayRoot),i=D(n,e.crossFileDuplication,r);return{file:r,worstFunction:E(n.functions),duplicatedLineCount:i.size,duplicatedLineRatio:n.lines.code===0?0:i.size/n.lines.code,crossFilePartners:[],ncss:n.ncssCount,codeLines:n.lines.code}}),r=T(n.map(e=>e.worstFunction?.cognitiveComplexity??0)),i=T(n.map(e=>e.duplicatedLineCount)),a=T(n.map(e=>e.ncss)),o=n.map(e=>({...e,score:r(e.worstFunction?.cognitiveComplexity??0)+i(e.duplicatedLineCount)+a(e.ncss)})).toSorted((e,t)=>t.score-e.score||t.ncss-e.ncss||e.file.localeCompare(t.file));return w(o.slice(0,t),e.crossFileDuplication),o}function w(e,t){if(!t)return;let n=new Map(e.map(e=>[e.file,new Set]));for(let e of t.groups)for(let t of e.files){let r=n.get(t);if(r)for(let n of e.files)n!==t&&r.add(n)}for(let t of e)t.crossFilePartners=[...n.get(t.file)??[]].toSorted()}function T(e){let t=e.toSorted((e,t)=>e-t);return e=>{let n=0,r=t.length;for(;n<r;){let i=n+r>>1;t[i]<e?n=i+1:r=i}return t.length===0?0:n/t.length}}function E(e){let t;for(let n of e)(!t||n.cognitiveComplexity>t.cognitiveComplexity||n.cognitiveComplexity===t.cognitiveComplexity&&n.ncss>t.ncss)&&(t=n);if(t)return{name:t.name??`<anonymous>`,startLine:t.startLine,endLine:t.endLine,cognitiveComplexity:t.cognitiveComplexity,ncss:t.ncss,nestingDepth:t.nestingDepth}}function D(e,t,n){let r=new Set(e.duplication.duplicateLineNumbers),i=t&&Object.hasOwn(t.duplicateLineNumbersByFile,n)?t.duplicateLineNumbersByFile[n]??[]:[];for(let e of i)r.add(e);return r}function O(e,t,n){let r=t.slice(0,n.top);z(JSON.stringify({summary:j(e.files),totalRankedFiles:t.length,truncated:r.length<t.length,files:r,errors:e.errors,warnings:e.warnings},void 0,2)+`
3
+ `)}function k(e,t,n,r){if(t.fatalError){B(`Error: ${t.fatalError}\n`);return}let i=j(t.files);if(z(`Measured ${i.fileCount} files under ${e} (code LOC ${i.linesOfCode}, NCSS ${i.ncssCount}, functions ${i.functionCount})\n`),n.length===0)z(`No measurable files found.
4
+ `);else{let e=n.slice(0,r.top),t=n.length>e.length?` of ${n.length}`:``;z(`\nRefactoring candidates (top ${e.length}${t}):\n`);for(let[t,n]of e.entries())z(`${t+1}. ${A(n)}\n`)}if(t.warnings.length>0){B(`\nDegraded ${t.warnings.length} files (measured, but excluded from cross-file matching):\n`);for(let e of t.warnings.slice(0,10))B(`- ${e}\n`);t.warnings.length>10&&B(`- ... ${t.warnings.length-10} more\n`)}if(t.errors.length>0){B(`\nSkipped ${t.errors.length} files or directories:\n`);for(let e of t.errors.slice(0,10))B(`- ${e}\n`);t.errors.length>10&&B(`- ... ${t.errors.length-10} more\n`)}}function A(e){let t=[];if(e.worstFunction){let n=e.worstFunction;t.push(`worst function ${n.name} (L${n.startLine}-${n.endLine}) cognitive ${n.cognitiveComplexity}, NCSS ${n.ncss}, nesting ${n.nestingDepth}`)}if(e.duplicatedLineCount>0){let n=e.crossFilePartners.length>0?`, shared with ${e.crossFilePartners.slice(0,3).join(`, `)}${e.crossFilePartners.length>3?`, ...`:``}`:``;t.push(`duplicated lines ${e.duplicatedLineCount} (${Math.round(e.duplicatedLineRatio*100)}%${n})`)}return t.push(`file NCSS ${e.ncss}`),`${e.file} (score ${e.score.toFixed(2)}): ${t.join(`; `)}`}function j(e){let t=0,n=0,r=0,i=0;for(let a of e)t+=a.metrics.functions.length,n+=a.metrics.lines.code,r=Math.max(r,a.metrics.maxCognitiveComplexity),i+=a.metrics.ncssCount;return{fileCount:e.length,functionCount:t,linesOfCode:n,maxCognitiveComplexity:r,ncssCount:i}}function M(e,t){return l.has(e)?!0:!t.includeTests&&u.has(e)}function N(e,t){let n=o.default.relative(t,e);return n===``||n!==`..`&&!n.startsWith(`..${o.default.sep}`)&&!o.default.isAbsolute(n)}function P(e,t,n=!1){let r=e.toLowerCase();if(!(!n&&(r.endsWith(`.d.ts`)||r.endsWith(`.d.mts`)||r.endsWith(`.d.cts`)||r.endsWith(`.min.js`)||r.endsWith(`.pnp.cjs`)))&&!(!n&&!t.includeTests&&(d.test(o.default.basename(e))||f.test(o.default.basename(e)))))return o.default.extname(e)===`.C`?`cpp`:c.get(o.default.extname(r))}function F(e){let t=L(e);if(t>100)throw new s.InvalidArgumentError(`Expected an integer between 1 and 100.`);return t}function I(e){if(!/^\d+$/u.test(e))throw new s.InvalidArgumentError(`Expected a non-negative integer.`);let t=Number(e);if(!Number.isSafeInteger(t)||t<0)throw new s.InvalidArgumentError(`Expected a non-negative integer.`);return t}function L(e){if(!/^[1-9]\d*$/u.test(e))throw new s.InvalidArgumentError(`Expected a positive integer.`);let t=Number(e);if(!Number.isSafeInteger(t)||t<1)throw new s.InvalidArgumentError(`Expected a positive integer.`);return t}function R(e,t){return o.default.relative(t,e)||o.default.basename(e)}function z(e){process.stdout.write(e)}function B(e){process.stderr.write(e)}function V(e){return e instanceof Error?e.message:String(e)}
5
5
  //# sourceMappingURL=cli.cjs.map
package/dist/cli.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.cjs","names":["Command","configFileName","loadConfig","resolveOptions","os","path","stat","realpath","measureTypeScriptProject","measureArchitecture","readdir","readFile","measureCode","collectCrossFileDuplicationFileData","measureCrossFileDuplication","resolveThresholds","InvalidArgumentError"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { readdir, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { Command, InvalidArgumentError } from 'commander';\nimport { measureArchitecture, type ArchitectureFileMetrics, type ArchitectureMetrics } from './architectureMetrics.js';\nimport {\n type CliOptions,\n configFileName,\n loadConfig,\n type ResolvedOptions,\n resolveOptions,\n resolveThresholds,\n type Thresholds,\n} from './cliConfig.js';\nimport { measureCrossFileDuplication, type CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData } from './duplication.js';\nimport { collectCrossFileDuplicationFileData, measureCode } from './metrics.js';\nimport { measureTypeScriptProject, type TypeScriptProjectMetrics } from './typescriptProject.js';\nimport type { CodeMetrics, FunctionMetrics, LanguageName } from './types.js';\n\ninterface FileMetrics {\n file: string;\n metrics: CodeMetrics;\n /** Cross-file duplicate candidates and token/statement data, collected only for directory scans. */\n duplicationCandidates?: CrossFileDuplicationFileData;\n}\n\ninterface RiskTrigger {\n /** Optional location hint (e.g. duplicated block line ranges) appended to the printed trigger. */\n detail?: string;\n metric: string;\n score: number;\n threshold: number;\n value: number;\n}\n\ninterface RiskFinding {\n cognitiveComplexity: number;\n cyclomaticComplexity: number;\n endLine?: number;\n file: string;\n kind: 'component' | 'file' | 'function';\n language: LanguageName;\n name?: string;\n score: number;\n startLine?: number;\n triggers: RiskTrigger[];\n}\n\ninterface ScanResult {\n architecture?: ArchitectureMetrics;\n componentFunctionKeys?: Set<string>;\n crossFileDuplication?: CrossFileDuplicationMetrics;\n displayRoot: string;\n errors: string[];\n /** Non-fatal degradations (e.g. cross-file candidates unavailable); the file is still measured. */\n warnings: string[];\n fatalError?: string;\n files: FileMetrics[];\n namedComponentFunctionKeys?: Set<string>;\n typeScriptProject?: TypeScriptProjectMetrics;\n}\n\nconst languageByExtension = new Map<string, LanguageName>([\n ['.c', 'c'],\n ['.c++', 'cpp'],\n ['.cc', 'cpp'],\n ['.cjs', 'javascript'],\n ['.cp', 'cpp'],\n ['.cpp', 'cpp'],\n ['.tcc', 'cpp'],\n ['.cts', 'typescript'],\n ['.cxx', 'cpp'],\n ['.go', 'go'],\n // Headers may be C or C++; the C++ grammar parses both.\n ['.h', 'cpp'],\n ['.hh', 'cpp'],\n ['.hpp', 'cpp'],\n ['.hxx', 'cpp'],\n ['.java', 'java'],\n ['.js', 'javascript'],\n ['.jsx', 'jsx'],\n ['.mjs', 'javascript'],\n ['.mts', 'typescript'],\n ['.py', 'python'],\n ['.rb', 'ruby'],\n ['.rs', 'rust'],\n ['.ts', 'typescript'],\n ['.tsx', 'tsx'],\n]);\n\nconst ignoredDirectoryNames = new Set([\n '.agents',\n '.claude',\n '.cursor',\n '.git',\n '.next',\n '.playwright-cli',\n '.tox',\n '.tmp',\n '.turbo',\n '.venv',\n '.yarn',\n '__fixtures__',\n '__generated__',\n '__pycache__',\n 'coverage',\n 'dist',\n 'fixtures',\n 'generated',\n 'node_modules',\n 'target',\n 'test-fixtures',\n 'vendor',\n 'venv',\n]);\n\n/** Caps the `Duplicate symbols` section so large repositories do not flood the report. */\nconst maxDuplicateSymbolGroupLines = 10;\n/** Caps the `Cross-file duplicate blocks` section so large repositories do not flood the report. */\nconst maxCrossFileDuplicateGroupLines = 10;\n/** Caps how many cross-file group locations a single risk finding repeats as detail. */\nconst maxCrossFileDuplicateDetailGroups = 3;\n\nconst testDirectoryNames = new Set(['__tests__', 'test', 'tests', 'spec']);\nconst testFilePattern = /(?:^test(?:[_-].*)?|\\.(?:spec|test)|[_-](?:test|spec))\\.[^.]+$/iu;\n// JUnit tests use a case-sensitive `Test.java` suffix; case-insensitive matching would catch\n// production files like `contest.java`.\nconst javaTestFilePattern = /Test\\.java$/u;\n\n// oxlint-disable-next-line unicorn/prefer-top-level-await -- CommonJS build output cannot preserve top-level await.\nvoid main().catch((error: unknown) => {\n writeStderr(`Error: ${formatError(error)}\\n`);\n process.exitCode = 1;\n});\n\nasync function main(): Promise<void> {\n const program = new Command()\n .name('code-gauge')\n .description('Measure code metrics and list high-risk findings.')\n .argument('[target]', 'file or directory to measure', '.')\n .option('--config <path>', `config file to use instead of the auto-detected ${configFileName}`)\n .option('--file-loc-threshold <number>', 'minimum file code LOC to report', parsePositiveInteger)\n .option('--function-loc-threshold <number>', 'minimum function physical LOC span to report', parsePositiveInteger)\n .option(\n '--component-loc-threshold <number>',\n 'minimum React component physical LOC span to report',\n parsePositiveInteger\n )\n .option('--cognitive-threshold <number>', 'minimum cognitive complexity to report', parsePositiveInteger)\n .option('--cyclomatic-threshold <number>', 'minimum cyclomatic complexity to report', parsePositiveInteger)\n .option('--call-threshold <number>', 'minimum function call count to report', parsePositiveInteger)\n .option('--import-threshold <number>', 'minimum unique import sources per file to report', parsePositiveInteger)\n .option('--fan-out-threshold <number>', 'minimum intra-file fan-out per function to report', parsePositiveInteger)\n .option('--parameter-threshold <number>', 'minimum function parameter count to report', parsePositiveInteger)\n .option(\n '--duplicate-block-threshold <number>',\n 'minimum count of duplicated code blocks per file to report',\n parsePositiveInteger\n )\n .option(\n '--duplication-ratio-percent-threshold <number>',\n 'minimum percentage (1-100) of duplicated lines per file to report',\n parsePercentInteger\n )\n .option(\n '--cross-file-duplicate-block-threshold <number>',\n 'minimum count of cross-file duplicate block groups per file to report',\n parsePositiveInteger\n )\n .option(\n '--duplication-min-tokens <number>',\n 'minimum normalized token count for a duplicate region (default 40)',\n parsePositiveInteger\n )\n .option(\n '--duplication-max-gap-tokens <number>',\n 'maximum token gap merged into one gapped clone group; 0 disables merging (default 30)',\n parseNonNegativeInteger\n )\n .option(\n '--duplication-min-similarity-percent <number>',\n 'minimum similarity percent (1-100) for near-miss (Type-3) clone blocks; 100 reports exact matches only (default 70)',\n parsePercentInteger\n )\n .option(\n '--transitive-dependency-threshold <number>',\n 'minimum transitively reachable local files to report',\n parsePositiveInteger\n )\n .option(\n '--structural-breadth-threshold <number>',\n 'minimum structural breadth score to report',\n parsePositiveInteger\n )\n .option(\n '--structural-coordination-threshold <number>',\n 'minimum structural coordination score to report',\n parsePositiveInteger\n )\n .option('--state-mutation-threshold <number>', 'minimum state mutation score to report', parsePositiveInteger)\n .option(\n '--duplicate-symbol-group-threshold <number>',\n 'minimum duplicate symbol group count to report',\n parsePositiveInteger\n )\n .option('--max-findings <number>', 'maximum number of risk findings to print', parsePositiveInteger)\n .option('--largest-files <number>', 'number of largest files by code LOC to list', parsePositiveInteger)\n .option('--include-tests', 'include test files and test directories')\n .option('--tsconfig <path>', 'TypeScript project file to use instead of auto-detected tsconfig.json')\n .option('--json', 'print JSON output')\n .option('--fail-on-error', 'exit with code 1 when files or directories cannot be scanned')\n .option('--fail-on-risk', 'exit with code 1 when high-risk findings are found');\n\n program.action(async (target: string, cliOptions: CliOptions) => {\n const resolvedTarget = resolveTarget(target);\n const config = await loadConfig(cliOptions.config, await configSearchDirectory(resolvedTarget));\n const options = resolveOptions(cliOptions, config);\n const result = await scanTarget(resolvedTarget, options);\n addCrossFileDuplication(result, options);\n await addArchitectureMetrics(result);\n await addTypeScriptProjectMetrics(result, options, resolvedTarget);\n const risks = findRiskyFunctions(\n result.files,\n result.architecture,\n result.crossFileDuplication,\n result.componentFunctionKeys,\n result.namedComponentFunctionKeys,\n options,\n result.displayRoot\n );\n\n if (options.json) {\n printJson(result, risks, options);\n } else {\n printTextReport(resolvedTarget, result, risks, options);\n }\n\n if (\n result.fatalError ||\n (options.failOnError && result.errors.length > 0) ||\n (options.failOnRisk && risks.length > 0)\n ) {\n process.exitCode = 1;\n }\n });\n\n await program.parseAsync();\n}\n\nfunction resolveTarget(target: string): string {\n if (target === '~') {\n return os.homedir();\n }\n\n if (target.startsWith('~/')) {\n return path.join(os.homedir(), target.slice(2));\n }\n\n return path.resolve(target);\n}\n\n/** Returns the directory from which the config file search should start (the target itself if it is a directory). */\nasync function configSearchDirectory(target: string): Promise<string> {\n try {\n const targetStat = await stat(target);\n return targetStat.isDirectory() ? target : path.dirname(target);\n } catch {\n return path.dirname(target);\n }\n}\n\n/** Shared state of one scan, threaded through the directory walk instead of positional plumbing. */\ninterface ScanContext {\n options: ResolvedOptions;\n files: FileMetrics[];\n errors: string[];\n warnings: string[];\n visitedDirectories: Set<string>;\n visitedFiles: Set<string>;\n /** Scan root: paths are displayed relative to it, and symbolic links may not escape it. */\n rootDirectory: string;\n}\n\nasync function scanTarget(target: string, options: ResolvedOptions): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n let canonicalTarget = target;\n try {\n canonicalTarget = await realpath(target);\n } catch {\n // stat below reports missing targets with the original path.\n }\n\n const fallbackDisplayRoot = path.dirname(canonicalTarget);\n let targetStat;\n\n try {\n targetStat = await stat(canonicalTarget);\n } catch (error) {\n const fatalError = `${formatPath(canonicalTarget, fallbackDisplayRoot)}: ${formatError(error)}`;\n return { displayRoot: fallbackDisplayRoot, files, errors: [fatalError], warnings, fatalError };\n }\n\n if (targetStat.isFile()) {\n const displayRoot = path.dirname(canonicalTarget);\n const language = getLanguage(canonicalTarget, options, true);\n if (!language) {\n const fatalError = `${formatPath(canonicalTarget, displayRoot)}: unsupported file type`;\n return { displayRoot, files, errors: [fatalError], warnings, fatalError };\n }\n\n const context = makeScanContext(options, files, errors, warnings, displayRoot);\n await measureFile(canonicalTarget, language, 'single-file', context, canonicalTarget);\n return { displayRoot, files, errors, warnings };\n }\n\n await scanDirectory(canonicalTarget, makeScanContext(options, files, errors, warnings, canonicalTarget));\n return { displayRoot: canonicalTarget, files, errors, warnings };\n}\n\nfunction makeScanContext(\n options: ResolvedOptions,\n files: FileMetrics[],\n errors: string[],\n warnings: string[],\n rootDirectory: string\n): ScanContext {\n return { options, files, errors, warnings, visitedDirectories: new Set(), visitedFiles: new Set(), rootDirectory };\n}\n\nasync function addTypeScriptProjectMetrics(\n result: ScanResult,\n options: ResolvedOptions,\n resolvedTarget: string\n): Promise<void> {\n if (result.fatalError) {\n return;\n }\n if (result.files.length === 0) {\n return;\n }\n\n const explicitConfigFile = options.tsconfig;\n const isExplicitConfig = explicitConfigFile !== undefined;\n if (!isExplicitConfig && !result.files.some(({ file }) => isTypeScriptProjectCandidateFile(file))) {\n return;\n }\n\n const configFile = explicitConfigFile ? resolveTarget(explicitConfigFile) : await findNearestTsconfig(resolvedTarget);\n if (!configFile) {\n return;\n }\n\n try {\n result.typeScriptProject = await measureTypeScriptProject(\n configFile,\n result.files.map(({ file }) => file)\n );\n result.componentFunctionKeys = new Set(\n result.typeScriptProject.reactComponentFunctions.map((component) =>\n functionLocationKey(component.file, component.startLine, component.startColumn)\n )\n );\n result.namedComponentFunctionKeys = new Set(\n result.typeScriptProject.reactComponentFunctions.flatMap((component) =>\n component.name ? [functionNameLocationKey(component.file, component.name, component.startLine)] : []\n )\n );\n } catch (error) {\n if (isExplicitConfig) {\n result.errors.push(`${formatPath(configFile, result.displayRoot)}: ${formatError(error)}`);\n }\n }\n}\n\nfunction isTypeScriptProjectCandidateFile(file: string): boolean {\n return ['.cjs', '.cts', '.js', '.jsx', '.mjs', '.mts', '.ts', '.tsx'].includes(path.extname(file));\n}\n\nasync function findNearestTsconfig(target: string): Promise<string | undefined> {\n const targetStat = await stat(target);\n let currentDirectory = targetStat.isDirectory() ? target : path.dirname(target);\n while (true) {\n const configFile = path.join(currentDirectory, 'tsconfig.json');\n if (await fileExists(configFile)) {\n return configFile;\n }\n\n const parentDirectory = path.dirname(currentDirectory);\n if (parentDirectory === currentDirectory) {\n return undefined;\n }\n currentDirectory = parentDirectory;\n }\n}\n\nasync function fileExists(file: string): Promise<boolean> {\n try {\n const fileStat = await stat(file);\n return fileStat.isFile();\n } catch {\n return false;\n }\n}\n\nasync function addArchitectureMetrics(result: ScanResult): Promise<void> {\n if (result.fatalError) {\n return;\n }\n\n try {\n result.architecture = measureArchitecture(\n result.files.map(({ file, metrics }) => ({ file, metrics })),\n result.displayRoot\n );\n } catch (error) {\n result.errors.push(`architecture metrics: ${formatError(error)}`);\n }\n}\n\nasync function scanDirectory(directory: string, context: ScanContext): Promise<void> {\n let resolvedDirectory;\n try {\n resolvedDirectory = await realpath(directory);\n } catch (error) {\n context.errors.push(`${formatPath(directory, context.rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n if (!isWithinDirectory(resolvedDirectory, context.rootDirectory)) {\n return;\n }\n\n if (context.visitedDirectories.has(resolvedDirectory)) {\n return;\n }\n context.visitedDirectories.add(resolvedDirectory);\n\n let entries;\n try {\n entries = await readdir(directory, { withFileTypes: true });\n } catch (error) {\n context.errors.push(`${formatPath(directory, context.rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n for (const entry of entries) {\n const entryPath = path.join(directory, entry.name);\n if (entry.isSymbolicLink()) {\n await scanSymbolicLink(entry.name, entryPath, context);\n continue;\n }\n\n if (entry.isDirectory()) {\n if (shouldSkipDirectory(entry.name, context.options)) {\n continue;\n }\n await scanDirectory(entryPath, context);\n continue;\n }\n\n if (entry.isFile()) {\n await measureScannableFile(entryPath, context);\n }\n }\n}\n\nasync function scanSymbolicLink(name: string, entryPath: string, context: ScanContext): Promise<void> {\n let resolvedPath;\n try {\n resolvedPath = await realpath(entryPath);\n } catch (error) {\n context.errors.push(`${formatPath(entryPath, context.rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n if (!isWithinDirectory(resolvedPath, context.rootDirectory)) {\n return;\n }\n\n let entryStat;\n try {\n entryStat = await stat(entryPath);\n } catch (error) {\n context.errors.push(`${formatPath(entryPath, context.rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n if (entryStat.isDirectory()) {\n if (\n shouldSkipDirectory(name, context.options) ||\n shouldSkipDirectory(path.basename(resolvedPath), context.options)\n ) {\n return;\n }\n await scanDirectory(entryPath, context);\n return;\n }\n\n if (entryStat.isFile()) {\n await measureScannableFile(entryPath, context, resolvedPath, resolvedPath);\n }\n}\n\nasync function measureScannableFile(\n file: string,\n context: ScanContext,\n languageFile = file,\n realFile?: string\n): Promise<void> {\n const language = getLanguage(languageFile, context.options);\n if (language) {\n await measureFile(file, language, 'directory', context, realFile);\n }\n}\n\nasync function measureFile(\n file: string,\n language: LanguageName,\n mode: 'single-file' | 'directory',\n context: ScanContext,\n realFile?: string\n): Promise<void> {\n try {\n const resolvedFile = realFile ?? (await realpath(file));\n if (context.visitedFiles.has(resolvedFile)) {\n return;\n }\n context.visitedFiles.add(resolvedFile);\n\n const code = await readFile(file, 'utf8');\n const measureOptions = { language, duplication: context.options.duplication };\n const fileMetrics: FileMetrics = { file, metrics: measureCode(code, measureOptions) };\n // Only directory scans compare files against each other; a single-file target has no peers.\n // Candidate collection failing (it always parses with the JavaScript binding, which can give\n // up where the native backend measured fine) must not discard the measured metrics.\n if (mode === 'directory') {\n try {\n fileMetrics.duplicationCandidates = collectCrossFileDuplicationFileData(code, measureOptions);\n } catch (error) {\n // A warning, not an error: the file's metrics are complete, only its participation in\n // cross-file matching is lost, so it is not \"skipped\" and must not fail --fail-on-error.\n context.warnings.push(\n `${formatPath(file, context.rootDirectory)}: cross-file duplication candidates unavailable: ${formatError(error)}`\n );\n }\n }\n context.files.push(fileMetrics);\n } catch (error) {\n context.errors.push(`${formatPath(file, context.rootDirectory)}: ${formatError(error)}`);\n }\n}\n\n/** Runs after the scan so every measured file's candidates participate. */\nfunction addCrossFileDuplication(result: ScanResult, options: ResolvedOptions): void {\n if (result.fatalError || result.files.length < 2) {\n return;\n }\n const sourceFiles = result.files.flatMap(({ file, duplicationCandidates }) =>\n duplicationCandidates ? [{ file: formatPath(file, result.displayRoot), ...duplicationCandidates }] : []\n );\n if (sourceFiles.length < 2) {\n return;\n }\n result.crossFileDuplication = measureCrossFileDuplication(sourceFiles, options.duplication);\n}\n\nfunction findRiskyFunctions(\n files: FileMetrics[],\n architecture: ArchitectureMetrics | undefined,\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n componentFunctionKeys: Set<string> | undefined,\n namedComponentFunctionKeys: Set<string> | undefined,\n options: ResolvedOptions,\n displayRoot: string\n): RiskFinding[] {\n const architectureByFile = new Map(architecture?.files.map((file) => [file.file, file]));\n const findings = files.flatMap(({ file, metrics }) => {\n const isReactFile = metrics.functions.some(\n (fn) => fn.returnsJsx || isReactComponent(file, fn, componentFunctionKeys, namedComponentFunctionKeys)\n );\n const thresholds = resolveThresholds(options, metrics.language, isReactFile);\n return [\n ...findRiskyFileMetrics(\n file,\n metrics,\n architectureByFile.get(formatPath(file, displayRoot)),\n crossFileDuplication,\n thresholds,\n displayRoot\n ),\n ...metrics.functions.flatMap((fn) =>\n findRiskyFunctionMetrics(\n file,\n metrics.language,\n fn,\n thresholds,\n displayRoot,\n componentFunctionKeys,\n namedComponentFunctionKeys\n )\n ),\n ];\n });\n\n findings.sort(compareRiskFindings);\n return findings;\n}\n\nfunction findRiskyFileMetrics(\n file: string,\n metrics: CodeMetrics,\n architecture: ArchitectureFileMetrics | undefined,\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n thresholds: Thresholds,\n displayRoot: string\n): RiskFinding[] {\n const triggers: RiskTrigger[] = [];\n const formattedFile = formatPath(file, displayRoot);\n addTrigger(triggers, 'file LOC', metrics.lines.code, thresholds.fileLoc);\n addTrigger(triggers, 'import sources', metrics.coupling.importSourceCount, thresholds.import);\n const duplicateBlockDetail = formatDuplicateBlockGroups(metrics.duplication.duplicateBlockGroups);\n addTrigger(\n triggers,\n 'duplicated blocks',\n metrics.duplication.duplicateBlockCount,\n thresholds.duplicateBlock,\n duplicateBlockDetail\n );\n // Maximal-region selection deliberately compresses adjacent clones into few blocks, so severity\n // must track line coverage, not the block count. Flooring compares like the unrounded ratio\n // against the integer threshold (29.5% must not trigger a >= 30 threshold). The block ranges are\n // repeated as detail because this trigger can fire alone, and a percentage without locations is\n // not actionable.\n addTrigger(\n triggers,\n 'duplicated lines (%)',\n Math.floor(metrics.duplication.duplicationRatio * 100),\n thresholds.duplicationRatioPercent,\n duplicateBlockDetail\n );\n if (crossFileDuplication) {\n // Object.hasOwn: a file named like an Object.prototype member must not read an inherited value.\n addTrigger(\n triggers,\n 'cross-file duplicated blocks',\n Object.hasOwn(crossFileDuplication.duplicateBlockGroupCountByFile, formattedFile)\n ? (crossFileDuplication.duplicateBlockGroupCountByFile[formattedFile] ?? 0)\n : 0,\n thresholds.crossFileDuplicateBlock,\n formatCrossFileDuplicateDetail(crossFileDuplication, formattedFile)\n );\n }\n if (architecture) {\n const hasFileScaleRisk = metrics.lines.code >= 100 || architecture.directLocalDependencyCount >= 8;\n if (hasFileScaleRisk) {\n addTrigger(\n triggers,\n 'transitive local dependencies',\n architecture.transitiveLocalDependencyCount,\n thresholds.transitiveDependency\n );\n }\n if (\n triggers.length > 0 ||\n architecture.directLocalDependencyCount >= 8 ||\n architecture.structuralCoordination.score >= thresholds.structuralCoordination\n ) {\n addTrigger(triggers, 'structural breadth', architecture.structuralBreadthScore, thresholds.structuralBreadth);\n }\n addTrigger(\n triggers,\n 'structural coordination',\n architecture.structuralCoordination.score,\n thresholds.structuralCoordination\n );\n addTrigger(\n triggers,\n 'state mutation',\n architecture.structuralCoordination.stateMutationScore,\n thresholds.stateMutation\n );\n addTrigger(\n triggers,\n 'duplicate symbol groups',\n architecture.duplicateSymbolGroupCount,\n thresholds.duplicateSymbolGroup\n );\n }\n if (triggers.length === 0) {\n return [];\n }\n\n return [\n {\n file: formattedFile,\n language: metrics.language,\n kind: 'file',\n cyclomaticComplexity: metrics.cyclomaticComplexity,\n cognitiveComplexity: metrics.cognitiveComplexity,\n triggers,\n score: maxTriggerScore(triggers),\n },\n ];\n}\n\nfunction findRiskyFunctionMetrics(\n file: string,\n language: LanguageName,\n fn: FunctionMetrics,\n thresholds: Thresholds,\n displayRoot: string,\n componentFunctionKeys?: Set<string>,\n namedComponentFunctionKeys?: Set<string>\n): RiskFinding[] {\n const loc = fn.endLine - fn.startLine + 1;\n const isComponent = isReactComponent(file, fn, componentFunctionKeys, namedComponentFunctionKeys);\n const kind = isComponent ? 'component' : 'function';\n const triggers: RiskTrigger[] = [];\n addTrigger(triggers, 'cognitive complexity', fn.cognitiveComplexity, thresholds.cognitive);\n addTrigger(triggers, 'cyclomatic complexity', fn.cyclomaticComplexity, thresholds.cyclomatic);\n addTrigger(triggers, isComponent ? 'component LOC' : 'function LOC', loc, getLocThreshold(isComponent, thresholds));\n addTrigger(triggers, 'function calls', fn.callCount, thresholds.call);\n addTrigger(triggers, 'fan-out', fn.fanOut, thresholds.fanOut);\n addTrigger(triggers, 'parameters', fn.parameterCount, thresholds.parameter);\n if (triggers.length === 0) {\n return [];\n }\n\n return [\n {\n file: formatPath(file, displayRoot),\n language,\n kind,\n name: fn.name ?? '<anonymous>',\n startLine: fn.startLine,\n endLine: fn.endLine,\n cyclomaticComplexity: fn.cyclomaticComplexity,\n cognitiveComplexity: fn.cognitiveComplexity,\n triggers,\n score: maxTriggerScore(triggers),\n },\n ];\n}\n\nfunction addTrigger(triggers: RiskTrigger[], metric: string, value: number, threshold: number, detail?: string): void {\n if (value < threshold) {\n return;\n }\n\n triggers.push({ metric, value, threshold, score: value / threshold, detail });\n}\n\n/** Formats the cross-file groups a file participates in as `12-34 ~ b.ts:56-78; ...` (capped). */\nfunction formatCrossFileDuplicateDetail(\n crossFileDuplication: CrossFileDuplicationMetrics,\n formattedFile: string\n): string | undefined {\n const involved = crossFileDuplication.groups.filter((group) => group.files.includes(formattedFile));\n if (involved.length === 0) {\n return undefined;\n }\n const formatted = involved\n .slice(0, maxCrossFileDuplicateDetailGroups)\n .map((group) =>\n group.occurrences\n .map(({ file, startLine, endLine }) =>\n file === formattedFile ? `${startLine}-${endLine}` : `${file}:${startLine}-${endLine}`\n )\n .join(' ~ ')\n )\n .join('; ');\n const truncatedSuffix = involved.length > maxCrossFileDuplicateDetailGroups ? '; ...' : '';\n return `${formatted}${truncatedSuffix}`;\n}\n\n/** Formats duplicated block groups as `12-34 ~ 56-78; 90-99 ~ 100-109` (copies joined by ` ~ `, groups by `; `). */\nfunction formatDuplicateBlockGroups(groups: { endLine: number; startLine: number }[][]): string | undefined {\n if (groups.length === 0) {\n return undefined;\n }\n\n return groups.map((group) => group.map(({ startLine, endLine }) => `${startLine}-${endLine}`).join(' ~ ')).join('; ');\n}\n\nfunction isReactComponent(\n file: string,\n fn: FunctionMetrics,\n componentFunctionKeys: Set<string> | undefined,\n namedComponentFunctionKeys: Set<string> | undefined\n): boolean {\n return (\n componentFunctionKeys?.has(functionLocationKey(file, fn.startLine, fn.startColumn)) ||\n (fn.name ? namedComponentFunctionKeys?.has(functionNameLocationKey(file, fn.name, fn.startLine)) : false) ||\n false\n );\n}\n\nfunction getLocThreshold(isComponent: boolean, thresholds: Thresholds): number {\n return isComponent ? thresholds.componentLoc : thresholds.functionLoc;\n}\n\nfunction functionLocationKey(file: string, startLine: number, startColumn: number): string {\n return `${path.resolve(file)}:${startLine}:${startColumn}`;\n}\n\nfunction functionNameLocationKey(file: string, name: string, startLine: number): string {\n return `${path.resolve(file)}:${name}:${startLine}`;\n}\n\nfunction maxTriggerScore(triggers: RiskTrigger[]): number {\n return Math.max(...triggers.map((trigger) => trigger.score));\n}\n\nfunction compareRiskFindings(left: RiskFinding, right: RiskFinding): number {\n return (\n right.score - left.score ||\n left.file.localeCompare(right.file) ||\n (left.startLine ?? 0) - (right.startLine ?? 0) ||\n (left.endLine ?? 0) - (right.endLine ?? 0) ||\n left.kind.localeCompare(right.kind)\n );\n}\n\nfunction printJson(result: ScanResult, risks: RiskFinding[], options: ResolvedOptions): void {\n const summary = summarize(result.files);\n const reportedRisks = risks.slice(0, options.maxFindings);\n writeStdout(\n JSON.stringify(\n {\n summary,\n thresholds: options.thresholds,\n profileThresholds: options.profileThresholds,\n totalRisks: risks.length,\n truncated: reportedRisks.length < risks.length,\n largestFiles:\n options.largestFiles > 0\n ? findLargestFiles(result.files, options.largestFiles, result.displayRoot)\n : undefined,\n architecture: result.architecture,\n crossFileDuplication: result.crossFileDuplication,\n typeScriptProject: result.typeScriptProject,\n risks: reportedRisks,\n errors: result.errors,\n warnings: result.warnings,\n },\n undefined,\n 2\n ) + '\\n'\n );\n}\n\nfunction printTextReport(target: string, result: ScanResult, risks: RiskFinding[], options: ResolvedOptions): void {\n if (result.fatalError) {\n writeStderr(`Error: ${result.fatalError}\\n`);\n return;\n }\n\n const { thresholds } = options;\n const summary = summarize(result.files);\n writeStdout(`Measured ${summary.fileCount} files under ${target}\\n`);\n writeStdout(\n `LOC ${summary.linesOfCode}, NCSS ${summary.ncssCount}, functions ${summary.functionCount}, max cyclomatic ${summary.maxCyclomaticComplexity}, max cognitive ${summary.maxCognitiveComplexity}\\n`\n );\n writeStdout(\n `Calls ${summary.callCount}, internal edges ${summary.internalEdgeCount}, max call depth ${summary.maxCallDepth}, imports ${summary.importSourceCount}, exports ${summary.exportCount}\\n`\n );\n writeStdout(\n `Type annotations ${summary.typeAnnotationCount}, type aliases ${summary.typeAliasCount}, interfaces ${summary.interfaceCount}, avg cohesion ${summary.averageFunctionIdentifierOverlap.toFixed(2)}\\n`\n );\n if (result.architecture) {\n writeStdout(`${formatArchitectureMetrics(result.architecture)}\\n`);\n }\n if (result.typeScriptProject) {\n writeStdout(`${formatTypeScriptProjectMetrics(result.typeScriptProject)}\\n`);\n }\n writeStdout(\n `Risk thresholds: file LOC >= ${thresholds.fileLoc}, function LOC >= ${thresholds.functionLoc}, component LOC >= ${thresholds.componentLoc}, cognitive >= ${thresholds.cognitive}, cyclomatic >= ${thresholds.cyclomatic}, calls >= ${thresholds.call}, imports >= ${thresholds.import}, fan-out >= ${thresholds.fanOut}, parameters >= ${thresholds.parameter}, duplicated blocks >= ${thresholds.duplicateBlock}, duplicated lines (%) >= ${thresholds.duplicationRatioPercent}, cross-file duplicated blocks >= ${thresholds.crossFileDuplicateBlock}\\n`\n );\n const profileOverrides = formatProfileOverrides(options.profileThresholds);\n if (profileOverrides) {\n writeStdout(`Per-language overrides: ${profileOverrides}\\n`);\n }\n\n if (risks.length === 0) {\n writeStdout('No high-risk findings found.\\n');\n } else {\n const reportedRisks = risks.slice(0, options.maxFindings);\n const totalSuffix = risks.length > reportedRisks.length ? ` of ${risks.length}` : '';\n writeStdout(`\\nHigh-risk findings (top ${reportedRisks.length}${totalSuffix}):\\n`);\n for (const risk of reportedRisks) {\n writeStdout(`${formatRiskLocation(risk)} ${formatRiskName(risk)} ${formatRiskMetrics(risk)}\\n`);\n }\n }\n\n const crossFileGroups = result.crossFileDuplication?.groups ?? [];\n if (crossFileGroups.length > 0) {\n const reportedGroups = crossFileGroups.slice(0, maxCrossFileDuplicateGroupLines);\n const totalSuffix = crossFileGroups.length > reportedGroups.length ? ` of ${crossFileGroups.length}` : '';\n writeStdout(`\\nCross-file duplicate blocks (top ${reportedGroups.length}${totalSuffix}):\\n`);\n for (const group of reportedGroups) {\n writeStdout(\n `${group.tokenCount} tokens: ${group.occurrences\n .map(({ file, startLine, endLine }) => `${file}:${startLine}-${endLine}`)\n .join(', ')}\\n`\n );\n }\n }\n\n const duplicateSymbolGroups = result.architecture?.duplicateSymbolGroups ?? [];\n if (duplicateSymbolGroups.length > 0) {\n const reportedGroups = duplicateSymbolGroups\n .toSorted((left, right) => right.files.length - left.files.length || left.name.localeCompare(right.name))\n .slice(0, maxDuplicateSymbolGroupLines);\n const totalSuffix =\n duplicateSymbolGroups.length > reportedGroups.length ? ` of ${duplicateSymbolGroups.length}` : '';\n writeStdout(`\\nDuplicate symbols (top ${reportedGroups.length}${totalSuffix}):\\n`);\n for (const group of reportedGroups) {\n writeStdout(\n `${group.name}: ${group.declarations.map((declaration) => `${declaration.file}:${declaration.line}`).join(', ')}\\n`\n );\n }\n }\n\n if (options.largestFiles > 0) {\n const largestFiles = findLargestFiles(result.files, options.largestFiles, result.displayRoot);\n writeStdout(`\\nLargest files by code LOC (top ${largestFiles.length}):\\n`);\n for (const { file, codeLoc } of largestFiles) {\n writeStdout(`${file} (code LOC ${codeLoc})\\n`);\n }\n }\n\n if (result.warnings.length > 0) {\n writeStderr(`\\nDegraded ${result.warnings.length} files (measured, but excluded from cross-file matching):\\n`);\n for (const warning of result.warnings.slice(0, 10)) {\n writeStderr(`- ${warning}\\n`);\n }\n if (result.warnings.length > 10) {\n writeStderr(`- ... ${result.warnings.length - 10} more\\n`);\n }\n }\n\n if (result.errors.length > 0) {\n writeStderr(`\\nSkipped ${result.errors.length} files or directories:\\n`);\n for (const error of result.errors.slice(0, 10)) {\n writeStderr(`- ${error}\\n`);\n }\n if (result.errors.length > 10) {\n writeStderr(`- ... ${result.errors.length - 10} more\\n`);\n }\n }\n}\n\nfunction findLargestFiles(\n files: FileMetrics[],\n count: number,\n displayRoot: string\n): { file: string; codeLoc: number }[] {\n return files\n .map(({ file, metrics }) => ({ file: formatPath(file, displayRoot), codeLoc: metrics.lines.code }))\n .toSorted((left, right) => right.codeLoc - left.codeLoc || left.file.localeCompare(right.file))\n .slice(0, count);\n}\n\nfunction formatProfileOverrides(profileThresholds: ResolvedOptions['profileThresholds']): string {\n return Object.entries(profileThresholds)\n .map(\n ([profile, overrides]) =>\n `${profile} { ${Object.entries(overrides)\n .map(([metric, value]) => `${metric} ${value}`)\n .join(', ')} }`\n )\n .join('; ');\n}\n\nfunction formatRiskLocation(risk: RiskFinding): string {\n return risk.startLine === undefined || risk.endLine === undefined\n ? risk.file\n : `${risk.file}:${risk.startLine}-${risk.endLine}`;\n}\n\nfunction formatRiskName(risk: RiskFinding): string {\n return risk.name ? `${risk.kind} ${risk.name}` : risk.kind;\n}\n\nfunction formatRiskMetrics(risk: RiskFinding): string {\n const triggerText = risk.triggers\n .map(\n (trigger) =>\n `${trigger.metric} ${formatMetricValue(trigger.value)} >= ${formatMetricValue(trigger.threshold)}${trigger.detail ? ` [${trigger.detail}]` : ''}`\n )\n .join(', ');\n return `(${triggerText}; cyclomatic ${risk.cyclomaticComplexity}, cognitive ${risk.cognitiveComplexity})`;\n}\n\nfunction formatMetricValue(value: number): string {\n return Number.isInteger(value) ? String(value) : value.toFixed(2);\n}\n\nfunction formatArchitectureMetrics(metrics: ArchitectureMetrics): string {\n const maxStateMutationScore = Math.max(\n 0,\n ...metrics.files.map((file) => file.structuralCoordination.stateMutationScore)\n );\n return `Architecture max reachable files ${metrics.maxTransitiveLocalDependencyCount}, max structural breadth ${metrics.maxStructuralBreadthScore}, max structural coordination ${metrics.maxStructuralCoordinationScore}, max state mutation ${maxStateMutationScore}, duplicate symbol groups ${metrics.duplicateSymbolGroups.length}`;\n}\n\nfunction formatTypeScriptProjectMetrics(metrics: TypeScriptProjectMetrics): string {\n return `TypeScript project root files ${metrics.rootFileCount}, measured roots ${metrics.measuredRootFileCount}, semantic diagnostics ${metrics.semanticDiagnosticCount}, resolved calls ${metrics.resolvedCallExpressionCount}/${metrics.callExpressionCount} (${(metrics.resolvedCallExpressionRatio * 100).toFixed(1)}%)`;\n}\n\nfunction summarize(files: FileMetrics[]): {\n fileCount: number;\n functionCount: number;\n linesOfCode: number;\n maxCognitiveComplexity: number;\n maxCyclomaticComplexity: number;\n ncssCount: number;\n callCount: number;\n internalEdgeCount: number;\n maxCallDepth: number;\n importSourceCount: number;\n relativeImportCount: number;\n externalImportCount: number;\n exportCount: number;\n averageFunctionIdentifierOverlap: number;\n typeAnnotationCount: number;\n typeAliasCount: number;\n interfaceCount: number;\n genericParameterCount: number;\n} {\n let functionCount = 0;\n let linesOfCode = 0;\n let maxCyclomaticComplexity = 0;\n let maxCognitiveComplexity = 0;\n let ncssCount = 0;\n let callCount = 0;\n let internalEdgeCount = 0;\n let maxCallDepth = 0;\n let importSourceCount = 0;\n let relativeImportCount = 0;\n let externalImportCount = 0;\n let exportCount = 0;\n let cohesionTotal = 0;\n let typeAnnotationCount = 0;\n let typeAliasCount = 0;\n let interfaceCount = 0;\n let genericParameterCount = 0;\n\n for (const file of files) {\n functionCount += file.metrics.functionCount;\n linesOfCode += file.metrics.lines.code;\n maxCyclomaticComplexity = Math.max(maxCyclomaticComplexity, file.metrics.maxCyclomaticComplexity);\n maxCognitiveComplexity = Math.max(maxCognitiveComplexity, file.metrics.maxCognitiveComplexity);\n ncssCount += file.metrics.ncssCount;\n callCount += file.metrics.callGraph.callCount;\n internalEdgeCount += file.metrics.callGraph.internalEdgeCount;\n maxCallDepth = Math.max(maxCallDepth, file.metrics.callGraph.maxCallDepth);\n importSourceCount += file.metrics.coupling.importSourceCount;\n relativeImportCount += file.metrics.coupling.relativeImportCount;\n externalImportCount += file.metrics.coupling.externalImportCount;\n exportCount += file.metrics.coupling.exportCount;\n cohesionTotal += file.metrics.cohesion.averageFunctionIdentifierOverlap;\n typeAnnotationCount += file.metrics.typeComplexity.typeAnnotationCount;\n typeAliasCount += file.metrics.typeComplexity.typeAliasCount;\n interfaceCount += file.metrics.typeComplexity.interfaceCount;\n genericParameterCount += file.metrics.typeComplexity.genericParameterCount;\n }\n\n return {\n fileCount: files.length,\n functionCount,\n linesOfCode,\n maxCyclomaticComplexity,\n maxCognitiveComplexity,\n ncssCount,\n callCount,\n internalEdgeCount,\n maxCallDepth,\n importSourceCount,\n relativeImportCount,\n externalImportCount,\n exportCount,\n averageFunctionIdentifierOverlap: files.length === 0 ? 0 : cohesionTotal / files.length,\n typeAnnotationCount,\n typeAliasCount,\n interfaceCount,\n genericParameterCount,\n };\n}\n\nfunction shouldSkipDirectory(name: string, options: ResolvedOptions): boolean {\n if (ignoredDirectoryNames.has(name)) {\n return true;\n }\n\n if (options.includeTests) {\n return false;\n }\n\n return testDirectoryNames.has(name);\n}\n\nfunction isWithinDirectory(candidate: string, directory: string): boolean {\n const relative = path.relative(directory, candidate);\n return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));\n}\n\nfunction getLanguage(file: string, options: ResolvedOptions, explicitTarget = false): LanguageName | undefined {\n const lowerFile = file.toLowerCase();\n if (\n !explicitTarget &&\n (lowerFile.endsWith('.d.ts') ||\n lowerFile.endsWith('.d.mts') ||\n lowerFile.endsWith('.d.cts') ||\n lowerFile.endsWith('.min.js') ||\n lowerFile.endsWith('.pnp.cjs'))\n ) {\n return undefined;\n }\n\n if (\n !explicitTarget &&\n !options.includeTests &&\n (testFilePattern.test(path.basename(file)) || javaTestFilePattern.test(path.basename(file)))\n ) {\n return undefined;\n }\n\n // GCC treats an uppercase `.C` as C++; lowercasing first would misparse it with the C grammar.\n if (path.extname(file) === '.C') {\n return 'cpp';\n }\n\n return languageByExtension.get(path.extname(lowerFile));\n}\n\nfunction parsePercentInteger(value: string): number {\n const parsed = parsePositiveInteger(value);\n if (parsed > 100) {\n throw new InvalidArgumentError('Expected an integer between 1 and 100.');\n }\n return parsed;\n}\n\nfunction parseNonNegativeInteger(value: string): number {\n if (!/^\\d+$/u.test(value)) {\n throw new InvalidArgumentError('Expected a non-negative integer.');\n }\n\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed < 0) {\n throw new InvalidArgumentError('Expected a non-negative integer.');\n }\n return parsed;\n}\n\nfunction parsePositiveInteger(value: string): number {\n if (!/^[1-9]\\d*$/u.test(value)) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed < 1) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n return parsed;\n}\n\nfunction formatPath(file: string, base: string): string {\n return path.relative(base, file) || path.basename(file);\n}\n\nfunction writeStdout(message: string): void {\n process.stdout.write(message);\n}\n\nfunction writeStderr(message: string): void {\n process.stderr.write(message);\n}\n\nfunction formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.cjs","names":["Command","configFileName","loadConfig","resolveOptions","os","path","stat","realpath","readdir","readFile","measureCode","collectCrossFileDuplicationFileData","measureCrossFileDuplication","InvalidArgumentError"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { readdir, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { Command, InvalidArgumentError } from 'commander';\nimport { type CliOptions, configFileName, loadConfig, type ResolvedOptions, resolveOptions } from './cliConfig.js';\nimport { measureCrossFileDuplication, type CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData } from './duplication.js';\nimport { collectCrossFileDuplicationFileData, measureCode } from './metrics.js';\nimport type { CodeMetrics, FunctionMetrics, LanguageName } from './types.js';\n\ninterface FileMetrics {\n file: string;\n metrics: CodeMetrics;\n /** Cross-file duplicate candidates and token/statement data, collected only for directory scans. */\n duplicationCandidates?: CrossFileDuplicationFileData;\n}\n\ninterface ScanResult {\n crossFileDuplication?: CrossFileDuplicationMetrics;\n displayRoot: string;\n errors: string[];\n /** Non-fatal degradations (e.g. cross-file candidates unavailable); the file is still measured. */\n warnings: string[];\n fatalError?: string;\n files: FileMetrics[];\n}\n\n/** The worst (highest-cognitive-complexity) function of a file, reported as the ranking evidence. */\ninterface WorstFunction {\n name: string;\n startLine: number;\n endLine: number;\n cognitiveComplexity: number;\n ncss: number;\n nestingDepth: number;\n}\n\n/**\n * One ranked refactoring candidate. The score is the sum of the file's repo-relative percentile\n * ranks (each in [0, 1)) over three dimensions: worst-function cognitive complexity, duplicated\n * lines (within-file and cross-file combined), and file NCSS. Ranking is relative to the scanned\n * project, so no absolute threshold is involved.\n */\ninterface RankedFile {\n file: string;\n score: number;\n worstFunction?: WorstFunction;\n /** Distinct lines covered by within-file duplicate blocks or cross-file duplicate occurrences. */\n duplicatedLineCount: number;\n /** duplicatedLineCount / code lines (0 when the file has no code). */\n duplicatedLineRatio: number;\n /** Other files sharing cross-file duplicate blocks; filled only for the reported top files. */\n crossFilePartners: string[];\n ncss: number;\n codeLines: number;\n}\n\nconst languageByExtension = new Map<string, LanguageName>([\n ['.c', 'c'],\n ['.c++', 'cpp'],\n ['.cc', 'cpp'],\n ['.cjs', 'javascript'],\n ['.cp', 'cpp'],\n ['.cpp', 'cpp'],\n ['.tcc', 'cpp'],\n ['.cts', 'typescript'],\n ['.cxx', 'cpp'],\n ['.go', 'go'],\n // Headers may be C or C++; the C++ grammar parses both.\n ['.h', 'cpp'],\n ['.hh', 'cpp'],\n ['.hpp', 'cpp'],\n ['.hxx', 'cpp'],\n ['.java', 'java'],\n ['.js', 'javascript'],\n ['.jsx', 'jsx'],\n ['.mjs', 'javascript'],\n ['.mts', 'typescript'],\n ['.py', 'python'],\n ['.rb', 'ruby'],\n ['.rs', 'rust'],\n ['.ts', 'typescript'],\n ['.tsx', 'tsx'],\n]);\n\nconst ignoredDirectoryNames = new Set([\n '.agents',\n '.claude',\n '.cursor',\n '.git',\n '.next',\n '.playwright-cli',\n '.tox',\n '.tmp',\n '.turbo',\n '.venv',\n '.yarn',\n '__fixtures__',\n '__generated__',\n '__pycache__',\n 'coverage',\n 'dist',\n 'fixtures',\n 'generated',\n 'node_modules',\n 'target',\n 'test-fixtures',\n 'vendor',\n 'venv',\n]);\n\n/** Caps how many cross-file partners a ranked file lists so the report stays scannable. */\nconst maxCrossFilePartners = 3;\n\nconst testDirectoryNames = new Set(['__tests__', 'test', 'tests', 'spec']);\nconst testFilePattern = /(?:^test(?:[_-].*)?|\\.(?:spec|test)|[_-](?:test|spec))\\.[^.]+$/iu;\n// JUnit tests use a case-sensitive `Test.java` suffix; case-insensitive matching would catch\n// production files like `contest.java`.\nconst javaTestFilePattern = /Test\\.java$/u;\n\n// oxlint-disable-next-line unicorn/prefer-top-level-await -- CommonJS build output cannot preserve top-level await.\nvoid main().catch((error: unknown) => {\n writeStderr(`Error: ${formatError(error)}\\n`);\n process.exitCode = 1;\n});\n\nasync function main(): Promise<void> {\n const program = new Command()\n .name('code-gauge')\n .description('Rank the files of a project by refactoring priority.')\n .argument('[target]', 'file or directory to measure', '.')\n .option('--config <path>', `config file to use instead of the auto-detected ${configFileName}`)\n .option('--top <number>', 'number of top-ranked files to report (default: 10)', parsePositiveInteger)\n .option(\n '--duplication-min-tokens <number>',\n 'minimum normalized token count for a duplicate region (default 40)',\n parsePositiveInteger\n )\n .option(\n '--duplication-max-gap-tokens <number>',\n 'maximum token gap merged into one gapped clone group; 0 disables merging (default 30)',\n parseNonNegativeInteger\n )\n .option(\n '--duplication-min-similarity-percent <number>',\n 'minimum similarity percent (1-100) for near-miss (Type-3) clone blocks; 100 reports exact matches only (default 70)',\n parsePercentInteger\n )\n .option('--include-tests', 'include test files and test directories')\n .option('--json', 'print JSON output')\n .option('--fail-on-error', 'exit with code 1 when files or directories cannot be scanned');\n\n program.action(async (target: string, cliOptions: CliOptions) => {\n const resolvedTarget = resolveTarget(target);\n const config = await loadConfig(cliOptions.config, await configSearchDirectory(resolvedTarget));\n const options = resolveOptions(cliOptions, config);\n const result = await scanTarget(resolvedTarget, options);\n addCrossFileDuplication(result, options);\n const rankedFiles = rankFiles(result, options.top);\n\n if (options.json) {\n printJson(result, rankedFiles, options);\n } else {\n printTextReport(resolvedTarget, result, rankedFiles, options);\n }\n\n if (result.fatalError || (options.failOnError && result.errors.length > 0)) {\n process.exitCode = 1;\n }\n });\n\n await program.parseAsync();\n}\n\nfunction resolveTarget(target: string): string {\n if (target === '~') {\n return os.homedir();\n }\n\n if (target.startsWith('~/')) {\n return path.join(os.homedir(), target.slice(2));\n }\n\n return path.resolve(target);\n}\n\n/** Returns the directory from which the config file search should start (the target itself if it is a directory). */\nasync function configSearchDirectory(target: string): Promise<string> {\n try {\n const targetStat = await stat(target);\n return targetStat.isDirectory() ? target : path.dirname(target);\n } catch {\n return path.dirname(target);\n }\n}\n\n/** Shared state of one scan, threaded through the directory walk instead of positional plumbing. */\ninterface ScanContext {\n options: ResolvedOptions;\n files: FileMetrics[];\n errors: string[];\n warnings: string[];\n visitedDirectories: Set<string>;\n visitedFiles: Set<string>;\n /** Scan root: paths are displayed relative to it, and symbolic links may not escape it. */\n rootDirectory: string;\n}\n\nasync function scanTarget(target: string, options: ResolvedOptions): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n let canonicalTarget = target;\n try {\n canonicalTarget = await realpath(target);\n } catch {\n // stat below reports missing targets with the original path.\n }\n\n const fallbackDisplayRoot = path.dirname(canonicalTarget);\n let targetStat;\n\n try {\n targetStat = await stat(canonicalTarget);\n } catch (error) {\n const fatalError = `${formatPath(canonicalTarget, fallbackDisplayRoot)}: ${formatError(error)}`;\n return { displayRoot: fallbackDisplayRoot, files, errors: [fatalError], warnings, fatalError };\n }\n\n if (targetStat.isFile()) {\n const displayRoot = path.dirname(canonicalTarget);\n const language = getLanguage(canonicalTarget, options, true);\n if (!language) {\n const fatalError = `${formatPath(canonicalTarget, displayRoot)}: unsupported file type`;\n return { displayRoot, files, errors: [fatalError], warnings, fatalError };\n }\n\n const context = makeScanContext(options, files, errors, warnings, displayRoot);\n await measureFile(canonicalTarget, language, 'single-file', context, canonicalTarget);\n return { displayRoot, files, errors, warnings };\n }\n\n await scanDirectory(canonicalTarget, makeScanContext(options, files, errors, warnings, canonicalTarget));\n return { displayRoot: canonicalTarget, files, errors, warnings };\n}\n\nfunction makeScanContext(\n options: ResolvedOptions,\n files: FileMetrics[],\n errors: string[],\n warnings: string[],\n rootDirectory: string\n): ScanContext {\n return { options, files, errors, warnings, visitedDirectories: new Set(), visitedFiles: new Set(), rootDirectory };\n}\n\nasync function scanDirectory(directory: string, context: ScanContext): Promise<void> {\n let resolvedDirectory;\n try {\n resolvedDirectory = await realpath(directory);\n } catch (error) {\n context.errors.push(`${formatPath(directory, context.rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n if (!isWithinDirectory(resolvedDirectory, context.rootDirectory)) {\n return;\n }\n\n if (context.visitedDirectories.has(resolvedDirectory)) {\n return;\n }\n context.visitedDirectories.add(resolvedDirectory);\n\n let entries;\n try {\n entries = await readdir(directory, { withFileTypes: true });\n } catch (error) {\n context.errors.push(`${formatPath(directory, context.rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n for (const entry of entries) {\n const entryPath = path.join(directory, entry.name);\n if (entry.isSymbolicLink()) {\n await scanSymbolicLink(entry.name, entryPath, context);\n continue;\n }\n\n if (entry.isDirectory()) {\n if (shouldSkipDirectory(entry.name, context.options)) {\n continue;\n }\n await scanDirectory(entryPath, context);\n continue;\n }\n\n if (entry.isFile()) {\n await measureScannableFile(entryPath, context);\n }\n }\n}\n\nasync function scanSymbolicLink(name: string, entryPath: string, context: ScanContext): Promise<void> {\n let resolvedPath;\n try {\n resolvedPath = await realpath(entryPath);\n } catch (error) {\n context.errors.push(`${formatPath(entryPath, context.rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n if (!isWithinDirectory(resolvedPath, context.rootDirectory)) {\n return;\n }\n\n let entryStat;\n try {\n entryStat = await stat(entryPath);\n } catch (error) {\n context.errors.push(`${formatPath(entryPath, context.rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n if (entryStat.isDirectory()) {\n if (\n shouldSkipDirectory(name, context.options) ||\n shouldSkipDirectory(path.basename(resolvedPath), context.options)\n ) {\n return;\n }\n await scanDirectory(entryPath, context);\n return;\n }\n\n if (entryStat.isFile()) {\n await measureScannableFile(entryPath, context, resolvedPath, resolvedPath);\n }\n}\n\nasync function measureScannableFile(\n file: string,\n context: ScanContext,\n languageFile = file,\n realFile?: string\n): Promise<void> {\n const language = getLanguage(languageFile, context.options);\n if (language) {\n await measureFile(file, language, 'directory', context, realFile);\n }\n}\n\nasync function measureFile(\n file: string,\n language: LanguageName,\n mode: 'single-file' | 'directory',\n context: ScanContext,\n realFile?: string\n): Promise<void> {\n try {\n const resolvedFile = realFile ?? (await realpath(file));\n if (context.visitedFiles.has(resolvedFile)) {\n return;\n }\n context.visitedFiles.add(resolvedFile);\n\n const code = await readFile(file, 'utf8');\n const measureOptions = { language, duplication: context.options.duplication };\n const fileMetrics: FileMetrics = { file, metrics: measureCode(code, measureOptions) };\n // Only directory scans compare files against each other; a single-file target has no peers.\n // Candidate collection failing (it always parses with the JavaScript binding, which can give\n // up where the native backend measured fine) must not discard the measured metrics.\n if (mode === 'directory') {\n try {\n fileMetrics.duplicationCandidates = collectCrossFileDuplicationFileData(code, measureOptions);\n } catch (error) {\n // A warning, not an error: the file's metrics are complete, only its participation in\n // cross-file matching is lost, so it is not \"skipped\" and must not fail --fail-on-error.\n context.warnings.push(\n `${formatPath(file, context.rootDirectory)}: cross-file duplication candidates unavailable: ${formatError(error)}`\n );\n }\n }\n context.files.push(fileMetrics);\n } catch (error) {\n context.errors.push(`${formatPath(file, context.rootDirectory)}: ${formatError(error)}`);\n }\n}\n\n/** Runs after the scan so every measured file's candidates participate. */\nfunction addCrossFileDuplication(result: ScanResult, options: ResolvedOptions): void {\n if (result.fatalError || result.files.length < 2) {\n return;\n }\n const sourceFiles = result.files.flatMap(({ file, duplicationCandidates }) =>\n duplicationCandidates ? [{ file: formatPath(file, result.displayRoot), ...duplicationCandidates }] : []\n );\n if (sourceFiles.length < 2) {\n return;\n }\n result.crossFileDuplication = measureCrossFileDuplication(sourceFiles, options.duplication);\n}\n\nfunction rankFiles(result: ScanResult, top: number): RankedFile[] {\n const candidates = result.files.map(({ file, metrics }) => {\n const formattedFile = formatPath(file, result.displayRoot);\n const duplicatedLines = collectDuplicatedLines(metrics, result.crossFileDuplication, formattedFile);\n return {\n file: formattedFile,\n worstFunction: findWorstFunction(metrics.functions),\n duplicatedLineCount: duplicatedLines.size,\n duplicatedLineRatio: metrics.lines.code === 0 ? 0 : duplicatedLines.size / metrics.lines.code,\n crossFilePartners: [] as string[],\n ncss: metrics.ncssCount,\n codeLines: metrics.lines.code,\n };\n });\n\n const cognitivePercentile = makePercentile(candidates.map((c) => c.worstFunction?.cognitiveComplexity ?? 0));\n const duplicationPercentile = makePercentile(candidates.map((c) => c.duplicatedLineCount));\n const ncssPercentile = makePercentile(candidates.map((c) => c.ncss));\n\n const ranked = candidates\n .map((candidate) => ({\n ...candidate,\n score:\n cognitivePercentile(candidate.worstFunction?.cognitiveComplexity ?? 0) +\n duplicationPercentile(candidate.duplicatedLineCount) +\n ncssPercentile(candidate.ncss),\n }))\n .toSorted(\n (left, right) => right.score - left.score || right.ncss - left.ncss || left.file.localeCompare(right.file)\n );\n // Partner evidence is attached only to the files that will be reported: expanding partners for\n // every scanned file first would retain TH(F^2) strings when one clone group spans F files, and\n // rescanning all groups per file would be O(files x groups) on the post-scan ranking step.\n attachCrossFilePartners(ranked.slice(0, top), result.crossFileDuplication);\n return ranked;\n}\n\n/** One pass over the groups fills the reported files' partner lists (other files sharing a group). */\nfunction attachCrossFilePartners(\n reportedFiles: RankedFile[],\n crossFileDuplication: CrossFileDuplicationMetrics | undefined\n): void {\n if (!crossFileDuplication) {\n return;\n }\n const partnersByFile = new Map(reportedFiles.map((ranked) => [ranked.file, new Set<string>()]));\n for (const group of crossFileDuplication.groups) {\n for (const file of group.files) {\n const partners = partnersByFile.get(file);\n if (!partners) {\n continue;\n }\n for (const partner of group.files) {\n if (partner !== file) {\n partners.add(partner);\n }\n }\n }\n }\n for (const ranked of reportedFiles) {\n ranked.crossFilePartners = [...(partnersByFile.get(ranked.file) ?? [])].toSorted();\n }\n}\n\n/**\n * Percentile rank within the scanned project: the fraction of files with a strictly smaller\n * value, in [0, 1). Relative ranking needs no absolute threshold, which sidesteps the metric\n * calibration problem entirely — the top of the list is worth refactoring first regardless of\n * where any cutoff would sit.\n */\nfunction makePercentile(values: number[]): (value: number) => number {\n const sorted = values.toSorted((left, right) => left - right);\n return (value: number) => {\n let low = 0;\n let high = sorted.length;\n while (low < high) {\n const middle = (low + high) >> 1;\n if ((sorted[middle] as number) < value) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return sorted.length === 0 ? 0 : low / sorted.length;\n };\n}\n\n/** The highest-cognitive-complexity function; NCSS breaks ties so the larger body is reported. */\nfunction findWorstFunction(functions: FunctionMetrics[]): WorstFunction | undefined {\n let worst: FunctionMetrics | undefined;\n for (const fn of functions) {\n if (\n !worst ||\n fn.cognitiveComplexity > worst.cognitiveComplexity ||\n (fn.cognitiveComplexity === worst.cognitiveComplexity && fn.ncss > worst.ncss)\n ) {\n worst = fn;\n }\n }\n if (!worst) {\n return undefined;\n }\n return {\n name: worst.name ?? '<anonymous>',\n startLine: worst.startLine,\n endLine: worst.endLine,\n cognitiveComplexity: worst.cognitiveComplexity,\n ncss: worst.ncss,\n nestingDepth: worst.nestingDepth,\n };\n}\n\n/**\n * Distinct 1-based code lines covered by within-file or cross-file duplicated content. Both\n * sources expose the exact lines carrying matched tokens (never block bounding ranges, which\n * would over-count comment/blank lines and the unmatched gap of a merged clone), so the union is\n * a subset of the file's code lines and the derived ratio can never exceed 1.\n */\nfunction collectDuplicatedLines(\n metrics: CodeMetrics,\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n formattedFile: string\n): Set<number> {\n const lines = new Set(metrics.duplication.duplicateLineNumbers);\n // Object.hasOwn: a file named like an Object.prototype member must not read an inherited value.\n const crossFileLines =\n crossFileDuplication && Object.hasOwn(crossFileDuplication.duplicateLineNumbersByFile, formattedFile)\n ? (crossFileDuplication.duplicateLineNumbersByFile[formattedFile] ?? [])\n : [];\n for (const line of crossFileLines) {\n lines.add(line);\n }\n return lines;\n}\n\nfunction printJson(result: ScanResult, rankedFiles: RankedFile[], options: ResolvedOptions): void {\n const reportedFiles = rankedFiles.slice(0, options.top);\n writeStdout(\n JSON.stringify(\n {\n summary: summarize(result.files),\n totalRankedFiles: rankedFiles.length,\n truncated: reportedFiles.length < rankedFiles.length,\n files: reportedFiles,\n errors: result.errors,\n warnings: result.warnings,\n },\n undefined,\n 2\n ) + '\\n'\n );\n}\n\nfunction printTextReport(\n target: string,\n result: ScanResult,\n rankedFiles: RankedFile[],\n options: ResolvedOptions\n): void {\n if (result.fatalError) {\n writeStderr(`Error: ${result.fatalError}\\n`);\n return;\n }\n\n const summary = summarize(result.files);\n writeStdout(\n `Measured ${summary.fileCount} files under ${target} (code LOC ${summary.linesOfCode}, NCSS ${summary.ncssCount}, functions ${summary.functionCount})\\n`\n );\n\n if (rankedFiles.length === 0) {\n writeStdout('No measurable files found.\\n');\n } else {\n const reportedFiles = rankedFiles.slice(0, options.top);\n const totalSuffix = rankedFiles.length > reportedFiles.length ? ` of ${rankedFiles.length}` : '';\n writeStdout(`\\nRefactoring candidates (top ${reportedFiles.length}${totalSuffix}):\\n`);\n for (const [index, ranked] of reportedFiles.entries()) {\n writeStdout(`${index + 1}. ${formatRankedFile(ranked)}\\n`);\n }\n }\n\n if (result.warnings.length > 0) {\n writeStderr(`\\nDegraded ${result.warnings.length} files (measured, but excluded from cross-file matching):\\n`);\n for (const warning of result.warnings.slice(0, 10)) {\n writeStderr(`- ${warning}\\n`);\n }\n if (result.warnings.length > 10) {\n writeStderr(`- ... ${result.warnings.length - 10} more\\n`);\n }\n }\n\n if (result.errors.length > 0) {\n writeStderr(`\\nSkipped ${result.errors.length} files or directories:\\n`);\n for (const error of result.errors.slice(0, 10)) {\n writeStderr(`- ${error}\\n`);\n }\n if (result.errors.length > 10) {\n writeStderr(`- ... ${result.errors.length - 10} more\\n`);\n }\n }\n}\n\n/** One ranked file as a single line: the location, the evidence, and where the duplication points. */\nfunction formatRankedFile(ranked: RankedFile): string {\n const reasons: string[] = [];\n if (ranked.worstFunction) {\n const fn = ranked.worstFunction;\n reasons.push(\n `worst function ${fn.name} (L${fn.startLine}-${fn.endLine}) cognitive ${fn.cognitiveComplexity}, NCSS ${fn.ncss}, nesting ${fn.nestingDepth}`\n );\n }\n if (ranked.duplicatedLineCount > 0) {\n const partnersSuffix =\n ranked.crossFilePartners.length > 0\n ? `, shared with ${ranked.crossFilePartners.slice(0, maxCrossFilePartners).join(', ')}${ranked.crossFilePartners.length > maxCrossFilePartners ? ', ...' : ''}`\n : '';\n reasons.push(\n `duplicated lines ${ranked.duplicatedLineCount} (${Math.round(ranked.duplicatedLineRatio * 100)}%${partnersSuffix})`\n );\n }\n reasons.push(`file NCSS ${ranked.ncss}`);\n return `${ranked.file} (score ${ranked.score.toFixed(2)}): ${reasons.join('; ')}`;\n}\n\nfunction summarize(files: FileMetrics[]): {\n fileCount: number;\n functionCount: number;\n linesOfCode: number;\n maxCognitiveComplexity: number;\n ncssCount: number;\n} {\n let functionCount = 0;\n let linesOfCode = 0;\n let maxCognitiveComplexity = 0;\n let ncssCount = 0;\n\n for (const file of files) {\n functionCount += file.metrics.functions.length;\n linesOfCode += file.metrics.lines.code;\n maxCognitiveComplexity = Math.max(maxCognitiveComplexity, file.metrics.maxCognitiveComplexity);\n ncssCount += file.metrics.ncssCount;\n }\n\n return { fileCount: files.length, functionCount, linesOfCode, maxCognitiveComplexity, ncssCount };\n}\n\nfunction shouldSkipDirectory(name: string, options: ResolvedOptions): boolean {\n if (ignoredDirectoryNames.has(name)) {\n return true;\n }\n\n if (options.includeTests) {\n return false;\n }\n\n return testDirectoryNames.has(name);\n}\n\nfunction isWithinDirectory(candidate: string, directory: string): boolean {\n const relative = path.relative(directory, candidate);\n return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));\n}\n\nfunction getLanguage(file: string, options: ResolvedOptions, explicitTarget = false): LanguageName | undefined {\n const lowerFile = file.toLowerCase();\n if (\n !explicitTarget &&\n (lowerFile.endsWith('.d.ts') ||\n lowerFile.endsWith('.d.mts') ||\n lowerFile.endsWith('.d.cts') ||\n lowerFile.endsWith('.min.js') ||\n lowerFile.endsWith('.pnp.cjs'))\n ) {\n return undefined;\n }\n\n if (\n !explicitTarget &&\n !options.includeTests &&\n (testFilePattern.test(path.basename(file)) || javaTestFilePattern.test(path.basename(file)))\n ) {\n return undefined;\n }\n\n // GCC treats an uppercase `.C` as C++; lowercasing first would misparse it with the C grammar.\n if (path.extname(file) === '.C') {\n return 'cpp';\n }\n\n return languageByExtension.get(path.extname(lowerFile));\n}\n\nfunction parsePercentInteger(value: string): number {\n const parsed = parsePositiveInteger(value);\n if (parsed > 100) {\n throw new InvalidArgumentError('Expected an integer between 1 and 100.');\n }\n return parsed;\n}\n\nfunction parseNonNegativeInteger(value: string): number {\n if (!/^\\d+$/u.test(value)) {\n throw new InvalidArgumentError('Expected a non-negative integer.');\n }\n\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed < 0) {\n throw new InvalidArgumentError('Expected a non-negative integer.');\n }\n return parsed;\n}\n\nfunction parsePositiveInteger(value: string): number {\n if (!/^[1-9]\\d*$/u.test(value)) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed < 1) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n return parsed;\n}\n\nfunction formatPath(file: string, base: string): string {\n return path.relative(base, file) || path.basename(file);\n}\n\nfunction writeStdout(message: string): void {\n process.stdout.write(message);\n}\n\nfunction writeStderr(message: string): void {\n process.stderr.write(message);\n}\n\nfunction formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":";gTA2DA,MAAM,EAAsB,IAAI,IAA0B,CACxD,CAAC,KAAM,GAAG,EACV,CAAC,OAAQ,KAAK,EACd,CAAC,MAAO,KAAK,EACb,CAAC,OAAQ,YAAY,EACrB,CAAC,MAAO,KAAK,EACb,CAAC,OAAQ,KAAK,EACd,CAAC,OAAQ,KAAK,EACd,CAAC,OAAQ,YAAY,EACrB,CAAC,OAAQ,KAAK,EACd,CAAC,MAAO,IAAI,EAEZ,CAAC,KAAM,KAAK,EACZ,CAAC,MAAO,KAAK,EACb,CAAC,OAAQ,KAAK,EACd,CAAC,OAAQ,KAAK,EACd,CAAC,QAAS,MAAM,EAChB,CAAC,MAAO,YAAY,EACpB,CAAC,OAAQ,KAAK,EACd,CAAC,OAAQ,YAAY,EACrB,CAAC,OAAQ,YAAY,EACrB,CAAC,MAAO,QAAQ,EAChB,CAAC,MAAO,MAAM,EACd,CAAC,MAAO,MAAM,EACd,CAAC,MAAO,YAAY,EACpB,CAAC,OAAQ,KAAK,CAChB,CAAC,EAEK,EAAwB,IAAI,IAAI,CACpC,UACA,UACA,UACA,OACA,QACA,kBACA,OACA,OACA,SACA,QACA,QACA,eACA,gBACA,cACA,WACA,OACA,WACA,YACA,eACA,SACA,gBACA,SACA,MACF,CAAC,EAKK,EAAqB,IAAI,IAAI,CAAC,YAAa,OAAQ,QAAS,MAAM,CAAC,EACnE,EAAkB,mEAGlB,EAAsB,eAGvB,EAAK,CAAC,CAAC,MAAO,GAAmB,CACpC,EAAY,UAAU,EAAY,CAAK,EAAE,GAAG,EAC5C,QAAQ,SAAW,CACrB,CAAC,EAED,eAAe,GAAsB,CACnC,IAAM,EAAU,IAAIA,EAAAA,QAAQ,CAAC,CAC1B,KAAK,YAAY,CAAC,CAClB,YAAY,sDAAsD,CAAC,CACnE,SAAS,WAAY,+BAAgC,GAAG,CAAC,CACzD,OAAO,kBAAmB,mDAAmDC,EAAAA,gBAAgB,CAAC,CAC9F,OAAO,iBAAkB,qDAAsD,CAAoB,CAAC,CACpG,OACC,oCACA,qEACA,CACF,CAAC,CACA,OACC,wCACA,wFACA,CACF,CAAC,CACA,OACC,gDACA,sHACA,CACF,CAAC,CACA,OAAO,kBAAmB,yCAAyC,CAAC,CACpE,OAAO,SAAU,mBAAmB,CAAC,CACrC,OAAO,kBAAmB,8DAA8D,EAE3F,EAAQ,OAAO,MAAO,EAAgB,IAA2B,CAC/D,IAAM,EAAiB,EAAc,CAAM,EACrC,EAAS,MAAMC,EAAAA,WAAW,EAAW,OAAQ,MAAM,EAAsB,CAAc,CAAC,EACxF,EAAUC,EAAAA,eAAe,EAAY,CAAM,EAC3C,EAAS,MAAM,EAAW,EAAgB,CAAO,EACvD,EAAwB,EAAQ,CAAO,EACvC,IAAM,EAAc,EAAU,EAAQ,EAAQ,GAAG,EAE7C,EAAQ,KACV,EAAU,EAAQ,EAAa,CAAO,EAEtC,EAAgB,EAAgB,EAAQ,EAAa,CAAO,GAG1D,EAAO,YAAe,EAAQ,aAAe,EAAO,OAAO,OAAS,KACtE,QAAQ,SAAW,EAEvB,CAAC,EAED,MAAM,EAAQ,WAAW,CAC3B,CAEA,SAAS,EAAc,EAAwB,CAS7C,OARI,IAAW,IACNC,EAAAA,QAAG,QAAQ,EAGhB,EAAO,WAAW,IAAI,EACjBC,EAAAA,QAAK,KAAKD,EAAAA,QAAG,QAAQ,EAAG,EAAO,MAAM,CAAC,CAAC,EAGzCC,EAAAA,QAAK,QAAQ,CAAM,CAC5B,CAGA,eAAe,EAAsB,EAAiC,CACpE,GAAI,CAEF,OAAO,MAAA,EADkBC,EAAAA,KAAAA,CAAK,CAAM,EAAA,CAClB,YAAY,EAAI,EAASD,EAAAA,QAAK,QAAQ,CAAM,CAChE,MAAQ,CACN,OAAOA,EAAAA,QAAK,QAAQ,CAAM,CAC5B,CACF,CAcA,eAAe,EAAW,EAAgB,EAA+C,CACvF,IAAM,EAAuB,CAAC,EACxB,EAAmB,CAAC,EACpB,EAAqB,CAAC,EACxB,EAAkB,EACtB,GAAI,CACF,EAAkB,MAAA,EAAME,EAAAA,SAAAA,CAAS,CAAM,CACzC,MAAQ,CAER,CAEA,IAAM,EAAsBF,EAAAA,QAAK,QAAQ,CAAe,EACpD,EAEJ,GAAI,CACF,EAAa,MAAA,EAAMC,EAAAA,KAAAA,CAAK,CAAe,CACzC,OAAS,EAAO,CACd,IAAM,EAAa,GAAG,EAAW,EAAiB,CAAmB,EAAE,IAAI,EAAY,CAAK,IAC5F,MAAO,CAAE,YAAa,EAAqB,QAAO,OAAQ,CAAC,CAAU,EAAG,WAAU,YAAW,CAC/F,CAEA,GAAI,EAAW,OAAO,EAAG,CACvB,IAAM,EAAcD,EAAAA,QAAK,QAAQ,CAAe,EAC1C,EAAW,EAAY,EAAiB,EAAS,EAAI,EAC3D,GAAI,CAAC,EAAU,CACb,IAAM,EAAa,GAAG,EAAW,EAAiB,CAAW,EAAE,yBAC/D,MAAO,CAAE,cAAa,QAAO,OAAQ,CAAC,CAAU,EAAG,WAAU,YAAW,CAC1E,CAEA,IAAM,EAAU,EAAgB,EAAS,EAAO,EAAQ,EAAU,CAAW,EAE7E,OADA,MAAM,EAAY,EAAiB,EAAU,cAAe,EAAS,CAAe,EAC7E,CAAE,cAAa,QAAO,SAAQ,UAAS,CAChD,CAGA,OADA,MAAM,EAAc,EAAiB,EAAgB,EAAS,EAAO,EAAQ,EAAU,CAAe,CAAC,EAChG,CAAE,YAAa,EAAiB,QAAO,SAAQ,UAAS,CACjE,CAEA,SAAS,EACP,EACA,EACA,EACA,EACA,EACa,CACb,MAAO,CAAE,UAAS,QAAO,SAAQ,WAAU,mBAAoB,IAAI,IAAO,aAAc,IAAI,IAAO,eAAc,CACnH,CAEA,eAAe,EAAc,EAAmB,EAAqC,CACnF,IAAI,EACJ,GAAI,CACF,EAAoB,MAAA,EAAME,EAAAA,SAAAA,CAAS,CAAS,CAC9C,OAAS,EAAO,CACd,EAAQ,OAAO,KAAK,GAAG,EAAW,EAAW,EAAQ,aAAa,EAAE,IAAI,EAAY,CAAK,GAAG,EAC5F,MACF,CAMA,GAJI,CAAC,EAAkB,EAAmB,EAAQ,aAAa,GAI3D,EAAQ,mBAAmB,IAAI,CAAiB,EAClD,OAEF,EAAQ,mBAAmB,IAAI,CAAiB,EAEhD,IAAI,EACJ,GAAI,CACF,EAAU,MAAA,EAAMC,EAAAA,QAAAA,CAAQ,EAAW,CAAE,cAAe,EAAK,CAAC,CAC5D,OAAS,EAAO,CACd,EAAQ,OAAO,KAAK,GAAG,EAAW,EAAW,EAAQ,aAAa,EAAE,IAAI,EAAY,CAAK,GAAG,EAC5F,MACF,CAEA,IAAK,IAAM,KAAS,EAAS,CAC3B,IAAM,EAAYH,EAAAA,QAAK,KAAK,EAAW,EAAM,IAAI,EACjD,GAAI,EAAM,eAAe,EAAG,CAC1B,MAAM,EAAiB,EAAM,KAAM,EAAW,CAAO,EACrD,QACF,CAEA,GAAI,EAAM,YAAY,EAAG,CACvB,GAAI,EAAoB,EAAM,KAAM,EAAQ,OAAO,EACjD,SAEF,MAAM,EAAc,EAAW,CAAO,EACtC,QACF,CAEI,EAAM,OAAO,GACf,MAAM,EAAqB,EAAW,CAAO,CAEjD,CACF,CAEA,eAAe,EAAiB,EAAc,EAAmB,EAAqC,CACpG,IAAI,EACJ,GAAI,CACF,EAAe,MAAA,EAAME,EAAAA,SAAAA,CAAS,CAAS,CACzC,OAAS,EAAO,CACd,EAAQ,OAAO,KAAK,GAAG,EAAW,EAAW,EAAQ,aAAa,EAAE,IAAI,EAAY,CAAK,GAAG,EAC5F,MACF,CAEA,GAAI,CAAC,EAAkB,EAAc,EAAQ,aAAa,EACxD,OAGF,IAAI,EACJ,GAAI,CACF,EAAY,MAAA,EAAMD,EAAAA,KAAAA,CAAK,CAAS,CAClC,OAAS,EAAO,CACd,EAAQ,OAAO,KAAK,GAAG,EAAW,EAAW,EAAQ,aAAa,EAAE,IAAI,EAAY,CAAK,GAAG,EAC5F,MACF,CAEA,GAAI,EAAU,YAAY,EAAG,CAC3B,GACE,EAAoB,EAAM,EAAQ,OAAO,GACzC,EAAoBD,EAAAA,QAAK,SAAS,CAAY,EAAG,EAAQ,OAAO,EAEhE,OAEF,MAAM,EAAc,EAAW,CAAO,EACtC,MACF,CAEI,EAAU,OAAO,GACnB,MAAM,EAAqB,EAAW,EAAS,EAAc,CAAY,CAE7E,CAEA,eAAe,EACb,EACA,EACA,EAAe,EACf,EACe,CACf,IAAM,EAAW,EAAY,EAAc,EAAQ,OAAO,EACtD,GACF,MAAM,EAAY,EAAM,EAAU,YAAa,EAAS,CAAQ,CAEpE,CAEA,eAAe,EACb,EACA,EACA,EACA,EACA,EACe,CACf,GAAI,CACF,IAAM,EAAe,GAAa,MAAA,EAAME,EAAAA,SAAAA,CAAS,CAAI,EACrD,GAAI,EAAQ,aAAa,IAAI,CAAY,EACvC,OAEF,EAAQ,aAAa,IAAI,CAAY,EAErC,IAAM,EAAO,MAAA,EAAME,EAAAA,SAAAA,CAAS,EAAM,MAAM,EAClC,EAAiB,CAAE,WAAU,YAAa,EAAQ,QAAQ,WAAY,EACtE,EAA2B,CAAE,OAAM,QAASC,EAAAA,YAAY,EAAM,CAAc,CAAE,EAIpF,GAAI,IAAS,YACX,GAAI,CACF,EAAY,sBAAwBC,EAAAA,oCAAoC,EAAM,CAAc,CAC9F,OAAS,EAAO,CAGd,EAAQ,SAAS,KACf,GAAG,EAAW,EAAM,EAAQ,aAAa,EAAE,mDAAmD,EAAY,CAAK,GACjH,CACF,CAEF,EAAQ,MAAM,KAAK,CAAW,CAChC,OAAS,EAAO,CACd,EAAQ,OAAO,KAAK,GAAG,EAAW,EAAM,EAAQ,aAAa,EAAE,IAAI,EAAY,CAAK,GAAG,CACzF,CACF,CAGA,SAAS,EAAwB,EAAoB,EAAgC,CACnF,GAAI,EAAO,YAAc,EAAO,MAAM,OAAS,EAC7C,OAEF,IAAM,EAAc,EAAO,MAAM,SAAS,CAAE,OAAM,2BAChD,EAAwB,CAAC,CAAE,KAAM,EAAW,EAAM,EAAO,WAAW,EAAG,GAAG,CAAsB,CAAC,EAAI,CAAC,CACxG,EACI,EAAY,OAAS,IAGzB,EAAO,qBAAuBC,EAAAA,4BAA4B,EAAa,EAAQ,WAAW,EAC5F,CAEA,SAAS,EAAU,EAAoB,EAA2B,CAChE,IAAM,EAAa,EAAO,MAAM,KAAK,CAAE,OAAM,aAAc,CACzD,IAAM,EAAgB,EAAW,EAAM,EAAO,WAAW,EACnD,EAAkB,EAAuB,EAAS,EAAO,qBAAsB,CAAa,EAClG,MAAO,CACL,KAAM,EACN,cAAe,EAAkB,EAAQ,SAAS,EAClD,oBAAqB,EAAgB,KACrC,oBAAqB,EAAQ,MAAM,OAAS,EAAI,EAAI,EAAgB,KAAO,EAAQ,MAAM,KACzF,kBAAmB,CAAC,EACpB,KAAM,EAAQ,UACd,UAAW,EAAQ,MAAM,IAC3B,CACF,CAAC,EAEK,EAAsB,EAAe,EAAW,IAAK,GAAM,EAAE,eAAe,qBAAuB,CAAC,CAAC,EACrG,EAAwB,EAAe,EAAW,IAAK,GAAM,EAAE,mBAAmB,CAAC,EACnF,EAAiB,EAAe,EAAW,IAAK,GAAM,EAAE,IAAI,CAAC,EAE7D,EAAS,EACZ,IAAK,IAAe,CACnB,GAAG,EACH,MACE,EAAoB,EAAU,eAAe,qBAAuB,CAAC,EACrE,EAAsB,EAAU,mBAAmB,EACnD,EAAe,EAAU,IAAI,CACjC,EAAE,CAAC,CACF,UACE,EAAM,IAAU,EAAM,MAAQ,EAAK,OAAS,EAAM,KAAO,EAAK,MAAQ,EAAK,KAAK,cAAc,EAAM,IAAI,CAC3G,EAKF,OADA,EAAwB,EAAO,MAAM,EAAG,CAAG,EAAG,EAAO,oBAAoB,EAClE,CACT,CAGA,SAAS,EACP,EACA,EACM,CACN,GAAI,CAAC,EACH,OAEF,IAAM,EAAiB,IAAI,IAAI,EAAc,IAAK,GAAW,CAAC,EAAO,KAAM,IAAI,GAAa,CAAC,CAAC,EAC9F,IAAK,IAAM,KAAS,EAAqB,OACvC,IAAK,IAAM,KAAQ,EAAM,MAAO,CAC9B,IAAM,EAAW,EAAe,IAAI,CAAI,EACnC,KAGL,IAAK,IAAM,KAAW,EAAM,MACtB,IAAY,GACd,EAAS,IAAI,CAAO,CAG1B,CAEF,IAAK,IAAM,KAAU,EACnB,EAAO,kBAAoB,CAAC,GAAI,EAAe,IAAI,EAAO,IAAI,GAAK,CAAC,CAAE,CAAC,CAAC,SAAS,CAErF,CAQA,SAAS,EAAe,EAA6C,CACnE,IAAM,EAAS,EAAO,UAAU,EAAM,IAAU,EAAO,CAAK,EAC5D,MAAQ,IAAkB,CACxB,IAAI,EAAM,EACN,EAAO,EAAO,OAClB,KAAO,EAAM,GAAM,CACjB,IAAM,EAAU,EAAM,GAAS,EAC1B,EAAO,GAAqB,EAC/B,EAAM,EAAS,EAEf,EAAO,CAEX,CACA,OAAO,EAAO,SAAW,EAAI,EAAI,EAAM,EAAO,MAChD,CACF,CAGA,SAAS,EAAkB,EAAyD,CAClF,IAAI,EACJ,IAAK,IAAM,KAAM,GAEb,CAAC,GACD,EAAG,oBAAsB,EAAM,qBAC9B,EAAG,sBAAwB,EAAM,qBAAuB,EAAG,KAAO,EAAM,QAEzE,EAAQ,GAGP,KAGL,MAAO,CACL,KAAM,EAAM,MAAQ,cACpB,UAAW,EAAM,UACjB,QAAS,EAAM,QACf,oBAAqB,EAAM,oBAC3B,KAAM,EAAM,KACZ,aAAc,EAAM,YACtB,CACF,CAQA,SAAS,EACP,EACA,EACA,EACa,CACb,IAAM,EAAQ,IAAI,IAAI,EAAQ,YAAY,oBAAoB,EAExD,EACJ,GAAwB,OAAO,OAAO,EAAqB,2BAA4B,CAAa,EAC/F,EAAqB,2BAA2B,IAAkB,CAAC,EACpE,CAAC,EACP,IAAK,IAAM,KAAQ,EACjB,EAAM,IAAI,CAAI,EAEhB,OAAO,CACT,CAEA,SAAS,EAAU,EAAoB,EAA2B,EAAgC,CAChG,IAAM,EAAgB,EAAY,MAAM,EAAG,EAAQ,GAAG,EACtD,EACE,KAAK,UACH,CACE,QAAS,EAAU,EAAO,KAAK,EAC/B,iBAAkB,EAAY,OAC9B,UAAW,EAAc,OAAS,EAAY,OAC9C,MAAO,EACP,OAAQ,EAAO,OACf,SAAU,EAAO,QACnB,EACA,IAAA,GACA,CACF,EAAI;CACN,CACF,CAEA,SAAS,EACP,EACA,EACA,EACA,EACM,CACN,GAAI,EAAO,WAAY,CACrB,EAAY,UAAU,EAAO,WAAW,GAAG,EAC3C,MACF,CAEA,IAAM,EAAU,EAAU,EAAO,KAAK,EAKtC,GAJA,EACE,YAAY,EAAQ,UAAU,eAAe,EAAO,aAAa,EAAQ,YAAY,SAAS,EAAQ,UAAU,cAAc,EAAQ,cAAc,IACtJ,EAEI,EAAY,SAAW,EACzB,EAAY;CAA8B,MACrC,CACL,IAAM,EAAgB,EAAY,MAAM,EAAG,EAAQ,GAAG,EAChD,EAAc,EAAY,OAAS,EAAc,OAAS,OAAO,EAAY,SAAW,GAC9F,EAAY,iCAAiC,EAAc,SAAS,EAAY,KAAK,EACrF,IAAK,GAAM,CAAC,EAAO,KAAW,EAAc,QAAQ,EAClD,EAAY,GAAG,EAAQ,EAAE,IAAI,EAAiB,CAAM,EAAE,GAAG,CAE7D,CAEA,GAAI,EAAO,SAAS,OAAS,EAAG,CAC9B,EAAY,cAAc,EAAO,SAAS,OAAO,4DAA4D,EAC7G,IAAK,IAAM,KAAW,EAAO,SAAS,MAAM,EAAG,EAAE,EAC/C,EAAY,KAAK,EAAQ,GAAG,EAE1B,EAAO,SAAS,OAAS,IAC3B,EAAY,SAAS,EAAO,SAAS,OAAS,GAAG,QAAQ,CAE7D,CAEA,GAAI,EAAO,OAAO,OAAS,EAAG,CAC5B,EAAY,aAAa,EAAO,OAAO,OAAO,yBAAyB,EACvE,IAAK,IAAM,KAAS,EAAO,OAAO,MAAM,EAAG,EAAE,EAC3C,EAAY,KAAK,EAAM,GAAG,EAExB,EAAO,OAAO,OAAS,IACzB,EAAY,SAAS,EAAO,OAAO,OAAS,GAAG,QAAQ,CAE3D,CACF,CAGA,SAAS,EAAiB,EAA4B,CACpD,IAAM,EAAoB,CAAC,EAC3B,GAAI,EAAO,cAAe,CACxB,IAAM,EAAK,EAAO,cAClB,EAAQ,KACN,kBAAkB,EAAG,KAAK,KAAK,EAAG,UAAU,GAAG,EAAG,QAAQ,cAAc,EAAG,oBAAoB,SAAS,EAAG,KAAK,YAAY,EAAG,cACjI,CACF,CACA,GAAI,EAAO,oBAAsB,EAAG,CAClC,IAAM,EACJ,EAAO,kBAAkB,OAAS,EAC9B,iBAAiB,EAAO,kBAAkB,MAAM,EAAG,CAAoB,CAAC,CAAC,KAAK,IAAI,IAAI,EAAO,kBAAkB,OAAS,EAAuB,QAAU,KACzJ,GACN,EAAQ,KACN,oBAAoB,EAAO,oBAAoB,IAAI,KAAK,MAAM,EAAO,oBAAsB,GAAG,EAAE,GAAG,EAAe,EACpH,CACF,CAEA,OADA,EAAQ,KAAK,aAAa,EAAO,MAAM,EAChC,GAAG,EAAO,KAAK,UAAU,EAAO,MAAM,QAAQ,CAAC,EAAE,KAAK,EAAQ,KAAK,IAAI,GAChF,CAEA,SAAS,EAAU,EAMjB,CACA,IAAI,EAAgB,EAChB,EAAc,EACd,EAAyB,EACzB,EAAY,EAEhB,IAAK,IAAM,KAAQ,EACjB,GAAiB,EAAK,QAAQ,UAAU,OACxC,GAAe,EAAK,QAAQ,MAAM,KAClC,EAAyB,KAAK,IAAI,EAAwB,EAAK,QAAQ,sBAAsB,EAC7F,GAAa,EAAK,QAAQ,UAG5B,MAAO,CAAE,UAAW,EAAM,OAAQ,gBAAe,cAAa,yBAAwB,WAAU,CAClG,CAEA,SAAS,EAAoB,EAAc,EAAmC,CAS5E,OARI,EAAsB,IAAI,CAAI,EACzB,GAGT,CAAI,EAAQ,cAIL,EAAmB,IAAI,CAAI,CACpC,CAEA,SAAS,EAAkB,EAAmB,EAA4B,CACxE,IAAM,EAAWP,EAAAA,QAAK,SAAS,EAAW,CAAS,EACnD,OAAO,IAAa,IAAO,IAAa,MAAQ,CAAC,EAAS,WAAW,KAAKA,EAAAA,QAAK,KAAK,GAAK,CAACA,EAAAA,QAAK,WAAW,CAAQ,CACpH,CAEA,SAAS,EAAY,EAAc,EAA0B,EAAiB,GAAiC,CAC7G,IAAM,EAAY,EAAK,YAAY,EAEjC,MAAC,IACA,EAAU,SAAS,OAAO,GACzB,EAAU,SAAS,QAAQ,GAC3B,EAAU,SAAS,QAAQ,GAC3B,EAAU,SAAS,SAAS,GAC5B,EAAU,SAAS,UAAU,KAM/B,GAAC,GACD,CAAC,EAAQ,eACR,EAAgB,KAAKA,EAAAA,QAAK,SAAS,CAAI,CAAC,GAAK,EAAoB,KAAKA,EAAAA,QAAK,SAAS,CAAI,CAAC,IAU5F,OAJIA,EAAAA,QAAK,QAAQ,CAAI,IAAM,KAClB,MAGF,EAAoB,IAAIA,EAAAA,QAAK,QAAQ,CAAS,CAAC,CACxD,CAEA,SAAS,EAAoB,EAAuB,CAClD,IAAM,EAAS,EAAqB,CAAK,EACzC,GAAI,EAAS,IACX,MAAM,IAAIQ,EAAAA,qBAAqB,wCAAwC,EAEzE,OAAO,CACT,CAEA,SAAS,EAAwB,EAAuB,CACtD,GAAI,CAAC,SAAS,KAAK,CAAK,EACtB,MAAM,IAAIA,EAAAA,qBAAqB,kCAAkC,EAGnE,IAAM,EAAS,OAAO,CAAK,EAC3B,GAAI,CAAC,OAAO,cAAc,CAAM,GAAK,EAAS,EAC5C,MAAM,IAAIA,EAAAA,qBAAqB,kCAAkC,EAEnE,OAAO,CACT,CAEA,SAAS,EAAqB,EAAuB,CACnD,GAAI,CAAC,cAAc,KAAK,CAAK,EAC3B,MAAM,IAAIA,EAAAA,qBAAqB,8BAA8B,EAG/D,IAAM,EAAS,OAAO,CAAK,EAC3B,GAAI,CAAC,OAAO,cAAc,CAAM,GAAK,EAAS,EAC5C,MAAM,IAAIA,EAAAA,qBAAqB,8BAA8B,EAE/D,OAAO,CACT,CAEA,SAAS,EAAW,EAAc,EAAsB,CACtD,OAAOR,EAAAA,QAAK,SAAS,EAAM,CAAI,GAAKA,EAAAA,QAAK,SAAS,CAAI,CACxD,CAEA,SAAS,EAAY,EAAuB,CAC1C,QAAQ,OAAO,MAAM,CAAO,CAC9B,CAEA,SAAS,EAAY,EAAuB,CAC1C,QAAQ,OAAO,MAAM,CAAO,CAC9B,CAEA,SAAS,EAAY,EAAwB,CAC3C,OAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,CAC9D"}