@onlineapps/conn-orch-validator 9.0.0 → 11.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +546 -0
  2. package/README.md +337 -19
  3. package/docs/DESIGN.md +32 -9
  4. package/manifests/biz-service.manifest.json +56 -6
  5. package/package.json +3 -2
  6. package/src/CookbookTestRunner.js +134 -22
  7. package/src/ValidationOrchestrator.js +312 -73
  8. package/src/cli/biz-ci-gate.js +191 -15
  9. package/src/cli/oa-sync-template.js +23 -8
  10. package/src/cli/oa-validate.js +70 -2
  11. package/src/index.js +21 -13
  12. package/src/lint/scripts/lintScripts.js +65 -18
  13. package/src/manifest/checks/composeRunnerBlock.js +37 -20
  14. package/src/manifest/checks/discoveryOrphan.js +2 -1
  15. package/src/manifest/checks/docsLintBridge.js +79 -21
  16. package/src/manifest/checks/gitTracked.js +14 -28
  17. package/src/manifest/checks/libraryPackage.js +3 -1
  18. package/src/manifest/checks/libraryWorkspace.js +18 -3
  19. package/src/manifest/checks/readmeRegion.js +9 -1
  20. package/src/manifest/checks/serviceConfig.js +29 -12
  21. package/src/manifest/checks/serviceDb.js +176 -7
  22. package/src/manifest/checks/serviceFiles.js +34 -7
  23. package/src/manifest/checks/serviceIdentityRows.js +3 -1
  24. package/src/manifest/checks/serviceRuntime.js +126 -1
  25. package/src/manifest/discovery.js +25 -7
  26. package/src/manifest/gitCheckout.js +84 -0
  27. package/src/manifest/runManifest.js +58 -7
  28. package/src/manifest/workspaceRoot.js +91 -5
  29. package/src/sync/serviceTemplate.js +76 -7
  30. package/src/sync/sharedEnv.js +11 -4
  31. package/src/sync/uniformFiles.js +91 -21
  32. package/src/utils/bizCiGateContract.js +25 -1
  33. package/src/utils/dbAccountGrants.js +126 -0
  34. package/src/utils/envContract.js +36 -6
  35. package/src/utils/envReads.js +102 -0
  36. package/src/utils/installContract.js +46 -5
  37. package/src/utils/libCompat.js +39 -19
  38. package/src/utils/preValidation.js +56 -11
  39. package/src/utils/stepFailure.js +106 -19
  40. package/src/utils/stepReferences.js +278 -0
  41. package/src/utils/testCoverageContract.js +60 -2
  42. package/src/utils/throwawaySchema.js +92 -7
  43. package/src/validatorIdentity.js +31 -0
  44. package/src/validators/ServiceStructureValidator.js +47 -15
  45. package/src/validators/ValidationProofGenerator.js +73 -34
  46. package/templates/business-service/.dockerignore +9 -1
  47. package/templates/business-service/.gitlab-ci.yml +199 -35
  48. package/templates/business-service/Dockerfile +49 -16
  49. package/templates/business-service/README.md +56 -9
  50. package/templates/business-service/config/env-templates/__SERVICE_NAME__.env +17 -5
  51. package/templates/business-service/config/env-templates/shared.env +8 -2
  52. package/templates/business-service/docker-compose.production.yml +9 -0
  53. package/templates/business-service/docker-compose.yml +17 -0
  54. package/templates/business-service/docs/80-setup/PLATFORM_MATRIX.md +1 -1
  55. package/templates/business-service/docs/80-setup/VALIDATION.md +1 -1
  56. package/templates/business-service/jest.config.js +9 -1
  57. package/templates/business-service/package.json.template +1 -1
  58. package/src/mocks/MockStorage.js +0 -188
package/README.md CHANGED
@@ -23,7 +23,7 @@ This is **NOT** a development testing tool. This is a **production validation or
23
23
  2. **Validates configuration** - `config.json`, `operations.json` and `integration-contract.json`, through the manifest rows that own them
24
24
  3. **Validates the environment contract** - every `env.required` name is set
25
25
  4. **Validates operations** - the v3 handler-registry rules
26
- 5. **Validates business logic** - cookbook tests with mocked infrastructure
26
+ 5. **Validates business logic** - cookbook tests against the two mocked doubles named below
27
27
  6. **Validates connector integration** - the two declarations agree and the environment backs them
28
28
  7. **Generates validation proof** - SHA256 proof carried into registration
29
29
 
@@ -58,7 +58,7 @@ runs Tier-1 validation in phase 0.2 — before MQ connects, before registration.
58
58
  sentences of its own, and the same defect was reported twice
