@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 CHANGED
@@ -141,7 +141,7 @@ The per-tool knowledge (eslint → `eslint.config.js`, tsc → `tsconfig`, …)
141
141
 
142
142
  **Registry: public npm**, under `@hublo`. We started on **GitHub Packages (private)** and moved off it: it authenticates every consumer, including a one-off run, 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`.
143
143
 
144
- **Node.** sentinel needs **Node >= 24.15**, which is the version the monorepo it serves pins in its own `.node-version`. The floor follows the consumer deliberately: a lower one means testing a runtime nobody runs, and it twice let an API through that the types allowed and the runtime did not. 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.
144
+ **Node.** sentinel needs **Node >= 24.15**, which is the version the monorepo it serves pins in its own `.node-version`. The floor follows the consumer deliberately: a lower one means testing a runtime nobody runs, and it twice let an API through that the types allowed and the runtime did not. It fails fast with a clear message on an older runtime rather than crashing. That message comes from sentinel itself, so `npx` must run on Node >= 24.15 too: the npm 10 that Node 20 and 22 bundle fails while installing sentinel (`Cannot read properties of null (reading 'edgesOut')`), before sentinel's own check can run. 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.
145
145
 
146
146
  **Try it without installing.** A one-off run needs no auth and touches nothing:
147
147
 
@@ -254,7 +254,7 @@ The rules live in `sentinel`. Each module keeps a **thin, generated stub** per t
254
254
 
255
255
  - **Rules in sentinel** — one source of truth, versioned.
256
256
  - **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.
257
- - **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.
257
+ - **Drift guard** — sentinel validates that each stub is _only_ the sanctioned `extends`, with nothing of the preset's overridden. Unsanctioned drift is flagged in CI; a genuine exception must be **declared in an allowlist** (visible, reviewed), never silent. One addition is sanctioned, in lint: what the preset does not govern (a rule it does not name, a plugin, jsPlugin or setting it does not set) is the module's own, kept by `--init`, not drift, and listed by `--inspect`; a rule the preset names, set to another value, or an entry that collides with the preset's, is drift ([lint-adoption.md](docs/lint-adoption.md#what---inspect-calls-drift-and-what-it-lists-as-yours)).
258
258
  - **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.
259
259
  - **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.
260
260
 
@@ -467,7 +467,8 @@ preset guessing): for each module it reads the committed `tsconfig` `extends` ch
467
467
  reports whether it is **adopted** (extends a sentinel preset), which preset, and whether
468
468
  it is **conformant** (drift-free, a re-`update` would change nothing), plus a coverage
469
469
  footer. This is the **drift guard** as a command, `--status --ci` exits non-zero when an
470
- adopted module has drifted.
470
+ adopted module has drifted. For lint, what a module adds that its preset does not govern is not
471
+ drift; a rule the preset names, set differently (stricter included, for now), is.
471
472
 
472
473
  ```
473
474
  ✓ host-admin (react) typescript — adopted (react) conformant
@@ -12,10 +12,10 @@ import {
12
12
  presetNotInstalledYet,
13
13
  registerAdapters,
14
14
  resolve
15
- } from "../chunk-PC5R4DX5.js";
15
+ } from "../chunk-R7DS7VN4.js";
16
16
  import {
17
17
  resolveContext
18
- } from "../chunk-P7NYSMLS.js";
18
+ } from "../chunk-67QLOX6F.js";
19
19
  import {
20
20
  ensureWorkspacePrep,
21
21
  findWorkspaceRoot,
@@ -34,7 +34,7 @@ import {
34
34
  TARGETS,
35
35
  VERBS
36
36
  } from "../chunk-7E6YTTAK.js";
37
- import "../chunk-NAFI7OLI.js";
37
+ import "../chunk-4EIY7TTR.js";
38
38
  import "../chunk-O7REVMOC.js";
39
39
  import "../chunk-QXFCZON7.js";
40
40
 
@@ -183,6 +183,11 @@ async function analyse(params) {
183
183
  }
184
184
  return { results, worstCode };
185
185
  }
