@hublo/sentinel 1.3.0 → 1.4.0-alpha.10

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.
Files changed (61) hide show
  1. package/README.md +8 -4
  2. package/dist/bin/sentinel.d.ts +1 -1
  3. package/dist/bin/sentinel.js +22 -9
  4. package/dist/chunk-2XLX6PFR.js +132 -0
  5. package/dist/chunk-2XLX6PFR.js.map +1 -0
  6. package/dist/chunk-3TDUIKVQ.js +178 -0
  7. package/dist/chunk-3TDUIKVQ.js.map +1 -0
  8. package/dist/chunk-CPCUPK4J.js +70 -0
  9. package/dist/chunk-CPCUPK4J.js.map +1 -0
  10. package/dist/chunk-CRKUEP4J.js +427 -0
  11. package/dist/chunk-CRKUEP4J.js.map +1 -0
  12. package/dist/{chunk-676GBPMS.js → chunk-L7WS36XV.js} +3921 -556
  13. package/dist/chunk-PWV3BMDA.js +15 -0
  14. package/dist/chunk-PWV3BMDA.js.map +1 -0
  15. package/dist/chunk-WLFE5RUU.js +264 -0
  16. package/dist/chunk-WLFE5RUU.js.map +1 -0
  17. package/dist/index.d.ts +13 -0
  18. package/dist/index.js +2 -2
  19. package/dist/roles/build/nest/toolchain.d.ts +4 -36
  20. package/dist/roles/build/nest/toolchain.js +10 -178
  21. package/dist/roles/build/nest/toolchain.js.map +1 -0
  22. package/dist/roles/build/toolchain.js.map +1 -0
  23. package/dist/roles/test/nest/toolchain.d.ts +9 -0
  24. package/dist/roles/test/nest/toolchain.js +25 -0
  25. package/dist/roles/test/nest/toolchain.js.map +1 -0
  26. package/dist/roles/test/react/toolchain.d.ts +66 -0
  27. package/dist/roles/test/react/toolchain.js +14 -1
  28. package/dist/roles/test/react/toolchain.js.map +1 -0
  29. package/dist/roles/test/setup/jest-parity.d.ts +2 -0
  30. package/dist/roles/test/setup/jest-parity.js +159 -0
  31. package/dist/roles/test/setup/jest-parity.js.map +1 -0
  32. package/dist/roles/test/setup/mock-extended.d.ts +46 -0
  33. package/dist/roles/test/setup/mock-extended.js +65 -0
  34. package/dist/roles/test/setup/mock-extended.js.map +1 -0
  35. package/dist/roles/test/setup/msw-lifecycle.d.ts +16 -0
  36. package/dist/roles/test/setup/msw-lifecycle.js +12 -0
  37. package/dist/roles/test/setup/msw-lifecycle.js.map +1 -0
  38. package/dist/roles/test/setup/msw-server.d.ts +3 -0
  39. package/dist/roles/test/setup/msw-server.js +10 -0
  40. package/dist/roles/test/setup/msw-server.js.map +1 -0
  41. package/dist/roles/test/setup/workspace-entry.d.ts +2 -0
  42. package/dist/roles/test/setup/workspace-entry.js +8 -0
  43. package/dist/roles/test/setup/workspace-entry.js.map +1 -0
  44. package/dist/roles/test/shared-test-config.d.ts +32 -0
  45. package/dist/roles/test/shared-test-config.js +8 -0
  46. package/dist/roles/test/shared-test-config.js.map +1 -0
  47. package/dist/roles/test/tools/msw.d.ts +1 -0
  48. package/dist/roles/test/tools/msw.js +3 -0
  49. package/dist/roles/test/tools/msw.js.map +1 -0
  50. package/dist/tsconfig-aliases-Ce6axdJ4.d.ts +36 -0
  51. package/docs/.gitkeep +0 -0
  52. package/docs/build-adoption.md +521 -0
  53. package/docs/format-adoption.md +321 -0
  54. package/docs/lint-adoption.md +290 -0
  55. package/docs/performance.md +49 -0
  56. package/docs/test-adoption.md +219 -0
  57. package/docs/typescript-adoption.md +184 -0
  58. package/docs/typescript-traces.md +798 -0
  59. package/docs/using-sentinel.md +195 -0
  60. package/docs/validating-a-change.md +101 -0
  61. package/package.json +35 -6
