@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.
@@ -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 | escomplex parses **67.4%** of `network`, and a module-level `MI` computed over two thirds of its files is not that module's `MI`. Three ways out: publish it with its coverage beside it, find whether one node type explains the 708 internal failures, or recompute Halstead on `oxc-parser`, which is already a direct dependency and parses everything the lint role parses |
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
- ## 9. Sources
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.