186
+ function definedOnly(source, keys) {
187
+ return Object.fromEntries(
188
+ keys.flatMap((key) => source[key] === void 0 ? [] : [[key, source[key]]])
189
+ );
190
+ }
186
191
  async function analyseRepository(params) {
187
192
  const results = [];
188
193
  let worstCode = 0;
@@ -209,8 +214,10 @@ async function analyseRepository(params) {
209
214
  ok: result.ok,
210
215
  status: result.ok ? "ok" : "failed",
211
216
  // A run row carries the verdict's two inputs and nothing more: the report itself was just
212
- // printed above it, and repeating it inline would print it twice.
213
- data: params.verb === "run" ? { level: metrics.level, blocking: metrics.blocking } : metrics
217
+ // printed above it, and repeating it inline would print it twice. A run that could not
218
+ // start has no blocking count, and carries its `error` instead: a key is left out when it
219
+ // has no value, never printed as `undefined`.
220
+ data: params.verb === "run" ? definedOnly(metrics, ["level", "blocking", "error"]) : metrics
214
221
  });
215
222
  worstCode = Math.max(worstCode, result.code);
216
223
  } catch (error) {
@@ -577,7 +584,7 @@ async function runValidate(ctx) {
577
584
  );
578
585
  }
579
586
  if (!ctx.targets.includes("test")) return 1;
580
- const { validateModuleTests } = await import("../validate-5SD5CTTO.js");
587
+ const { validateModuleTests } = await import("../validate-NP7X6XCG.js");
581
588
  const outcomes = modules.flatMap(
582
589
  (module) => validateModuleTests(module.name, module.root)
583
590
  );
@@ -609,7 +616,9 @@ var HEAD_KEYS = /* @__PURE__ */ new Set([
609
616
  "diagnosticsTruncated"
610
617
  ]);
611
618
  function isRuleList(value) {
612
- return Array.isArray(value) && value.every((entry) => typeof entry === "object" && entry !== null && "rule" in entry);
619
+ return Array.isArray(value) && value.every(
620
+ (entry) => typeof entry === "object" && entry !== null && ("rule" in entry || "entry" in entry)
621
+ );
613
622
  }
