@hublo/sentinel 1.5.1-alpha.2 → 1.6.0-alpha.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/dist/index.d.ts CHANGED
@@ -32,11 +32,31 @@ declare function registerAdapters(): void;
32
32
  declare const VERBS: readonly ["run", "inspect", "init", "migrate", "validate", "report", "status"];
33
33
  type Verb = (typeof VERBS)[number];
34
34
  /** Targets: the kind of check. The CLI `--<target>` flags map 1:1 to these. */
35
- declare const TARGETS: readonly ["lint", "format", "typescript", "build", "dev", "test", "static-analysis", "runtime-analysis", "arch"];
35
+ declare const TARGETS: readonly ["lint", "format", "typescript", "build", "dev", "test", "static-analysis", "env", "runtime-analysis", "arch"];
36
36
  type Target = (typeof TARGETS)[number];
37
37
  /** Presets: the stack preset a project resolves to (strict by default). */
38
38
  declare const PRESET_NAMES: readonly ["react", "nest", "svelte", "node", "tools"];
39
39
  type PresetName = (typeof PRESET_NAMES)[number];
40
+ /**
41
+ * What the env role does with what it finds, from `SENTINEL_ENV_LEVEL`.
42
+ *
43
+ * | level | reports | in GitHub Actions | exit |
44
+ * | --- | --- | --- | --- |
45
+ * | `info` | yes | nothing more | 0 |
46
+ * | `warning`, the default | yes | a `::warning` annotation per finding | 0 |
47
+ * | `error` | yes | `::error` on a blocking finding, `::warning` on the rest | 1 on a blocking finding |
48
+ *
49
+ * An environment variable and not a flag, because the same command runs in a developer's shell and
50
+ * in every repository's CI, and what changes between them is the policy, not the command: a team
51
+ * moves from `warning` to `error` by setting one variable in its workflow, without editing the step.
52
+ *
53
+ * ⛔ Here and not in the role for the reason `OUTPUT_SHAPES` is: it is read and VALIDATED at the CLI
54
+ * edge, once, before any role runs, and handed to the role as data. A role that read the
55
+ * environment itself would be the one place in this package whose behaviour a test cannot pin by
56
+ * its inputs.
57
+ */
58
+ declare const ENV_LEVELS: readonly ["info", "warning", "error"];
59
+ type EnvLevel = (typeof ENV_LEVELS)[number];
40
60
  /**
41
61
  * @deprecated Renamed to `PresetName`. Kept so an existing `import type { Flavour }` keeps
42
62
  * compiling: the CLI kept `--flavour` working for the same reason, and breaking the type
@@ -120,6 +140,16 @@ interface RunContext {
120
140
  * The role rejects a shape it does not know, by name.
121
141
  */
122
142
  output?: string;
143
+ /**
144
+ * `SENTINEL_ENV_LEVEL`, validated by the CLI. Read by the env role only, and only through here:
145
+ * the role never reads the environment, so a test pins its behaviour by passing a value.
146
+ */
147
+ envLevel?: EnvLevel;
148
+ /**
149
+ * Whether the run is inside GitHub Actions (`GITHUB_ACTIONS=true`), read once by the CLI for the
150
+ * same reason as `envLevel`. A role that can annotate a pull request does so only when this is set.
151
+ */
152
+ githubActions?: boolean;
123
153
  }