59
59
  3. **Environment Contract** - every variable the contract declares `env.required` is set
60
60
  4. **Operations Compliance** - every operation declares `handler`, `bundle_scope`, `input`, `output`, and none carries a retired v2 field (`endpoint`, `method`, `path`); a missing `description` is a warning
61
- 5. **Cookbook Tests** - business logic + integration (MOCKED infra)
61
+ 5. **Cookbook Tests** - business logic + integration; every step is dispatched in-process, with no transport, and against the service's real database (see below)
62
62
  6. **Connector Integration** - the connector declarations agree and the environment backs them
63
63
  7. **Manifest conformance** - the repository against the uniform manifest shipped in this package (see below)
64
64
 
@@ -68,6 +68,207 @@ signal saved to `ci/deployability.json`
68
68
  > The list said "Service Readiness — HTTP API works" until 2026-08-27. That step
69
69
  > was removed on 2026-08-22 under ADR 0005 and this README kept describing it.
70
70
 
71
+ ### The shape of a run's result
72
+
73
+ ```js
74
+ {
75
+ success, steps: { structure, config, env, operations, cookbooks, connectors, manifest },
76
+ errors: [ … ], warnings: [ … ],
77
+ totalTests, passedTests, failedTests,
78
+ deployable, deployFindings, notRun, durationMs, proof?, fingerprint?
79
+ }
80
+ ```
81
+
82
+ **Every step that was reached has a record in `steps`, and a step that THREW has one
83
+ too** — `{ valid: false, threw: true, errors: [ … ] }`, with whatever it had already
84
+ measured kept beside it. Absence therefore means one thing only: the run stopped before
85
+ that step (it fail-fasts after steps 1 and 3). That distinction is not cosmetic:
86
+ `@onlineapps/service-wrapper` 9.0.x decides whether a failed FÁZE 0.2 may be retried by
87
+ reading `steps.<step>.valid` — `structure`, `config`, `env`, `operations` and `manifest`
88
+ read files alone, so their verdict is permanent, while `cookbooks` and an exception of the
89
+ validator itself keep the retry budget. Until d.602 a step whose body threw left NO record,
90
+ so a structural defect read as "the validator threw" and bought the full six attempts:
91
+ 18,5 minutes of a boot that cannot succeed (measured on biz-converter, W591).
92
+
93
+ **`errors` is one flat array of two shapes**, and a reader must handle both:
94
+
95
+ | Shape | Written by | Carries `step` |
96
+ |---|---|---|
97
+ | object `{ type, path?, message, fix?, step }` | step 1, and every step that THREW (`type: 'STEP_THREW'`) | yes |
98
+ | sentence, already in the `[Context] Problem - Expected/Fix` shape | steps 2-7 by their rules | no — see below |
99
+
100
+ The `step` marker is ADDED to the objects, never substituted for what they carried, and the
101
+ sentences keep their shape. That asymmetry is deliberate and measured: the released
102
+ wrapper's `describeValidationFinding()` renders a string verbatim and an object only when
103
+ it carries `type` **and** `message`, so rewriting the sentences into `{ step, message }`
104
+ objects would deliver them to the operator as raw JSON. Unifying on objects is therefore a
105
+ BREAKING change that waits for a wrapper which reads `step` (reported to BIZ-general with
106
+ d.602). A finding's own `errors` array inside `steps.<step>` is the step's answer and is
107
+ left untouched — the marker is on the copy that travels in `errors`.
108
+
109
+ ---
110
+
111
+ ## Pre-validation runs in-process — and against the real database
112
+
113
+ A cookbook step is dispatched **in this process**, against the service's own v3
114
+ handler, with no transport of any kind. There is nothing to mock: the ctx slots
115
+ are `null` (pinned by `tests/unit/CookbookTestRunner.ctxShape.test.js`), and a
116
+ handler that needs a schema reaches it the way it reaches it in production — by
117
+ importing the service's own database module. **That connection is real.**
118
+
119
+ `mockInfrastructure` — the option that once built a MockMQClient and a
120
+ MockRegistry for transport dispatch — is retired: the runner and the cookbook
121
+ format both refuse it by name (`tests/unit/mockInfrastructureRetired.test.js`).
122
+
123
+ What the real database requires is ORDER, and the contract is what makes the
124
+ order decidable:
125
+
126
+ - a service that needs a database declares **both** `requiredConnectors.db: true`
127
+ **and** the `database` block; the contract refuses either one standing alone,
128
+ in both directions;
129
+ - `setup-db` therefore builds a schema exactly where one is declared, and reports
130
+ `NOT APPLICABLE` — never `OK` — where none is;
131
+ - `run-prevalidation` (`npm run test:cookbooks`) comes **after** `ci:gate:setup`
132
+ in the job, because by then the schema its handlers will reach exists.
133
+
134
+ Run out of that order, or against a service whose schema was never built, the
135
+ cookbooks fail in the handler's own database error. That is the truthful
136
+ failure.
137
+
138
+ ---
139
+
140
+ ## A recipe builds its own fixture — `{{steps.<step_id>.output.…}}`
141
+
142
+ A step's `input` may reference what an EARLIER step of the same cookbook
143
+ returned. The runner resolves those references before it calls the handler, so a
144
+ recipe can create the data it needs, use it, and delete it again — instead of
145
+ depending on a seed that exists in one environment and not in another.
146
+
147
+ ```json
148
+ {
149
+ "steps": [
150
+ { "step_id": "create_fixture", "service": "biz-x", "operation": "create-row",
151
+ "input": { "label": "boot-fixture" } },
152
+ { "step_id": "use_fixture", "service": "biz-x", "operation": "convert",
153
+ "depends_on": ["create_fixture"],
154
+ "input": { "id": "{{steps.create_fixture.output.id}}" } },
155
+ { "step_id": "delete_fixture", "service": "biz-x", "operation": "delete-row",
156
+ "depends_on": ["create_fixture"],
157
+ "input": { "id": "{{steps.create_fixture.output.id}}" } }
158
+ ]
159
+ }
160
+ ```
161
+
162
+ **The syntax is not this package's.** Which forms exist, that a whole-value
163
+ expression keeps the resolved type while an expression inside text is
164
+ stringified, that a step is addressed by `step_id` and never by position, and
165
+ that an unresolvable reference survives as literal text — all of it is owned by
166
+ [`api/docs/biz/40-cookbooks/variable-references.md`](../../../docs/biz/40-cookbooks/variable-references.md),
167
+ and the traversal is `@onlineapps/cookbook-core`
168
+ (`resolveReferencesWith` + `resolveReferencePath`) — the same two functions
169
+ `WorkflowOrchestrator` calls. There is no second parser here, which is what keeps
170
+ a recipe behaving the same at boot and in a production workflow.
171
+
172
+ What Tier-1 adds is the SCOPE, and only the scope
173
+ (`src/utils/stepReferences.js`):
174
+
175
+ - `input` is the only field expanded. `expect`, `service`, `operation` and
176
+ `depends_on` are read literally — the format names three expansion sites and
177
+ none of them is `expect`
178
+ ([variable-references.md](../../../docs/biz/40-cookbooks/variable-references.md)
179
+ § Limitations).
180
+ - A step entry is spelled the way
181
+ [`format.md`](../../../docs/biz/40-cookbooks/format.md) § Runtime context
182
+ (`context.steps`)
183
+ spells it: the step definition plus `output` and `_execution`, addressed by
184
+ `step_id`.
185
+ - `output` appears on a step that PASSED. A failed step is recorded with its
186
+ status alone, so a reference into it stays literal and the failing input shows
187
+ the reader which step did not deliver.
188
+ - The scope is per COOKBOOK. A run over a directory gives every cookbook its own,
189
+ so two recipes may use the same step names.
190
+ - A Tier-1 run has no workflow input and no delivery, so `{{api_input.…}}`,
191
+ `{{delivery.…}}` and `{{current.…}}` resolve to nothing and stay literal.
192
+
193
+ ### What the runner REFUSES, because production does too
194
+
195
+ A literal `{{…}}` is the right outcome for a value that is missing at that
196
+ moment — a step that has not run yet, or one that failed. It is the wrong
197
+ outcome for an expression no run could ever resolve, because then a green Tier-1
198
+ certifies a recipe production does not accept. Two such shapes are refused
199
+ BEFORE any step of the cookbook runs, each with its own `Fix:`:
200
+
201
+ | Written in a step's `input` | Tier-1 | Production |
202
+ |---|---|---|
203
+ | `{{webalizeString(…)}}`, `{{string2file(…)}}` — any `name(…)` | refused: `Step <id> input calls the template helper "<name>"` | the orchestrator evaluates it through its helper registry, and throws on a name that registry does not carry |
204
+ | `{{steps.<id>.…}}` naming no step THIS cookbook defines (`{{steps.0.…}}` included — `0` is a name no recipe declares) | refused: `Step <id> references an undefined step` | 400 `Invalid cookbook references` at submission |
205
+ | `depends_on` naming no step this cookbook defines | refused: `Step <id> depends on an undefined step` | — |
206
+
207
+ The helper registry is absent from the runner on purpose: a helper reaches its
208
+ own runtime (`@onlineapps/content-resolver`, files, storage), which is not what a
209
+ startup probe may do. A recipe that needs a computed value builds it in an
210
+ earlier step and references that step's output.
211
+
212
+ The reference half is the receiver's side of the owner's 2026-09-02 decision
213
+ ([`cookbook-validation-placement.md`](../../../docs/governance/confirmations/cookbook-validation-placement.md)
214
+ 001): the gateway answers 400 at submission, and the paths that bypass the
215
+ gateway — a boot run is one — refuse it themselves. What stays literal is
216
+ unchanged: `{{steps[0].…}}` (the bracket form is not a step reference),
217
+ `{{api_input.…}}`, `{{context.…}}`, `{{current.…}}`, and a reference into a
218
+ step that is defined but has not produced anything yet.
219
+
220
+ Why the runner gained this: biz-converter's boot recipe needed a row that only a
221
+ TEST_ONLY seed supplies, so on production Tier-1 answered `NOT_CONVERTIBLE` and
222
+ the container restarted in a loop. The owner's decision was that the recipe stays
223
+ as written and the runner learns what the orchestrator already does —
224
+ `api/docs/governance/confirmations/converter-boot-cookbook.md` 001.
225
+
226
+ ---
227
+
228
+ ## The CI database account is the service's own (`setup-db-account`)
229
+
230
+ `db-migrations-first-deploy` 001 says it in one line: **migrace nikdy pod
231
+ rootem.** Production applies a service's migration set as `oagen_<service>`, an
232
+ account granted its own schemas and nothing else. Until d.567 seven biz
233
+ pipelines applied the SAME set under `DB_USER: "root"`, so CI proved the
234
+ migrations under rights no production box grants.
235
+
236
+ `setup-db-account` closes that gap. It runs **first** in `before_script` and is
237
+ the **only** step of the pipeline that uses the database root account:
238
+
239
+ ```yaml
240
+ before_script:
241
+ - npx oa-biz-ci-gate setup-db-account # the one root step
242
+ - npm run ci:gate:setup # …and everything after it is the service
243
+ ```
244
+
245
+ Three facts, three owners, none of them invented by the command:
246
+
247
+ | Fact | Owner |
248
+ |---|---|
249
+ | the schema | `database.schema` of `config/service/integration-contract.json` — what `setup-db` builds |
250
+ | the account | `DB_USER` of `config/env-templates/<service>.env` — the declaration uniform row `D-DB-ACCOUNT` measures |
251
+ | the statements | `src/utils/dbAccountGrants.js` — the same definition the production runbook installs from |
252
+
253
+ The job supplies the credentials, and the two identities are named **separately**
254
+ (`architecture-principles.md` §8 — the whole point is that they stop being one
255
+ value):
256
+
257
+ | Variable | What it is |
258
+ |---|---|
259
+ | `CI_DB_ROOT_USER` / `CI_DB_ROOT_PASSWORD` | the administrative account of the database sidecar, used by this step alone |
260
+ | `DB_USER` | the account every later step connects as; it must equal what the env template declares, and the command refuses the job otherwise |
261
+ | `DB_PASSWORD` | the throwaway password the account is **created with** and connects with — an env template declares the key, never the secret |
262
+
263
+ A service whose contract declares no `database` block reports `NOT APPLICABLE`
264
+ and exits 0, the way `setup-db` does. Neither password is ever printed, on any
265
+ path.
266
+
267
+ **CI gets one grant production does not**: `` `<schema>\_%` ``, the throwaway
268
+ namespace an integration tier builds into (`src/utils/throwawaySchema.js`). The
269
+ `_` is escaped, so the grant stays inside one service — unescaped it is a LIKE
270
+ wildcard, and `oagen_meta_%` would also match `oagen_metadata`.
271
+
71
272
  ---
