@hublo/sentinel 0.1.0-alpha.8 → 1.0.0

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
@@ -23,9 +23,9 @@ A large monorepo accumulates:
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.
26
- - **`--update` = adopt, refresh, _and_ migrate** — the same command generates the stubs the first time (**adopt**), regenerates them after a change like a runner swap (**refresh**), and is run module by module to roll out gradually (**migrate**).
27
- - **Move one module at a time** — installed per module, so you migrate at your pace; a module can adopt sentinel while its neighbour keeps the old setup. No big-bang.
28
- - **Swap tools without touching projects** — change eslint → biome (or benchmark them) in one place; `--update` regenerates the stubs.
26
+ - **`--init` sets a module up** — the one command generates the stubs the first time (**adopt**), regenerates them after a change like a runner swap (**refresh**), and applies the workspace prep the module needs. Run it module by module to roll out gradually. (`--migrate`, for changing an already-initialized setup, is a reserved future verb.)
27
+ - **Move one module at a time** — installed per module, so you adopt at your pace; a module can adopt sentinel while its neighbour keeps the old setup. No big-bang.
28
+ - **Swap tools without touching projects** — change eslint → biome (or benchmark them) in one place; `--init` regenerates the stubs.
29
29
  - **No silent drift** — the guard keeps every project's config converged on the source of truth.
30
30
 
31
31
  ### Before → after
@@ -35,7 +35,7 @@ A large monorepo accumulates:
35
35
  | **Config** | ~116 eslint + ~528 tsconfig files with real, drifting content | thin stubs that `extends` a versioned preset; rules in one place |
36
36
  | **Tooling deps** | ~165 devDeps at the root, shared by all | one `@hublo/sentinel` per module; the toolchain rides its version |
37
37
  | **Upgrade a tool** | big-bang: every project at once, untested in isolation | bump one version, tested in sentinel first, atomic |
38
- | **Swap a tool** | edit config in every project | swap an adapter + `--update`; zero project churn |
38
+ | **Swap a tool** | edit config in every project | swap an adapter + `--init`; zero project churn |
39
39
  | **A rule change** | edit many configs, hope they stay consistent | change once; the drift guard enforces it |
40
40
  | **Migration** | all-or-nothing | module by module, at your pace |
41
41
 
@@ -43,13 +43,17 @@ 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
- **Step 1 put a module on sentinel** (once per module, by a dev; the files are committed):
46
+ > **Shipped today:** only the **TypeScript** tool, so `--init` 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
+
48
+ **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:
47
49
 
48
50
  ```bash
49
- pnpm add -D @hublo/sentinel
50
- sentinel --update # run from the app dir; writes eslint.config.js, tsconfig, ... then you commit them
51
+ sentinel --init --typescript --flavour <react|nest|node>
52
+ pnpm install # fetch what --init declared, then commit
51
53
  ```
52
54
 
55
+ `--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
+
53
57
  Those files are tiny, they just point at a sentinel preset. What gets committed:
54
58
 
55
59
  ```js
@@ -93,12 +97,31 @@ And the app's `package.json` scripts route every check through the one CLI (run
93
97
  **Step 3 — evolve the toolchain, in one central place:**
94
98
 
95
99
  - a **rule change** → bump the `@hublo/sentinel` version; the stubs already point at it, so there is **nothing to regenerate**;
96
- - a **structural change** (new tool, new preset, runner swap) → run `sentinel --update` once to refresh the stubs (sentinel tells you when this is needed).
100
+ - a **structural change** (new tool, new preset, runner swap) → run `sentinel --init` once to refresh the stubs (sentinel tells you when this is needed).
97
101
 
98
102
  **Step 4 — stay converged:** a drift guard in CI flags any module whose config quietly diverged from the shared source.
99
103
 
100
104
  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.
101
105
 
