energy-state-analyzer 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.esaignore ADDED
@@ -0,0 +1,6 @@
1
+ # Paths the energy-state analyzer itself should not scan.
2
+ # One pattern per line — a literal path/directory (also matches at any depth if it has no
3
+ # '/'), or a single '*' basename glob. See src/core/esaignore.ts for the exact rules.
4
+
5
+ # Deliberately bad/deeply-nested code used only to exercise the detectors themselves.
6
+ src/test/fixtures
@@ -0,0 +1,7 @@
1
+ node_modules/
2
+ dist/
3
+ out/
4
+ grammars/
5
+ *.vsix
6
+ CHANGELOG.md
7
+ src/test/fixtures/
@@ -0,0 +1,7 @@
1
+ {
2
+ "tabWidth": 4,
3
+ "singleQuote": true,
4
+ "semi": true,
5
+ "printWidth": 120,
6
+ "trailingComma": "none"
7
+ }
package/AGENTS.md ADDED
@@ -0,0 +1,84 @@
1
+ # AGENTS.md
2
+
3
+ This file provides guidance to AI coding agents when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ Energy State Analyzer is a VS Code extension that visualizes "energy states" in Python, F#, TypeScript, and Kotlin code via real-time static analysis. It parses source with `web-tree-sitter` (per-language WASM grammars in `grammars/`) and highlights code that is complex, deeply nested, or otherwise hard to maintain using editor decorations, gutter icons, and Problems-panel diagnostics.
8
+
9
+ See `energy-state.md` for the original design doc (energy-state principle, planned "detection agents", and known issues/next steps at project inception).
10
+
11
+ ## Build Commands
12
+
13
+ Commands are wrapped in a `Justfile`; run `just --list` to see all of them. Prefer these over calling `npm run` directly.
14
+
15
+ ```bash
16
+ just install # npm install
17
+ just build # Build extension bundle via webpack (dev mode)
18
+ just watch # Webpack in watch mode
19
+ just lint # ESLint over src/**/*.ts
20
+ just format # Format src/**/*.ts in place with Prettier
21
+ just format-check # Check formatting without writing changes (used by CI)
22
+ just analyze # Run the CLI's own analyzer over src/ (or `just analyze <path...>` for specific files/dirs)
23
+ just test # compile-tests + compile + lint, then run the VS Code extension test host
24
+ just pack # Production build + package into a .vsix via vsce
25
+ just clean # Remove build artifacts (dist, out, *.vsix)
26
+ ```
27
+
28
+ The underlying `npm run` scripts (`compile`, `watch`, `package`, `lint`, `format`, `format-check`, `compile-tests`, `watch-tests`, `pretest`, `test`, `analyze`) still work directly if you need finer control than the Justfile recipes give you.
29
+
30
+ To run and debug the extension interactively, press `F5` in VS Code — this launches an Extension Development Host with the extension loaded, per `.vscode/launch.json`.
31
+
32
+ There is a single test suite (`src/test/extension.test.ts`); there's no mechanism yet to run a single test by name — use the Extension Test Runner's Testing view in VS Code, or edit the suite temporarily with `.only`.
33
+
34
+ ## Architecture
35
+
36
+ Everything lives in one file, `src/extension.ts`, structured as:
37
+
38
+ 1. **Activation (`activate`)** — initializes the tree-sitter `Parser`, creates decoration types, registers the `energy-state-analyzer.analyze` command, and wires up editor/document change listeners to re-analyze on the fly. Each language's grammar (`grammars/tree-sitter-<language>.wasm`, path resolved via `context.extensionPath`) is loaded lazily on first use of that language, not up front — see `getOrLoadLanguage`. Activation is gated by the `onLanguage:*` entries in `package.json` (python, fsharp, typescript, kotlin).
39
+ 2. **Analysis pipeline (`analyzeDocument`)** — parses the active document's text into a tree-sitter AST, then runs a fixed set of independent detector passes over it, each returning `EnergyViolation[]`:
40
+ - `analyzeNesting` — flags `if`/`for`/`while`/`with` nesting deeper than 3 levels.
41
+ - `analyzeFunctionComplexity` — computes cyclomatic complexity per function, flags >10.
42
+ - `analyzeFileCoherence` — flags files with too many functions or imports (utils/helpers sprawl).
43
+ - `analyzeMagicValues` — flags "magic" numeric/string literals outside constant context.
44
+ - `analyzeParameterCount` — flags functions with >5 parameters.
45
+ - `analyzeInversionOpportunities` — flags large dominant if-blocks, nested validation chains, and deep if-nesting that could be rewritten as guard clauses / early returns.
46
+ - `extractTypeInformation` — walks the AST separately to collect function/class/variable/import type info (currently only logged; scaffolding for future features, not yet used for violations).
47
+ Each detector does its own `traverse(node)` walk of the tree-sitter tree; there's no shared visitor abstraction.
48
+ `src/core/analyze.ts` (the actual current pipeline entry point) runs a larger, up-to-date set of these plus `applySuppressions` (`src/core/suppressions.ts`) as a final pass — it filters out violations covered by an `esa-ignore`/`esa-ignore-file` comment and emits low-severity `suppression` findings for directives that are unused or name an unknown type. See `docs/detectors/suppression.md`.
49
+ 3. **Presentation** — `applyDecorations` maps violations to `vscode.TextEditorDecorationType` ranges (color/severity: red=high, yellow=medium, green=low, rendered as background tint + gutter lightning-bolt icon via `createLightningIcon`), and `updateProblemsPanel` mirrors the same violations into a `vscode.DiagnosticCollection` so they also show in the Problems panel.
50
+ 4. **Violation model** — `EnergyViolation { line, column, type, severity, message }`, with `type` and `severity` string-literal unions backed by the `VIOLATION_TYPE`/`SEVERITY` constant objects near the top of the file (keep these two in sync when adding a new detector).
51
+
52
+ ### Adding a new detector
53
+
54
+ Follow the existing pattern: write an `analyze<Thing>(tree, document): EnergyViolation[]` function that walks `tree.rootNode`, push it into the list in `analyzeDocument`, and add a new `VIOLATION_TYPE` entry if it's a new category. If the violation needs special range highlighting, add a case in `applyDecorations`.
55
+
56
+ ### Build/packaging notes
57
+
58
+ - Webpack bundles `src/extension.ts` → `dist/extension.js` (CommonJS, `vscode` module treated as external).
59
+ - `web-tree-sitter`'s own `tree-sitter.wasm` is copied into `dist/` via `CopyWebpackPlugin` (webpack.config.js); the per-language grammar WASMs in `grammars/` ship separately and are loaded at runtime by path, not bundled.
60
+ - `tsconfig.json` targets ES2022/commonjs with `strict: true`.
61
+
62
+ ## Before Committing or Opening a PR
63
+
64
+ Run `just format`, `just lint`, and `just analyze` (this project dogfoods its own analyzer over `src/`) before every commit or PR, and fix what they flag. Don't rely on CI to catch formatting, lint, or energy-state violations you could have caught locally.
65
+
66
+ If satisfying `just analyze` on your change requires refactoring existing code (e.g. splitting a file to fix a coherence violation, extracting a function to fix complexity/nesting) rather than just the new code you're adding, do that refactor as its own preceding PR, merged before the PR with the actual change. Don't mix the two in one PR — a refactor bundled with a behavior change makes the diff hard to review and obscures what the change is actually about.
67
+
68
+ ## Releasing
69
+
70
+ Release automation runs through EasyBuild.ShipIt (see `RELEASING.md`). Use
71
+ Conventional Commit subjects (`feat:`, `fix:`, `docs:`, `chore:`, `ci:`, etc.)
72
+ for commits and PR titles — CI enforces this on PR titles, and ShipIt uses
73
+ them to generate `CHANGELOG.md` and open release PRs. Do not hand-edit
74
+ generated changelog entries or bump `package.json`'s version manually.
75
+
76
+ ## Agent Decision Comments
77
+
78
+ This repository uses Agent Decision Comments.
79
+ See `AGENT_DECISION_COMMENTS.md` for the locally adopted convention.
80
+ Upstream releases: https://github.com/dbrattli/adc/releases
81
+
82
+ Before modifying code, read the ADCs already governing it.
83
+ Treat them as active constraints and justify any change explicitly.
84
+ Add ADCs for non-obvious rationale introduced by your change.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- last_commit_released: 489d195ffe125b4e3ebcb9905f561f2d94daf1ad
2
+ last_commit_released: c52e34266a2eaeedc6f15e737f593420f3e340f9
3
3
  name: energy-state-analyzer
