energy-state-analyzer 0.2.0 β†’ 0.3.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.
@@ -0,0 +1,4 @@
1
+ {
2
+ "default": true,
3
+ "MD013": false
4
+ }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- last_commit_released: b3dda28dffdd3dc8c6cee2d5b07d5a99d50761cb
2
+ last_commit_released: 489d195ffe125b4e3ebcb9905f561f2d94daf1ad
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,15 @@ 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.3.0 - 2026-08-25
15
+
16
+ ### πŸš€ Features
17
+
18
+ * Add Kotlin support and lazy-load language grammars (#21) ([3a1078e](https://github.com/cardamomcode/energy-state-analyzer/commit/3a1078ecac31ea1574635cb01f794e767b098183))
19
+ * Repo scanning, PR baseline diffing, and a complexity-scored human report (#20) ([489d195](https://github.com/cardamomcode/energy-state-analyzer/commit/489d195ffe125b4e3ebcb9905f561f2d94daf1ad))
20
+
21
+ <strong><small>[View changes on Github](https://github.com/cardamomcode/energy-state-analyzer/compare/b3dda28dffdd3dc8c6cee2d5b07d5a99d50761cb..489d195ffe125b4e3ebcb9905f561f2d94daf1ad)</small></strong>
22
+
14
23
  ## 0.2.0 - 2026-08-25
15
24
 
16
25
  ### πŸš€ Features
package/README.md CHANGED
@@ -102,6 +102,105 @@ npx energy-state-analyzer path/to/file.py \
102
102
 
103
103
  All threshold flags are optional: `--medium-nesting`, `--high-nesting`, `--medium-cyclomatic`, `--high-cyclomatic`, `--medium-cognitive`, `--high-cognitive`.
104
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.
203
+
105
204
  ## Requirements
106
205
 
107
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.
package/action.yml ADDED
@@ -0,0 +1,87 @@
1
+ name: 'Energy State Report'
2
+ description: 'Run energy-state-analyzer over a path or PR diff and post a Markdown report as a sticky PR comment.'
3
+ author: 'dbrattli'
4
+ branding:
5
+ icon: 'zap'
6
+ color: 'yellow'
7
+
8
+ # Requires the caller's checkout step to use `fetch-depth: 0` when base-ref is
9
+ # set, so both the head and base commits are present locally for `git diff`/
10
+ # `git show` (see src/cliModes.ts's changedFilesFromGit/readAtRef).
11
+
12
+ inputs:
13
+ path:
14
+ description: 'Path(s) to scan, space-separated. Ignored in diff mode (base-ref set), which only scans changed files.'
15
+ required: false
16
+ default: '.'
17
+ base-ref:
18
+ description: 'Git ref to diff the current HEAD against (e.g. origin/main). When set, runs in PR-diff mode instead of a full scan.'
19
+ required: false
20
+ default: ''
21
+ report-format:
22
+ description: 'Report format: md, json, or human. Diff mode (base-ref set) only supports md/json.'
23
+ required: false
24
+ default: 'md'
25
+ version:
26
+ description: 'energy-state-analyzer npm version or dist-tag to run via npx (e.g. 0.2.0, latest).'
27
+ required: false
28
+ default: 'latest'
29
+ fail-on-regression:
30
+ description: 'Fail the step when the analyzer reports a blocking result (scan mode: any medium/high violation; diff mode: any worsened file).'
31
+ required: false
32
+ default: 'true'
33
+ post-comment:
34
+ description: 'Post the report as a sticky PR comment. No-ops outside pull_request events.'
35
+ required: false
36
+ default: 'true'
37
+ comment-header:
38
+ description: 'Header used to identify and update the sticky PR comment across pushes.'
39
+ required: false
40
+ default: 'energy-state-report'
41
+
42
+ outputs:
43
+ report-path:
44
+ description: 'Path to the generated report file.'
45
+ value: ${{ steps.run.outputs.report-path }}
46
+ exit-code:
47
+ description: 'Exit code returned by energy-state-analyzer (0 = clean, 1 = blocking violations/regressions).'
48
+ value: ${{ steps.run.outputs.exit-code }}
49
+
50
+ runs:
51
+ using: composite
52
+ steps:
53
+ - name: Setup Node
54
+ uses: actions/setup-node@v5
55
+ with:
56
+ node-version: 22
57
+
58
+ - name: Run energy-state-analyzer
59
+ id: run
60
+ shell: bash
61
+ run: |
62
+ set +e
63
+ if [ -n "${{ inputs.base-ref }}" ]; then
64
+ npx --yes "energy-state-analyzer@${{ inputs.version }}" --base-ref "${{ inputs.base-ref }}" --report "${{ inputs.report-format }}" > energy-state-report.md
65
+ else
66
+ npx --yes "energy-state-analyzer@${{ inputs.version }}" ${{ inputs.path }} --report "${{ inputs.report-format }}" > energy-state-report.md
67
+ fi
68
+ code=$?
69
+ echo "exit-code=$code" >> "$GITHUB_OUTPUT"
70
+ echo "report-path=energy-state-report.md" >> "$GITHUB_OUTPUT"
71
+ # decision: always exit 0 here regardless of the analyzer's own exit code β€” a blocking
72
+ # result must still reach the comment step below (marocchino/sticky-pull-request-comment
73
+ # needs the report file to exist), so failure is deferred to the dedicated step at the
74
+ # end instead of short-circuiting the rest of this composite action.
75
+ exit 0
76
+
77
+ - name: Comment on PR
78
+ if: ${{ inputs.post-comment == 'true' && github.event_name == 'pull_request' }}
79
+ uses: marocchino/sticky-pull-request-comment@v2
80
+ with:
81
+ header: ${{ inputs.comment-header }}
82
+ path: ${{ steps.run.outputs.report-path }}
83
+
84
+ - name: Fail on regression
85
+ if: ${{ inputs.fail-on-regression == 'true' && steps.run.outputs.exit-code != '0' }}
86
+ shell: bash
87
+ run: exit ${{ steps.run.outputs.exit-code }}