106
+ ## Requirements & installing
107
+
108
+ **Registry: public npm**, under `@hublo`. We started on **GitHub Packages (private)** and moved off it: it authenticates every consumer, including a one-off `pnpm dlx`, which is incompatible with the zero-setup adoption above. Public npm needs no consumer auth, so a module adopts sentinel without any `.npmrc` or token. Releases go out through the repo's `publish` workflow, never from a laptop; a prerelease is published under its prerelease dist-tag (`alpha`) and a stable one under `latest`.
109
+
110
+ **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.
111
+
112
+ **Try it without installing.** A one-off run needs no auth and touches nothing:
113
+
114
+ ```bash
115
+ pnpm dlx @hublo/sentinel@<exact-version> --inspect --typescript --module <name>
116
+ ```
117
+
118
+ **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.
119
+
120
+ ## Docs & cheat sheets
121
+
122
+ - [`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
+ - [`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
+
102
125
  ## Architecture: `target → runner → flavour`
103
126
 
104
127
  Every check is described by three layers:
@@ -112,7 +135,7 @@ Every check is described by three layers:
112
135
  A run is `target × runner × flavour`, e.g. `sentinel --run --lint --runner=eslint` from the `host-admin` dir.
113
136
 
114
137
  - `--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).
115
- - 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.
138
+ - 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 `--init` (that is where the preset is chosen). `--run`/`--report` don't depend on it, they run the tool on the committed config.
116
139
 
117
140
  ### Adapters & the engine (ports & adapters)
118
141
 
@@ -125,12 +148,12 @@ A run is `target × runner × flavour`, e.g. `sentinel --run --lint --runner=esl
125
148
  `sentinel` does not reimplement tools. The contract:
126
149
 
127
150
  - **`appliesTo(flavour)`** — which flavours this adapter handles (resolution filters on it, so a React-only adapter is never picked for Nest)
128
- - **`plan(flavour)`** — PURE: returns a declarative `UpdatePlan` of file operations (used by `--update`). The adapter never touches the disk; the engine applies the plan.
151
+ - **`plan(flavour)`** — PURE: returns a declarative `UpdatePlan` of file operations (used by `--init`). The adapter never touches the disk; the engine applies the plan.
129
152
  - **`run(ctx)`** — invoke the tool's bin against the project (used by `--run`)
130
153
  - **`inspect(flavour)`** — the adapter's resolved base config (used by `--inspect`)
131
154
  - **`report(ctx)`** — metrics (used by `--report`)
132
155
 
133
- **`--update` is a declarative plan, not file-writing inside the adapter.** The adapter describes intent as operations; the engine executes them:
156
+ **`--init` is a declarative plan, not file-writing inside the adapter.** The adapter describes intent as operations; the engine executes them:
134
157
 
135
158
  - `write { path, contents }` — a file the adapter fully owns (the thin stub)
136
159
  - `merge-json { path, value }` — pin the keys sentinel owns while **preserving** a project's own (this is how a tsconfig's `paths`/`include` survive)
@@ -150,7 +173,7 @@ flowchart LR
150
173
  D --> R["registry.resolve<br/>(target, flavour, runner)"]
151
174
  R --> A["adapter<br/>eslint / tsc / vitest / ..."]
152
175
  A -->|"--run"| Run["tool binary on the project"]
153
- A -->|"--update"| Upd["declarative plan → engine writes"]
176
+ A -->|"--init"| Upd["declarative plan → engine writes"]
154
177
  A -->|"--inspect"| Ins["resolved config"]
155
178
  A -->|"--report"| Rep["metrics"]
156
179
  ```
@@ -160,7 +183,7 @@ flowchart LR
160
183
  ```mermaid
161
184
  flowchart TB
162
185
  S["@hublo/sentinel<br/>rules = one source of truth"]
163
- S -->|"--update generates"| Stub["thin stub per module<br/>(extends sentinel)"]
186
+ S -->|"--init generates"| Stub["thin stub per module<br/>(extends sentinel)"]
164
187
  Stub --> IDE["editor: live lint / type / format"]
165
188
  Stub --> NX["nx: target inference"]
166
189
  S -->|"--run injects config"| CI["CLI / CI: run tool on target"]
@@ -172,10 +195,10 @@ flowchart TB
172
195
  The rules live in `sentinel`. Each module keeps a **thin, generated stub** per tool, a few lines that `extends`/re-export the sentinel preset:
173
196
 
174
197
  - **Rules in sentinel** — one source of truth, versioned.
175
- - **Thin stubs per module** — the stub is what keeps the **editor working** (VS Code discovers config by file, real-time lint/type/format stay live) and what **nx** uses to infer targets. Stubs are **generated by `sentinel --update`**, never hand-written; swapping a runner regenerates them.
198
+ - **Thin stubs per module** — the stub is what keeps the **editor working** (VS Code discovers config by file, real-time lint/type/format stay live) and what **nx** uses to infer targets. Stubs are **generated by `sentinel --init`**, never hand-written; swapping a runner regenerates them.
176
199
  - **Drift guard** — sentinel validates that each stub is _only_ the sanctioned `extends`, with nothing added or overridden. Unsanctioned drift is flagged in CI; a genuine exception must be **declared in an allowlist** (visible, reviewed), never silent.
177
200
  - **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.
178
- - **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.
201
+ - **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.
179
202
 
180
203
  ## Composition & precedence
181
204
 
@@ -245,7 +268,7 @@ VERBS --run execute the target's tool
245
268
  --inspect show the resolved configuration (incl. deferred rules)
246
269
  --report metrics and health
247
270
  --status adoption + conformity (coverage + drift), read from configs
248
- --update generate/apply the config stubs (writes; one module only)
271
+ --init generate/apply the config stubs (writes; one module only)
249
272
 