72
273
 
73
274
  ## Environment contract (`env` block)
@@ -320,6 +521,18 @@ a stateless service is silent — `pdfgen` is the measured case.
320
521
  (confirmation `db-accounts-per-service` 001: root stops being an operational identity —
321
522
  it was the identity of nine containers across eight repositories). A repository that
322
523
  declares a database and no `DB_USER` is a finding and not a silence.
524
+ * **`D-DB-CI-ACCOUNT`** — the row above measures the DECLARATION; this one measures the
525
+ one environment where that migration set is applied every day. Measured 2026-09-16:
526
+ seven of eight biz pipelines set `DB_USER: "root"` in their `test` job while declaring
527
+ `oagen_<service>` in the template, so CI proved the set under rights no production box
528
+ grants — the gap `db-migrations-first-deploy` 001 names ("migrace nikdy pod rootem") in
529
+ the only place a migration can be tried before a deploy. It reports that value, and a
530
+ `ci:gate:setup` with no `setup-db-account` step before it. A SEPARATE row rather than a
531
+ wider `D-DB-ACCOUNT`: another file, another fix, and one row with two fixes is two
532
+ mechanisms under one name (`automation-gates.md` §1.2). It reads the lines OUTSIDE the
533
+ `oa-ci v1` block, which is `G-CI`'s, and it asks WHO the database steps run as —
534
+ deliberately not WHETHER a repository runs them, since a job that builds no schema has
535
+ no account for the step to create.
323
536
 
