@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/README.md +4 -3
- package/dist/bin/sentinel.js +23 -10
- package/dist/{chunk-NAFI7OLI.js → chunk-4EIY7TTR.js} +22 -8
- package/dist/chunk-4EIY7TTR.js.map +1 -0
- package/dist/{chunk-P7NYSMLS.js → chunk-67QLOX6F.js} +2737 -28
- package/dist/{chunk-5VNYQIFD.js → chunk-EESVRILJ.js} +1 -1
- package/dist/{chunk-5VNYQIFD.js.map → chunk-EESVRILJ.js.map} +1 -1
- package/dist/{chunk-PC5R4DX5.js → chunk-R7DS7VN4.js} +178 -2658
- package/dist/index.js +3 -3
- package/dist/roles/env/config.js +1 -1
- package/dist/roles/test/nest/toolchain.js +1 -1
- package/dist/roles/test/react/toolchain.js +1 -1
- package/dist/roles/test/shared-test-config.js +1 -1
- package/dist/{validate-5SD5CTTO.js → validate-NP7X6XCG.js} +1 -1
- package/docs/env-adoption.md +46 -1
- package/docs/lint-adoption.md +87 -6
- package/docs/using-sentinel.md +10 -10
- package/docs/validating-a-change.md +99 -3
- package/package.json +1 -1
- package/dist/chunk-NAFI7OLI.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -6,13 +6,13 @@ import {
|
|
|
6
6
|
registerAdapters,
|
|
7
7
|
resolve,
|
|
8
8
|
setDefaultRunner
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-R7DS7VN4.js";
|
|
10
10
|
import {
|
|
11
11
|
BaseAdapter
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-67QLOX6F.js";
|
|
13
13
|
import "./chunk-JRZI5IIN.js";
|
|
14
14
|
import "./chunk-7E6YTTAK.js";
|
|
15
|
-
import "./chunk-
|
|
15
|
+
import "./chunk-4EIY7TTR.js";
|
|
16
16
|
import "./chunk-O7REVMOC.js";
|
|
17
17
|
import "./chunk-QXFCZON7.js";
|
|
18
18
|
export {
|
package/dist/roles/env/config.js
CHANGED
package/docs/env-adoption.md
CHANGED
|
@@ -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
|
|
package/docs/lint-adoption.md
CHANGED
|
@@ -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
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
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
|
package/docs/using-sentinel.md
CHANGED
|
@@ -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.
|
|
145
|
-
one more refused verb, 19 new ones,
|
|
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.
|
|
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":[]}
|