@hublo/sentinel 1.5.0 → 1.5.1-alpha.1

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.
@@ -17,21 +17,29 @@ pnpm install
17
17
 
18
18
  Then run `pnpm run lint`.
19
19
 
20
- `--init` with no target adopts **every role sentinel ships**, and for lint that is the
20
+ `--init` with no target on a module that has adopted nothing adopts **every role sentinel
21
+ ships** (on a module that has, it re-inits the roles it has), and for lint that is the
21
22
  intended way round: it runs the linter's autofix, then writes the format config and formats
22
- the module, so the files it generated are already in your module's style. Adopting lint
23
- ALONE (`--init --lint`) leaves you to format them yourself.
23
+ the module, so the files it generated are already in your module's style.
24
+
25
+ **Lint and format are adopted together.** `--init --lint`, `--init --format` and
26
+ `--init --lint --format` do the same thing: oxlint AND oxfmt, and the module excluded from the
27
+ root `.prettierignore`, exactly as a bare `--init` does for those two roles. Since 1.5.1; before
28
+ it, lint alone left the module to the root Prettier, which then rejected the `.oxlintrc.json`
29
+ adoption had just written.
24
30
 
25
31
  `--init` fixes what a machine can fix, once. It does not loop, and it does not edit your code
26
32
  beyond what the tools' own fixers do. What is left afterwards is real debt: see
27
33
  [Reading the numbers](#reading-the-numbers).
28
34
 
29
- > **Order matters, and `--init` with no target gets it right for you.** The linter's autofix
30
- > rewrites code, and sentinel writes JSON in its own style, so **formatting has to come after
31
- > linting**, never before. Run whole-module `--init` and it happens automatically: format runs
32
- > LAST, over everything every role wrote. If you adopt roles one at a time, run
33
- > `sentinel --init --format` after `sentinel --init --lint`, or your first `pnpm run lint`
34
- > fails at a formatting step that has nothing to do with linting.
35
+ **Previewing the source rewrite.** `--init --dry-run` says the autofix can rewrite source, without
36
+ running it. `--init --dry-run --rehearse` runs it in a throwaway copy of the module and names the
37
+ files, at the cost of the real pass (minutes on a large nest module, nearly all of it
38
+ `@nx/enforce-module-boundaries`).
39
+
40
+ > **Order matters, and `--init` gets it right for you.** The linter's autofix rewrites code,
41
+ > and sentinel writes JSON in its own style, so **formatting has to come after linting**, never
42
+ > before. Whatever targets you name, format runs LAST, over everything every role wrote.
35
43
 
36
44
  ## What `--init` does
37
45
 
@@ -150,6 +158,11 @@ a **layer above the preset**, the way the planning team did:
150
158
  Order matters, sentinel's preset first, or your layer cannot override anything. `--inspect`
151
159
  reports the layer so it stays visible.
152
160
 
161
+ A rule set directly in `.oxlintrc.json` is still reported as drift, and a layer is the place for
162
+ it. Since 1.5.1 a re-`init` no longer deletes it, if the preset does not configure that rule: it
163
+ is kept, in your order, and `--init` says so. A rule the preset does configure is dropped, as
164
+ before, and named: set in the stub, it would override the preset and the baseline both.
165
+
153
166
  Think of it as a queue rather than a fork: a rule promoted into the preset is deleted from the
154
167
  layer. It is the exact mirror of `.oxlintrc.baseline.json`, which holds rules **below** the
155
168
  preset and empties as debt is fixed, while a layer holds rules **above** it and empties as they
@@ -160,6 +173,56 @@ whatever `extends` it finds, so a layer already there survives and is respected
160
173
  that follows. Added afterwards, the autofix has already run, and nothing undoes what it
161
174
  rewrote. One team met this as twelve rewritten files.
162
175
 
176
+ ### Where a team puts its own guards
177
+
178
+ Lint is always migrated, so a guard your ESLint carried and the preset does not is yours to add,
179
+ in your own config. A re-`init` keeps all of it (since 1.5.1): your `extends` layers in their
180
+ order after sentinel's preset, and in `.oxlintrc.json` your `rules`, `plugins`, `jsPlugins`,
181
+ `settings` and `overrides`. `--init` prints one line per block it kept.
182
+
183
+ `console` keeps two rules the react preset does not configure, directly in its stub:
184
+
185
+ ```json
186
+ {
187
+ "extends": ["./node_modules/@hublo/sentinel/lint/react.json"],
188
+ "rules": {
189
+ "import/no-cycle": "error",
190
+ "oxc/no-barrel-file": ["error", { "threshold": 0 }]
191
+ }
192
+ }
193
+ ```
194
+
195
+ A local plugin and file-scoped rules, the shape a planning layer takes:
196
+
197
+ ```json
198
+ {
199
+ "extends": ["./node_modules/@hublo/sentinel/lint/react.json"],
200
+ "jsPlugins": [{ "name": "planning", "specifier": "./lint/planning-plugin.mjs" }],
201
+ "overrides": [
202
+ {
203
+ "files": ["src/modules/planning/**/*.tsx"],
204
+ "rules": { "planning/no-raw-date": "error", "max-depth": ["error", 3] }
205
+ }
206
+ ]
207
+ }
208
+ ```
209
+
210
+ Two limits, both the preset's governance:
211
+
212
+ - **An entry the preset already has is the preset's.** A rule it configures (top level or in one
213
+ of its overrides, at any severity), a `jsPlugins` entry it registers under the same name, a
214
+ `settings` key it sets: written in the stub, it would override sentinel's, so a re-`init` drops
215
+ it and names it. That includes `no-console` and `no-restricted-imports` in the react preset. To
216
+ set one of those for your module, put it in an `extends` layer after the preset (above): a
217
+ layer is reviewed as yours and `--inspect` shows it.
218
+ - **Your `overrides` blocks are kept as written**, with their `files`, and their rules go through
219
+ the same filter. Two of your blocks with the same `files` are merged into one on re-`init`; a
220
+ block is never merged into a preset override, which lives behind `extends` where the stub
221
+ cannot write. Measured on oxlint 1.77.0, the version sentinel ships: an `overrides` block in an
222
+ extended file DOES apply, and `plugins` and `jsPlugins` in the stub add to the extended ones
223
+ rather than replace them. The planning layer's notes say overrides do not cross `extends`; that
224
+ is not what 1.77.0 does.
225
+
163
226
  ## What a preset does not enforce
