@hublo/sentinel 0.1.0-alpha.1 → 0.1.0-alpha.11

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 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: **scaffold / design**. This README is the design reference; tools are implemented one at a time on top of this foundation.
7
+ > Status: **foundation + the TypeScript tool are shipped** (`@hublo/sentinel@0.1.0-alpha.x` 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
 
@@ -43,6 +43,8 @@ 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:** only the **TypeScript** tool, so `--update` writes the `tsconfig` stub, and `--run`/`--report`/`--status` work for `--typescript`. The `eslint.config.js` / `--lint` / `--test` snippets below illustrate the end state; those subpaths (`@hublo/sentinel/lint/*`, …) land with their tool ticket.
47
+
46
48
  **Step 1 — put a module on sentinel** (once per module, by a dev; the files are committed):
47
49
 
48
50
  ```bash
@@ -59,14 +61,17 @@ export default react
59
61
  ```
60
62
 
61
63
  ```jsonc
62
- // tsconfig.json — extends the preset; only project-specific paths/include stay local
64
+ // tsconfig.json — COMPOSES the repo base with the sentinel preset (see "Composition
65
+ // & precedence"); only project-specific paths/include stay local
63
66
  {
64
- "extends": "@hublo/sentinel/typescript/react",
67
+ "extends": ["../../tsconfig.base.json", "@hublo/sentinel/tsconfig/react"],
65
68
  "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] } },
66
69
  "include": ["src"]
67
70
  }
68
71
  ```
69
72
 
73
+ > A module can extend the preset alone (`"extends": "@hublo/sentinel/tsconfig/react"`) when there is no repo base. Compose with the base when the repo owns structure (a monorepo `paths` map) the preset should not, see "Composition & precedence".
74
+
70
75
  And the app's `package.json` scripts route every check through the one CLI (run from the app dir, sentinel scopes to it):
71
76
 
72
77
  ```jsonc
@@ -96,6 +101,23 @@ And the app's `package.json` scripts route every check through the one CLI (run
96
101
 
97
102
  The per-tool knowledge (eslint → `eslint.config.js`, tsc → `tsconfig`, …) lives **inside sentinel as an adapter**, swappable centrally, but never a runtime dependency of the project.
98
103
 
104
+ ## Requirements & installing
105
+
106
+ **Node.** sentinel needs **Node >= 20.12** (its coloured output uses `util.styleText`, added in 20.12). It fails fast with a clear message on an older runtime rather than crashing. If a project runs on an older Node (e.g. a legacy app on Node 10), run sentinel with a modern Node via `fnm`/`nvm`; you do not need to change the project's own Node.
107
+
108
+ **Try it without installing.** A one-off run needs no auth and touches nothing:
109
+
110
+ ```bash
111
+ pnpm dlx @hublo/sentinel@<exact-version> --inspect --typescript --module <name>
112
+ ```
113
+
114
+ **Installing a pre-release (`minimumReleaseAge`).** The monorepo enforces a 3-day `minimumReleaseAge` supply-chain gate (a freshly published version cannot be installed until it has aged 3 days). A brand-new `alpha` therefore cannot be added yet, so while testing pre-releases you either exclude the package (`pnpm-workspace.yaml` → `minimumReleaseAgeExclude`) or install with `--config.minimumReleaseAge=0`. This is a deliberate protection, not a bug: **always pin the exact version** (`@hublo/sentinel@0.1.0-alpha.9`) rather than `@latest`, so a run is reproducible and the gate stays meaningful.
115
+
116
+ ## Docs & cheat sheets
117
+
118
+ - [`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.
119
+ - [`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`.
120
+
99
121
  ## Architecture: `target → runner → flavour`
100
122
 
101
123
  Every check is described by three layers:
@@ -104,12 +126,12 @@ Every check is described by three layers:
104
126
  | -------------------- | --------------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------- |
105
127
  | **target** (role) | `--lint`, `--typescript`, … | the _kind_ of check, stable | `lint` `format` `typescript` `build` `test` `static-analysis` `runtime-analysis` |
106
128
  | **runner** (adapter) | `--runner=<tool>` | the _tool_ behind the target, swappable | lint: `eslint`/`biome`/`oxlint` · types: `tsc`/`tsgo` · build: `vite` · test: `vitest` |
107
- | **flavour** (preset) | declared | the _variant_ per stack (strict by default) | `react` `nest` `svelte` `node` |
129
+ | **flavour** (preset) | detected / `--flavour` | the _variant_ per stack (strict by default) | `react` `nest` `node` (svelte declared, preset deferred) |
108
130
 
109
131
  A run is `target × runner × flavour`, e.g. `sentinel --run --lint --runner=eslint` from the `host-admin` dir.
110
132
 
111
133
  - `--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).
112
- - The **flavour is declared, never detected.** Once a project is on sentinel, its committed config already says which preset (`extends '@hublo/sentinel/lint/react'`), so `--run`/`--report` don't need a flag, the tool reads that config. `--update`/`--inspect` take it explicitly with `--flavour` (the first `--update` is where you declare it). sentinel never guesses the stack, so a Nest app can't be silently treated as React.
134
+ - The **flavour is detected** from the module's dependencies (deterministic: a framework dep its flavour, else `node`), and **`--flavour` overrides it**. Detection can be wrong where deps are hoisted at the repo root (it returns `node`), so pass `--flavour` for `--update` (that is where the preset is chosen). `--run`/`--report` don't depend on it, they run the tool on the committed config.
113
135
 
114
136
  ### Adapters & the engine (ports & adapters)
115
137
 
@@ -174,30 +196,115 @@ The rules live in `sentinel`. Each module keeps a **thin, generated stub** per t
174
196
  - **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.
175
197
  - **Runner binaries** (`eslint`, `typescript`, `vite`, `vitest`, …) are **peer dependencies** so they install once and stay resolvable by the editor + nx, while sentinel still dictates their versions.
176
198
 
199
+ ## Composition & precedence
200
+
201
+ A repo often has a base config that is **structural**, not just tooling, e.g. a
202
+ monorepo `tsconfig.base.json` carrying the workspace `paths` map. sentinel does **not**
203
+ replace it; it **composes** with it, so the structure survives and sentinel owns the
204
+ standards:
205
+
206
+ ```jsonc
207
+ "extends": ["../../tsconfig.base.json", "@hublo/sentinel/tsconfig/react"]
208
+ ```
209
+
210
+ **Precedence — sentinel wins.** With array `extends` (TS 5.0), entries merge in order
211
+ and the **last one wins**; the module's own `compilerOptions` win over both. So it is
212
+ always **base → sentinel → module-local**. sentinel is listed **last on purpose**:
213
+ it is a **reference** (a source of truth), not a suggestion the base can silently
214
+ veto. For any option sentinel declares, sentinel's value is the effective one.
215
+
216
+ For a module composing the base, the resolved config (`tsc --showConfig`) looks like:
217
+
218
+ | Option | Effective value | Owner |
219
+ | ----------------------------- | -------------------- | ------------------------------------------ |
220
+ | `strict`, `jsx`, decorators | preset's | **sentinel** (quality + framework) |
221
+ | `module` / `moduleResolution` | `esnext` / `bundler` | **sentinel** |
222
+ | `target` / `lib` / `paths` | the repo's | **base** (environment; sentinel won't set) |
223
+ | `noImplicitAny` | **off** (phase 1) | **base**, the preset defers it (see below) |
224
+
225
+ - **sentinel owns quality + framework semantics** (`strict`, `jsx`, decorators, module
226
+ system). The base can never quietly undo those, which is what makes sentinel a
227
+ reliable reference.
228
+ - **the base owns the environment** (`paths`, `target`, `lib`, project references).
229
+ sentinel does **not** set these, `extends` REPLACES arrays rather than merging, so
230
+ overriding `lib`/`target` would drop the repo's DOM libs or flip class-field emit.
231
+
232
+ **Least astonishment / phased strictness.** So a reference doesn't turn every rule on
233
+ the instant a module adopts it, sentinel **phases** the rules that would surface new
234
+ errors (`noImplicitAny`, `noUnusedLocals`, `noUnusedParameters`). They are kept
235
+ **commented** in the preset (visible, never silently dropped) and announced by a
236
+ runtime **warning**. `strict` stays on (so a standalone consumer still gets
237
+ `noImplicitAny`), but a repo base that relaxes `noImplicitAny` keeps it off until that
238
+ relaxation is removed. First adoption is therefore a **non-breaking lateral move**;
239
+ the rules are enabled **centrally** in a later wave.
240
+
241
+ **Removing the base is two decoupled moves, and neither loses quality:**
242
+
243
+ 1. **Compose** (`extends: [base, preset]`). Quality flips to sentinel **immediately**,
244
+ because it wins. The base's own tooling options are still present but now **dead
245
+ config** (overridden).
246
+ 2. **Slim the base later** to structure only (`paths` + references). This moves nothing
247
+ quality-wise, sentinel already won in step 1, so it is safe. The base is never fully
248
+ removed while it still owns `paths`.
249
+
250
+ **The discipline this demands:** sentinel must **explicitly declare every quality
251
+ option it means to own.** An option sentinel forgets to set falls through to the base,
252
+ a silent gap in the reference. (Be deliberate with array/replace options like `types`:
253
+ setting them **overrides** the base's rather than merging, so only own them when you
254
+ mean to.)
255
+
177
256
  ## CLI
178
257
 
258
+ A command **composes** three axes: **verb + type + location**.
259
+
179
260
  ```
180
- sentinel <verb> <target> [options]
261
+ sentinel <verb> [type] [options]
181
262
 
182
263
  VERBS --run execute the target's tool
183
- --inspect show the resolved configuration
184
- --update generate/apply the config stubs
264
+ --inspect show the resolved configuration (incl. deferred rules)
185
265
  --report metrics and health
266
+ --status adoption + conformity (coverage + drift), read from configs
267
+ --update generate/apply the config stubs (writes; one module only)
186
268
 
187
- TARGETS --lint --format --typescript --build --test
188
- --static-analysis --runtime-analysis --arch --all
269
+ TYPES --lint --format --typescript --build --test
270
+ --static-analysis --runtime-analysis --arch
271
+ (omit a type → ALL types; or --all)
189
272
 
190
- OPTIONS --module <name> scope to a module (planned; run sentinel from the module dir for now)
191
- --flavour <name> stack preset, declared not detected (react, nest, ...)
273
+ LOCATION in a MODULE dir → that module (do NOT pass --module)
274
+ at the workspace ROOT → --module <name> (one) · --ci (affected) · else all
275
+
276
+ OPTIONS --module <name> from the root: scope to one module
277
+ --flavour <name> override the detected stack preset (react, nest, ...)
192
278
  --runner <tool> override the default runner
193
- --ci non-zero exit on failure
279
+ --ci from the root: affected only; non-zero exit on failure
194
280
  --fix auto-fix where applicable
281
+ --dry-run preview a --update without writing
282
+ --json machine-readable output (report / inspect / status / --dry-run)
283
+
284
+ EXAMPLES sentinel --run --typescript # in a module → that module
285
+ sentinel --report --typescript --module bff-admin # from root → one module
286
+ sentinel --report # from root → all types, all modules
287
+ sentinel --status --typescript # from root → adoption coverage
288
+ sentinel --status --ci # from root → fail CI on drift
289
+ sentinel --update --typescript --flavour react # write stubs for the current module
290
+ ```
291
+
292
+ `--run`/`--inspect`/`--report`/`--status` share one context rule (developer from a
293
+ module, or from the root for a name / affected / all); `--update` writes, so it targets
294
+ one module only (adopting every module at once is refused, adopt gradually).
195
295
 
196
- EXAMPLES (run from the app directory)
197
- sentinel --run --typescript
198
- sentinel --update --lint --flavour react
199
- sentinel --run --lint --runner=oxlint
200
- sentinel --report --all --ci
296
+ **`--status` — adoption + conformity.** A cheap, workspace-wide read (no tool run, no
297
+ flavour guessing): for each module it reads the committed `tsconfig` `extends` chain and
298
+ reports whether it is **adopted** (extends a sentinel preset), which preset, and whether
299
+ it is **conformant** (drift-free, a re-`update` would change nothing), plus a coverage
300
+ footer. This is the **drift guard** as a command, `--status --ci` exits non-zero when an
301
+ adopted module has drifted.
302
+
303
+ ```
304
+ ✓ host-admin (react) typescript — adopted (react) conformant
305
+ ✗ some-bff (nest) typescript — adopted (nest) drift: strict, target
306
+ · legacy-app (node) typescript — not adopted
307
+ coverage: 12/40 adopted · 11/12 conformant · 1 drifted
201
308
  ```
202
309
 
203
310
  ## Repository layout
@@ -222,11 +329,12 @@ src/
222
329
  runners/ # one runner per sub-tool (duplication, complexity, ...)
223
330
  shared/ # reusable utils (package-json, deep-merge, text)
224
331
  tests/ # unit tests + tests/e2e (runs the built dist binary)
225
- .changeset/ # release notes
226
- .github/workflows/ # ci.yml (PR checks) + release.yml (changesets publish)
332
+ .github/workflows/ # ci.yml (PR checks) + publish.yml (manual, version-input publish)
227
333
  ```
228
334
 
229
- Subpath exports (in `package.json`) expose presets to consumers, e.g. `@hublo/sentinel/lint/react`, `@hublo/sentinel/typescript/nest`.
335
+ Subpath exports (in `package.json`) expose presets to consumers. Shipped today:
336
+ `@hublo/sentinel/tsconfig/react` · `.../tsconfig/nest` · `.../tsconfig/node`. Other
337
+ subpaths (e.g. `.../lint/react`) land with their tool ticket.
230
338
 
231
339
  ## FAQ
232
340
 
@@ -252,8 +360,8 @@ nx is a **task runner**: it just runs the target's script. So `project.json` bar
252
360
 
253
361
  This scaffold is the foundation; each tool is added one at a time on top of it:
254
362
 
255
- 1. **Foundation** — repo, exports, CLI skeleton, adapter contract, changesets, CI/release. _(this scaffold)_
256
- 2. **TypeScript** (`--typescript`, CI gate) — runner `tsc`, later `tsgo`.
363
+ 1. **Foundation** — repo, exports, CLI skeleton, adapter contract, changesets, CI/release. **✅ shipped**
364
+ 2. **TypeScript** (`--typescript`) — runner `tsc` (later `tsgo`): presets react/nest/node, `--run`/`--inspect`/`--report`/`--update`, the composable grid, phased (non-breaking) strictness. **✅ shipped in `0.1.0-alpha.x`**
257
365
  3. **Lint** (`--lint`) — benchmark `eslint` vs `biome` vs `oxlint`.
258
366
  4. **Build** (`--build`) — `vite`.
259
367
  5. **Test** (`--test`) — `vitest`, plus a11y / w3c setups.