@hublo/sentinel 1.5.0-alpha.2 → 1.5.0-alpha.21
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 +8 -6
- package/dist/{chunk-AHA7LGGA.js → chunk-AFM4AJHA.js} +1934 -254
- package/dist/{chunk-Z7L4FGKP.js → chunk-AMCGMLPS.js} +2 -2
- package/dist/chunk-AYEOGJQR.js +169 -0
- package/dist/chunk-AYEOGJQR.js.map +1 -0
- package/dist/{chunk-E3MYVYEZ.js → chunk-KLEXJFJ6.js} +1 -1
- package/dist/{chunk-KMKQDGI6.js → chunk-YQRDKWSH.js} +31 -2
- package/dist/chunk-YQRDKWSH.js.map +1 -0
- package/dist/index.js +4 -4
- package/dist/roles/static-analysis/config.d.ts +3 -3
- package/dist/roles/static-analysis/config.js +1 -1
- package/dist/roles/test/setup/jest-parity.js +2 -2
- package/dist/roles/test/setup/workspace-entry.js +2 -2
- package/dist/{validate-STUK2AGG.js → validate-AWWMBSH5.js} +2 -2
- package/docs/static-analysis-study.md +416 -2
- package/docs/typescript-traces.md +157 -4
- package/package.json +20 -2
- package/dist/chunk-GJGZRXM3.js +0 -92
- package/dist/chunk-GJGZRXM3.js.map +0 -1
- package/dist/chunk-KMKQDGI6.js.map +0 -1
- /package/dist/{chunk-Z7L4FGKP.js.map → chunk-AMCGMLPS.js.map} +0 -0
|
@@ -157,6 +157,29 @@ to centrality, ranking all 12 372 files twice:
|
|
|
157
157
|
**Rank correlation DC against LOC: 0.388.** Far from 1, so the two rankings designate different
|
|
158
158
|
files and centrality carries information size does not. The criticism does not bite here.
|
|
159
159
|
|
|
160
|
+
### What centrality is evidence FOR, and where that evidence stops
|
|
161
|
+
|
|
162
|
+
Two measurements above say what centrality is NOT: not file size in disguise, not a proxy for change
|
|
163
|
+
frequency. Neither says what it is good for, and the literature does.
|
|
164
|
+
|
|
165
|
+
| source | what it establishes |
|
|
166
|
+
| ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
167
|
+
| Zimmermann and Nagappan, ICSE 2008, on Windows Server 2003 | models built from network measures reach a recall **10 percentage points** higher than models built from complexity metrics, and network measures identified **60%** of the binaries the developers themselves called critical, **twice** what complexity metrics found |
|
|
168
|
+
| Tosun, Turhan and Bener, PROMISE 2009, five systems (three small embedded, two Eclipse versions) | network measures are important indicators of defective modules for large and complex systems, **and have no significant effect on small-scale projects** |
|
|
169
|
+
|
|
170
|
+
⛔ The second one is a boundary on everything this role can claim, so it is written here rather than
|
|
171
|
+
left implicit. Centrality's predictive value is demonstrated on LARGE systems. A module of forty
|
|
172
|
+
files gets a `DC` figure that means exactly what it says about its graph and nothing at all about
|
|
173
|
+
where its defects are. `host-admin` at 5 880 files sits on one side of that line; most of
|
|
174
|
+
`libs/nest` sits on the other. It becomes the `caveat` of the centrality help entry, unchanged.
|
|
175
|
+
|
|
176
|
+
And it settles what the number is FOR, which is a question the role has to answer before it prints
|
|
177
|
+
anything. Test impact analysis selects: given this diff, which tests can catch a defect in it.
|
|
178
|
+
Centrality is a property of the code and knows nothing about a diff, so it cannot select. What it can
|
|
179
|
+
do is ORDER: which tests to run first, and where strengthening tests pays. The two compose, and the
|
|
180
|
+
composition has a sharp consequence worth stating: a highly central file is precisely the one where
|
|
181
|
+
impact-based selection saves nothing, because changing it reaches almost everything.
|
|
182
|
+
|
|
160
183
|
### Centrality is not a proxy for change frequency
|
|
161
184
|
|
|
162
185
|
The hypothesis was that a central file, being heavily used, is frequently changed, which would
|
|
@@ -762,15 +785,406 @@ The two are complementary as long as every figure carries its unit.
|
|
|
762
785
|
| Merge and precedence | **settled.** The project wins; declared facts are added, `overrides` replaces and every path it sets is printed as `(module)`. A file matched by two declared journeys is still to be verified in the report |
|
|
763
786
|
| 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 |
|
|
764
787
|
| Dead code inside a library | `libs/front/components` reports **30.3%** of its lines dead, and no internal declaration fixes it: the package declares no `main`, `module`, `types`, `exports` or `files`, it has no barrel, and consumers import deep paths through a tsconfig alias (`@front/components/data-display/badge/Badge`). Reachability inside a library is therefore meaningless at module scope. Either the pass runs at workspace scope for libraries, or the metric is marked not applicable to them |
|
|
765
|
-
| Halstead and MI on nest |
|
|
788
|
+
| Halstead and MI on nest | **settled: both leave, and cognitive complexity takes their place.** The third way out was written and measured; see section 9 |
|
|
789
|
+
|
|
790
|
+
---
|
|
791
|
+
|
|
792
|
+
## 9. The two scores: what is settled, what is still ours to choose
|
|
793
|
+
|
|
794
|
+
Section 8 left `MI` open on escomplex's coverage. Working that question through replaced it. This
|
|
795
|
+
section records the model that came out, every step of it measured, and marks plainly what is
|
|
796
|
+
still a decision rather than a finding.
|
|
797
|
+
|
|
798
|
+
### 9.1 Halstead and `MI` leave, and the reason is not the code
|
|
799
|
+
|
|
800
|
+
The third way out of section 8, recomputing Halstead on `oxc-parser`, was written and run against
|
|
801
|
+
escomplex over the 443 files of `apps/front/console` that it reads:
|
|
802
|
+
|
|
803
|
+
| count | identical |
|
|
804
|
+
| ---------------------- | --------: |
|
|
805
|
+
| n1, distinct operators | 93.9% |
|
|
806
|
+
| N1, total operators | 82.8% |
|
|
807
|
+
| n2, distinct operands | 74.3% |
|
|
808
|
+
| N2, total operands | 26.0% |
|
|
809
|
+
| logical SLOC | 42.7% |
|
|
810
|
+
|
|
811
|
+
Five causes were found by measuring and closed, each worth tens of points: `ignoreKeys`, walking
|
|
812
|
+
INTO `TS*` nodes rather than skipping their subtree, suppressing annotations only inside a
|
|
813
|
+
parameter list, one statement per function or class scope, and the implicit return of an
|
|
814
|
+
expression-bodied arrow. The rest is a long tail of single-rule differences in both directions.
|
|
815
|
+
|
|
816
|
+
⛔ **The gap cannot be closed, because the target is not defined.**
|
|
817
|
+
|
|
818
|
+
- **IBM publishes the seven formulas and not the classification.** Its documentation says the
|
|
819
|
+
measures come directly from the operators and operands, and defines neither.
|
|
820
|
+
- **The ecosystem is one implementation.** `code-complexity`, `complexity-report` and `plato` all
|
|
821
|
+
depend on escomplex; `typhonjs-escomplex` is a fork of it.
|
|
822
|
+
- **The one independent implementation disagrees.** `ts-complex`, built on the TypeScript
|
|
823
|
+
compiler, agrees with escomplex on operands 8 fixtures out of 12 and on operators **0 out of
|
|
824
|
+
12**: it counts neither `[]` nor `{}` nor `()`. On `return [1, 2, 3]` one says `n1 = 4`, the
|
|
825
|
+
other `n1 = 1`. On the 8 agreeing fixtures ours matches both.
|
|
826
|
+
- ⚠️ **And `ts-complex` cannot be the reference anyway.** It reads 3 683 files here with zero
|
|
827
|
+
failures, which looks better than escomplex until the instrument is checked: it runs on
|
|
828
|
+
TypeScript 2.9.2 and, on `satisfies`, `??` and `?.`, raises nothing and returns operand lists
|
|
829
|
+
containing an empty string. Silent degradation is worse than a throw.
|
|
830
|
+
- **A check needing no tool was tried and failed.** Halstead's length equation does not
|
|
831
|
+
discriminate: swapping operators and operands entirely leaves the correlation at 0.9222,
|
|
832
|
+
unchanged, because it is symmetric in `n1` and `n2`. It detects only total randomness.
|
|
833
|
+
|
|
834
|
+
`src/roles/static-analysis/complexity/halstead.ts` stays in the tree, imported by nothing, with
|
|
835
|
+
its figures in its header: the record of a path walked far enough to know why it is not taken.
|
|
836
|
+
|
|
837
|
+
### 9.2 Cognitive complexity takes the place
|
|
838
|
+
|
|
839
|
+
| | Halstead and `MI` | cognitive complexity |
|
|
840
|
+
| -------------------------- | ------------------------------------ | --------------------------------- |
|
|
841
|
+
| specification of the count | **none** published for TypeScript | Campbell 2018, an exact algorithm |
|
|
842
|
+
| reference implementation | two, disagreeing 12 times out of 12 | `eslint-plugin-sonarjs`, by Sonar |
|
|
843
|
+
| reads our code | 61% to 99% depending on the family | **100%**, 0 fatal errors |
|
|
844
|
+
| published threshold | none transferable on the 0-171 scale | **15**, rule S3776 |
|
|
845
|
+
| how ours gets validated | hand-counted fixtures only | function by function, as `CC` was |
|
|
846
|
+
|
|
847
|
+
Sonar's own implementation, run over four modules with its threshold lowered to 0 so that every
|
|
848
|
+
function is reported: sentinel 752 functions and 41 above 15, network 1 701 and 19, recruitment
|
|
849
|
+
1 050 and 6, console 510 and 0. Zero fatal errors on 4 572 files, where escomplex fails on 37.9%
|
|
850
|
+
of `network` and 39.0% of `recruitment`. The worst module is this package.
|
|
851
|
+
|
|
852
|
+
It is absent from oxlint 1.77, so it is written once here and used twice. See 9.5.
|
|
853
|
+
|
|
854
|
+
### 9.3 The maintainability figure
|
|
855
|
+
|
|
856
|
+
**Which indicators are in it.** Each at its published threshold, and the ranking is by how often
|
|
857
|
+
each one fires per 100 files, which is measured rather than judged:
|
|
858
|
+
|
|
859
|
+
| indicator | published threshold | per 100 files |
|
|
860
|
+
| ---------------------- | ------------------: | ------------: |
|
|
861
|
+
| `max-depth` | 4 | 0.6 |
|
|
862
|
+
| `max-classes-per-file` | 1 | 1.8 |
|
|
863
|
+
| `complexity` | 20 | 3.7 |
|
|
864
|
+
| cognitive complexity | 15, S3776 | 6.5 |
|
|
865
|
+
| `max-lines` | 300 | 7.5 |
|
|
866
|
+
| `max-params` | 3 | 9.6 |
|
|
867
|
+
| `max-nested-callbacks` | 10 | 0 |
|
|
868
|
+
|
|
869
|
+
**Which are out, and why.** There is a break in the data, a factor of 4.5, not a line we drew:
|
|
870
|
+
|
|
871
|
+
| kept as a descriptor | per 100 files |
|
|
872
|
+
| ------------------------ | ------------: |
|
|
873
|
+
| `max-statements` | 42.9 |
|
|
874
|
+
| `max-lines-per-function` | 46.9 |
|
|
875
|
+
|
|
876
|
+
They fire on nearly one file in two, so they would fire on nearly every pull request. A gate that
|
|
877
|
+
always fires is noise. They stay in the report as descriptors and out of the figure.
|
|
878
|
+
|
|
879
|
+
⚠️ **Rarity is not severity.** It says what _can_ be a gate, not what is serious. Ordering the
|
|
880
|
+
seven that remain still needs one argument per indicator, and that is open; see 9.6.
|
|
881
|
+
|
|
882
|
+
**The computation.**
|
|
883
|
+
|
|
884
|
+
```
|
|
885
|
+
per file breaches_f = indicators above their published threshold, in that file
|
|
886
|
+
→ names the worst file, and counts the clean ones
|
|
887
|
+
the figure rate = Σ breaches / files × 100
|
|
888
|
+
MI = 1 / (1 + rate/10) 0 to 1, higher is better
|
|
889
|
+
beside it the share of files with no breach at all
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
Measured: console 0.651, components 0.390, recruitment 0.237, sentinel 0.234. Clean files: 95%,
|
|
893
|
+
87%, 74%, 82%.
|
|
894
|
+
|
|
895
|
+
⛔ **Three things the measurement forced, each against an idea that looked better:**
|
|
896
|
+
|
|
897
|
+
- **Equal weights.** A coefficient fitted to today's distribution expires when the distribution
|
|
898
|
+
moves, which is an argument for no weights rather than for smarter ones.
|
|
899
|
+
- **A denominator is unavoidable.** `1/Σ` ranks by module size, and all four modules land between
|
|
900
|
+
0.0005 and 0.0042, unreadable. `1/log(1+Σ)` does not fix it, it compresses it: a factor 1.4
|
|
901
|
+
between best and worst. No monotone transform of a raw count escapes size; only dividing by one.
|
|
902
|
+
- **Never the mean of a per-file score.** 74% to 95% of files are clean, so the mean of `1/(1+n)`
|
|
903
|
+
saturates between 0.86 and 0.97, a factor 1.13, and the median is 1.000 everywhere. Jensen. The
|
|
904
|
+
per-file computation is kept for what it alone gives: the worst file, named, and the share that
|
|
905
|
+
is clean.
|
|
906
|
+
|
|
907
|
+
⚠️ The `/10` is a display scale, chosen here and arbitrary. It changes the spread, never the
|
|
908
|
+
order, and it is to be replaced; see 9.6. And sentinel at 0.234 and recruitment at 0.240 are
|
|
909
|
+
indistinguishable, not ranked.
|
|
910
|
+
|
|
911
|
+
### 9.4 Structure is not a score
|
|
912
|
+
|
|
913
|
+
Each of these says its own thing, and they are reported side by side. Summing them would mean
|
|
914
|
+
inventing both a scale and a direction, which is exactly what 9.3 eliminated.
|
|
915
|
+
|
|
916
|
+
| metric | what it says | why it cannot be summed |
|
|
917
|
+
| --------------------- | ---------------------------------------- | --------------------------------------------------------- |
|
|
918
|
+
| cycles | files importing each other in a circle | 0 expected, **size-independent**: the only summable one |
|
|
919
|
+
| `Q` + community count | the quality of the assignment | high is not good: a module split into islands scores high |
|
|
920
|
+
| `Ca` / `Ce` / `I` | coupling, per file, the 10 most imported | Martin defined them for a component, not a file |
|
|
921
|
+
| `DC` | centrality, beside the raw count | normalised by `n − 1`, so incomparable between modules |
|
|
922
|
+
| density | a web or a tree | 0.0216 at 101 files, 0.0005 at 4 380: size, not structure |
|
|
923
|
+
|
|
924
|
+
### 9.5 Where a rule lives: gate or gauge
|
|
925
|
+
|
|
926
|
+
The same algorithm is written once and consumed twice, and the split is forced by adoption rather
|
|
927
|
+
than preferred.
|
|
928
|
+
|
|
929
|
+
| | the lint plugin | the static-analysis role |
|
|
930
|
+
| ------------ | --------------------------------------- | ------------------------------------------- |
|
|
931
|
+
| what it is | a **gate**: it refuses | a **gauge**: it measures |
|
|
932
|
+
| configurable | yes, a team may derogate | **no**, or a number stops meaning one thing |
|
|
933
|
+
| threshold | whatever the preset sets | the published one, identical everywhere |
|
|
934
|
+
| reaches | **52 modules of 198** that adopted lint | **198**, it reads the oxc tree |
|
|
935
|
+
|
|
936
|
+
A score sourced from the plugin would exist on a quarter of the repository and be incomparable
|
|
937
|
+
between two modules that configured the rules differently. Hence the gauge, always present.
|
|
938
|
+
|
|
939
|
+
**The role never runs the module's lint.** It writes its own config to a tmpdir and passes it to
|
|
940
|
+
oxlint, exactly as `tool-config.ts` already does for jscpd. Verified: a config outside the module,
|
|
941
|
+
with `jsPlugins` naming a plugin by path, lints 523 files and leaves `.oxlintrc.json` untouched.
|
|
942
|
+
So no new setting reaches a module's configuration, and the gauge's thresholds live in a file the
|
|
943
|
+
module never sees, which is what makes them non-negotiable.
|
|
944
|
+
|
|
945
|
+
> ⚠️ Gate and gauge will disagree out loud, and the report must say so. `complexity` is published
|
|
946
|
+
> at 20 and the `nest` preset refuses at 30, so a nest developer will read breaches beside a green
|
|
947
|
+
> lint. That is correct, not a defect: the figure compares modules, the rule refuses code. Each
|
|
948
|
+
> number is printed with the line that produced it, under "measured with".
|
|
949
|
+
|
|
950
|
+
### 9.6 The gate is on the diff, because on a module it saturates
|
|
951
|
+
|
|
952
|
+
A worst-breach rating needs no weights and no denominator, which is why it was tried. Measured
|
|
953
|
+
both ways:
|
|
954
|
+
|
|
955
|
+
| | the whole module | one commit |
|
|
956
|
+
| --- | --------------------- | ---------: |
|
|
957
|
+
| A | — | 39 (85%) |
|
|
958
|
+
| C | — | 6 (13%) |
|
|
959
|
+
| D | console | 0 |
|
|
960
|
+
| E | sentinel, recruitment | 1 (2%) |
|
|
961
|
+
|
|
962
|
+
On a module it saturates: at 1 500 to 3 700 functions, something crosses any per-function line.
|
|
963
|
+
On a diff it discriminates, and the shape suits it: over 300 first-parent commits of 90 days, 18%
|
|
964
|
+
touch no production file at all and the median touches **4**.
|
|
965
|
+
|
|
966
|
+
> The 46-commit figure reads the files' **current** content, not their content at the commit, so
|
|
967
|
+
> it answers "would this change be flagged today" rather than "did it introduce the defect".
|
|
968
|
+
|
|
969
|
+
### 9.7 What is still a decision
|
|
970
|
+
|
|
971
|
+
| open | what it needs |
|
|
972
|
+
| ---------------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
973
|
+
| the `/10` in the `MI` | the distribution over the 198 modules. It changes the spread, never the order |
|
|
974
|
+
| the criticality order of the seven | one argument per indicator. Rarity rules two out, it does not rank the rest |
|
|
975
|
+
| the overhead line of the SQL study | the harness rebuilt. #90 corrects the attribution and withdraws the 71%, which was never a ratio |
|
|
976
|
+
| the workspace index for libraries | built and measured in 10.4 and 10.5. Nothing open here any more |
|
|
977
|
+
| a feedback channel on findings | unchanged from section 8, and it is what would let any of this be proven wrong |
|
|
978
|
+
|
|
979
|
+
The one measurement that unblocks the first two is the same: **the eight indicators over all 198
|
|
980
|
+
modules, once.** Until then the figures in this section come from four modules and are quoted as
|
|
981
|
+
such.
|
|
982
|
+
|
|
983
|
+
---
|
|
984
|
+
|
|
985
|
+
## 10. What the perimeter is, and what a figure refuses to answer
|
|
986
|
+
|
|
987
|
+
Section 9 settled the two scores. Building them raised a different question four times in one
|
|
988
|
+
evening, always the same one under a new coat: **on which files does a figure speak, and what is
|
|
989
|
+
it entitled to conclude about them?** Every answer below is measured, and two of them reverse a
|
|
990
|
+
decision taken earlier in this document.
|
|
991
|
+
|
|
992
|
+
### 10.1 The measured perimeter is a PLACE, not a list of patterns
|
|
993
|
+
|
|
994
|
+
What is measured is `src`, or the module itself when it has none.
|
|
995
|
+
|
|
996
|
+
| over the 208 modules with a `src` | files |
|
|
997
|
+
| --------------------------------- | ------: |
|
|
998
|
+
| product files under `src` | 30 008 |
|
|
999
|
+
| outside it | 4 004 |
|
|
1000
|
+
| of those, tooling configuration | **205** |
|
|
1001
|
+
|
|
1002
|
+
The rest outside `src` is mostly `tests/doubles/`. One rule answers, identically for every pass,
|
|
1003
|
+
the three questions a reader actually asks: no, your `jest.config.ts` is not measured; no, your
|
|
1004
|
+
tests are not; no, nothing outside `src` is. A list of 24 exclusion patterns answered the same
|
|
1005
|
+
questions one argument at a time, and left the configs in.
|
|
1006
|
+
|
|
1007
|
+
⚠️ The fallback is not a detail: **232 of the 440 nx projects have no `src` at all**, mostly cloud
|
|
1008
|
+
lambdas. Measuring nothing there would be a silence that reads as a pass.
|
|
1009
|
+
|
|
1010
|
+
### 10.2 Being measured and being a USER are two different roles
|
|
1011
|
+
|
|
1012
|
+
⭐ **A file outside the perimeter is never counted in a figure, and still proves that what it
|
|
1013
|
+
imports is alive.** A test is not measured; a test that imports a file shows the file is used.
|
|
1014
|
+
|
|
1015
|
+
The measurement that forced it, on `libs/nest/auth`: **11 production files are imported only from a
|
|
1016
|
+
test file**, `auth.module.ts` among them, and **47 projects depend on that module**. Treating "not
|
|
1017
|
+
measured" as "does not exist" turns all 11 into dead code.
|
|
1018
|
+
|
|
1019
|
+
So reachability reads imports from everywhere, and only the population reported on is bounded.
|
|
1020
|
+
The same holds for a configuration file: it is never measured, and an import from it still counts.
|
|
1021
|
+
|
|
1022
|
+
⚠️ And a file that **only** tests import is not dead either, it is _used only by tests_: a third
|
|
1023
|
+
category with its own line. Measured: 6 such entry points on `auth`, **39 on `libs/front/components`**
|
|
1024
|
+
(`CalendarHeader`, `MonthCalendar`, `Cell`).
|
|
1025
|
+
|
|
1026
|
+
### 10.3 A figure nothing can reach is not published
|
|
1027
|
+
|
|
1028
|
+
Two of the four original passes answer a question of the form "does anything reach this?", and both
|
|
1029
|
+
become WRONG rather than partial when a file cannot be parsed. Measured on `apps/front/hr-management`,
|
|
1030
|
+
232 product `.ts`/`.js` beside 252 `.svelte`:
|
|
1031
|
+
|
|
1032
|
+
| what was reported | what it was worth |
|
|
1033
|
+
| ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
1034
|
+
| 161 edges, 124 communities, 163 entry points over 232 files | dust, not an application |
|
|
1035
|
+
| 33 files reported unused | **41 of the 232 are imported only from a `.svelte` file**: `debounce.ts`, `clickOutside.ts`, `isMobile.ts`, `modals.ts` |
|
|
1036
|
+
|
|
1037
|
+
So the graph and the dead-code figures are suppressed for such a module, and the drawing with them:
|
|
1038
|
+
figures carry their reserve in the report, an image carries none. Duplication and complexity stay,
|
|
1039
|
+
because each value they produce is true of the file it was measured on.
|
|
1040
|
+
|
|
1041
|
+
⛔ **By language, not by a share.** A cut on the unreadable proportion would be a threshold nobody
|
|
1042
|
+
can source, and the defect does not arrive gradually: one unreadable component disconnects the
|
|
1043
|
+
helpers only it imports. Across the repository this touches **2 modules of 175**: `libs/front/ui`
|
|
1044
|
+
(73% unreadable) and `apps/front/hr-management` (52%).
|
|
1045
|
+
|
|
1046
|
+
### 10.4 For a library, reachability is decided outside the module
|
|
1047
|
+
|
|
1048
|
+
This reverses section 7's assumption that one module's graph can answer its own dead code.
|
|
1049
|
+
|
|
1050
|
+
An application has an entry point: something runs, and what it cannot reach is dead. A library has
|
|
1051
|
+
none. Its consumers are outside, and the role's graph stops at the module edge on purpose. So the
|
|
1052
|
+
figure does not measure dead code there, it measures "the module does not import itself", which is
|
|
1053
|
+
true of every library.
|
|
1054
|
+
|
|
1055
|
+
| `libs/nest/auth`, 100 production files | |
|
|
1056
|
+
| -------------------------------------- | -----: |
|
|
1057
|
+
| reported dead by the entry-point walk | **87** |
|
|
1058
|
+
| `Ca = 0` inside the module graph | 29 |
|
|
1059
|
+
| of those, imported from another module | 17 |
|
|
1060
|
+
| **imported by nobody, anywhere** | **12** |
|
|
1061
|
+
|
|
1062
|
+
⛔ **An index, real or synthetic, does not rescue it.** On `libs/front/components`, 432 distinct
|
|
1063
|
+
specifiers are used across the repository and **4 go through a barrel, 428 enter by a deep path**.
|
|
1064
|
+
The barrel exists and is bypassed 99 times out of 100, so a synthetic index would have to re-export
|
|
1065
|
+
everything and the figure would read **0% by construction**. Shape of the 164 libraries with a
|
|
1066
|
+
`src`: wildcard alias only **75 (46%)**, barrel and wildcard **71 (43%)**, barrel only **3 (2%)**,
|
|
1067
|
+
no alias of their own 15 (9%).
|
|
1068
|
+
|
|
1069
|
+
So the decision is to extend `Ca` past the module edge for a library, and only for a library: an
|
|
1070
|
+
application is imported by nothing, so its outside `Ca` is zero by construction, and 39 of its 44
|
|
1071
|
+
already declare what runs.
|
|
1072
|
+
|
|
1073
|
+
**The sentence of principle, written rather than slipped in.** For this one figure, and only this
|
|
1074
|
+
one, the role reads outside the module. Everywhere else the module edge is the boundary and that is
|
|
1075
|
+
deliberate: a figure comparable between modules must be computed the same way in each. Here the
|
|
1076
|
+
question is _who imports this_, the answer is by definition elsewhere, and refusing to look does not
|
|
1077
|
+
make the figure local, it makes it wrong. Measured on `libs/nest/auth`: 87 against 12.
|
|
1078
|
+
|
|
1079
|
+
**Built, and what changed.** The walk is kept and **seeded** rather than replaced, so it keeps the
|
|
1080
|
+
one thing it does that a degree count cannot: transitivity, a file reachable only through a dead one
|
|
1081
|
+
stays dead. Its entry points become the module's declared patterns plus the files another module
|
|
1082
|
+
imports.
|
|
1083
|
+
|
|
1084
|
+
| module | unused files | dead lines | used only by tests |
|
|
1085
|
+
| ----------------------- | -----------: | -------------: | -----------------: |
|
|
1086
|
+
| `libs/nest/auth` | **87 → 20** | 40,1 % → 4,9 % | 6 |
|
|
1087
|
+
| `libs/front/components` | **362 → 10** | → 1,4 % | **39** |
|
|
1088
|
+
| `recruitment`, an app | 10 → 10 | → 1,1 % | - |
|
|
1089
|
+
| `console`, an app | 2 → 2 | → 0,8 % | - |
|
|
1090
|
+
|
|
1091
|
+
The two libraries move and the two applications do not, which is the shape the measurement
|
|
1092
|
+
predicted: an application is imported by nothing, so its outside `Ca` is zero by construction.
|
|
1093
|
+
|
|
1094
|
+
⭐ **And a third state appeared, which neither of the two could hold.** A file no production code
|
|
1095
|
+
imports but another module's **specs** do is not dead, and not plainly alive either: it is alive for
|
|
1096
|
+
the tests alone. `usedOnlyByTests` is its own row, 6 files on `auth` and **39** on
|
|
1097
|
+
`libs/front/components`, among them `CalendarHeader`, `MonthCalendar` and `Cell`.
|
|
1098
|
+
|
|
1099
|
+
⚠️ The role got **faster**, not slower: `auth` runs in 529 ms against 873 ms before the index
|
|
1100
|
+
existed, because the walk has far less to report.
|
|
1101
|
+
|
|
1102
|
+
### 10.5 The index: the cost is I/O, so the answer is amortisation and not a data structure
|
|
1103
|
+
|
|
1104
|
+
Finding a library's consumers costs 2 to 5 seconds with `git grep`, against 873 ms for the whole
|
|
1105
|
+
role on `auth`. The reflex is to reach for a better structure. Measured, that is the wrong end:
|
|
1106
|
+
|
|
1107
|
+
| | time |
|
|
1108
|
+
| ---------------------------------------------------------- | -----------: |
|
|
1109
|
+
| enumerate the 39 308 files (`git ls-files`) | **56 ms** |
|
|
1110
|
+
| `git grep` for a pattern that matches **nothing** | **8 994 ms** |
|
|
1111
|
+
| `git grep` for the real pattern, 2 708 lines found | 9 343 ms |
|
|
1112
|
+
| `git grep` for **every** alias at once, 22 303 lines found | 9 349 ms |
|
|
1113
|
+
| build the in-memory index from that output, 5 089 entries | **13 ms** |
|
|
1114
|
+
|
|
1115
|
+
A pattern that finds nothing costs what a pattern finding 22 303 lines costs: the price is reading
|
|
1116
|
+
the disk. A balanced tree would accelerate an insertion that already takes 13 ms, and a lookup that
|
|
1117
|
+
is free.
|
|
1118
|
+
|
|
1119
|
+
⭐ **The lever is in the last row: one scan serves every library.** The index is built once, not per
|
|
1120
|
+
module.
|
|
1121
|
+
|
|
1122
|
+
| | |
|
|
1123
|
+
| -------------------------------- | --------: |
|
|
1124
|
+
| today, per library | ~4 900 ms |
|
|
1125
|
+
| one scan, the whole index | 9 024 ms |
|
|
1126
|
+
| amortised over the 164 libraries | **55 ms** |
|
|
1127
|
+
|
|
1128
|
+
Thirteen minutes become nine seconds. Restricting the scan to the consumers' `src` saves nothing
|
|
1129
|
+
(4 884 against 4 911 ms) and loses 5 real consumers, so it is not done.
|
|
1130
|
+
|
|
1131
|
+
**Where the cache lives: `node_modules/.cache/sentinel/`.** Not per module, because the index
|
|
1132
|
+
describes the workspace and 164 copies would be 164 different staleness states. Not inside the
|
|
1133
|
+
installed package, because that is the tool's directory rather than the workspace's, and an install
|
|
1134
|
+
replaces it.
|
|
1135
|
+
|
|
1136
|
+
The convention already exists in this repository: `node_modules/.cache` holds 208 MB and six tools
|
|
1137
|
+
use it, `prettier` and `storybook` among them. Three consequences come free: nothing to add to
|
|
1138
|
+
`.gitignore` since `node_modules` is already ignored, one cache per workspace which is the scope the
|
|
1139
|
+
data has, and a fresh install clearing it.
|
|
1140
|
+
|
|
1141
|
+
⚠️ **That it survives an install is measured, not assumed**: `node_modules/.cache` was last written
|
|
1142
|
+
on 2026-09-17 and `.modules.yaml`, which pnpm rewrites on every install, carries 2026-09-27. An
|
|
1143
|
+
install ran ten days later and the cache is still there. pnpm removes only what it tracks.
|
|
1144
|
+
|
|
1145
|
+
And if the workspace ever switches to a hoisted linker: no collision is possible, since an npm
|
|
1146
|
+
package name cannot begin with a dot; the index itself is unaffected, since it maps specifiers to
|
|
1147
|
+
this repository's own sources and never reads `node_modules`; and the switch rebuilds `node_modules`
|
|
1148
|
+
wholesale, so the cache is lost once and rebuilt in nine seconds, which is the correct invalidation
|
|
1149
|
+
for a change that can change the installed code.
|
|
1150
|
+
|
|
1151
|
+
Keyed on the git state, `HEAD` plus the uncommitted files, rather than on mtimes: a `git checkout`
|
|
1152
|
+
moves every mtime without changing a byte.
|
|
1153
|
+
|
|
1154
|
+
### 10.6 The size indicators, and three silent traps
|
|
1155
|
+
|
|
1156
|
+
The seven size rules are measured with oxlint, run from a config in a temporary directory at the
|
|
1157
|
+
threshold each rule publishes, and the module's own `.oxlintrc.json` is never read. Each trap below
|
|
1158
|
+
cost a wrong measurement before it was found, and none of them produced an error:
|
|
1159
|
+
|
|
1160
|
+
- **oxlint loads 65 rules by default.** Neither `plugins: []` nor `categories: {}` stops it. On
|
|
1161
|
+
`libs/nest/auth` it reported `no-unused-vars` and `no-useless-rename`, which are not indicators of
|
|
1162
|
+
this gauge. Every category has to be named and turned off; then exactly 8 load.
|
|
1163
|
+
- **`ignorePatterns` in the config is accepted and ignored.** Same patterns on the same module: 137
|
|
1164
|
+
files read through the config key against **111** through `--ignore-pattern`, and the worst file
|
|
1165
|
+
it produced was a 1 436-line `permissions.guard.spec.ts`. The gauge was measuring the test suite.
|
|
1166
|
+
- **A diagnostic is not a breach.** `max-nested-callbacks` fires once per nesting level, so the unit
|
|
1167
|
+
is the (file, rule) pair: counting diagnostics inflates the rate by **1.8x** at the published
|
|
1168
|
+
thresholds and **2.8x** at lower ones.
|
|
1169
|
+
|
|
1170
|
+
And `warn` rather than `error`, for the reason `tool-config.ts` already gives about jscpd's
|
|
1171
|
+
`threshold`: at `error`, oxlint exits 1 on any breach and a finding becomes indistinguishable from
|
|
1172
|
+
the binary failing. Measured: same 18 diagnostics either way, exit 1 against exit 0.
|
|
766
1173
|
|
|
767
1174
|
---
|
|
768
1175
|
|
|
769
|
-
##
|
|
1176
|
+
## 11. Sources
|
|
770
1177
|
|
|
771
1178
|
- Ben Khalfallah, H. _Code Health Meter: A Quantitative and Graph-Theoretic Foundation for
|
|
772
1179
|
Automated Code Quality and Architecture Assessment._ ACM TOSEM 35(4), article 114, March 2026.
|
|
773
1180
|
<https://doi.org/10.1145/3737670>
|
|
1181
|
+
- Zimmermann, T. and Nagappan, N. _Predicting Defects using Network Analysis on Dependency Graphs._
|
|
1182
|
+
ICSE 2008.
|
|
1183
|
+
<https://www.microsoft.com/en-us/research/publication/predicting-defects-using-network-analysis-on-dependency-graphs/>
|
|
1184
|
+
- Tosun, A., Turhan, B. and Bener, A. _Validation of network measures as indicators of defective
|
|
1185
|
+
modules in software systems._ PROMISE 2009. <https://doi.org/10.1145/1540438.1540446>
|
|
1186
|
+
- Hammant, P. _The Rise of Test Impact Analysis._ martinfowler.com, August 2017.
|
|
1187
|
+
<https://martinfowler.com/articles/rise-test-impact-analysis.html>
|
|
774
1188
|
- Hassan, A. E. _Predicting Faults Using the Complexity of Code Changes._ ICSE 2009.
|
|
775
1189
|
<https://sailresearch.github.io/sail-website/data/pdfs/ICSE2009_PredictingFaultsUsingTheComplexityOfCodeChanges.pdf>
|
|
776
1190
|
- Sadowski, C. et al. _Tricorder: Building a Program Analysis Ecosystem._ Google.
|