124
154
  /**
125
155
  * A single, DECLARATIVE file operation an `--init` wants to happen. Adapters
package/dist/index.js CHANGED
@@ -6,12 +6,13 @@ import {
6
6
  registerAdapters,
7
7
  resolve,
8
8
  setDefaultRunner
9
- } from "./chunk-JHLNNIZC.js";
9
+ } from "./chunk-PC5R4DX5.js";
10
10
  import {
11
11
  BaseAdapter
12
12
  } from "./chunk-P7NYSMLS.js";
13
13
  import "./chunk-JRZI5IIN.js";
14
- import "./chunk-WBDBRGLV.js";
14
+ import "./chunk-7E6YTTAK.js";
15
+ import "./chunk-NAFI7OLI.js";
15
16
  import "./chunk-O7REVMOC.js";
16
17
  import "./chunk-QXFCZON7.js";
17
18
  export {
@@ -0,0 +1,90 @@
1
+ /**
2
+ * `defineEnv`: what a repository writes to declare its own env facts, all of them optional.
3
+ *
4
+ * ```js
5
+ * // env.config.mjs, at the repository root
6
+ * import { defineEnv } from '@hublo/sentinel/env'
7
+ *
8
+ * export default defineEnv({
9
+ * externallyProvided: ['NODE_ENV'],
10
+ * allowNonBlankedSecrets: ['apps/front/x/.env.example:LEGACY_API_KEY'],
11
+ * })
12
+ * ```
13
+ *
14
+ * A repository that does not install sentinel, and runs it with `npx`, cannot import this module:
15
+ * its own `node_modules` has no `@hublo/sentinel`. So the file may export the same object PLAIN,
16
+ * and the role passes it through this function itself. Both spellings mean the same thing, and the
17
+ * function exists for the one that can import it: it validates at the moment the file is written,
18
+ * in an editor that knows the types.
19
+ *
20
+ * ## What a repository declares, and what stays sentinel's
21
+ *
22
+ * A repository declares FACTS about itself: which variables its CI injects, which committed value
23
+ * was reviewed and is not a secret, which Dockerfile must run as root, where its code lives. It
24
+ * does not redefine what a secret NAME looks like or how code reads env, which are the same in
25
+ * every repository and ship in `presets/shared.json`.
26
+ *
27
+ * Two semantics, stated once:
28
+ *
29
+ * - `readers`, `envFiles` and `contracts` ADD to what sentinel ships, so no repository loses
30
+ * `getOrFail` or `**\/.env` by declaring a reader or a glob of its own;
31
+ * - `scan` and `references` REPLACE, key by key: a repository naming its `roots` says where its
32
+ * code is, and keeping `.` beside them would scan everything anyway.
33
+ *
34
+ * ⛔ An unknown key is refused, by name. The file is `.mjs`, so nothing type-checks it, and a key
35
+ * spelled `allowNonBlankedSecret` would otherwise be ignored in silence while the finding it was
36
+ * meant to downgrade kept failing the build.
37
+ */
38
+ /** A source scope a repository may narrow, every key optional. */
39
+ interface EnvScopeInput {
40
+ roots?: string[];
41
+ ext?: string[];
42
+ excludeSuffixes?: string[];
43
+ excludeDirs?: string[];
44
+ }
45
+ /** What `env.config.mjs` exports. */
46
+ interface EnvInput {
47
+ /** Required variables in no contract because CI injects them or a secret store provides them. */
48
+ externallyProvided?: string[];
49
+ /** Declared variables consumed outside scannable source, so never reported dead (rule 4). */
50
+ knownExternalVars?: string[];
51
+ /** The same, by pattern, for a whole category such as `/_DATABASE_URL$/`. */
52
+ knownExternalPatterns?: (RegExp | string)[];
53
+ /** `<file>:<VAR>` locations whose committed value was reviewed and is not a secret (rule 6). */
54
+ allowNonBlankedSecrets?: string[];
55
+ /** Committed real env files permitted anyway, such as a test fixture (rule 2). */
56
+ allowedCommittedEnvFiles?: string[];
57
+ /** Globs of env files to read, ADDED to `**\/.env`, `**\/.env.*` and `**\/*.env`. */
58
+ envFiles?: string[];
59
+ /** Globs of contracts, ADDED to `**\/*.env*.example`. */
60
+ contracts?: string[];
61
+ /** Where the named readers are looked for (rules 1 and 3). Replaces, key by key. */
62
+ scan?: EnvScopeInput;
63
+ /** Where direct `process.env` reads are looked for (rules 3 and 4). Replaces, key by key. */
64
+ references?: EnvScopeInput;
65
+ /** Reader names ADDED to sentinel's: `required` throw when absent, `optional` default. */
66
+ readers?: {
67
+ required?: string[];
68
+ optional?: string[];
69
+ };
70
+ /** Docker rules: Dockerfiles allowed to run as root, and path prefixes not checked at all. */
71
+ docker?: {
72
+ allowRoot?: string[];
73
+ excludePaths?: string[];
74
+ };
75
+ }
76
+ /**
77
+ * Validate a repository's env declaration and return it unchanged.
78
+ *
79
+ * The defaults are NOT merged in here. They ship with sentinel and are applied when the role runs,
80
+ * so changing one reaches every repository at its next sentinel version instead of leaving a copy
81
+ * frozen in each config. That also makes this function idempotent, which is what lets the role
82
+ * pass a plain exported object through it a second time.
83
+ *
84
+ * @param input the declaration; omitted, the repository declares nothing
85
+ * @returns the same declaration
86
+ * @throws naming every unknown key and every malformed value at once
87
+ */
88
+ declare function defineEnv(input?: EnvInput): EnvInput;
89
+
90
+ export { type EnvInput, type EnvScopeInput, defineEnv };
@@ -0,0 +1,7 @@
1
+ import {
2
+ defineEnv
3
+ } from "../../chunk-NAFI7OLI.js";
4
+ export {
5
+ defineEnv
6
+ };
7
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  defineStaticAnalysis
3
- } from "../../chunk-WBDBRGLV.js";
3
+ } from "../../chunk-7E6YTTAK.js";
4
4
  export {
5
5
  defineStaticAnalysis
6
6
  };
