@onlineapps/conn-orch-validator 10.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,179 @@ All notable changes to this package. Follows [Keep a Changelog](https://keepacha
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [11.0.0] — 2026-09-17
8
+
9
+ ### Changed — deploy job aplikuje seedy třídy PRODUCTION_LIKE (d.589, W589)
10
+
11
+ `templates/business-service/.gitlab-ci.yml` (blok `oa-ci v1`): po migracích a před přepnutím se aplikují deklarované
12
+ `database.seeds`, jejichž hlavička nese `Dataset-Class: PRODUCTION_LIKE` (standard `repository-installation-sql-contract.md`
13
+ §4/§5; converter po 10 migracích měl 22 prázdných tabulek). Filtr běží na runneru — box neparsuje JSON ani hlavičky a o třídě
14
+ nerozhoduje; `TEST_ONLY` se nikdy neaplikuje a job to řekne jménem souboru. Seedy se netrackují a jedou při každém nasazení,
15
+ licencí je `-- Idempotency: yes`, její absence i třída mimo slovník odmítnou nasazení před ssh. Devátý poziční argument
16
+ `BIZ_DB_SEEDS`; na boxu `apply_mariadb_seeds_over_tcp <host> <port> <db> <file>...` z běžce neseného z klonu api
17
+ (INFRA `7045a736`), s pojmenovaným odmítnutím mezi předpoklady, když ho klon nenese. Bats `biz-deploy-db-migrations.bats` 24 → 29.
18
+
19
+ ### Fixed — box se resetuje na commit, který runner měřil (d.589b, W589)
20
+
21
+ Desátý poziční argument `$CI_COMMIT_SHA`; `git merge-base --is-ancestor` + `git reset --hard "$COMMIT_SHA"` místo
22
+ `origin/production` — push během běhu jobu dosud rozešel to, co runner četl v kontraktu, s tím, co box nasadil.
23
+
24
+ ### Fixed — existence `../shared.env` a `config/env-active/<svc>.env` se ověřuje pro každou službu (d.589c, W589)
25
+
26
+ Dosud jen ve větvi s migracemi; služba bez databáze (pdfgen, hello) se o chybějícím souboru dozvěděla až od
27
+ `docker compose`. Kontrola je před větvením s `Fix:` na runbook, který ty dva soubory zakládá
28
+ (`docs/operations/production-box-provisioning.md` § 3).
29
+
30
+
31
+ ### Added — řádek uniformy `R-USER` (check `process-identity`) (d.588c, W588)
32
+
33
+ Produkční stage `Dockerfile` deklaruje `USER node` a uzel služby v `docker-compose.yml` pinuje `user: "1000:1000"`. Druhý
34
+ platformní fakt vyjmutý ze třídy `own` (prvním je node major, `R-NODE`); běžec drží `F-RUNNER`, produkční compose `G-PROD`
35
+ (kontrolní případy, že `R-USER` mlčí, kde hlásí ten druhý). Zčervená u všech osmi služeb — vždy jedním nálezem o `Dockerfile`
36
+ (dev compose už `user:` má 8/8). `doc` = `docs/biz/00-model/service-shape.md` (vzor `R-PORTS-DEV`); sekce „Process identity"
37
+ v README šablony, generovaná oblast řádků přegenerována.
38
+
39
+ ### Fixed — krok, který spadl výjimkou, nechá svůj záznam; nález nese krok (d.602, W602)
40
+
41
+ Wrapper 9.0.x rozhoduje o opakování padlé FÁZE 0.2 podle `result.steps.<krok>.valid`; krok, jehož tělo vyhodilo výjimku, ale
42
+ záznam nezanechal, došel k wrapperu jako „validator spadl" = přechodně, a koupil si šest pokusů (18,5 min bootu, který uspět
43
+ nemůže). `runStep()` je síť pod všemi sedmi kroky: zapíše `{ valid: false, success: false, threw: true, errors: […] }` s tím,
44
+ co krok naměřil, a nález `{ type: 'STEP_THREW', step, message }`; objektové nálezy kroku 1 nesou v `result.errors` `step`
45
+ (kopie). Věty kroků 2–7 zůstávají větami — `describeValidationFinding()` vydaného wrapperu vykreslí objekt jen s `type` i
46
+ `message`; sjednocení na objekty čeká na wrapper, který `errors[].step` čte (pak 11.0.0). `attributeToStep()` odmítne nálezy,
47
+ které nejsou pole, hláškou §5. Krok 5 hlásí `success`, ostatní `valid` — dvě pole pro jeden pojem, zapsáno jako nález.
48
+
49
+
50
+ ### Changed — BREAKING: šablona biz služby — produkce běží jako uid 1000, běžec má vlastní `conn-runtime` (d.588, W588)
51
+
52
+ `templates/business-service/Dockerfile`: obě stage deklarují `USER node` (uid 1000/gid 1000 v `node:24-alpine`) a zakládají
53
+ `/app/logs` + `/app/conn-runtime` pod tím účtem (`COPY --chown=node:node`). Produkční obraz dosud běžel jako root (změřeno
54
+ `docker run --rm <obraz> id` → `uid=0`), zatímco dev kontejner jako 1000:1000 — asymetrie, kterou nikdo nedeklaroval.
55
+ `docker-compose.production.yml`: `user: "1000:1000"` shodně s dev → řádek `G-PROD` u každé služby zčervená do
56
+ `oa-sync-template docker-compose.production.yml`. Blok `oa-test-runner v1` v `docker-compose.yml` dostal anonymní svazek
57
+ `/app/conn-runtime`, aby proof běžce nepřepisoval proof služby (změřeno converterem: běžec nad `.:/app` zapsal
58
+ `validation-proof.json` běžící instance) → řádek `F-RUNNER` zčervená do `oa-sync-template docker-compose.yml`. Nové sady
59
+ `templateProcessIdentity`, `templateRunnerRuntime`, `templateImageIdentity.integration` (skutečný `docker build` + `docker run`;
60
+ bez démona/checkoutu/registry NOT RUN s důvodem, nikdy červená za síť).
61
+
62
+ ### Changed — BREAKING: šablona neměří uniformu při buildu obrazu — verdikt patří CI (d.588b, W588)
63
+
64
+ Řádek `RUN … oa-validate.js .` ve stage `production` (d.215b, `4b5ed9e6`) měřil uniformu nad build kontextem bez `.git`,
65
+ compose a README — u každé reálné služby (`.dockerignore` je správně vyřazuje) `NOT DEPLOYABLE — 10 finding(s)`, všechny
66
+ „absent", a build spadl. Konfirmace 006/2 („information, never a gate"), 010 (`validate-uniform` z každého commitu nad
67
+ checkoutem) a 011 (strom bez git checkoutu = NOT RUN) větu 001 §3.1 překonaly; řádek je pryč i s testem `add-service.bats`
68
+ 54, který teď tvrdí opak. `templateImageIdentity` už nepotřebuje srovnat scaffold pinovanou verzí (19 s místo 46 s).
69
+
70
+ ### Fixed — fixtury uniformy dosynchronizovány na SSOT sdílené sady (dovětek d.533, W588)
71
+
72
+ 14 fixtur `shared.env` a 3 `shared-env.json` se od `f61501cc` lišily jedním klíčem (`JWT_SECRET` `why`/`consumers`);
73
+ `manifestServiceConfig` (`G-SHARED-ENV`) byla proto v HEAD červená. Záměrně rozejitá `service-workspace/api_biz/beta`
74
+ zůstala (vstup případu „not the platform manifest rendered").
75
+
76
+ ### Changed — BREAKING: krok 7 mimo git checkout je NOT RUN, ne „NOT DEPLOYABLE" (d.592, W592)
77
+
78
+ Produkční obraz není checkout a nenese compose ani README, takže uniforma měřená tam hlásila 12 nálezů nepřítomnosti a
79
+ služba se registrovala `deployable: false` (BIZ-pdfgen 2026-09-16). Verdikt z toho není přísnost, ale špatná odpověď:
80
+ krok vypíše jednu větu NOT RUN s odkazem na CI job `validate-uniform`, `results.deployable` zůstává `null` (= neměřeno,
81
+ jiné tvrzení než `false`), banner ani `ci/deployability.json` nevznikají (konfirmace `biz-service-manifest` 010, 011).
82
+ Sonda `git ls-files` má jedno místo `src/manifest/gitCheckout.js` (`gitCommand`, `listTrackedFiles`, `isGitCheckout`,
83
+ věta `NOT_A_CHECKOUT`); `X-IGNORED` ji bere odtud. Fixtury kroku 7 jsou nově checkouty přes `makeGitCheckout`.
84
+
85
+ ### Added — Tier-1 běžec předává výstup kroku dalším krokům (d.590, W590)
86
+
87
+ `{{steps.<step_id>.output.…}}` v `input` kroku se rozřeší proti tomu, co dřívější kroky téhož receptu vrátily, takže recept
88
+ si fixturu vytvoří, použije a smaže (vlastník: konfirmace `converter-boot-cookbook` 001 — „opravit v knihovně"). Syntaxe
89
+ i průchod zůstávají cookbook formátu a `@onlineapps/cookbook-core` (`resolveReferencesWith`, `resolveReferencePath` —
90
+ táž kolej jako `WorkflowOrchestrator`); běžec přidává jen scope běhu (`src/utils/stepReferences.js`) tvaru `format.md`
91
+ § Runtime context (`context.steps`): definice kroku + `output` + `_execution`, adresováno `step_id`. Expanduje se jen
92
+ `input`; `output` se zapisuje jen u kroku, který prošel; scope je per cookbook. `CookbookTestRunner.executeStep()` má
93
+ pátý parametr (kontext běhu; samostatný krok dostane prázdný). Po spadlém kroku s `expect` se další kroky nepouští
94
+ (dnešní chování, nově připnuté testem). NOVÁ ZÁVISLOST: `@onlineapps/cookbook-core` (SSOT) — do `dependencies` před vydáním.
95
+
96
+ ### Added — Tier-1 běžec odmítá recept, který produkce nepřijme (d.598, W598)
97
+
98
+ Před během: volání template helperu v `input` (`jméno(...)` — Tier-1 registr helperů nemá a `cookbook-template-helpers`
99
+ táhne `content-resolver`, tedy I/O), `{{steps.<id>}}` na krok, který recept nedefinuje (včetně tečkového `{{steps.0}}`;
100
+ gateway odpovídá 400, konfirmace `cookbook-validation-placement` 001 = throw u příjemce jako pojistka), a `depends_on` na
101
+ nedefinovaný krok. Dosud obojí tiše propadlo jako literál. `utils/stepReferences.checkStepInputExpressions()` používá týž
102
+ `resolveReferencesWith()`. Literálem zůstává, co jím podle normy být má (`{{steps[0]}}`, `{{api_input.…}}`, `{{context.…}}`,
103
+ `{{current.…}}`, odkaz na definovaný, ale neběžící/selhavší krok). Dopad na dnešní recepty služeb: 0 ze 43.
104
+
105
+ ### Fixed — `MISSING_COOKBOOKS` nese `Fix:` právě jednou (d.599, W598)
106
+
107
+ Jediný ze 17 nálezů `ServiceStructureValidator` měl nápravu i uvnitř `message`; wrapper 9.0.1 složením `message (Fix: fix)`
108
+ ji tiskl dvakrát.
109
+
110
+ ### Added — `oa-validate --env-reads [<serviceRoot>]` (d.533c, W533c)
111
+
112
+ Vypíše jména proměnných prostředí, která služba čte: po řádcích, seřazená, bez duplicit, nic jiného (žádný banner, žádný
113
+ verdikt, žádné `ci/deployability.json`), exit 0; bez čitelného `config/service/integration-contract.json` exit 2 a
114
+ `[oa-validate] … Fix: …` na stderr; nekombinuje se s `--json`, `--library`, `--all`, `--workspace`. Odpovídá bráně nasazení
115
+ `api/scripts/validate-env.sh`, která u biz cíle shazovala nasazení na `JWT_SECRET=CHANGE_ME` — klíč, který žádný biz obraz
116
+ nečte (dohoda s INFRA 2026-09-17). `utils/envContract.js` vystavuje `collectDeclaredEnvNames({serviceRoot, contract})` —
117
+ množina, proti které měří řádek `C-ENV-READS` (`env` blok ∪ `${VAR}` z `config/service/*.json` ∪ endpointové proměnné
118
+ vyžadovaných konektorů), vytažená z `verifyEnvCompleteness`, aby druhý konzument nezaložil druhou definici.
119
+
120
+
121
+ ### Documentation — zabalený `shared.env` nese komentář u `JWT_SECRET` podle SSOT (d.533)
122
+
123
+ Šablona `templates/business-service/config/env-templates/shared.env` je render SSOT `api/config/shared-env.json`; `why` u `JWT_SECRET`
124
+ teď jmenuje čtyři infra čtenáře a říká, že business řetězec klíč nese a nečte (`@onlineapps/service-common` bere tajemství jako vstup).
125
+ Vydání 10.0.0 (22:32) přistálo o čtyři minuty dřív než `f61501cc` — zabalená kopie je o tento komentář pozadu, chování beze změny.
126
+
127
+ ### Added — CI pod účtem služby (d.567)
128
+
129
+ Nový příkaz `oa-biz-ci-gate setup-db-account`: založí účet, který služba **deklaruje** (`DB_USER`
130
+ v `config/env-templates/<služba>.env` — týž klíč, který měří řádek `D-DB-ACCOUNT`), a udělí mu granty na
131
+ jeho schéma. Je to **jediný** krok biz pipeline pod rootem databáze a poslední takový: všechno za ním
132
+ se připojuje jako služba, stejně jako produkce. Důvod je konfirmace `db-migrations-first-deploy` 001
133
+ („migrace nikdy pod rootem"): sedm pipeline dnes aplikuje tutéž migrační sadu pod `DB_USER: "root"`,
134
+ takže CI dokazuje průchod migrací právy, která produkce neuděluje.
135
+
136
+ Věty SQL mají **jednu definici** — `src/utils/dbAccountGrants.js`. Produkční runbook a CI jsou dva
137
+ volající, ne dvě kopie (`db-migrations-first-deploy` 001: „žádný nový generátor SQL, žádná druhá kolej").
138
+ Jediný rozdíl CI proti produkci je grant na zahazovací jmenný prostor `` `<schéma>\_%` `` (staví do něj
139
+ integrační tier, `throwawaySchema.js`); `_` je escapované, takže grant zůstává uvnitř jedné služby —
140
+ neescapovaný je `_` zástupný znak LIKE a `oagen_meta_%` by sáhlo i na `oagen_metadata`.
141
+ `GRANT … ON information_schema.*` v sadě **není**: server ho odmítá i rootu (změřeno na dev
142
+ `gen_mariadb10.5`, MariaDB 10.5.17, `ERROR 1044`), a každý účet ten katalog čte filtrovaně sám.
143
+
144
+ Rootové přihlášení má vlastní jména `CI_DB_ROOT_USER` / `CI_DB_ROOT_PASSWORD` a nebere se z `DB_USER`
145
+ (`architecture-principles.md` §8) — právě to, že ty dvě identity byly jedna hodnota, příkaz končí.
146
+ Heslo účtu je zahazovací hodnota jobu (`DB_PASSWORD`), nikdy `CHANGE_ME` ze šablony: šablona deklaruje
147
+ klíč, ne tajemství. Služba bez bloku `database` hlásí `NOT APPLICABLE` a končí nulou, jako `setup-db`.
148
+ Žádné heslo se na výstup nedostane ani na cestě selhání.
149
+
150
+ Že ty granty **stačí** — a pořád nejsou příliš — není vlastnost textu SQL, ale odpovědi serveru, takže je
151
+ to změřeno proti skutečné MariaDB (dev `gen_mariadb10.5`, 10.5.17): se samotným produkčním grantem server
152
+ stavbu zahazovacího schématu odmítne (`ERROR 1044`), po `setup-db-account` projde, a účet přitom pořád
153
+ nedosáhne na sousední službu — ani na její schéma, ani na zahazovací schéma v jejím jmenném prostoru, ani
154
+ na řádky, ani na její jméno v katalogu. Soused se jmenuje `<ns>ax` k `<ns>a` právě proto, že neescapované
155
+ `_` by ho pustilo dovnitř.
156
+
157
+ Kaskáda: sedm repozitářů s databází přepíše v jobu `test` `DB_USER: "root"` na účet služby a zařadí
158
+ `setup-db-account` před `ci:gate:setup`.
159
+
160
+ ### Added — řádek uniformy `D-DB-CI-ACCOUNT` (d.567)
161
+
162
+ `D-DB-ACCOUNT` měří deklaraci (`DB_USER` v šabloně prostředí); nic neměřilo prostředí, ve kterém se ta
163
+ migrační sada každý den skutečně aplikuje. Nový řádek (`severity: deploy`, vlastník BIZ-general) hlásí
164
+ `DB_USER` s hodnotou `root` a `ci:gate:setup`, před kterým neběží `setup-db-account`. Samostatný řádek,
165
+ ne rozšíření `D-DB-ACCOUNT`: jiný soubor, jiná oprava, a jeden řádek se dvěma opravami jsou dva
166
+ mechanismy pod jedním jménem (`automation-gates.md` §1.2). Čte řádky **mimo** blok `oa-ci v1` (ten je
167
+ G-CI), takže nekřísí problém, kvůli kterému byl celosouborový řádek nad `.gitlab-ci.yml` po třech dnech
168
+ stažen.
169
+
170
+ Měří, KDO kroky nad databází pouští, a záměrně ne, ZDA je repozitář pouští: job, který nestaví schéma,
171
+ nemá účet, který by tento krok zakládal. Hranice je napsaná, protože nevyslovená hranice se čte jako
172
+ pokrytí (`automation-gates.md` §5). Obě pravopisné podoby téhož: `DB_USER: "root"` v `variables:` i
173
+ `DB_USER=root` jako předřazené přiřazení na samotném kroku (změřený tvar `api_biz/hello-service`).
174
+ Komentář, který jmenuje `ci:gate:setup`, krokem není (změřený tvar `api_biz/meta`).
175
+
176
+ Změřeno nad všemi osmi repozitáři: sedm hlásí dva nálezy s číslem řádku a proveditelnou opravou,
177
+ `pdfgen` (bez bloku `database`) mlčí z konstrukce. Zrcadla generovaného seznamu řádků (README uniforma
178
+ šablony i všech fixtur, `api/templates/business-service/README.md`) přegenerována v témže commitu.
179
+
7
180
  ## [10.0.0] — 2026-09-16
8
181
 
9
182
  ### Removed — `MockStorage` (d.583)
package/README.md CHANGED
@@ -68,6 +68,44 @@ 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
+
71
109
  ---
72
110
 
73
111
  ## Pre-validation runs in-process — and against the real database
@@ -99,6 +137,140 @@ failure.
99
137
 
100
138
  ---
101
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
+
272
+ ---
273
+
102
274
  ## Environment contract (`env` block)
103
275
 
104
276
  One declaration in `config/service/integration-contract.json`, two consumers:
@@ -349,6 +521,18 @@ a stateless service is silent — `pdfgen` is the measured case.
349
521
  (confirmation `db-accounts-per-service` 001: root stops being an operational identity —
350
522
  it was the identity of nine containers across eight repositories). A repository that
351
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.
352
536
 
353
537
  One more row about the database sits outside this section on purpose. **`X-DB-CONFIG`**
354
538
  is in `files.forbidden`, beside `X-HOOKS` and `X-PREVAL`, and it forbids one path:
@@ -552,9 +736,9 @@ severity means it never stops one.
552
736
  ### Step 7 and the deployability signal
553
737
 
554
738
  The same check is step **7/7** of the boot validation (`ValidationOrchestrator`), so a
555
- service measures itself at every start against the manifest its pin selected. The two
556
- blocking severities have different consequences, and that difference is the point
557
- (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):
558
742
 
559
743
  | Severity | Boot | `results` | What the operator sees |
560
744
  |---|---|---|---|
@@ -590,8 +774,34 @@ belongs to the uniform (`verdict.incomplete` in the manifest, with `{count}` and
590
774
  in it), never to the renderer: a library says `publish row(s)` there, and
591
775
  `PUBLISHABLE — no findings` where nothing was skipped.
592
776
 
593
- The same verdict is written, in the same step, to **`ci/deployability.json`** of the
594
- 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`
595
805
  CLI writes the same file, through the same builder and writer
596
806
  (`src/manifest/deployabilitySignal.js`), for the service run it just printed — one file
597
807
  shape with two producers, never two shapes:
@@ -610,10 +820,10 @@ shape with two producers, never two shapes:
610
820
  ```
611
821
 
612
822
  **Two claims, not one.** `deployable` says nothing blocked among the rows that RAN;
613
- `complete` says no row that CAN block this bearer was skipped. Inside a service container
614
- the rows reading the workspace cannot run at all, so a signal is routinely deployable and
615
- incomplete at once — and a gate reading only the first was honouring a `--skip` nobody had
616
- 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
617
827
  true**; the complete run is produced from the api checkout with
618
828
  `npx oa-validate --workspace <root> <service root>`, and since d.232 that run writes the
619
829
  file it is asked for. A service root OUTSIDE the workspace root cannot produce a complete
@@ -630,7 +840,8 @@ rows that could not look are not passes, and each entry carries the `severity` o
630
840
  that did not run — which is what decides `complete`.
631
841
 
632
842
  There is no switch that skips the step and none that moves the file — one path,
633
- 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).
634
845
 
635
846
  ### Two run modes, three scopes
636
847
 
@@ -700,6 +911,39 @@ looked and found nothing", so each row that needs the workspace — `workspace`
700
911
  alike — is reported `NOT RUN — service root is outside the workspace root <root>` instead,
701
912
  in the banner and in the signal alike.
702
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
+
703
947
  ### Uniforma knihoven (`oa-validate --library`)
704
948
 
705
949
  `manifests/library.manifest.json` is the same concept for the shared libraries, in the
package/docs/DESIGN.md CHANGED
@@ -65,7 +65,15 @@ unit test in d.583 (`tests/unit/mockInfrastructureRetired.test.js`).
65
65
  - `ValidationProofGenerator` — fingerprint + codec helpers
66
66
  - `CookbookTestRunner` — cookbook execution probe. The single rail: the second
67
67
  one, `WorkflowTestRunner`, was deleted on 2026-09-05 (no consumer anywhere in
68
- 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
69
77
  - `CookbookTestUtils` — static cookbook-structure validators
70
78
 
71
79
  ### Test Suite Helpers
@@ -93,7 +101,8 @@ unit test in d.583 (`tests/unit/mockInfrastructureRetired.test.js`).
93
101
 
94
102
  1. **Service contract** — structure, config files and the v3 operations rules
95
103
  2. **Environment contract** — every name declared `env.required` is set
96
- 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
97
106
  4. **Connector contract** — the two declarations agree and the environment backs them
98
107
 
99
108
  ## What We DO NOT Test
@@ -108,7 +108,7 @@
108
108
  ],
109
109
  "own": {
110
110
  "concern": "the files of the template whose CONTENT the service decides, and the reason each is not held to the platform's copy",
111
- "why": "a file added to the template is a decision about who guards it, and this class is the decision written down: no row holds these to the template, because their content IS the service. Dockerfile is the exception that proves the rule - its one platform fact, the node major, is R-NODE's row, and everything else in it is this service's build. docs/README.md is the same decision for the map of the documentation tree: which branches a service has is the service's, and what the platform holds the tree to is not that prose but D-LINT, which asks the documentation lint whether the tree is clean. It is the map alone, not docs/ - the three installation documents under docs/80-setup/ ARE held to a skeleton, and a class covering them would exempt what a row requires. The class exists so that the completeness check of finding 18 has an answer other than silence for every file the template carries (automation-gates.md \u00a75)",
111
+ "why": "a file added to the template is a decision about who guards it, and this class is the decision written down: no row holds these to the template, because their content IS the service. Dockerfile is the exception that proves the rule - its TWO platform facts are rows of their own, and everything else in it is this service's build: the node major is R-NODE's row, and WHICH USER the process runs as is R-USER's (added d.588c, after a production image built from the template was measured running as uid 0 while the dev container beside it ran as 1000). What a service BUILDS is its own; what it RUNS AS is the platform's. docs/README.md is the same decision for the map of the documentation tree: which branches a service has is the service's, and what the platform holds the tree to is not that prose but D-LINT, which asks the documentation lint whether the tree is clean. It is the map alone, not docs/ - the three installation documents under docs/80-setup/ ARE held to a skeleton, and a class covering them would exempt what a row requires. The class exists so that the completeness check of finding 18 has an answer other than silence for every file the template carries (automation-gates.md \u00a75)",
112
112
  "allowed": [
113
113
  "Dockerfile",
114
114
  "index.js",
@@ -497,6 +497,20 @@
497
497
  "fix": "in docker-compose.yml set the service block's command to [\"node\", \"index.js\"] (the entrypoint init.sh execs it, so node becomes PID 1), then recreate the container and smoke it",
498
498
  "doc": "api/docs/governance/confirmations/biz-compose-pid1.md"
499
499
  },
500
+ {
501
+ "id": "R-USER",
502
+ "check": "process-identity",
503
+ "image_path": "Dockerfile",
504
+ "image_stage": "production",
505
+ "path": "docker-compose.yml",
506
+ "service_user": "node",
507
+ "service_uid": "1000:1000",
508
+ "severity": "deploy",
509
+ "owner": "BIZ-general",
510
+ "why": "WHICH USER the process is, which is a platform fact and not a service build. Measured 2026-09-17 on a production image built from the platform template: `docker run --rm <image> id` answered uid=0(root) gid=0(root), while the dev container beside it ran as 1000:1000 because docker-compose.yml pins it - nobody had decided that difference, the production stage simply declared no USER, and nothing in the uniform could say so because Dockerfile is a file of the own class. This row is the SECOND platform fact carved out of that class, for the same reason as the first (R-NODE, the node major): what a service BUILDS is its own, what it RUNS AS is the platform. It measures the two places nothing else holds - the production stage of the Dockerfile, which is the stage CI builds and the one the defect lived in, and the SERVICE node of the dev compose. The runner user: sits inside the block F-RUNNER holds byte for byte, and the production compose is the whole-file render of G-PROD, so neither is repeated here (change-discipline.md, One rail per concern). The two spellings are ONE identity, and the row carries both because no machine resolves an account name to a uid without the image: node is uid 1000, gid 1000 in the node images this platform builds on, measured docker run --rm node:24-alpine id node",
511
+ "fix": "in the production stage of Dockerfile put RUN mkdir -p /app/logs /app/conn-runtime && chown -R node:node /app, then USER node before npm ci, and --chown=node:node on both COPY lines; in docker-compose.yml give the service node the line user: \"1000:1000\"",
512
+ "doc": "api/docs/biz/00-model/service-shape.md"
513
+ },
500
514
  {
501
515
  "id": "R-PORTS-DEV",
502
516
  "check": "compose-no-ports",
@@ -587,6 +601,19 @@
587
601
  "fix": "set DB_USER in this service's env template to oagen_<shortname>, where <shortname> is what config/service/config.json declares; the account and its grants are created by the operator installing the schema",
588
602
  "doc": "api/docs/governance/confirmations/db-accounts-per-service.md"
589
603
  },
604
+ {
605
+ "id": "D-DB-CI-ACCOUNT",
606
+ "check": "db-ci-account",
607
+ "path": ".gitlab-ci.yml",
608
+ "block": "oa-ci v1",
609
+ "key": "DB_USER",
610
+ "account": "root",
611
+ "severity": "deploy",
612
+ "owner": "BIZ-general",
613
+ "why": "D-DB-ACCOUNT one row up measures the DECLARATION - the account production installs. Nothing measured the ONE environment where that migration set is applied every day. Measured 2026-09-16 over the eight biz repositories: seven set DB_USER: \"root\" in their test job while declaring oagen_<service> in the env template, so CI proved the set under rights no production box grants - the gap db-migrations-first-deploy 001 names (\"migrace nikdy pod rootem\") in the only place a migration can be tried before a deploy. A separate row and not a wider D-DB-ACCOUNT: another file, another fix, and one row with two fixes is two mechanisms under one name (automation-gates.md §1.2). It reads the lines OUTSIDE the oa-ci v1 block, because that block is the platform's and G-CI compares it byte for byte; reading a region and not the whole file is also what keeps it clear of the defect that had the whole-file row over .gitlab-ci.yml withdrawn after three days. It asks WHO the database steps run as and deliberately not WHETHER a repository runs them: a job that builds no schema has no account for the step to create, and demanding one would be this row inventing a rule (truth-over-agreement.md §6). Silent for a service with no database block by construction - pdfgen is the live case",
614
+ "fix": "in the test job of .gitlab-ci.yml set DB_USER to the account config/env-templates/<service>.env declares, give DB_PASSWORD a throwaway value of the job, name the sidecar's root credential CI_DB_ROOT_USER / CI_DB_ROOT_PASSWORD, and run `npx oa-biz-ci-gate setup-db-account` as the first before_script step, before ci:gate:setup",
615
+ "doc": "api/docs/governance/confirmations/db-accounts-per-service.md"
616
+ },
590
617
  {
591
618
  "id": "D-DB-HEADERS",
592
619
  "check": "install-contract",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/conn-orch-validator",
3
- "version": "10.0.0",
3
+ "version": "11.0.0",
4
4
  "description": "Validation orchestrator for OA Drive microservices - coordinates validation across all layers (base, infra, orch, business)",
5
5
  "oa": {
6
6
  "category": "orchestration"
@@ -28,6 +28,7 @@
28
28
  "author": "OnlineApps",
29
29
  "license": "PROPRIETARY",
30
30
  "dependencies": {
31
+ "@onlineapps/cookbook-core": "6.0.0",
31
32
  "@onlineapps/logger-contract": "2.0.0",
32
33
  "@onlineapps/service-validator-core": "2.1.0"
33
34
  },