4
4
  updaters:
5
5
  - command: npm version {version} --no-git-tag-version --allow-same-version
@@ -11,6 +11,32 @@ All notable changes to the "energy-state-analyzer" extension are generated by
11
11
  [EasyBuild.ShipIt](https://github.com/easybuild-org/EasyBuild.ShipIt) from
12
12
  [Conventional Commits](https://www.conventionalcommits.org/).
13
13
 
14
+ ## 0.5.0 - 2026-08-27
15
+
16
+ ### 🚀 Features
17
+
18
+ * Exempt test files from the magic-number detector (#33) ([18023bb](https://github.com/cardamomcode/energy-state-analyzer/commit/18023bb92d74f66f189ac7207c1c9bb40f17aca9))
19
+
20
+ ### 🐞 Bug Fixes
21
+
22
+ * Count distinct import sources, not raw lines, for import sprawl (#35) ([c52e342](https://github.com/cardamomcode/energy-state-analyzer/commit/c52e34266a2eaeedc6f15e737f593420f3e340f9))
23
+
24
+ <strong><small>[View changes on Github](https://github.com/cardamomcode/energy-state-analyzer/compare/e7c2d41d1347a4a10f7a921e077a45f745eade4c..c52e34266a2eaeedc6f15e737f593420f3e340f9)</small></strong>
25
+
26
+ ## 0.4.0 - 2026-08-26
27
+
28
+ ### 🚀 Features
29
+
30
+ * Add .esaignore to exclude paths from the analyzer (#29) ([bd526b4](https://github.com/cardamomcode/energy-state-analyzer/commit/bd526b45970c72d611e127e2ed7f111f2655f937))
31
+ * Inline esa-ignore suppression directives (#25) ([e7c2d41](https://github.com/cardamomcode/energy-state-analyzer/commit/e7c2d41d1347a4a10f7a921e077a45f745eade4c))
32
+
33
+ ### 🐞 Bug Fixes
34
+
35
+ * Remove duplicate activation toasts on startup (#27) ([d144a00](https://github.com/cardamomcode/energy-state-analyzer/commit/d144a00770492252b50870b4cf3cebc1b44ba4c5))
36
+ * Count try/catch blocks toward nesting-depth violations (#28) ([d483c16](https://github.com/cardamomcode/energy-state-analyzer/commit/d483c165b4ba63a2e6a4c6eae5dfd62bc9868ecc))
37
+
38
+ <strong><small>[View changes on Github](https://github.com/cardamomcode/energy-state-analyzer/compare/489d195ffe125b4e3ebcb9905f561f2d94daf1ad..e7c2d41d1347a4a10f7a921e077a45f745eade4c)</small></strong>
39
+
14
40
  ## 0.3.0 - 2026-08-25
15
41
 
16
42
  ### 🚀 Features
package/README.md CHANGED
@@ -4,19 +4,20 @@ Visualizes "energy states" in Python, F#, and TypeScript code as you edit: parts
4
4
 
5
5
  ## Features
6
6
 
7
- - **Real-time analysis** of the active Python, F#, or TypeScript file, re-run on every edit and on editor focus change.
8
- - **Cyclomatic complexity** — flags functions with too many independent execution paths (`if`/`for`/`while`/`except`/boolean operators/ternaries all count equally, regardless of nesting).
9
- - **Cognitive complexity** flags functions that are hard to *read*, weighting each decision point by how deeply it's nested and not penalizing early-return guard clauses.
10
- - **Excessive nesting** flags `if`/`for`/`while`/`with` blocks nested more than 3 levels deep.
11
- - **File coherence** — flags files with too many functions or imports (a sign of "utils/helpers sprawl"), and separately flags files with too many large functions (regardless of total function count, so languages like F# with many small functions per module aren't penalized). A file where most functions share a leading name word (e.g. all `extractFoo`/`extractBar`) is treated as one coherent domain and exempted from the function-count check.
12
- - **Magic numbers** — flags numeric literals used outside of a named binding, an index/key position, or a default parameter value. Numbers get no free pass for "looking like prose" the way strings do, so this stays broad; an `energyStateAnalyzer.magicNumber.allowlist` setting (default `[0, 1, -1, 2]`) exempts the values that recur constantly without carrying hidden meaning.
13
- - **Magic strings** — flags a string literal only where an unnamed one actually risks a silent typo: compared with `==`/`===`, checked for membership (Python's `x in (...)`), or used as a dict/object key. A message being logged, thrown, or returned isn't a decision point, so it's left alone entirely — as is any f-string/template-literal/`.format()`/`%`-formatted string, since a placeholder is itself evidence the string isn't a stand-in for an enum value. To cut single-use false positives further, a qualifying literal is only flagged once it recurs at a decision point at least `energyStateAnalyzer.magicString.minDuplicates` times (default `2`) across the file.
14
- - **Parameter explosion** flags functions with more than 5 parameters.
15
- - **Inversion opportunities** flags large dominant `if` blocks and nested validation chains that could be rewritten as guard clauses with early returns.
16
- - **Primitive obsession** — flags consecutive same-typed primitive parameters (e.g. `lat: float, lon: float`) that callers can silently swap, and variables compared against 3+ distinct string literals (a de facto enum encoded as strings). Runs on Python, F#, and TypeScript; Python additionally flags a variable checked against a literal tuple/list/set in one `in` expression, since F# and TypeScript have no direct equivalent construct. In Python, a same-typed pair is not flagged when both parameters are keyword-only (after a bare `*` or `*args` in the signature) since a caller can no longer pass them positionally — named-parameter naming is still a weaker mitigation than a distinct type (`NewType`, a dataclass, etc.), since nothing stops a future `**kwargs`-splat call from transposing the values. This suppression doesn't apply to TypeScript (no argument-labeling syntax for positional params) or F# (named arguments are optional at the call site, so they don't prevent a positional call).
17
- - **Match opportunities** — flags an `if`/`elif`/`elif` chain (or TypeScript's nested `else if`) of 3 or more branches that all compare the same single variable to a literal, suggesting a `match`/`switch` statement instead. Runs on Python, F#, and TypeScript.
18
- - **Logical operator as control flow** — flags a bare `condition && doSomething()` (or `condition || fallback()`) statement, an `if` hidden behind a boolean operator instead of written as one. Runs on Python and TypeScript; not on F#, which has no such statement-level idiom in its grammar.
19
- - **Opaque boolean literal** flags a bare `true`/`false` passed positionally into a call (e.g. `configure(true)`), since a reader can't tell what it means without checking the callee's signature. Suppressed when the boolean is labeled at the call site: a Python keyword argument (`configure(retries=True)`), a TypeScript object-literal field (`configure({ retries: true })`), or F#'s named-argument syntax (`configure(retries = true)`) — unlike the primitive-obsession suppression above, F#'s named args count here even though they're optional, since this rule is about reader comprehension at this call site, not about preventing a future misuse. The preferred fix is usually splitting into two clearly named functions (`enableRetries()`/`disableRetries()`) or an enum; naming the argument is an acceptable but weaker mitigation. Deliberately conservative: only literal `true`/`false` are flagged, not bare `0`/`1`, to avoid noise on ordinary numeric arguments.
7
+ Real-time analysis of the active Python, F#, or TypeScript file, re-run on every edit and on editor focus change, via these detectors (see [docs/detectors](docs/detectors/README.md) for full detail on each):
8
+
9
+ - [Cyclomatic complexity](docs/detectors/cyclomatic-complexity.md), too many independent execution paths.
10
+ - [Cognitive complexity](docs/detectors/cognitive-complexity.md), too hard to read due to nesting.
11
+ - [Excessive nesting](docs/detectors/excessive-nesting.md), control-flow blocks nested too deep.
12
+ - [File coherence](docs/detectors/file-coherence.md), files that have lost a single responsibility.
13
+ - [Magic numbers](docs/detectors/magic-numbers.md), unnamed numeric literals.
14
+ - [Magic strings](docs/detectors/magic-strings.md), unnamed string literals at decision points.
15
+ - [Parameter explosion](docs/detectors/parameter-explosion.md), functions with too many parameters.
16
+ - [Inversion opportunities](docs/detectors/inversion-opportunities.md), nested conditionals that could be guard clauses.
17
+ - [Primitive obsession](docs/detectors/primitive-obsession.md), strings/numbers standing in for a real type.
18
+ - [Match opportunities](docs/detectors/match-opportunities.md), if/elif chains that could be a match/switch.
19
+ - [Logical operator as control flow](docs/detectors/logical-operator-control-flow.md), an `if` hidden behind `&&`/`||`.
20
+ - [Opaque boolean literal](docs/detectors/opaque-boolean-literal.md), an unlabeled `true`/`false` at a call site.
20
21
 
21
22
  Violations are shown three ways:
22
23
 
@@ -24,216 +25,54 @@ Violations are shown three ways:
24
25
  - A hover tooltip explaining the specific violation.
25
26
  - An entry in the Problems panel, sourced as "Energy State Analyzer".
26
27
 
27
- For functions flagged as too complex (cyclomatic or cognitive), a progressive heatmap in the configured high-energy color (orange by default) is also painted across the function body: each contributing line (an `if`, `for`, `and`, etc.) is shaded from light to dark based on how much it drives up that function's complexity relative to its own worst line so you can see exactly which branches to break apart first, instead of just knowing the function as a whole is complex.
28
+ For functions flagged as too complex (cyclomatic or cognitive), a progressive heatmap in the configured high-energy color (orange by default) is also painted across the function body: each contributing line (an `if`, `for`, `and`, etc.) is shaded from light to dark based on how much it drives up that function's complexity relative to its own worst line, so you can see exactly which branches to break apart first, instead of just knowing the function as a whole is complex.
28
29
 
29
30
  ## Energy and Entropy
30
31
 
31
- The name is a deliberate analogy to thermodynamics, not just a metaphor for "bad code."
32
-
33
- In physics, energy constrains which microstates a system can occupy, and entropy counts how many of those microstates are compatible with what we observe: `S(E) = k_B ln Ω(E)`. Adding energy usually increases entropy, because there are more ways to distribute it, but *how* it's distributed matters just as much as how much there is. A hot object next to a cold one has lower entropy than the same total energy spread evenly across both, which is why heat spontaneously flows from hot to cold: the system moves toward the macrostate with more compatible microstates.
34
-
35
- Code behaves the same way. A function's "energy" here is its cyclomatic/cognitive complexity, nesting depth, parameter count, and so on: the raw amount of decision-making and structure packed into it. Its "entropy" is the number of ways a reader can misunderstand it, the number of code paths a change can silently break, and the number of mental states a maintainer has to hold at once to reason about it correctly. Just as in physics, higher energy tends to raise entropy: a function with more branches and deeper nesting generally has more ways to go wrong. But it's not purely amount, *how* that complexity is arranged matters too:
36
-
37
- - A long function with 20 sequential, flat `if`s is high cyclomatic complexity but comparatively low entropy: each branch is independent and easy to reason about in isolation (the "evenly spread" case).
38
- - The same 20 decision points nested five deep inside each other is high *cognitive* complexity: the reader must hold all five levels in mind simultaneously, which is a much higher-entropy (harder to predict, easier to break) arrangement of the same energy.
39
-
40
- This is why the extension tracks cyclomatic and cognitive complexity as separate metrics rather than one score: they capture the *energy* and its *arrangement* respectively. Guard clauses, extracted functions, and early returns don't necessarily remove energy from a codebase; they redistribute it into a lower-entropy arrangement, the code equivalent of letting a hot and cold object equilibrate: same total energy, fewer surprising configurations, easier to hold a correct mental model of.
41
-
42
- Entropy here also depends on the observer, not just the code. A function's energy is fixed by what's written, but its entropy, the number of arrangements consistent with what someone currently knows, can grow over time even if the code never changes: the original author forgets the reasoning, or a new developer inherits the file with no context. This detector only measures the static, code-side half of that (the energy and its arrangement); the knowledge-decay half is a reason to keep energy low in the first place, since low-entropy code is cheaper to relearn from scratch.
43
-
44
- ## Cyclomatic Complexity
45
-
46
- Counts the number of independent paths through a function. Starting from a base of **1**, every decision point adds **+1**, regardless of how deeply it's nested:
47
-
48
- - `if` / `elif` / `while` / `for` / `except`
49
- - `and` / `or`
50
- - ternary (`a if cond else b`)
51
-
52
- Two functions with the same number of `if`s score the same, whether those `if`s are sequential or nested five deep — it measures *how many paths exist*, not how hard the code is to follow.
53
-
54
- ## Cognitive Complexity
55
-
56
- Modeled on [SonarSource's metric](https://www.sonarsource.com/resources/cognitive-complexity/): it measures how hard a function is to *read*, so nesting is penalized and straight-line control flow isn't.
57
-
58
- - Each decision point (`if`, `elif`, `for`, `while`, `except`, ternary, nested `def`/`lambda`) adds **1 + current nesting depth**.
59
- - `else` adds a flat **+1** — no nesting penalty, since it doesn't add a new branch to reason about.
60
- - Nesting depth only increases when descending into a block body, so an `if` inside two other `if`s scores higher than three sequential `if`s at the top level, even though both have the same cyclomatic complexity.
61
- - Chained boolean operators of the same kind (`a and b and c`) count as a **single** increment rather than one per operator; mixing `and`/`or` starts a new increment.
62
-
63
- This project's implementation is a simplified first pass on the SonarSource spec: `for`/`while` `else` clauses are scored like `if`/`else`, boolean-chain merging only looks at the immediate parent operator, and recursive calls aren't specially detected.
64
-
65
- ### Interpreting the Scores
66
-
67
- A raw number like "34" doesn't mean much on its own. For cyclomatic complexity, McCabe's original 1976 paper proposed risk bands that are still the closest thing to an industry consensus (echoed by SonarQube, ESLint's `complexity` rule, and NIST guidance):
68
-
69
- | Score | Risk | Roughly |
70
- | --- | --- | --- |
71
- | 1–10 | Low | Simple, easy to test exhaustively |
72
- | 11–20 | Moderate | Getting harder to cover with tests |
73
- | 21–50 | High | Complex, testing all paths is impractical |
74
- | 50+ | Very high | Effectively untestable |
75
-
76
- Cognitive complexity has no equivalent formal consensus, since it's a newer, vendor-originated metric, but SonarSource's own convention (and this extension's defaults) treat **15** as the point where a function is hard enough to hold in your head that it's worth splitting up, with scores past 25 or so being seriously hard to follow regardless of how testable the underlying paths are.
77
-
78
- The two scores can diverge on the same function: a flat function with many independent branches can have high cyclomatic complexity but modest cognitive complexity (easy to read, hard to test exhaustively), while deeply nested code can be the reverse. See [Energy and Entropy](#energy-and-entropy) above for why this extension tracks them separately rather than collapsing them into one score.
32
+ The name is a deliberate analogy to thermodynamics: a function's "energy" is its complexity, nesting, and parameter count, while its "entropy" is how many ways a reader can misunderstand it or a change can silently break it. See [docs/energy-and-entropy.md](docs/energy-and-entropy.md) for the full explanation of why cyclomatic and cognitive complexity are tracked as separate metrics rather than one score.
79
33
 
80
34
  ## Command-Line Usage
81
35
 
82
- The same detectors also run headlessly, without VS Code useful for CI or for an AI coding agent that wants to check the complexity of code it just generated and keep refactoring until it's clean. Published to npm, so no clone or install step is required:
36
+ The same detectors also run headlessly, without VS Code, useful for CI or for an AI coding agent that wants to check the complexity of code it just generated and keep refactoring until it's clean:
83
37
 
84
38
  ```bash
85
39
  npx energy-state-analyzer path/to/file.py # or .fs / .fsx / .ts
86
40
  ```
87
41
 
88
- Or install it as a project/global dependency and call it directly:
89
-
90
- ```bash
91
- npm install --save-dev energy-state-analyzer
92
- npx energy-state-analyzer path/to/file.py
93
- ```
94
-
95
- It prints violations as JSON to stdout and exits `1` if any medium/high-severity violation was found (`0` otherwise), so it can gate a loop:
96
-
97
- ```bash
98
- npx energy-state-analyzer path/to/file.py \
99
- --medium-cyclomatic 8 --high-cyclomatic 12 \
100
- --medium-cognitive 12 --high-cognitive 20
101
- ```
102
-
103
- All threshold flags are optional: `--medium-nesting`, `--high-nesting`, `--medium-cyclomatic`, `--high-cyclomatic`, `--medium-cognitive`, `--high-cognitive`.
104
-
105
- ### Scanning a repo or subtree
106
-
107
- Pass more than one path, a directory, or a `dir/**/*.ext`-style pattern to scan every supported file underneath it (skipping `node_modules`, `.git`, `dist`, `out`, `build`, `.next`, `coverage`, `.vscode-test`) and get an aggregated report instead of a single file's violations:
108
-
109
- ```bash
110
- npx energy-state-analyzer src --report md
111
- ```
112
-
113
- ```
114
- # Energy State Report
115
-
116
- **3 files scanned** — 2 clean, 1 with violations
117
-
118
- | File | Score | High | Medium | Low |
119
- | --- | --- | --- | --- | --- |
120
- | src/foo.py | 13 | 1 | 1 | 0 |
121
- | src/bar.ts | 0 | 0 | 0 | 0 |
122
- | src/baz.fs | 0 | 0 | 0 | 0 |
123
-
124
- **Total score: 13** (1 high, 1 medium, 0 low)
125
- ```
126
-
127
- `--report json` prints the same data as a structured `{ files, totalScore, totalCounts }` object instead. The per-file **score** is a simple heuristic — `1×low + 4×medium + 9×high` violation counts — meant for spotting hotspots and tracking direction over time, not a certified complexity metric.
128
-
129
- Only one glob shape is supported: a trailing `**/*.ext` pattern on an otherwise literal directory prefix (e.g. `src/**/*.py`). There's no brace expansion, negation, or mid-path wildcards — pass explicit directories/files for anything more complex.
130
-
131
- #### A report for humans: `--report human`
132
-
133
- `--report md`/`--report json` are compact, built for scripts and PR comments. `--report human` produces a longer, prose-and-tables report meant to be read by a person auditing a repo or subtree: a section per flagged file, each with its findings translated into plain language, followed by a repo-wide "Total evaluation":
134
-
135
- ```bash
136
- npx energy-state-analyzer src --report human
137
- ```
138
-
139
- ```
140
- # Energy State Report
141
-
142
- ## Score legend
143
-
144
- | Score | Risk | Roughly | Cyclomatic/cognitive complexity |
145
- | --- | --- | --- | --- |
146
- | 0.0 | None | No violations found | — |
147
- | 0.1–3.9 | Low | Simple, easy to test exhaustively | 1–10 |
148
- | 4.0–6.9 | Medium | Getting harder to cover with tests | 11–20 |
149
- | 7.0–8.9 | High | Complex, testing all paths is impractical | 21–50 |
150
- | 9.0–10.0 | Critical | Effectively untestable | 50+ |
151
-
152
- **25 files scanned** — 8 clean, 17 flagged
153
-
154
- ## src/foo.py — High (score 7.8)
155
-
156
- - **Cyclomatic complexity**: 1 function scores 34 — score 7.8 (High): complex, testing all paths is impractical.
157
- - **Primitive obsession**: 2 findings (2 medium) — adjacent same-typed values a caller could silently swap without the compiler noticing.
158
-
159
- ...
160
-
161
- ## Total evaluation
162
-
163
- **Repo score: 7.8 (High)** — driven by the worst file in the scan, `src/foo.py` (complex, testing all paths is impractical).
164
-
165
- | Risk | Files |
166
- | --- | --- |
167
- | None | 8 |
168
- | Low | 12 |
169
- | Medium | 3 |
170
- | High | 2 |
171
- | Critical | 0 |
172
-
173
- **51 total findings** (1 high, 25 medium, 25 low) — breadth of issues across the scan, independent of peak severity.
174
- ```
175
-
176
- Risk is reported on a 0.0–10.0 complexity score, sorted into the same None/Low/Medium/High/Critical levels used elsewhere in this tool, rather than a bespoke label set. The score is a direct re-expression of the McCabe risk table above: cyclomatic/cognitive complexity numbers are converted onto it by linear interpolation anchored at the same 10/20/50 breakpoints (see [Interpreting the Scores](#interpreting-the-scores)), so "High" here means the same thing it always has in this project, just expressed as a single number. Every other detector reports a finding count and severity instead, since it flags a pattern rather than a path count — a file with only non-complexity findings gets a fixed score from its worst one (Low 2.0 / Medium 5.0 / High 7.5), which can never reach Critical (Critical is reserved for genuinely extreme complexity).
177
-
178
- Both the per-file score and the repo-wide "Repo score" are the **maximum** found, not an average. Averaging a file's (or a repo's) scores lets one severely complex function or file hide behind many trivial ones — nine functions at complexity 2 and one at 60 average to about 8 (which itself would still misleadingly read as "Low"), hiding exactly the function most worth fixing. Total finding counts are reported separately as a breadth indicator, deliberately not folded into the same number. Flagged files are listed worst-first.
179
-
180
- ### Diffing a PR against a base branch
181
-
182
- `--base-ref <ref>` compares the current working tree against a git ref, so a GitHub Actions job can report whether a PR increased or decreased complexity relative to its base branch:
183
-
184
- ```bash
185
- npx energy-state-analyzer --base-ref origin/main --report md
186
- ```
187
-
188
- With no path arguments, changed files are discovered via `git diff --name-only <ref>...HEAD`; pass explicit paths to override that. Each changed file's pre-PR content is read with `git show <ref>:<path>` and re-analyzed in memory — a file that doesn't exist at the base ref (new file, or a rename `git diff` didn't resolve) is reported as `new` rather than erroring out.
189
-
190
- ```
191
- # Energy State Diff vs `origin/main`
192
-
193
- | File | Base | Head | Δ | Status |
194
- | --- | --- | --- | --- | --- |
195
- | src/foo.py | 4 | 13 | +9 | 🔴 worsened |
196
- | src/bar.ts | 9 | 0 | -9 | 🟢 improved |
197
- | src/new.py | — | 5 | — | 🆕 new |
198
-
199
- _2 files changed, 1 worsened, 1 improved, 1 new._
200
- ```
201
-
202
- The exit code in every mode (single-file, scan, or diff) follows the same rule: `1` if any medium/high-severity violation exists in the current (head) code, `0` otherwise — whether a diff made things better or worse is visible in the report, not encoded as a separate exit code. `energy-state-cli <single-file>` with no other flags keeps its original behavior (flat JSON violation array, same exit rule) unchanged.
42
+ See [docs/cli.md](docs/cli.md) for scanning a whole repo, aggregated markdown/JSON/human reports, and diffing a PR against a base branch.
203
43
 
204
44
  ## Requirements
205
45
 
206
- The extension activates automatically when you open a Python, F#, or TypeScript file; it bundles its own grammars for parsing (via `web-tree-sitter`), so no external tools are required. F# files only get a `fsharp` language ID (and so trigger analysis) if you have an F# language extension installed (e.g. [Ionide](https://ionide.io/)) VS Code otherwise treats `.fs` files as plain text.
46
+ The extension activates automatically when you open a Python, F#, or TypeScript file; it bundles its own grammars for parsing (via `web-tree-sitter`), so no external tools are required. F# files only get a `fsharp` language ID (and so trigger analysis) if you have an F# language extension installed (e.g. [Ionide](https://ionide.io/)), VS Code otherwise treats `.fs` files as plain text.
207
47
 
208
48
  ## Extension Settings
209
49
 
210
- Detector thresholds are configurable under **Settings → Energy State Analyzer**:
211
-
212
- - `energyStateAnalyzer.cyclomaticComplexity.mediumThreshold` / `.highThreshold`
213
- - `energyStateAnalyzer.cognitiveComplexity.mediumThreshold` / `.highThreshold`
214
- - `energyStateAnalyzer.coherence.largeFunctionLines` — line count above which a function counts as "large" (default `20`).
215
- - `energyStateAnalyzer.coherence.maxLargeFunctions` — number of large functions a file can contain before it's flagged (default `5`).
216
- - `energyStateAnalyzer.coherence.singleDomainNameShare` — share (0-1) of a file's functions that must share a leading name word (e.g. `extractFoo`/`extractBar`) to be treated as one coherent domain, skipping the function-count sprawl check (default `0.7`).
217
- - `energyStateAnalyzer.matchOpportunity.minBranches` — number of branches an if/elif chain must have, all keyed on the same variable, before it's flagged as a match/switch opportunity (default `3`).
218
- - `energyStateAnalyzer.magicNumber.enabled` — whether to flag magic numbers (default `true`).
219
- - `energyStateAnalyzer.magicNumber.allowlist` — numeric literals that are never flagged, regardless of context (default `[0, 1, -1, 2]`).
220
- - `energyStateAnalyzer.magicString.enabled` — whether to flag magic strings (default `true`).
221
- - `energyStateAnalyzer.magicString.minDuplicates` — number of times the same string literal must recur at a decision point before it's flagged (default `2`).
222
- - `energyStateAnalyzer.magicString.allowlist` — string literals that are never flagged, regardless of context (default `["", "utf-8", "__main__"]`).
223
- - `energyStateAnalyzer.colors.highEnergy` / `.mediumEnergy` / `.lowEnergy` — hex colors for the high/medium/low severity background tint and gutter icon (defaults `#fb8500` orange, `#ffb703` gold, `#99dd99` green).
224
- - `energyStateAnalyzer.colors.backgroundOpacity` — opacity of the severity background tint (default `0.1`).
50
+ Detector thresholds are configurable under **Settings → Energy State Analyzer**. See each detector's doc (linked under Features above) for what a setting does; the keys and defaults are:
51
+
52
+ - `energyStateAnalyzer.cyclomaticComplexity.mediumThreshold` / `.highThreshold` (`10` / `15`)
53
+ - `energyStateAnalyzer.cognitiveComplexity.mediumThreshold` / `.highThreshold` (`15` / `25`)
54
+ - `energyStateAnalyzer.coherence.largeFunctionLines` (`20`)
55
+ - `energyStateAnalyzer.coherence.maxLargeFunctions` (`5`)
56
+ - `energyStateAnalyzer.coherence.singleDomainNameShare` (`0.7`)
57
+ - `energyStateAnalyzer.matchOpportunity.minBranches` (`3`)
58
+ - `energyStateAnalyzer.magicNumber.enabled` (`true`)
59
+ - `energyStateAnalyzer.magicNumber.allowlist` (`[0, 1, -1, 2]`)
60
+ - `energyStateAnalyzer.magicString.enabled` (`true`)
61
+ - `energyStateAnalyzer.magicString.minDuplicates` (`2`)
62
+ - `energyStateAnalyzer.magicString.allowlist` (`["", "utf-8", "__main__"]`)
63
+ - `energyStateAnalyzer.colors.highEnergy` / `.mediumEnergy` / `.lowEnergy` (`#fb8500` / `#ffb703` / `#99dd99`)
64
+ - `energyStateAnalyzer.colors.backgroundOpacity` (`0.1`)
225
65
 
226
66
  Changes take effect immediately on the active editor.
227
67
 
68
+ To exclude files/folders (e.g. test fixtures, generated code) from both the extension's live analysis and the CLI, add a `.esaignore` file to your workspace root — see [`docs/cli.md`](docs/cli.md#excluding-files-and-folders-esaignore).
69
+
228
70
  ## Commands
229
71
 
230
- - **Energy State Analyzer: Analyze Energy State** (`energy-state-analyzer.analyze`) manually re-run analysis on the active editor.
72
+ - **Energy State Analyzer: Analyze Energy State** (`energy-state-analyzer.analyze`), manually re-run analysis on the active editor.
231
73
 
232
74
  ## Known Issues
233
75
 
234
- - Nesting depth and parameter count thresholds are not yet configurable only cyclomatic complexity, cognitive complexity, the large-function coherence check, the match-opportunity branch count, and the magic-number/magic-string detectors are.
235
- - The magic-string detector's decision-point scan (equality/membership/dict-key) and its formatted-string exemption are fully implemented for Python and partially for TypeScript (no `.includes()` membership support yet) and F# (no dict/subscript node, no interpolated-string exemption) — see the `LanguageAdapter` fields in `src/core/language.ts` for exactly what's modeled per language.
236
- - The magic-string detector doesn't (yet) special-case enum-like keyword/default arguments (e.g. `mode="fast"`) as a lower-confidence decision point — only equality, membership, and dict/index-key positions count.
237
- - The inversion-opportunities detector only fires for Python and TypeScript; F#'s grammar has no block-boundary node to anchor that heuristic on (see Architecture).
238
- - TypeScript arrow functions aren't analyzed by complexity/parameter-count/coherence (same limitation Python already has for `lambda`) — only named `function` declarations and class methods are.
239
- - The primitive-obsession detector's `in (a, b, c)`-style membership check only runs on Python; F#'s grammar has no direct equivalent, and TypeScript's idiom (`[...].includes(x)`) is a call expression rather than a comparison node.
76
+ - Nesting depth and parameter count thresholds are not yet configurable via VS Code settings, only cyclomatic complexity, cognitive complexity, the large-function coherence check, the match-opportunity branch count, and the magic-number/magic-string detectors are.
77
+ - TypeScript arrow functions aren't analyzed by complexity/parameter-count/coherence (same limitation Python already has for `lambda`), only named `function` declarations and class methods are.
78
+ - Several detectors have per-language gaps beyond the above, see the "Known limitations" section of the relevant [detector doc](docs/detectors/README.md).