@hublo/sentinel 1.5.0-alpha.9 → 1.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.
Files changed (41) hide show
  1. package/README.md +9 -7
  2. package/dist/bin/sentinel.js +61 -16
  3. package/dist/{chunk-KLEXJFJ6.js → chunk-43HQ6BVJ.js} +61 -15
  4. package/dist/{chunk-YQRDKWSH.js → chunk-IL5PPLXE.js} +11 -1
  5. package/dist/chunk-IL5PPLXE.js.map +1 -0
  6. package/dist/chunk-MOELAQPC.js +10365 -0
  7. package/dist/chunk-WBDBRGLV.js +214 -0
  8. package/dist/chunk-WBDBRGLV.js.map +1 -0
  9. package/dist/{chunk-AMCGMLPS.js → chunk-Y57KA4QS.js} +2 -2
  10. package/dist/index.d.ts +28 -1
  11. package/dist/index.js +4 -4
  12. package/dist/roles/build/nest/toolchain.js +68 -18
  13. package/dist/roles/build/nest/toolchain.js.map +1 -1
  14. package/dist/roles/static-analysis/config.d.ts +15 -1
  15. package/dist/roles/static-analysis/config.js +1 -1
  16. package/dist/roles/test/setup/jest-parity.js +2 -2
  17. package/dist/roles/test/setup/workspace-entry.js +2 -2
  18. package/dist/{validate-AWWMBSH5.js → validate-FRMENKHM.js} +2 -2
  19. package/docs/bundle-analysis-spec.md +272 -0
  20. package/docs/sql-verification-study.md +401 -0
  21. package/docs/static-analysis-adoption.md +285 -0
  22. package/docs/static-analysis-study.md +824 -18
  23. package/docs/typescript-adoption.md +1 -1
  24. package/docs/typescript-traces.md +27 -12
  25. package/docs/using-sentinel.md +22 -12
  26. package/docs/validating-a-change.md +185 -0
  27. package/lint/nest.json +98 -18
  28. package/lint/node.json +83 -3
  29. package/lint/react-lib.json +121 -31
  30. package/lint/react.json +101 -2
  31. package/package.json +27 -6
  32. package/plugins/hublo/no-palette-common.js +76 -0
  33. package/plugins/hublo/no-regex-lookbehind.js +70 -0
  34. package/plugins/hublo/require-reload-document.js +70 -0
  35. package/plugins/hublo.js +20 -145
  36. package/plugins/sonar-cognitive.js +108 -0
  37. package/dist/chunk-EDFIVLUA.js +0 -100
  38. package/dist/chunk-EDFIVLUA.js.map +0 -1
  39. package/dist/chunk-OZZICSSZ.js +0 -6469
  40. package/dist/chunk-YQRDKWSH.js.map +0 -1
  41. /package/dist/{chunk-AMCGMLPS.js.map → chunk-Y57KA4QS.js.map} +0 -0