164
227
 
165
228
  Every unenforced rule carries a reason, readable with `sentinel --inspect --lint`:
@@ -274,7 +337,7 @@ The numbers come from oxlint's own JSON, not from parsing what it prints for hum
274
337
 
275
338
  | Symptom | Cause and fix |
276
339
  | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
277
- | `pnpm run lint` fails on **Prettier** right after adopting | you adopted lint alone, so the files sentinel generated are not in your module's format. Run `sentinel --init --format` (or your formatter) on them |
340
+ | `pnpm run lint` fails on **Prettier** right after adopting | the module was adopted by a sentinel older than 1.5.1 with lint alone, so it is still formatted by the root Prettier. Re-run `sentinel --init --lint`: it adopts format with it |
278
341
  | `Plugin 'x' not found` / `extends … does not exist` | the preset path assumes pnpm's isolated layout. Run `pnpm install` in the module; if the workspace switched to a hoisted linker, that breaks every adopted module at once |
279
342
  | A file that was never linted now reports errors | your ESLint config ignored it. `--init` carries your module's `ignores` across, but it **cannot read a computed one** (e.g. `includeIgnoreFile(gitignorePath)`) and says so when it hits one. Add what you need to `ignorePatterns` |
280
343
  | `error: Unexpected token` in a `.svelte` file | oxlint reads a `<script>` block as JavaScript unless it declares `lang="ts"`. Add it, or ignore the file |
@@ -76,6 +76,7 @@ sentinel --inspect --typescript # adoption + conformity coverage
76
76
  | `--json` | machine-readable envelope on stdout, for every verb; progress goes to stderr |
77
77
  | `--max-diagnostics <n>` | cap the diagnostics embedded per module in `--run --json` (`0` = no cap) |
78
78
  | `--dry-run` | preview what `--init` would write, changing nothing |
79
+ | `--rehearse` | with `--dry-run`: run the autofix passes in a copy and name the source they rewrite |
79
80
  | `--runner <tool>` | override the default runner |
80
81
  | `-- <tool options>` | `--run` only, one type named: everything after `--` goes to tsc itself (see below) |
81
82
 
@@ -1298,10 +1298,259 @@ $ sentinel --init --dry-run
1298
1298
  + }
1299
1299
  + }
1300
1300
  + }
