@hublo/sentinel 1.5.0-alpha.21 → 1.5.0-alpha.23
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/README.md +1 -0
- package/dist/bin/sentinel.js +4 -4
- package/dist/{chunk-KLEXJFJ6.js → chunk-IFK6KGUW.js} +1 -1
- package/dist/{chunk-YQRDKWSH.js → chunk-JEYGPPSL.js} +11 -1
- package/dist/chunk-JEYGPPSL.js.map +1 -0
- package/dist/{chunk-AMCGMLPS.js → chunk-SPES5MQT.js} +2 -2
- package/dist/{chunk-AFM4AJHA.js → chunk-TL2P2X45.js} +284 -46
- package/dist/index.js +3 -3
- package/dist/roles/test/setup/jest-parity.js +2 -2
- package/dist/roles/test/setup/workspace-entry.js +2 -2
- package/dist/{validate-AWWMBSH5.js → validate-7VBODB4M.js} +2 -2
- package/docs/static-analysis-adoption.md +195 -0
- package/docs/static-analysis-study.md +378 -11
- package/docs/using-sentinel.md +3 -2
- package/package.json +2 -1
- package/plugins/sonar-cognitive.js +71 -0
- package/dist/chunk-YQRDKWSH.js.map +0 -1
- /package/dist/{chunk-AMCGMLPS.js.map → chunk-SPES5MQT.js.map} +0 -0
|
@@ -0,0 +1,195 @@
|
|
|
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`, and pins `@hublo/sentinel` into its own `devDependencies`. 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 and journeys there, everything else comes from the role
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Nothing else is required.** The config stub is written with empty `entry` and `journeys` 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)).
|
|
30
|
+
|
|
31
|
+
## Verbs at a glance
|
|
32
|
+
|
|
33
|
+
| Verb | Who / when | Runs the tools? | What you get |
|
|
34
|
+
| ----------- | ------------------ | ----------------- | ---------------------------------------------------------------------- |
|
|
35
|
+
| `--run` | dev, everyday | **yes** | the figures, in one of three shapes. Never exits non-zero on a finding |
|
|
36
|
+
| `--inspect` | dev, everyday | no (reads config) | the resolved settings, and whether this module has adopted |
|
|
37
|
+
| `--init` | dev, once to adopt | writes files | the config stub, the three scripts, the pinned dep |
|
|
38
|
+
| `--migrate` | planned | not available yet | reserved for changing an already-initialized setup |
|
|
39
|
+
|
|
40
|
+
**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.
|
|
41
|
+
|
|
42
|
+
## Everyday use
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pnpm run static-analysis # the summary, in your terminal. Writes nothing
|
|
46
|
+
pnpm run static-analysis:json # static-analysis-report/static-analysis.json, for a machine
|
|
47
|
+
pnpm run static-analysis:html # static-analysis-report/static-analysis.html, for a reader
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or without the scripts:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
sentinel --run --static-analysis # same as the first script
|
|
54
|
+
sentinel --run --static-analysis --output html # same as the third
|
|
55
|
+
sentinel --inspect --static-analysis # the settings, without running anything
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**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 the findings, the places to look, and an explorable import graph.
|
|
59
|
+
|
|
60
|
+
⚠️ The `html` shape pays for a graph layout that the other two 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.
|
|
61
|
+
|
|
62
|
+
## Reading the report
|
|
63
|
+
|
|
64
|
+
The console summary of a nest microservice, in full:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
sentinel static-analysis: contract
|
|
68
|
+
Duplicated lines 3.95 %
|
|
69
|
+
Clones 61 entries
|
|
70
|
+
Duplicated lines (tests) 10.93 %
|
|
71
|
+
Clones (tests) 196 entries
|
|
72
|
+
Entry points 46 of 656 files
|
|
73
|
+
Unreachable lines 1.07 %
|
|
74
|
+
Unreachable files 5 files
|
|
75
|
+
Unused exports 1 exports
|
|
76
|
+
Unused symbols 1 symbols
|
|
77
|
+
Unused imports 0 imports
|
|
78
|
+
Complexity, worst function 27 max per function
|
|
79
|
+
Complexity 11-20, moderate risk 18 of 1386 functions
|
|
80
|
+
Complexity 21-50, high risk 1 of 1386 functions
|
|
81
|
+
Imports between files 1135 between 495 files
|
|
82
|
+
Density 0.0046 of the possible edges
|
|
83
|
+
Import cycles 3 groups, 8 files
|
|
84
|
+
Communities 12 of 495 files
|
|
85
|
+
Modularity (Q) 0.753
|
|
86
|
+
Files the gauge read 495 files the gauge read
|
|
87
|
+
Files with a complex function 18 files over 10, of 495
|
|
88
|
+
Files with deep nesting 4 files over 3, of 495
|
|
89
|
+
Long files 43 files over 150, of 495
|
|
90
|
+
Files with a long function 5 files over 109, of 495
|
|
91
|
+
Files with a long function body 47 files over 10, of 495
|
|
92
|
+
Files with a wide signature 33 files over 3, of 495
|
|
93
|
+
Files with several classes 16 files over 1, of 495
|
|
94
|
+
Files hard to follow 23 files over 7, of 495
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Four groups, and they answer different questions:
|
|
98
|
+
|
|
99
|
+
| | what it answers |
|
|
100
|
+
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
101
|
+
| duplication | how much of this module repeats itself, product and tests counted apart |
|
|
102
|
+
| dead code | what nothing reaches: files, exports, symbols, imports |
|
|
103
|
+
| 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 |
|
|
104
|
+
| structure and size | how the files depend on each other, and which ones are outliers |
|
|
105
|
+
|
|
106
|
+
**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.
|
|
107
|
+
|
|
108
|
+
**A row that is absent was not measured.** It is never a silent zero. When a pass cannot 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.
|
|
109
|
+
|
|
110
|
+
### Three rows people misread
|
|
111
|
+
|
|
112
|
+
**`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).
|
|
113
|
+
|
|
114
|
+
**`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.
|
|
115
|
+
|
|
116
|
+
**`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.
|
|
117
|
+
|
|
118
|
+
### Where the size thresholds come from
|
|
119
|
+
|
|
120
|
+
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.
|
|
121
|
+
|
|
122
|
+
| row | threshold | what decided it |
|
|
123
|
+
| --------------------------------- | ------------: | ----------------------------------------------------------------------------------------- |
|
|
124
|
+
| `Files with a complex function` | 10 | McCabe 1976 says 10, our p90 says 11 |
|
|
125
|
+
| `Files with a long function` | 109 | our p90. IfSQ SP-1 says 150 and loses for being more permissive than our code |
|
|
126
|
+
| `Long files` | 150 | our p90 of 157, rounded down. No research sets a file's length |
|
|
127
|
+
| `Files with deep nesting` | 3 | IfSQ SP-2, and our p90 agrees |
|
|
128
|
+
| `Files with nested callbacks` | 3 | same nesting family |
|
|
129
|
+
| `Files with a long function body` | 10 statements | the rule's default, already the strictest candidate |
|
|
130
|
+
| `Files with a wide signature` | 3 parameters | beyond three the fix is an object or currying, so this is a design boundary |
|
|
131
|
+
| `Files with several classes` | 1 | the rule's floor |
|
|
132
|
+
| `Files hard to follow` | 7 | Sonar publishes 15 for cognitive complexity and says no formula supports it; our p90 is 7 |
|
|
133
|
+
|
|
134
|
+
⚠️ **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.
|
|
135
|
+
|
|
136
|
+
## The `html` report
|
|
137
|
+
|
|
138
|
+
Written to `static-analysis-report/`, two files:
|
|
139
|
+
|
|
140
|
+
- `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.
|
|
141
|
+
- `static-analysis-graph.svg`, the import graph as a plain image, for when you want to paste a picture.
|
|
142
|
+
|
|
143
|
+
The page carries what the console summary leaves out: every finding with its path, the code of each clone, and an **explorable import graph**. Point at a dot for its path and its two degrees, 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 naming and the filtering.
|
|
144
|
+
|
|
145
|
+
⚠️ `static-analysis-report/` is generated. The rule lives in the **root** `.gitignore`, unanchored so it matches at any depth, rather than one line added to every adopting module's own file. You have nothing to do and nothing to commit.
|
|
146
|
+
|
|
147
|
+
## Declaring entry points
|
|
148
|
+
|
|
149
|
+
`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.
|
|
150
|
+
|
|
151
|
+
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:
|
|
152
|
+
|
|
153
|
+
```js
|
|
154
|
+
import { defineStaticAnalysis } from '@hublo/sentinel/static-analysis'
|
|
155
|
+
|
|
156
|
+
export default defineStaticAnalysis({
|
|
157
|
+
root: import.meta.dirname,
|
|
158
|
+
entry: ['src/emails/**'],
|
|
159
|
+
journeys: {},
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**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.
|
|
164
|
+
|
|
165
|
+
## Two limits worth knowing before you read a number
|
|
166
|
+
|
|
167
|
+
**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:
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
(252 product file(s) in .svelte cannot be parsed, 53% of this module: no graph
|
|
171
|
+
and no dead-code figure is reported, because a file the role cannot open still
|
|
172
|
+
imports the files it imports)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Absent with its reason, rather than a figure that is wrong by half.
|
|
176
|
+
|
|
177
|
+
**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.
|
|
178
|
+
|
|
179
|
+
## Troubleshooting
|
|
180
|
+
|
|
181
|
+
| | |
|
|
182
|
+
| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
183
|
+
| a wall of unreachable files under one directory | an undeclared entry point, see above |
|
|
184
|
+
| the graph and dead-code rows are missing | the module holds `.svelte`, `.vue` or `.astro`; the note above the figures says the share |
|
|
185
|
+
| `Entry points 0 of N` | nothing matched, so the walk had no start. Declare `entry` |
|
|
186
|
+
| 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 |
|
|
187
|
+
| the run is slow | you are on `--output html`, which pays the graph layout. Use the plain script in a loop |
|
|
188
|
+
| `oxlint could not be run, so no size indicator is reported` | the binary did not resolve; the size rows are absent rather than zero |
|
|
189
|
+
|
|
190
|
+
## What the role will not do
|
|
191
|
+
|
|
192
|
+
- **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.
|
|
193
|
+
- **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.
|
|
194
|
+
- **It will not look outside the module**, except for the one measurement that cannot be answered inside it: a library's reachability.
|
|
195
|
+
- **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.
|
|
@@ -548,6 +548,13 @@ The related work carries two results that challenge the Complexity category: Her
|
|
|
548
548
|
that most code complexity metrics correlate highly with LOC, and Moser et al. that process
|
|
549
549
|
metrics outperform code metrics as fault predictors.
|
|
550
550
|
|
|
551
|
+
### IfSQ
|
|
552
|
+
|
|
553
|
+
The one standard found that attaches a citation to a numeric limit, and the only external source
|
|
554
|
+
used to fix this role’s own thresholds. It is a human-inspection standard rather than a tool, so its
|
|
555
|
+
table sits with the decision it serves, in **11.4**: three of its fifteen indicators carry a number,
|
|
556
|
+
and none of them is about a file’s length.
|
|
557
|
+
|
|
551
558
|
---
|
|
552
559
|
|
|
553
560
|
## 6. The tool, measured rather than read
|
|
@@ -849,7 +856,14 @@ function is reported: sentinel 752 functions and 41 above 15, network 1 701 and
|
|
|
849
856
|
1 050 and 6, console 510 and 0. Zero fatal errors on 4 572 files, where escomplex fails on 37.9%
|
|
850
857
|
of `network` and 39.0% of `recruitment`. The worst module is this package.
|
|
851
858
|
|
|
852
|
-
|
|
859
|
+
⛔ **Corrected on 2026-10-13: it is not written here, it is LOADED.** oxlint 1.77 accepts an ESLint
|
|
860
|
+
rule as a JS plugin, so Sonar's own rule runs inside our own pass with none of their code in this
|
|
861
|
+
repository. The reasoning, the measurements and the licence that decides it are in **11.8**.
|
|
862
|
+
|
|
863
|
+
⚠️ **The four figures above were produced with a version this document does not name**, and the
|
|
864
|
+
plugin's behaviour moved between majors: sequences of `||` and `??` stopped incrementing. So treat
|
|
865
|
+
them as an order of magnitude, not as a baseline. The figures measured on the current version, and
|
|
866
|
+
what the metric actually adds beside McCabe, are in 11.8.
|
|
853
867
|
|
|
854
868
|
### 9.3 The maintainability figure
|
|
855
869
|
|
|
@@ -866,6 +880,8 @@ each one fires per 100 files, which is measured rather than judged:
|
|
|
866
880
|
| `max-params` | 3 | 9.6 |
|
|
867
881
|
| `max-nested-callbacks` | 10 | 0 |
|
|
868
882
|
|
|
883
|
+
⛔ **Superseded by 10.7, and closed by 11.6.** The two figures below, and the break they rest on, were measured on four modules before two defects in the gauge were fixed: diagnostics counted as breaches, and a population that included the test suite. Over 177 modules the same two rules measure 10.4 and 6.9 per 100 files against the next at 4.6, a ratio of 1.5 rather than 4.5. Section 11 then changed the thresholds themselves, and at those the most frequent rule reaches 11.0% of files against 10.7% for the next: there is no break left to split on, so `DESCRIPTORS`, `scored` and `described` are removed and every indicator counts.
|
|
884
|
+
|
|
869
885
|
**Which are out, and why.** There is a break in the data, a factor of 4.5, not a line we drew:
|
|
870
886
|
|
|
871
887
|
| kept as a descriptor | per 100 files |
|
|
@@ -968,17 +984,18 @@ touch no production file at all and the median touches **4**.
|
|
|
968
984
|
|
|
969
985
|
### 9.7 What is still a decision
|
|
970
986
|
|
|
971
|
-
| open
|
|
972
|
-
|
|
|
973
|
-
| the `/10` in the `MI`
|
|
974
|
-
| the criticality order of the
|
|
975
|
-
| the overhead line of the SQL study
|
|
976
|
-
| the workspace index for libraries
|
|
977
|
-
| a feedback channel on findings
|
|
987
|
+
| open | what it needs |
|
|
988
|
+
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
989
|
+
| the `/10` in the `MI` | the distribution over the 198 modules. It changes the spread, never the order |
|
|
990
|
+
| the criticality order of the **nine** | one argument per indicator. **Rarity no longer rules any out:** see 11.6, where the split is removed. Ranking them is still open |
|
|
991
|
+
| the overhead line of the SQL study | the harness rebuilt. #90 corrects the attribution and withdraws the 71%, which was never a ratio |
|
|
992
|
+
| the workspace index for libraries | built and measured in 10.4 and 10.5. Nothing open here any more |
|
|
993
|
+
| a feedback channel on findings | unchanged from section 8, and it is what would let any of this be proven wrong |
|
|
978
994
|
|
|
979
995
|
The one measurement that unblocks the first two is the same: **the eight indicators over all 198
|
|
980
|
-
modules, once.**
|
|
981
|
-
|
|
996
|
+
modules, once.** That measurement now exists, in 10.7 over 177 modules and in 11 over a five-module
|
|
997
|
+
bench, and it is what fixed the thresholds. The figures in THIS section still come from four modules
|
|
998
|
+
and are quoted as such.
|
|
982
999
|
|
|
983
1000
|
---
|
|
984
1001
|
|
|
@@ -1173,7 +1190,357 @@ the binary failing. Measured: same 18 diagnostics either way, exit 1 against exi
|
|
|
1173
1190
|
|
|
1174
1191
|
---
|
|
1175
1192
|
|
|
1176
|
-
|
|
1193
|
+
### 10.7 The whole repository, measured, and what it does to 9.3
|
|
1194
|
+
|
|
1195
|
+
Section 9.3 chose which indicators enter the maintainability figure by how often each fires per 100
|
|
1196
|
+
files, and excluded two of them on a **break of a factor 4.5** in that data. Those figures came from
|
|
1197
|
+
four modules, measured before two defects found while building the gauge were fixed. Here is the
|
|
1198
|
+
same question over **177 modules and 29 770 files**, with the gauge reading `src` only and counting
|
|
1199
|
+
a file once per indicator.
|
|
1200
|
+
|
|
1201
|
+
| indicator | per 100 files | modules above zero |
|
|
1202
|
+
| ------------------------ | ------------: | -----------------: |
|
|
1203
|
+
| `max-lines-per-function` | **10.4** | 75 |
|
|
1204
|
+
| `max-statements` | **6.9** | 82 |
|
|
1205
|
+
| `max-params` | 4.6 | 69 |
|
|
1206
|
+
| `max-classes-per-file` | 3.5 | 53 |
|
|
1207
|
+
| `max-lines` | 2.1 | 59 |
|
|
1208
|
+
| `complexity` | 0.8 | 33 |
|
|
1209
|
+
| `max-depth` | 0.0 | 12 |
|
|
1210
|
+
| `max-nested-callbacks` | 0.0 | 0 |
|
|
1211
|
+
|
|
1212
|
+
⛔ **The 4.5 break is gone, and it was an artefact of the measurement rather than a fact about the
|
|
1213
|
+
code.** Section 9.3 reported 46.9 and 42.9 for the two descriptors; the **same three modules**
|
|
1214
|
+
measured now give **14.4 and 7.7**. Two defects fixed tonight explain the direction and the order of
|
|
1215
|
+
magnitude:
|
|
1216
|
+
|
|
1217
|
+
- diagnostics were counted where a breach is a (file, rule) pair. `max-nested-callbacks` fires once
|
|
1218
|
+
per nesting level; measured, counting diagnostics inflates by **1.8x at the published thresholds
|
|
1219
|
+
and 2.8x at lower ones**.
|
|
1220
|
+
- the population included the test suite, because `ignorePatterns` in oxlint's config is accepted
|
|
1221
|
+
and silently ignored. Measured on `libs/front/components`: `max-lines-per-function` falls from
|
|
1222
|
+
**33.6 to 23.9** per 100 files once the specs leave.
|
|
1223
|
+
|
|
1224
|
+
What the clean data shows instead is a **smooth gradient**, consecutive ratios of 1.5, 1.5, 1.3,
|
|
1225
|
+
1.7, then 2.6 and finally 17.1 between `complexity` and `max-depth`:
|
|
1226
|
+
|
|
1227
|
+
max-lines-per-function 10.4 -> max-statements 6.9 x1.5
|
|
1228
|
+
max-statements 6.9 -> max-params 4.6 x1.5
|
|
1229
|
+
max-params 4.6 -> max-classes 3.5 x1.3
|
|
1230
|
+
max-classes 3.5 -> max-lines 2.1 x1.7
|
|
1231
|
+
max-lines 2.1 -> complexity 0.8 x2.6
|
|
1232
|
+
complexity 0.8 -> max-depth 0.0 x17.1
|
|
1233
|
+
|
|
1234
|
+
⚠️ **So the descriptor split now rests on nothing measured.** The two rules excluded from the figure
|
|
1235
|
+
are still the two most frequent, which is a reason to treat them with care, but 1.5x is not the 4.5x
|
|
1236
|
+
the decision was taken on, and inventing a cut here would be exactly the kind of threshold this
|
|
1237
|
+
document refuses elsewhere. The relative ORDER did survive: `complexity` below `max-lines` below
|
|
1238
|
+
`max-params` as before, with only `max-classes-per-file` moving up, from below `complexity` to above
|
|
1239
|
+
it.
|
|
1240
|
+
|
|
1241
|
+
**This is left open rather than re-decided.** What the data supports today is one clean statement:
|
|
1242
|
+
`max-nested-callbacks` fires **nowhere** in the repository, and `max-depth` in 12 modules for 14
|
|
1243
|
+
files in all, so those two carry almost no information here whatever the figure does with them.
|
|
1244
|
+
|
|
1245
|
+
---
|
|
1246
|
+
|
|
1247
|
+
## 11. How each threshold was chosen
|
|
1248
|
+
|
|
1249
|
+
Sections 9.3 and 10.7 left one question open: the gauge ran at the thresholds ESLint ships, which is
|
|
1250
|
+
not a choice, and 10.7 showed the one decision taken on those figures rested on a measurement
|
|
1251
|
+
defect. This section closes it. Every number in `GAUGE_THRESHOLDS` comes from one rule applied eight
|
|
1252
|
+
times, and the rule is stated before any figure so it cannot be fitted to a result afterwards.
|
|
1253
|
+
|
|
1254
|
+
### 11.1 The rule: `min(research, our p90, the rule's default)`
|
|
1255
|
+
|
|
1256
|
+
Three candidates per indicator, the strictest wins, and there is no fourth. The minimum is what
|
|
1257
|
+
makes it safe in **both** directions of failure, and both directions were observed here:
|
|
1258
|
+
|
|
1259
|
+
- ⛔ **A threshold above what the code reaches detects nothing.** `max-nested-callbacks` shipped at
|
|
1260
|
+
10 while the deepest chain in the repository is 4. It reported 0 files in 177 modules, for as long
|
|
1261
|
+
as it existed. 10.7 recorded that as "carries almost no information"; the cause is simpler, the
|
|
1262
|
+
scale was wrong.
|
|
1263
|
+
- ⛔ **Research can be more permissive than our own code, and then our code wins.** IfSQ's SP-1 puts
|
|
1264
|
+
a routine at 150 lines. Our p90 is 109. Adopting 150 because a published standard says so would
|
|
1265
|
+
relax a control to match a document, which is the opposite of measuring.
|
|
1266
|
+
|
|
1267
|
+
A vendor default is a candidate, never a reference: it is shipped to fit every codebase, so it was
|
|
1268
|
+
calibrated on none. What it cost here is in 11.6.
|
|
1269
|
+
|
|
1270
|
+
### 11.2 Three traps in deriving a threshold from our own distribution
|
|
1271
|
+
|
|
1272
|
+
⚠️ **The p90 over the whole population collapses to 0 for four of the eight.** Ninety per cent of
|
|
1273
|
+
files never nest a block, never nest a callback, declare at most one class and take at most one
|
|
1274
|
+
parameter, so the percentile sits at the floor and a threshold of 0 flags everything. So "our p90"
|
|
1275
|
+
means **the p90 over the files the rule touches**, which is the only convention that does not
|
|
1276
|
+
collapse:
|
|
1277
|
+
|
|
1278
|
+
| indicator | p90 over all 8 083 files | p90 over the files it touches |
|
|
1279
|
+
| ---------------------- | -----------------------: | ----------------------------: |
|
|
1280
|
+
| `max-depth` | 0 | 3 |
|
|
1281
|
+
| `max-nested-callbacks` | 0 | 3 |
|
|
1282
|
+
| `max-classes-per-file` | 0 | 5 |
|
|
1283
|
+
| `max-params` | 3 | 4 |
|
|
1284
|
+
|
|
1285
|
+
⚠️ **Rounding goes downward only.** `max-lines` derives a p90 of 157 and publishes 150. Rounding up
|
|
1286
|
+
would move a threshold away from the evidence that produced it, in the one direction the rule above
|
|
1287
|
+
exists to forbid.
|
|
1288
|
+
|
|
1289
|
+
⛔ **The harness read seven of the eight message shapes.** oxlint parenthesises the measured value,
|
|
1290
|
+
`too many statements (20)`, except for `complexity`, which writes `has a complexity of 15`. A reader
|
|
1291
|
+
of the parenthesised form alone returns 0 for every complexity breach and says nothing about it: the
|
|
1292
|
+
first run of this bench reported **0 files above complexity 10, 15 and 20 alike**, which reads as
|
|
1293
|
+
"nothing to see" rather than as a broken instrument. The same defect sat in the role's own
|
|
1294
|
+
`valueOf`, where it was latent because no report row consumed the value yet. Both are fixed, and the
|
|
1295
|
+
regression test now uses the messages oxlint really emits rather than a paraphrase.
|
|
1296
|
+
|
|
1297
|
+
### 11.3 The bench
|
|
1298
|
+
|
|
1299
|
+
**8 083 product files over five modules**: `console` and `host-admin` (react), `front-components`
|
|
1300
|
+
(react library), `recruitment` and `network` (nest). The exclusion list is the role's own, so no
|
|
1301
|
+
spec, fixture, mock, story, generated or declaration file is in any figure below. Distributions come
|
|
1302
|
+
from one oxlint run per rule set with every maximum at 1, so each occurrence reports its real value
|
|
1303
|
+
instead of only its breach.
|
|
1304
|
+
|
|
1305
|
+
### 11.4 What IfSQ gives, and what it does not
|
|
1306
|
+
|
|
1307
|
+
IfSQ (ifsq.org) is the one standard found that states numeric limits with citations attached. It is
|
|
1308
|
+
a standard for **human inspection**: the level pages say what to look for, and `assessment.html`
|
|
1309
|
+
says who inspects, from self-assessment to third-party certification. There is no defect-density
|
|
1310
|
+
rule and no grade boundary anywhere on the site, checked page by page.
|
|
1311
|
+
|
|
1312
|
+
**Three of its fifteen indicators carry a number:**
|
|
1313
|
+
|
|
1314
|
+
| code | indicator | threshold | citations given |
|
|
1315
|
+
| -------- | ------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1316
|
+
| **SP-1** | Routine Too Long | > 150 lines, comments and blank lines excluded | IBM, "the most error-prone routines were those larger than 500 lines"; "when routines averaged 100 to 150 lines each, code was more stable"; "routines with 143 lines or less are 2.4 times less expensive to fix" |
|
|
1317
|
+
| **SP-2** | Nesting Too Deep | > 3 levels of conditional nesting | "few people can understand more than 3 levels of nested if statements". ⚠️ One finding page says "more than four" instead |
|
|
1318
|
+
| **SP-3** | Routine Too Complex | > 10 binary terms in conditionals | McCabe 1976 |
|
|
1319
|
+
|
|
1320
|
+
⛔ **IfSQ sources nothing about a FILE's length.** All three citations above are about routines. So
|
|
1321
|
+
`max-lines` has no research candidate at all, and its threshold is our p90 and nothing else.
|
|
1322
|
+
|
|
1323
|
+
The remaining twelve state a defect to look for with no measurement: WIP-1 Vague "To Do", WIP-2
|
|
1324
|
+
Disabled Code, WIP-3 Empty Statement Block, SPM-1 Magic Numbers, SPM-2 Magic Strings, SPM-3
|
|
1325
|
+
Copy/Paste Programming, DP-1 Parameter Not Checked, DP-2 Status Ignored After Call, DP-3 Unexpected
|
|
1326
|
+
State Not Trapped, and the three Causes for Concern, which are an inspector's judgement. Of those,
|
|
1327
|
+
SPM-3 is already covered by the duplication pass and WIP-2 partly by the dead-code pass; WIP-1,
|
|
1328
|
+
WIP-3, SPM-1, SPM-2, DP-1 and DP-2 are candidates for indicators this role does not have yet.
|
|
1329
|
+
|
|
1330
|
+
### 11.5 The eight thresholds, with their origin
|
|
1331
|
+
|
|
1332
|
+
| indicator | was | **is** | research | our p90 | what decided it |
|
|
1333
|
+
| ------------------------ | --: | ------: | -------- | ------: | --------------------------------------------------------------------------------------------------------- |
|
|
1334
|
+
| `complexity` | 20 | **10** | 10 | 11 | the only indicator where two independent references agree within a unit |
|
|
1335
|
+
| `max-lines-per-function` | 50 | **109** | 150 | 109 | research is more permissive than our code, so our code wins |
|
|
1336
|
+
| `max-lines` | 300 | **150** | none | 157 | our p90, rounded down. No research sets a file's length |
|
|
1337
|
+
| `max-depth` | 4 | **3** | 3 | 3 | research and our code agree |
|
|
1338
|
+
| `max-nested-callbacks` | 10 | **3** | 3 (SP-2) | 3 | same nesting family as SP-2, no citation of its own. Was above the repository's maximum of 4 |
|
|
1339
|
+
| `max-statements` | 10 | **10** | none | 15 | the default is the stricter candidate and stays |
|
|
1340
|
+
| `max-params` | 3 | **3** | none | 4 | the default stays. Beyond three parameters the fix is an object or currying, so this is a design boundary |
|
|
1341
|
+
| `max-classes-per-file` | 1 | **1** | none | 5 | 1 is the rule's floor, there is no stricter value |
|
|
1342
|
+
|
|
1343
|
+
Five move, and `complexity` is worth one line on its own: 94.8% of our 11 050 measured functions
|
|
1344
|
+
already sit in the 1-10 band the SEI calls low risk, so 10 is a ratchet rather than a demand.
|
|
1345
|
+
|
|
1346
|
+
| SEI band | functions | share |
|
|
1347
|
+
| ------------------- | --------: | ---------: |
|
|
1348
|
+
| 1-10, low risk | 10 475 | **94.8 %** |
|
|
1349
|
+
| 11-15 | 319 | 2.9 % |
|
|
1350
|
+
| 16-20 | 120 | 1.1 % |
|
|
1351
|
+
| 21-50, high risk | 128 | 1.2 % |
|
|
1352
|
+
| over 50, untestable | 8 | 0.1 % |
|
|
1353
|
+
|
|
1354
|
+
### 11.6 What the eight size rules say together, and the end of the descriptor split
|
|
1355
|
+
|
|
1356
|
+
A threshold is only half a decision: what matters is what the **set** reports.
|
|
1357
|
+
|
|
1358
|
+
| | at the vendor defaults | at the thresholds above |
|
|
1359
|
+
| ----------------------------- | ------------------------------: | ----------------------: |
|
|
1360
|
+
| union of the eight size rules | 2 236 files, 27.7 % | **1 708 files, 21.1 %** |
|
|
1361
|
+
| largest single rule | `max-lines-per-function`, 1 724 | `max-statements`, 891 |
|
|
1362
|
+
| its share of the union | **77 %** | **52 %** |
|
|
1363
|
+
|
|
1364
|
+
⛔ **At the defaults one rule was the report.** `max-lines-per-function` at 50 flagged 21.3% of all
|
|
1365
|
+
product files and carried 77% of the union, so the other seven indicators were inaudible: the
|
|
1366
|
+
report said "your functions are longer than 50 lines" and nothing else could be heard beside it.
|
|
1367
|
+
|
|
1368
|
+
And this closes what 10.7 left open. The descriptor split held `max-statements` and
|
|
1369
|
+
`max-lines-per-function` out of the figure on a **break of a factor 4.5** between 42.9/46.9 and 9.6
|
|
1370
|
+
per 100 files. 10.7 already showed those numbers were an artefact of two defects. At the thresholds
|
|
1371
|
+
above the two most frequent rules are 11.0% and 10.7%, and the next is 7.8%:
|
|
1372
|
+
|
|
1373
|
+
| indicator | files | per 100 files | ratio to the next |
|
|
1374
|
+
| ------------------------ | ----: | ------------: | ----------------: |
|
|
1375
|
+
| `max-statements` | 891 | 11.0 | 1.03 |
|
|
1376
|
+
| `max-lines` | 864 | 10.7 | 1.37 |
|
|
1377
|
+
| `max-lines-per-function` | 630 | 7.8 | 1.26 |
|
|
1378
|
+
| `complexity` | 499 | 6.2 | 1.34 |
|
|
1379
|
+
| `max-params` | 373 | 4.6 | 2.32 |
|
|
1380
|
+
| `max-classes-per-file` | 161 | 2.0 | 17.89 |
|
|
1381
|
+
| `max-nested-callbacks` | 9 | 0.1 | 1.13 |
|
|
1382
|
+
| `max-depth` | 8 | 0.1 | - |
|
|
1383
|
+
|
|
1384
|
+
**The top five are a smooth gradient, 1.03 to 1.37, so there is no population to exclude.** The one
|
|
1385
|
+
large break, 17.9, sits at the BOTTOM, between `max-classes-per-file` and the two nesting rules: it
|
|
1386
|
+
says those two are rare here, not that the frequent ones are noise. `DESCRIPTORS`, `scored` and
|
|
1387
|
+
`described` are removed: every indicator counts, and the asymmetry that needed explaining in two
|
|
1388
|
+
places no longer exists. Note also that the two nesting rules moved from reporting **nothing at all**
|
|
1389
|
+
to reporting 9 and 8 files: lowering them from 10 and 4 to 3 is what gave them any information.
|
|
1390
|
+
|
|
1391
|
+
### 11.7 Why `max-statements` is not a second reading of `max-lines-per-function`
|
|
1392
|
+
|
|
1393
|
+
The obvious objection to keeping both is that a long function has many statements. Measured by
|
|
1394
|
+
joining the two diagnostics on the same function span, over 25 658 functions of which 10 441 carry
|
|
1395
|
+
both values:
|
|
1396
|
+
|
|
1397
|
+
| | |
|
|
1398
|
+
| --------------------------------------- | -------------------: |
|
|
1399
|
+
| Pearson, statements against lines | 0.726 |
|
|
1400
|
+
| Spearman | 0.600 |
|
|
1401
|
+
| lines per statement, p25 / median / p90 | 2.5 / **4.4** / 14.0 |
|
|
1402
|
+
|
|
1403
|
+
Correlated, so not independent. But the density varies by a factor of **5.6** across functions, and
|
|
1404
|
+
that is what forbids giving them one value. At the chosen thresholds:
|
|
1405
|
+
|
|
1406
|
+
| | functions | what it is |
|
|
1407
|
+
| ----------------------------- | --------: | ------------------------ |
|
|
1408
|
+
| over both | 399 | long and dense |
|
|
1409
|
+
| `max-statements` only | **699** | dense logic in few lines |
|
|
1410
|
+
| `max-lines-per-function` only | **255** | long with little logic |
|
|
1411
|
+
| overlap | **29 %** | |
|
|
1412
|
+
|
|
1413
|
+
Both shapes are real, and in this repository they are two different kinds of file:
|
|
1414
|
+
|
|
1415
|
+
| file | lines | statements | shape |
|
|
1416
|
+
| --------------------------------------- | ----: | ---------: | ---------------------- |
|
|
1417
|
+
| `add-to-network/JobsEvaluationList.tsx` | 259 | 2 | one large JSX return |
|
|
1418
|
+
| `entities/job-offer.entity.ts` | 48 | 46 | one statement per line |
|
|
1419
|
+
|
|
1420
|
+
So `max-lines-per-function` measures volume including markup, and `max-statements` measures
|
|
1421
|
+
imperative density, and neither sees what the other sees. Giving `max-statements` the same 109 would
|
|
1422
|
+
kill it: at the median density that is a function of roughly 480 lines, so the line count would
|
|
1423
|
+
always fire first and the statement count would never say anything again.
|
|
1424
|
+
|
|
1425
|
+
---
|
|
1426
|
+
|
|
1427
|
+
### 11.8 The ninth indicator: cognitive complexity, loaded rather than written
|
|
1428
|
+
|
|
1429
|
+
Section 9.2 chose this metric and said it would be "written once here". It is not written. oxlint
|
|
1430
|
+
1.77 accepts an ESLint rule as a JS plugin, so Sonar's own `cognitive-complexity` runs inside the
|
|
1431
|
+
same invocation as the eight rules above, through a bridge that re-exposes it and contains none of
|
|
1432
|
+
their code.
|
|
1433
|
+
|
|
1434
|
+
**Why loaded and not reimplemented**, in order of weight:
|
|
1435
|
+
|
|
1436
|
+
⛔ **The licence decides it.** `eslint-plugin-sonarjs` ships under the Sonar Source-Available
|
|
1437
|
+
License, whose grant is limited to a "Non-competitive Purpose", and sentinel is published under MIT.
|
|
1438
|
+
Depending on a package is not preparing a derivative work of it; transcribing its code into this
|
|
1439
|
+
repository would be, in any language, and would mean granting rights we never received. ⚠️ Its
|
|
1440
|
+
`package.json` declares `LGPL-3.0-only` while the LICENSE file and every source header say SSAL.
|
|
1441
|
+
That contradiction is Sonar's; it is recorded here, not resolved.
|
|
1442
|
+
|
|
1443
|
+
**The metric's author works there.** Campbell 2018 is theirs, so their implementation is the
|
|
1444
|
+
specification's own house rather than one reading of it among several.
|
|
1445
|
+
|
|
1446
|
+
**It costs almost nothing.** Measured: 0 advisories at every level, 94 packages, and `eslint` is
|
|
1447
|
+
**not** pulled, because pnpm leaves the peer unmet and oxlint provides the context itself. It does
|
|
1448
|
+
pull `typescript`, which nothing of ours imports. The rule adds 2.4 s over 8 083 files to a pass
|
|
1449
|
+
that was already reading them.
|
|
1450
|
+
|
|
1451
|
+
#### The behaviour is not the paper's, deliberately
|
|
1452
|
+
|
|
1453
|
+
Measured on the same file with the same parser, before any of it was believed:
|
|
1454
|
+
|
|
1455
|
+
| | sonarjs 0.25.1 | sonarjs 4.2.2 |
|
|
1456
|
+
| --------------------------- | -------------: | ------------: |
|
|
1457
|
+
| `if (a \|\| b \|\| c)` | 2 | **1** |
|
|
1458
|
+
| `if (a && b && c)` | 2 | 2 |
|
|
1459
|
+
| `const x = a \|\| b \|\| c` | 1 | **0** |
|
|
1460
|
+
|
|
1461
|
+
⛔ **This is not a regression, which is what it was first called here.** Sequences of `||` and `??`
|
|
1462
|
+
are excluded on purpose: SonarSource state it themselves, "we tried to adjust the cognitive
|
|
1463
|
+
complexity rule to accommodate common idioms", and made the exclusion universal in SonarQube 10.8
|
|
1464
|
+
after it had been inconsistent between contexts. In JavaScript those two operators are
|
|
1465
|
+
overwhelmingly default-value shorthand rather than control flow. The lesson is the general one: an
|
|
1466
|
+
implementation that diverges from a published algorithm is a question to ask, not a defect to
|
|
1467
|
+
assume.
|
|
1468
|
+
|
|
1469
|
+
#### The host is validated, not assumed
|
|
1470
|
+
|
|
1471
|
+
Running somebody else's rule inside another linter removes the question "is our counting right" and
|
|
1472
|
+
replaces it with a narrower one: **does oxlint's tree feed the rule the same thing ESLint's does.**
|
|
1473
|
+
A fixture cannot answer that, because the constructs at risk are the ones real code has and
|
|
1474
|
+
fixtures do not: decorators, enums, `satisfies`, generics, and the JSX short-circuit the rule
|
|
1475
|
+
treats specially.
|
|
1476
|
+
|
|
1477
|
+
So the same rule was run through both hosts over two real modules and compared **function by
|
|
1478
|
+
function**, matched on file and line:
|
|
1479
|
+
|
|
1480
|
+
| | functions | identical | disagreements | seen by one host only |
|
|
1481
|
+
| ------------------------ | --------: | --------: | ------------: | --------------------: |
|
|
1482
|
+
| `contract`, nest | 539 | **539** | 0 | 0 |
|
|
1483
|
+
| `console`, react and tsx | 581 | **581** | 0 | 0 |
|
|
1484
|
+
|
|
1485
|
+
⚠️ This was missing when the figure first shipped, and the gap was not obvious: the earlier check
|
|
1486
|
+
compared the two hosts on about twenty fixture functions, which proves the plumbing and not the
|
|
1487
|
+
parse. It is the same bar `complexity` was held to against oxlint, 839 functions and 839 identical
|
|
1488
|
+
values, and it is the bar any rule borrowed from another ecosystem has to clear.
|
|
1489
|
+
|
|
1490
|
+
#### The threshold
|
|
1491
|
+
|
|
1492
|
+
`min(research, our p90, the rule's default)`, the same rule as the eight above. Sonar publishes
|
|
1493
|
+
**15** for S3776, and that is a vendor default rather than a research value: one of their own
|
|
1494
|
+
engineers has said publicly that no formula supports it. Our p90 over the files the rule touches is
|
|
1495
|
+
**7**, so 7 wins. It reports **342 of 8 083 files, 4.2%**, inside the band the other eight occupy.
|
|
1496
|
+
|
|
1497
|
+
#### What it adds, and the honest version of that
|
|
1498
|
+
|
|
1499
|
+
⚠️ **Almost nothing to the union.** Of its 342 files, **330 were already flagged** by one of the
|
|
1500
|
+
eight: the union goes from 1 708 files to 1 720, 21.1% to 21.3%. Anyone judging the indicator by
|
|
1501
|
+
coverage should stop here and conclude it is redundant.
|
|
1502
|
+
|
|
1503
|
+
That reading is wrong, and the measurement that shows why is the comparison with McCabe, which is
|
|
1504
|
+
the thing it was brought in to complement:
|
|
1505
|
+
|
|
1506
|
+
| on 8 083 product files | |
|
|
1507
|
+
| ------------------------------------------------- | ------: |
|
|
1508
|
+
| flagged by both complexities | 242 |
|
|
1509
|
+
| **cyclomatic only**, branchy but readable | **257** |
|
|
1510
|
+
| **cognitive only**, tangled without being branchy | **100** |
|
|
1511
|
+
| agreement between the two | **40%** |
|
|
1512
|
+
| Spearman over the 3 823 files carrying both | 0.764 |
|
|
1513
|
+
|
|
1514
|
+
The two disagree on **60% of the files either one flags**, which is the whole point. A flat `switch`
|
|
1515
|
+
with twelve cases is cyclomatically 13 and cognitively 1, because nothing is nested and nothing
|
|
1516
|
+
surprises; three `if`s inside each other are cyclomatically 4 and cognitively 6, because each level
|
|
1517
|
+
is charged for the levels above it. The 257 are the first shape and the 100 are the second. A figure
|
|
1518
|
+
that agreed with McCabe would have been the redundant one.
|
|
1519
|
+
|
|
1520
|
+
#### Three defects on the way, all with one symptom
|
|
1521
|
+
|
|
1522
|
+
Each produced exactly `oxlint could not be run, so no size indicator is reported`, which names
|
|
1523
|
+
neither the plugin nor the path, and each silently removed **all nine** figures rather than one:
|
|
1524
|
+
|
|
1525
|
+
- the bridge written straight into `plugins/`, which is git-ignored and wiped by every build. It
|
|
1526
|
+
survived until the next `pnpm build`.
|
|
1527
|
+
- its path computed by counting `..` from `import.meta.url`. `tsup` bundles into chunks whose depth
|
|
1528
|
+
is not the source tree's, so it resolved from `src/` and not from `dist/`. ⚠️
|
|
1529
|
+
`roles/test/shared-test-config.ts` already records this lesson at length, after it cost a module
|
|
1530
|
+
83 passing tests; it was repeated anyway. The path is now walked up to.
|
|
1531
|
+
- the rule is namespaced by its plugin in the config, `sonar/cognitive-complexity`, and bare in the
|
|
1532
|
+
diagnostic. One name in both places gives a config oxlint accepts and a breach map that never
|
|
1533
|
+
matches.
|
|
1534
|
+
|
|
1535
|
+
All three are now tests.
|
|
1536
|
+
|
|
1537
|
+
---
|
|
1538
|
+
|
|
1539
|
+
## 12. Sources
|
|
1540
|
+
|
|
1541
|
+
- IfSQ. _Level 1, Level 2 and Level 3 Standards for Code Quality._ <https://www.ifsq.org/level-1.html>,
|
|
1542
|
+
<https://www.ifsq.org/level-2.html>, <https://www.ifsq.org/structured-programming.html>,
|
|
1543
|
+
<https://www.ifsq.org/research.html>, <https://www.ifsq.org/assessment.html>
|
|
1177
1544
|
|
|
1178
1545
|
- Ben Khalfallah, H. _Code Health Meter: A Quantitative and Graph-Theoretic Foundation for
|
|
1179
1546
|
Automated Code Quality and Architecture Assessment._ ACM TOSEM 35(4), article 114, March 2026.
|
package/docs/using-sentinel.md
CHANGED
|
@@ -4,8 +4,9 @@ The commands, what an adopted module ends up looking like, and how versions move
|
|
|
4
4
|
migrating a module see [`lint-adoption.md`](lint-adoption.md),
|
|
5
5
|
[`format-adoption.md`](format-adoption.md),
|
|
6
6
|
[`typescript-adoption.md`](typescript-adoption.md),
|
|
7
|
-
[`build-adoption.md`](build-adoption.md)
|
|
8
|
-
[`test-adoption.md`](test-adoption.md)
|
|
7
|
+
[`build-adoption.md`](build-adoption.md),
|
|
8
|
+
[`test-adoption.md`](test-adoption.md) and
|
|
9
|
+
[`static-analysis-adoption.md`](static-analysis-adoption.md), which also carries the reference gate `--test` runs a
|
|
9
10
|
migration against.
|
|
10
11
|
|
|
11
12
|
## The grid
|