@@ -0,0 +1,285 @@
1
+ # Static analysis adoption cheat sheet
2
+
3
+ A quick reference for adopting the sentinel static analysis role on a module, and for reading what it prints. The reasoning behind every number, and the measurements that fixed the thresholds, are in [`static-analysis-study.md`](static-analysis-study.md); this page is what you need to run it.
4
+
5
+ **It measures, it never refuses.** Nothing here fails a build or blocks a merge. A figure answers "is this module drifting", which is a question about a trend, and a check that blocks a merge on a trend blocks the wrong thing. Refusing a single unit is the linter's job and oxlint already does it.
6
+
7
+ ## Adopt a module in two steps
8
+
9
+ Run from the **module's own directory**. You do not need to install sentinel first: use `npx --yes` for a one-off.
10
+
11
+ ```bash
12
+ # 1. write the module's config + the pinned devDependency
13
+ cd apps/.../<module>
14
+ npx --yes @hublo/sentinel@<version> --init --static-analysis
15
+
16
+ # 2. fetch what step 1 declared
17
+ pnpm install
18
+ ```
19
+
20
+ `--init` writes `static-analysis.config.mjs`, adds the three scripts below to the module's `package.json` along with its nx target, pins `@hublo/sentinel` into its own `devDependencies`, and ignores the report directory in the repository's root `.gitignore`. It prints what it did:
21
+
22
+ ```
23
+ wrote static-analysis.config.mjs
24
+ wrote package.json
25
+ added the three `static-analysis` scripts and static-analysis.config.mjs; declare
26
+ this module's entry points there, everything else comes from the role
27
+ ignored static-analysis-report/ in the root .gitignore
28
+ run `pnpm install` to apply the workspace changes
29
+ ```
30
+
31
+ ⚠️ **If your module has a `project.json` and no `package.json`, `--init` creates one.** Measured on
32
+ this repository, **49 of 243** nx projects are in that case, and none of them has adopted any
33
+ sentinel role yet, which is the same fact seen from the other side. The consequence is worth
34
+ knowing before you read the diff: the pnpm workspace globs are `apps/**` and `libs/**`, so a new
35
+ `package.json` there makes the directory a workspace package and the lockfile gains an importer for
36
+ it. Verified: `pnpm install` stays green, and nx keeps reading the project, now from two files.
37
+
38
+ This is not specific to this role. `--init --format` on the same module prints `create
39
+ package.json` too: it is the shape of every sentinel adoption, because a role declares its nx
40
+ target in `package.json` and nowhere else.
41
+
42
+ **Nothing else is required.** The config stub is written with an empty `entry` on purpose: for most modules the figures are already right without declaring anything, and the stub exists so you can correct the one case where they are not (see [Declaring entry points](#declaring-entry-points)).
43
+
44
+ ## Verbs at a glance
45
+
46
+ | Verb | Who / when | Runs the tools? | What you get |
47
+ | ----------- | ------------------ | ----------------- | --------------------------------------------------------------------- |
48
+ | `--run` | dev, everyday | **yes** | the figures, in one of four shapes. Never exits non-zero on a finding |
49
+ | `--inspect` | dev, everyday | no (reads config) | the resolved settings, and whether this module has adopted |
50
+ | `--init` | dev, once to adopt | writes files | the config stub, the three scripts, the pinned dep |
51
+ | `--migrate` | planned | not available yet | reserved for changing an already-initialized setup |
52
+
53
+ **Location** (any verb): from a module dir → that module; from the root → `--module <name>`, `--ci` (affected), or every module. `--init` writes, so it targets one module only.
54
+
55
+ ⚠️ **From the workspace root, `pnpm exec sentinel` does not resolve.** sentinel is a devDependency of each adopting MODULE, not of the root, so the root's `.bin` has no `sentinel` and you get `Command "sentinel" not found`. Three things do work:
56
+
57
+ ```bash
58
+ pnpm --filter <module> exec sentinel --run --static-analysis # the module becomes the cwd
59
+ pnpm --filter <module> run static-analysis # its own script, same thing
60
+ npx @hublo/sentinel --run --static-analysis --module <module> # when nothing is installed
61
+ ```
62
+
63
+ `--module` is for the third form. With the first two the cwd is already the module, so it is redundant.
64
+
65
+ ## Everyday use
66
+
67
+ ```bash
68
+ pnpm run static-analysis # the summary, in your terminal. Writes nothing
69
+ pnpm run static-analysis:json # static-analysis-report/static-analysis.json, for a machine
70
+ pnpm run static-analysis:html # static-analysis-report/static-analysis.html, for a reader
71
+ ```
72
+
73
+ Or without the scripts:
74
+
75
+ ```bash
76
+ sentinel --run --static-analysis # same as the first script
77
+ sentinel --run --static-analysis --output html # same as the third
78
+ sentinel --run --static-analysis --output markdown # static-analysis.md, the pull-request recap
79
+ sentinel --inspect --static-analysis # the settings, without running anything
80
+ ```
81
+
82
+ **Which shape to use.** `console` is the one you run in a loop: it writes nothing and skips the expensive drawing. `json` is for CI and for a script. `html` is the one you open and send: it carries every place with **the code behind it** and an explorable import graph. `markdown` is six lines for a pull-request comment, and it has no npm script on purpose: nobody runs it by hand.
83
+
84
+ ⚠️ The `html` shape pays for a graph layout that the other three skip, 5 seconds on the largest module measured against 14 ms for the rest of the graph pass. That is fine for a page somebody opens and wrong for a commit hook. `html` and `json` also open the source files behind the places they list, 201 of them on the largest front measured; `console` and `markdown` print no code and open none.
85
+
86
+ ## Reading the report
87
+
88
+ The console summary of a nest microservice, in full:
89
+
90
+ ```
91
+ sentinel static-analysis: contract
92
+ Duplicated lines 3.95 %
93
+ Clones 61 entries
94
+ Duplicated lines (tests) 10.93 %
95
+ Clones (tests) 196 entries
96
+ Entry points 46 of 656 files
97
+ Unreachable lines 1.07 %
98
+ Unreachable files 5 files
99
+ Unused exports 1 exports
100
+ Unused symbols 1 symbols
101
+ Unused imports 0 imports
102
+ Complexity, worst function 27 max per function
103
+ Complexity 11-20, moderate risk 18 of 1386 functions
104
+ Complexity 21-50, high risk 1 of 1386 functions
105
+ Imports between files 1135 between 495 files
106
+ Density 0.0046 of the possible edges
107
+ Import cycles 3 groups, 8 files
108
+ Communities 12 of 495 files
109
+ Modularity (Q) 0.753
110
+ Files the gauge read 495 files the gauge read
111
+ Files with a complex function 18 files over 10, of 495
112
+ Files with deep nesting 4 files over 3, of 495
113
+ Long files 43 files over 150, of 495
114
+ Files with a long function 5 files over 109, of 495
115
+ Files with a long function body 47 files over 10, of 495
116
+ Files with a wide signature 33 files over 3, of 495
117
+ Files with several classes 16 files over 1, of 495
118
+ Files hard to follow 23 files over 7, of 495
119
+ ```
120
+
121
+ Four groups, and they answer different questions:
122
+
123
+ | | what it answers |
124
+ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
125
+ | duplication | how much of this module repeats itself, product and tests counted apart |
126
+ | dead code | what nothing reaches: files, exports, symbols, imports |
127
+ | complexity | two measures, on purpose: how hard the hardest functions are to TEST (McCabe) and how hard they are to FOLLOW (cognitive). They disagree on 60% of the files either one flags |
128
+ | structure and size | how the files depend on each other, and which ones are outliers |
129
+
130
+ **Every row carries its denominator**, because none of them compares between modules without one: three breaches over 50 files and three over 3 000 are not the same module.
131
+
132
+ **An absent row means one of two things, and the notes tell you which.** When a pass could not produce a figure, the report says so in its own line under the figures rather than printing `0.00%`, because "no dead code" and "the walk had no entry point" are different facts. When a COUNT is zero, the row is dropped silently and on purpose: the complexity bands and the nine size indicators would otherwise print a column of zeroes on a healthy module, which makes the normal case the loudest part of the report.
133
+
134
+ So: a row missing **with** a note under the figures was not measured; a row missing **without** one was measured and found nothing.
135
+
136
+ ### Three rows people misread
137
+
138
+ **`Unreachable files` is not a delete list.** It means nothing in this module imports them. A file another module's spec imports is alive, and it has its own row, `Used only by tests`. For a library the question cannot even be answered inside the module, so the role leaves it (see below).
139
+
140
+ **`Import cycles` reports GROUPS, not paths.** A group of six files can hold a dozen distinct circles, and breaking one does not necessarily break the group. The group is the unit precisely for that reason.
141
+
142
+ **`Density` must not be compared between modules of different sizes.** Its denominator grows with the square of the file count, so a module gets a lower density by growing rather than by improving. Measured: `libs/nest/auth` 0.0216 at 101 files, `apps/front/host-admin` 0.0005 at 4 380. It is good for one module watched over time.
143
+
144
+ ### Where the size thresholds come from
145
+
146
+ The nine size rows are measured with oxlint at thresholds this role fixes once for every module, so the same question gets the same answer everywhere. Each one is the **strictest of three candidates**: published research, our own p90, and the rule's own default.
147
+
148
+ | row | threshold | what decided it |
149
+ | --------------------------------- | ------------: | ----------------------------------------------------------------------------------------- |
150
+ | `Files with a complex function` | 10 | McCabe 1976 says 10, our p90 says 11 |
151
+ | `Files with a long function` | 109 | our p90. IfSQ SP-1 says 150 and loses for being more permissive than our code |
152
+ | `Long files` | 150 | our p90 of 157, rounded down. No research sets a file's length |
153
+ | `Files with deep nesting` | 3 | IfSQ SP-2, and our p90 agrees |
154
+ | `Files with nested callbacks` | 3 | same nesting family |
155
+ | `Files with a long function body` | 10 statements | the rule's default, already the strictest candidate |
156
+ | `Files with a wide signature` | 3 parameters | beyond three the fix is an object or currying, so this is a design boundary |
157
+ | `Files with several classes` | 1 | the rule's floor |
158
+ | `Files hard to follow` | 7 | Sonar publishes 15 for cognitive complexity and says no formula supports it; our p90 is 7 |
159
+
160
+ ⚠️ **These will disagree with your green lint, out loud.** The `nest` lint preset refuses `complexity` at 30 while this reports above 10, so a nest developer reads breaches beside a passing build. That is correct: the lint is the line your build refuses at, this is a gauge that answers the same question the same way in every module, including the ones that have not adopted lint at all. The report prints its thresholds under `measured with` so the two are never confused.
161
+
162
+ ## Bundle analysis, for a front application
163
+
164
+ Six rows appear when the module is a front application **that has been built**. They answer how much
165
+ JavaScript a user downloads to open it, and what that costs twice over.
166
+
167
+ | row | what it is |
168
+ | ------------------------- | --------------------------------------------------------------------------------- |
169
+ | initial load, transfer | gzipped, what crosses the network, against a **170 KB** budget and the web median |
170
+ | initial load, to parse | decompressed, what the engine reads, against **717 KB** |
171
+ | initial load, files | how many files the entry pulls |
172
+ | heaviest single asset | the largest file, named, against **100 KB** per script |
173
+ | dynamic layer | what no entry references statically, as a pair of bounds over its group count |
174
+ | referenced by the entries | everything the entries reference, read as code-splitting health |
175
+
176
+ **Why two weights and never one.** Compression is a network lever with no effect on the engine:
177
+ "900 KB of decompressed JavaScript is still 900 KB to the parser and compiler, even though it may be
178
+ roughly 300 KB when compressed." Measured across these fronts the ratio runs from 3.30 to 3.70, so
179
+ neither figure can be read off the other.
180
+
181
+ **Why two references for the transfer weight.** The web median answers _are we normal_, the
182
+ engineering budget answers _are we good_, and they routinely disagree: every front here sits below
183
+ the median and above the budget. Both are true. The budget carries its conditions, and they travel
184
+ with it: getting interactive in under five seconds on a slow 3G connection on a median mobile
185
+ device.
186
+
187
+ Each figure's band is the word beside it: `ok` below 70% of its reference, `near` from 70% to 100%,
188
+ `exceed` above. And each row carries its own lever and its sources in the `html` report, because a
189
+ weight nobody can act on is a weight nobody acts on.
190
+
191
+ ⚠️ **Three conditions are printed with the figures, and they decide what the figures mean**: how old
192
+ the build is, that the brand it was built for is not recorded anywhere in the output, and any path
193
+ the manifest announced that resolved to no file. A measurement on a stale build is silently wrong.
194
+
195
+ ⚠️ **No build, no figures, and the report says so** rather than printing zeros. A module that has
196
+ never been built and a module weighing nothing are different facts. A component library or a
197
+ back-end service produces no entry manifest at all, so it will always read that line: that is the
198
+ perimeter, not a defect.
199
+
200
+ **You also get a recap on the pull request**, for the fronts it touched, with each band. It is
201
+ posted on every run rather than only on a breach, because a comment that appears only when
202
+ something is wrong is a comment nobody learns to read, and it updates the same comment instead of
203
+ adding a second one.
204
+
205
+ ⛔ **It measures, it never refuses.** Nothing here fails a build, including that comment. Blocking
206
+ is a later choice, and the thing worth blocking on is the **delta** against the default branch
207
+ rather than the level: that needs CI to keep a baseline, so it comes with the baseline.
208
+
209
+ ## The `html` report
210
+
211
+ Written to `static-analysis-report/`, two files:
212
+
213
+ - `static-analysis.html`, self-contained: no CDN, no library, no font, nothing fetched. Open it from disk, attach it to a ticket, serve it from a CI artifact.
214
+ - `static-analysis-graph.svg`, the import graph as a plain image, for when you want to paste a picture.
215
+
216
+ The page carries what the console summary leaves out, and it is laid out the way a scanner is: **one card per figure**, grouped into tabs by family, each carrying its own help and its own places. A place is a card too: the path with its line, the measure in its own unit, the rule it broke, and **the code**, collapsed.
217
+
218
+ The code is read from your source files for every place the page lists, not just for clones: a clone comes with its fragment, while a size or complexity finding is a file and a line, so the page opens the file. Which lines it shows depends on what the rule points at, which was measured rather than assumed: a rule naming a declaration shows ten lines from it, `max-depth` and `max-nested-callbacks` show the three lines above too because the nesting they report is there, and `max-lines` shows the file's HEAD because it reports at the file's last line. A dead export with no line is located by its name when exactly one line declares it, and shows nothing rather than a guess otherwise.
219
+
220
+ The **import graph** is explorable. Point at a dot for its path, which the browser shows under your cursor; click it to keep only that file and its neighbours, click the background to come back. The drawing renders with scripting off; the script only adds the filtering.
221
+
222
+ ## The pull-request recap
223
+
224
+ `--output markdown` writes `static-analysis-report/static-analysis.md`: one table, one row per family, the headline figure and how many places that family holds. It is what a workflow posts as a comment, one section per affected module.
225
+
226
+ ⛔ **It does not compare with `main`.** It says so on itself, because a number in a pull-request comment is read as a delta by default, and that reading is wrong in the dangerous direction: a module already at 6.81% looks exactly like one this branch pushed there. Comparing needs a baseline kept between runs, which is the next version's work.
227
+
228
+ ⚠️ `static-analysis-report/` is generated, and **`--init` ignores it for you**: it writes one line into your repository's root `.gitignore`, unanchored so it matches at every depth, rather than one line in every adopting module's own file. It checks first, in any spelling, so adopting a second module adds nothing. You have nothing to do and nothing to commit beyond that line.
229
+
230
+ ⛔ **The one case where it is not there**: a repository that runs the role WITHOUT ever adopting it, which is what a CI workflow measuring modules with `npx` does. `--init` never ran, so nothing wrote the rule, and every measured module leaves an untracked directory. If that is your situation, add the line yourself:
231
+
232
+ ```gitignore
233
+ static-analysis-report/
234
+ ```
235
+
236
+ ⚠️ This paragraph said the opposite for one commit, on the strength of that very case: the line was missing from a repository where no module had adopted, and the conclusion drawn was that the documentation lied, without checking what writes it. `--init` does, in `src/core/workspace-prep.ts`. Verify yours with `git check-ignore -v <module>/static-analysis-report/`.
237
+
238
+ ## Declaring entry points
239
+
240
+ `entry` names the files that **run although nothing imports them**. They are where reachability starts, and they are added to the patterns the role already ships rather than replacing them.
241
+
242
+ Most modules need nothing. Declare an entry point when the framework loads a file by convention the role has not met, or when code is loaded by name at run time:
243
+
244
+ ```js
245
+ import { defineStaticAnalysis } from '@hublo/sentinel/static-analysis'
246
+
247
+ export default defineStaticAnalysis({
248
+ root: import.meta.dirname,
249
+ entry: ['src/emails/**'],
250
+ })
251
+ ```
252
+
253
+ **The symptom that tells you one is missing**: a large, tightly-grouped block of unreachable files under one directory. Measured on `libs/cloud/hermes`, 228 of its 231 findings sit under `src/emails`, which holds templates loaded by name rather than imported. That is one line of config, not 228 deletions.
254
+
255
+ ## Two limits worth knowing before you read a number
256
+
257
+ **A single-file component format cannot be parsed.** `.svelte`, `.vue` and `.astro` files are not read, so on a module holding them the role reports the duplication, complexity and size figures and **omits the graph and the dead-code figures entirely**, with the share printed in their place:
258
+
259
+ ```
260
+ (252 product file(s) in .svelte cannot be parsed, 53% of this module: no graph
261
+ and no dead-code figure is reported, because a file the role cannot open still
262
+ imports the files it imports)
263
+ ```
264
+
265
+ Absent with its reason, rather than a figure that is wrong by half.
266
+
267
+ **A library's dead code is decided outside the module.** An application has an entry point; a library has none, so a module-local walk would report "the module does not import itself", which is true of every library. The role builds a workspace-wide index of who imports what, once, and caches it under `node_modules/.cache/sentinel/`. Measured on `libs/nest/auth`: **15** unreachable files with the index against **87** without it.
268
+
269
+ ## Troubleshooting
270
+
271
+ | | |
272
+ | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
273
+ | a wall of unreachable files under one directory | an undeclared entry point, see above |
274
+ | the graph and dead-code rows are missing | the module holds `.svelte`, `.vue` or `.astro`; the note above the figures says the share |
275
+ | `Entry points 0 of N` | nothing matched, so the walk had no start. Declare `entry` |
276
+ | the numbers differ from another module's | check `measured with`: a setting this module moved is marked `(module)`, and two modules measured differently cannot be compared |
277
+ | the run is slow | you are on `--output html`, which pays the graph layout. Use the plain script in a loop |
278
+ | `oxlint could not be run, so no size indicator is reported` | the binary did not resolve; the size rows are absent rather than zero |
279
+
280
+ ## What the role will not do
281
+
282
+ - **It will not fail your build.** No threshold here refuses anything, by design, for as long as adoption is incomplete. The only non-zero exits are an unknown `--output` value and a tool that could not be run at all, never a finding.
283
+ - **It will not read your module's `.oxlintrc.json`.** The size gauge writes its own config to a temporary directory, so its scale is the same in every module. A gauge whose scale moves per module compares nothing.
284
+ - **It will not look outside the module**, except for the one measurement that cannot be answered inside it: a library's reachability.
285
+ - **It will not measure your tests.** Specs, fixtures, mocks, stories and generated files are out of every product figure. Duplication reports the test suite separately, on its own two rows, because that one is worth seeing and must not be blended into the product number.