1301
+ dry run: the lint autofix runs on adoption and can rewrite source, as moving to another linter does; add --rehearse to name the files (it runs the pass in a throwaway copy, as long as the real one)
1302
+ dry run: the format autofix runs on adoption and can rewrite source, as moving to another formatter does; add --rehearse to name the files (it runs the pass in a throwaway copy, as long as the real one)
1301
1303
  dry run: would ignore static-analysis-report/ in the root .gitignore
1302
1304
  # exit: 0
1303
1305
  ```
1304
1306
 
1307
+ **--init --dry-run --rehearse (runs the autofixes in a copy, names what they rewrite)**
1308
+
1309
+ ```console
1310
+ $ sentinel --init --dry-run --rehearse
1311
+ dry run: no files written
1312
+ wrote .oxlintrc.json extending ./node_modules/@hublo/sentinel/lint/nest.json (@hublo/sentinel@<version>)
1313
+ 20 rule(s) not enforced by this preset (17 jest, 1 no-barrel-files, 1 core, 1 vitest). Run --inspect --lint for each rule and why.
1314
+ run `pnpm install`, then `pnpm run lint`
1315
+
1316
+ create .oxlintrc.json
1317
+ + {
1318
+ + "extends": [
1319
+ + "./node_modules/@hublo/sentinel/lint/nest.json"
1320
+ + ],
1321
+ + "ignorePatterns": [
1322
+ + "**/dist/**",
1323
+ + "**/node_modules/**",
1324
+ + "coverage/**",
1325
+ + "**/jest.*.{ts,js,cjs,mjs}",
1326
+ + "**/eslint.config.{js,cjs,mjs}",
1327
+ + "**/*.timestamp*",
1328
+ + "**/*.d.ts"
1329
+ + ]
1330
+ + }
1331
+
1332
+ update package.json
1333
+ - { "name": "bff-admin", "dependencies": { "@nestjs/core": "^10" } }
1334
+ + {
1335
+ + "name": "bff-admin",
1336
+ + "dependencies": {
1337
+ + "@nestjs/core": "^10"
1338
+ + },
1339
+ + "scripts": {
1340
+ + "lint": "sentinel --run --lint"
1341
+ + },
1342
+ + "devDependencies": {
1343
+ + "@hublo/sentinel": "<version>"
1344
+ + },
1345
+ + "nx": {
1346
+ + "targets": {
1347
+ + "lint": {
1348
+ + "cache": true,
1349
+ + "inputs": [
1350
+ + "default",
1351
+ + "^default",
1352
+ + "{projectRoot}/.oxlintrc.json"
1353
+ + ]
1354
+ + }
1355
+ + }
1356
+ + }
1357
+ + }
1358
+ dry run: no files written
1359
+ run `pnpm install` to fetch @hublo/sentinel (added to the module devDependencies) so `extends` and the typecheck script resolve
1360
+
1361
+ update tsconfig.json
1362
+ - { "extends": "../../tsconfig.base.json", "include": ["src"] }
1363
+ + { "extends": [
1364
+ + "../../tsconfig.base.json",
1365
+ + "@hublo/sentinel/typescript/nest"
1366
+ + ], "include": ["src"] }
1367
+
1368
+ update package.json
1369
+ - { "name": "bff-admin", "dependencies": { "@nestjs/core": "^10" } }
1370
+ + {
1371
+ + "name": "bff-admin",
1372
+ + "dependencies": {
1373
+ + "@nestjs/core": "^10"
1374
+ + },
1375
+ + "scripts": {
1376
+ + "typecheck": "sentinel --run --typescript"
1377
+ + },
1378
+ + "devDependencies": {
1379
+ + "@hublo/sentinel": "<version>"
1380
+ + },
1381
+ + "nx": {
1382
+ + "targets": {
1383
+ + "typecheck": {
1384
+ + "cache": true,
1385
+ + "inputs": [
1386
+ + "default",
1387
+ + "^default",
1388
+ + "{projectRoot}/tsconfig.json"
1389
+ + ]
1390
+ + }
1391
+ + }
1392
+ + }
1393
+ + }
1394
+ build: no Vite config in this module, so there is nothing to point at sentinel. This role moves an existing config's imports; it does not introduce a bundler. If this module builds another way (38 services here build through `@nx/webpack`), the build role does not apply to it.
1395
+ test: this module has no test and no jest leftovers, so there is nothing to do here.
1396
+ dry run: no files written
1397
+ added the three `static-analysis` scripts and static-analysis.config.mjs; declare this module's entry points there, everything else comes from the role
1398
+
1399
+ create static-analysis.config.mjs
1400
+ + import { defineStaticAnalysis } from '@hublo/sentinel/static-analysis'
1401
+ +
1402
+ + export default defineStaticAnalysis({
1403
+ + root: import.meta.dirname,
1404
+ +
1405
+ + // Files that RUN although nothing imports them: they are where reachability starts. Added to
1406
+ + // the patterns sentinel ships, never replacing them.
1407
+ + entry: [],
1408
+ +
1409
+ + // ---------------------------------------------------------------------------------------------
1410
+ + // What this module can move, with the role's value today. A setting you leave out keeps that
1411
+ + // value; a setting you write REPLACES it, including inside a list. Anything you set is printed
1412
+ + // under "measured with" in the report and marked `(module)`, because two modules measured with
1413
+ + // different settings can no longer be compared.
1414
+ + //
1415
+ + // ignore 19 patterns
1416
+ + // languages javascript, typescript, jsx, tsx, css, scss
1417
+ + // duplication.minTokens 50
1418
+ + // duplication.minLines 5
1419
+ + // duplication.tests 3 patterns
1420
+ + // deadCode.minConfidence 60
1421
+ + // deadCode.categories unused-file, unused-export, unused-symbol, unused-import
1422
+ + // graph.resolution 1
1423
+ + //
1424
+ + // A list shown as a count is not copyable, and replacing one means writing back what you keep,
1425
+ + // so read the values in full with:
1426
+ + //
1427
+ + // pnpm exec sentinel --inspect --static-analysis --json
1428
+ + //
1429
+ + // duplication.kinds: only `exact` is supported: any other kind overlaps it and double-counts
1430
+ + // tools: sentinel's own passthrough to the tool underneath; a module never names the vendor
1431
+ + //
1432
+ + // overrides: {
1433
+ + // duplication: { minTokens: 100 },
1434
+ + // },
1435
+ + })
1436
+
1437
+ update package.json
1438
+ - { "name": "bff-admin", "dependencies": { "@nestjs/core": "^10" } }
1439
+ + {
1440
+ + "name": "bff-admin",
1441
+ + "dependencies": {
1442
+ + "@nestjs/core": "^10"
1443
+ + },
1444
+ + "scripts": {
1445
+ + "static-analysis": "sentinel --run --static-analysis",
1446
+ + "static-analysis:json": "sentinel --run --static-analysis --output json",
1447
+ + "static-analysis:html": "sentinel --run --static-analysis --output html"
1448
+ + },
1449
+ + "devDependencies": {
1450
+ + "@hublo/sentinel": "<version>"
1451
+ + },
1452
+ + "nx": {
1453
+ + "targets": {
1454
+ + "static-analysis": {
1455
+ + "cache": true,
1456
+ + "inputs": [
1457
+ + "default",
1458
+ + "{projectRoot}/static-analysis.config.mjs"
1459
+ + ]
1460
+ + }
1461
+ + }
1462
+ + }
1463
+ + }
1464
+ dry run: no files written
1465
+ materialized the nest format preset into .oxfmtrc.json (@hublo/sentinel@<version>). oxfmt has no `extends`, so the values live here; `sentinel --status --format` reports modules left behind by a preset change
1466
+
1467
+ create .oxfmtrc.json
1468
+ + {
1469
+ + "$sentinel": {
1470
+ + "preset": "nest",
1471
+ + "version": "<version>",
1472
+ + "local": []
1473
+ + },
1474
+ + "printWidth": 80,
1475
+ + "semi": false,
1476
+ + "singleQuote": true,
1477
+ + "trailingComma": "all",
1478
+ + "sortPackageJson": false,
1479
+ + "sortImports": {
1480
+ + "customGroups": [
1481
+ + {
1482
+ + "groupName": "workspace",
1483
+ + "elementNamePattern": [
1484
+ + "@hublo/**",
1485
+ + "@shared/**",
1486
+ + "@front/**",
1487
+ + "@network/**"
1488
+ + ]
1489
+ + }
1490
+ + ],
1491
+ + "groups": [
1492
+ + "builtin",
1493
+ + "external",
1494
+ + "workspace",
1495
+ + [
1496
+ + "parent",
1497
+ + "index"
1498
+ + ],
1499
+ + "sibling"
1500
+ + ],
1501
+ + "newlinesBetween": true,
1502
+ + "order": "asc",
1503
+ + "ignoreCase": false
1504
+ + },
1505
+ + "ignorePatterns": [
1506
+ + "**/*.toml"
1507
+ + ]
1508
+ + }
1509
+
1510
+ update package.json
1511
+ - { "name": "bff-admin", "dependencies": { "@nestjs/core": "^10" } }
1512
+ + {
1513
+ + "name": "bff-admin",
1514
+ + "dependencies": {
1515
+ + "@nestjs/core": "^10"
1516
+ + },
1517
+ + "scripts": {
1518
+ + "format": "sentinel --run --format",
1519
+ + "format:fix": "sentinel --run --format --fix"
1520
+ + },
1521
+ + "devDependencies": {
1522
+ + "@hublo/sentinel": "<version>"
1523
+ + },
1524
+ + "nx": {
1525
+ + "targets": {
1526
+ + "format": {
1527
+ + "cache": true,
1528
+ + "inputs": [
1529
+ + "default",
1530
+ + "{projectRoot}/.oxfmtrc.json"
1531
+ + ]
1532
+ + },
1533
+ + "format:fix": {
1534
+ + "cache": false
1535
+ + }
1536
+ + }
1537
+ + }
1538
+ + }
1539
+ dry run: the lint autofix cannot be rehearsed yet: its preset is not installed in this module, which is expected before the first `pnpm install`. After it, this dry-run names what it rewrites
1540
+ dry run: the format autofix would rewrite 1 file beyond the ones above: project.json
1541
+ dry run: would ignore static-analysis-report/ in the root .gitignore
1542
+ # exit: 0
1543
+ ```
1544
+
1545
+ **--rehearse without --dry-run**
1546
+
1547
+ ```console
1548
+ $ sentinel --init --rehearse
1549
+ --rehearse only applies to --init --dry-run: the real --init runs the autofix passes themselves.
1550
+ (run "sentinel --help" for usage)
1551
+ # exit: 1
1552
+ ```
1553
+
1305
1554
  **no module and no workspace root**
1306
1555
 
1307
1556
  ```console