324
537
  One more row about the database sits outside this section on purpose. **`X-DB-CONFIG`**
325
538
  is in `files.forbidden`, beside `X-HOOKS` and `X-PREVAL`, and it forbids one path:
@@ -373,6 +586,17 @@ the test precisely so that the next file cannot be exempted by editing a test
373
586
  behind two files that had no row at all: `gitignore` (now `F-GITIGNORE`) and
374
587
  `.gitlab-ci.yml` (now `G-CI`).
375
588
 
589
+ The same suite reads the other two directions, which were silent until d.511 and silent in
590
+ a worse way, because nothing reads a `from:` reference until a run needs it. Every
591
+ `from: { package }` reference the manifest makes must name a file this package carries:
592
+ `describeFromProblem` (`manifestShape.js`) checks a reference's SHAPE and never whether the
593
+ file is there, so a template file renamed or deleted left every suite green here and threw
594
+ `[ManifestDiscovery] Packaged file not found` later — inside whichever repository ran the
595
+ uniform next, a service's CI or a container at boot. And every `files.own.allowed` entry
596
+ must have something behind it in the template, so the class stays a decision about real
597
+ files rather than a list nobody re-derives. Both read the references through `collectRows`,
598
+ the manifest's own walk, so a row that moves into another block cannot fall out of them.
599
+
376
600
  #### `.gitignore`: the `contains` class
