@hublo/sentinel 1.1.0-alpha.3 → 1.1.0-alpha.5
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 +184 -41
- package/dist/bin/sentinel.js +262 -218
- package/dist/chunk-NAUACDXT.js +4707 -0
- package/dist/index.d.ts +92 -37
- package/dist/index.js +1 -1
- package/oxlint/nest.json +71 -159
- package/oxlint/node.json +12 -21
- package/oxlint/react-lib.json +376 -0
- package/oxlint/react.json +138 -221
- package/oxlint/svelte.json +209 -0
- package/oxlint/tools.json +197 -0
- package/package.json +25 -15
- package/dist/chunk-QPT6V2GK.js +0 -2116
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
`sentinel` is a standalone, semver-versioned package (published to a registry, consumed by a repo as a normal dependency) that unifies a repo's tooling, config, and quality checks into one place, so projects stop copying config files everywhere and stop carrying a pile of duplicated tooling dependencies.
|
|
6
6
|
|
|
7
|
-
> Status: **foundation
|
|
7
|
+
> Status: **the foundation, the TypeScript tool and the lint tool are shipped** (`@hublo/sentinel` on npm). The rest of this README is the design reference for the tools still to come, added one at a time on top of this foundation.
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -19,7 +19,7 @@ A large monorepo accumulates:
|
|
|
19
19
|
|
|
20
20
|
## What it gives you
|
|
21
21
|
|
|
22
|
-
**The shift:** instead of today's **big-bang** (change every project and every tool at once, untested in isolation), evolution is **app-scoped and versioned**, evolving a project is a **version bump**, a new/swapped tool is just a **new runner**, and a new stack a **new
|
|
22
|
+
**The shift:** instead of today's **big-bang** (change every project and every tool at once, untested in isolation), evolution is **app-scoped and versioned**, evolving a project is a **version bump**, a new/swapped tool is just a **new runner**, and a new stack a **new preset**. No repo-wide edits.
|
|
23
23
|
|
|
24
24
|
- **One source of truth for config** — every project just `extends @hublo/sentinel/...`; the actual rules live in one versioned place. Change a rule once, everyone gets it on the next version bump.
|
|
25
25
|
- **One source of truth for tooling dependencies** — a project depends on `@hublo/sentinel`, not on a scattered pile of eslint / vitest / plugin devDeps. Bump one version and the whole toolchain moves, atomically, tested in isolation first.
|
|
@@ -43,23 +43,47 @@ A large monorepo accumulates:
|
|
|
43
43
|
|
|
44
44
|
sentinel writes **standard config files** into a project (each just `extends` a sentinel preset) and runs the checks. Your editor and the tools read those **normal files natively**, they never call sentinel at runtime, so nothing is coupled to it or brittle.
|
|
45
45
|
|
|
46
|
-
> **Shipped today:**
|
|
46
|
+
> **Shipped today:** the **TypeScript**, **lint** and **format** tools. The `--test` / `--build`
|
|
47
|
+
> snippets below illustrate the end state and land with their own ticket.
|
|
47
48
|
|
|
48
49
|
**Step 1 — put a module on sentinel** (once per module, by a dev; the files are committed). Run from the app dir; `--init` does it all, nothing is hand-edited:
|
|
49
50
|
|
|
50
51
|
```bash
|
|
51
|
-
sentinel --init --
|
|
52
|
+
sentinel --init --preset <react|nest|node|svelte> # every role sentinel ships
|
|
52
53
|
pnpm install # fetch what --init declared, then commit
|
|
53
54
|
```
|
|
54
55
|
|
|
56
|
+
With no target named, `--init` adopts every role, in the order that makes the result correct:
|
|
57
|
+
the linter's autofix runs, then the formatter runs **last** and formats everything every role
|
|
58
|
+
wrote. Naming one target (`--init --lint`) adopts just that role and leaves the rest alone.
|
|
59
|
+
|
|
55
60
|
`--init` writes the config stubs, the `typecheck`/`lint`/... scripts, and pins the `@hublo/sentinel` devDependency into the module (no manual `pnpm add`); it scaffolds a `package.json` for a `project.json`-only module. It also applies, once, the workspace prep that module needs at the root, only when the root's own config shows it is needed (e.g. an i18next singleton override when the repo runs a second TypeScript, a release-age allow-list when the repo uses that pnpm gate). See the [adoption cheat sheet](docs/typescript-adoption.md) for the full list.
|
|
56
61
|
|
|
57
62
|
Those files are tiny, they just point at a sentinel preset. What gets committed:
|
|
58
63
|
|
|
59
|
-
```
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
```jsonc
|
|
65
|
+
// .oxlintrc.json — generated. Adoption REPLACES eslint rather than sitting beside it, so
|
|
66
|
+
// the module's eslint config is DELETED and this is the only linter config it keeps.
|
|
67
|
+
// The RULES live in the preset, so a preset change arrives by reinstalling. `ignorePatterns`
|
|
68
|
+
// cannot: it does not cross an `extends` boundary, so it is written here, which is what
|
|
69
|
+
// makes the file correct for your editor and for `oxlint -c` on its own.
|
|
70
|
+
{
|
|
71
|
+
"extends": ["./node_modules/@hublo/sentinel/oxlint/react.json"],
|
|
72
|
+
"ignorePatterns": ["**/dist/**", "**/node_modules/**", "..."],
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```jsonc
|
|
77
|
+
// .oxfmtrc.json — generated, and the one config that is NOT a stub: oxfmt has no `extends`
|
|
78
|
+
// and ignores the key silently, so the preset's values are written in. Anything the module
|
|
79
|
+
// deliberately formats differently is declared in `$sentinel.local` and survives a re-init.
|
|
80
|
+
{
|
|
81
|
+
"$sentinel": { "preset": "base", "version": "1.1.0", "local": [] },
|
|
82
|
+
"printWidth": 80,
|
|
83
|
+
"semi": false,
|
|
84
|
+
"singleQuote": true,
|
|
85
|
+
"sortImports": { "groups": ["builtin", "external", "internal", ["parent", "index"], "sibling"] },
|
|
86
|
+
}
|
|
63
87
|
```
|
|
64
88
|
|
|
65
89
|
```jsonc
|
|
@@ -68,7 +92,7 @@ export default react
|
|
|
68
92
|
{
|
|
69
93
|
"extends": ["../../tsconfig.base.json", "@hublo/sentinel/tsconfig/react"],
|
|
70
94
|
"compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] } },
|
|
71
|
-
"include": ["src"]
|
|
95
|
+
"include": ["src"],
|
|
72
96
|
}
|
|
73
97
|
```
|
|
74
98
|
|
|
@@ -80,11 +104,15 @@ And the app's `package.json` scripts route every check through the one CLI (run
|
|
|
80
104
|
// package.json
|
|
81
105
|
{
|
|
82
106
|
"scripts": {
|
|
83
|
-
|
|
84
|
-
|
|
107
|
+
// `lint` checks BOTH, the way `prettier --check . && eslint .` used to. In the :fix
|
|
108
|
+
// pair the formatter runs last: the linter's autofix rewrites code.
|
|
109
|
+
"lint": "sentinel --run --lint && sentinel --run --format",
|
|
110
|
+
"lint:fix": "sentinel --run --lint --fix && sentinel --run --format --fix",
|
|
111
|
+
"format": "sentinel --run --format",
|
|
112
|
+
"format:fix": "sentinel --run --format --fix",
|
|
85
113
|
"typecheck": "sentinel --run --typescript",
|
|
86
|
-
"test": "sentinel --run --test"
|
|
87
|
-
}
|
|
114
|
+
"test": "sentinel --run --test",
|
|
115
|
+
},
|
|
88
116
|
}
|
|
89
117
|
```
|
|
90
118
|
|
|
@@ -119,10 +147,14 @@ pnpm dlx @hublo/sentinel@<exact-version> --inspect --typescript --module <name>
|
|
|
119
147
|
|
|
120
148
|
## Docs & cheat sheets
|
|
121
149
|
|
|
150
|
+
- [`docs/performance.md`](docs/performance.md) — the measured baseline (`pnpm bench`), so a regression is something you can see rather than argue about, and what is deliberately left unoptimised.
|
|
151
|
+
- [`docs/using-sentinel.md`](docs/using-sentinel.md) — every verb for every role, scoping a run to some files, what a fully adopted module looks like, and how versions move between modules.
|
|
152
|
+
- [`docs/lint-adoption.md`](docs/lint-adoption.md) — migrating a module from ESLint to Oxlint: the two steps, what `--init` writes and removes, the two react tiers, and the failures worth recognising.
|
|
153
|
+
- [`docs/format-adoption.md`](docs/format-adoption.md) — migrating a module from Prettier to oxfmt: why this config is materialized rather than a stub, how a module keeps its own formatting, and what did not survive the move.
|
|
122
154
|
- [`docs/typescript-adoption.md`](docs/typescript-adoption.md) — the adoption cheat sheet: the two adoption steps, the command model (verb x type x location), options, reading a report, and troubleshooting.
|
|
123
155
|
- [`docs/typescript-traces.md`](docs/typescript-traces.md) — a **generated, versioned** reference of live command + output traces (every verb, option, config result and edge case) against the mock monorepo. Regenerate after CLI changes with `pnpm docs:traces`.
|
|
124
156
|
|
|
125
|
-
## Architecture: `target → runner →
|
|
157
|
+
## Architecture: `target → runner → preset`
|
|
126
158
|
|
|
127
159
|
Every check is described by three layers:
|
|
128
160
|
|
|
@@ -130,12 +162,12 @@ Every check is described by three layers:
|
|
|
130
162
|
| -------------------- | --------------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
131
163
|
| **target** (role) | `--lint`, `--typescript`, … | the _kind_ of check, stable | `lint` `format` `typescript` `build` `test` `static-analysis` `runtime-analysis` |
|
|
132
164
|
| **runner** (adapter) | `--runner=<tool>` | the _tool_ behind the target, swappable | lint: `eslint`/`biome`/`oxlint` · types: `tsc`/`tsgo` · build: `vite` · test: `vitest` |
|
|
133
|
-
| **
|
|
165
|
+
| **preset** (preset) | detected / `--preset` | the _variant_ per stack (strict by default) | `react` `nest` `node` `svelte` (svelte: lint only, no tsconfig preset yet) |
|
|
134
166
|
|
|
135
|
-
A run is `target × runner ×
|
|
167
|
+
A run is `target × runner × preset`, e.g. `sentinel --run --lint --runner=eslint` from the `host-admin` dir.
|
|
136
168
|
|
|
137
169
|
- `--runner` is an **optional override on a central default**. `sentinel --lint` uses the configured default runner, so swapping a tool globally is a one-place change; `--runner=biome` overrides for a single run (great for benchmarking eslint vs biome vs oxlint, and for gradual migration).
|
|
138
|
-
- The **
|
|
170
|
+
- The **preset is detected** from the module's dependencies (deterministic: a framework dep → its preset, else `node`), and **`--preset` overrides it**. Detection can be wrong where deps are hoisted at the repo root (it returns `node`), so pass `--preset` for `--init` (that is where the preset is chosen). `--run`/`--report` don't depend on it, they run the tool on the committed config.
|
|
139
171
|
|
|
140
172
|
### Adapters & the engine (ports & adapters)
|
|
141
173
|
|
|
@@ -143,14 +175,14 @@ A run is `target × runner × flavour`, e.g. `sentinel --run --lint --runner=esl
|
|
|
143
175
|
|
|
144
176
|
- the **engine** (core) is tool-agnostic: it parses the CLI, resolves an adapter, and owns **all IO and repo structure**, finding the project root, reading, merging and writing files;
|
|
145
177
|
- an **adapter** is the boundary to one tool (eslint, tsc, vitest, …). It carries the tool knowledge (how to run it, what its config means) and implements one contract (types in `src/core/types.ts`, optional base in `src/core/base-adapter.ts`);
|
|
146
|
-
- a **context** is a plain data object the engine passes to an adapter for a run (`app`, `cwd`, optional `
|
|
178
|
+
- a **context** is a plain data object the engine passes to an adapter for a run (`app`, `cwd`, optional `preset`, `ci`, `fix`). It is data only, it never carries a filesystem capability.
|
|
147
179
|
|
|
148
180
|
`sentinel` does not reimplement tools. The contract:
|
|
149
181
|
|
|
150
|
-
- **`appliesTo(
|
|
151
|
-
- **`plan(
|
|
182
|
+
- **`appliesTo(preset)`** — which presets this adapter handles (resolution filters on it, so a React-only adapter is never picked for Nest)
|
|
183
|
+
- **`plan(preset)`** — PURE: returns a declarative `UpdatePlan` of file operations (used by `--init`). The adapter never touches the disk; the engine applies the plan.
|
|
152
184
|
- **`run(ctx)`** — invoke the tool's bin against the project (used by `--run`)
|
|
153
|
-
- **`inspect(
|
|
185
|
+
- **`inspect(preset)`** — the adapter's resolved base config (used by `--inspect`)
|
|
154
186
|
- **`report(ctx)`** — metrics (used by `--report`)
|
|
155
187
|
|
|
156
188
|
**`--init` is a declarative plan, not file-writing inside the adapter.** The adapter describes intent as operations; the engine executes them:
|
|
@@ -170,7 +202,7 @@ So the tool-_meaning_ lives in the adapter and the read/merge/write _mechanics_
|
|
|
170
202
|
```mermaid
|
|
171
203
|
flowchart LR
|
|
172
204
|
CLI["sentinel --verb --target<br/>[--runner]"] --> D[dispatch]
|
|
173
|
-
D --> R["registry.resolve<br/>(target,
|
|
205
|
+
D --> R["registry.resolve<br/>(target, preset, runner)"]
|
|
174
206
|
R --> A["adapter<br/>eslint / tsc / vitest / ..."]
|
|
175
207
|
A -->|"--run"| Run["tool binary on the project"]
|
|
176
208
|
A -->|"--init"| Upd["declarative plan → engine writes"]
|
|
@@ -200,6 +232,80 @@ The rules live in `sentinel`. Each module keeps a **thin, generated stub** per t
|
|
|
200
232
|
- **Per-module install** — `@hublo/sentinel` is added per module, so adoption is **gradual** (migrate lot by lot; a module can adopt sentinel while its neighbour still uses the old config). Root configs are removed only once the **last** module has migrated.
|
|
201
233
|
- **Runner binaries** (`eslint`, `typescript`, `vite`, `vitest`, …) are **resolved from the adopting module** at run time (sentinel looks for the tool in the module, then falls back to `PATH`), so the editor and nx keep using the exact binary the project already installs. sentinel does **not** declare them as dependencies today, so it does not pin their versions: the module still owns its own `typescript`. Having sentinel dictate those versions (as peer dependencies, so the whole toolchain rides the sentinel version) is the intended end state, and it lands with the tool tickets that actually bundle a runner.
|
|
202
234
|
|
|
235
|
+
## What a preset does not enforce
|
|
236
|
+
|
|
237
|
+
A preset that silently enforces less than the config it replaced is worse than no preset, so
|
|
238
|
+
every rule that is not enforced carries a reason and is listed by `sentinel --inspect --lint`:
|
|
239
|
+
|
|
240
|
+
| State | Meaning | Can it be switched on? |
|
|
241
|
+
| -------------- | -------------------------------------------------------------------- | ------------------------------- |
|
|
242
|
+
| **deferred** | scheduled for a later migration wave | yes, on schedule |
|
|
243
|
+
| **disabled** | a decision: it argues with the architecture, or only false-positives | yes, by revisiting the decision |
|
|
244
|
+
| **downgraded** | it runs and reports, it just cannot fail the build | yes, once the debt is paid |
|
|
245
|
+
|
|
246
|
+
A small number of rules sit outside those three states, because the preset cannot carry them at
|
|
247
|
+
any severity. They are documented here rather than reported by `--inspect`: oxlint validates
|
|
248
|
+
rule and plugin names when it **parses** a config, so naming one would make the config fail to
|
|
249
|
+
parse and the module would lint nothing at all. They are still recorded as data in the preset,
|
|
250
|
+
so the tests can count every rule a module enforces and fail on anything unaccounted for.
|
|
251
|
+
|
|
252
|
+
### Rules the presets do not carry (nest, react)
|
|
253
|
+
|
|
254
|
+
| Rule | Why |
|
|
255
|
+
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
256
|
+
| `typescript/member-ordering` | oxlint has no native rule, and the preset deliberately does not host `@typescript-eslint/eslint-plugin`: a plugin costs a dependency, a load step and a pass over every file, which is not worth one rule |
|
|
257
|
+
| `no-invalid-this` | no oxlint equivalent, and hosting does not help (the rule crashes on every file under oxlint's plugin API). Its coverage moves to TypeScript's `noImplicitThis` |
|
|
258
|
+
| `no-octal` | no oxlint equivalent |
|
|
259
|
+
| ~15 stylistic rules | formatting, which the formatter owns. They move to **oxfmt**, deliberately the last step of the migration, so nothing enforces them in between |
|
|
260
|
+
|
|
261
|
+
The formatting group is the only one that will ever shrink; the rest are settled losses.
|
|
262
|
+
|
|
263
|
+
`no-return-await` is **not** in that table, despite having no oxlint rule of that name. It is
|
|
264
|
+
deprecated in ESLint core and oxlint ships the typescript-eslint successor, so the presets carry
|
|
265
|
+
`typescript/return-await` with the `never` option, which is what the old rule meant. It is held
|
|
266
|
+
at `warn`: both modules set the old rule to error, but it was never actually reporting (the nx
|
|
267
|
+
lint target expanded to 2 files of 2888), and running it over every file finds 9 real violations
|
|
268
|
+
on bff-admin and 5 on host-admin. Fix those and it can be raised.
|
|
269
|
+
|
|
270
|
+
### Rules with no oxlint equivalent (svelte)
|
|
271
|
+
|
|
272
|
+
Oxlint parses a `.svelte` file's `<script>` block and **not its template**. Measured on oxlint
|
|
273
|
+
1.77 with a component carrying a violation in each half: the `<script>` violation is reported,
|
|
274
|
+
and `{@html}`, `{@debug}` and duplicate style properties in the template produce nothing.
|
|
275
|
+
Adding a svelte plugin does not fix this, because the missing piece is the **parser**, not the
|
|
276
|
+
rule implementations.
|
|
277
|
+
|
|
278
|
+
So sixteen rules the two svelte modules enforce today have **no oxlint equivalent**. Fourteen
|
|
279
|
+
read the template:
|
|
280
|
+
|
|
281
|
+
`svelte/comment-directive` · `svelte/no-at-debug-tags` · **`svelte/no-at-html-tags`** ·
|
|
282
|
+
`svelte/no-dupe-else-if-blocks` · `svelte/no-dupe-style-properties` ·
|
|
283
|
+
`svelte/no-dynamic-slot-name` · `svelte/no-inner-declarations` ·
|
|
284
|
+
`svelte/no-not-function-handler` · `svelte/no-object-in-text-mustaches` ·
|
|
285
|
+
`svelte/no-shorthand-style-property-overrides` · `svelte/no-unknown-style-directive-property` ·
|
|
286
|
+
`svelte/no-unused-svelte-ignore` · `svelte/system` · `svelte/valid-compile`
|
|
287
|
+
|
|
288
|
+
and two core rules oxlint does not implement: `no-dupe-args` (TypeScript rejects duplicate
|
|
289
|
+
parameter names anyway) and `no-octal`.
|
|
290
|
+
|
|
291
|
+
**The one worth knowing about is `svelte/no-at-html-tags`**, an XSS guard that fires on the
|
|
292
|
+
modules today. Adopting the svelte lint preset gives that check up.
|
|
293
|
+
|
|
294
|
+
These rules are **absent from the generated config rather than set to `off`**, and that is
|
|
295
|
+
forced, not stylistic. Oxlint rejects an unknown rule or plugin when it _parses_ a config, even
|
|
296
|
+
when the value is `off`:
|
|
297
|
+
|
|
298
|
+
```console
|
|
299
|
+
$ oxlint -c '{"rules": {"svelte/no-at-html-tags": "off"}}'
|
|
300
|
+
Failed to parse oxlint configuration file.
|
|
301
|
+
x Plugin 'svelte' not found
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Listing them as disabled would not weaken the config, it would stop the config from parsing and
|
|
305
|
+
the module would lint nothing at all. They are recorded as data in the preset (`uncovered`), so
|
|
306
|
+
the preset tests can count every rule the modules enforce and fail on anything unaccounted for,
|
|
307
|
+
and they are documented here for the developers who own those modules.
|
|
308
|
+
|
|
203
309
|
## Composition & precedence
|
|
204
310
|
|
|
205
311
|
A repo often has a base config that is **structural**, not just tooling, e.g. a
|
|
@@ -264,11 +370,11 @@ A command **composes** three axes: **verb + type + location**.
|
|
|
264
370
|
```
|
|
265
371
|
sentinel <verb> [type] [options]
|
|
266
372
|
|
|
267
|
-
VERBS --run execute the target's tool
|
|
268
|
-
--inspect
|
|
269
|
-
--
|
|
270
|
-
--
|
|
271
|
-
--
|
|
373
|
+
VERBS --run execute the target's tool (--json for metrics + diagnostics)
|
|
374
|
+
--inspect the resolved configuration, what is deferred, adoption + drift
|
|
375
|
+
--init generate/apply the config stubs (writes; one module only)
|
|
376
|
+
--report deprecated → --run --json
|
|
377
|
+
--status deprecated → --inspect
|
|
272
378
|
|
|
273
379
|
TYPES --lint --format --typescript --build --test
|
|
274
380
|
--static-analysis --runtime-analysis --arch
|
|
@@ -278,27 +384,55 @@ LOCATION in a MODULE dir → that module (do NOT pass --module)
|
|
|
278
384
|
at the workspace ROOT → --module <name> (one) · --ci (affected) · else all
|
|
279
385
|
|
|
280
386
|
OPTIONS --module <name> from the root: scope to one module
|
|
281
|
-
--
|
|
387
|
+
--preset <name> override the detected stack preset (react, nest, ...)
|
|
282
388
|
--runner <tool> override the default runner
|
|
283
389
|
--ci from the root: affected only; non-zero exit on failure
|
|
284
390
|
--fix auto-fix where applicable
|
|
285
391
|
--dry-run preview a --init without writing
|
|
286
392
|
--json machine-readable output (report / inspect / status / --dry-run)
|
|
393
|
+
-- <tool options> --run only: pass the rest to the tool itself (one type)
|
|
287
394
|
|
|
288
395
|
EXAMPLES sentinel --run --typescript # in a module → that module
|
|
289
|
-
sentinel --
|
|
290
|
-
sentinel --
|
|
291
|
-
sentinel --
|
|
292
|
-
sentinel --
|
|
293
|
-
sentinel --
|
|
396
|
+
sentinel --run --typescript -- --noImplicitAny # ask tsc a question of your own
|
|
397
|
+
sentinel --run --typescript --module bff-admin # from root → one module
|
|
398
|
+
sentinel --run --json # from root → all types, machine output
|
|
399
|
+
sentinel --inspect --typescript # from root → adoption coverage
|
|
400
|
+
sentinel --inspect --ci # from root → fail CI on drift
|
|
401
|
+
sentinel --init --typescript --preset react # write stubs for the current module
|
|
294
402
|
```
|
|
295
403
|
|
|
296
404
|
`--run`/`--inspect`/`--report`/`--status` share one context rule (developer from a
|
|
297
405
|
module, or from the root for a name / affected / all); `--init` writes, so it targets
|
|
298
406
|
one module only (adopting every module at once is refused, adopt gradually).
|
|
299
407
|
|
|
408
|
+
### Asking the tool your own question (`--`)
|
|
409
|
+
|
|
410
|
+
Everything after `--` goes to the tool, not to sentinel. It applies to `--run` only, and
|
|
411
|
+
needs exactly one type named, since one tool's options are not another's:
|
|
412
|
+
|
|
413
|
+
```bash
|
|
414
|
+
sentinel --run --typescript -- --noImplicitAny # what would this deferred rule cost?
|
|
415
|
+
sentinel --run --lint -- --deny-warnings # what would zero warnings take?
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
This exists because a preset **defers** rules to keep first adoption non-breaking, and
|
|
419
|
+
`--inspect` can only say that a rule is deferred. "Deferred" is honest, and it is not the
|
|
420
|
+
answer anyone wants: the question is how much debt is behind it. Rather than sentinel
|
|
421
|
+
modelling that question one flag at a time, the tool's own options are forwarded, so
|
|
422
|
+
anything the tool can already answer is one command away.
|
|
423
|
+
|
|
424
|
+
Two things to know:
|
|
425
|
+
|
|
426
|
+
- **A modified run always says so**, on stderr and in the `--json` envelope (`toolArgs`).
|
|
427
|
+
These options can weaken a check as easily as strengthen it, and `--run` is what CI
|
|
428
|
+
calls, so a green result must never be mistakable for a clean standard check.
|
|
429
|
+
- **For TypeScript it changes how tsc is invoked**, because build mode refuses compiler
|
|
430
|
+
options (`error TS5094`). Sentinel switches to project mode and checks each project the
|
|
431
|
+
solution `references`, since the solution itself holds no files. Emit is forced off, so a
|
|
432
|
+
question about the code never writes build output.
|
|
433
|
+
|
|
300
434
|
**`--status` — adoption + conformity.** A cheap, workspace-wide read (no tool run, no
|
|
301
|
-
|
|
435
|
+
preset guessing): for each module it reads the committed `tsconfig` `extends` chain and
|
|
302
436
|
reports whether it is **adopted** (extends a sentinel preset), which preset, and whether
|
|
303
437
|
it is **conformant** (drift-free, a re-`update` would change nothing), plus a coverage
|
|
304
438
|
footer. This is the **drift guard** as a command, `--status --ci` exits non-zero when an
|
|
@@ -320,14 +454,14 @@ src/
|
|
|
320
454
|
core/
|
|
321
455
|
types.ts # the adapter contract, pure types (Adapter, FileOperation, UpdatePlan)
|
|
322
456
|
base-adapter.ts # optional convenience base class for adapters
|
|
323
|
-
domain.ts # vocabulary + derived types (verbs, targets,
|
|
457
|
+
domain.ts # vocabulary + derived types (verbs, targets, presets)
|
|
324
458
|
settings.ts # tunables (workspace-root marker, ...)
|
|
325
|
-
registry.ts # register +
|
|
459
|
+
registry.ts # register + preset-aware resolve
|
|
326
460
|
dispatch.ts # verb → adapter method
|
|
327
461
|
apply-plan.ts # the engine's filesystem port (applies --init operations)
|
|
328
462
|
roles/<config-role>/ # lint, format, typescript, build, test
|
|
329
463
|
adapters/<runner>/ # one adapter per tool (implements the contract)
|
|
330
|
-
|
|
464
|
+
presets/<stack>/ # config presets per stack (react, nest, svelte, ...)
|
|
331
465
|
roles/{static-analysis,runtime-analysis}/ # analysis roles
|
|
332
466
|
configs/ # fixed configs (internal, not exported)
|
|
333
467
|
runners/ # one runner per sub-tool (duplication, complexity, ...)
|
|
@@ -336,9 +470,18 @@ tests/ # unit tests + tests/e2e (runs the built dist binar
|
|
|
336
470
|
.github/workflows/ # ci.yml (PR checks) + publish.yml (manual, version-input publish)
|
|
337
471
|
```
|
|
338
472
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
473
|
+
Presets reach consumers two different ways, decided by the tool rather than by us:
|
|
474
|
+
|
|
475
|
+
- **tsconfig, as subpath exports**: `@hublo/sentinel/tsconfig/react` · `.../tsconfig/nest` ·
|
|
476
|
+
`.../tsconfig/node`. tsc resolves `extends` through the `exports` map.
|
|
477
|
+
- **oxlint, as physical paths at the package root**: `oxlint/react.json`, `oxlint/nest.json`,
|
|
478
|
+
`oxlint/node.json`, `oxlint/svelte.json`, referenced as
|
|
479
|
+
`./node_modules/@hublo/sentinel/oxlint/<preset>.json`. Oxlint resolves every `extends`
|
|
480
|
+
entry as a PATH and never as a package specifier, so it never consults `exports` and the
|
|
481
|
+
physical layout IS the contract. The same is true of `jsPlugins`, which is why the local
|
|
482
|
+
Hublo plugin ships beside them in `plugins/`.
|
|
483
|
+
|
|
484
|
+
Presets for the remaining tools land with their own ticket.
|
|
342
485
|
|
|
343
486
|
## FAQ
|
|
344
487
|
|
|
@@ -358,7 +501,7 @@ Yes. Per app (app A defaults to eslint, app B to biome), or even in the same app
|
|
|
358
501
|
Most tools expose `extends` or a plugin mechanism to compose config, so the stub just points at the sentinel preset. For the rare tool that doesn't, sentinel exposes the config **directly**, it generates the full config from its preset (still one source, drift-checked).
|
|
359
502
|
|
|
360
503
|
**What does an nx `project.json` look like?**
|
|
361
|
-
nx is a **task runner**: it just runs the target's script.
|
|
504
|
+
nx is a **task runner**: it just runs the target's script, so the command belongs in `package.json` and nx infers a target from it (verified: it does this even when the module also has a `project.json`). Adoption therefore REMOVES the role's target from `project.json`, leaving every other one alone, and writes only what nx alone needs (cache inputs) into the `nx` block of `package.json`. Each role a module adopts decouples one more thing from nx and shrinks that file. nx keeps the graph, affected set, and cache; sentinel provides the config + execution.
|
|
362
505
|
|
|
363
506
|
## Roadmap
|
|
364
507
|
|