@hublo/sentinel 1.6.0-alpha.0 → 1.6.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6,13 +6,13 @@ import {
6
6
  registerAdapters,
7
7
  resolve,
8
8
  setDefaultRunner
9
- } from "./chunk-PC5R4DX5.js";
9
+ } from "./chunk-R7DS7VN4.js";
10
10
  import {
11
11
  BaseAdapter
12
- } from "./chunk-P7NYSMLS.js";
12
+ } from "./chunk-67QLOX6F.js";
13
13
  import "./chunk-JRZI5IIN.js";
14
14
  import "./chunk-7E6YTTAK.js";
15
- import "./chunk-NAFI7OLI.js";
15
+ import "./chunk-4EIY7TTR.js";
16
16
  import "./chunk-O7REVMOC.js";
17
17
  import "./chunk-QXFCZON7.js";
18
18
  export {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  defineEnv
3
- } from "../../chunk-NAFI7OLI.js";
3
+ } from "../../chunk-4EIY7TTR.js";
4
4
  export {
5
5
  defineEnv
6
6
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  sharedTestConfig
3
- } from "../../../chunk-5VNYQIFD.js";
3
+ } from "../../../chunk-EESVRILJ.js";
4
4
  import {
5
5
  decoratorMetadata,
6
6
  tsconfigAliases
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  jestExportConditions,
3
3
  sharedTestConfig
4
- } from "../../../chunk-5VNYQIFD.js";
4
+ } from "../../../chunk-EESVRILJ.js";
5
5
  import "../../../chunk-SWWQ7X7B.js";
6
6
  import "../../../chunk-QXFCZON7.js";
7
7
 
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  sharedTestConfig
3
- } from "../../chunk-5VNYQIFD.js";
3
+ } from "../../chunk-EESVRILJ.js";
4
4
  import "../../chunk-SWWQ7X7B.js";
5
5
  import "../../chunk-QXFCZON7.js";
6
6
  export {
@@ -7,7 +7,7 @@ import {
7
7
  readTestAdoption,
8
8
  resolveVitest,
9
9
  suiteOf
10
- } from "./chunk-P7NYSMLS.js";
10
+ } from "./chunk-67QLOX6F.js";
11
11
  import "./chunk-JRZI5IIN.js";
12
12
  import "./chunk-O7REVMOC.js";
13
13
  import "./chunk-QXFCZON7.js";