@@ -34,7 +34,15 @@ everywhere (`--run --json` and `--inspect` answer them), and `--migrate` is refu
34
34
  | `--static-analysis` | the config stub + three scripts + nx metadata, and ignores the report directory at the repository root | measures and reports; never exits non-zero on a finding | runner, config file, resolved settings, what this module moved |
35
35
 
36
36
  `--json` works for every verb, and stdout carries **only** the envelope, so `| jq` always
37
- parses. `--dry-run` applies to `--init` and writes nothing.
37
+ parses. `--dry-run` applies to `--init` and writes nothing. Besides the config diffs, it says
38
+ that the lint autofix and the formatter run on adoption and can rewrite source, which is what
39
+ moving to another linter or formatter does. It does not run them: on a large module that costs
40
+ what the real pass costs (260 s measured on `network`).
41
+
42
+ `--init --dry-run --rehearse` runs them, and names the source files they would rewrite
43
+ (`rewrites` in `--json`, absent without `--rehearse`): both run in a throwaway copy of the
44
+ module, outside the workspace, and what changed is read back. `--rehearse` without `--dry-run`
45
+ is refused, because a real `--init` runs the passes itself.
38
46
 
39
47
  `--output <shape>` is different, and only a role that WRITES a report reads it: `console` is the
40
48
  default and writes no file, `html` is the page you open and send, `json` is for CI and for a