250
273
  TYPES --lint --format --typescript --build --test
251
274
  --static-analysis --runtime-analysis --arch
@@ -259,18 +282,19 @@ OPTIONS --module <name> from the root: scope to one module
259
282
  --runner <tool> override the default runner
260
283
  --ci from the root: affected only; non-zero exit on failure
261
284
  --fix auto-fix where applicable
262
- --dry-run preview a --update without writing
285
+ --dry-run preview a --init without writing
286
+ --json machine-readable output (report / inspect / status / --dry-run)
263
287
 
264
288
  EXAMPLES sentinel --run --typescript # in a module → that module
265
289
  sentinel --report --typescript --module bff-admin # from root → one module
266
290
  sentinel --report # from root → all types, all modules
267
291
  sentinel --status --typescript # from root → adoption coverage
268
292
  sentinel --status --ci # from root → fail CI on drift
269
- sentinel --update --typescript --flavour react # write stubs for the current module
293
+ sentinel --init --typescript --flavour react # write stubs for the current module
270
294
  ```
271
295
 
272
296
  `--run`/`--inspect`/`--report`/`--status` share one context rule (developer from a
273
- module, or from the root for a name / affected / all); `--update` writes, so it targets
297
+ module, or from the root for a name / affected / all); `--init` writes, so it targets
274
298
  one module only (adopting every module at once is refused, adopt gradually).
275
299
 
276
300
  **`--status` — adoption + conformity.** A cheap, workspace-wide read (no tool run, no
@@ -300,7 +324,7 @@ src/
300
324
  settings.ts # tunables (workspace-root marker, ...)
301
325
  registry.ts # register + flavour-aware resolve
302
326
  dispatch.ts # verb → adapter method
303
- apply-plan.ts # the engine's filesystem port (applies --update operations)
327
+ apply-plan.ts # the engine's filesystem port (applies --init operations)
304
328
  roles/<config-role>/ # lint, format, typescript, build, test
305
329
  adapters/<runner>/ # one adapter per tool (implements the contract)
306
330
  flavours/<stack>/ # config presets per stack (react, nest, svelte, ...)
@@ -309,8 +333,7 @@ src/
309
333
  runners/ # one runner per sub-tool (duplication, complexity, ...)
310
334
  shared/ # reusable utils (package-json, deep-merge, text)
311
335
  tests/ # unit tests + tests/e2e (runs the built dist binary)
312
- .changeset/ # release notes
313
- .github/workflows/ # ci.yml (PR checks) + release.yml (changesets publish)
336
+ .github/workflows/ # ci.yml (PR checks) + publish.yml (manual, version-input publish)
314
337
  ```
315
338
 
316
339
  Subpath exports (in `package.json`) expose presets to consumers. Shipped today:
@@ -341,8 +364,8 @@ nx is a **task runner**: it just runs the target's script. So `project.json` bar
341
364
 
342
365
  This scaffold is the foundation; each tool is added one at a time on top of it:
343
366
 
344
- 1. **Foundation** — repo, exports, CLI skeleton, adapter contract, changesets, CI/release. **✅ shipped**
345
- 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`**
367
+ 1. **Foundation** — repo, exports, CLI skeleton, adapter contract, CI + the single dispatch `publish` workflow. **✅ shipped**
368
+ 2. **TypeScript** (`--typescript`) — runner `tsc` (later `tsgo`): presets react/nest/node, `--run`/`--inspect`/`--report`/`--init`, the composable grid, phased (non-breaking) strictness. **✅ shipped in `0.1.0-alpha.x`**
346
369
  3. **Lint** (`--lint`) — benchmark `eslint` vs `biome` vs `oxlint`.
347
370
  4. **Build** (`--build`) — `vite`.
348
371
  5. **Test** (`--test`) — `vitest`, plus a11y / w3c setups.
@@ -352,7 +375,6 @@ This scaffold is the foundation; each tool is added one at a time on top of it:
352
375
 
353
376
  ## Open decisions & risks being validated
354
377
 
355
- - **Registry:** **GitHub Packages (private)** for the foundation, publishing needs no extra secret and there is no consumer to authenticate yet. Public npm under `@hublo` (zero consumer auth, OSS-ready) stays an option once the package is stable; the consuming repo's `.npmrc`/auth is set up with the first tool ticket, when there is actually something to install.
356
378
  - **Performance:** prefer adopting Rust-native tools (`oxlint`/`biome`, `tsgo`) over hand-written Rust+WASM; reserve custom WASM for a _measured_ hot path only.
357
379
  - **Validation spike (before wide rollout):** wire one real module to sentinel and confirm on real pnpm layout that (1) the editor keeps live lint/type/format, (2) runner binaries + plugins resolve per module, (3) nx target inference survives a runner swap, (4) old-config and sentinel modules coexist during migration.
358
380