377
601
 
378
602
  `F-GITIGNORE` compares the ignore ENTRIES the packaged template declares against the
@@ -512,9 +736,9 @@ severity means it never stops one.
512
736
  ### Step 7 and the deployability signal
513
737
 
514
738
  The same check is step **7/7** of the boot validation (`ValidationOrchestrator`), so a
515
- service measures itself at every start against the manifest its pin selected. The two
516
- blocking severities have different consequences, and that difference is the point
517
- (confirmation `biz-service-manifest` 001 §4):
739
+ service started from a git checkout measures itself at every start against the manifest
740
+ its pin selected. The two blocking severities have different consequences, and that
741
+ difference is the point (confirmation `biz-service-manifest` 001 §4):
518
742
 
519
743
  | Severity | Boot | `results` | What the operator sees |
520
744
  |---|---|---|---|
@@ -550,8 +774,34 @@ belongs to the uniform (`verdict.incomplete` in the manifest, with `{count}` and
550
774
  in it), never to the renderer: a library says `publish row(s)` there, and
551
775
  `PUBLISHABLE — no findings` where nothing was skipped.
552
776
 
553
- The same verdict is written, in the same step, to **`ci/deployability.json`** of the
554
- service root, which `deploy-production` reads and refuses on a single row. The `oa-validate`
777
+ #### Outside a git checkout the step does not measure (d.592)
778
+
779
+ A production image is not a checkout and carries neither `docker-compose.yml` nor
780
+ `README.md`, so the uniform measured THERE reports absences that say nothing about the
781
+ repository — 12 findings for a healthy service. Owner decision 2026-09-17 (confirmation
782
+ `biz-service-manifest` 011): the step then reports NOT RUN, in one line, and measures
783
+ nothing:
784
+
785
+ ```
786
+ [ValidationOrchestrator] NOT RUN manifest conformance — this tree is not a git checkout
787
+ (git ls-files could not answer), so what a clone would receive cannot be read — a container
788
+ and an exported tarball are in exactly this state. Deployability is proven per commit by
789
+ the CI job `validate-uniform` over the checkout (confirmation biz-service-manifest 010).
790
+ ```
791
+
792
+ `results.deployable` is then `null`, which is NOT MEASURED — a different statement from
793
+ `false`, which is why the field has three states. No banner, no table, and **no
794
+ `ci/deployability.json`**: the file IS a verdict, and this run reached none. The probe is
795
+ the one `X-IGNORED` already used (`src/manifest/gitCheckout.js`), so the row and the step
796
+ cannot answer the same question differently.
797
+
798
+ What proves deployability is the CI job `validate-uniform`, from every commit, over the
799
+ checkout (confirmation `biz-service-manifest` 010) — the only place that can see what a
800
+ clone receives.
801
+
802
+ The verdict of a run that DID measure is written, in the same step, to
803
+ **`ci/deployability.json`** of the service root, which `deploy-production` reads and
804
+ refuses on a single row. The `oa-validate`
555
805
  CLI writes the same file, through the same builder and writer