@@ -0,0 +1,321 @@
1
+ # Format adoption cheat sheet
2
+
3
+ Migrating one module from Prettier to oxfmt, through sentinel. For linting see
4
+ [`lint-adoption.md`](lint-adoption.md), for TypeScript
5
+ [`typescript-adoption.md`](typescript-adoption.md).
6
+
7
+ ## Adopt a module
8
+
9
+ Run from the **module's own directory**.
10
+
11
+ ```bash
12
+ cd apps/.../<module>
13
+ npx --yes @hublo/sentinel@<version> --init --preset <react|nest|node|svelte>
14
+ pnpm install
15
+ ```
16
+
17
+ `--init` with no target adopts every role, and that is the intended order: lint fixes what it
18
+ can, then the formatter runs **last** and formats everything every role wrote. Adopting the
19
+ formatter alone is `--init --format`.
20
+
21
+ **`--init` reformats your module.** That is deliberate: adoption that leaves a module failing
22
+ its own new format check is not adoption, it is a chore handed to someone else. Commit it on
23
+ its own, so the reformat is one reviewable diff rather than noise on top of a real change.
24
+
25
+ > **Order matters, and `--init` with no target gets it right for you.** The linter's autofix
26
+ > rewrites code, and sentinel writes JSON in its own style, so **formatting has to come after
27
+ > linting**, never before. Run whole-module `--init` and it happens automatically: format runs
28
+ > LAST, over everything every role wrote. If you adopt roles one at a time, run
29
+ > `sentinel --init --format` after `sentinel --init --lint`, or your first `pnpm run lint`
30
+ > fails at a formatting step that has nothing to do with linting.
31
+
32
+ ## What `--init` does
33
+
34
+ | It writes | Why |
35
+ | ------------------------------- | ---------------------------------------------------------------------------- |
36
+ | `.oxfmtrc.json` | the preset's options, **materialized**, plus anything your module differs on |
37
+ | `format` / `format:fix` scripts | `format` is the CHECK (what CI runs), `format:fix` writes |
38
+ | the nx `format` target | with the config as an input, so a preset change invalidates the cache |
39
+ | `@hublo/sentinel` devDependency | pinned, so the CLI resolves |
40
+
41
+ | It removes | Why |
42
+ | ----------------------------------- | -------------------------------------------------------------------------------------------------- |
43
+ | your `.prettierrc*` | adoption REPLACES Prettier; two configured formatters means whichever the runner picks wins |
44
+ | your module's Prettier dependencies | they served that config. The **root** keeps its tooling, which every unmigrated module still needs |
45
+
46
+ Any other script that called `prettier` is rewritten too (a `format:check` left pointing at a
47
+ removed binary fails the first time someone runs it, not at adoption when it would be obvious).
48
+ Only the prettier segment is replaced, so `svelte-kit sync && <format> && eslint .` keeps its
49
+ other commands.
50
+
51
+ ## Commands
52
+
53
+ Every verb, for every role, plus scoping a run to some files and what a fully adopted module
54
+ looks like: [`using-sentinel.md`](using-sentinel.md).
55
+
56
+ ## Why this config is not a one-line stub
57
+
58
+ Every other role commits a stub that `extends` a preset shipped inside the package. **oxfmt has
59
+ no `extends`, and it ignores unknown top-level keys silently.** A stub like
60
+
61
+ ```json
62
+ { "extends": ["./node_modules/@hublo/sentinel/oxfmt/base.json"] }
63
+ ```
64
+
65
+ parses fine, reports as adopted, and formats with oxfmt's **defaults** (double quotes,
66
+ semicolons). Verified: under that stub a file in this repo's style fails the check and a file
67
+ in oxfmt's default style passes. Shipping it would have rewritten the whole monorepo into a
68
+ style nobody chose.
69
+
70
+ Note that `base` here **is** a preset you name, unlike the other roles. Formatting does not
71
+ vary by stack: the repo has always formatted Nest, React, Svelte and tooling from one root
72
+ `.prettierrc`, so there is one style and `base` is it (`svelte` and `nest` are that same style
73
+ with one option flipped). In the lint, typescript and build roles the shared layer is called
74
+ `shared.json`, is never published, and is not something a module names.
75
+
76
+ So the preset's values are written INTO your config. Two things follow:
77
+
78
+ - Your editor's oxfmt extension reads this file, so format-on-save agrees with CI.
79
+ - A preset change does **not** reach you by reinstalling. It needs a re-`init`.
80
+ `sentinel --inspect --format` from the root reports which modules are behind.
81
+
82
+ ## Keeping your module's own formatting
83
+
84
+ The preset carries the repo's formatting. If your module genuinely differs, that is recorded
85
+ rather than overwritten: adoption reads your Prettier config and carries what it finds. Three
86
+ modules in this repo already differ (tabs at 100 columns, semicolons), and reformatting them
87
+ on the way past would have destroyed the adoption diff.
88
+
89
+ An override lives in the config and is DECLARED in `$sentinel.local`:
90
+
91
+ ```json
92
+ {
93
+ "$sentinel": { "preset": "base", "version": "1.1.0", "local": ["printWidth", "useTabs"] },
94
+ "printWidth": 100,
95
+ "useTabs": true,
96
+ "semi": false
97
+ }
98
+ ```
99
+
100
+ A re-`init` keeps exactly the keys listed in `local` and refreshes the rest. Adding an override
101
+ by hand means adding the key **and** listing it there; a value that differs without being
102
+ declared is reported as drift, because nobody said this module should format differently.
103
+
104
+ `ignorePatterns` and `overrides` are always yours and never need declaring.
105
+
106
+ A setting is carried only if oxfmt accepts the **value**, not just the key: what it rejects is
107
+ dropped and named in the adoption output (see "What did not survive the move"). The check asks
108
+ oxfmt's own shipped schema, so it stays true across oxfmt releases.
109
+
110
+ ### What sentinel adds to `ignorePatterns`, and why
111
+
112
+ Prettier ran from the workspace **root**, so the root `.prettierignore` governed every file in
113
+ the repo. oxfmt reads `.gitignore` and `.prettierignore` from its **current directory**, and
114
+ sentinel runs it in your module, so the root file is simply not seen.
115
+
116
+ Left alone that silently widens what gets formatted, and oxfmt's reach is wider than
117
+ Prettier's was here: it formats markdown, YAML, HTML, CSS **and Handlebars**. The root file
118
+ excludes `**/*.md` and `**/*.hbs` among others, so the first `format:fix` would have
119
+ reformatted every README and rewritten Handlebars templates as HTML.
120
+
121
+ So `--init` reads the root file and carries forward the patterns that were already reaching
122
+ into your module: those with no slash (`node_modules`), and those anchored at the root but
123
+ spanning any depth (`/**/*.md` becomes `**/*.md`). Root-only patterns like `/dist` are
124
+ **not** carried, because they never applied inside a module and carrying them would ignore
125
+ more than before.
126
+
127
+ Your module's own `.prettierignore` is left exactly as it is: it sits in the module
128
+ directory, so oxfmt already reads it.
129
+
130
+ ### And your module is excluded from the ROOT Prettier
131
+
132
+ The mirror of the same problem. The root Prettier still owns every file in the repo, and it
133
+ does not know your module now formats itself, so `nx format:write` or `prettier --write .`
134
+ from the root would reformat your module back to the ROOT's style, and your next
135
+ `pnpm run format` would reverse it. Whichever ran last wins, which is precisely the state
136
+ adoption removes inside a module.
137
+
138
+ So `--init --format` adds one line to the root `.prettierignore`:
139
+
140
+ ```
141
+ # @hublo/sentinel: modules formatted by oxfmt, not by the root Prettier
142
+ /apps/front/your-module/
143
+ ```
144
+
145
+ This is the only file outside your module that adoption touches, and it is the same
146
+ deliberate exception the workspace prep already makes. The root Prettier config itself is
147
+ untouched: it still serves every module that has not migrated.
148
+
149
+ ## Svelte modules
150
+
151
+ Svelte is the one preset with a formatting difference, and it is not a style. oxfmt reads
152
+ `.svelte` files only when its `svelte` option is on (its default is **disabled**), so on the
153
+ base preset a Svelte module adopts cleanly, exits 0, and quietly stops formatting the files it
154
+ is mostly made of. `--preset svelte` materializes the same style with `"svelte": true`:
155
+
156
+ ```json
157
+ {
158
+ "$sentinel": { "preset": "svelte", "version": "1.1.0", "local": [] },
159
+ "printWidth": 80,
160
+ "svelte": true
161
+ }
162
+ ```
163
+
164
+ Prettier said the same thing through `prettier-plugin-svelte`, which adoption removes, so this
165
+ carries your formatting across rather than adding to it. oxfmt does not bundle the Svelte
166
+ compiler: it requires `svelte` from your module, which every Svelte module has.
167
+
168
+ ## What did not survive the move
169
+
170
+ Adoption reads your Prettier config and reports, per module, what it could not carry. Across
171
+ this repo that is:
172
+
173
+ - **`endOfLine: "auto"`**: oxfmt accepts `lf`, `crlf` and `cr` only. It always writes LF, which
174
+ is what git stores anyway. Carried verbatim it produced a config oxfmt refuses to parse, so a
175
+ value oxfmt rejects is now dropped and reported rather than written.
176
+ - **`prettier-plugin-gherkin`**, a real loss: `.feature` files stop being formatted. They are
177
+ not source, no check enforces their formatting today, and oxfmt has no plugin API to carry
178
+ it. Accepted, and written down here rather than discovered later.
179
+ - **`prettier-plugin-svelte`**, which is NOT a loss: `--preset svelte` covers it (above), and
180
+ adoption says so rather than listing it as dropped.
181
+
182
+ Prettier `overrides` are translated, not copied: `files` is a string in Prettier and a list in
183
+ oxfmt, and an override whose only option was a `parser` (a plugin concept) is dropped whole
184
+ rather than written empty.
185
+
186
+ `sortPackageJson` is switched **off**. Left on, the first `format:fix` in every module reorders
187
+ its `package.json` into a diff nobody can review beside the adoption itself.
188
+
189
+ ## Import ordering: on for nest, off for the front
190
+
191
+ Since 1.1.3, **adoption reformats your module; whether it also reorders your imports depends
192
+ on your stack**, because the two stacks were in genuinely different positions.
193
+
194
+ | Preset | Import ordering | Why |
195
+ | ----------------------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
196
+ | `nest` | **on** | `import/order` is enforced in `eslint.config.back.js`. Turning it off would remove a check you have today |
197
+ | `react`, `react-lib`, `node`, `svelte`, `tools` | **off** | The front and root ESLint configs order nothing. You never had the check, and adoption should not charge you thousands of files to gain one |
198
+
199
+ ### Where the number came from
200
+
201
+ Share of reformatted files whose diff touches **only** the import block, on 1.1.2:
202
+
203
+ | Module | Files reformatted | Import-block only |
204
+ | ------------------- | ----------------: | ----------------: |
205
+ | `host-admin` | 2,670 | **2,374 (89%)** |
206
+ | `bff-admin` | 1,740 | **1,590 (91%)** |
207
+ | `hr-management` | 303 | **290 (96%)** |
208
+ | `libs/front/shared` | 13 | 7 (54%) |
209
+
210
+ The two halves had different causes, which is why they get different answers.
211
+
212
+ **The front had no ordering at all**, so sorting moved almost every import. Off is also
213
+ oxfmt's own default, so `base` now simply stops overriding it. Written as
214
+ `"sortImports": false` rather than omitted, so the config states the decision.
215
+
216
+ **Nest was already ordered, and its churn was not sorting.** Of bff-admin's 1,740 changed
217
+ files, **1,687 differed by one blank line** and only 3 by a real reordering. ESLint grouped the
218
+ workspace scopes (`@hublo/**`, `@shared/**`, ...) as _internal_; oxfmt reads them as
219
+ _external_, so two groups collapsed into one and the line between them disappeared.
220
+
221
+ The `nest` preset restores the grouping with `customGroups`:
222
+
223
+ ```json
224
+ "sortImports": {
225
+ "customGroups": [
226
+ { "groupName": "workspace",
227
+ "elementNamePattern": ["@hublo/**", "@shared/**", "@front/**", "@network/**"] }
228
+ ],
229
+ "groups": ["builtin", "external", "workspace", ["parent", "index"], "sibling"],
230
+ "newlinesBetween": true
231
+ }
232
+ ```
233
+
234
+ Measured on bff-admin: **1,740 changed files down to 91**, of which 43 are genuine ordering
235
+ fixes worth having and 48 are formatting.
236
+
237
+ `internalPattern` looks like the option for this and is not: in oxfmt 0.63.0 setting it
238
+ collapses the external and internal groups into one whatever the pattern (a pattern matching
239
+ only `zod` produced the same single block as one matching only `@hublo/`). `customGroups` is
240
+ the one that works.
241
+
242
+ ### Turning ordering on for a front module
243
+
244
+ One line, and it is worth doing in a commit of its own:
245
+
246
+ ```jsonc
247
+ // .oxfmtrc.json
248
+ {
249
+ "sortImports": true,
250
+ }
251
+ ```
252
+
253
+ Declare it in `$sentinel.local` so a re-`init` keeps it, then run `pnpm run format:fix` and
254
+ expect most of the module to be touched.
255
+
256
+ ### Finding the modules that do not check it
257
+
258
+ `sentinel --inspect --format` reports it, per module, under **not checked at all**:
259
+
260
+ ```console
261
+ ✓ host-admin (react) format — adopted=true preset=base conformant=true
262
+ not checked at all:
263
+ • sortImports — import order is not checked here. Enable it with `"sortImports": true` …
264
+ ```
265
+
266
+ The gap is reported rather than warned on every run: it is a state of the module, not an
267
+ event. A message that fires on every `pnpm run lint` is a message people stop reading.
268
+
269
+ ### The one grouping difference, and why it stands
270
+
271
+ `import/order` explicitly listed this repo's 14 workspace scopes (`@hublo/**`, `@shared/**`,
272
+ `@front/**`, `@network/**`, and the rest) as **internal**. oxfmt classifies them as
273
+ **external**, because to a formatter they look like any other scoped package. `@/...` is
274
+ still recognised as internal, and builtins and relative imports are unaffected.
275
+
276
+ So on adoption, imports of workspace packages move from the internal group up into the
277
+ external one. It is cosmetic, it happens once, and it is part of the reformat commit.
278
+
279
+ It is not fixed by configuration, and that was checked rather than assumed. oxfmt's
280
+ `sortImports` accepts an `internalPattern` key, but in 0.63.0 setting it **collapses the
281
+ external and internal groups into one**, whatever the pattern: a pattern matching only `zod`
282
+ produced the same single block as a pattern matching only `@hublo/`. The default behaviour
283
+ keeps three distinct groups and is the better of the two, so sentinel does not set it. If a
284
+ later oxfmt implements it properly, one line in the preset restores the old grouping and
285
+ `--inspect` will report every module as behind until they re-`init`.
286
+
287
+ ## Troubleshooting
288
+
289
+ | Symptom | Cause |
290
+ | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
291
+ | `could not find the oxfmt binary` | oxfmt ships with sentinel; the module needs `pnpm install` |
292
+ | `no sentinel .oxfmtrc.json in this module` | not adopted yet (an `.oxfmtrc.json` you wrote yourself is not adoption, `--init` records provenance) |
293
+ | `--inspect` says the module has **drifted** | a preset-owned value was edited without declaring it in `$sentinel.local`, or the preset moved on. Re-`init` |
294
+ | `pnpm run format` fails on files you did not touch | the module was adopted but never formatted. Run `pnpm run format:fix` as its own commit |
295
+ | `.feature` files lost their formatting | expected, see above |
296
+
297
+ ### `TS(1098): Type parameter list cannot be empty` (and friends)
298
+
299
+ oxfmt refuses to format a file it cannot parse, and its TypeScript parser is stricter than
300
+ Prettier's. **Two files in the whole monorepo** are affected, both `.d.ts`, both genuine
301
+ TypeScript errors that Prettier recovered from:
302
+
303
+ | File | Error | Fix |
304
+ | ----------------------------------------------- | -------------------------------------------- | ------------------------------------------ |
305
+ | `apps/front/hr-management/src/app.d.ts` | `TS(1098)` `interface HTMLAttributes<>` | delete the empty `<>` |
306
+ | `libs/front/theme/types/breakpoints/index.d.ts` | `TS(1039)` an initializer in an ambient file | it holds a value, so it belongs in a `.ts` |
307
+
308
+ This is debt the migration surfaces rather than causes, and it is yours to fix: a formatter
309
+ cannot invent the missing syntax, and silently adding the file to `ignorePatterns` would be the
310
+ tool deciding to stop checking your code without telling you.
311
+
312
+ You will not have to go looking. `--init` names each file, its line and the reason, and says
313
+ that `pnpm run format` fails until they are fixed. It still exits 0: adoption itself worked, and
314
+ a module reported as un-adopted gets reverted for no reason.
315
+
316
+ ### `.svelte files are not formatted yet`
317
+
318
+ Expected on the first adoption of a Svelte module. oxfmt does not bundle the Svelte compiler, it
319
+ loads it from your module, and `npx --yes ... --init` writes the config before any install has
320
+ happened. Run `pnpm install`, then `pnpm run format:fix` — both of which adoption tells you to
321
+ run anyway.
@@ -0,0 +1,290 @@
1
+ # Lint adoption cheat sheet
2
+
3
+ Migrating one module from ESLint to Oxlint, through sentinel. For the formatter see
4
+ [`format-adoption.md`](format-adoption.md), for TypeScript
5
+ [`typescript-adoption.md`](typescript-adoption.md); each is a separate role and can adopt
6
+ independently.
7
+
8
+ ## Adopt a module in two steps
9
+
10
+ Run from the **module's own directory**. You do not need sentinel installed to run it.
11
+
12
+ ```bash
13
+ cd apps/.../<module>
14
+ npx --yes @hublo/sentinel@<version> --init --preset <react|nest|node|svelte>
15
+ pnpm install
16
+ ```
17
+
18
+ Then run `pnpm run lint`.
19
+
20
+ `--init` with no target adopts **every role sentinel ships**, and for lint that is the
21
+ 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.
24
+
25
+ `--init` fixes what a machine can fix, once. It does not loop, and it does not edit your code
26
+ beyond what the tools' own fixers do. What is left afterwards is real debt: see
27
+ [Reading the numbers](#reading-the-numbers).
28
+
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
+
36
+ ## What `--init` does
37
+
38
+ | It writes | Why |
39
+ | ------------------------------- | -------------------------------------------------------------------------------------- |
40
+ | `.oxlintrc.json` | a stub that `extends` the shipped preset, plus the ignore patterns (see below) |
41
+ | `lint` script | so `pnpm run lint` and the nx target cannot disagree about which linter runs |
42
+ | the nx `lint` target | replaced, not merged: the ESLint executor's options mean nothing to a different runner |
43
+ | `@hublo/sentinel` devDependency | pinned, so the preset resolves |
44
+
45
+ | It removes | Why |
46
+ | --------------------------------- | -------------------------------------------------------------------------------------------------- |
47
+ | your `eslint.config.*` | adoption REPLACES ESLint; two configured linters means whichever the runner picks wins |
48
+ | your module's ESLint dependencies | they served that config. The **root** keeps its tooling, which every unmigrated module still needs |
49
+
50
+ **It then runs the autofix once, and tells you what that did.** Adoption enforces rules your
51
+ module may have had switched off, and the fixer acts on them immediately, so this is the step
52
+ that can change your source:
53
+
54
+ ```console
55
+ fixing what lint can fix automatically...
56
+ the lint autofix rewrote 12 files: src/a.ts, src/b.ts and 10 more. Review the diff
57
+ before committing: rules this module used to have switched off are enforced from now on.
58
+ ```
59
+
60
+ It says `changed nothing` when there was nothing to do, and says so explicitly when the pass
61
+ **could not run** at all, which used to be silent: neither the exit status nor the output
62
+ stream distinguishes "your config did not load" from "here are your remaining violations", so
63
+ a module could look adopted while nothing had been checked.
64
+
65
+ It also **rewrites suppression comments** that a plugin rename would otherwise void. A hosted
66
+ plugin can be registered under a different name than ESLint knows it by (`@nx/eslint-plugin`
67
+ is hosted as `nx`), and every rule id changes with it, so `// eslint-disable @nx/...` would
68
+ silently stop suppressing while the findings it held back reappear. One stale comment produced
69
+ 80 phantom warnings during the proof of concept. Same for `@next/next/*`, which oxlint ships
70
+ natively as `nextjs/*`.
71
+
72
+ **Import ordering is not a lint rule here.** `import/order` moved to the format role: oxfmt
73
+ rewrites the whole import block, so it sorts a file completely in one pass, where a lint fixer
74
+ emits edits over overlapping ranges and needs several. `eslint-plugin-import` is no longer
75
+ hosted at all, since its only other rule (`no-duplicates`) is native in oxlint.
76
+
77
+ ### Why `ignorePatterns` is in your config and the rules are not
78
+
79
+ The rules stay in the preset, so changing them reaches every module by reinstalling. Ignore
80
+ patterns cannot work that way: oxlint does **not** inherit `ignorePatterns` through `extends`
81
+ (verified), so a preset declaring them gives them to nobody.
82
+
83
+ Sentinel used to compensate by passing `--ignore-pattern` flags whenever it ran oxlint. That
84
+ made the committed config correct only while sentinel was the thing running it. Everything
85
+ else read the file and linted your build output:
86
+
87
+ ```console
88
+ $ oxlint -c .oxlintrc.json . # what your IDE effectively does
89
+ dist/built.js:1:1 error eslint(no-var)
90
+ node_modules/junk/bad.js:1:1 error eslint(no-var)
91
+ ```
92
+
93
+ So they are written into your module's config. Your own patterns are carried across from the
94
+ eslint config on top of the defaults, minus any that merely restate one (`dist/**` beside
95
+ `**/dist/**`).
96
+
97
+ If `--run --lint` warns that your config declares no `ignorePatterns`, it was written by an
98
+ older sentinel: re-run `sentinel --init --lint`.
99
+
100
+ ## Commands
101
+
102
+ Every verb, for every role, plus scoping a run to some files and what a fully adopted module
103
+ looks like: [`using-sentinel.md`](using-sentinel.md).
104
+
105
+ ## Presets, and the one you don't choose
106
+
107
+ Declare the stack: `react`, `nest`, `svelte`, or `node` (the stack-less base, for a module
108
+ that belongs to no stack).
109
+
110
+ **React has two tiers, and you do not pick them.** `apps/**` gets the application set, `libs/**`
111
+ gets the library set, because that is a structural fact of the workspace rather than a
112
+ judgement call. They differ by 96 rules, 61 of which are accessibility: an app shell has a
113
+ user to be accessible to, a component library does not, and a library instead runs rules an
114
+ app has no use for (`nextjs/*`, `jest-dom/*`, and the `storybook/*` rules that apply only to
115
+ `*.stories.*`).
116
+
117
+ Always pass `--preset` in a monorepo. Dependencies are hoisted, so detection honestly answers
118
+ `node` for a React app that declares no `react` of its own.
119
+
120
+ ### Your module extends exactly one of them
121
+
122
+ There is nothing to stack. Every preset already contains the shared rule layer, flattened in
123
+ at build time, so the file you extend is self-contained:
124
+
125
+ | preset | rules | of which shared by every stack |
126
+ | ----------- | ----- | ------------------------------ |
127
+ | `react` | 188 | 21 |
128
+ | `react-lib` | 134 | 21 |
129
+ | `nest` | 110 | 21 |
130
+ | `svelte` | 81 | 21 |
131
+ | `tools` | 67 | 21 |
132
+ | `node` | 21 | 21 (the shared layer alone) |
133
+
134
+ The layer lives in the repo as `src/roles/lint/presets/shared.json` and is never published on
135
+ its own. If you are reading the source, that file is ours, not something a module names.
136
+
137
+ ### If a rule you need is missing
138
+
139
+ Tell us and it goes in the preset, where every module gets it. That is the whole point of
140
+ sentinel: the rules were the same in module after module, each copy free to drift, and they
141
+ now live in one place.
142
+
143
+ If your team genuinely needs something the preset should not impose on everyone, declare it as
144
+ a **layer above the preset**, the way the planning team did:
145
+
146
+ ```json
147
+ { "extends": [".../lint/nest.json", "./team-rules.json"] }
148
+ ```
149
+
150
+ Order matters, sentinel's preset first, or your layer cannot override anything. `--inspect`
151
+ reports the layer so it stays visible.
152
+
153
+ Think of it as a queue rather than a fork: a rule promoted into the preset is deleted from the
154
+ layer. It is the exact mirror of `.oxlintrc.baseline.json`, which holds rules **below** the
155
+ preset and empties as debt is fixed, while a layer holds rules **above** it and empties as they
156
+ are adopted.
157
+
158
+ **Wire your layer before your first `--init`, not after.** `--init` prefixes the preset onto
159
+ whatever `extends` it finds, so a layer already there survives and is respected by the autofix
160
+ that follows. Added afterwards, the autofix has already run, and nothing undoes what it
161
+ rewrote. One team met this as twelve rewritten files.
162
+
163
+ ## What a preset does not enforce
164
+
165
+ Every unenforced rule carries a reason, readable with `sentinel --inspect --lint`:
166
+
167
+ | State | Meaning |
168
+ | ----------------------------- | ------------------------------------------------------------------------------ |
169
+ | **not enforced, by decision** | it argues with a deliberate architectural choice, or only false-positives here |
170
+ | **reported, not blocking** | it runs and reports; it just cannot fail a build, so the debt stays visible |
171
+ | **uncovered** | oxlint cannot run it at all, so it can be neither enforced nor switched off |
172
+
173
+ `uncovered` is the one to read before adopting, because it is the only state where a check you
174
+ had simply stops existing. The clearest case is `no-restricted-syntax`: oxlint has no such
175
+ rule, so AST-selector guards cannot be carried across as configuration. This repo's eight
176
+ frontend selectors were rewritten as real rules in sentinel's own `hublo` plugin, which the
177
+ React tiers host. A Nest service that had architectural guards of its own has nowhere for them
178
+ to land yet, and one adopter lost two that way before noticing.
179
+
180
+ If that is your module, tell us which guards you rely on and they ship in the `hublo` plugin
181
+ alongside the frontend ones. A house rule copied per module drifts, which is what sentinel
182
+ exists to stop.
183
+
184
+ The governing rule for both: **adoption never turns a green module red.** A rule that would
185
+ newly fail is held at `warn`, visibly and with its reason, even when the previous
186
+ configuration was wrong or was not really running. You were green on Tuesday; you do not
187
+ arrive to a red build on Wednesday for code that was already there.
188
+
189
+ ### How that promise is kept, since 1.1.1
190
+
191
+ Two mechanisms, and the second exists because the first is not enough on its own.
192
+
193
+ The **preset** holds back rules measured as newly-failing across the repository. That is
194
+ correct on the day it is measured and decays from then on: the repository keeps moving, and a
195
+ commit landing afterwards can violate a rule the preset recorded as clean. The failure then
196
+ arrives for whoever adopts LAST, on code they did not write. It happened: `host-admin` adopted
197
+ green on 1.1.0 and red a week later, on `jsx-a11y/no-noninteractive-tabindex` and
198
+ `typescript/no-duplicate-type-constituents`, neither of which the repo's ESLint had ever
199
+ enforced.
200
+
201
+ So `--init` also measures **your module**, after the autofix pass, with type information, and
202
+ writes what it already violates to `.oxlintrc.baseline.json`:
203
+
204
+ ```jsonc
205
+ {
206
+ "rules": {
207
+ // 1 violation when this module adopted
208
+ "jsx-a11y/no-noninteractive-tabindex": "warn",
209
+ },
210
+ }
211
+ ```
212
+
213
+ Your `.oxlintrc.json` extends it after the preset, which is what lowers the severity. Four
214
+ things follow, and they are the point:
215
+
216
+ - the rules still **run and still report**, they just cannot fail the build for code that
217
+ predates the migration. A hold is never `off`;
218
+ - the file is **regenerated, not accumulated**. Fix the last violation, re-run
219
+ `sentinel --init --lint`, and the rule drops out and returns to the preset severity;
220
+ - your own config stays `extends` + `ignorePatterns`, so a rule **you** change by hand is
221
+ still reported as drift and is never confused with a hold sentinel made;
222
+ - `--init` says what it held and how many violations each hold covers, so the debt is
223
+ announced rather than discovered.
224
+
225
+ Do not edit `.oxlintrc.baseline.json` by hand. Commit it: it is part of the adoption, and it
226
+ is what makes the promise above true on the day you adopt rather than on the day the preset
227
+ was built.
228
+
229
+ A small number of rules cannot be carried at any severity, because oxlint validates rule and
230
+ plugin names when it _parses_ a config: naming one it does not know makes the config fail to
231
+ parse and the module lints **nothing**. Those are documented in the README rather than
232
+ silently dropped.
233
+
234
+ ## Ask the linter your own question
235
+
236
+ Everything after `--` goes to oxlint:
237
+
238
+ ```bash
239
+ sentinel --run --lint -- --deny-warnings # what would zero warnings take?
240
+ sentinel --run --lint -- --max-warnings 100 # hold a ceiling while you pay debt down
241
+ ```
242
+
243
+ A run with extra options says so on every line of output and carries `toolArgs` in the
244
+ `--json` envelope, because these options can weaken a check as easily as strengthen it, and
245
+ `--run` is what CI calls. Never put one in a committed `lint` script.
246
+
247
+ ## Reading the numbers
248
+
249
+ ```console
250
+ $ sentinel --run --lint --json --max-diagnostics 2
251
+ {
252
+ "results": [
253
+ {
254
+ "project": "hr-management",
255
+ "adopted": true,
256
+ "errors": 0,
257
+ "warnings": 72,
258
+ "rules": [
259
+ { "rule": "no-explicit-any", "severity": "warning", "count": 54 },
260
+ { "rule": "no-unused-vars", "severity": "warning", "count": 15 }
261
+ ],
262
+ "diagnostics": [{ "file": "src/…/index.ts", "line": 91, "col": 37, "rule": "no-explicit-any", … }],
263
+ "diagnosticsTruncated": true
264
+ }
265
+ ]
266
+ }
267
+ ```
268
+
269
+ The `rules` breakdown is the useful part: a total tells you a module is dirty, a per-rule
270
+ count tells you which rule to fix first, and across the fleet, which rule to fix everywhere.
271
+ The numbers come from oxlint's own JSON, not from parsing what it prints for humans.
272
+
273
+ ## Troubleshooting
274
+
275
+ | Symptom | Cause and fix |
276
+ | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
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 |
278
+ | `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
+ | 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
+ | `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 |
281
+ | Type-aware rules seem not to run | they need `oxlint-tsgolint`, which ships with sentinel. `sentinel --inspect --lint` reports `typeAware` so you can check rather than assume |
282
+ | A `eslint-disable` comment stopped working | its rule was renamed by a plugin alias. `--init` rewrites the ones it knows; `--inspect` lists the renames a preset causes |
283
+
284
+ ## Svelte, specifically
285
+
286
+ Oxlint parses a `.svelte` file's `<script>` block and **not its template**. Fourteen
287
+ `svelte/*` rules therefore cannot run at all, whatever the config says, including
288
+ `svelte/no-at-html-tags`, an XSS guard that fires on the modules today. Adding a Svelte
289
+ plugin does not help: the missing piece is the parser. This is documented rather than hidden,
290
+ and it is the honest cost of the migration for those two modules.
@@ -0,0 +1,49 @@
1
+ # Performance baseline
2
+
3
+ Numbers so a regression is something you can see rather than argue about. Reproduce with:
4
+
5
+ ```bash
6
+ pnpm build && pnpm bench <workspace-root>
7
+ ```
8
+
9
+ Recorded against the Hublo monorepo (**441 nx projects**, ~2900 source files in the largest
10
+ module) on an Apple Silicon laptop, sentinel `1.1.0-alpha.4`. Machine, disk and cache state all
11
+ move these, so compare **ratios to this table**, not absolute milliseconds. A number that
12
+ doubles is a finding; a number that moves 30% is a different laptop.
13
+
14
+ | What | Baseline | Why it is measured |
15
+ | ------------------------------------------------- | ----------: | ----------------------------------------------------------------------------------------------------------- |
16
+ | `--version` | **33 ms** | the floor under every command. If this grows, everything grows |
17
+ | `--inspect --lint` in one module | **35 ms** | sentinel's own overhead: config reads, no tool spawned |
18
+ | `--inspect --lint --json` across the workspace | **1300 ms** | what the migration status page runs, and the only command whose cost grows with the repo. ~3 ms per project |
19
+ | `--run --format` (144 files, sentinel's own repo) | **291 ms** | oxfmt doing real work |
20
+ | `--run --lint` (one module) | **~1.3 s** | dominated by oxlint, including its type-aware pass |
21
+
22
+ ## Measured separately
23
+
24
+ **The source probe** (`hasSourceFiles`, which decides whether a role has anything to do) is not
25
+ in `pnpm bench`: timing it would mean exporting it from the package's public entry point, and
26
+ widening the published surface to benchmark something is the wrong trade. Measured directly:
27
+
28
+ | | |
29
+ | --------------------------------------------------- | ----------: |
30
+ | `apps/front/host-admin` (4210 files), match found | **0.11 ms** |
31
+ | `libs/api-types` (2001 files), match found | **0.44 ms** |
32
+ | worst case: exhaust a 2001-file tree, match nothing | **47 ms** |
33
+
34
+ It stops at the first match and never descends into `node_modules` or build output, which is
35
+ where the difference comes from: a `**` glob over the same tree measured **515 ms** against
36
+ **2 ms** bounded.
37
+
38
+ ## What is deliberately not optimised
39
+
40
+ `readOwnPackage()` walks up the filesystem and re-parses sentinel's own manifest at each of its
41
+ call sites, with no memo. At this scale it is invisible, and a module-level cache is a footgun
42
+ in tests. If the whole-workspace scan ever becomes a problem, measure before assuming this is
43
+ why.
44
+
45
+ ## If a number regresses
46
+
47
+ Take the ratio, not the difference. The scan is the one to watch: it is per-project work, so a
48
+ change that adds one file read per module costs 441 of them. The probe is the other, because it
49
+ runs on every `--init` and its worst case is unbounded by anything except the tree it walks.