@@ -217,5 +225,9 @@ visible. Closing it in one action is [issue #17](https://github.com/hublo/sentin
217
225
  today it is one command per module.
218
226
 
219
227
  **Upgrading:** bump the pin, `pnpm install`, then `sentinel --init` in the module and commit
220
- what changes. `--init` is idempotent, so on a version that changed nothing relevant it reports
221
- nothing to change.
228
+ what changes. On a module that has already adopted a role, a bare `--init` re-inits only the roles
229
+ it has adopted, the ones `sentinel --inspect` reports, and names them in one line. It does not
230
+ adopt a new role on the way: adopting the test role, for instance, is a migration of its own, so
231
+ it is asked for by name (`sentinel --init --test`), or every role at once with `--init --all`.
232
+ `--init` is idempotent, so on a version that changed nothing relevant it reports nothing to
233
+ change.
@@ -135,6 +135,136 @@ majority of the migration, and only a sweep over every real module found it.
135
135
 
136
136
  ---
137
137
 
138
+ ## The `--init` matrix for 1.5.1, 29 October 2026
139
+
140
+ Six fixes, found by re-initializing the monorepo's 63 adopted modules with 1.5.1-alpha.0. Every
141
+ cell below is a test in this repo, against the built CLI unless it says unit, and every fix has a
142
+ negative control: the fix reverted in the source, the cells run again, and the cells that must
143
+ fail do. No existing cell changed: against the alpha, the test tree gains lines and loses none
144
+ (`git diff --numstat` on `tests/`: 0 deletions), in 10 new files and 3 existing ones.
145
+
146
+ ### 1. a bare `--init` re-inits the roles a module has (`init-bare-on-adopted`)
147
+
148
+ | module state | from the module | from the root, `--module` |
149
+ | ---------------------------- | -------------------------------- | ------------------------- |
150
+ | nothing adopted | every role | every role |
151
+ | typescript adopted | typescript only | typescript only |
152
+ | lint adopted | lint and format | (pairing, same rule) |
153
+ | typescript adopted, `--all` | every role | |
154
+ | typescript adopted, real run | no lint or format config written | |
155
+
156
+ ### 2. the lint baseline runs the lint script's leading commands (`init-lint-baseline-runs-script-prefix`)
157
+
158
+ | lint script | baseline |
159
+ | --------------------------------------- | ------------------------------------------ |
160
+ | `eslint .` (control) | holds `typescript/tsconfig-error` |
161
+ | `node generate.mjs && eslint .` | nothing held, the command is named |
162
+ | `node -e "process.exit(3)" && eslint .` | nothing written, says the command exited 3 |
163
+
164
+ Unit (`leading-commands`): `&&` chains only; `;`, `||` and a script that never reaches sentinel
165
+ return nothing.
166
+
167
+ ### 3. the `$sentinel` stamp follows the content (unit, `format-adoption`)
168
+
169
+ | re-init | stamp |
170
+ | --------------------------------- | ------------------------ |
171
+ | same content | kept (no diff on a bump) |
172
+ | preset changed (`base` to `nest`) | this release |
173
+ | a carried ignore pattern added | this release |
174
+
175
+ ### 4. two re-init messages (unit `build-init`, e2e `init-tools-defers-every-role`)
176
+
177
+ | case | said |
178
+ | --------------------------------------------------- | ---------------------------------------------------- |
179
+ | vite config rewritten, format adopted / not adopted | the format line, naming the rewritten files |
180
+ | vite config already adopted | no format line |
181
+ | lint config declares `tools`, `--init --typescript` | deferred, naming the lint config, tsconfig untouched |
182
+ | same module, bare `--init` | every role deferred |
183
+ | same module, `--preset node` | not deferred, the explicit preset decides |
184
+
185
+ ### 5. a deliberate import order survives the sort (`init-format-import-order-matrix`, `init-format-keeps-deliberate-import-order`)
186
+
187
+ Each file is one cell, run on a first adoption AND on a re-init, nest preset:
188
+
189
+ | signal in the file | order kept | pins |
190
+ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | -------------------------------------------------------- |
191
+ | `eslint-disable-next-line import/order` | yes | 1 |
192
+ | `eslint-disable import/order` region, then `eslint-enable` | inside yes, after it sorted | 2 |
193
+ | file-wide `/* eslint-disable import/order */` | yes | 3 |
194
+ | `eslint-disable-line import/first` | yes | 1 |
195
+ | `eslint-disable-next-line simple-import-sort/imports` | yes | 1 |
196
+ | first import: `dd-trace` (value) | yes | 1 |
197
+ | first import: `@opentelemetry/sdk-node` (value) | yes | 1 |
198
+ | first import, side effect: `dd-trace/init`, `reflect-metadata`, `source-map-support/register`, `core-js/stable`, `regenerator-runtime/runtime` | yes | 0: oxfmt 0.63.0 does not move a first side-effect import |
199
+ | side-effect `reflect-metadata` with an import the sort crosses | yes | 1 |
200
+ | two signals in one file, normal imports between them | pinned yes, the rest sorted | 2 |
201
+ | a `.tsx` file | yes | 1 |
202
+ | the tracer file of monorepo#23314 | yes | 3 |
203
+ | no signal at all (two files) | byte-identical to oxfmt alone | 0 |
204
+
205
+ And: the report names exactly the pinned files; `format:fix` then the format check keep every
206
+ order; a second re-init changes no file and adds no pin; a module that does not sort (react
207
+ preset) gets no pin. Unit (`deliberate-import-order`): a type-only import is never pinned, an
208
+ `eslint-disable` naming no order rule is ignored.
209
+
210
+ Negative control, `keepDeliberateImportOrder` disabled: 14 cells fail, the ten pinned files in
211
+ both modes, the report, `format:fix` and the tracer cells. The five first-position side-effect
212
+ cells pass without the fix, which is what the measurement above says.
213
+
214
+ ### 6. a re-init keeps what a team added (`init-keeps-team-config-matrix`, `init-keeps-team-lint-config`)
215
+
216
+ | block in the module's config | after re-init |
217
+ | -------------------------------------------------------------------------------- | ---------------------------------------------------------- |
218
+ | a rule outside the preset | kept |
219
+ | a rule the preset configures | dropped, named |
220
+ | `jsPlugins`: a local path and a package entry | kept |
221
+ | `jsPlugins` under a name the preset registers | dropped, named |
222
+ | `overrides` on files no preset override names | kept |
223
+ | `overrides` on the preset override's own files | kept as the module block, governed rules dropped and named |
224
+ | `plugins` | kept, minus the preset's own |
225
+ | `settings` | kept |
226
+ | a second `extends` entry | kept, in order, after sentinel's |
227
+ | `.oxfmtrc.json` `sortImports` false / true / a groups object (nest) | kept, declared in `local`, stamp moves |
228
+ | `.oxfmtrc.json` module `ignorePatterns` | kept, stamp unchanged |
229
+ | every block at once | all kept; a second re-init is a no-op |
230
+ | every block at once, bare `--init` | re-inits lint and format only, all kept |
231
+ | the same from the workspace root, `--module` | the same |
232
+ | fixes 5 and 6 together: tracer imports, a team `sortImports`, a second `extends` | all three kept |
233
+
234
+ Unit (`lint-module-own-config`, `format-adoption`, `extends-preservation`): two module blocks
235
+ with the same `files` merge; a block left with only `files` is not written; a `sortImports`
236
+ equal to what the RECORDED preset sets is sentinel's, so the async-api-types libs corrected from
237
+ base to nest take nest's sort.
238
+
239
+ Negative controls, one per block, each reverting one read-back: plugins 5 cells fail, jsPlugins
240
+ 6, settings 5, overrides 6, extends 5, rules 6, sortImports 7, format `ignorePatterns` 1. Each
241
+ fails its own cells plus the every-block cells, and nothing else.
242
+
243
+ ### against the consumer, the 63 adopted modules
244
+
245
+ The packed branch, through a pnpm `file:` override, on a fresh worktree of the monorepo at
246
+ 7177eb86038, nx graph built first, a BARE `--init` in each module:
247
+
248
+ | | result |
249
+ | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
250
+ | `--init` exit 0 | 63 of 63 |
251
+ | roles re-inited = roles the alpha was given by name | 63 of 63 |
252
+ | files differing from the alpha's explicit-roles run | 8, all `.oxfmtrc.json` stamps (fix 3) |
253
+ | files differing from the fixes 1 to 4 run | 0 |
254
+ | the ten planning modules' second `extends` | byte for byte |
255
+ | lint / format / typecheck / build | 62 / 59 / 63 / 31, all green |
256
+ | test | 43 projects, 7 red under `--parallel=3`: the 4 environment reds known on origin/main (the `/private` checkout path in 3 jest suites, no Redis for bff-planning), host-admin's French-locale date, and two load reds that are green run alone (bff-worker, a jest worker SIGSEGV; regulation-balance, 15 s timeouts) |
257
+
258
+ Run with `--skip-nx-cache` from the workspace root after every `prisma:gen:*`, career's
259
+ `prepare-runtime-artifacts`, `svelte-kit sync` and `VITE_BRAND_ID=hublo`. Run alone, host-admin
260
+ fails only the locale test (1 806 of 1 807 files).
261
+
262
+ The two PR heads the fixes came from, each in a throwaway worktree, nothing pushed:
263
+ `libs/nest/tracer` at monorepo#23314's head keeps its import order byte for byte, and with the
264
+ pre-adoption `index.ts` restored, the bare `--init` pins it (3 pins) and the format check passes.
265
+ host-admin at monorepo#23342's head now adopts lint with its other roles, which is the rule:
266
+ ESLint is removed, and the guards the react preset does not carry are the team's to add.
267
+
138
268
  ## The static analysis role's validation matrix, 19 October 2026
139
269
 
140
270
  Re-run from scratch because **53 commits landed on the role since 13 October**, and three of them
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hublo/sentinel",
3
- "version": "1.5.0",
3
+ "version": "1.5.1-alpha.1",
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",
@@ -12,6 +12,7 @@
12
12
  "engines": {
13
13
  "node": ">=24.15.0"
14
14
  },
15
+ "packageManager": "pnpm@10.26.0",
15
16
  "bin": {
16
17
  "sentinel": "./dist/bin/sentinel.js"
17
18
  },
@@ -107,6 +108,28 @@
107
108
  "oxlint",
108
109
  "plugins"
109
110
  ],
