@hublo/sentinel 1.4.3 → 1.4.4
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/dist/bin/sentinel.js +3 -3
- package/dist/{chunk-DYK6UEMO.js → chunk-BSERYTT5.js} +15 -3
- package/dist/{chunk-TDNPPNGK.js → chunk-NFGIHMXB.js} +29 -1
- package/dist/index.js +2 -2
- package/dist/roles/build/nest/toolchain.js +68 -18
- package/dist/roles/build/nest/toolchain.js.map +1 -1
- package/dist/{validate-3QMCX2HW.js → validate-L52THU22.js} +1 -1
- package/docs/sql-verification-study.md +401 -0
- package/docs/static-analysis-study.md +527 -0
- package/docs/typescript-traces.md +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
# Static analysis: the study behind the role
|
|
2
|
+
|
|
3
|
+
Research record for the `static-analysis` role (HUB-41506), written 2026-10-06. It is **not a
|
|
4
|
+
specification**: it records what was measured, what was decided, and the reason attached to each
|
|
5
|
+
decision, so that the next person can disagree with a reason rather than with a preference.
|
|
6
|
+
|
|
7
|
+
Every figure below was measured on this monorepo on `origin/main`, with madge 8.0.0, nx 24,
|
|
8
|
+
jscpd 5.4.0 and oxlint 1.77.0. The conditions matter, and the first section says why.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 0. Notation
|
|
13
|
+
|
|
14
|
+
Every symbol used below, in one place. They come from four different traditions, which is why
|
|
15
|
+
the same idea sometimes carries two names.
|
|
16
|
+
|
|
17
|
+
| symbol | name | what it is |
|
|
18
|
+
| ------------------ | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
19
|
+
| `Ca` | afferent coupling | how many components depend on this one. Incoming edges |
|
|
20
|
+
| `Ce` | efferent coupling | how many components this one depends on. Outgoing edges |
|
|
21
|
+
| `DC` | degree centrality | `Ca + Ce`. The overall connectedness of one node. The ACM study calls the two halves `IDC` and `ODC` |
|
|
22
|
+
| `I` | instability | `Ce / (Ca + Ce)`, from 0 to 1. 0 is a stable foundation, 1 is a pure consumer |
|
|
23
|
+
| `A` | abstractness | abstract classes over total classes in a component. ArchUnit counts public ones only |
|
|
24
|
+
| `D` | distance from the main sequence | `abs(A + I - 1)`. How far a component sits from the ideal line `A + I = 1` |
|
|
25
|
+
| `SCC` | strongly connected component | a set of nodes that can all reach each other. One SCC of size n contains every cycle among those n nodes |
|
|
26
|
+
| `CC` | cyclomatic complexity | McCabe's count of independent paths through a function |
|
|
27
|
+
| `HV` | Halstead volume | a size measure built from the distinct and total operators and operands |
|
|
28
|
+
| `MI` | maintainability index | a composite of `HV`, `CC` and lines of code. Formula in section 5 |
|
|
29
|
+
| `LOC` | lines of code | physical lines, unless stated otherwise |
|
|
30
|
+
| `Q` | modularity | how well a graph splits into communities, maximised by the Louvain method |
|
|
31
|
+
| `H` | relational cohesion | `(R + 1) / N`, with `R` the edges internal to a module and `N` its members |
|
|
32
|
+
| `LCOM4` | lack of cohesion of methods | connected components among the methods that share a field. Above 1 signals several responsibilities |
|
|
33
|
+
| `DependsOn` | Lakos | the transitive closure of a component, itself included |
|
|
34
|
+
| `CCD` | cumulative component dependency | the sum of every `DependsOn` in the system |
|
|
35
|
+
| `ACD`, `RACD` | average and relative average component dependency | `CCD / N` and `ACD / N` |
|
|
36
|
+
| `NCCD` | normalised cumulative component dependency | `CCD` over that of a balanced binary tree of the same size |
|
|
37
|
+
| `RV`, `ARV`, `GRV` | relative visibility, average, global | visible elements over total elements, per component, averaged, and system wide |
|
|
38
|
+
| churn | | how many commits touched a file over a period |
|
|
39
|
+
| entropy | change entropy | how **scattered** those commits were across files, which is a different question |
|
|
40
|
+
|
|
41
|
+
Two conventions worth stating because they bite. A **percentile** is written `p95` and means the
|
|
42
|
+
value below which 95% of the measured population sits. A **rank correlation** is Spearman's: 1
|
|
43
|
+
means two rankings are identical, 0 means they are unrelated.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 1. Three traps that make a number wrong without saying so
|
|
48
|
+
|
|
49
|
+
Each was hit while preparing this study. Each produces a plausible figure and no error message,
|
|
50
|
+
which is precisely the failure the ACM study criticises in existing tools: a result nobody can
|
|
51
|
+
trace back to its cause.
|
|
52
|
+
|
|
53
|
+
### The wrong tsconfig loses 61% of the graph
|
|
54
|
+
|
|
55
|
+
The 403 path aliases live in `tsconfig.base.json` at the **workspace root**. The applications'
|
|
56
|
+
own tsconfigs declare none. 40% of internal import edges go through an alias, so pointing madge
|
|
57
|
+
at an application's tsconfig hands it an empty resolution table.
|
|
58
|
+
|
|
59
|
+
| madge invocation | nodes | edges |
|
|
60
|
+
| ----------------------------------------------- | --------: | --------: |
|
|
61
|
+
| `--ts-config` on the app's tsconfig (0 aliases) | 859 | 1 930 |
|
|
62
|
+
| `--ts-config tsconfig.base.json` (403 aliases) | **1 785** | **4 882** |
|
|
63
|
+
|
|
64
|
+
Measured on one subtree. The witness file went from 1 edge to 3: both of its alias imports
|
|
65
|
+
appeared.
|
|
66
|
+
|
|
67
|
+
### nx `implicit` edges invent 20% of cyclicality
|
|
68
|
+
|
|
69
|
+
The nx project graph mixes two kinds of edge. A lambda imports its parent project: a `static`
|
|
70
|
+
edge going up. The parent declares that it owns the lambda: an `implicit` edge coming down.
|
|
71
|
+
Tarjan sees a cycle where there is only an ownership relation.
|
|
72
|
+
|
|
73
|
+
| edges kept | SCCs | projects in cycles | cyclicality |
|
|
74
|
+
| --------------------- | ----: | -----------------: | ----------: |
|
|
75
|
+
| `static` + `implicit` | 14 | 90 | **20.45%** |
|
|
76
|
+
| `static` only | **0** | **0** | **0.00%** |
|
|
77
|
+
|
|
78
|
+
Graph-wide: 2 008 `static` edges, 270 `implicit`.
|
|
79
|
+
|
|
80
|
+
### A partial scan biases every centrality ranking
|
|
81
|
+
|
|
82
|
+
Scanning a subtree and following its outgoing edges makes libraries appear as targets only.
|
|
83
|
+
Their efferent coupling is then zero by construction and the top of the ranking fills with
|
|
84
|
+
artificial sinks. On a first pass, the four most central files all had `Ce = 0`, not because
|
|
85
|
+
they depend on nothing but because they had not been read.
|
|
86
|
+
|
|
87
|
+
> **Consequence.** Every published figure carries a resolution fingerprint: tsconfig used, edge
|
|
88
|
+
> types kept, scope scanned, tool versions. Without it, a configuration change reads as an
|
|
89
|
+
> architectural regression.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 2. What the code actually measures
|
|
94
|
+
|
|
95
|
+
### File graph, whole front end (11 109 files scanned)
|
|
96
|
+
|
|
97
|
+
| | value |
|
|
98
|
+
| -------------------------------------- | -------------------------------------------------------------------- |
|
|
99
|
+
| nodes, edges | 12 372, 42 472, mean out-degree 3.43 |
|
|
100
|
+
| System Cyclicality (Lattix definition) | **1.48%**, 183 files in 68 strongly connected components, largest 12 |
|
|
101
|
+
| connectedness (reachable pairs) | **4.65%**, against 0.153% edge density |
|
|
102
|
+
| DC median / p95 / p99 / max | 4 / 17 / 43 / 1 244 |
|
|
103
|
+
| DC = 0, dead files | **160** (1.3%) |
|
|
104
|
+
| DC > 15 | **721** files (5.8%) |
|
|
105
|
+
|
|
106
|
+
### Project graph, nx, `static` edges only
|
|
107
|
+
|
|
108
|
+
440 projects, 2 008 dependencies, **0 cycles**, unstable dependencies 1.1% (23 edges).
|
|
109
|
+
|
|
110
|
+
> **The architectural reading.** Module boundaries are sound; the disorder is inside the
|
|
111
|
+
> modules. `nx/enforce-module-boundaries` polices the project graph and nothing polices the file
|
|
112
|
+
> graph, so that is where this role adds something nx does not already provide.
|
|
113
|
+
|
|
114
|
+
### Timings
|
|
115
|
+
|
|
116
|
+
| | |
|
|
117
|
+
| ------------------------------------------- | -------------------- |
|
|
118
|
+
| madge, whole front end | **46 s** |
|
|
119
|
+
| nx graph | **11 s** |
|
|
120
|
+
| git churn per file, 12 months | **2 s** |
|
|
121
|
+
| the same pass with lines, authors and dates | **5 s** |
|
|
122
|
+
| git churn per function (`git log -L`) | **106 ms per range** |
|
|
123
|
+
|
|
124
|
+
Which settles the "continuous" question: per-file analysis is affordable on every pull request.
|
|
125
|
+
Per-function churn is not, at 3.6 hours for the whole front end, but it is affordable on a short
|
|
126
|
+
list: 106 seconds for the 100 most central files.
|
|
127
|
+
|
|
128
|
+
### The files the graph points at
|
|
129
|
+
|
|
130
|
+
| file | DC | Ca | Ce | I | reading |
|
|
131
|
+
| ------------------------------------------- | ----: | ----: | --: | ---: | -------------------------------------------------------------------------------------------- |
|
|
132
|
+
| `libs/front/components/.../Typography.tsx` | 1 244 | 1 244 | 0 | 0.00 | Healthy. Martin's rule is that a heavily depended-upon component should be stable, and it is |
|
|
133
|
+
| `libs/api-types/bff-admin/.../api-types.ts` | 962 | 962 | 0 | 0.00 | Generated. Must be excluded before any rule speaks about tests |
|
|
134
|
+
| `libs/front/tests/src/render/index.ts` | 910 | 908 | 2 | 0.00 | Touching this helper, or the test setup this package ships, reaches 908 files |
|
|
135
|
+
| `libs/front/api/src/bff-admin/config.ts` | 655 | 348 | 307 | 0.47 | **The real finding.** 348 files depend on something that itself depends on 307 others |
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 3. Three measured results that decided the model
|
|
140
|
+
|
|
141
|
+
Each was run because a plausible assumption deserved a test rather than an argument.
|
|
142
|
+
|
|
143
|
+
### Centrality is not file size in disguise
|
|
144
|
+
|
|
145
|
+
A known criticism (Herraiz et al., cited in Hassan's related work) is that most code complexity
|
|
146
|
+
metrics correlate highly with lines of code, so they rediscover that big files are big. Applied
|
|
147
|
+
to centrality, ranking all 12 372 files twice:
|
|
148
|
+
|
|
149
|
+
**Rank correlation DC against LOC: 0.388.** Far from 1, so the two rankings designate different
|
|
150
|
+
files and centrality carries information size does not. The criticism does not bite here.
|
|
151
|
+
|
|
152
|
+
### Centrality is not a proxy for change frequency
|
|
153
|
+
|
|
154
|
+
The hypothesis was that a central file, being heavily used, is frequently changed, which would
|
|
155
|
+
make git history redundant.
|
|
156
|
+
|
|
157
|
+
| | |
|
|
158
|
+
| --------------------------------- | ------------- |
|
|
159
|
+
| rank correlation DC against churn | **0.361** |
|
|
160
|
+
| files common to both top 100 | **13 of 100** |
|
|
161
|
+
|
|
162
|
+
| counter-example | DC | churn |
|
|
163
|
+
| ------------------------------------------------ | --: | ----: |
|
|
164
|
+
| `front-legacy/src/constants/routes/index.ts` | 424 | 2 |
|
|
165
|
+
| `libs/front/translations/.../main.planning.json` | 2 | 309 |
|
|
166
|
+
|
|
167
|
+
So a central file can be perfectly stable, which is often the sign that it is well made. The two
|
|
168
|
+
are **the two factors of a product, not substitutes**: centrality is the consequence term,
|
|
169
|
+
churn is the likelihood term. This is why CodeScene's hotspot is a multiplication.
|
|
170
|
+
|
|
171
|
+
> A corollary that shaped the model: a vector of uncorrelated metrics **combines, it does not
|
|
172
|
+
> stack**. Requiring central AND long AND complex AND poor MI selects almost nothing.
|
|
173
|
+
|
|
174
|
+
### The graph does not see the product
|
|
175
|
+
|
|
176
|
+
Raised in a tech lead review of the test strategy: none of these metrics says which user journeys
|
|
177
|
+
matter, so a file can be central and uninteresting, or peripheral and critical. The objection
|
|
178
|
+
holds, and it holds twice.
|
|
179
|
+
|
|
180
|
+
**A screen is a structural leaf.** Over the 596 front end files that live under a `routes/` or a
|
|
181
|
+
`pages/` directory, `routeTree.gen.ts` excluded:
|
|
182
|
+
|
|
183
|
+
| | share of the 596 |
|
|
184
|
+
| ----------------------------------------------- | ---------------- |
|
|
185
|
+
| Ca ≤ 1, at most one file depends on them | **83.2%** |
|
|
186
|
+
| Ca = 0, nothing in the codebase depends on them | **46.3%** |
|
|
187
|
+
|
|
188
|
+
This is not a defect in the metric, it is what afferent coupling means: a screen is where the
|
|
189
|
+
product value is and it is the last node of the graph, so centrality ranks it last by
|
|
190
|
+
construction.
|
|
191
|
+
|
|
192
|
+
**And the graph cannot carve a journey out on its own.** If a journey were given by its entry
|
|
193
|
+
point, there are two ways to expand it into a set of files, and both collapse. Measured on the
|
|
194
|
+
182 route files of `host-admin`:
|
|
195
|
+
|
|
196
|
+
| expansion | result |
|
|
197
|
+
| ------------------------------------------------------ | ------------------------------------------------------------------------------------ |
|
|
198
|
+
| transitive closure of one route file | median **440** files, max 5 733. The 5 largest routes alone reach **70%** of the app |
|
|
199
|
+
| exclusive, reachable from this route and from no other | median **0**. 13 of the 18 route groups own exactly zero files |
|
|
200
|
+
|
|
201
|
+
The two bracket the problem. Expanded transitively a journey is the whole application; reduced to
|
|
202
|
+
what it owns alone it is empty. The useful set in between, the files that actually carry the
|
|
203
|
+
journey, is not in the graph. It is in the team's head.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## 4. What was decided, and why
|
|
208
|
+
|
|
209
|
+
### No single score. Six categories instead.
|
|
210
|
+
|
|
211
|
+
Weighting would need experimentation to fix the formula, and a weight nobody can justify is a
|
|
212
|
+
claim nobody can defend. Six correct numbers beat one indefensible score.
|
|
213
|
+
|
|
214
|
+
This is consistent with three independent positions: the ACM study's own 6D signature per
|
|
215
|
+
module, Lattix's documentation ("it is hard to come up with a single number that represents the
|
|
216
|
+
architectural health of a system... instead, a set of numbers"), and the ACM study's criticism
|
|
217
|
+
of Sonar, which is not that it aggregates but that it aggregates opaquely.
|
|
218
|
+
|
|
219
|
+
The door stays open: the correlation matrix that would justify weights later is the same one
|
|
220
|
+
that validates the categories now. Two of its cells are already filled, and both are weak.
|
|
221
|
+
|
|
222
|
+
**A category is sound when its metrics lead to the same action.** Otherwise it is a filing
|
|
223
|
+
cabinet, not a tool.
|
|
224
|
+
|
|
225
|
+
| category | the question | contains | the action |
|
|
226
|
+
| --------------- | ------------------------------- | --------------------------------------------- | ------------------- |
|
|
227
|
+
| **Impact** | what does it break | Ca, Ce, DC, I, later A and D | none. It **orders** |
|
|
228
|
+
| **Activity** | how much does it move | churn, entropy, authors, co-change | none. It **orders** |
|
|
229
|
+
| **Cycles** | what bites its own tail | SCCs, System Cyclicality | move one import |
|
|
230
|
+
| **Cohesion** | is the split right | Q, H, LCOM4, density per module | re-cut, move a file |
|
|
231
|
+
| **Complexity** | is it readable | CC, nesting, lines per function, Halstead, MI | split a function |
|
|
232
|
+
| **Duplication** | does it already exist elsewhere | jscpd | extract, or accept |
|
|
233
|
+
|
|
234
|
+
Cycles is separate from Impact although both come from the graph, because the action differs: a
|
|
235
|
+
cycle is fixed, a high centrality is not. **Impact × Activity** are the two terms of risk and
|
|
236
|
+
repair nothing; the other four repair.
|
|
237
|
+
|
|
238
|
+
On naming: a category is named after what it **measures**, never after where the number comes
|
|
239
|
+
from and never as a verdict. "Centrality" was rejected because it is the name of one metric
|
|
240
|
+
inside the category and describes I, A and D badly. "Importance" was rejected because it smuggles
|
|
241
|
+
a judgement, and the counter-example is in our own data: `Typography.tsx` is the most central
|
|
242
|
+
file in the front end and it is perfectly healthy.
|
|
243
|
+
|
|
244
|
+
### Everything is computed per module, never globally
|
|
245
|
+
|
|
246
|
+
A global top 100 by centrality returns 63 files from `libs/front` and 23 from `host-admin`, and
|
|
247
|
+
**three entire applications never appear**: `console` (526 files), `hr-management` (298) and
|
|
248
|
+
`career` (75), whose most central files sit at DC 43, 27 and 25. Such a list does not say "the
|
|
249
|
+
100 most critical files", it says "`libs/front` exists", and three teams never receive a line of
|
|
250
|
+
the report.
|
|
251
|
+
|
|
252
|
+
Per module also repairs two metrics: density discriminates again at small N, and `H = (R+1)/N`
|
|
253
|
+
only means something with R counted as edges internal to the module.
|
|
254
|
+
|
|
255
|
+
### The product axis is declared, not observed
|
|
256
|
+
|
|
257
|
+
The objection above has an obvious candidate answer, production telemetry. Real user monitoring
|
|
258
|
+
(RUM), Datadog's in our case, knows which screens are really used. It was not taken, and the
|
|
259
|
+
reason is not the quality of that data.
|
|
260
|
+
|
|
261
|
+
**Static analysis runs before, telemetry reports after.** RUM describes what is in production. The
|
|
262
|
+
decisions this role exists to inform are taken earlier: while the code is being written, when a
|
|
263
|
+
technical choice is made, in CI before a merge. Code that is being written now has no traffic, and
|
|
264
|
+
that is not a marginal case here:
|
|
265
|
+
|
|
266
|
+
| front end `.ts` and `.tsx` files, generated route trees excluded | |
|
|
267
|
+
| ---------------------------------------------------------------- | ----------------- |
|
|
268
|
+
| tracked in git | 10 947 |
|
|
269
|
+
| added in the last 90 days and still present | **3 115 (28.4%)** |
|
|
270
|
+
|
|
271
|
+
The structural axis cannot stand in for the missing signal either, because it does not separate
|
|
272
|
+
that population:
|
|
273
|
+
|
|
274
|
+
| | median DC | DC > 15 | p95 |
|
|
275
|
+
| ------------------------- | --------- | -------- | --- |
|
|
276
|
+
| added in the last 90 days | 4 | **4.3%** | 15 |
|
|
277
|
+
| older | 4 | **6.3%** | 18 |
|
|
278
|
+
|
|
279
|
+
So the input is a **declaration**. The team states which paths carry a critical journey, the way a
|
|
280
|
+
lint configuration states which rules a project wants. It sits in the repository, it is reviewed in
|
|
281
|
+
a pull request, it is versioned with the code it describes, and it can therefore name a feature
|
|
282
|
+
that does not exist in production yet.
|
|
283
|
+
|
|
284
|
+
```jsonc
|
|
285
|
+
{
|
|
286
|
+
"critical": [
|
|
287
|
+
{
|
|
288
|
+
"name": "contract signature",
|
|
289
|
+
"paths": ["features/contracts/**", "libs/front/api/src/bff-admin/contracts.ts"],
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
"report": { "top": 100 },
|
|
293
|
+
"activity": { "months": 12 },
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
A glob or an explicit file, matched the same way. There is deliberately no grammar for a sequence
|
|
298
|
+
of steps: the order of a journey is never used by anything downstream, so asking for it would be
|
|
299
|
+
asking teams to maintain a fact we do not read.
|
|
300
|
+
|
|
301
|
+
### A declared set is a named subgraph, not a selector
|
|
302
|
+
|
|
303
|
+
`critical` is not an input to centrality. It names a subgraph, and **every category is then
|
|
304
|
+
computable on that subgraph** rather than only on the module. That is what the declaration actually
|
|
305
|
+
buys, and for two categories it is the difference between a measurement and no measurement at all.
|
|
306
|
+
|
|
307
|
+
| category | on a declared set | what was impossible without it |
|
|
308
|
+
| --------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------- |
|
|
309
|
+
| **Impact** | the set's own incoming and outgoing coupling, as one unit | a journey's blast radius. Per-file `Ca` never sums to it |
|
|
310
|
+
| **Cohesion** | internal edge share and `H` of the set | there was no candidate unit to test other than the folder tree |
|
|
311
|
+
| **Cycles** | cycles that **cross** two declared sets, reported ahead of cycles inside one | a cycle's product meaning. Two coupled journeys is a different fact |
|
|
312
|
+
| **Duplication** | clones inside one journey against clones **between** two journeys | telling local repetition from an unextracted shared concept |
|
|
313
|
+
| **Complexity** | the same thresholds, with the declared sets ordered first | ordering by anything other than the metric itself |
|
|
314
|
+
| **Activity** | per-function churn, at 106 ms per range, on a short list that means something | the only affordable short list was the top 100 by centrality |
|
|
315
|
+
|
|
316
|
+
The shape was checked rather than assumed. Taking `console/src/features/*`, which is literally the
|
|
317
|
+
shape of a `paths` entry:
|
|
318
|
+
|
|
319
|
+
| feature | files | internal edge share | H |
|
|
320
|
+
| ------------ | ----: | ------------------: | --------: |
|
|
321
|
+
| client | 334 | 78% | 2.76 |
|
|
322
|
+
| audit | 42 | 58% | 2.02 |
|
|
323
|
+
| institutions | 35 | 61% | 1.63 |
|
|
324
|
+
| auth | 7 | **20%** | **0.57** |
|
|
325
|
+
| home | 1 | 0% | 1.00 |
|
|
326
|
+
| **total** | 419 | **74%** | mean 1.60 |
|
|
327
|
+
|
|
328
|
+
74% of the edges stay inside, and 3 of the 5 land in NDepend's 1.5 to 4.0 band for `H`, so a
|
|
329
|
+
declared set behaves like a unit and not like an arbitrary list. And `auth` is immediately a finding
|
|
330
|
+
rather than noise: 7 files with 3 internal edges against 12 outgoing is a wrapper, not a feature.
|
|
331
|
+
|
|
332
|
+
> ⚠️ **A declared set is a cover, not a partition.** The same file can serve two journeys, which is
|
|
333
|
+
> exactly the exclusive footprint result above. So modularity `Q` does **not** apply to it: `Q` is
|
|
334
|
+
> defined on a partition. What applies is per-set cohesion and conductance, which tolerate overlap.
|
|
335
|
+
> `Q` stays a module-level metric computed on the folder tree.
|
|
336
|
+
|
|
337
|
+
**Within Impact, the report is two blocks, and their order is the whole point.**
|
|
338
|
+
|
|
339
|
+
1. the files matching a declared `paths` pattern, grouped under their `name`
|
|
340
|
+
2. the top N, **minus** everything already listed in block 1
|
|
341
|
+
|
|
342
|
+
N defaults to 100 and is configurable. A file appears once, and a declared file that is also
|
|
343
|
+
central is reported as declared. Without the subtraction the declaration would buy nothing: it
|
|
344
|
+
would add names in front of a list that already contained the central files, and still never
|
|
345
|
+
surface the critical file that is not central, which is the entire gap being closed.
|
|
346
|
+
|
|
347
|
+
### The line: scope and volume are the team's, thresholds are ours
|
|
348
|
+
|
|
349
|
+
| the config carries | the config does not carry |
|
|
350
|
+
| ------------------------------------------------------------------------ | ---------------------------- |
|
|
351
|
+
| **scope**: which paths carry a journey, and the name the product uses | the value of any measurement |
|
|
352
|
+
| **volume**: how many files to report, how many months of history to read | what counts as central |
|
|
353
|
+
|
|
354
|
+
`DC > 15`, cyclomatic complexity at 30, nesting depth 4, duplication at 50 tokens over 5 lines:
|
|
355
|
+
those stay on this side. A team declares a fact about its domain and chooses how much output it
|
|
356
|
+
wants, it does not move the instrument. Those values will be calibrated from use and from the
|
|
357
|
+
feedback the role collects, which is the same commitment a preset rule carries in
|
|
358
|
+
`CONTRIBUTING.md`.
|
|
359
|
+
|
|
360
|
+
Two consequences of how it is written, rather than of what it contains:
|
|
361
|
+
|
|
362
|
+
- It is filled **with the product**, not by developers alone, which is why `name` is a field and
|
|
363
|
+
not a comment: the name is the part a product person validates, the paths are its translation.
|
|
364
|
+
- `--init` generates it like every other config this tool writes, and the declared part survives a
|
|
365
|
+
re-init. The precedent is the format role's `$sentinel.local`, where the keys a module owns are
|
|
366
|
+
**declared rather than inferred**, so that an override survives a preset bump only when someone
|
|
367
|
+
said out loud that it should.
|
|
368
|
+
|
|
369
|
+
**Why patterns and not route resolution.** Deriving the journey from the router was considered and
|
|
370
|
+
dropped: only 3 of the 9 front end applications have a `routeTree.gen.ts`, so it would work on a
|
|
371
|
+
third of the code and silently do nothing on the rest. A pattern also reads at review time without
|
|
372
|
+
running anything, and it can name a file that does not exist yet.
|
|
373
|
+
|
|
374
|
+
### What was deliberately not taken
|
|
375
|
+
|
|
376
|
+
| | reason |
|
|
377
|
+
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
378
|
+
| Sonar's maintainability rating | `debt / (30 min × LOC)` puts size in the **denominator**, so a longer file gets a better rating at constant debt. The Halstead-based MI subtracts `16.2 × ln(LOC)`, so it gets worse. They move in opposite directions with respect to size and are not two approximations of the same thing |
|
|
379
|
+
| Arcan's dynamic threshold | It is `max(system threshold, benchmark threshold)` against the Qualitas Corpus, which is Java. And it solves a problem we do not have: DC = 15 sits at the p94 in `host-admin` (5 880 files) and the p94.7 in `career` (75) |
|
|
380
|
+
| dependency-cruiser | It implements cycles, orphans and Martin's instability with the same formula, and can fail a build, but an earlier benchmark judged its values unreliable. The ACM study kept madge and graphology |
|
|
381
|
+
| Edge density as a threshold | Measured at 0.00153. It decays as 1/N, so it always reads "very sparse" at our scale. Replaced by Lattix's connectedness |
|
|
382
|
+
| A second duplication diff engine | jscpd 5.4 already has `--baseline`, `--update-baseline`, `--fail-on-new-clones` and `--baseline-from-ref origin/main` |
|
|
383
|
+
| RUM traffic as a criticality input | It answers a question this role asks earlier than production can answer it, and 28.4% of the front end is less than 90 days old. Declared paths replace it, see above. Nothing prevents comparing the two later, once there is a report to compare against |
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## 5. Published thresholds, by source
|
|
388
|
+
|
|
389
|
+
What each tool publishes, and whether it survives contact with our code.
|
|
390
|
+
|
|
391
|
+
### NDepend
|
|
392
|
+
|
|
393
|
+
| metric | threshold | our state |
|
|
394
|
+
| ----------------------------- | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
395
|
+
| cyclomatic complexity | **> 15** hard, **> 30** split | **already enforced**: `complexity: ["error", 30]` in the nest lint preset, exactly their upper bound |
|
|
396
|
+
| lines per method | > 20 hard, > 40 split | not enforced. The ticket allows 50 in TS and 250 in TSX |
|
|
397
|
+
| nesting depth | > 4 hard, > 8 extreme | not enforced. `max-depth` exists in the planning layer only |
|
|
398
|
+
| efferent coupling, type level | Ce > 50 | 11 files. The ACM study's ODC > 15 designates 228 |
|
|
399
|
+
| relational cohesion H | `(R + 1) / N`, good range **1.5 to 4.0** | not computed. Computable on the graph: R = edges internal to the module |
|
|
400
|
+
| distance from main sequence | ` | A + I − 1 | `, **> 0.7 problematic** | blocked: A is not graph-derivable |
|
|
401
|
+
| maintainability index | `MAX(0, (171 − 5.2·ln(HV) − 0.23·CC − 16.2·ln(LOC)) × 100/171)`, Visual Studio bands 0-9 / 10-19 / 20-100 | not computed |
|
|
402
|
+
|
|
403
|
+
NDepend's own warning about its own metric is the most useful line in their documentation: the
|
|
404
|
+
coefficients were calibrated on languages that predate C#, so an absolute score is not reliable.
|
|
405
|
+
Their recommendation is to **rank the worst**, to **combine with complexity, coupling and change
|
|
406
|
+
history**, and to **detect regressions** rather than impose a floor.
|
|
407
|
+
|
|
408
|
+
### Lattix
|
|
409
|
+
|
|
410
|
+
| metric | formula | threshold |
|
|
411
|
+
| -------------------------- | ---------------------------------------------------- | ------------------------------------- |
|
|
412
|
+
| System Cyclicality | **percentage** of elements in a cycle, not a count | none |
|
|
413
|
+
| Intercomponent Cyclicality | same, across components | **target: zero** |
|
|
414
|
+
| Coupling | `100 × n / (V(V−1)/2)`, n = strongly connected pairs | none |
|
|
415
|
+
| Connectedness | `100 × reachable pairs / V(V−1)` | none |
|
|
416
|
+
| System Stability | `100 − (average impact / atom count) × 100` | **must increase as the system grows** |
|
|
417
|
+
|
|
418
|
+
### ArchUnit
|
|
419
|
+
|
|
420
|
+
| metric | formula | threshold |
|
|
421
|
+
| ---------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
|
422
|
+
| DependsOn (Lakos) | transitive closure of a component, itself included | none |
|
|
423
|
+
| CCD | sum of all DependsOn | lower is better. A cycle inflates it: all members share one value |
|
|
424
|
+
| ACD, RACD | `CCD/N`, `ACD/N` | **RACD bounded by 0.6** for an acyclic graph of at least 5 components |
|
|
425
|
+
| NCCD | CCD over that of a balanced binary tree of the same size | lower is better |
|
|
426
|
+
| A, abstractness | abstract over total classes, **public only** | none |
|
|
427
|
+
| RV, ARV, GRV (Dowalil) | visible over total elements, per component then averaged then global | lower is better |
|
|
428
|
+
|
|
429
|
+
`FreezingArchRule` stores **individual violations** under version control; only new ones are
|
|
430
|
+
reported and the store shrinks by itself as violations are fixed. It ignores line numbers, so a
|
|
431
|
+
violation that moves stays known.
|
|
432
|
+
|
|
433
|
+
Note that `DependsOn` measures the **outgoing** transitive closure, which is what can break this
|
|
434
|
+
component, not what this component breaks. Blast radius is the incoming closure. Measured on our
|
|
435
|
+
project graph, its rank correlation with plain Ce is 0.819, so it adds little to a ranking.
|
|
436
|
+
|
|
437
|
+
### CodeScene
|
|
438
|
+
|
|
439
|
+
Code Health is a weighted average of 25+ factors, weighted by each file's lines of code, over
|
|
440
|
+
three bands. Factors include LCOM4 for cohesion, Brain Class and Brain Method, Bumpy Road,
|
|
441
|
+
Complex Conditional and Primitive Obsession. Brain Class is a **conjunction**: many lines, many
|
|
442
|
+
functions, and at least one Brain Method, which makes it harder to contest than a single
|
|
443
|
+
threshold. Numeric thresholds are not published on their documentation pages.
|
|
444
|
+
|
|
445
|
+
Hotspot is code health **multiplied by** change frequency, and reframes the question from "is
|
|
446
|
+
this code bad" to "is this bad code important now".
|
|
447
|
+
|
|
448
|
+
### Arcan
|
|
449
|
+
|
|
450
|
+
| smell | detection | threshold |
|
|
451
|
+
| ------------------- | ------------------------------------------------------------------------------------- | ------------------------ |
|
|
452
|
+
| Cyclic Dependency | Sedgewick-Wayne, Tarjan, or Laval-Falleri for layered cycles | algorithmic |
|
|
453
|
+
| God Component | **too many lines of code** in a container, not too many dependencies | `Max(system, benchmark)` |
|
|
454
|
+
| Hub-Like Dependency | `FanIn + FanOut` | same dynamic threshold |
|
|
455
|
+
| Unstable Dependency | a **relation between two containers**: one depends on a less stable one, with a delta | Martin's instability |
|
|
456
|
+
|
|
457
|
+
Two vocabulary collisions worth knowing: "God Component" means size here and fan-out in the ACM
|
|
458
|
+
study, and "Unstable Dependency" is a property of an edge here, not of a node.
|
|
459
|
+
|
|
460
|
+
### Google and Meta, on measuring the analyser rather than the code
|
|
461
|
+
|
|
462
|
+
| metric | formula | threshold |
|
|
463
|
+
| --------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
464
|
+
| not-useful rate (Tricorder) | `NOT USEFUL / (NOT USEFUL + PLEASE FIX + APPLY FIX)`. `PREVIEW FIX` is not in the denominator | **≥ 10% probation, > 25% may be turned off immediately** |
|
|
465
|
+
| fix rate (Infer) | findings that led to a change, over findings emitted | the same analyser with the same defects: nearly nothing fixed in batch, **over 70% fixed at code review** |
|
|
466
|
+
|
|
467
|
+
The Infer result is the one with a design consequence: the integration point weighs as much as
|
|
468
|
+
the algorithm, which separates the developer surface (a pull request comment) from the staff
|
|
469
|
+
surface (a trend) even though both run the same engine.
|
|
470
|
+
|
|
471
|
+
### Change entropy (Hassan, ICSE 2009)
|
|
472
|
+
|
|
473
|
+
`Hₙ(P) = −Σ pₖ · log₂ pₖ`, where P is, **over a period**, the probability distribution of a
|
|
474
|
+
given file being modified. Maximum entropy when every file changes equally, minimum when one
|
|
475
|
+
file absorbs everything.
|
|
476
|
+
|
|
477
|
+
Entropy is not churn. Churn counts modifications; entropy measures how **scattered** they were.
|
|
478
|
+
Ten changes in one file is zero entropy; ten changes across ten files is maximal. It is the
|
|
479
|
+
observable trace, in the history, of what modularity claims about the structure.
|
|
480
|
+
|
|
481
|
+
Three models: the Basic and Extended Code Change models give one value per period for a
|
|
482
|
+
project, and the **File Code Change model redistributes it onto individual files**, which is the one that would serve here. Validated on six large open source
|
|
483
|
+
projects, where the change complexity metrics were better predictors of fault potential than
|
|
484
|
+
prior modifications and than prior faults.
|
|
485
|
+
|
|
486
|
+
The related work carries two results that challenge the Complexity category: Herraiz et al. show
|
|
487
|
+
that most code complexity metrics correlate highly with LOC, and Moser et al. that process
|
|
488
|
+
metrics outperform code metrics as fault predictors.
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## 6. Open questions
|
|
493
|
+
|
|
494
|
+
| | what it needs |
|
|
495
|
+
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
496
|
+
| Abstractness A in TypeScript | A is a property of the code, not of the graph. ArchUnit counts public classes only, which suggests "the share of a module's exported API that is a type or an interface". Until it is settled, A and therefore D are blocked |
|
|
497
|
+
| Centrality against test coverage | a per-file coverage source, which the front end does not have today |
|
|
498
|
+
| A feedback channel on findings | without it no rule can ever prove it deserves to exist, and "it depends" simply moves up one level, to our own tools |
|
|
499
|
+
| Q and betweenness | in the ACM study, absent from the ticket, not computed here |
|
|
500
|
+
| Git history | agreed as a second phase. The cost is not the obstacle: 2 seconds per file over twelve months |
|
|
501
|
+
| Cyclomatic complexity as a measurement | enforced as a lint rule at 30, never measured as a distribution |
|
|
502
|
+
| The config's file name | `static-config.json` is the working name. This is the first config sentinel owns rather than inherits from a vendor, so there is no `.oxlintrc.json` precedent to follow, and the name is a free choice to make once |
|
|
503
|
+
| Merge and precedence | two declared journeys can match the same file, and a module config can sit under a workspace one. The rule has to be as explicit as a lint override's, and the report has to be verified for a file counted twice |
|
|
504
|
+
| A stricter threshold on a declared path | the lint override analogy invites it, and the recommendation is to refuse it in a first version: a per-path threshold makes two files with the same measurement get different verdicts, and the first thing this role has to be able to say is that a number means the same thing everywhere |
|
|
505
|
+
|
|
506
|
+
---
|
|
507
|
+
|
|
508
|
+
## 7. Sources
|
|
509
|
+
|
|
510
|
+
- Ben Khalfallah, H. _Code Health Meter: A Quantitative and Graph-Theoretic Foundation for
|
|
511
|
+
Automated Code Quality and Architecture Assessment._ ACM TOSEM 35(4), article 114, March 2026.
|
|
512
|
+
<https://doi.org/10.1145/3737670>
|
|
513
|
+
- Hassan, A. E. _Predicting Faults Using the Complexity of Code Changes._ ICSE 2009.
|
|
514
|
+
<https://sailresearch.github.io/sail-website/data/pdfs/ICSE2009_PredictingFaultsUsingTheComplexityOfCodeChanges.pdf>
|
|
515
|
+
- Sadowski, C. et al. _Tricorder: Building a Program Analysis Ecosystem._ Google.
|
|
516
|
+
<https://research.google/pubs/tricorder-building-a-program-analysis-ecosystem/>
|
|
517
|
+
- _Scaling Static Analyses at Facebook._ Meta.
|
|
518
|
+
<https://doi.org/10.1145/3338112>
|
|
519
|
+
- NDepend, code metrics: <https://www.ndepend.com/docs/code-metrics>
|
|
520
|
+
- NDepend on the maintainability index: <https://blog.ndepend.com/maintainability-index/>
|
|
521
|
+
- Lattix, metrics: <https://docs.lattix.com/lattix/userGuide/Metrics.html>
|
|
522
|
+
- ArchUnit user guide, metrics and `FreezingArchRule`:
|
|
523
|
+
<https://www.archunit.org/userguide/html/000_Index.html>
|
|
524
|
+
- Arcan, architectural smells: <https://docs.arcan.tech/latest/architectural_smells/>
|
|
525
|
+
- CodeScene, code health and hotspots: <https://codescene.io/docs/guides/technical/code-health.html>
|
|
526
|
+
- Sonar, Clean as You Code: <https://www.sonarsource.com/blog/clean-as-you-code/>
|
|
527
|
+
- Martin's package metrics: <https://en.wikipedia.org/wiki/Software_package_metrics>
|
|
@@ -1003,7 +1003,7 @@ $ sentinel --init --dry-run
|
|
|
1003
1003
|
+ "ignorePatterns": [
|
|
1004
1004
|
+ "**/dist/**",
|
|
1005
1005
|
+ "**/node_modules/**",
|
|
1006
|
-
+ "
|
|
1006
|
+
+ "coverage/**",
|
|
1007
1007
|
+ "**/jest.*.{ts,js,cjs,mjs}",
|
|
1008
1008
|
+ "**/eslint.config.{js,cjs,mjs}",
|
|
1009
1009
|
+ "**/*.timestamp*",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hublo/sentinel",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.4",
|
|
4
4
|
"description": "One CLI that guards code health across Hublo repos: shared lint/typescript/build/test presets, static & dynamic analysis, and architecture checks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -180,6 +180,7 @@
|
|
|
180
180
|
"test": "vitest run",
|
|
181
181
|
"test:watch": "vitest",
|
|
182
182
|
"test:e2e": "vitest run tests/e2e",
|
|
183
|
+
"test:coverage": "vitest run --coverage --testTimeout=60000",
|
|
183
184
|
"cli": "node --import tsx bin/sentinel.ts",
|
|
184
185
|
"docs:traces": "pnpm build && bash scripts/gen-traces.sh",
|
|
185
186
|
"docs:traces:check": "pnpm build && bash scripts/gen-traces.sh --check",
|