614
623
  var RULE_BLOCKS = [
615
624
  { key: "deferred", label: "deferred (phase 1, non-breaking):" },
@@ -623,7 +632,11 @@ var RULE_BLOCKS = [
623
632
  // adopted module can be missing a check nobody in it ever chose to drop. Reduced coverage
624
633
  // has to be readable, or the difference between an accepted cost and a regression is only
625
634
  // that nobody looked.
626
- { key: "unenforced", label: "not checked at all:" }
635
+ { key: "unenforced", label: "not checked at all:" },
636
+ // The other direction, and listed for the same reason: a rule a module adds to its lint
637
+ // preset is not drift, so nothing else on the row would ever show it was there.
638
+ { key: "added", label: "added by the module (kept by --init, not drift):" },
639
+ { key: "addedConfig", label: "config added by the module (kept by --init, not drift):" }
627
640
  ];
628
641
  var MAX_INLINE_WIDTH = 60;
629
642
  function renderValue(key, value, p) {
@@ -667,9 +680,9 @@ function renderSummary(item, p) {
667
680
  for (const { label, rules } of blocks) {
668
681
  if (rules.length === 0) continue;
669
682
  lines.push(` ${p.warn(label)}`);
670
- for (const { rule, phase, reason } of rules) {
683
+ for (const { rule, entry, phase, reason } of rules) {
671
684
  const detail = phase === void 0 ? reason : `phase ${phase}: ${reason}`;
672
- lines.push(` ${p.warn("\u2022")} ${p.strong(rule)} ${p.dim(`\u2014 ${detail}`)}`);
685
+ lines.push(` ${p.warn("\u2022")} ${p.strong(rule ?? entry ?? "?")} ${p.dim(`\u2014 ${detail}`)}`);
673
686
  }
674
687
  }
675
688
  return lines.join("\n");
@@ -22,10 +22,23 @@ var NESTED_KEYS = {
22
22
  function isRecord(value) {
23
23
  return typeof value === "object" && value !== null && !Array.isArray(value);
24
24
  }
25
+ function kindOf(value) {
26
+ if (value === null) return "null";
27
+ if (Array.isArray(value)) return "an array";
28
+ if (value instanceof RegExp) return "a RegExp";
29
+ if (value === "") return "an empty string";
30
+ const type = typeof value;
31
+ return type === "object" ? "an object" : `a ${type}`;
32
+ }
33
+ function badEntry(path, list, valid) {
34
+ const index = list.findIndex((entry) => !valid(entry));
35
+ return index === -1 ? void 0 : `${path}[${index}] is ${kindOf(list[index])}`;
36
+ }
25
37
  function stringListProblems(path, value) {
26
38
  if (value === void 0) return [];
27
- if (!Array.isArray(value)) return [`${path} must be an array of strings`];
28
- return value.some((entry) => typeof entry !== "string" || entry === "") ? [`${path} must hold non-empty strings only`] : [];
39
+ if (!Array.isArray(value)) return [`${path} must be an array of strings, got ${kindOf(value)}`];
40
+ const bad = badEntry(path, value, (entry) => typeof entry === "string" && entry !== "");
41
+ return bad ? [`${path} must hold non-empty strings only: ${bad}`] : [];
29
42
  }
30
43
  function problemsOf(input) {
31
44
  const problems = [];
@@ -37,10 +50,11 @@ function problemsOf(input) {
37
50
  const nested = NESTED_KEYS[key];
38
51
  if (nested === void 0) {
39
52
  if (key === "knownExternalPatterns") {
53
+ const bad = Array.isArray(value) ? badEntry(key, value, (entry) => entry instanceof RegExp || typeof entry === "string") : void 0;
40
54
  if (value !== void 0 && !Array.isArray(value)) {
41
- problems.push(`${key} must be an array of RegExp or strings`);
42
- } else if (Array.isArray(value) && value.some((entry) => !(entry instanceof RegExp) && typeof entry !== "string")) {
43
- problems.push(`${key} must hold RegExp or strings only`);
55
+ problems.push(`${key} must be an array of RegExp or strings, got ${kindOf(value)}`);
56
+ } else if (bad) {
57
+ problems.push(`${key} must hold RegExp or strings only: ${bad}`);
44
58
  }
45
59
  } else {
46
60
  problems.push(...stringListProblems(key, value));
@@ -49,7 +63,7 @@ function problemsOf(input) {
49
63
  }
50
64
  if (value === void 0) continue;
51
65
  if (!isRecord(value)) {
52
- problems.push(`${key} must be an object`);
66
+ problems.push(`${key} must be an object, got ${kindOf(value)}`);
53
67
  continue;
54
68
  }
55
69
  for (const [inner, innerValue] of Object.entries(value)) {
@@ -60,7 +74,7 @@ function problemsOf(input) {
60
74
  return problems;
61
75
  }
62
76
  function defineEnv(input = {}) {
63
- if (!isRecord(input)) throw new Error("env config must export an object");
77
+ if (!isRecord(input)) throw new Error(`env config must export an object, got ${kindOf(input)}`);
64
78
  const problems = problemsOf(input);
65
79
  if (problems.length > 0) {
66
80
  throw new Error(`invalid env config:
@@ -72,4 +86,4 @@ function defineEnv(input = {}) {
72
86
  export {
73
87
  defineEnv
74
88
  };
75
- //# sourceMappingURL=chunk-NAFI7OLI.js.map
89
+ //# sourceMappingURL=chunk-4EIY7TTR.js.map
@@ -0,0 +1 @@
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/**\n * What a value IS, in a word, for a message that says what was found instead of what was wanted.\n * Its kind, never its content: a config line can hold a secret's location, and a refusal has no\n * reason to repeat it.\n */\nfunction kindOf(value: unknown): string {\n if (value === null) return 'null'\n if (Array.isArray(value)) return 'an array'\n if (value instanceof RegExp) return 'a RegExp'\n if (value === '') return 'an empty string'\n const type = typeof value\n return type === 'object' ? 'an object' : `a ${type}`\n}\n\n/** The first entry of a list that fails `valid`, named by its index and its kind. */\nfunction badEntry(path: string, list: readonly unknown[], valid: (entry: unknown) => boolean) {\n const index = list.findIndex((entry) => !valid(entry))\n return index === -1 ? undefined : `${path}[${index}] is ${kindOf(list[index])}`\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, got ${kindOf(value)}`]\n const bad = badEntry(path, value, (entry) => typeof entry === 'string' && entry !== '')\n return bad ? [`${path} must hold non-empty strings only: ${bad}`] : []\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 const bad = Array.isArray(value)\n ? badEntry(key, value, (entry) => entry instanceof RegExp || typeof entry === 'string')\n : undefined\n if (value !== undefined && !Array.isArray(value)) {\n problems.push(`${key} must be an array of RegExp or strings, got ${kindOf(value)}`)\n } else if (bad) {\n problems.push(`${key} must hold RegExp or strings only: ${bad}`)\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, got ${kindOf(value)}`)\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, got ${kindOf(input)}`)\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":"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","names":[]}