111
+ "scripts": {
112
+ "build": "tsup && tsx scripts/build-presets.ts",
113
+ "dev": "tsup --watch",
114
+ "typecheck": "tsc --noEmit",
115
+ "lint": "node dist/bin/sentinel.js --run --lint && node dist/bin/sentinel.js --run --format",
116
+ "lint:fix": "node dist/bin/sentinel.js --run --lint --fix && node dist/bin/sentinel.js --run --format --fix",
117
+ "format": "node dist/bin/sentinel.js --run --format",
118
+ "test": "vitest run",
119
+ "test:watch": "vitest",
120
+ "test:e2e": "vitest run tests/e2e",
121
+ "test:coverage": "vitest run --coverage --testTimeout=60000",
122
+ "cli": "node --import tsx bin/sentinel.ts",
123
+ "docs:traces": "pnpm build && bash scripts/gen-traces.sh",
124
+ "docs:traces:check": "pnpm build && bash scripts/gen-traces.sh --check",
125
+ "format:fix": "node dist/bin/sentinel.js --run --format --fix",
126
+ "bench": "node scripts/bench.mjs",
127
+ "deps:trust": "tsx scripts/check-trust-policy.ts",
128
+ "prepack": "tsx scripts/prune-sourcemaps.ts",
129
+ "static-analysis": "sentinel --run --static-analysis",
130
+ "static-analysis:json": "sentinel --run --static-analysis --output json",
131
+ "static-analysis:html": "sentinel --run --static-analysis --output html"
132
+ },
110
133
  "dependencies": {
111
134
  "@nx/eslint-plugin": "23.0.1",
112
135
  "@tailwindcss/vite": "4.1.18",
@@ -155,6 +178,11 @@
155
178
  "tsup": "^8.3.0",
156
179
  "tsx": "^4.19.0"
157
180
  },