@@ -1,3 +1,7 @@
1
+ import {
2
+ clearDeepMocks,
3
+ resetDeepMocks
4
+ } from "../../../chunk-2XLX6PFR.js";
1
5
  import {
2
6
  alreadyInstalled
3
7
  } from "../../../chunk-NUOQXAYR.js";
@@ -5,10 +9,6 @@ import {
5
9
  installWorkspaceSetup
6
10
  } from "../../../chunk-LFNOBQZU.js";
7
11
  import "../../../chunk-ELZHIN6E.js";
8
- import {
9
- clearDeepMocks,
10
- resetDeepMocks
11
- } from "../../../chunk-2XLX6PFR.js";
12
12
  import "../../../chunk-JRZI5IIN.js";
13
13
 
14
14
  // src/roles/test/setup/jest-parity.ts
@@ -0,0 +1,229 @@
1
+ # Env conformance cheat sheet
2
+
3
+ The `env` role checks a **repository**, not a module: which env files git tracks, whether the code
4
+ reads a variable no contract declares, whether a secret-named variable carries a committed value,
5
+ and whether the Dockerfiles copy paths that exist and drop root at runtime. It is the monorepo's
6
+ `tools/repo-env` (`validate`, `docker-check`, `inventory` and `ci-gate`) made to run on any
7
+ repository, including one that is not an nx workspace.
8
+
9
+ ```bash
10
+ # from the repository ROOT (the git top-level), nothing to install
11
+ npx --yes @hublo/sentinel@<version> --run --env
12
+ ```
13
+
14
+ It writes no config, so there is no `--init`. With no `env.config.mjs` it runs on sentinel's
15
+ defaults, and says so in its header.
16
+
17
+ ## The level: `SENTINEL_ENV_LEVEL`
18
+
19
+ The same findings are reported at every level. The level decides what they DO.
20
+
21
+ | `SENTINEL_ENV_LEVEL` | reports | in GitHub Actions (`GITHUB_ACTIONS=true`) | exit |
22
+ | -------------------- | ------- | ----------------------------------------------------------- | --------------------------- |
23
+ | `info` | yes | nothing more | 0 |
24
+ | `warning` (default) | yes | one `::warning file=…,line=…::` annotation per finding | 0 |
25
+ | `error` | yes | `::error` on a blocking finding, `::warning` on an advisory | **1** on a blocking finding |
26
+
27
+ An unknown value is refused with exit 1 rather than read as the default: a typo of `error` that ran
28
+ as `warning` would turn a gate into a report, and nobody would see it stop gating.
29
+
30
+ ⚠️ It is refused like an unknown option: before ANY role runs. A bare `sentinel --run` at the
31
+ repository root includes `env`, so an invalid `SENTINEL_ENV_LEVEL` there refuses the whole sweep,
32
+ lint, format, typescript, build and test included, not only `env`. The variable is read only when
33
+ `env` is part of the run: `sentinel --run --lint`, a bare `--run` from a module below the root, and
34
+ every other verb never read it, so it cannot refuse them.
35
+
36
+ Under `--json` the annotations go to **stderr**, so stdout stays one JSON document; the runner
37
+ reads workflow commands from both streams.
38
+
39
+ ## The rules
40
+
41
+ | # | finding id | what it reports | severity |
42
+ | --- | -------------------------------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------- |
43
+ | 1 | `required-var-undeclared` | a variable a required reader (`getOrFail`, …) reads, declared in no contract | blocking |
44
+ | 2 | `committed-env-file` | a real env file tracked by git (anything that is not an `.example` contract) | blocking |
45
+ | 3 | `var-not-in-app-contract` | a variable an app reads, declared only in ANOTHER app's contract | advisory |
46
+ | 4 | `dead-var` | a declared variable read nowhere in source | advisory |
47
+ | 5 | `public-secret-to-review` | a public-prefixed (`VITE_`, `PUBLIC_`, `NEXT_PUBLIC_`, `REACT_APP_`) secret-named value | advisory |
48
+ | 6 | `secret-not-blanked` | a secret-named variable with a committed value, in a contract OR a real env file | blocking (`secret-allowlisted`: advisory) |
49
+ | 7 | `dead-copy-path` | a `COPY` source that does not exist | blocking |
50
+ | 8 | `nx-project-missing` / `nx-target-missing` | an `nx run <project>:<target>` that does not exist | blocking |
51
+ | 9 | `node-version-mismatch` | a `FROM node:<v>` that differs from `.node-version` | blocking |
52
+ | 10 | `dockerfile-runs-as-root` / `copy-not-owned` | a runtime stage running as root, or a runtime `COPY` without `--chown` | blocking |
53
+
54
+ The blocking set is `repo-env ci-gate`'s, unchanged. Rules 3, 4 and 5 never block, at any level: a
55
+ variable "read nowhere" can be read by a branch not merged yet, which is how the monorepo once
56
+ deleted `VITE_CAREER_URL` from main while an open pull request read it.
57
+
58
+ A finding carries a **file, a line, a variable name and a rule**. Never a value: there is no field
59
+ a value could travel in, so no shape can print one.
60
+
61
+ What the role cannot check, it says, in a `note:` line: no contract committed (rules 1, 3 and 4
62
+ have nothing to compare with), no required reader call found (rule 1, naming the readers looked
63
+ for, which `readers.required` extends), no `.node-version` (rule 9), no `nx.json` or an nx that cannot run
64
+ (rule 8, with the count of references left unchecked).
65
+
66
+ ## Where it runs
67
+
68
+ | from | `sentinel --run --env` | `sentinel --run` (sweep) |
69
+ | ----------------------------------- | ---------------------------------------- | --------------------------------------- |
70
+ | the repository root (git top-level) | runs, once | includes `env` once, beside the modules |
71
+ | a directory inside it (a module) | **refused**, naming the root to run from | the module's roles only, as before |
72
+ | a directory in no git repository | **refused** | the module's roles only, as before |
73
+
74
+ ⛔ Inside a module it is refused, not scoped. Rule 3 compares one app's reads with another app's
75
+ contract and rule 2's file may sit two directories up, so a scoped report is a smaller report that
76
+ looks like a clean one. `--inspect`, `--init`, `--validate` and `--status` refuse `--env` and never
77
+ sweep it: it has no config to read or write. `--module`, `--fix` and options after `--` are refused
78
+ beside it.
79
+
80
+ Discovery is `git ls-files`, never nx: a repository with no `nx.json` has no project graph to ask,
81
+ and a filesystem walk would report a developer's untracked `.env` as committed. Rule 8 is the one
82
+ place nx is used, the repository's OWN nx from its `node_modules`, and only when `nx.json` exists.
83
+
84
+ ## Output shapes
85
+
86
+ | shape | what |
87
+ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
88
+ | console (default) | the report, grouped by rule with each rule's fix, then notes and the verdict |
89
+ | `--json` | the engine envelope; the env row carries `level`, `verdict`, `blocking`, `findings`, `notes`, `counts` and the full `inventory` (required and optional variables, contracts) |
90
+ | `--output json` | the same document, written to `env-report/env.json` |
91
+ | `--output markdown` | the pull-request recap, written to `env-report/env.md`, opening with `<!-- sentinel-env-report -->` |
92
+
93
+ `html` is not a shape this role writes, and asking it of `--env` alone is refused.
94
+
95
+ ## The repository's config: `env.config.mjs`
96
+
97
+ Optional, at the repository root. A repository declares FACTS about itself; what a secret name
98
+ looks like and how code reads env ship with sentinel.
99
+
100
+ ```js
101
+ // env.config.mjs
102
+ export default {
103
+ externallyProvided: ['NODE_ENV'], // required, but CI or a secret store provides it
104
+ knownExternalVars: ['CACHE_PATH'], // declared, consumed outside source (rule 4)
105
+ knownExternalPatterns: [/_DATABASE_URL$/], // the same, by category
106
+ allowNonBlankedSecrets: ['apps/x/.env.example:LEGACY_API_KEY'], // reviewed, not a secret (rule 6)
107
+ allowedCommittedEnvFiles: ['tests/fixtures/.env'], // rule 2
108
+ envFiles: ['**/*.envrc'], // ADDED to **/.env, **/.env.*, **/*.env
109
+ contracts: ['templates/*.env.dist'], // ADDED to **/*.env*.example
110
+ scan: { roots: ['apps', 'libs'] }, // where the named readers are looked for; replaces key by key
111
+ references: { roots: ['apps', 'libs'] }, // where direct process.env reads are looked for
112
+ readers: { required: ['mustGetEnv'] }, // ADDED to getOrFail, requireEnv, requireEnvVariable
113
+ docker: { allowRoot: ['images/legacy/Dockerfile'], excludePaths: ['local/'] },
114
+ }
115
+ ```
116
+
117
+ A repository that installs sentinel can write the same object through `defineEnv`, which an editor
118
+ type-checks:
119
+
120
+ ```js
121
+ import { defineEnv } from '@hublo/sentinel/env'
122
+ export default defineEnv({ externallyProvided: ['NODE_ENV'] })
123
+ ```
124
+
125
+ A repository that runs sentinel with `npx` cannot import it, since its own `node_modules` has no
126
+ `@hublo/sentinel`; the plain object is the spelling for it, and the error says so if the import is
127
+ tried. An unknown key is refused by name, in both spellings: the file is `.mjs`, and an ignored
128
+ `allowNonBlankedSecret` would silently stop downgrading the finding it was written for.
129
+
130
+ ### Sentinel's defaults
131
+
132
+ `presets/shared.json` in the role. Secret names match **case-insensitively** (`SECRET`, `TOKEN`,
133
+ `PASSWORD`, `PRIVATE_?KEY`, `ACCESS_?KEY`, `API_?KEY`), so serviceapp's `facturationPassword` and a
134
+ 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.
136
+
137
+ ## In CI
138
+
139
+ The role reads the plain checkout, so the step goes BEFORE anything that writes into the tree, a
140
+ secret materialization or an env-file generation included:
141
+
142
+ ```yaml
143
+ permissions:
144
+ contents: read
145
+ pull-requests: write
146
+ steps:
147
+ - uses: actions/checkout@v4
148
+ - uses: actions/setup-node@v4
149
+ with:
150
+ node-version: 24.15.0
151
+ - run: npx --yes @hublo/sentinel@<version> --run --env --output markdown
152
+ env:
153
+ SENTINEL_ENV_LEVEL: warning
154
+ # then post env-report/env.md as a sticky comment, found by its first line
155
+ ```
156
+
157
+ sentinel needs Node 24.15 to run, whatever Node the repository itself uses: `setup-node` here
158
+ installs it for this step only.
159
+
160
+ GitHub documents a cap of 10 warning and 10 error annotations per step
161
+ (<https://docs.github.com/en/rest/checks/runs>): on a repository with more findings, the
162
+ pull request shows only part of the annotations, and the markdown recap is the complete list.
163
+
164
+ ## What changed from `tools/repo-env`
165
+
166
+ Measured by running both on the same monorepo commit, the monorepo's `repo-env.config.mjs`
167
+ translated into an `env.config.mjs`: **51 of 52 findings identical**, plus every optional variable,
168
+ and the one difference is intended (the first line below). Measured twice: on `origin/main` 3364bb9
169
+ (97 optional variables) and again on 50c5157 on 30 October (102), after the COPY fix below, with
170
+ the same single difference.
171
+
172
+ | command of `tools/repo-env` | with sentinel |
173
+ | --------------------------- | --------------------------------------------------------------- |
174
+ | `validate` | `sentinel --run --env` (level `warning` or `info`: exit 0) |
175
+ | `docker-check` | the same run: rules 7 to 10 are in every report |
176
+ | `ci-gate` | `SENTINEL_ENV_LEVEL=error sentinel --run --env` |
177
+ | `inventory` | `sentinel --run --env --json`, the row's `inventory` |
178
+ | `validate --app <name>` | not ported: the role refuses a scoped run (see "Where it runs") |
179
+ | `materialize` | not ported, stays in the monorepo |
180
+
181
+ - `Dockerfile.dockerignore` is no longer audited as an image running as root. A Dockerfile is read
182
+ from its NAME (`Dockerfile`, `Dockerfile.<x>`, `Dockerfile-<x>`, `<x>.Dockerfile`), never from a
183
+ `.dockerignore` or a `.md` that mentions one.
184
+ - A lowercase or camel-case key is a key (`facturationPassword=` used to be skipped), and a
185
+ reference to one is seen, so it is not reported dead forever.
186
+ - A bare `$VAR` in a `COPY` is a build argument, not a path that does not exist.
187
+ - A `COPY` source is read the way Docker reads it: `COPY "entrypoint.sh" /app` checks
188
+ `entrypoint.sh`, the JSON form `COPY ["a", "b", "/app/"]` checks `a` and `b`, a heredoc checks
189
+ nothing. The original kept the quotes and reported servicecontract's `entrypoint.sh`, which sits
190
+ next to its Dockerfile, as a dead path, blocking.
191
+ - A `v`-prefixed `.node-version` (`v10.15.3`) compares as a version; a missing one is a note, not
192
+ a crash.
193
+ - Rule 6 reads every committed env file, not only contracts, and no contract at all is a note
194
+ rather than a refusal to run.
195
+ - Every finding has a line, so an annotation lands on it.
196
+
197
+ ## Outside the monorepo: what `tools/repo-env` assumed, and what the port does
198
+
199
+ `repo-env`'s README calls `repo-io.mjs` "mostly" repo-agnostic. These are the places where it, or
200
+ its config, assumed the monorepo, and what the role does in a repository without nx and without
201
+ that layout. Each line was RUN, from the packed tarball through `npx`, on six hublo repositories
202
+ that are not the monorepo (serviceapp, serviceapi, servicecontract, servicemission, contract-front,
203
+ servicebackoffice), on 30 October.
204
+
205
+ | monorepo assumption | in the port | outside the monorepo |
206
+ | ----------------------------------------------------------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
207
+ | the root is the script's own `../..` | the git top-level | works, 6 of 6 |
208
+ | sources found by walking `apps/nest`, `libs/nest`, `apps/front`, `libs/front` | `git ls-files` under `.` | works, 6 of 6 |
209
+ | required reads are `getOrFail`, `requireEnv`, `requireEnvVariable` | the same names, extendable with `readers` | **degrades, said**: a repository reading `process.env.X` directly has no required read, so rule 1 has nothing to check, and a note says so, naming the readers (3 of 6) |
210
+ | contracts are `*.env*.example` | the same, extendable with `contracts` | **degrades, said**: with none, rules 1, 3 and 4 are skipped with a note (serviceapp, servicebackoffice) |
211
+ | a real env file is `.env` or `.env.<x>` | also `**/*.env` | works: serviceapp's 9 `variables/*.variables.env` |
212
+ | secret names are upper-case | case-insensitive | works: serviceapp's `facturationPassword` |
213
+ | public prefixes are `VITE_`, `PUBLIC_` | also `REACT_APP_`, `NEXT_PUBLIC_` | works: `REACT_APP_SEGMENT_API_KEY_ADMINS` is reviewed, not blocked |
214
+ | apps are directories with a `project.json` or a named `package.json` | the same, from tracked manifests | **degrades, silent**: a single-package repository has no app below its root, so rule 3 never fires (0 in 6 of 6) |
215
+ | rule 8 asks `pnpm exec nx` | the repository's own nx, only when `nx.json` exists | **degrades, said**: rule 8 skipped with a note (6 of 6) |
216
+ | the Node version is in `.node-version` | the same file, not configurable | **degrades, said**: a repository with `.nvmrc` only gets the rule 9 note (servicebackoffice) |
217
+ | a Dockerfile is any path containing `Dockerfile` | read from its name | works: `Dockerfile.prod` is an image |
218
+ | a `COPY` source is a space-separated word | read as Docker reads it | **broke, fixed**: servicecontract's quoted `"entrypoint.sh"` was a blocking dead path |
219
+ | local dev-compose Dockerfiles live in `local/` | no default, `docker.excludePaths` | works |
220
+ | the allowlists name monorepo files | empty defaults, `env.config.mjs` | works, as a plain object through `npx` and through `defineEnv` from an installed tarball |
221
+
222
+ Two original behaviours stay as they were, because they are judgements, not layout: a final stage
223
+ on an image that is not node (contract-front's `nginx`) is reported as running as root, and a
224
+ variable read by building its name at runtime (serviceapi's `DATABASE_URL_READ2`, found by a
225
+ `process.env` key filter) is reported dead, which `knownExternalPatterns` answers.
226
+
227
+ `materialize` (seeding test env files from contracts) stays in the monorepo: it writes runtime
228
+ files for one repository's test setup, which is functional, not conformance. Secret scanning by
229
+ VALUE is gitleaks' job and is not part of this role.