556
806
  (`src/manifest/deployabilitySignal.js`), for the service run it just printed — one file
557
807
  shape with two producers, never two shapes:
@@ -570,10 +820,10 @@ shape with two producers, never two shapes:
570
820
  ```
571
821
 
572
822
  **Two claims, not one.** `deployable` says nothing blocked among the rows that RAN;
573
- `complete` says no row that CAN block this bearer was skipped. Inside a service container
574
- the rows reading the workspace cannot run at all, so a signal is routinely deployable and
575
- incomplete at once — and a gate reading only the first was honouring a `--skip` nobody had
576
- flagged (`automation-gates.md` §1.5). **The deploy gate takes a signal only when both are
823
+ `complete` says no row that CAN block this bearer was skipped. A run from a checkout that
824
+ cannot reach the workspace root (the `--workspace` that names it) answers the workspace
825
+ rows NOT RUN, so its signal is deployable and incomplete at once — and a gate reading only
826
+ the first was honouring a `--skip` nobody had flagged (`automation-gates.md` §1.5). **The deploy gate takes a signal only when both are
577
827
  true**; the complete run is produced from the api checkout with
578
828
  `npx oa-validate --workspace <root> <service root>`, and since d.232 that run writes the
579
829
  file it is asked for. A service root OUTSIDE the workspace root cannot produce a complete
@@ -590,7 +840,8 @@ rows that could not look are not passes, and each entry carries the `severity` o
590
840
  that did not run — which is what decides `complete`.
591
841
 
592
842
  There is no switch that skips the step and none that moves the file — one path,
593
- unbypassable.
843
+ unbypassable. What decides whether the step measures is not a flag anybody sets but
844
+ whether git can answer in the tree it was handed (above).
594
845
 
595
846
  ### Two run modes, three scopes
596
847
 
@@ -628,19 +879,71 @@ the directories it walks (`requiresSiblings({ row, block })`, derived from the r
628
879
  patterns), and the runner verifies them before running it:
629
880
 
630
881
  ```