181
+ "pnpm": {
182
+ "onlyBuiltDependencies": [
183
+ "esbuild"
184
+ ]
185
+ },
158
186
  "publishConfig": {
159
187
  "registry": "https://registry.npmjs.org",
160
188
  "access": "public"
@@ -191,26 +219,5 @@
191
219
  "nx": {
192
220
  "optional": true
193
221
  }
194
- },
195
- "scripts": {
196
- "build": "tsup && tsx scripts/build-presets.ts",
197
- "dev": "tsup --watch",
198
- "typecheck": "tsc --noEmit",
199
- "lint": "node dist/bin/sentinel.js --run --lint && node dist/bin/sentinel.js --run --format",
200
- "lint:fix": "node dist/bin/sentinel.js --run --lint --fix && node dist/bin/sentinel.js --run --format --fix",
201
- "format": "node dist/bin/sentinel.js --run --format",
202
- "test": "vitest run",
203
- "test:watch": "vitest",
204
- "test:e2e": "vitest run tests/e2e",
205
- "test:coverage": "vitest run --coverage --testTimeout=60000",
206
- "cli": "node --import tsx bin/sentinel.ts",
207
- "docs:traces": "pnpm build && bash scripts/gen-traces.sh",
208
- "docs:traces:check": "pnpm build && bash scripts/gen-traces.sh --check",
209
- "format:fix": "node dist/bin/sentinel.js --run --format --fix",
210
- "bench": "node scripts/bench.mjs",
211
- "deps:trust": "tsx scripts/check-trust-policy.ts",
212
- "static-analysis": "sentinel --run --static-analysis",
213
- "static-analysis:json": "sentinel --run --static-analysis --output json",
214
- "static-analysis:html": "sentinel --run --static-analysis --output html"
215
222
  }
216
- }
223
+ }