@@ -127,12 +127,46 @@ A repository that runs sentinel with `npx` cannot import it, since its own `node
127
127
  tried. An unknown key is refused by name, in both spellings: the file is `.mjs`, and an ignored
128
128
  `allowNonBlankedSecret` would silently stop downgrading the finding it was written for.
129
129
 
130
+ A refused config fails the run (exit 1, at every level) and says why twice: in full on stderr, one
131
+ line per problem, and on the summary row, `level=warning error=env.config.mjs: invalid env config:
132
+ unknown key "materialize"`, which is also the row's `error` in `--json`. A value of the wrong type is
133
+ named with what was found, its kind and never its content: `scan.roots[1] is an empty string`.
134
+
135
+ ### Not the monorepo's `repo-env.config.mjs`
136
+
137
+ `env.config.mjs` is sentinel's own schema, smaller than the monorepo's `tools/repo-env` config, and
138
+ not a copy of it. Reusing the monorepo's file as-is in another repository is not correct either way:
139
+ its allowlists, scan roots and readers describe the monorepo. A repository declares its own facts,
140
+ with these keys and no others: `externallyProvided`, `knownExternalVars`, `knownExternalPatterns`,
141
+ `allowNonBlankedSecrets`, `allowedCommittedEnvFiles`, `envFiles`, `contracts`, `scan` and
142
+ `references` (each `roots`, `ext`, `excludeSuffixes`, `excludeDirs`), `readers` (`required`,
143
+ `optional`) and `docker` (`allowRoot`, `excludePaths`).
144
+
145
+ Four keys of `repo-env.config.mjs` are refused, on purpose:
146
+
147
+ | `repo-env.config.mjs` key | in sentinel 1.6 |
148
+ | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
149
+ | `materialize` | monorepo-only: it seeds that repository's test env files from its contracts, which is functional, not conformance. It stays in the monorepo |
150
+ | `references.patterns` | fixed in V1, built in: `process.env.X`, `process.env['X']`, `import.meta.env.X`, `import.meta.env['X']`, `$env/static/*` imports, `env.X`, `{ X } = env` |
151
+ | `secretNamePatterns` | fixed in V1, built in: `SECRET`, `TOKEN`, `PASSWORD`, `PRIVATE_?KEY`, `ACCESS_?KEY`, `API_?KEY`, case-insensitive |
152
+ | `publicEnvPrefixes` | fixed in V1, built in: `VITE_`, `PUBLIC_`, `NEXT_PUBLIC_`, `REACT_APP_` |
153
+
154
+ The three fixed ones describe how code reads env and what a name means, which do not vary by
155
+ repository. The migration of the monorepo onto the role is where they are revisited.
156
+
130
157
  ### Sentinel's defaults
131
158
 
132
159
  `presets/shared.json` in the role. Secret names match **case-insensitively** (`SECRET`, `TOKEN`,
133
160
  `PASSWORD`, `PRIVATE_?KEY`, `ACCESS_?KEY`, `API_?KEY`), so serviceapp's `facturationPassword` and a
134
161
  camel-case `apiKey` are seen. The scans read `.ts .tsx .mts .cts .js .jsx .mjs .cjs` (and `.svelte`,
135
- `.vue` for direct reads) across the whole repository, with test files left out of the reader scan.
162
+ `.vue` for direct reads) across the whole repository, with test files left out of the reader scan:
163
+ `.test.<ext>` and `.spec.<ext>` for each of those eight extensions. A value is read from what is
164
+ written in the committed file: a quoted one ends at its closing quote, an unquoted one at a `#`
165
+ preceded by whitespace. So `API_TOKEN= # filled by the vault` and `API_TOKEN=` are blank,
166
+ `API_TOKEN=abc #def` is `abc`, and `API_TOKEN=abc#def` and `DATABASE_PASSWORD=#Pa55word!` are
167
+ filled. This deliberately differs from dotenv on a `#` with no whitespace before it: dotenv's
168
+ README says "Comments begin where a `#` exists", so dotenv loads `DATABASE_PASSWORD=#Pa55word!` as
169
+ blank, and a secret committed in clear text would go unreported by rule 6.
136
170
 
137
171
  ## In CI
138
172
 
@@ -160,6 +194,10 @@ installs it for this step only.
160
194
  GitHub documents a cap of 10 warning and 10 error annotations per step
161
195
  (<https://docs.github.com/en/rest/checks/runs>): on a repository with more findings, the
162
196
  pull request shows only part of the annotations, and the markdown recap is the complete list.
197
+ Past the cap, sentinel still emits one annotation per finding, and emits FIRST, so it is always
198
+ shown, one summary annotation of the same type: at `error` on serviceapp, `55 blocking findings
199
+ (GitHub shows at most 10 error annotations per step): the full list is in the env report, ...`.
200
+ One per type at most, and none at or under 10.
163
201
 
164
202
  ## What changed from `tools/repo-env`
165
203
 
@@ -193,6 +231,13 @@ the same single difference.
193
231
  - Rule 6 reads every committed env file, not only contracts, and no contract at all is a note
194
232
  rather than a refusal to run.
195
233
  - Every finding has a line, so an annotation lands on it.
234
+ - A comment after a value, after whitespace, is a comment: `KEY= # note` is blank, not a committed
235
+ secret. And a quoted value followed by one, `KEY="" # note`, no longer opens a multi-line value
236
+ that hid every key after it in the file. The original read `KEY= # note` as filled. A `#` with no
237
+ whitespace before it stays part of the value, as in the original and unlike dotenv:
238
+ `KEY=#Pa55word!` is filled (see Sentinel's defaults).
239
+ - Test files are left out of the reader scan for every extension it reads; the original read
240
+ `.ts` and `.tsx` only, and left out `.spec.ts` and `.test.ts`.
196
241
 
197
242
  ## Outside the monorepo: what `tools/repo-env` assumed, and what the port does
198
243
 
@@ -158,10 +158,12 @@ a **layer above the preset**, the way the planning team did:
158
158
  Order matters, sentinel's preset first, or your layer cannot override anything. `--inspect`
159
159
  reports the layer so it stays visible.
160
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.
161
+ A rule can also be set directly in `.oxlintrc.json`. Since 1.5.1 a re-`init` keeps it, in your
162
+ order, and says so, when the preset does not name that rule; `--inspect` lists it as added by
163
+ the module and does not call it drift (see
164
+ [What `--inspect` calls drift](#what---inspect-calls-drift-and-what-it-lists-as-yours)). A rule
165
+ the preset does name is dropped, as before, and named: set in the stub, it would override the
166
+ preset and the baseline both. To change one of those for your module, a layer is the place.
165
167
 
166
168
  Think of it as a queue rather than a fork: a rule promoted into the preset is deleted from the
167
169
  layer. It is the exact mirror of `.oxlintrc.baseline.json`, which holds rules **below** the
@@ -233,6 +235,85 @@ Two limits, both the preset's governance:
233
235
  rather than replace them. The planning layer's notes say overrides do not cross `extends`; that
234
236
  is not what 1.77.0 does.
235
237
 
238
+ ### What `--inspect` calls drift, and what it lists as yours
239
+
240
+ Since 1.5.1, `--inspect` (and `--status`) judge what you add to `.oxlintrc.json` (`rules`,
241
+ `plugins`, `jsPlugins`, `settings`, `overrides`) by the criterion a re-`init` keeps or drops it
242
+ by, so the two never disagree. It rests on how every
243
+ preset is built: each one turns every oxlint category off and names every rule it runs. A rule
244
+ the preset does not name is therefore off unless you turn it on, and setting it takes nothing
245
+ from the preset. Nobody maintains a list: "the preset names it" is read from the preset, by the
246
+ rule oxlint runs (so an alias counts as the rule it stands for).
247
+
248
+ **A rule the preset names is governed.** Whatever value you give it, a re-`init` drops it.
249
+ `--inspect` compares it with the preset:
250
+
251
+ | you set a rule the preset names... | `--inspect` |
252
+ | ----------------------------------------------------------------- | ---------------------------------------- |
253
+ | to the preset's own value and options (`deny` is `error`) | redundant: not drift, still conformant |
254
+ | to `off` | conflict: drift, not conformant |
255
+ | to `warn` where the preset says `error`, or the reverse | conflict: drift, not conformant |
256
+ | with other options (another `no-restricted-imports` pattern list) | conflict: drift, not conformant |
257
+ | under an alias (`eslint/no-var`, `typescript/no-restricted-...`) | the same as the name it stands for |
258
+ | stricter than the preset (`no-console: error` where it is `off`) | conflict: drift, not conformant, for now |
259
+
260
+ **A rule the preset does not name is added by the module.** A re-`init` keeps it, `--inspect`
261
+ does not call it drift, and lists it, with its value, so it stays visible:
262
+
263
+ | you set a rule the preset does not name... | `--inspect` |
264
+ | ------------------------------------------ | --------------------------------------------------------- |
265
+ | at any severity, with any options | added by the module: not drift, listed under `added` |
266
+ | to `off` | the same, listed, and the line says `off` changes nothing |
267
+ | in one of your `overrides` blocks | the same, listed with the block's `files` |
268
+
269
+ console, after its re-`init`:
270
+
271
+ ```
272
+ ✓ console (react) lint — ... added=2 ... conformant=true drift=[] ...
273
+ added by the module (kept by --init, not drift):
274
+ • import/no-cycle — "error": the preset does not name this rule
275
+ • oxc/no-barrel-file — ["error",{"threshold":0}]: the preset does not name this rule
276
+ ```
277
+
278
+ `--inspect --lint --json` carries the same list as `added: [{ rule, value, files?, reason }]`.
279
+
280
+ **The other blocks follow the re-`init` exactly**: what it keeps is added by the module, listed
281
+ under `addedConfig` ("config added by the module" on the console); what it drops as the
282
+ preset's is a collision, and drift.
283
+
284
+ | you add... | `--inspect` |
285
+ | ------------------------------------------------------------- | ---------------------------------------- |
286
+ | a `plugins` entry the preset does not enable | added, listed |
287
+ | a `plugins` entry the preset already enables | redundant: not drift, not listed |
288
+ | a `jsPlugins` entry under a name the preset does not register | added, listed |
289
+ | a `jsPlugins` entry under a name the preset registers | collision: drift (a re-`init` drops it) |
290
+ | a `settings` key the preset does not set | added, listed |
291
+ | a `settings` key the preset sets | collision: drift (a re-`init` drops it) |
292
+ | a key of an `overrides` block other than `files` and `rules` | added, listed with the block's `files` |
293
+ | a block without `files`, or a top-level key such as `env` | drift: a re-`init` does not read it back |
294
+
295
+ host-admin at monorepo#23342 shows both sides. Its planning `jsPlugins` entry and the planning
296
+ rules of its override blocks are additions, kept and listed. Its top-level `rules` set 46 rules
297
+ the react preset names, 41 of them at another value (the TanStack Query set at `error` where the
298
+ preset holds `warn`, `no-explicit-any`, `no-console`, the MUI barrel bans added to
299
+ `no-restricted-imports`, ...), and five of its ten blocks set `no-restricted-imports` or
300
+ `switch-exhaustiveness-check` (six entries): conflicts, drift. A re-`init` drops all 46 top-level ones
301
+ and the 12 override entries the preset governs, the redundant ones included (measured, `--dry-run`). Raising what the
302
+ preset governs belongs in a layer above the preset, where a re-`init` keeps it.
303
+
304
+ Three details:
305
+
306
+ - **Stricter is still a conflict, for now.** Raising a rule the preset governs, even one it
307
+ parks `off`, is reported as drift: the preset is where that decision is made, for every
308
+ module, and a layer above the preset is where a team makes it for itself.
309
+ - **An `overrides` block** is judged the same way, rule by rule. A governed rule in it is
310
+ compared with the preset's override for the same `files` when the preset has one that names
311
+ the rule, else with the preset's top-level value; a rule the preset names only in one of its
312
+ own overrides, set at another scope, is a conflict.
313
+ - **What stays drift**: any top-level key a re-`init` does not read back (`env`, `globals`,
314
+ `categories`, ...), and everything a module on a preset sentinel does not ship adds, since
315
+ there is nothing to compare it with.
316
+
236
317
  ## What a preset does not enforce
237
318
 
238
319
  Every unenforced rule carries a reason, readable with `sentinel --inspect --lint`:
@@ -290,8 +371,8 @@ things follow, and they are the point:
290
371
  predates the migration. A hold is never `off`;
291
372
  - the file is **regenerated, not accumulated**. Fix the last violation, re-run
292
373
  `sentinel --init --lint`, and the rule drops out and returns to the preset severity;
293
- - your own config stays `extends` + `ignorePatterns`, so a rule **you** change by hand is
294
- still reported as drift and is never confused with a hold sentinel made;
374
+ - your own config stays `extends` + `ignorePatterns`, so a preset rule **you** change by hand
375
+ is still reported as drift and is never confused with a hold sentinel made;
295
376
  - `--init` says what it held and how many violations each hold covers, so the debt is
296
377
  announced rather than discovered;
297
378
  - a hold is written under the **preset's** name for the rule (since 1.5.1). Oxlint reports
@@ -23,16 +23,16 @@ sentinel --run # no target -> every wired tar
23
23
  Read it by target, since that is what you pick first. `--report` and `--status` are deprecated
24
24
  everywhere (`--run --json` and `--inspect` answer them), and `--migrate` is refused everywhere.
25
25
 
26
- | Target | `--init` writes | `--run` does | `--inspect` shows |
27
- | ------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
28
- | `--lint` | stub + scripts + nx metadata, removes the ESLint config and the module's ESLint deps, then autofixes once | lints; `--fix` autofixes in the same pass | rule count, what is disabled or downgraded and **why**, drift |
29
- | `--format` | materializes the preset + scripts + nx metadata, removes the Prettier config and deps, then formats once | **checks**; `--fix` writes | effective options, every departure from the standard and why |
30
- | `--typescript` | writes/extends the tsconfig + `typecheck` script | typechecks | resolved options, what is deferred, drift |
31
- | `--build` | points the module's Vite config at sentinel's toolchain + scripts + nx metadata | builds | runner, config file, whose Vite, how many overrides, adoption |
32
- | `--dev` | adopts the BUILD too: one plan writes both `build` and `serve` | serves; long-running, so never swept by an unqualified `--run` | refuses, and sends you to `--inspect --build`: it is that config |
33
- | `--test` | the Vitest config + scripts + nx metadata, and migrates every spec file | runs Vitest | runner, which configs were found, adoption state, drift |
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
- | `--env` | refuses: it writes no config (an optional `env.config.mjs` at the repository root declares exceptions) | checks the whole REPOSITORY, from its git root only; exits 1 on a blocking finding at `SENTINEL_ENV_LEVEL=error`; an invalid level refuses the whole run, a bare root `--run` included | refuses: nothing to inspect |
26
+ | Target | `--init` writes | `--run` does | `--inspect` shows |
27
+ | ------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
28
+ | `--lint` | stub + scripts + nx metadata, removes the ESLint config and the module's ESLint deps, then autofixes once | lints; `--fix` autofixes in the same pass | rule count, what is disabled or downgraded and **why**, what the module adds, drift |
29
+ | `--format` | materializes the preset + scripts + nx metadata, removes the Prettier config and deps, then formats once | **checks**; `--fix` writes | effective options, every departure from the standard and why |
30
+ | `--typescript` | writes/extends the tsconfig + `typecheck` script | typechecks | resolved options, what is deferred, drift |
31
+ | `--build` | points the module's Vite config at sentinel's toolchain + scripts + nx metadata | builds | runner, config file, whose Vite, how many overrides, adoption |
32
+ | `--dev` | adopts the BUILD too: one plan writes both `build` and `serve` | serves; long-running, so never swept by an unqualified `--run` | refuses, and sends you to `--inspect --build`: it is that config |
33
+ | `--test` | the Vitest config + scripts + nx metadata, and migrates every spec file | runs Vitest | runner, which configs were found, adoption state, drift |
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
+ | `--env` | refuses: it writes no config (an optional `env.config.mjs` at the repository root declares exceptions) | checks the whole REPOSITORY, from its git root only; exits 1 on a blocking finding at `SENTINEL_ENV_LEVEL=error`; an invalid level refuses the whole run, a bare root `--run` included | refuses: nothing to inspect |
36
36
 
37
37
  `--json` works for every verb, and stdout carries **only** the envelope, so `| jq` always
38
38
  parses. `--dry-run` applies to `--init` and writes nothing. Besides the config diffs, it says
@@ -141,8 +141,10 @@ Every cell is a test in `tests/e2e/env.e2e.test.ts`, against the built CLI, on g
141
141
  serviceapp-shaped repository (not nx, committed `variables/*.env`), the same with a package below
142
142
  its root, an nx workspace with a module, and a directory in no repository. `SENTINEL_ENV_LEVEL` and
143
143
  `GITHUB_ACTIONS` are pinned on every call, and "unset" removes the variable from the child's
144
- environment, which is a different input from the empty string. 52 cells: the 31 of the first draft,
145
- one more refused verb, 19 new ones, and the rule 1 note.
144
+ environment, which is a different input from the empty string. 58 cells: the 31 of the first draft,
145
+ one more refused verb, 19 new ones, the rule 1 note, the 5 of the first adopter's feedback
146
+ (a refused config's row, in console and `--json`; test files of every extension; a comment after a
147
+ value; the summary annotation past GitHub's cap), and a secret whose value starts with `#`.
146
148
 
147
149
  ### verb, from the repository root
148
150
 
@@ -201,8 +203,21 @@ fails nothing would be decoration; none is, and every new cell fails under at le
201
203
  | `--json` annotations on stderr | 2 |
202
204
  | the rule 1 note, never emitted (unit + e2e) | 3 |
203
205
  | the rule 1 note, always emitted (unit + e2e) | 2 |
206
+ | test files of every extension (suffixes reverted) | 2 |
207
+ | a comment after a value (parser reverted) | 5 |
208
+ | a `#` with no whitespace before it (dotenv's rule) | 4 |
209
+ | the refusal's `error` on the row (adapter reverted) | 2 |
210
+ | no `undefined` key on the row (orchestrate reverted) | 1 |
211
+ | a refusal names the kind found (define reverted) | 4 |
212
+ | the summary annotation past the cap (report reverted) | 4 |
204
213
 
205
214
  The `COPY` fix is unit (`env-checks-fixes`): reverted, its 3 tests fail and the 66 others pass.
215
+ The six feedback controls ran unit and e2e together over the six env suites (187 tests, 193 for
216
+ the last one, once the summary cells existed): each failed only its own new cells, and no cell that existed before. The two
217
+ summary-annotation guards (nothing at exactly 10, nothing at `info`) pass reverted, as guards do.
218
+ The `#` control restored dotenv's rule (an unquoted value ends at its first `#`) over the six env
219
+ suites, 203 tests: its 4 new cells fail, 199 pass. One existing cell changed, on purpose: it
220
+ asserted dotenv's reading of `B_TOKEN=#c` as blank, which is the reading this rule refuses.
206
221
 
207
222
  ### Against real repositories, from the packed tarball
208
223
 
@@ -240,7 +255,7 @@ Rule 8 against a real nx ran on the monorepo only (nx installed in a throwaway w
240
255
 
241
256
  ## The `--init` matrix for 1.5.1, 29 October 2026
242
257
 
243
- Six fixes, then a seventh (below) found by the alpha.1 validation. Found by re-initializing the monorepo's 63 adopted modules with 1.5.1-alpha.0. Every
258
+ Six fixes, then a seventh (below) found by the alpha.1 validation and an eighth found by the alpha.2 one. Found by re-initializing the monorepo's 63 adopted modules with 1.5.1-alpha.0. Every
244
259
  cell below is a test in this repo, against the built CLI unless it says unit, and every fix has a
245
260
  negative control: the fix reverted in the source, the cells run again, and the cells that must
246
261
  fail do. No existing cell changed: against the alpha, the test tree gains lines and loses none
@@ -384,6 +399,87 @@ Checked in the monorepo at 7177eb86038: none of the 66 committed `.oxlintrc*.jso
384
399
  team layers spells a governed rule under another name, so no adopted module carries the gap
385
400
  today.
386
401
 
402
+ ### 8. `--inspect` judges a module's own rules as a re-init does (`inspect-lint-module-additions`, unit `lint-module-rule-standing`)
403
+
404
+ Found by the alpha.2 sweep: `--init` kept console's `import/no-cycle` and `oxc/no-barrel-file`
405
+ ("kept 2 rule(s)"), and `--inspect` reported console's lint `status: failed, conformant: false,
406
+ drift: ["rules"]`, because any `rules` key was drift. The criterion is now the re-init's
407
+ (`governedRules`, by `canonicalRuleName`): it holds because every preset turns every oxlint
408
+ category off and names every rule it runs, checked first on all six shipped presets against
409
+ oxlint 1.77.0's seven categories (a unit cell), and on the binary: `no-const-assign`, a
410
+ correctness rule react does not name, fires under bare oxlint and not under console's stub.
411
+
412
+ | the module's `.oxlintrc.json` sets, react preset | `conformant`, `drift` | listed in `added` |
413
+ | ---------------------------------------------------------------------------------- | ---------------------------- | ---------------------- |
414
+ | console's two rules, which the preset does not name | true, `[]` | both, with values |
415
+ | an unnamed rule at `off` | true, `[]` | yes, "changes nothing" |
416
+ | an unnamed rule in an `overrides` block | true, `[]` | yes, with `files` |
417
+ | `no-var` at the preset's `error` (unit: also `deny`, `["error"]`, `eslint/no-var`) | true, `[]` | no |
418
+ | unit: `no-restricted-imports` at the preset's options | true, `[]` | no |
419
+ | `no-var` at `off`, at `warn` | false, `["rules"]` | no |
420
+ | `no-restricted-imports` with other options (unit: also as `typescript/...`) | false, `["rules"]` | no |
421
+ | `eslint/no-var: off` | false, `["rules"]` | no |
422
+ | `no-console: error`, stricter than the preset's `off` (a conflict for now) | false, `["rules"]` | no |
423
+ | unit: a rule the preset names only in its test override, set at the top level | false, `["rules"]` | no |
424
+ | the same rows inside an `overrides` block (unit; e2e: `no-var: off`) | the same, `["overrides"]` | the same |
425
+ | unit: the preset override's rule at its own value, for its own `files` | true, `[]` | no |
426
+ | unit: rules under a preset sentinel does not ship | drift `["rules"]`, as before | n/a |
427
+ | `--status`, console's two rules | `1/1 conformant` | |
428
+ | re-init of console's shape, then `--inspect` | kept 2, then true, `[]` | both |
429
+
430
+ Negative control, the source reverted (`git stash` of `src/`, new tests kept, rebuilt): 27 of
431
+ the 47 new cells fail, every cell that expects "not drift" or the `added` list. The 20 that pass
432
+ are the conflict cells, which were drift before too, the category check, the unshipped preset
433
+ and the block carrying `env`. No existing cell changed: `lint-adoption-read`,
434
+ `adoption-contract` and `lint.e2e` set `no-debugger: off` and `no-console: off` on nest, both
435
+ named there at another value, so they are conflicts and still drift.
436
+
437
+ On the monorepo, `--inspect --json` from each of the 63 modules of the alpha.2 sweep worktree,
438
+ with this branch's built CLI, nothing written (`git status` hash unchanged): console's lint row
439
+ goes `failed` to `ok`, `conformant` true, `drift` `[]`, its two rules listed, and the workspace
440
+ coverage footer of console's own run reads 4 of 4 adopted roles conformant instead of 3. The 62 other modules differ from the
441
+ alpha.2 run only by an empty `added: []` on their lint row.
442
+
443
+ **The other blocks a re-init reads back**, the same criterion, from `moduleOwnBlocks` itself (no
444
+ second definition): what it keeps is an addition, listed under `addedConfig`; what it drops as
445
+ the preset's is a collision, and drift. Found on host-admin at monorepo#23342, whose planning
446
+ `jsPlugins` entry a re-init keeps and `--inspect` called drift.
447
+
448
+ | the module's `.oxlintrc.json` adds, react preset | `conformant`, `drift` | listed in `addedConfig` |
449
+ | ------------------------------------------------------------------------------ | ---------------------- | ------------------------------------- |
450
+ | host-admin's planning jsPlugin (a path) and a named one | true, `[]` | yes |
451
+ | a `plugins` entry the preset does not enable | true, `[]` | yes |
452
+ | `plugins: ["import"]`, already enabled | true, `[]` | no |
453
+ | a `settings` key the preset does not set | true, `[]` | yes |
454
+ | an override block key beyond `files` and `rules` (`env`) | true, `[]` | yes, with `files` |
455
+ | a jsPlugin under a name the preset registers (`nx`), or the preset's own path | false, `["jsPlugins"]` | no |
456
+ | unit: a `settings` key the preset sets (synthetic preset: none ships settings) | drift `["settings"]` | no |
457
+ | unit: `plugins` not a list of names, `settings` not an object | drift | n/a |
458
+ | unit: an override block without `files`; a top-level `env` or `categories` | drift | n/a |
459
+ | an addition at `off` in an override block | true, `[]` | in `added`, without "changes nothing" |
460
+
461
+ host-admin's committed config at #23342, in a fixture copy with its planning plugin, before and
462
+ after this change: `drift: ["jsPlugins", "rules", "overrides"]` becomes `["rules", "overrides"]`,
463
+ with 18 rule additions and the jsPlugin listed. What remains is real: its top-level `rules` set
464
+ 46 rules the react preset names, 41 at another value (the TanStack Query set at `error`, the
465
+ `no-unsafe-*` set, `no-explicit-any`, `no-console`, `no-restricted-imports` with the MUI barrel
466
+ bans, ...), and 5 of its 10 blocks set `no-restricted-imports` or
467
+ `switch-exhaustiveness-check` (6 entries). A re-init's `--dry-run` on the same copy drops those
468
+ 46 rules and 12 governed override entries and keeps the jsPlugin and 4 blocks.
469
+
470
+ Negative control for this part, the source at 3060996 (`git stash` of `src/`, rebuilt): 15 of
471
+ the 64 cells fail, the 12 new ones that expect "not drift" or an `addedConfig` list, and the
472
+ three existing cells of 3060996 whose expectation this changes. The 49 that pass include the
473
+ collision cells and the unread shapes, drift before too. Four cells of 3060996 changed, all in
474
+ this PR's own new files and all asserting what this part reverses: the override block with `env`
475
+ (drift then, an addition now), the `settings: { team }` cell (replaced by `env` and `categories`,
476
+ which a re-init does not read back), and the two `readModuleAdditions` cells (it now returns
477
+ `{ rules, config }`).
478
+
479
+ The 63-module inspect, re-run with this build: identical to the run above for every module,
480
+ console included, except an empty `addedConfig: []` on each lint row; `git status` hash
481
+ unchanged, 63 of 63 exit 0.
482
+
387
483
  ### against the consumer, the 63 adopted modules
388
484
 
389
485
  The packed branch, through a pnpm `file:` override, on a fresh worktree of the monorepo at
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hublo/sentinel",
3
- "version": "1.6.0-alpha.0",
3
+ "version": "1.6.0-alpha.2",
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",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/roles/env/configs/define.ts"],"sourcesContent":["/**\n * `defineEnv`: what a repository writes to declare its own env facts, all of them optional.\n *\n * ```js\n * // env.config.mjs, at the repository root\n * import { defineEnv } from '@hublo/sentinel/env'\n *\n * export default defineEnv({\n * externallyProvided: ['NODE_ENV'],\n * allowNonBlankedSecrets: ['apps/front/x/.env.example:LEGACY_API_KEY'],\n * })\n * ```\n *\n * A repository that does not install sentinel, and runs it with `npx`, cannot import this module:\n * its own `node_modules` has no `@hublo/sentinel`. So the file may export the same object PLAIN,\n * and the role passes it through this function itself. Both spellings mean the same thing, and the\n * function exists for the one that can import it: it validates at the moment the file is written,\n * in an editor that knows the types.\n *\n * ## What a repository declares, and what stays sentinel's\n *\n * A repository declares FACTS about itself: which variables its CI injects, which committed value\n * was reviewed and is not a secret, which Dockerfile must run as root, where its code lives. It\n * does not redefine what a secret NAME looks like or how code reads env, which are the same in\n * every repository and ship in `presets/shared.json`.\n *\n * Two semantics, stated once:\n *\n * - `readers`, `envFiles` and `contracts` ADD to what sentinel ships, so no repository loses\n * `getOrFail` or `**\\/.env` by declaring a reader or a glob of its own;\n * - `scan` and `references` REPLACE, key by key: a repository naming its `roots` says where its\n * code is, and keeping `.` beside them would scan everything anyway.\n *\n * ⛔ An unknown key is refused, by name. The file is `.mjs`, so nothing type-checks it, and a key\n * spelled `allowNonBlankedSecret` would otherwise be ignored in silence while the finding it was\n * meant to downgrade kept failing the build.\n */\n\n/** A source scope a repository may narrow, every key optional. */\nexport interface EnvScopeInput {\n roots?: string[]\n ext?: string[]\n excludeSuffixes?: string[]\n excludeDirs?: string[]\n}\n\n/** What `env.config.mjs` exports. */\nexport interface EnvInput {\n /** Required variables in no contract because CI injects them or a secret store provides them. */\n externallyProvided?: string[]\n /** Declared variables consumed outside scannable source, so never reported dead (rule 4). */\n knownExternalVars?: string[]\n /** The same, by pattern, for a whole category such as `/_DATABASE_URL$/`. */\n knownExternalPatterns?: (RegExp | string)[]\n /** `<file>:<VAR>` locations whose committed value was reviewed and is not a secret (rule 6). */\n allowNonBlankedSecrets?: string[]\n /** Committed real env files permitted anyway, such as a test fixture (rule 2). */\n allowedCommittedEnvFiles?: string[]\n /** Globs of env files to read, ADDED to `**\\/.env`, `**\\/.env.*` and `**\\/*.env`. */\n envFiles?: string[]\n /** Globs of contracts, ADDED to `**\\/*.env*.example`. */\n contracts?: string[]\n /** Where the named readers are looked for (rules 1 and 3). Replaces, key by key. */\n scan?: EnvScopeInput\n /** Where direct `process.env` reads are looked for (rules 3 and 4). Replaces, key by key. */\n references?: EnvScopeInput\n /** Reader names ADDED to sentinel's: `required` throw when absent, `optional` default. */\n readers?: { required?: string[]; optional?: string[] }\n /** Docker rules: Dockerfiles allowed to run as root, and path prefixes not checked at all. */\n docker?: { allowRoot?: string[]; excludePaths?: string[] }\n}\n\nconst TOP_LEVEL_KEYS = new Set<string>([\n 'externallyProvided',\n 'knownExternalVars',\n 'knownExternalPatterns',\n 'allowNonBlankedSecrets',\n 'allowedCommittedEnvFiles',\n 'envFiles',\n 'contracts',\n 'scan',\n 'references',\n 'readers',\n 'docker',\n])\nconst SCOPE_KEYS = new Set(['roots', 'ext', 'excludeSuffixes', 'excludeDirs'])\nconst NESTED_KEYS: Readonly<Record<string, ReadonlySet<string>>> = {\n scan: SCOPE_KEYS,\n references: SCOPE_KEYS,\n readers: new Set(['required', 'optional']),\n docker: new Set(['allowRoot', 'excludePaths']),\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** Every problem with one list: it must be an array of non-empty strings. */\nfunction stringListProblems(path: string, value: unknown): string[] {\n if (value === undefined) return []\n if (!Array.isArray(value)) return [`${path} must be an array of strings`]\n return value.some((entry) => typeof entry !== 'string' || entry === '')\n ? [`${path} must hold non-empty strings only`]\n : []\n}\n\n/** Every problem with the input, each naming the key, so one run lists them all. */\nfunction problemsOf(input: Record<string, unknown>): string[] {\n const problems: string[] = []\n for (const [key, value] of Object.entries(input)) {\n if (!TOP_LEVEL_KEYS.has(key)) {\n problems.push(`unknown key \"${key}\"`)\n continue\n }\n const nested = NESTED_KEYS[key]\n if (nested === undefined) {\n if (key === 'knownExternalPatterns') {\n if (value !== undefined && !Array.isArray(value)) {\n problems.push(`${key} must be an array of RegExp or strings`)\n } else if (\n Array.isArray(value) &&\n value.some((entry) => !(entry instanceof RegExp) && typeof entry !== 'string')\n ) {\n problems.push(`${key} must hold RegExp or strings only`)\n }\n } else {\n problems.push(...stringListProblems(key, value))\n }\n continue\n }\n if (value === undefined) continue\n if (!isRecord(value)) {\n problems.push(`${key} must be an object`)\n continue\n }\n for (const [inner, innerValue] of Object.entries(value)) {\n if (!nested.has(inner)) problems.push(`unknown key \"${key}.${inner}\"`)\n else problems.push(...stringListProblems(`${key}.${inner}`, innerValue))\n }\n }\n return problems\n}\n\n/**\n * Validate a repository's env declaration and return it unchanged.\n *\n * The defaults are NOT merged in here. They ship with sentinel and are applied when the role runs,\n * so changing one reaches every repository at its next sentinel version instead of leaving a copy\n * frozen in each config. That also makes this function idempotent, which is what lets the role\n * pass a plain exported object through it a second time.\n *\n * @param input the declaration; omitted, the repository declares nothing\n * @returns the same declaration\n * @throws naming every unknown key and every malformed value at once\n */\nexport function defineEnv(input: EnvInput = {}): EnvInput {\n if (!isRecord(input)) throw new Error('env config must export an object')\n const problems = problemsOf(input)\n if (problems.length > 0) {\n throw new Error(`invalid env config:\\n - ${problems.join('\\n - ')}`)\n }\n return input\n}\n"],"mappings":";AAwEA,IAAM,iBAAiB,oBAAI,IAAY;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AACD,IAAM,aAAa,oBAAI,IAAI,CAAC,SAAS,OAAO,mBAAmB,aAAa,CAAC;AAC7E,IAAM,cAA6D;AAAA,EACjE,MAAM;AAAA,EACN,YAAY;AAAA,EACZ,SAAS,oBAAI,IAAI,CAAC,YAAY,UAAU,CAAC;AAAA,EACzC,QAAQ,oBAAI,IAAI,CAAC,aAAa,cAAc,CAAC;AAC/C;AAEA,SAAS,SAAS,OAAkD;AAClE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAGA,SAAS,mBAAmB,MAAc,OAA0B;AAClE,MAAI,UAAU,OAAW,QAAO,CAAC;AACjC,MAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,QAAO,CAAC,GAAG,IAAI,8BAA8B;AACxE,SAAO,MAAM,KAAK,CAAC,UAAU,OAAO,UAAU,YAAY,UAAU,EAAE,IAClE,CAAC,GAAG,IAAI,mCAAmC,IAC3C,CAAC;AACP;AAGA,SAAS,WAAW,OAA0C;AAC5D,QAAM,WAAqB,CAAC;AAC5B,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,QAAI,CAAC,eAAe,IAAI,GAAG,GAAG;AAC5B,eAAS,KAAK,gBAAgB,GAAG,GAAG;AACpC;AAAA,IACF;AACA,UAAM,SAAS,YAAY,GAAG;AAC9B,QAAI,WAAW,QAAW;AACxB,UAAI,QAAQ,yBAAyB;AACnC,YAAI,UAAU,UAAa,CAAC,MAAM,QAAQ,KAAK,GAAG;AAChD,mBAAS,KAAK,GAAG,GAAG,wCAAwC;AAAA,QAC9D,WACE,MAAM,QAAQ,KAAK,KACnB,MAAM,KAAK,CAAC,UAAU,EAAE,iBAAiB,WAAW,OAAO,UAAU,QAAQ,GAC7E;AACA,mBAAS,KAAK,GAAG,GAAG,mCAAmC;AAAA,QACzD;AAAA,MACF,OAAO;AACL,iBAAS,KAAK,GAAG,mBAAmB,KAAK,KAAK,CAAC;AAAA,MACjD;AACA;AAAA,IACF;AACA,QAAI,UAAU,OAAW;AACzB,QAAI,CAAC,SAAS,KAAK,GAAG;AACpB,eAAS,KAAK,GAAG,GAAG,oBAAoB;AACxC;AAAA,IACF;AACA,eAAW,CAAC,OAAO,UAAU,KAAK,OAAO,QAAQ,KAAK,GAAG;AACvD,UAAI,CAAC,OAAO,IAAI,KAAK,EAAG,UAAS,KAAK,gBAAgB,GAAG,IAAI,KAAK,GAAG;AAAA,UAChE,UAAS,KAAK,GAAG,mBAAmB,GAAG,GAAG,IAAI,KAAK,IAAI,UAAU,CAAC;AAAA,IACzE;AAAA,EACF;AACA,SAAO;AACT;AAcO,SAAS,UAAU,QAAkB,CAAC,GAAa;AACxD,MAAI,CAAC,SAAS,KAAK,EAAG,OAAM,IAAI,MAAM,kCAAkC;AACxE,QAAM,WAAW,WAAW,KAAK;AACjC,MAAI,SAAS,SAAS,GAAG;AACvB,UAAM,IAAI,MAAM;AAAA,MAA4B,SAAS,KAAK,QAAQ,CAAC,EAAE;AAAA,EACvE;AACA,SAAO;AACT;","names":[]}