631
- NOT RUN L-CONSUMER — sibling root api_biz is not present in this checkout
882
+ NOT RUN L-CONSUMER — sibling root api_biz is not present in this checkout - an absent
883
+ directory is not an empty one, so this row is not answered. Fix: run with --workspace
884
+ pointing at a checkout that carries api_biz.
632
885
  ```
633
886
 
634
887
  Measured against a `git archive` export of this repository on 2026-09-09: without the gate,
635
888
  `--library --all` raised a blocking `L-CONSUMER` on `conn-base-db` about files nobody
636
889
  opened. Never a finding, never a pass (`automation-gates.md` §5).
637
890
 
891
+ The line carries its fix because of where it is read. Measured 2026-09-15 over a
892
+ `git archive` export of `api_biz/hello-service` placed beside an api clone with no
893
+ `api_biz/` around them: the reason ended at "is not present in this checkout", and the
894
+ report's own incomplete-run sentence — the one that does name the command — prints only on
895
+ a CLEAR verdict (`report.js` § `describeClearOutcome`), so a run that also had findings
896
+ named no fix anywhere. `automation-gates.md` §1 requirement 4 asks for the exact command,
897
+ and the sync half of this package already gave it (`src/sync/uniformFiles.js` § `planRow`).
898
+ The command is derived from the missing roots, so a row that starts reading a new sibling
899
+ needs no second edit.
900
+
901
+ In the layout the deploy gate demands — the service at `<root>/api_biz/<service>` beside
902
+ `<root>/api`, which `templates/business-service/.gitlab-ci.yml` asks GitLab for through
903
+ `GIT_CLONE_PATH` — nothing is skipped: measured the same day over that export, `notRun` is
904
+ empty and both `U-ORPHAN` and `R-NODE` decide. `R-NODE` left the NOT RUN list in d.238 and
905
+ decides even where `api_biz` is absent, because the platform major it reads is this
906
+ package's own `engines.node`.
907
+
638
908
  One case is neither mode: a service root that does not lie under the given workspace root.
639
909
  Filtering there would drop every finding and the empty table would read as "the row
640
910
  looked and found nothing", so each row that needs the workspace — `workspace` and `bearer`
641
911
  alike — is reported `NOT RUN — service root is outside the workspace root <root>` instead,
642
912
  in the banner and in the signal alike.
643
913
 
914
+ ### Which names the image reads (`oa-validate --env-reads`)
915
+
916
+ A third mode, and not a verdict at all: it answers ONE question — which environment
917
+ names this service reads — for a deploy gate that must not judge a service's `.env`
918
+ against the platform's whole key set.
919
+
920
+ ```bash
921
+ mapfile -t reads < <(npx oa-validate --env-reads "$repo") || exit 1
922
+ for key in "${reads[@]}"; do
923
+ grep -q "^${key}=" "$env_file" || fail "$key is declared and missing"
924
+ done
925
+ ```
926
+
927
+ Names on stdout, one per line, sorted, without duplicates, and nothing else: no
928
+ banner, no table, no `ci/deployability.json`. Exit 0 with the list, exit 2 with an
929
+ `[oa-validate] … Fix: …` line on stderr where there is no readable
930
+ `config/service/integration-contract.json` — a gate that cannot get the set must
931
+ stop, never continue with an empty one. The mode combines with no other option
932
+ (`--json`, `--library`, `--all`, `--workspace` are refused), because every other
933
+ mode prints a verdict into the same stdout.
934
+
935
+ The set is the one row `C-ENV-READS` measures the repository against — the contract's
936
+ `env` block, the `${VAR}` placeholders of `config/service/*.json`, and the endpoint
937
+ variables of the connectors the contract declares required — so the gate and the
938
+ uniform cannot disagree about what a service reads. It is deliberately NOT a grep for
939
+ `process.env`: most of the platform's environment is read inside installed libraries,
940
+ where a grep over `src/` sees nothing, and in a gate a set too small means a key
941
+ silently unchecked.
942
+
943
+ The other direction is deliberate too: a name the SOURCE reads that the contract does
944
+ not declare is absent here. That is a blocking finding of `C-ENV-READS`, with its own
945
+ fix — and a gate must not demand a value for a key nobody owns.
946
+
644
947
  ### Uniforma knihoven (`oa-validate --library`)
645
948
 
646
949
  `manifests/library.manifest.json` is the same concept for the shared libraries, in the
@@ -875,12 +1178,17 @@ class — `src/**`, `tests/**`, the content of `docs/**` — are never written.
875
1178
  file, and since d.229 most of them read a file this package carries. The predicate is the
876
1179
  one the manifest run uses (`rowNeedsWorkspace`), so the `fix` command of `F-INIT`,
877
1180
  `F-JEST` and `F-RUNNER` can be typed where their finding is now raised: inside the service
878
- image. A row that does need one is printed `NOT RUN` by name, never skipped in silence.
879
-
880
- A row the manifest does not make renderable is printed `NOT RUN` with the reason instead
881
- of being filled from a guess. Today that is `G-PROD-IMAGE` and `G-SETUP`: they declare no
882
- `from:` reference, so nothing says what their content is rendered from — the first is a
883
- requirement ABOUT a file, the second a directory that must exist. A row whose file
1181
+ image. A row that does need one is printed `NOT RUN` by name with the `--workspace` fix,
1182
+ never skipped in silence.
1183
+
1184
+ A row the manifest does not make renderable is **not a row of this run**. Today that is
1185
+ `G-PROD-IMAGE` and `G-SETUP`: they declare no `from:` reference, so nothing says what
1186
+ their content would be rendered from — the first is a requirement ABOUT a file, the second
1187
+ a directory that must exist, and `oa-validate` is the run that answers both. Until d.507
1188
+ the sync planned them and printed `NOT RUN`, which every `--check` in every repository
1189
+ ended on: a line naming a state its reader could do nothing about, with no `Fix`
1190
+ (`automation-gates.md` §5 and §1 requirement 4). Naming a requirement path as an argument
1191
+ is refused by what it is, with the run that does answer it. A row whose file
884
1192
  blocks the write — an `init.sh` carrying no block marker, where inserting one would mean
885
1193
  choosing a position among somebody's own install steps; an installation document missing a
886
1194
  section, where writing one would mean rewriting somebody's prose — is printed `BLOCKED`
@@ -1107,6 +1415,16 @@ services/my-service/
1107
1415
  ```
1108
1416
 
1109
1417
  **Proof Lifecycle:**
1418
+ - **Every number in it is a measurement.** `testsRun`, `testsPassed`,
1419
+ `testsFailed` and `durationMs` are read from the aggregate the runner returns,
1420
+ and a missing one is refused by name rather than filled with `0`: the same
1421
+ number would otherwise stand for both "measured, and it was zero" and "nobody
1422
+ measured this", with no way for a reader to tell them apart. A run that
1423
+ executed nothing — a service whose `tests/cookbooks/` holds no `.json` file —
1424
+ is refused outright: it has no failing step, so it used to publish a signed
1425
+ proof asserting a passing validation of zero tests, which
1426
+ `ValidationProofCodec.decode()` then refused at the registry as `NO_TESTS`,
1427
+ days later and far from the cause.
1110
1428
  - **Written:** on every successful validation, i.e. on every boot. There is no
1111
1429
  proof cache — the one that existed skipped steps 1-3 and 5-6 to save 6 ms and
1112
1430
  bought a window of up to seven days in which validation asserted something no
package/docs/DESIGN.md CHANGED
@@ -25,17 +25,31 @@ surface was removed together with the now-retired `ServiceValidator` /
25
25
  - **Production-ready** — the exact same code runs locally, in CI and during
26
26
  wrapper startup.
27
27
  - **Fail-fast** — missing logger, missing operations.json: immediate throw.
28
- `serviceUrl` is *not* among them: it fed the retired `/health` probe and
29
- nothing reads it (ADR 0005). It is accepted and ignored so existing callers
30
- keep working.
28
+ `serviceUrl` is not among them because it is gone: it fed the retired
29
+ `/health` probe (ADR 0005), and neither `ValidationOrchestrator` nor
30
+ `CookbookTestRunner` reads, stores or documents it any more
31
+ (`tests/unit/ValidationOrchestrator.unit.test.js` § Constructor). Nor is one
32
+ passed: `ServiceWrapper._createValidationOrchestrator()` builds the
33
+ orchestrator from four options and this is not one of them
34
+ (`tests/unit/preValidation.test.js` § the runner is built with the service
35
+ identity and no retired options).
31
36
 
32
37
  ## Components
33
38
 
34
- ### Mock Infrastructure (for unit tests)
39
+ ### Internal test doubles — not published
35
40
 
36
- - `MockMQClient` — in-memory message queue simulation
37
- - `MockRegistry` — service registry simulation
38
- - `MockStorage` — object storage simulation
41
+ The package exports no infrastructure doubles. `mockInfrastructure`, the option
42
+ that built them, is retired (d.576): a cookbook step is dispatched in-process
43
+ against the service's own v3 handler, so there is no transport to stand in for
44
+ and the database a handler reaches is the real one
45
+ (`tests/unit/mockInfrastructureRetired.test.js`).
46
+
47
+ - `MockRegistry` — the registry stand-in of `helpers/createServiceReadinessTests`,
48
+ internal to the package
49
+ - `MockMQClient` — used by `tests/unit/RegistrationFlow.test.js` alone
50
+
51
+ A double with no consumer is not kept: `MockStorage` was deleted with its own
52
+ unit test in d.583 (`tests/unit/mockInfrastructureRetired.test.js`).
39
53
 
40
54
  ### Production Validation
41
55
 
@@ -51,7 +65,15 @@ surface was removed together with the now-retired `ServiceValidator` /
51
65
  - `ValidationProofGenerator` — fingerprint + codec helpers
52
66
  - `CookbookTestRunner` — cookbook execution probe. The single rail: the second
53
67
  one, `WorkflowTestRunner`, was deleted on 2026-09-05 (no consumer anywhere in
54
- the workspace, and it read the banned `step.id`)
68
+ the workspace, and it read the banned `step.id`). A step's `input` may
69
+ reference an earlier step's output, so a recipe can build, use and remove its
70
+ own fixture instead of depending on a seed one environment has and another does
71
+ not (owner: `../../../docs/governance/confirmations/converter-boot-cookbook.md`
72
+ 001). The syntax belongs to the cookbook format
73
+ ([variable-references.md](../../../docs/biz/40-cookbooks/variable-references.md))
74
+ and the traversal to `@onlineapps/cookbook-core`, the same rail
75
+ `WorkflowOrchestrator` runs on; `src/utils/stepReferences.js` supplies only the
76
+ run scope, and README § "A recipe builds its own fixture" says what it holds
55
77
  - `CookbookTestUtils` — static cookbook-structure validators
56
78
 
57
79
  ### Test Suite Helpers
@@ -79,7 +101,8 @@ surface was removed together with the now-retired `ServiceValidator` /
79
101
 
80
102
  1. **Service contract** — structure, config files and the v3 operations rules
81
103
  2. **Environment contract** — every name declared `env.required` is set
82
- 3. **Workflow capability** — can process cookbooks, in-process, with mocks
104
+ 3. **Workflow capability** — can process cookbooks, in-process, including a
105
+ recipe whose later steps read what its earlier steps returned
83
106
  4. **Connector contract** — the two declarations agree and the environment backs them
84
107
 
85
108
  ## What We DO NOT Test