@onlineapps/conn-orch-validator 7.0.0 → 8.1.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 +2582 -2
- package/README.md +1038 -4
- package/docs/DESIGN.md +3 -1
- package/manifests/biz-service.manifest.json +658 -0
- package/manifests/library.manifest.json +324 -0
- package/package.json +12 -6
- package/src/CookbookTestRunner.js +408 -101
- package/src/CookbookTestUtils.js +7 -8
- package/src/ServiceReadinessValidator.js +10 -35
- package/src/ValidationOrchestrator.js +219 -71
- package/src/cli/biz-ci-gate.js +176 -33
- package/src/cli/oa-lint-scripts.js +221 -0
- package/src/cli/oa-sync-template.js +1020 -0
- package/src/cli/oa-validate.js +474 -0
- package/src/helpers/README.md +2 -1
- package/src/helpers/createServiceReadinessTests.js +60 -4
- package/src/index.js +33 -3
- package/src/lint/scripts/lintScripts.js +298 -0
- package/src/manifest/checks/composeRunnerBlock.js +222 -0
- package/src/manifest/checks/composeShape.js +165 -0
- package/src/manifest/checks/contractBridge.js +181 -0
- package/src/manifest/checks/discoveryOrphan.js +50 -0
- package/src/manifest/checks/docsLintBridge.js +553 -0
- package/src/manifest/checks/fileAbsent.js +35 -0
- package/src/manifest/checks/gitTracked.js +204 -0
- package/src/manifest/checks/index.js +111 -0
- package/src/manifest/checks/libraryContext.js +226 -0
- package/src/manifest/checks/libraryDocs.js +75 -0
- package/src/manifest/checks/libraryPackage.js +272 -0
- package/src/manifest/checks/librarySource.js +274 -0
- package/src/manifest/checks/libraryTests.js +121 -0
- package/src/manifest/checks/libraryWorkspace.js +293 -0
- package/src/manifest/checks/readmeRegion.js +135 -0
- package/src/manifest/checks/scriptHeaders.js +79 -0
- package/src/manifest/checks/serviceConfig.js +390 -0
- package/src/manifest/checks/serviceConnectors.js +81 -0
- package/src/manifest/checks/serviceDb.js +388 -0
- package/src/manifest/checks/serviceFiles.js +754 -0
- package/src/manifest/checks/serviceIdentityRows.js +351 -0
- package/src/manifest/checks/serviceRuntime.js +295 -0
- package/src/manifest/checks/serviceScripts.js +213 -0
- package/src/manifest/deployabilitySignal.js +121 -0
- package/src/manifest/discovery.js +386 -0
- package/src/manifest/loadManifest.js +62 -0
- package/src/manifest/manifestShape.js +446 -0
- package/src/manifest/report.js +245 -0
- package/src/manifest/runManifest.js +449 -0
- package/src/manifest/serviceIdentity.js +140 -0
- package/src/manifest/walk.js +74 -0
- package/src/manifest/workspaceRoot.js +242 -0
- package/src/mocks/MockMQClient.js +13 -30
- package/src/mocks/MockRegistry.js +4 -2
- package/src/mocks/MockStorage.js +4 -2
- package/src/sync/docsRegion.js +463 -0
- package/src/sync/generatedRegion.js +228 -0
- package/src/sync/readmeLocation.js +182 -0
- package/src/sync/readmePointer.js +477 -0
- package/src/sync/serviceTemplate.js +583 -0
- package/src/sync/sharedEnv.js +162 -0
- package/src/sync/uniformFiles.js +474 -0
- package/src/utils/bizCiGateContract.js +131 -7
- package/src/utils/connectorContract.js +97 -7
- package/src/utils/cookbookFormat.js +81 -40
- package/src/utils/deployContract.js +140 -9
- package/src/utils/envContract.js +57 -1
- package/src/utils/handlerRef.js +181 -0
- package/src/utils/installContract.js +287 -41
- package/src/utils/libCompat.js +29 -7
- package/src/utils/migrationOrder.js +163 -0
- package/src/utils/preValidation.js +20 -7
- package/src/utils/setupDatabase.js +194 -13
- package/src/utils/testCoverageContract.js +539 -0
- package/src/utils/testNamespace.js +247 -23
- package/src/utils/throwawaySchema.js +207 -0
- package/src/validators/ServiceStructureValidator.js +2 -1
- package/templates/business-service/.dockerignore +42 -0
- package/templates/business-service/.gitlab-ci.yml +409 -0
- package/templates/business-service/Dockerfile +27 -0
- package/templates/business-service/README.md +213 -0
- package/templates/business-service/config/biz-docs-lint.tree.json +10 -0
- package/templates/business-service/config/env-templates/__SERVICE_NAME__.env +22 -0
- package/templates/business-service/config/env-templates/shared.env +65 -0
- package/templates/business-service/config/service/config.json +14 -0
- package/templates/business-service/config/service/integration-contract.json +12 -0
- package/templates/business-service/config/service/operations.json +41 -0
- package/templates/business-service/docker-compose.production.yml +60 -0
- package/templates/business-service/docker-compose.yml +93 -0
- package/templates/business-service/docs/80-setup/INSTALL.md +123 -0
- package/templates/business-service/docs/80-setup/PLATFORM_MATRIX.md +65 -0
- package/templates/business-service/docs/80-setup/README.md +18 -0
- package/templates/business-service/docs/80-setup/VALIDATION.md +78 -0
- package/templates/business-service/docs/README.md +18 -0
- package/templates/business-service/gitignore +42 -0
- package/templates/business-service/index.js +10 -0
- package/templates/business-service/init.sh +54 -0
- package/templates/business-service/jest.config.js +6 -0
- package/templates/business-service/package.json.template +31 -0
- package/templates/business-service/scripts/verify-deploy-uniform.sh +180 -0
- package/templates/business-service/src/handlers/v3/echo.js +39 -0
- package/templates/business-service/tests/cookbooks/echo.json +36 -0
- package/templates/business-service/tests/unit/handler.test.js +78 -0
- package/src/WorkflowTestRunner.js +0 -402
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,2588 @@ All notable changes to this package. Follows [Keep a Changelog](https://keepacha
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
## [8.1.0] — 2026-09-15
|
|
8
|
+
|
|
9
|
+
### Changed — deploy job šablony aplikuje migrace DB mezi `pull` a `up` (d.421)
|
|
10
|
+
|
|
11
|
+
Blok `oa-ci v1` v `templates/business-service/.gitlab-ci.yml` (řádek `G-CI`) má nový krok: po
|
|
12
|
+
`compose pull` a **před** `compose up` pustí na biz boxu běžec `api/scripts/lib/mariadb-migrations.sh`
|
|
13
|
+
z klonu api, který si krok uniformy vedle checkoutu stejně dělá — fáze 2 rozhodnutí vlastníka
|
|
14
|
+
`api/docs/governance/confirmations/db-migrations-first-deploy.md` 001. Pořadí je ta pojistka:
|
|
15
|
+
nový obraz je stažený, starý kontejner ještě obsluhuje, takže selhání migrace zastaví nasazení
|
|
16
|
+
s běžící předchozí verzí. Běžec se **nese**, nekopíruje: proud, kterým ssh krmí `bash -s`, je řádek
|
|
17
|
+
hesla, volby shellu, běžec z klonu api a teprve pak tělo skriptu — druhá implementace aplikátoru
|
|
18
|
+
nevzniká nikde (`change-discipline.md` § One rail per concern).
|
|
19
|
+
|
|
20
|
+
Co o schématu rozhoduje: `config/service/integration-contract.json`. Služba bez bloku `database`
|
|
21
|
+
vypíše `NOT APPLICABLE (no database block)` a nasazuje se dál; služba s blokem dostane
|
|
22
|
+
`database.schema` migrované z adresáře, který jmenuje `database.migrations`, s trackerem
|
|
23
|
+
`config/runtime/applied-migrations-<schema>.txt` ve vlastním checkoutu na boxu (tentýž plochý formát
|
|
24
|
+
jako fáze 1). Účet je účet služby (`MARIADB_MIGRATION_USER`/`_PASSWORD` z env souborů na boxu, nově
|
|
25
|
+
deklarované v `config/env-templates/__SERVICE_NAME__.env`), nikdy root, a krok **nic nezakládá**:
|
|
26
|
+
chybí-li klient `mysql`, klíč, env soubor nebo nemá-li účet na schéma právo, job zčervená s hláškou
|
|
27
|
+
na runbook `api/docs/setup/INSTALL.md`.
|
|
28
|
+
|
|
29
|
+
## [8.0.0] — 2026-09-14
|
|
30
|
+
|
|
31
|
+
### Added — zahazovací testovací schéma je export balíčku (d.407)
|
|
32
|
+
|
|
33
|
+
`createThrowawaySchema` (`src/utils/throwawaySchema.js`, na indexu balíčku) staví schéma, které
|
|
34
|
+
integrační sada vlastní: z deklarace `database` v `config/service/integration-contract.json`, se
|
|
35
|
+
stejným rozhodnutím jako brána CI. Sdílí se **všechno kromě jednoho**: sada migrací a její pořadí
|
|
36
|
+
(`resolveMigrationPlan`, `migrationOrder.js`), požadavek na collation (`requireCollation`), příkaz
|
|
37
|
+
`CREATE DATABASE … COLLATE` (`createDatabaseSql`) i volání klienta (`defaultExec`) mají jedinou
|
|
38
|
+
definici, v `setupDatabase.js`. Liší se ta jediná věc, kvůli které mají dvě jména: `buildSchema`
|
|
39
|
+
staví OSTRÉ schéma služby, a proto odmítne cíl, který nezaložil; `createThrowawaySchema` staví
|
|
40
|
+
schéma testu, a proto ho zahodí a založí znovu — a odmítá jediné jméno, to deklarované.
|
|
41
|
+
|
|
42
|
+
Tvar vychází z toho, co si dnes staví služby samy (biz-meta `tests/helpers/schemaFixture.js`, tři
|
|
43
|
+
sady): `before` zastaví před jmenovanou migrací (stav, na který se ta migrace aplikuje),
|
|
44
|
+
`applyMigration(name)` pustí jednu, `readMigration(name)` vrátí její SQL, `dispose()` schéma zahodí,
|
|
45
|
+
`seeds: true` doplní deklarované seedy (bez toho se neaplikují). Exekutor je injektovaný a smí být
|
|
46
|
+
asynchronní — služba, jejíž testovací obraz nemá klienta `mysql/mariadb`, si předá vlastní.
|
|
47
|
+
|
|
48
|
+
**Schéma vybírá volající, nikdy soubor.** Migrace psaná pro instalační běžec začíná
|
|
49
|
+
``USE `oagen_<služba>`;``, tedy jménem ostrého schématu; aplikovaná doslova pošle celý soubor tam.
|
|
50
|
+
Tyto řádky se proto odstraňují a výběr schématu, který strip přežije, běh zastaví. Změřeno na živé
|
|
51
|
+
MariaDB: bez stripu vznikne tabulka v deklarovaném schématu a další migrace spadne na
|
|
52
|
+
`Table '…_throwaway.probe' doesn't exist`.
|
|
53
|
+
|
|
54
|
+
### Changed — R8 čte import helperu, ne řetězec `CREATE DATABASE` (d.407)
|
|
55
|
+
|
|
56
|
+
Třetí propustka R8 (`fec9100a`) pouštěla soubor, který obsahuje `CREATE DATABASE` a přesměrování
|
|
57
|
+
`DB_NAME`. To je textová heuristika: přečte dva řetězce a z nich usuzuje, kam dotazy dopadnou.
|
|
58
|
+
Jakmile služba přesune stavbu schématu do sdíleného helperu — přesně to udělala biz-meta —
|
|
59
|
+
propustka zanikne, ačkoli soubor je bezpečnější než předtím. R8 proto nově pouští soubor, který na
|
|
60
|
+
vykonávaném řádku **importuje `createThrowawaySchema` z `@onlineapps/conn-orch-validator`**: kam
|
|
61
|
+
dotazy dopadnou, tam rozhoduje kód (odmítnuté deklarované schéma, jméno zahazovacího schématu u
|
|
62
|
+
každého příkazu, setřené výběry schématu v migracích), ne domněnka z textu.
|
|
63
|
+
|
|
64
|
+
Obě poloviny se musí zasloužit na vykonávaném řádku a repozitářový modul stejného jména nezíská nic
|
|
65
|
+
— důvěřuje se buildu tohoto balíčku, ničemu jinému. Měřeno nad `api_biz/meta`: dnes 3 hlášené
|
|
66
|
+
soubory (`migration013MeditestMove`, `migration014MeditestRefsLeave100`, `platformIdentitySeed`),
|
|
67
|
+
po záměně jejich importu za knihovní helper 0; táž kopie proti původnímu kódu R8 dál 3.
|
|
68
|
+
|
|
69
|
+
### Changed — VALIDATION.md šablony nese hlášku chybějící collation (d.407)
|
|
70
|
+
|
|
71
|
+
`templates/business-service/docs/80-setup/VALIDATION.md` (a jeho zrcadlo v `api/templates`) má v
|
|
72
|
+
§ Failure signatures položku `[SetupDatabase] Missing database.collation …` a co s ní: doplnit klíč
|
|
73
|
+
do bloku `database`. Blok `database` v šabloně nadále není — služba ho píše, až když databázi má.
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
### BREAKING — `database.collation` je deklarace služby: brána s ní schéma zakládá a cizí collation odmítá (d.434)
|
|
77
|
+
|
|
78
|
+
Rozhodnutí vlastníka 2026-09-14 (`api/docs/governance/confirmations/db-collation-declaration.md` 001,
|
|
79
|
+
`api/docs/biz/70-contracts/database-contract.md` §1): collation, se kterou se schéma služby zakládá,
|
|
80
|
+
deklaruje služba jedním klíčem v bloku `database`, a čte ho CI brána i produkční runbook. Žádný
|
|
81
|
+
default (princip 3).
|
|
82
|
+
|
|
83
|
+
`buildSchema` proto (1) bez klíče odmítne stavět a pojmenuje ho i s opravou, (2) **měří**
|
|
84
|
+
`information_schema.SCHEMATA.DEFAULT_COLLATION_NAME` cílového schématu a (3) zakládá
|
|
85
|
+
`CREATE DATABASE <schema> COLLATE <collation>` — nikdy `IF NOT EXISTS`. Ta tichá varianta je to,
|
|
86
|
+
co tu dosud bylo, a nad existující databází NIC nezmění (změřeno BIZ-DOCS 2026-09-14 v jednorázovém
|
|
87
|
+
`mariadb:10.5.29`): šest biz pipeline si nechá schéma založit sidecarem z `MARIADB_DATABASE` ještě
|
|
88
|
+
před jobem, takže přesně v prostředí, kde brána běží, databáze už existuje — s defaultem obrazu
|
|
89
|
+
`utf8mb4_general_ci`. Existující schéma s jinou collation tedy nově běh zastaví a pojmenuje obě
|
|
90
|
+
collation, server i opravu (`MARIADB_COLLATION` v sidecaru, nebo `ALTER DATABASE`); se stejnou
|
|
91
|
+
collation se nezakládá nic a migrace běží.
|
|
92
|
+
|
|
93
|
+
Kontrakt (`bizCiGateContract.js`) validuje TVAR hodnoty, když je uvedena (utf8mb4 collation —
|
|
94
|
+
interpoluje se do `CREATE DATABASE … COLLATE`, a klient i všechny tabulky platformy mluví utf8mb4),
|
|
95
|
+
a nese ji v normalizovaném bloku dál. Řádek uniformy **`D-DB-COLLATION`** (`db`, severity `deploy`)
|
|
96
|
+
hlásí repozitář s blokem `database`, který klíč nedeklaruje; služba bez bloku mlčí. Tři otázky, tři
|
|
97
|
+
místa, žádná kopie: tvar = `C-CONTRACT`, přítomnost = `D-DB-COLLATION`, „bez hodnoty nestavím" =
|
|
98
|
+
`setupDatabase`.
|
|
99
|
+
|
|
100
|
+
**BREAKING pro služby**: `biz-ci-gate setup-db` selže každé službě s databází, která `collation`
|
|
101
|
+
nedeklaruje. Na HEAD 2026-09-14 blok `database` deklaruje sedm repozitářů a klíč z nich má jediný
|
|
102
|
+
(`property`, `utf8mb4_bin`); `converter`, `emailer`, `hello-service`, `ingest`, `invoicing` a `meta`
|
|
103
|
+
ho doplní ve své kaskádě 8.0.0 (`pdfgen` databázi nemá a mlčí).
|
|
104
|
+
|
|
105
|
+
RED → GREEN (jednotkově): `npx jest tests/unit/databaseDeclaration.test.js` 6 failed / 20 passed →
|
|
106
|
+
26/26; `tests/unit/setupDatabase.test.js` 11 failed / 28 passed → 39/39; `tests/unit/manifestDbRows.test.js`
|
|
107
|
+
3 failed / 26 passed → 29/29. Integračně proti živé MariaDB 10.5 (`setupDatabaseLive.integration.test.js`,
|
|
108
|
+
sonda `tests/helpers/setupDatabaseProbe.js` v kontejneru nad `gen_mariadb10.5`): 3 failed / 6 passed →
|
|
109
|
+
9/9 — schéma neexistuje → založeno a zpětně změřeno `utf8mb4_bin`; existuje prázdné se správnou →
|
|
110
|
+
postaveno (4 migrace, 1 tabulka); existuje prázdné s `utf8mb4_general_ci` → odmítnuto, a po odmítnutí
|
|
111
|
+
má 0 tabulek a pořád svou původní collation. Kontrolní případy beze změny: populované schéma dál
|
|
112
|
+
padá na počtu tabulek dřív než na collation, služba bez bloku `database` dál hlásí
|
|
113
|
+
`NOT APPLICABLE setup-db`, a řádek `D-DB-COLLATION` mlčí nad službou bez databáze i nad hodnotou
|
|
114
|
+
špatného charsetu (ta patří `C-CONTRACT`).
|
|
115
|
+
|
|
116
|
+
### Added — test drží, že `doc` každého řádku manifestu vede na existující dokument a sekci (d.438)
|
|
117
|
+
|
|
118
|
+
`manifestShape.js` odmítá řádek BEZ `doc` (konfirmace `biz-service-manifest` 004 bod 1), ale nikdo
|
|
119
|
+
se neptal, jestli ten ukazatel někam vede: přejmenovaný nebo přečíslovaný dokument udělá ze všech
|
|
120
|
+
řádků, které ho citují, slepou uličku, a sada zůstane zelená — falešná záruka
|
|
121
|
+
(`.claude/rules/automation-gates.md` §5), a totéž hnití, které `L008` chytá v opačném směru.
|
|
122
|
+
`tests/unit/manifestDocPointers.test.js` prochází OBA manifesty a každý `doc`, kdekoli sedí: cesta
|
|
123
|
+
se řeší stejným `resolveWorkspacePath` jako `from:` odkazy a musí existovat; `§ N` musí v dokumentu
|
|
124
|
+
najít nadpis — a hlídají se obě reálné podoby, `## N.` ve `docs/biz/**` i `### §N` v konfirmacích,
|
|
125
|
+
protože kontrola znající jen jednu by druhou hlásila jako mrtvou. Nečíselná citace
|
|
126
|
+
(`§ Version pinning`) se řeší jako text.
|
|
127
|
+
|
|
128
|
+
Nad HEAD je mrtvých odkazů nula (85 ukazatelů), takže RED se dokládá zasazením: `doc` řádku
|
|
129
|
+
`D-DB-NAMING` přepsán na `database-contract.md § 42` → 1 failed se jménem řádku i chybějící sekce,
|
|
130
|
+
po vrácení 7/7. Kontrolní případy, které to drží poctivé: neexistující dokument je hlášen, obě
|
|
131
|
+
podoby nadpisu projdou, vymyšlená pojmenovaná sekce je hlášena, a kopie balíčku mimo checkout
|
|
132
|
+
řekne NOT RUN místo tichého průchodu.
|
|
133
|
+
|
|
134
|
+
### Fixed — `D-DB-NAMING` drží `database-contract.md` §5, takže `000_baseline.sql` je platné jméno (d.433)
|
|
135
|
+
|
|
136
|
+
Regex řádku žádal „aspoň dvě slova" (`(?:_[a-z0-9]+)+`), ale pojmenování migrací vlastní
|
|
137
|
+
normativní `api/docs/biz/70-contracts/database-contract.md` §5 a ten předepisuje
|
|
138
|
+
`NNN_snake_case.sql` — tři číslice, podtržítko, popis malými písmeny — a `000_baseline.sql`
|
|
139
|
+
uvádí jako svůj vlastní příklad; §8 z regenerované baseline dělá aplikovanou migraci. Řádek
|
|
140
|
+
byl tedy přísnější než kontrakt a odmítal jeho příklad: `meta` i `emailer` (a `invoicing`)
|
|
141
|
+
mají `migrations/000_baseline.sql` aplikovaný a dostávaly nález blokující deploy. Kvantifikátor
|
|
142
|
+
je `*` místo `+`, `why`/`fix` cituje vlastníka pravidla místo vlastní formulace a pole `doc`
|
|
143
|
+
míří na `database-contract.md § 5` — dosavadní `repository-installation-sql-contract.md`
|
|
144
|
+
pojmenování souborů nedefinuje vůbec (definuje obsah `BASELINE/` a `SEED/**`, §4, což hlídá
|
|
145
|
+
`D-DB-HEADERS`).
|
|
146
|
+
|
|
147
|
+
RED (`npx jest tests/unit/manifestDbRows.test.js`): 1 failed / 22 passed — kontrolní případ
|
|
148
|
+
`000_baseline.sql` dostal nález „three digits, then at least two snake_case words". GREEN:
|
|
149
|
+
23/23. Kontrolní případy, které se nesměly hnout a nehnuly: `010_core_tables.sql` (druhý
|
|
150
|
+
příklad §5) mlčí, `10_x.sql` (dvě číslice), `010_CamelCase.sql` (velká písmena), `010_.sql`
|
|
151
|
+
(prázdný popis), `999a_diary_header_alias.sql` (přípona u pořadí) i soubor bez pořadí dál
|
|
152
|
+
nález jsou; `BASELINE/`, `SEED/**` a služba bez databáze dál mlčí. Přes CLI
|
|
153
|
+
(`node src/cli/oa-validate.js <repo> --json`) `meta` i `emailer` před opravou hlásily
|
|
154
|
+
`D-DB-NAMING` na `migrations/000_baseline.sql`, po opravě nic; nad zbylými šesti biz
|
|
155
|
+
repozitáři je řádek beze změny tichý.
|
|
156
|
+
|
|
157
|
+
### Fixed — R6 odmítne pin na `@onlineapps/*` balíček, který SSOT nezná v žádném seznamu (d.413)
|
|
158
|
+
|
|
159
|
+
Zúžení na `infraConsumed` pokrývá balíček, který SSOT DEKLARUJE a žádná infra služba ho
|
|
160
|
+
neinstaluje — tam není s čím verzi porovnávat. Pin na `@onlineapps/*` balíček, který SSOT
|
|
161
|
+
nedeklaruje ANI v `libraries`, ANI v `infraConsumed`, padal do téhož `notGated`, takže
|
|
162
|
+
R6 hlásil OK pro balíček, který žádný platformní kontrakt nekryje (změřeno 2026-09-14,
|
|
163
|
+
nález W401, `src/utils/libCompat.js:88` před opravou). Pravidlo R6 je, že služba pinuje přesně
|
|
164
|
+
to, co SSOT deklaruje (`.claude/rules/architecture-principles.md` § Version pinning; kanál dat
|
|
165
|
+
brány = SSOT, konfirmace `libraries-ssot-channel` 001) — u nedeklarovaného balíčku žádná
|
|
166
|
+
taková verze neexistuje, takže se odmítá, ne zužuje (jinak je to fallback na „not gated",
|
|
167
|
+
princip 3). `notGated` zůstává pro deklarovaný balíček, který infra neinstaluje; balíčky mimo
|
|
168
|
+
scope `@onlineapps/*` se do něj nikdy nedostaly (filtr `SCOPE` v `checkLibCompat`).
|
|
169
|
+
|
|
170
|
+
RED (`npx jest tests/unit/libCompat.test.js tests/unit/bizCiGateCli.libCompat.integration.test.js`):
|
|
171
|
+
4 failed / 32 passed — jednotkově `ok` bylo `true` místo `false`, přes CLI prázdný `stderr` a
|
|
172
|
+
exit 0. GREEN: 36/36. Kontrolní případy, které se nesměly hnout: balíček v `infraConsumed`
|
|
173
|
+
chybějící v `libraries` dál hlásí svou větev (`not present in the infra library set`), shodný
|
|
174
|
+
pin dál prochází, týž repozitář s toutéž pinovanou verzí při deklaraci v SSOT je dál `not
|
|
175
|
+
gated`. Fixtura `api/tests/scripts/lib-compat.bats` (smoke obálky `verify-lib-compat`)
|
|
176
|
+
deklarovala balíček pro případ „narrowing skipped" v žádném seznamu; po opravě ho SSOT fixtury
|
|
177
|
+
deklaruje, takže případ dál popisuje to, k čemu byl napsán (5/5).
|
|
178
|
+
|
|
179
|
+
### Added — R6 má sadu, která drží, že se na něj `verify-contract` DOSTANE (d.401, `436095a7`)
|
|
180
|
+
|
|
181
|
+
Ze čtyř sousedních poskládaných kontraktů má každý svou `bizCiGateCli.*.integration.test.js`;
|
|
182
|
+
R6 byl jediný bez ní — a je to ten, který biz repozitáře pouštějí každou pipeline.
|
|
183
|
+
`bizCiGateCli.libCompat.integration.test.js` měří akceptační kritérium R6 přes CLI, které
|
|
184
|
+
pipeline skutečně spouští (`ci:gate:contract` = `biz-ci-gate verify-contract`, 8/8 repozitářů;
|
|
185
|
+
samostatný `verify-lib-compat` v žádném `.gitlab-ci.yml` není): neshodný pin → exit 1 s oběma
|
|
186
|
+
verzemi, caret odmítnut, devDependency stejně jako dependency, balíček mimo `infraConsumed`
|
|
187
|
+
hlášen jako not gated, nenakonfigurovaný nebo nečitelný SSOT odmítnut místo tichého průchodu,
|
|
188
|
+
a shoda verdiktu se samostatným subcommandem. RED s odstraněným fold-inem: 8 failed / 2 passed
|
|
189
|
+
(projdou právě dva testy samostatné koleje — kontrolní případ); GREEN 10/10.
|
|
190
|
+
|
|
191
|
+
### Changed — konformní fixtura biz repozitáře má jedno místo (d.401, `436095a7`)
|
|
192
|
+
|
|
193
|
+
`tests/helpers/conformantBizRepo.js`: repozitář, který projde deploy, installation, env
|
|
194
|
+
a test-coverage kontraktem, takže exit kód poskládaného běhu je přiřaditelný. Dvě sady
|
|
195
|
+
potřebují totéž; definice opsaná dvakrát se rozejde, jakmile kterýkoli z kontraktů dostane
|
|
196
|
+
pravidlo (`change-discipline.md` § One rail per concern).
|
|
197
|
+
|
|
198
|
+
### Fixed — `checkSetupDocs` and the manifest check ask for the exact spelling (d.408)
|
|
199
|
+
|
|
200
|
+
`fs.existsSync` answers true for `install.md` when the contract spells `INSTALL.md` on a case-insensitive filesystem (macOS APFS), so the install-contract gate passed on a developer machine and failed in `node:24-alpine` (BIZ-PROPERTY, 2026-09-14: 361 findings vs 363). Both places now use `existsExactly` from the git-tracked check — one rail for the trap, exported rather than copied. RED on macOS: a setup doc renamed to lower case passed; GREEN: it is reported as missing on every filesystem.
|
|
201
|
+
|
|
202
|
+
### Fixed — the contract gates verify their input on the way in (d.400, `98cbb2ab`)
|
|
203
|
+
|
|
204
|
+
`verifyEnvCompleteness` and `verifyInstallContract` returned `ok: true` over a loader envelope (`{contract, source, …}`) instead of the normalized contract — a gate passing over a declaration it never checked (`automation-gates.md` §5). They now refuse anything but the normalized shape with `[Context] Problem - Expected/Fix`, through the one normalizer the CLI already uses.
|
|
205
|
+
|
|
206
|
+
### Fixed — `write-summary` cannot be talked out of a failure it measured (d.392b, `3b445f05`)
|
|
207
|
+
|
|
208
|
+
`--gate-verdict pass` no longer withdraws a failing integration minimum: the measured verdict wins, `--gate-verdict` may only make the verdict worse, and the artefact's reason names the refused override in `[Context] Problem - Fix` form — neither the caller's `--gate-reason` nor the failure's own message stands in its place (`automation-gates.md` §1.5). The CLI help for `--gate-verdict` says what the switch does now. A backtick inside the help template literal took the whole CLI down during the change (`SyntaxError`), caught by the suite; `node --check src/cli/biz-ci-gate.js` is part of the gate from here.
|
|
209
|
+
|
|
210
|
+
### Changed — the service template stops stating library versions by hand (d.392, `e7943612` … `a8d3c281`)
|
|
211
|
+
|
|
212
|
+
- `templates/business-service/package.json` pins every `@onlineapps/*` dependency with the `__SSOT_PIN__` placeholder; `oa-sync-template --new` resolves it from `api/config/libraries.json` and refuses a placeholder it cannot resolve. The template used to claim `service-wrapper 2.1.119` and `service-common 1.1.1` while the SSOT said 7.0.0 / 2.0.1 — a number written by hand is a lie with a commencement date. Existing services keep getting their pins from the publish cascade, never from the template. (d.392/1)
|
|
213
|
+
- `docker-compose.production.yml` of the template no longer declares `DEPLOY_ENV` — zero readers anywhere in the workspace (`process.env.DEPLOY_ENV`, `$DEPLOY_ENV`: none); `NODE_ENV` from the shared env manifest owns that fact. Nine manifest fixtures follow (`G-PROD` is `template-render`). (d.392/3, 3b)
|
|
214
|
+
- `biz-ci-gate setup-db` over a service with no `database` block reports `NOT APPLICABLE` with the contract path, instead of `OK` for work it never did. The template's README and `docs/80-setup/INSTALL.md` state that the one-shot test runner is outside a plain `docker compose build` and name `docker compose --profile test build` (measured with `--dry-run`: one image vs two). (d.392/6)
|
|
215
|
+
|
|
216
|
+
### Fixed — `write-summary` names the step that failed (d.392/5, `47f9114c`)
|
|
217
|
+
|
|
218
|
+
`gate.reason` comes from the step that failed — the caller's `--gate-reason`, or the integration minimum when that is what failed, or `unknown step failed`. A declared failure with no reason no longer reports "integration minimum satisfied".
|
|
219
|
+
|
|
220
|
+
### Added — manifest self-test over the template's npm scripts (d.392/2, `4473b956`)
|
|
221
|
+
|
|
222
|
+
`tests/unit/templateScriptSelftest.test.js` checks the packaged template's scripts against the manifest in both directions, with the deliberately-free list and its reasons (`test:container` stays as a declared exception; `test:integration:container` is `only_with: tests/integration`, which the template does not ship). The CI `test` job of a service is the service's own — the `oa-ci v1` block does not render it (confirmation `biz-service-manifest` 008, d.251) — so the template carries no conditional `mariadb-client`/`XDG_RUNTIME_DIR` rendering; that finding is closed by the recorded decision, not by code.
|
|
223
|
+
|
|
224
|
+
### Fixed — every `format.md §` citation in the package names a section that exists (d.393b, `631d0cce`)
|
|
225
|
+
|
|
226
|
+
`CookbookTestRunner` sent cookbook authors to `format.md § Steps` from five messages. The node has no such section, and the five do not even share one owner: a missing `config/service/operations.json` is owned by `30-operations/schema-v3.md § File shape` — `format.md` decides nothing about that file, so the citation named the wrong NODE, not merely the wrong section — while the absent/empty `steps` belong to § Required fields and a step missing `service`/`operation` to § Step definition. All five now say where the answer is. `tests/unit/cookbookFormatDocReference.test.js` no longer checks a single module: it scans every `format.md §` citation under `src/` against the node's headings, carries its own case for the relocated operations.json citation, and says NOT RUN out loud when the node is absent from the checkout. `L008` cannot catch this class — it verifies the document behind a citation, never the section behind `§`.
|
|
227
|
+
|
|
228
|
+
### Fixed — every fixture cookbook obeys the `step_id` pattern, and a gate now holds it (d.393b)
|
|
229
|
+
|
|
230
|
+
The last six fixture steps spelling `step_id` with a hyphen (`v3-cookbook-service/**`, `v3-service-undeclared-handler/**`) are on the underscore spelling the schema requires (`^[a-zA-Z0-9_]+$`, `cookbook.v2.schema.json`, every step type); their six assertions prove the same outcomes under the new names. With compliance in the same commit — never ahead of it — `tests/unit/fixtureCookbookStepIds.test.js` widens from two fixture workspaces to the whole `tests/fixtures/**` tree, with no exception list. Files that are not valid JSON are counted rather than silently skipped, and a control case names the only one that is meant to be broken.
|
|
231
|
+
|
|
232
|
+
### Fixed — the cookbook-format refusals cite a section the node actually has (d.393, `c5611383`)
|
|
233
|
+
|
|
234
|
+
`readCookbookSteps()` sent a cookbook author to `api/docs/biz/40-cookbooks/format.md § Steps`. The node has no such section: both halves of the rule — `steps` is an array, and a step identifies itself with `step_id` — are owned by § The array is the only accepted shape. `L008` cannot see this class of defect; it checks the document behind a citation, never the section behind `§`. Both messages now name the real section, and `tests/unit/cookbookFormatDocReference.test.js` extracts the citation from the message the module actually throws and matches it against the node's headings (NOT RUN, loudly, when the node is absent from the checkout).
|
|
235
|
+
|
|
236
|
+
### Fixed — fixture cookbooks obey the `step_id` pattern the schema enforces (d.393)
|
|
237
|
+
|
|
238
|
+
Seven fixture steps spelled their `step_id` with a hyphen, which `cookbook-core/schemas/cookbook.v2.schema.json` refuses on every step type (`^[a-zA-Z0-9_]+$`) — a shape production rejects and the offline Tier-1 runner accepts, because the runner checks no pattern at all. The six cookbooks now use the underscore spelling, their suites assert the same outcomes under the new step names, and `tests/unit/fixtureCookbookStepIds.test.js` reads the pattern from the schema instead of restating it. Four fixture files under `v3-cookbook-service/**` and `v3-service-undeclared-handler/**` still carry the defect and are deliberately outside this test's scope — an open finding (d.393b), not a silent pass.
|
|
239
|
+
|
|
240
|
+
### Security — the biz CI template stops putting the registry password on a command line (d.389, `ccde5b17`)
|
|
241
|
+
|
|
242
|
+
`templates/business-service/.gitlab-ci.yml` logged in to the registry with `-p $CI_REGISTRY_PASSWORD` on the runner, and handed the same password to the deploy host as an argument of the remote `bash -s` — visible in `ps` on both machines for the length of the job (INFRA-DOCS finding, 2026-09-14). The password now travels on stdin only: the build logs in with `--password-stdin`, and the remote stream carries the password as its first line, which the remote shell reads and exports before `bash -s` gets the rest. The remote positional arguments are six and none of them is the secret. Proved by `tests/scripts/biz-deploy-secret-handling.bats`, which runs the template's own shell with doubles that record argv and stdin on both sides.
|
|
243
|
+
|
|
244
|
+
### Security — both boxes the deploy job talks to are pinned (d.389)
|
|
245
|
+
|
|
246
|
+
The job's `~/.ssh/config` said `StrictHostKeyChecking no`, so the deploy key, the registry password and the gate-reader key went to whatever answered on the address. The catch-all now carries `StrictHostKeyChecking yes` and `UserKnownHostsFile ~/.ssh/known_hosts`, written from the new CI variables `DEPLOY_HOST_KEY` and `GATE_INFRA_HOST_KEY` (one `ssh-keyscan -t ed25519` line each); the policy lives in `ssh_config` rather than on the command line because `run-post-deploy-gate.sh` opens its own ssh connection and takes no switches from the template — one rail for both. A missing pin fails the job before the first connection, naming the variable and the command that produces its value — never a fallback.
|
|
247
|
+
|
|
248
|
+
### Changed — `installContract.js` enforces the whole contract, not three directories and four keys (d.320, `d6321fd2`)
|
|
249
|
+
|
|
250
|
+
BREAKING for any repository whose live SQL lay outside `migrations/{BASELINE,SEED}`: the loop is now `migrations/**/*.sql` minus `archive/` and `superseded/` (confirmation `installation-sql-contract-scope` 001). Measured on HEAD: invoicing 5 files and property 90 files carried no header at all and passed. The header VOCABULARY is checked, not the presence of the key (§4): `Dataset-Class`, `Safe-For-Production` and `Idempotency` accept only the values the contract defines, the value is the first token and free text may follow it; `Target-DB` stays presence-only — the contract calls it a name, not a vocabulary, and comparing it against `database.schema` would make the header a second copy of that fact. Test-only data must carry its visible `TEST-ONLY` comment marker, and `Dataset-Class: TEST_ONLY` anywhere implies `Safe-For-Production: no` (the rule follows the class, not the directory). A manifest package is complete in BOTH directions (§5, §7): every file an `-- Applies:` line names exists, every `.sql` the installer applies is named, and the order is the installer's own (`migrationOrder.compareVersion`, shared with `setupDatabase.js` and the operator's `sort -V`); a file is a manifest exactly when it carries `-- Applies:`. The module cited ADR 0006, where the rule does not live — `Rules:` now names `api/docs/standards/repository-installation-sql-contract.md`, so L008 checks it. RED 14/31 → GREEN 31/31.
|
|
251
|
+
|
|
252
|
+
### Changed — the delay test states WHEN, not how long the machine took (d.379)
|
|
253
|
+
|
|
254
|
+
`MockMQClient.test.js` § Delay Simulation measured `process.hrtime` against a 2 ms
|
|
255
|
+
tolerance and a 500 ms ceiling, and went red on roughly one run in four under concurrent
|
|
256
|
+
load. A verdict that depends on how busy the machine is fails the first requirement
|
|
257
|
+
`automation-gates.md` §1.1 puts on any automatic mechanism, and a flake teaches everyone to
|
|
258
|
+
re-run instead of to read. Fake timers replace the stopwatch: the promise is pending at
|
|
259
|
+
`DELAY_MS - 1`, settled at `DELAY_MS`, and no timer is left behind. The three claims are
|
|
260
|
+
unchanged — it waited, it waited once, it waited in the right scale — and each was
|
|
261
|
+
confirmed by breaking `simulateDelay` those three ways and watching the suite go red.
|
|
262
|
+
|
|
263
|
+
### Removed — three declarations with no consumer (d.378)
|
|
264
|
+
|
|
265
|
+
- **`operation.headers`.** The dispatch descriptor carried `headers: operation.headers || {}`
|
|
266
|
+
and nothing read it: ctx.headers is built from `step.headers`, which is what the contract
|
|
267
|
+
prescribes (api/docs/biz/10-invocation/operation-context.md § ctx.headers names the STEP).
|
|
268
|
+
The operations.json rules in `service-validator-core` know no `headers` field, and not one
|
|
269
|
+
of the eight live `config/service/operations.json` declares it. A field nobody reads, in a
|
|
270
|
+
shape nobody may declare, taught readers that an operation could carry headers.
|
|
271
|
+
- **`mockInfrastructure` in seven cookbook fixtures.** The runner mocks on
|
|
272
|
+
`options.mockInfrastructure`, which `utils/preValidation.js` passes; the cookbook format
|
|
273
|
+
carries no such key (`cookbook-core/schemas/cookbook.v2.schema.json` calls `test`
|
|
274
|
+
acceptance-test metadata, `docs/biz/40-cookbooks/format.md` never names it). One
|
|
275
|
+
declaration, in the options. The schema admits additional properties at both levels, so
|
|
276
|
+
nothing can refuse the key at load time — `cookbookMockInfrastructure.test.js` is what
|
|
277
|
+
keeps it from growing back.
|
|
278
|
+
- **The second copy of the service fixture.** `tests/fixtures/cookbook-counts/service`
|
|
279
|
+
duplicated `v3-test-service` and had already drifted apart from it — the `returns-nothing`
|
|
280
|
+
operation and its handler existed in one copy and not the other — while
|
|
281
|
+
`cookbook-counts/two-cookbooks` held a byte-identical copy of that service's own cookbooks.
|
|
282
|
+
One fixture now, read by all three suites (`change-discipline.md` § One rail per concern).
|
|
283
|
+
|
|
284
|
+
### Fixed — the step aggregate adds up: `passed + failed === total` (d.378)
|
|
285
|
+
|
|
286
|
+
A cookbook that threw on load pushed one entry into `results.steps` and incremented
|
|
287
|
+
`failed`, and nothing incremented `total` — so a run could report `total 2 / failed 3`
|
|
288
|
+
(BIZ-converter, 2026-08-26): an aggregate whose own arithmetic contradicts itself, read by
|
|
289
|
+
`utils/preValidation.js` to decide whether a proof is written and folded by
|
|
290
|
+
`ValidationOrchestrator` into the run's totals. One definition now: everything in
|
|
291
|
+
`results.steps` is counted in `total` exactly once — a real step, or the single entry
|
|
292
|
+
standing for a cookbook that never produced any. Two suites had encoded the old arithmetic
|
|
293
|
+
(`total: 1` against `failed: 2`, and seven failures out of a total of zero) and now state
|
|
294
|
+
the rule instead.
|
|
295
|
+
|
|
296
|
+
### Fixed — `expect.error` is checked when the step SUCCEEDS too (d.378)
|
|
297
|
+
|
|
298
|
+
`validateExpectations` guarded the error block with `if (expect.error && result.error)`,
|
|
299
|
+
the sister defect of the `expect.output` guard one block up (d.365). A cookbook declaring
|
|
300
|
+
`expect.error` over a step whose handler returned normally skipped the block entirely, and
|
|
301
|
+
since `expect.status` is optional, a step declaring only `expect.error` came back with
|
|
302
|
+
`validationErrors: []` — green, over an expectation nothing evaluated
|
|
303
|
+
(`automation-gates.md` §5). The absence of the error is now the violation and says so in
|
|
304
|
+
the `[Context] Problem - Expected/Fix` form; a step that genuinely throws is judged exactly
|
|
305
|
+
as before.
|
|
306
|
+
|
|
307
|
+
### Fixed — the Tier-1 runner reads the workspace the cookbook declares (d.354)
|
|
308
|
+
|
|
309
|
+
`CookbookTestRunner` built its ctx from `getTestNamespace()` alone, so
|
|
310
|
+
`defaults.workspace_id` and the per-step override the format defines were fields
|
|
311
|
+
nothing read — six emailer cookbooks carried a value the boot probe ignored. Owner
|
|
312
|
+
confirmation `tier1-runner-workspace` 001 (2026-09-14): the runner reads the cookbook.
|
|
313
|
+
|
|
314
|
+
Per-step wins over `defaults`, and a cookbook that declares neither keeps the platform
|
|
315
|
+
namespace from the environment — 32 of 44 cookbooks in the eight live biz repositories
|
|
316
|
+
declare no workspace, so refusing there would fail six services' boot on day one
|
|
317
|
+
(`automation-gates.md` §3). The TENANT does not move: it stays `getTestNamespace()`'s, so
|
|
318
|
+
no cookbook can walk the probe out of an allowed environment class. A declared workspace
|
|
319
|
+
passes the same rail as the environment value — `assertWorkspaceId` in
|
|
320
|
+
`utils/testNamespace.js`, new and exported from that module — so a value that is not an id
|
|
321
|
+
is refused naming the cookbook key, never swapped for the env value.
|
|
322
|
+
|
|
323
|
+
### Added — `C-CONNECTORS`: the two connector declarations must agree (d.353)
|
|
324
|
+
|
|
325
|
+
`verifyConnectorContract` has compared `config/service/config.json` → `wrapper.<connector>`
|
|
326
|
+
against `integration-contract.json` → `requiredConnectors` since F12, and nothing acted on
|
|
327
|
+
the answer: at boot it went into `results.warnings` and the service started, and no CI job
|
|
328
|
+
ran it. Owner confirmation `connector-contract-check` 001 (2026-09-14): a uniform row,
|
|
329
|
+
severity `deploy`. The boot is unchanged — it still reports and still starts.
|
|
330
|
+
|
|
331
|
+
The row asks only the declaration half; the environment half (`REDIS_URL` and friends)
|
|
332
|
+
stays at boot, where an environment exists. The rule itself stays in one place —
|
|
333
|
+
`utils/connectorContract.js` → `mismatches()` — with `verifyConnectorDeclarations` and
|
|
334
|
+
`verifyConnectorContract` as its two phrasings. Measured against all eight services before
|
|
335
|
+
the row landed: zero findings.
|
|
336
|
+
|
|
337
|
+
### Fixed — cookbooks and steps are counted apart, and `expect.output` is always checked (d.365)
|
|
338
|
+
|
|
339
|
+
- `Cookbook tests: X/Y` counted STEPS while saying cookbooks, so three one-step cookbooks
|
|
340
|
+
read as "3/3" and the day one gained a second step the number rose with no cookbook added
|
|
341
|
+
(BIZ-converter, 2026-08-27). The aggregate now carries `cookbooks: {total, passed, failed}`
|
|
342
|
+
beside the step counts, and both lines name their unit: `Cookbooks: 2/2 passed (3/3 steps)`
|
|
343
|
+
at boot, `cookbooks — 2/2 passed, steps 3/3 passed, 0 failed` from the CLI. A cookbook that
|
|
344
|
+
cannot be loaded counts as a failed cookbook, never as one nobody counted.
|
|
345
|
+
- `expect.output` was guarded by `if (expect.output && result.actual)`, so a step whose
|
|
346
|
+
handler returned nothing skipped every field assertion and passed on `status` alone
|
|
347
|
+
(BIZ-pdfgen, 2026-08-29). The guard is gone: `validateOutput` already answers correctly
|
|
348
|
+
for an absent output — a required field is missing, and an `exists: false` assertion is
|
|
349
|
+
satisfied by no output at all.
|
|
350
|
+
|
|
351
|
+
### Added — `run-prevalidation --operation <regex>` (d.365)
|
|
352
|
+
|
|
353
|
+
Only the cookbooks holding a step whose operation matches are run, and they run WHOLE — a
|
|
354
|
+
later step reads what an earlier one wrote. A filtered run writes NO validation proof: a
|
|
355
|
+
proof is the claim that the whole cookbook set passed. An expression matching nothing is an
|
|
356
|
+
error naming the operations the directory does run, never an empty pass
|
|
357
|
+
(`automation-gates.md` §5). This is what lets BIZ-emailer delete
|
|
358
|
+
`tests/cookbooks/run-all.js`, the second way of running cookbooks their
|
|
359
|
+
`COOKBOOK_OPERATION_FILTER` kept alive (`change-discipline.md` § One rail per concern).
|
|
360
|
+
|
|
361
|
+
### Fixed — `run-prevalidation` ends instead of hanging on the service's own pool (d.365)
|
|
362
|
+
|
|
363
|
+
Measured 2026-09-14: with a handler that leaves one live handle behind — which is what a
|
|
364
|
+
service's module-level Sequelize pool is — the command printed every line, wrote the proof,
|
|
365
|
+
and then hung until a 15 s kill. The connectors are NOT closed, deliberately: at boot the
|
|
366
|
+
same dispatch runs inside `ServiceWrapper` phase 0.2 and that pool serves the service for
|
|
367
|
+
the rest of its life. What differs is the lifetime, so the one-shot command states its exit
|
|
368
|
+
code and ends, after flushing both streams (`process.exit` can truncate a pipe).
|
|
369
|
+
|
|
370
|
+
The finding that led here (BIZ-ingest, 2026-09-07) named a per-service
|
|
371
|
+
`scripts/run-pre-validation.js` masking the pool with `process.exit(0)`. That script is
|
|
372
|
+
retired — `X-PREVAL` forbids it and the template does not carry it — so nothing masked the
|
|
373
|
+
leak any more, and the hang is what was left.
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
### Fixed — a marker inside a fence is an example, and `--all` stops matching markers by hand (d.319)
|
|
377
|
+
|
|
378
|
+
Two modules carry the same marker shape: `api/scripts/ci/lib/generatedRegion.js`, which the
|
|
379
|
+
biz-facts and infra-queue generators render through, and this one, which `docsRegion.js`
|
|
380
|
+
and `readmePointer.js` render through. Only the first knew that a marker shown inside a
|
|
381
|
+
fenced code block is an EXAMPLE. The api side learned it the hard way on 2026-09-09, hours
|
|
382
|
+
after the queue generator landed: `api/docs/standards/INFRA-DOC-STANDARD.md` documents the
|
|
383
|
+
marker pair inside a fence — it still does, at the section on generated regions — and the
|
|
384
|
+
generator took its own documentation for a region naming a queue family that does not
|
|
385
|
+
exist, a gate red on the tree it had just been added to (`automation-gates.md` §3).
|
|
386
|
+
|
|
387
|
+
This module writes into the same tree, and the documents it writes into are precisely the
|
|
388
|
+
ones that TEACH the shape (`docs/biz/60-templates/service-template.md`,
|
|
389
|
+
`docs/guides/library-publishing-process.md`, `docs/guides/DEVELOPMENT.md` — the last already
|
|
390
|
+
shows the markers in prose at `library-publishing-process.md`). One input, two answers,
|
|
391
|
+
depending on which module met it, is what `automation-gates.md` §1.1 calls unpredictable.
|
|
392
|
+
`findRegion`, `extractRegion` and everything built on them now skip fenced markers, so a
|
|
393
|
+
document that only shows the pair declares no region and is never written into.
|
|
394
|
+
|
|
395
|
+
What did NOT change, and is covered by its own case: an UNFENCED lone marker is still
|
|
396
|
+
damage and still throws, and an inverted pair still throws. Fence-awareness must not turn a
|
|
397
|
+
half-written region into silence.
|
|
398
|
+
|
|
399
|
+
`generatedRegion.hasRegion()` is new, and `docsRegion.regionIdsIn()` — the question `--all`
|
|
400
|
+
asks before it renders anything — now goes through it. It was the one place in the package
|
|
401
|
+
that matched a marker string by hand, which is a second and fence-blind copy of this
|
|
402
|
+
module's own rule (`change-discipline.md` § One rail per concern).
|
|
403
|
+
|
|
404
|
+
**What d.319 deliberately did not do**, with the reason on record rather than as a silence:
|
|
405
|
+
|
|
406
|
+
- **`docs-region` is not retired.** Confirmation `biz-service-manifest` 002 §16.2 and 003
|
|
407
|
+
§3.3 require the descriptive lists of those three documents to be a rendered region fed by
|
|
408
|
+
the MANIFESTS; `sync-biz-facts.mjs` does not read a manifest (zero matches on `manifest` in
|
|
409
|
+
that file), so retiring this renderer would leave the confirmation unimplementable.
|
|
410
|
+
- **No `D-REGION` row over the biz repositories.** Measured 2026-09-14:
|
|
411
|
+
`grep -rn "BEGIN GENERATED" api_biz` returns nothing — not one of the eight trees carries a
|
|
412
|
+
region today, and these five regions live in `api/docs/**` in any case. A row checking
|
|
413
|
+
eight repositories for something none of them bears, and none of them should, is the gate
|
|
414
|
+
that matches nothing of `automation-gates.md` §5.
|
|
415
|
+
- **The two modules are still two.** They can become one the day this package is published:
|
|
416
|
+
the api gate runs `npm ci` and would import the RELEASED version, so importing it today
|
|
417
|
+
would tie a running gate to unreleased code. BIZ-DOCS recorded the same measurement and
|
|
418
|
+
offered the wiring (`api/shared/TODO.md`, their 2026-09-11 entry); this change makes the
|
|
419
|
+
merge possible by giving this module the behaviour the api one has. The divergence that
|
|
420
|
+
remains after that is the error contract — this module throws where the api one returns
|
|
421
|
+
`{error}` — and it is the caller's to absorb, in the change that deletes the copy.
|
|
422
|
+
|
|
423
|
+
### Fixed — `ci:gate:setup` builds the operator's schema, and refuses anybody's database (d.338)
|
|
424
|
+
|
|
425
|
+
Three defects in one path, all of them reported from services and none of them visible
|
|
426
|
+
from inside the gate's own green run.
|
|
427
|
+
|
|
428
|
+
**The order was a second definition of itself.** The real installation is built by
|
|
429
|
+
`api/scripts/apply-oagen-meta-install-sql.sh` with
|
|
430
|
+
`find … -name '*.sql' | LC_ALL=C sort -V`; `setupDatabase.js` used JavaScript `Array#sort`,
|
|
431
|
+
which is lexicographic. BIZ-META reported it on 2026-08-29 as latent, on the reasoning that
|
|
432
|
+
three-digit zero-padded prefixes sort identically either way. It is not latent: measured
|
|
433
|
+
2026-09-14 over `api_biz/property/migrations` (87 files), `999_layer_tick.sql` is applied
|
|
434
|
+
LAST by the operator and FIRST of the `999*` group by CI, because `_` (0x5F) precedes `a`
|
|
435
|
+
(0x61) byte for byte while version order compares the digit run first. That is exactly the
|
|
436
|
+
shape `.claude/rules/architecture-principles.md` § Version pinning names — CI proving one
|
|
437
|
+
artefact while the deployment runs another — and with migrations the order IS semantics.
|
|
438
|
+
|
|
439
|
+
`migrationOrder.js` is a port of what the tool actually does (GNU coreutils `filevercmp`
|
|
440
|
+
over gnulib `verrevcmp`), not an approximation that happens to agree on today's names, and
|
|
441
|
+
its expectations come from the tool's own output: the `@integration` case re-runs
|
|
442
|
+
`LC_ALL=C sort -V` during the suite and compares. The alternative — demanding the
|
|
443
|
+
`NNN_` convention `D-DB-NAMING` declares — was rejected because the nine `999a_`…`999i_`
|
|
444
|
+
files in property do not follow it, and a gate lands with compliance, never ahead of it
|
|
445
|
+
(`automation-gates.md` §3).
|
|
446
|
+
|
|
447
|
+
**A populated target was accepted.** `database.schema` is the REAL name (`oagen_emailer`,
|
|
448
|
+
the same string on the dev server and in production), so nothing about the name
|
|
449
|
+
distinguishes the CI sidecar from live data, and "does it already exist" cannot be the
|
|
450
|
+
criterion either — every biz `.gitlab-ci.yml` has the sidecar create it empty through
|
|
451
|
+
`MARIADB_DATABASE`. BIZ-emailer reported on 2026-09-04 that they had found nothing
|
|
452
|
+
blocking a `npm run ci:gate:setup` against dev; there was nothing. The criterion is the
|
|
453
|
+
premise the build already rests on: the set is proved by applying it to an EMPTY schema
|
|
454
|
+
(ADR 0006 §3). A target holding tables breaks it twice — the run proves nothing, because
|
|
455
|
+
what it would create is already there, and a declared seed carries no `IF NOT EXISTS`. So
|
|
456
|
+
the build now asks `information_schema` first and refuses, naming the schema, the server,
|
|
457
|
+
the count and how to get a legitimate target. A probe that cannot run, or answers nothing
|
|
458
|
+
countable, is a failure and never a pass.
|
|
459
|
+
|
|
460
|
+
**The charset flag had no test.** `--default-character-set=utf8mb4` has been on the client
|
|
461
|
+
invocation since the gate was written, and BIZ-invoicing confirmed on 2026-09-05 that the
|
|
462
|
+
installer is the clean path — but nothing held it there. `setupDatabaseLive.integration.test.js`
|
|
463
|
+
runs the real build against the dev MariaDB 10.5.17 in a throwaway schema it creates and
|
|
464
|
+
drops, and compares `HEX(note)` with the UTF-8 bytes of the source, so the answer does not
|
|
465
|
+
depend on the reading client either. Its control writes the same text through a latin1
|
|
466
|
+
client and requires the bytes to differ: without that, the case could pass on a leniently
|
|
467
|
+
configured server and prove nothing about the flag.
|
|
468
|
+
|
|
469
|
+
The executor contract gains one field, `capture`, which asks for the client's stdout in a
|
|
470
|
+
bare shape (`--batch --skip-column-names`). Only the emptiness probe uses it; a migration
|
|
471
|
+
produces nothing to read.
|
|
472
|
+
|
|
473
|
+
**And the message when no client is on PATH said the wrong reason.** It read "required
|
|
474
|
+
because migrations use DELIMITER"; BIZ-hello met it on 2026-09-14 in the first hello
|
|
475
|
+
pipeline to reach `ci:gate:setup` (job 2845685968) with no `DELIMITER` anywhere in its
|
|
476
|
+
migrations. The requirement and the fix were right, the reason was not — and a service
|
|
477
|
+
without triggers reads it as "this does not apply to me" and goes looking for a
|
|
478
|
+
`DELIMITER` that is not there. The client is required unconditionally, because every
|
|
479
|
+
statement goes through it, the first `CREATE DATABASE` included, and there is no driver
|
|
480
|
+
path; the message now says that (`architecture-principles.md` §5), with a case over its
|
|
481
|
+
text and a control that it no longer blames `DELIMITER`. Why the CLI rather than a driver
|
|
482
|
+
IS the DELIMITER story, and that stays where it belongs: the file's own header.
|
|
483
|
+
|
|
484
|
+
### Added — `F-DOCKERIGNORE`: a local production build stops copying live secrets into the image (d.316)
|
|
485
|
+
|
|
486
|
+
`templates/business-service/Dockerfile` line 6 is `COPY . .` and the template carried no
|
|
487
|
+
`.dockerignore` at all, so every service scaffolded from it was born with the defect
|
|
488
|
+
BIZ-hello measured on 2026-09-11: a LOCAL production image of hello came to **6.6 GB**,
|
|
489
|
+
5.2 GB of it `logs/`, and carried `config/env-active/*.env` with `DB_PASSWORD`,
|
|
490
|
+
`JWT_SECRET`, the MinIO keys and `RABBITMQ_URL`. Being ignored by git excludes nothing
|
|
491
|
+
from a docker context. The image CI builds is clean by accident — a fresh checkout has no
|
|
492
|
+
ignored file to copy — which is why the defect is invisible exactly where images are built.
|
|
493
|
+
|
|
494
|
+
The row is `F-GITIGNORE`'s shape (`contains`, severity `deploy`, fix = the sync) over the
|
|
495
|
+
SAME declaration, `templates/business-service/gitignore`: one list, two consumers
|
|
496
|
+
(`change-discipline.md` § One rail per concern). The derivation TRANSLATES rather than
|
|
497
|
+
copies, because the two formats disagree about the same line — a slash-less `.gitignore`
|
|
498
|
+
pattern matches at every depth, a `.dockerignore` one is matched from the context root, so
|
|
499
|
+
`*.log` there would catch `debug.log` and not `logs/debug.log`. Slash-less entries are
|
|
500
|
+
emitted in both shapes, anchored ones travel unchanged, a negation keeps its `!` in front
|
|
501
|
+
and its place, and `.git` is added — the one exclusion git never has to declare about
|
|
502
|
+
itself. The other trap hello named is avoided by construction: the derivation can never
|
|
503
|
+
exclude `tests/`, which Tier-1 of the boot reads at phase 0.2, because `.gitignore` does
|
|
504
|
+
not exclude it either.
|
|
505
|
+
|
|
506
|
+
The criterion is hello's comparison, not a list of directories, and it is now a test:
|
|
507
|
+
`dockerignoreBuildContext.integration.test.js` builds the template twice through docker
|
|
508
|
+
itself — `FROM scratch` + `COPY . /app` exported locally, so the matcher under test is
|
|
509
|
+
BuildKit's own — from a working tree carrying `logs/`, `config/env-active/`, `ci/` and
|
|
510
|
+
`node_modules/`, and from `git archive HEAD` of the same commit, and asserts the two file
|
|
511
|
+
lists are equal. Its failure path removes the file and asserts the secrets and logs come
|
|
512
|
+
back. With no docker daemon it says NOT RUN with the reason rather than passing.
|
|
513
|
+
|
|
514
|
+
Compliance landed with the row: the packaged template, the committed mirror
|
|
515
|
+
`api/templates/business-service` and every fixture carry the generated file.
|
|
516
|
+
|
|
517
|
+
RED→GREEN: `manifestDockerignore.test.js` 9 failed → 9 passed (99 suites, 1593 tests);
|
|
518
|
+
`dockerignoreBuildContext.integration.test.js` 2 passed against a live daemon;
|
|
519
|
+
`api/tests/scripts/add-service.bats` asserts a scaffolded service is born with both ignore
|
|
520
|
+
files, 68/68.
|
|
521
|
+
|
|
522
|
+
### Added — `R-PID1`: the process a biz container runs AS is part of the uniform (d.315)
|
|
523
|
+
|
|
524
|
+
Confirmation `biz-compose-pid1` 001 settled the substance on 2026-09-05 — `command:
|
|
525
|
+
["npm","start"]` makes npm process 1, and npm does not forward SIGTERM, so a stop is ten
|
|
526
|
+
seconds of waiting followed by SIGKILL through a service that never closed a channel — and
|
|
527
|
+
002 (2026-09-13) settled where the decision belongs: *"zaroven by to asi melo byt u vsech
|
|
528
|
+
biz servis reseno jednotne a tudiz by to melo byt soucasti uniformy"*. Until now it
|
|
529
|
+
travelled by being remembered: measured 2026-09-14 over the eight dev composes, **six still
|
|
530
|
+
say `npm`, two say `node`**, eight days after the decision.
|
|
531
|
+
|
|
532
|
+
`R-PID1` (section `runtime`, severity `deploy`, `doc` → `biz-compose-pid1.md`) reads the
|
|
533
|
+
service block of `docker-compose.yml` and reports anything that is not `node index.js`. It
|
|
534
|
+
accepts both spellings compose allows — `["node", "index.js"]` and `node index.js` — because
|
|
535
|
+
they are one decision written two ways, and a row grading the spelling would report a
|
|
536
|
+
service that is already right. An unreadable bracket form is a finding, not a pass. The
|
|
537
|
+
one-shot test runner is outside the row: it declares no command because `docker compose
|
|
538
|
+
run` supplies one, and its shape is `F-RUNNER`'s.
|
|
539
|
+
|
|
540
|
+
**Compliance landed with the row, not after it** (`automation-gates.md` §3): the packaged
|
|
541
|
+
template's own `docker-compose.yml` said `["npm","start"]` — every service
|
|
542
|
+
`add-service.sh --scaffold` had created since would have been born with the defect — and
|
|
543
|
+
now says `["node", "index.js"]`, with the reason beside it. The committed mirror and the
|
|
544
|
+
fixtures are regenerated in the same change. The eight live repositories switch in their
|
|
545
|
+
own 8.0.0 cascade commit, citing 001, the way confirmation `biz-service-manifest` 009
|
|
546
|
+
sequences `X-DB-CONFIG`; `R-PID1` ships in the same unreleased version, never ahead of it.
|
|
547
|
+
|
|
548
|
+
RED→GREEN: `tests/unit/manifestServiceConfig.test.js` 3 failed → 1580/1580 in 97 suites,
|
|
549
|
+
covering the finding, the service that declares no command at all, both spellings, the
|
|
550
|
+
unreadable form, and the CONTROL that the one-shot runner is never reported.
|
|
551
|
+
|
|
552
|
+
### Added — the biz `deploy-production` job runs the post-deploy gate of the target it declares (d.306)
|
|
553
|
+
|
|
554
|
+
Confirmation `deploy-gate-targets` 003 point 2. Until now the biz target of
|
|
555
|
+
`api/scripts/run-post-deploy-gate.sh` was called by nobody: measured 2026-09-10 over all
|
|
556
|
+
eight repositories and over this template, 0 callers. A deploy that ends at "the SSH step
|
|
557
|
+
returned 0" has confirmed that compose was told to start something, not that the service
|
|
558
|
+
came up — which is the false guarantee `automation-gates.md` §5 names, and it would have
|
|
559
|
+
made the `deploy-gate-restart-count` 001 decision (a restart loop is a FAIL) unreachable.
|
|
560
|
+
|
|
561
|
+
The `oa-ci v1` block's `deploy-production` job therefore:
|
|
562
|
+
|
|
563
|
+
- **declares its target** — `DEPLOY_GATE_TARGETS: "biz:__REGISTRY_NAME__"`. The gate has
|
|
564
|
+
no default; the job says which checks judge it (002). The name is the registry identity,
|
|
565
|
+
which is the key `api/config/services.json` and the contract file both use;
|
|
566
|
+
- **runs the gate after the SSH step**, in REMOTE mode
|
|
567
|
+
(`--biz-host "$DEPLOY_USER@$DEPLOY_HOST" --infra-host "$GATE_INFRA_USER@$GATE_INFRA_HOST"`),
|
|
568
|
+
out of the read-only `api` clone the uniform step already made beside the checkout. That
|
|
569
|
+
path is derived from `CI_PROJECT_DIR` rather than spelled a second time, so the layout
|
|
570
|
+
stays declared once, in `GIT_CLONE_PATH`;
|
|
571
|
+
- **carries a second SSH identity** — the gate-reader key for the infra box, in its own
|
|
572
|
+
`Host` block with `IdentitiesOnly yes`, so neither key is offered to the other box;
|
|
573
|
+
- **refuses BEFORE it installs or deploys anything** when `GATE_INFRA_HOST`,
|
|
574
|
+
`GATE_INFRA_USER` or `GATE_INFRA_SSH_PRIVATE_KEY` is empty, naming the missing ones and
|
|
575
|
+
the fix (`architecture-principles.md` §5). There is no branch that skips the gate
|
|
576
|
+
(`automation-gates.md` §1 requirement 5);
|
|
577
|
+
- **installs what the gate needs** on an alpine runner: `bash` (the script declares
|
|
578
|
+
`bash>=4` for `declare -A`), `jq`, `curl`. No `docker` — in remote mode every docker call
|
|
579
|
+
is carried to the box that holds the fact.
|
|
580
|
+
|
|
581
|
+
RED→GREEN: `api/tests/scripts/biz-deploy-uniform-gate.bats` 2 failed → 11/11, including
|
|
582
|
+
the failure path (no identity → exit 1 naming all three variables), the single-variable
|
|
583
|
+
case (the message names THAT one) and the CONTROL that a fully configured check says
|
|
584
|
+
nothing and gets out of the way. The 13 fixtures and the committed mirror
|
|
585
|
+
`api/templates/business-service` are regenerated in the same change.
|
|
586
|
+
|
|
587
|
+
### Added — `D-LINT`: the documentation tree is clean, not merely clean of five classes (d.303)
|
|
588
|
+
|
|
589
|
+
The documentation block named five classes — a localhost port, a dead script citation, a
|
|
590
|
+
missing npm script, a retired concept, a missing header — and stayed silent about every
|
|
591
|
+
other rule the lint raises. Measured 2026-09-09 over the eight repositories: **80 lint
|
|
592
|
+
findings, 16 of which fell to a row here.** A tree the documentation gate called broken
|
|
593
|
+
came back DEPLOYABLE from this uniform, which is the false guarantee `automation-gates.md`
|
|
594
|
+
§5 is about.
|
|
595
|
+
|
|
596
|
+
`D-LINT` (section `docs`, severity `deploy`, `doc` → `api/docs/biz/DOC-STANDARD.md`) says
|
|
597
|
+
the lint reports nothing over this service's tree. It **cites no rule id**: a citation
|
|
598
|
+
list here would have to be held equal to the linter's and would fall behind it the first
|
|
599
|
+
time BIZ-DOCS wrote a rule (confirmation `biz-service-manifest` 004 point 2 — the manifest
|
|
600
|
+
cites, it never restates). The five rows above keep their findings and their `fix`
|
|
601
|
+
sentences; one finding still lands on exactly one row, and this one takes what is left.
|
|
602
|
+
|
|
603
|
+
**The safeguard that lets it land before every tree is clean**: it counts only what the
|
|
604
|
+
LINT graded `error` — the set its own `--severity error` prints, read from the one run the
|
|
605
|
+
block already makes rather than bought with a second process. Grading belongs to BIZ-DOCS
|
|
606
|
+
(`api/config/biz-docs-lint.json` § severities), so a rule they add at `warn` is visible in
|
|
607
|
+
their run and blocks no deploy until they raise it on their own dated transition. Proved
|
|
608
|
+
with the real lint over the real fixture, in both directions: the same finding, graded
|
|
609
|
+
`error`, is counted; graded `warn`, it is not.
|
|
610
|
+
|
|
611
|
+
**Compliance landed with the gate, not after it** (`automation-gates.md` §3). The packaged
|
|
612
|
+
template's own documentation tree failed the lint on nine findings — no `docs/README.md`
|
|
613
|
+
and no `docs/80-setup/README.md`, so all three installation documents were orphans (S008,
|
|
614
|
+
L006 ×3), and all three `Owns:` lines claimed the same backticked `__REPO_NAME__` (M002 ×2,
|
|
615
|
+
M004 ×3), which is not the fact any of them owns. Every service `add-service.sh --scaffold`
|
|
616
|
+
had created since would have carried those nine. The template now ships both maps, the
|
|
617
|
+
three `Owns:` lines name what each node actually owns, and the tree measures 0 findings;
|
|
618
|
+
the committed mirror `api/templates/business-service` is regenerated in the same change.
|
|
619
|
+
`docs/README.md` joins the manifest's `own` class — the map of a tree is the service's, and
|
|
620
|
+
what the platform holds the tree to is `D-LINT`, not the template's prose.
|
|
621
|
+
|
|
622
|
+
Today's run from the api checkout over the eight services: **174 findings → 238**, of which
|
|
623
|
+
59 are `D-LINT` (invoicing 28, pdfgen 6, five services 5 each, property 0). The row is
|
|
624
|
+
therefore red for seven of eight and lands with the 8.0.0 cascade; the code is here now.
|
|
625
|
+
97 suites, 1577 tests green.
|
|
626
|
+
|
|
627
|
+
### Fixed — `C-SERVICE-DEAD`: `service.url` is dead FROM 8.0.0, and `wrapper.tenantContext` joins the list (d.304)
|
|
628
|
+
|
|
629
|
+
Two sentences in one row, both measured against the artefact a service is actually pinned
|
|
630
|
+
to rather than against this working tree.
|
|
631
|
+
|
|
632
|
+
- **`service.url` carries a date now.** The row's `why` said the key had "no reader
|
|
633
|
+
anywhere in the platform". The guard left the SOURCE in `c7ee2256`, but the PUBLISHED
|
|
634
|
+
`@onlineapps/service-wrapper@7.0.0` — every live service's pin — still throws
|
|
635
|
+
`Missing configuration - service.url is required` (measured 2026-09-14 in
|
|
636
|
+
`api_biz/invoicing/node_modules/@onlineapps/service-wrapper`). A rule telling an author
|
|
637
|
+
the key has no reader would have sent a service pinned to 7.0.0 into a failed boot
|
|
638
|
+
(BIZ-invoicing 2026-09-11 point 1). The finding now says it is dead from wrapper 8.0.0
|
|
639
|
+
and that the key travels WITH the pin — which is what confirmation `biz-service-port-url`
|
|
640
|
+
001 said all along ("v rámci pin kaskády"). Severity is unchanged: `deploy` names the
|
|
641
|
+
work the cascade carries and never stops a boot.
|
|
642
|
+
- **`wrapper.tenantContext` is a dead key the row did not know.** It was retired with
|
|
643
|
+
service shape v1.1 (`docs/biz/RETIRED-VOCABULARY.md`, confirmation
|
|
644
|
+
`service-shape-v11-retirement` 001) and its runtime defaults went in `b5e36431`;
|
|
645
|
+
`grep tenantContext api/shared/connector/service-wrapper/src` = 0 across 12 files. The
|
|
646
|
+
key stayed in five of eight live configs and in the packaged template, so the check
|
|
647
|
+
claiming "dead keys are refused" claimed more than it did.
|
|
648
|
+
- **The template no longer carries it.** `templates/business-service/config/service/config.json`
|
|
649
|
+
shipped `tenantContext.enabled` plus `requirePathPrefixes: ["/api/"]` and
|
|
650
|
+
`excludePathPrefixes: ["/health", "/metrics"]` — HTTP prefixes in a service with no HTTP
|
|
651
|
+
listener (ADR 0005), copied forward by every `add-service.sh --scaffold`. The block is
|
|
652
|
+
gone and the committed mirror `api/templates/business-service` is regenerated in the same
|
|
653
|
+
change (`oa-sync-template template --check` in sync).
|
|
654
|
+
|
|
655
|
+
The row's `why` and `DEAD_KEYS` are held equal by the test that already guards that pair,
|
|
656
|
+
so the enumeration cannot fall behind the list again. 97 suites, 1567 tests green.
|
|
657
|
+
|
|
658
|
+
### Added — `assertAllowedTenant()`: the tenant whitelist answers about any id, not only about the env (d.255)
|
|
659
|
+
|
|
660
|
+
`ALLOWED_TENANT_CLASSES` had exactly one consumer, inside its own file. A caller that
|
|
661
|
+
needed the other half of the same question — "may this script touch tenant X?" — had
|
|
662
|
+
nothing to call, so biz-property kept its own guard: `PLATFORM_LIVE_TENANT_ID`, one
|
|
663
|
+
refused id, every customer tenant from 101 up (101 = Meditest) through it. That is the
|
|
664
|
+
hole the whitelist replaced, surviving beside the whitelist because the whitelist was
|
|
665
|
+
unreachable (`api/shared/TODO.md`, "Od BIZ-PROPERTY (2026-09-11)").
|
|
666
|
+
|
|
667
|
+
```javascript
|
|
668
|
+
const target = assertAllowedTenant(args.tenant, { purpose: '--to-tenant', setBy: 'on the command line' });
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
- **Returns the id normalized** to an integer — a number and a string holding one both
|
|
672
|
+
arrive that way, the rail the env reader already used. Nothing else is coerced: `[99]`
|
|
673
|
+
stringifies to `"99"` and would have passed a bare `Number(String(x))`, so a type that
|
|
674
|
+
is neither number nor string is now refused by name.
|
|
675
|
+
- **One refusal, one place.** The sentence naming the allowed classes was written inline
|
|
676
|
+
in `getTestNamespace()`; it is rendered from one function now and `getTestNamespace()`
|
|
677
|
+
calls it. Writing it twice is how `97` stayed in the list for months after the
|
|
678
|
+
allocation retired it.
|
|
679
|
+
- **`purpose` / `setBy` belong to the caller**, because the fix does: an operator who
|
|
680
|
+
typed `--to-tenant 101` must not be sent to `config/env-active/shared.env`. The env
|
|
681
|
+
reader keeps its own "missing environment variable" sentence — six suites assert it
|
|
682
|
+
verbatim — while the empty-value condition, the integer check and the class refusal
|
|
683
|
+
stay one rail.
|
|
684
|
+
- **The class list is not exported.** A copy of a boundary is a second boundary; the
|
|
685
|
+
refusal names the classes so a caller never has to.
|
|
686
|
+
|
|
687
|
+
`getTestNamespace()` and `getForeignTestNamespace()` are unchanged, asserted by the
|
|
688
|
+
existing suite plus control cases in the new one: 97 suites, 1566 tests green.
|
|
689
|
+
|
|
690
|
+
### Fixed — a test that writes gets a directory of its own, outside the repository (d.233)
|
|
691
|
+
|
|
692
|
+
Two jest processes over this package shared one tree, so a suite went red without a line
|
|
693
|
+
of it changing. Measured 2026-09-11, before the change: `manifestServiceFiles` built
|
|
694
|
+
`tests/fixtures/manifest/service-workspace/api_biz/tmp-alpha-0` and a `manifestRun` running
|
|
695
|
+
beside it aggregated `F-RUNNER@api_biz/tmp-alpha-0/docker-compose.yml` into the union it
|
|
696
|
+
asserts; a second full run raised `[DeployContract] Service root not found -
|
|
697
|
+
…/api_biz/tmp-prod-onebyte` when one suite's `afterAll` removed what the other was reading;
|
|
698
|
+
`StandardLevels` and the two `serviceStructure*` suites wrote under fixed paths in `/tmp`
|
|
699
|
+
and lost them the same way; and a `.tmp-test-coverage-*` directory left by a killed run
|
|
700
|
+
crashed the jest-haste-map of every later run in the package. Two full runs side by side
|
|
701
|
+
ended 33 and 36 tests red.
|
|
702
|
+
|
|
703
|
+
- **One place a suite gets a writable directory**: `tests/helpers/tmpWorkspace.js` —
|
|
704
|
+
`mkdtemp` under `realpath(os.tmpdir())` (the engine compares roots through `realpath`,
|
|
705
|
+
d.227), a copy of a fixture workspace or of one fixture directory, and removal in
|
|
706
|
+
`afterAll`, which jest runs whether the cases passed or failed.
|
|
707
|
+
- **A temporary service repository can carry an installed jest**: `makeResolvableRoot`
|
|
708
|
+
writes `node_modules/jest` whose manifest names this package's real jest binary. The
|
|
709
|
+
test-coverage gate resolves the SERVICE's own jest from the repository root, and a
|
|
710
|
+
fixture is never `npm ci`-ed; the two alternatives are measured blind alleys, written out
|
|
711
|
+
where the function is.
|
|
712
|
+
- **Zero writes under `tests/fixtures/**`, measured** — `find tests/fixtures -newer <marker>`
|
|
713
|
+
answers nothing after two full runs side by side. The last of them was
|
|
714
|
+
`oaValidateCli.integration`, whose every root is now a copy that brings its own git
|
|
715
|
+
checkout: `X-IGNORED` asks git what a clone would receive, and a fixture inside the `api`
|
|
716
|
+
checkout borrows that answer from the repository it happens to sit in.
|
|
717
|
+
- **The completeness case changed its subject, deliberately.** `complete: true` means every
|
|
718
|
+
row ran, which needs the SSOTs and the sibling `api/scripts/ci` the documentation rows
|
|
719
|
+
read — so the case used to run over `tests/fixtures/manifest/conforming` and quietly
|
|
720
|
+
borrow the real workspace around it, the ambient dependence `automation-gates.md` §1.1
|
|
721
|
+
forbids, over a directory that is a SIBLING of the fixture workspace rather than a service
|
|
722
|
+
inside one. It measures `workspace-clean/api_biz/alpha` inside a workspace the suite builds
|
|
723
|
+
and declares, and the sentence is now true of the right subject.
|
|
724
|
+
|
|
725
|
+
Two full runs side by side end 97 suites and 1566 tests green on both sides, and
|
|
726
|
+
`find tests/fixtures -name '*tmp*'` answers nothing after them.
|
|
727
|
+
|
|
728
|
+
### `X-DB-CONFIG` cites the contract node, not the library README (d.254)
|
|
729
|
+
|
|
730
|
+
BIZ-DOCS wrote the prescriptive sentence into `docs/biz/70-contracts/database-contract.md`
|
|
731
|
+
§10 (`api@0b54101a`, owner decision biz-service-manifest 009), so the row's `doc` now
|
|
732
|
+
points at the concept that carries the rule — `doc-code-binding.md` §1.
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
### Changed — `workflow:` and `verify-installation-contract:` join the `oa-ci v1` block (d.251b)
|
|
736
|
+
|
|
737
|
+
Left outside the block by d.251 because the template did not carry them, not because anyone
|
|
738
|
+
had decided they were the service's. Measured over the eight repositories on 2026-09-11:
|
|
739
|
+
each of the two is BYTE identical in all eight (one sha apiece), and what they say is the
|
|
740
|
+
platform's — `workflow:` decides which pipelines run at all, and
|
|
741
|
+
`verify-installation-contract:` holds the installation document's SQL to the migrations.
|
|
742
|
+
So both are now written by the block and shipped in the template; no service loses a line,
|
|
743
|
+
because the text the block writes is the text all eight already carry.
|
|
744
|
+
|
|
745
|
+
`test:` is the only key left outside the markers, and the rule it stands on is sharper for
|
|
746
|
+
it: which database an integration needs, which `ci:gate:*` steps run and which artefact it
|
|
747
|
+
publishes are facts about the service. It was also the only key that actually differed
|
|
748
|
+
across the eight.
|
|
749
|
+
|
|
750
|
+
Evidence. Simulated on a copy of `api_biz/converter` outside the repository: `workflow:`,
|
|
751
|
+
`verify-installation-contract:` AND `test:` all byte-identical to what they were, nine root
|
|
752
|
+
keys, no duplicate, YAML parses, `test:` outside the block, second run `unchanged`, `G-CI`
|
|
753
|
+
clean. Live over the eight services: **8 of 8**, with the eight-key sentence.
|
|
754
|
+
|
|
755
|
+
Also in this change: `api/tests/scripts/biz-deploy-uniform-gate.bats` asserted "nothing
|
|
756
|
+
turns the gate off" over `sed -n '/^deploy-production:/,$p'` — a range that reached EOF.
|
|
757
|
+
That was the deploy job alone until the block landed; since d.251 it also covered the end
|
|
758
|
+
marker and this repository's own `test:` job. It now reads from the key to the next line at
|
|
759
|
+
column 0. Falsified both ways: `allow_failure: true` inside `deploy-production` makes the
|
|
760
|
+
case fail, the same line inside `test:` leaves it passing.
|
|
761
|
+
|
|
762
|
+
### Changed — `G-CI` is a platform BLOCK of `.gitlab-ci.yml`, not the whole file (d.251, finding 20)
|
|
763
|
+
|
|
764
|
+
The row landed in d.248 as `template-render` over the whole file, and its fix could not be
|
|
765
|
+
run: measured over all eight biz repositories on 2026-09-11, the template is 151 lines and
|
|
766
|
+
7 root keys while the services carry 176 to 198 lines and 9. Every difference is theirs —
|
|
767
|
+
the `test` job that stands up MariaDB, Redis and RabbitMQ as service containers, runs five
|
|
768
|
+
`ci:gate:*` steps and publishes `ci/integration-signal.json`, plus `workflow:` and
|
|
769
|
+
`verify-installation-contract:`. Running the sync would have deleted all of it and left
|
|
770
|
+
eight integration tiers running against no database: a finding a gate reports and nobody
|
|
771
|
+
can carry out (`automation-gates.md` §3, last bullet).
|
|
772
|
+
|
|
773
|
+
So `.gitlab-ci.yml` is split the way `docker-compose.yml` already is (`oa-test-runner v1`,
|
|
774
|
+
d.215d), one level up — a delimited block of top-level keys rather than one compose service:
|
|
775
|
+
|
|
776
|
+
- **The block `oa-ci v1`** carries `include:`, `stages:`, `variables:`, `build:`,
|
|
777
|
+
`secret_detection:` and `deploy-production:` — how a service is built, how its image is
|
|
778
|
+
identified (`R5`), how the digest reaches the deploy (`R1`, `R2`) and the uniform gate of
|
|
779
|
+
confirmation 008 before the SSH step. `stages:` declares all four stages, the service's
|
|
780
|
+
own jobs included; measured 2026-09-11, no biz service declares a fifth.
|
|
781
|
+
- **Outside the markers is the service's**, and the sync never rewrites a line of it: which database
|
|
782
|
+
and which gates its integration needs is a fact about the service. The block carries no
|
|
783
|
+
variable of a service either, so the sync cannot put back the `REGISTRY_URL` / `PORT` /
|
|
784
|
+
`SERVICE_URL` that d.244 removed from the biz side.
|
|
785
|
+
- **New check `delimited-block-render`** (`src/manifest/checks/serviceFiles.js`): the block
|
|
786
|
+
as the file carries it against the template's block RENDERED for this service — the
|
|
787
|
+
platform half names the service once, in `GIT_CLONE_PATH`. It is `template-block` plus
|
|
788
|
+
the render, and `template-render` minus the rest of the file.
|
|
789
|
+
- **The fix is a command again**: `npx oa-sync-template .gitlab-ci.yml --target .`. Three
|
|
790
|
+
states, and the third is the migration — a file with the block is spliced between its
|
|
791
|
+
markers; a file carrying the platform's keys UNMARKED (all eight, today) has exactly
|
|
792
|
+
those keys REPLACED by the block where the first of them stood, so no file ends with two
|
|
793
|
+
`build:` or two `deploy-production:` keys; an absent file is created whole, which is what
|
|
794
|
+
`--new` writes. Which keys the block claims is read from the block
|
|
795
|
+
(`serviceTemplate.js` § `topLevelKeys`), never from a list beside it. The migration moves
|
|
796
|
+
the service's own keys down past the block when they sat between the platform's — their
|
|
797
|
+
TEXT is untouched, and a mapping is unordered; a block that stayed in several pieces
|
|
798
|
+
would not be one region.
|
|
799
|
+
|
|
800
|
+
Evidence. RED over a copy of `api_biz/converter/.gitlab-ci.yml` normalised to the fixture
|
|
801
|
+
service (`tests/fixtures/ci-legacy/`): `carries no "oa-ci v1" block`. GREEN after the sync,
|
|
802
|
+
with `workflow:`, `test:` and `verify-installation-contract:` byte-identical to what they
|
|
803
|
+
were, nine root keys and no duplicate, and `G-CI` clean on the re-run. Simulated on a copy
|
|
804
|
+
of converter outside the repository: YAML parses, the three service keys unchanged, `build`
|
|
805
|
+
differing only in a comment, `deploy-production` gaining exactly the uniform gate of 008.
|
|
806
|
+
Live over the eight services: `G-CI` **8 of 8**, with the new sentence.
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
### Added — `G-CI`, `X-IGNORED`, and the manifest's completeness check over itself (d.248)
|
|
810
|
+
|
|
811
|
+
Three findings of the controller (`api/shared/TODO.md` 17, 18, 19) with one cause: nothing
|
|
812
|
+
held the manifest to the template, so the template could carry a file no row named, and a
|
|
813
|
+
service could `.gitignore` a file the uniform requires without a word being said.
|
|
814
|
+
|
|
815
|
+
- **The self-test (finding 18).** `tests/unit/templateCoverage.test.js` walks
|
|
816
|
+
`templates/business-service/**` and demands, for every file, either a row naming its path
|
|
817
|
+
or membership of the new class `files.own`. That list lives **in the manifest**
|
|
818
|
+
(`files.own.allowed`: `Dockerfile`, `index.js`, `package.json`, `src/`, `tests/`), never in
|
|
819
|
+
the test — an exemption written in a test is a second owner of the decision, and the next
|
|
820
|
+
file added would be exempted by editing the test. Adding a file to the template is now a
|
|
821
|
+
decision about who guards it. `Dockerfile` is the case that shows the class is not a
|
|
822
|
+
loophole: its one platform fact, the node major, is `R-NODE`'s row.
|
|
823
|
+
- **`G-CI`** (`files.generated`, check `template-render`, severity `deploy`, fix
|
|
824
|
+
`npx oa-sync-template .gitlab-ci.yml --target .`, doc confirmation
|
|
825
|
+
`biz-service-manifest` 008). Generated rather than identical because since d.246 the file
|
|
826
|
+
names the service once, in `GIT_CLONE_PATH`. The pipeline of a biz service is the
|
|
827
|
+
platform's: it runs the same suite, pins the deploy to the digest CI pushed (`R1`, `R2`,
|
|
828
|
+
`R5` read this file), and carries the step that measures the uniform before the SSH step —
|
|
829
|
+
a service that edits any of that edits the gate judging it.
|
|
830
|
+
- **`X-IGNORED`** (`files.tracked`, check `git-tracked`, severity `deploy`). Every file row
|
|
831
|
+
reads the WORKING TREE; what a clone, a CI checkout and the production image receive is
|
|
832
|
+
what git TRACKS. Measured 2026-09-10 on a copy of converter: `init.sh` and
|
|
833
|
+
`jest.config.js` in `.gitignore` and still on disk gave `F-INIT` and `F-JEST` **zero**
|
|
834
|
+
findings; the same two files deleted — the state a clean clone is in — gave both, NOT
|
|
835
|
+
DEPLOYABLE. Developer green, CI red. The row reads no list of its own: the paths are the
|
|
836
|
+
ones `files.identical`, `files.contains` and `files.generated` already name, a directory
|
|
837
|
+
among them covering its whole tree. Outside a git checkout it is NOT RUN with that reason,
|
|
838
|
+
never a pass — that is the state of a container and of an exported tarball.
|
|
839
|
+
- **`C-SERVICE-DEAD`** names `wrapper.registry.url` too (it entered `DEAD_KEYS` with d.244
|
|
840
|
+
and the row still listed four keys), and a test now holds the row's enumeration to the
|
|
841
|
+
list the code owns (`doc-code-binding.md` §1).
|
|
842
|
+
|
|
843
|
+
Evidence. Self-test RED on `.gitlab-ci.yml` (`placedBy` = `NOTHING`), GREEN 26/26 once
|
|
844
|
+
`G-CI` landed. `X-IGNORED` over a real git repository — a copy of the `conforming` fixture
|
|
845
|
+
with `init.sh` ignored: without the row `DEPLOYABLE, findings []`; with it
|
|
846
|
+
`NOT DEPLOYABLE, findings ["X-IGNORED@init.sh"]`. Live over the eight services on
|
|
847
|
+
2026-09-11: `G-CI` **8 of 8** (every one of them differs from the template at the same
|
|
848
|
+
place — `workflow:` where the template has `stages:`), `X-IGNORED` **0 of 8**, so that row
|
|
849
|
+
lands with compliance rather than ahead of it (`automation-gates.md` §3).
|
|
850
|
+
|
|
851
|
+
A trap measured on the way: `fs.existsSync` is the wrong question on a case-insensitive
|
|
852
|
+
filesystem. property carries `docs/80-setup/install.md` while `G-SETUP-INSTALL` names
|
|
853
|
+
`INSTALL.md`; the filesystem said yes, git's index said no, and the row reported two
|
|
854
|
+
untracked files that are tracked — hiding the real defect, which is the name the row does
|
|
855
|
+
not match. Paths are now verified segment by segment through `readdir`: a spelling no row
|
|
856
|
+
matches is ABSENT, and absence is the file row's own finding.
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
### Fixed — `S007` asks for the usage word of the LANGUAGE, not of the directory (d.249)
|
|
860
|
+
|
|
861
|
+
Reported by BIZ-ingest, 2026-09-11 (`api/shared/TODO.md`). The rule demanded `sourced` of
|
|
862
|
+
every file under `scripts/lib/` and `required` of every file under `scripts/ci/lib/` — the
|
|
863
|
+
tree those two lines were written against, not the tree there is. A Node library under
|
|
864
|
+
`scripts/lib/` could therefore go green only by declaring `sourced` about a file nothing
|
|
865
|
+
sources, which is the weakening `architecture-principles.md` §10 forbids. So the rule
|
|
866
|
+
moved; the files did not.
|
|
867
|
+
|
|
868
|
+
- The directory decides ONE thing: whether the question is asked at all. `scripts/lib/**`
|
|
869
|
+
and `scripts/ci/lib/**` hold libraries rather than entry points, so their `Usage:` line
|
|
870
|
+
carries a word instead of a command line. Which word is the extension's answer — `.sh`
|
|
871
|
+
is `sourced`, a Node module is `required`.
|
|
872
|
+
- The test is written as "shell, or else Node" rather than as a second list of Node
|
|
873
|
+
extensions beside `SCRIPT_EXTENSIONS`; a copy of that list here would drift
|
|
874
|
+
(`change-discipline.md` § One rail per concern). A `.cjs` is outside the scan today and
|
|
875
|
+
needs no edit here the day it enters.
|
|
876
|
+
- The message says what the file IS rather than where it lies: `A Node library declares
|
|
877
|
+
Usage: required, found "sourced"`. The id, the line and the `(SCRIPTS-STANDARD §2)`
|
|
878
|
+
citation are unchanged.
|
|
879
|
+
|
|
880
|
+
Measured 2026-09-11, the same trees before and after the change:
|
|
881
|
+
|
|
882
|
+
| tree | before | after |
|
|
883
|
+
|---|---|---|
|
|
884
|
+
| `api_biz/ingest` | 1 finding, `S007` | **0 findings** across 5 files |
|
|
885
|
+
| `api_biz/meta` | 7 findings, `S007` | **0 findings** across 12 files |
|
|
886
|
+
| `api_biz/converter` | 0 | 0 across 23 files |
|
|
887
|
+
| `api_biz/property` | 38 findings, none `S007` | 38, none `S007` — its scripts carry no header at all, so `S001` speaks first |
|
|
888
|
+
| `api/scripts` (`npm run scripts:lint`) | 0 across 105 files | 0 across 105 files |
|
|
889
|
+
| `templates/business-service` | 0 across 1 file | 0 across 1 file |
|
|
890
|
+
|
|
891
|
+
`api/tests/scripts/scripts-lint.bats` passes 18/18, exit 0 — including case 1, the
|
|
892
|
+
no-grandfathering gate over the real tree.
|
|
893
|
+
|
|
894
|
+
The row `S-SCRIPTS` of the biz uniform runs these rules, so ingest and meta stop being red
|
|
895
|
+
on it through no fault of their own. The standard's own §Scope rows still describe the old
|
|
896
|
+
split; the text for them went to INFRA-DOCS, who own `docs/standards/SCRIPTS-STANDARD.md`.
|
|
897
|
+
|
|
898
|
+
### Changed — `registry` odchází ze šablony služby, `wrapper.registry.url` je mrtvý klíč (d.244)
|
|
899
|
+
|
|
900
|
+
Konfirmace `biz-discovery-redis` 002. `templates/business-service/config/service/config.json`
|
|
901
|
+
už blok `registry` nenese: `@onlineapps/service-wrapper` adresu nevyžaduje ani
|
|
902
|
+
nepředává a `@onlineapps/conn-orch-registry` parametr `registryUrl` nemá.
|
|
903
|
+
|
|
904
|
+
`C-SERVICE-DEAD` (`config-dead-keys`) proto nově hlásí i `wrapper.registry.url`:
|
|
905
|
+
„nothing reads it; registration travels over MQ to registry.register and discovery
|
|
906
|
+
reads the Redis projection". Živě to je **8 nálezů z 8 služeb** — všech osm ten klíč
|
|
907
|
+
dnes v `config/service/config.json` deklaruje (změřeno 2026-09-11); zmizí s jejich
|
|
908
|
+
kaskádou 8.0.0, kde blok vypustí spolu s pinem wrapperu.
|
|
909
|
+
|
|
910
|
+
### Changed — `shared.env` šablony a fixtur přegenerován z `api/config/shared-env.json` (d.244)
|
|
911
|
+
|
|
912
|
+
Balíčková kopie i 13 fixtur pod `tests/fixtures/**` běhy `oa-sync-template shared-env`
|
|
913
|
+
(mechanismem, ne rukou). Zachycuje dvě změny manifestu: heslo v `REDIS_URL`
|
|
914
|
+
(INFRA `8d79478a`) a nové `why` u `REGISTRY_URL`. Fixtura
|
|
915
|
+
`service-workspace/api_biz/beta` **záměrně vynechána** — je to ručně rozjetá kopie,
|
|
916
|
+
na které `G-SHARED-ENV` dokazuje, že rozdíl umí najít.
|
|
917
|
+
|
|
918
|
+
### Added — the deploy-production job of a biz repository measures the complete uniform before its SSH step (d.246)
|
|
919
|
+
|
|
920
|
+
Confirmation `biz-service-manifest` 006 point 3, placed by 008: a biz service is deployed
|
|
921
|
+
only by its own repository's CI (`server-topology` 004), so the last place that can refuse
|
|
922
|
+
an unfit deploy is that repository's own `deploy-production` job — not
|
|
923
|
+
`api/scripts/deploy-production.sh`, which is not the biz deploy path at all.
|
|
924
|
+
|
|
925
|
+
- **`templates/business-service/scripts/verify-deploy-uniform.sh`** is the step, as a FILE
|
|
926
|
+
rather than a heredoc in the YAML. A heredoc can only ever be run by GitLab, so the one
|
|
927
|
+
place the gate would be proven is production; this file is called by the job with three
|
|
928
|
+
arguments and by `api/tests/scripts/biz-deploy-uniform-gate.bats` with three others, and
|
|
929
|
+
both run the same bytes. It reads nothing from the environment
|
|
930
|
+
(`architecture-principles.md` §1): the checkout, the api repository URL carrying the job
|
|
931
|
+
token, and the api ref production follows all arrive as arguments.
|
|
932
|
+
- It refuses on the engine's own non-zero exit **and** on `complete: false` — `deployable`
|
|
933
|
+
says nothing blocked among the rows that RAN, `complete` says no blocking row was
|
|
934
|
+
skipped, and reading only the first honours a skip nobody flagged
|
|
935
|
+
(`automation-gates.md` §1.5). No flag, variable or branch turns it off (§1 requirement
|
|
936
|
+
5), and messages print the clone URL with its userinfo cut out, so the job token never
|
|
937
|
+
reaches the log.
|
|
938
|
+
- **The layout is what makes the run complete, and the job asks GitLab for it.** Measured
|
|
939
|
+
2026-09-11 over a converter export against a shallow `api` clone: with the service at
|
|
940
|
+
`<root>/api_biz/<service>` beside `<root>/api` the run is `complete: true` in 1.1 s;
|
|
941
|
+
with the same two directories as unrelated siblings, seven blocking rows come back NOT
|
|
942
|
+
RUN — `U-ORPHAN`, `U-IDENTITY`, `D-PORT`, `D-NPM`, `D-SCRIPT`, `D-RETIRED`, `D-HEADER` —
|
|
943
|
+
and the signal reads `complete: false`. That is the same seven the container simulation
|
|
944
|
+
of 006 measured. A symlink is forbidden (api rule: No Symlinks) and a second copy of the
|
|
945
|
+
repository would leave two trees with no way to say which one the verdict was about, so
|
|
946
|
+
the job sets `GIT_CLONE_PATH: $CI_BUILDS_DIR/oa-uniform/api_biz/__SERVICE_NAME__` and the
|
|
947
|
+
script verifies it got what it asked for.
|
|
948
|
+
- The job runs on `node:24-alpine` (the major `R3` compares against `engines.node` and the
|
|
949
|
+
Dockerfile), adds `git`, and runs `npm ci` — the engine is the one this repository's pin
|
|
950
|
+
installed, never a floating `npx`.
|
|
951
|
+
- `EXECUTABLE_FILES` in `src/sync/serviceTemplate.js` gains the script: a rendered copy
|
|
952
|
+
without the bit fails the job with "permission denied" instead of measuring anything,
|
|
953
|
+
which is the false guarantee `automation-gates.md` §5 names. The packaged template is
|
|
954
|
+
now **23** files, and `tests/unit/serviceTemplate.test.js` names the 23rd rather than
|
|
955
|
+
only counting it.
|
|
956
|
+
|
|
957
|
+
`api/tests/scripts/biz-deploy-uniform-gate.bats` — 9 cases, RED 8/9 against the tree
|
|
958
|
+
before this change (the ninth is skipped in an api-only checkout), GREEN 9/9 after: the
|
|
959
|
+
step stands before the ssh line; the job asks for the layout and the committed mirror
|
|
960
|
+
carries the same bytes; nothing turns the gate off; each precondition refuses with its fix
|
|
961
|
+
and without the token; THE REAL RUN over a converter export comes back `complete: true`
|
|
962
|
+
with `notRun` empty and its 33 findings refuse the deploy; an incomplete run is refused
|
|
963
|
+
even when nothing blocked; and the CONTROL case — only a signal that is `deployable` AND
|
|
964
|
+
`complete` passes.
|
|
965
|
+
|
|
966
|
+
**UNCERTAIN, and it cannot be measured from here:** whether a real GitLab pipeline reaches
|
|
967
|
+
`complete: true`. Two things this simulation cannot answer — whether the runner allows a
|
|
968
|
+
custom build directory (`[runners.custom_build_dir] enabled`), and whether the api
|
|
969
|
+
project's CI job-token allowlist admits the biz project (the owner's step, 008 point 2).
|
|
970
|
+
The clone step's own message names the second one when it fails.
|
|
971
|
+
|
|
972
|
+
**What this change does NOT carry:** no manifest row covers `.gitlab-ci.yml` — measured
|
|
973
|
+
over the manifest, zero rows name it — so the step reaches none of the eight services by
|
|
974
|
+
`oa-sync-template`, and none of them is reported as drifting from it. A new service
|
|
975
|
+
created by `oa-sync-template --new` gets it; an existing one does not.
|
|
976
|
+
|
|
977
|
+
### Changed — the biz box's `shared.env` is rendered on the infra box, not written by role-biz (d.247)
|
|
978
|
+
|
|
979
|
+
Two sentences of the template — the host-contract comment in
|
|
980
|
+
`docker-compose.production.yml` and the production paragraph of `README.md` — said the one
|
|
981
|
+
host-level `/var/www/html/oadrive/api_biz/shared.env` is written by `role-biz`. Since
|
|
982
|
+
`843b0ba9` it is rendered on the INFRA box by
|
|
983
|
+
`api/scripts/provision/render-biz-shared-env.sh` from that box's own `env-active` and
|
|
984
|
+
carried over by the operator (`server-topology` 004, 005); `role-biz.sh` mentions
|
|
985
|
+
`shared.env` nowhere (0 occurrences) and creates only the `biz_network`, which those texts
|
|
986
|
+
still say and which is still true. The sentence was cascaded by the generator into the
|
|
987
|
+
eight fixtures that are conforming copies of the template; the four deliberately drifted
|
|
988
|
+
ones and `beta` were left as they are, because the sync would erase the defect they exist
|
|
989
|
+
to demonstrate.
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
### Added — `X-DB-CONFIG`: the private connection factory is a forbidden file (d.242)
|
|
993
|
+
|
|
994
|
+
Confirmation `biz-service-manifest` 009: `@onlineapps/conn-base-db` is kept and its
|
|
995
|
+
cascade finished. Seven of the eight services carried their own
|
|
996
|
+
`src/config/database.js` — a module singleton building its own Sequelize — beside the
|
|
997
|
+
library whose `createSequelize` exists to be the one connection factory
|
|
998
|
+
(`change-discipline.md` § One rail per concern). The 8.0.0 cascade commit of each service
|
|
999
|
+
switches its importers and deletes the file; this row is what keeps it deleted, and it
|
|
1000
|
+
ships in the release that does the switching, never ahead of it (`automation-gates.md`
|
|
1001
|
+
§3).
|
|
1002
|
+
|
|
1003
|
+
- **`X-DB-CONFIG`** (`files.forbidden`, check `file-absent`, severity `deploy`,
|
|
1004
|
+
`scope: service`) — path `src/config/database.js`, `doc`
|
|
1005
|
+
`api/shared/connector/conn-base-db/README.md`. No new check and no new class: the
|
|
1006
|
+
"this file must not exist" class is `files.forbidden`, which `X-HOOKS` and `X-PREVAL`
|
|
1007
|
+
already wear, and `file-absent` reads nothing but the path. Like them it answers inside
|
|
1008
|
+
a service container, with no workspace and no `api/` checkout.
|
|
1009
|
+
- The row does **not** ask `requiredConnectors.db` first. 009 names the path with no
|
|
1010
|
+
condition on it, and a service that declared no database has even less business carrying
|
|
1011
|
+
a connection factory. The three `D-DB-*` rows that DO ask the declaration are a different
|
|
1012
|
+
concern and are unchanged.
|
|
1013
|
+
|
|
1014
|
+
Live, 2026-09-11, `runManifest` per service against the workspace — 8 services, **7
|
|
1015
|
+
findings**, `pdfgen` silent:
|
|
1016
|
+
|
|
1017
|
+
| service | `requiredConnectors.db` | `src/config/database.js` | `X-DB-CONFIG` |
|
|
1018
|
+
|---|---|---|---|
|
|
1019
|
+
| converter | true | present | 1 |
|
|
1020
|
+
| emailer | true | present | 1 |
|
|
1021
|
+
| hello-service | true | present | 1 |
|
|
1022
|
+
| ingest | true | present | 1 |
|
|
1023
|
+
| invoicing | true | present | 1 |
|
|
1024
|
+
| meta | true | present | 1 |
|
|
1025
|
+
| pdfgen | false | absent | 0 |
|
|
1026
|
+
| property | true | present | 1 |
|
|
1027
|
+
|
|
1028
|
+
`pdfgen` is the control case in both halves: it declares no database AND carries no file,
|
|
1029
|
+
and the row stays silent without reading the declaration at all. The mirror
|
|
1030
|
+
`api/templates/business-service` carries no `src/config/database.js` either, so the row
|
|
1031
|
+
raises nothing over it.
|
|
1032
|
+
|
|
1033
|
+
The workspace run (`oa-validate --workspace <root>`, no service root) reports the row
|
|
1034
|
+
`NOT RUN`, beside `X-HOOKS` and `X-PREVAL` — a service-scoped row has no service to read
|
|
1035
|
+
there, and NOT RUN is printed rather than counted as a pass.
|
|
1036
|
+
|
|
1037
|
+
### Added — `S-SCRIPTS`, and the script-header rules move into this package (d.218)
|
|
1038
|
+
|
|
1039
|
+
Confirmation `biz-service-manifest` 003 §19: "`scripts/**` of every bearer passes
|
|
1040
|
+
`lint-scripts.mjs`, **shipped in the validator package**". The second half was the whole
|
|
1041
|
+
requirement. The rules lived in `api/scripts/ci/lint-scripts.mjs`, a file of ONE checkout —
|
|
1042
|
+
and a service has no `api/` directory in its own CI and none at all inside its image, so
|
|
1043
|
+
the row could only ever have been NOT RUN exactly where it has to answer
|
|
1044
|
+
(`automation-gates.md` §5).
|
|
1045
|
+
|
|
1046
|
+
- **`src/lint/scripts/lintScripts.js`** carries the rules. Every id, message and closed
|
|
1047
|
+
vocabulary is unchanged: `api/tests/scripts/scripts-lint.bats` passes 18/18, and over the
|
|
1048
|
+
api tree the two runs print the same line (0 findings across 104 files in scope, 97 in
|
|
1049
|
+
the citation scope).
|
|
1050
|
+
- **`oa-lint-scripts`** is the command; it owns argument parsing, the report and the exit
|
|
1051
|
+
code, nothing else.
|
|
1052
|
+
- **`api/scripts/ci/lint-scripts.mjs` is a thin caller** of the same module — the shape
|
|
1053
|
+
`api/scripts/ci/verify-lib-compat.mjs` already keeps. `npm run scripts:lint` and the CI
|
|
1054
|
+
job `validate-scripts` are unchanged.
|
|
1055
|
+
- **`S-SCRIPTS`** (new section `tooling`, severity `deploy`, `scope: service`) is the row;
|
|
1056
|
+
`src/manifest/checks/scriptHeaders.js` is the bridge to the module and decides nothing.
|
|
1057
|
+
|
|
1058
|
+
One rule changed behaviour, and it had to. `S006` checks that an `@see` target exists, and
|
|
1059
|
+
an `api/…` target used to be resolved by stripping the prefix and looking under the run's
|
|
1060
|
+
own root — correct exactly once, when the run IS the api checkout. In a service repository
|
|
1061
|
+
that would have measured `api/docs/standards/…` against `<service>/docs/standards/…` and
|
|
1062
|
+
reported a dead pointer that is not dead. The api checkout is now a PARAMETER: given, the
|
|
1063
|
+
target is resolved against it; absent, the lint returns it as UNRESOLVED and the row is
|
|
1064
|
+
NOT RUN with the targets named, never a silent pass. Measured 2026-09-10 over the eight
|
|
1065
|
+
services: 47 `@see` lines under their scripts directories, 4 with that prefix.
|
|
1066
|
+
|
|
1067
|
+
Live, 2026-09-10: **86 scripts, 86 findings** — not one carries a header (converter 23,
|
|
1068
|
+
property 38, meta 13, ingest 6, hello-service 2, invoicing 2, emailer 1, pdfgen 1). The
|
|
1069
|
+
mirror `api/templates/business-service` stays DEPLOYABLE with 0.
|
|
1070
|
+
|
|
1071
|
+
### Added — the `db` section says what the SQL package is called and who reaches it (d.218)
|
|
1072
|
+
|
|
1073
|
+
§19 names three duties beside "is there a database" and leaves the naming convention to be
|
|
1074
|
+
chosen. All three ask their question only of a repository that DECLARES a database; a
|
|
1075
|
+
stateless service is silent, and `pdfgen` is the measured case.
|
|
1076
|
+
|
|
1077
|
+
- **`D-DB-NAMING`** — `migrations/*.sql` reads `NNN_<subject>.sql`: three digits, then at
|
|
1078
|
+
least two snake_case words (chosen by BIZ-general, d.218). No leading verb is demanded,
|
|
1079
|
+
because the measured counter-example is right —
|
|
1080
|
+
`converter/migrations/009_cnv_batch_drop_pending_status.sql` opens with a table prefix;
|
|
1081
|
+
and the three digits are not decoration — `999a_` sorts after `999_` only by accident of
|
|
1082
|
+
the byte after the digits. The convention reaches ONE level: what `BASELINE/` and
|
|
1083
|
+
`SEED/**` must carry is the installation contract's §4, which `D-DB-HEADERS` already
|
|
1084
|
+
checks, and that contract names no shape for their file names at all — measured, not
|
|
1085
|
+
assumed, so no regex is invented where nothing owns one.
|
|
1086
|
+
- **`D-DB-README`** — the installation contract §3 asks for four things and
|
|
1087
|
+
`SQL_PACKAGE` (`D-DB-PACKAGE`) checks three directories. The fourth is
|
|
1088
|
+
`migrations/README.md`, the documented execution order of §5 point 4, and nothing was
|
|
1089
|
+
looking for it.
|
|
1090
|
+
- **`D-DB-ACCOUNT`** — `DB_USER` in this service's env template reads `oagen_<shortname>`,
|
|
1091
|
+
from the SAME derivation `C-IDENTITY` holds `database.schema` to, so the account and the
|
|
1092
|
+
schema cannot drift into two spellings (confirmation `db-accounts-per-service` 001: root
|
|
1093
|
+
stops being an operational identity — it was the identity of nine containers across eight
|
|
1094
|
+
repositories, which made rotating it a cross-repo batch nobody could run). A repository
|
|
1095
|
+
that declares a database and no `DB_USER` is a finding, not a silence.
|
|
1096
|
+
|
|
1097
|
+
Live, 2026-09-10: `D-DB-NAMING` 12 (emailer, invoicing, meta one each — `000_baseline.sql`;
|
|
1098
|
+
property nine, eight of them the `999a`–`999h` ordinals plus a single-word `050_views.sql`),
|
|
1099
|
+
`D-DB-README` 0, `D-DB-ACCOUNT` 4 (ingest, invoicing, property still `DB_USER=root`; meta
|
|
1100
|
+
`oagen_meta_app`). The mirror stays DEPLOYABLE with 0.
|
|
1101
|
+
|
|
1102
|
+
### Added — `F-GITIGNORE` and the `contains` file class (d.218, finding of d.240)
|
|
1103
|
+
|
|
1104
|
+
`ci/deployability.json` is written by `oa-validate` and by the pre-push hook of
|
|
1105
|
+
confirmation 006. Four of the eight services (converter, invoicing, meta, property) do not
|
|
1106
|
+
ignore `ci/`, so every run leaves an untracked file behind; the template has carried the
|
|
1107
|
+
line since d.212.
|
|
1108
|
+
|
|
1109
|
+
The class is `contains`, and the choice was measured rather than preferred. `identical` is
|
|
1110
|
+
impossible — a repository ignores its own scratch directories, and the eight services carry
|
|
1111
|
+
nine different files (d.220). A delimited `# --- oa-ignore v1` block was refused for the
|
|
1112
|
+
opposite reason: not one of the eight files carries a marker today and no generator ever
|
|
1113
|
+
wrote one, so a block row would report eight services for a defect four of them do not
|
|
1114
|
+
have, and would impose an ordering on lines that are legitimately theirs. What has to be
|
|
1115
|
+
true here is a SET of paths — every path this platform WRITES into a repository is a path
|
|
1116
|
+
git must not see — and a set is what the row compares: the template's entries against the
|
|
1117
|
+
repository's, in any order, every other line untouched.
|
|
1118
|
+
|
|
1119
|
+
A narrower pattern does not satisfy the entry it narrows, and that is not pedantry:
|
|
1120
|
+
property's `config/env-active/*.env` leaves every non-`.env` file in a directory of live
|
|
1121
|
+
secrets committable.
|
|
1122
|
+
|
|
1123
|
+
The generator still writes the labelled block — that is how a reader tells the platform's
|
|
1124
|
+
lines from the repository's own — but the block is the FIX's shape, never the check's. The
|
|
1125
|
+
renderer removes an existing block before computing what is missing, so it is idempotent:
|
|
1126
|
+
a second run adds nothing, and an entry the repository has since written by hand drops out
|
|
1127
|
+
of the block instead of standing beside it twice. `SYNCABLE_CLASSES` gains `contains`.
|
|
1128
|
+
|
|
1129
|
+
Live, 2026-09-10: 4 services, 5 findings. The mirror stays DEPLOYABLE with 0.
|
|
1130
|
+
|
|
1131
|
+
### Changed — `C-IDENTITY` holds the sixth spelling: `serviceName` (d.218)
|
|
1132
|
+
|
|
1133
|
+
`config/service/integration-contract.json` opens with `serviceName`, and it is the first
|
|
1134
|
+
file every platform gate reads. The row held five spellings after d.236 and not this one.
|
|
1135
|
+
It is compared against what `config/service/config.json` DECLARES, character for
|
|
1136
|
+
character — the same reading the npm name gets, and for the same reason: whether
|
|
1137
|
+
`service.name` itself must read `biz-<shortname>` is a question no row owns, and this is
|
|
1138
|
+
not the place a rule appears as a side effect (`truth-over-agreement.md` §6). A contract
|
|
1139
|
+
declaring no `serviceName` is `C-CONTRACT`'s finding, which calls the validator owning
|
|
1140
|
+
every rule of that file.
|
|
1141
|
+
|
|
1142
|
+
Live, 2026-09-10: all eight services already agree, so no finding moved — the gate lands
|
|
1143
|
+
with compliance (`automation-gates.md` §3). What the row DID find is that the template
|
|
1144
|
+
carried two placeholders for one name: `__REGISTRY_NAME__` in `config/service/config.json`
|
|
1145
|
+
and `__REPO_NAME__` in the contract. Both render to `biz-<name>` for any real service, so
|
|
1146
|
+
nothing changes downstream, but the one bearer that is the shape of all the others wore two
|
|
1147
|
+
spellings; `serviceName` is the platform identity, so the contract now writes
|
|
1148
|
+
`__REGISTRY_NAME__`. The same correction landed in the conformant fixture, whose contract
|
|
1149
|
+
said `biz-v3-cookbook-service` while its `package.json`, its `config.json` and every
|
|
1150
|
+
cookbook step said `v3-cookbook-service`.
|
|
1151
|
+
|
|
1152
|
+
### Fixed — a suite copying the fixture workspace no longer races the suites writing into it (d.218)
|
|
1153
|
+
|
|
1154
|
+
`manifestServiceFiles` and `manifestProductionCompose` create and delete temporary
|
|
1155
|
+
services under `api_biz/tmp-*` INSIDE the shared fixture workspace, and jest runs suites in
|
|
1156
|
+
parallel — so the three suites that copy that workspace whole walked a directory which can
|
|
1157
|
+
vanish between the readdir and the copy. Measured 2026-09-10: `ENOENT …
|
|
1158
|
+
api_biz/tmp-prod-onebyte`, in a suite that had been green twice before it appeared. The
|
|
1159
|
+
copy now filters those names out, and `cpSync` applies `filter` BEFORE it stats the entry
|
|
1160
|
+
(measured), so the window is closed rather than narrowed.
|
|
1161
|
+
|
|
1162
|
+
### Added — one service, one name: `C-IDENTITY` and `U-IDENTITY` (d.236)
|
|
1163
|
+
|
|
1164
|
+
`api/docs/biz/60-templates/naming.md` is normative and derives every spelling of a biz
|
|
1165
|
+
service from one short name. It is not a matter of tidiness: the gateway compares
|
|
1166
|
+
`service_code` from the entitlement data against the service name in a cookbook step
|
|
1167
|
+
CHARACTER BY CHARACTER, so a second spelling of one service is a request that gets
|
|
1168
|
+
refused. Measured 2026-09-10 over the eight live services: seven wear one name each, and
|
|
1169
|
+
`hello-service` wore four — directory `hello-service`, repository `biz-hello-service`, env
|
|
1170
|
+
template `hello-service.env`, everything else `hello`. No row said a word, and the
|
|
1171
|
+
standard's own § Current State had recorded the divergence as a fact rather than a defect.
|
|
1172
|
+
|
|
1173
|
+
**`C-IDENTITY`** (section `config`, severity `deploy`, `scope: bearer`, no workspace
|
|
1174
|
+
needed) holds the spellings INSIDE the repository to the short name
|
|
1175
|
+
`config/service/config.json` declares: the npm `name`, `container_name` in both compose
|
|
1176
|
+
files, `config/env-templates/<shortname>.env` together with the `env_file` entries loading
|
|
1177
|
+
it, and `database.schema` = `oagen_<shortname>` where the repository declares a database.
|
|
1178
|
+
It reads nothing outside the repository, so it answers in a container and in the service's
|
|
1179
|
+
own CI too.
|
|
1180
|
+
|
|
1181
|
+
**`U-IDENTITY`** (beside `U-ORPHAN`, `scope: bearer`) holds `directory`, `repo` and
|
|
1182
|
+
`container` in `api/config/services.json` to the same short name; those three are facts of
|
|
1183
|
+
the workspace, so without one the row is NOT RUN.
|
|
1184
|
+
|
|
1185
|
+
Four decisions the row makes and states:
|
|
1186
|
+
|
|
1187
|
+
- the npm `name` is compared against the DECLARED `service.name`, not against a derived
|
|
1188
|
+
`biz-<shortname>` — `naming.md` gives both the same spelling, and comparing against the
|
|
1189
|
+
derivation would smuggle in a rule about `service.name` itself that no row owns
|
|
1190
|
+
(`serviceIdentity.js` § `REGISTERED_NAME`);
|
|
1191
|
+
- only the LAST segment of `repo` is this row's business; the organisation prefix is
|
|
1192
|
+
`naming.md`'s, and a repository moved between groups is not a second name;
|
|
1193
|
+
- the database name is read where the repository declares it —
|
|
1194
|
+
`config/service/integration-contract.json` → `database.schema` (7 of 8 services;
|
|
1195
|
+
`pdfgen` is a stateless renderer and declares none, so the row stays silent);
|
|
1196
|
+
- a bearer the SSOT knows under neither a name nor a directory is `U-ORPHAN`'s finding and
|
|
1197
|
+
`U-IDENTITY` says nothing — one defect, one owner.
|
|
1198
|
+
|
|
1199
|
+
The template is a bearer of every file row and of neither SSOT row: it declares its
|
|
1200
|
+
identity as the placeholder, and `api/config/services.json` lists services, of which the
|
|
1201
|
+
template is not one. It is held to `C-IDENTITY` and passes — which took fixing the
|
|
1202
|
+
template itself (d.220 decision 4): its compose files loaded
|
|
1203
|
+
`env-active/__SERVICE_NAME__.env` while the file was called
|
|
1204
|
+
`config/env-templates/service.env`, so the one bearer that is the shape of all the others
|
|
1205
|
+
wore two names. The source is now `__SERVICE_NAME__.env`; the `--new` rename is unchanged.
|
|
1206
|
+
|
|
1207
|
+
### Fixed — `template --check` reads the template's own `.gitignore` (d.237)
|
|
1208
|
+
|
|
1209
|
+
`oa-validate` records its verdict in the tree it measured (d.232), so a run over the
|
|
1210
|
+
mirror `api/templates/business-service` leaves `ci/deployability.json` behind. The
|
|
1211
|
+
template gitignores `ci/`, so git does not see it — but `oa-sync-template template
|
|
1212
|
+
--check` did, and reported the mirror stale over the artefact of a legitimate run.
|
|
1213
|
+
|
|
1214
|
+
One rule, read from one place: what the template's `.gitignore` ignores, the comparison
|
|
1215
|
+
ignores too, on both sides. `compileGitignore()` reads git's own semantics in the part
|
|
1216
|
+
these files use — a trailing `/` covers directories only, a pattern carrying a `/` is
|
|
1217
|
+
anchored at the root and one without matches a name at any depth, a matched directory
|
|
1218
|
+
carries everything under it, and the LAST matching rule decides, which is what makes a `!`
|
|
1219
|
+
exception work. `listOutputFiles()` is that filter over `listTemplateFiles()`, and the
|
|
1220
|
+
mirror suite reads it too — it carried the same defect in its "carries exactly the files
|
|
1221
|
+
the packaged template renders" assertion. A file the `.gitignore` does not cover is still
|
|
1222
|
+
reported stale, and an ignored path never hides a real drift in a tracked file.
|
|
1223
|
+
|
|
1224
|
+
### Changed — `R-NODE` reads the platform major from the package's own `engines` (d.238)
|
|
1225
|
+
|
|
1226
|
+
The row referenced `api/.nvmrc`, a file that exists only in a workspace, so in a service
|
|
1227
|
+
container and in the service's own CI — the two places a service is actually built — the
|
|
1228
|
+
one row that catches a service quietly living on an older major reported NOT RUN. (`R3`
|
|
1229
|
+
of the deploy contract compares CI, Dockerfile and `engines.node` only against each other,
|
|
1230
|
+
so a service consistently on Node 22 is green there.)
|
|
1231
|
+
|
|
1232
|
+
The reference is now `{ "package": "package.json", "text": true }` — the fourth reference
|
|
1233
|
+
shape of d.229 — and the check reads `engines.node` from it. It is not a second copy of
|
|
1234
|
+
the number: the library uniform's `L-ENGINES` fails any package whose `engines.node`
|
|
1235
|
+
disagrees with `api/.nvmrc`, so this is a checked projection of the SSOT one hop further
|
|
1236
|
+
along, and it travels with the pin. `api/.nvmrc` stays the owner and `L-ENGINES` still
|
|
1237
|
+
reads it. Owner decision: `api/docs/governance/confirmations/biz-service-manifest.md`
|
|
1238
|
+
§ Confirmation 20260910-biz-service-manifest-006, point 2.
|
|
1239
|
+
|
|
1240
|
+
Measured over a service copy outside any checkout, with the package installed from a
|
|
1241
|
+
`npm pack` tarball: NOT RUN shrank to `U-ORPHAN` and the five `D-*` rows, and `R-NODE`
|
|
1242
|
+
reported both places the service states its major.
|
|
1243
|
+
|
|
1244
|
+
### Fixed — `L-CONNECTOR-ENV` asks its question of code, not of prose (d.239)
|
|
1245
|
+
|
|
1246
|
+
The check matched `process.env` in the raw file text, so a module that DOCUMENTS the duty
|
|
1247
|
+
it obeys — "this module reads no `process.env`" — was reported for the sentence in its own
|
|
1248
|
+
header. Measured: `api/shared/connector/conn-base-db/src/createSequelize.js`.
|
|
1249
|
+
|
|
1250
|
+
The file is now read as source: `codeOnly()` blanks comments, the text of string and
|
|
1251
|
+
template literals, and regular-expression literals before the search, keeping length and
|
|
1252
|
+
line breaks. What a template literal interpolates is code again, because
|
|
1253
|
+
`${process.env.X}` really does read the environment. The tokenizer is written by hand
|
|
1254
|
+
rather than pulled from `acorn`: that package is not a declared dependency, and adding a
|
|
1255
|
+
runtime dependency for one line-level question would change what every biz service
|
|
1256
|
+
installs (§ Shared Packages, "a package sees only what it declares", gate rule G6).
|
|
1257
|
+
|
|
1258
|
+
|
|
1259
|
+
### Changed — the production compose of a biz service is a generated file, not a shape (d.234)
|
|
1260
|
+
|
|
1261
|
+
`G-PROD` was a bridge to requirement **R1** of the deploy contract: it asked one question
|
|
1262
|
+
about `docker-compose.production.yml` — is the image pinned by digest — and said nothing
|
|
1263
|
+
about the file the rest of the platform depends on. That left the topology of the
|
|
1264
|
+
production compose maintained by hand in eight repositories at once, which is how seven of
|
|
1265
|
+
them came to name `api_network` and `gendb-network`: networks the biz box does not have.
|
|
1266
|
+
|
|
1267
|
+
The row is now a `generated` whole-file row (`check: template-render`), rendering
|
|
1268
|
+
`templates/business-service/docker-compose.production.yml` with the one parameter a service
|
|
1269
|
+
has, its DECLARED name (`config/service/config.json` → `service.name`, through
|
|
1270
|
+
`deriveParams`). The measurement the owner decided on is what makes that possible: on
|
|
1271
|
+
2026-09-10 the seven old-shape files were byte-identical once the service name was
|
|
1272
|
+
normalised, apart from `memory: 384M` in ingest and property — so what looked like seven
|
|
1273
|
+
per-service files was one file copied seven times, and adding or removing a service is a
|
|
1274
|
+
scaffold plus an SSOT entry rather than a hand-written file. Severity is `deploy` from the
|
|
1275
|
+
start; a `warn` phase would be a gate that does not gate (`automation-gates.md` §5).
|
|
1276
|
+
|
|
1277
|
+
The R1 bridge stays, under the id **`G-PROD-IMAGE`** — a requirement about a file is not
|
|
1278
|
+
the file, and it keeps answering on a repository whose compose the generator has not yet
|
|
1279
|
+
written. `G-PROD` is the id the owner decision names for the file itself, so it is the id
|
|
1280
|
+
the file row wears.
|
|
1281
|
+
|
|
1282
|
+
`npx oa-sync-template docker-compose.production.yml --target .` writes it. Unlike an
|
|
1283
|
+
installation document, the file is REWRITTEN rather than spliced or blocked: the whole file
|
|
1284
|
+
is the template's, so there is no prose of the service's own to preserve. A repository that
|
|
1285
|
+
has not declared who it is gets the `[ServiceIdentity] … Fix: …` fail-fast instead of a
|
|
1286
|
+
guessed name, and the check stays silent about it — that defect is `C-SERVICE`'s.
|
|
1287
|
+
|
|
1288
|
+
Owner decision: `api/docs/governance/confirmations/server-topology.md`
|
|
1289
|
+
§ Confirmation 20260910-server-topology-005.
|
|
1290
|
+
|
|
1291
|
+
### Fixed — `oa-validate` records its verdict, so a deploy gate can ever see a complete one (d.232)
|
|
1292
|
+
|
|
1293
|
+
`ci/deployability.json` had exactly one writer: step **7/7** of the boot validation, which
|
|
1294
|
+
runs inside the service container, where the workspace SSOTs are not present. Every signal
|
|
1295
|
+
that could exist therefore carried `complete: false` — and the gate INFRA was handed in
|
|
1296
|
+
d.230 ("accept the signal only when `deployable && complete`") could never be satisfied,
|
|
1297
|
+
because the run that IS complete, `npx oa-validate --workspace <root> <service root>` from
|
|
1298
|
+
the api checkout, wrote no file at all. Measured before the fix: a copy of `api_biz/converter`
|
|
1299
|
+
validated by the packaged CLI printed `NOT DEPLOYABLE` and left no `ci/deployability.json`
|
|
1300
|
+
behind.
|
|
1301
|
+
|
|
1302
|
+
A service run now writes the file into the service root it measured, through the same
|
|
1303
|
+
`buildDeployabilitySignal` / `writeDeployabilitySignal` step 7 uses — one file shape with
|
|
1304
|
+
two producers, asserted by a control case comparing the CLI's file against what the shared
|
|
1305
|
+
builder produces from the same run. The report is printed BEFORE the write, as in step 7,
|
|
1306
|
+
so an unwritable service root still leaves the human-readable verdict; the write itself has
|
|
1307
|
+
no switch (`automation-gates.md` §1 requirement 5), and a root that cannot take it ends the
|
|
1308
|
+
run with the writer's `[DeployabilitySignal] … Fix: …` and exit `2`.
|
|
1309
|
+
|
|
1310
|
+
The workspace mode (`--workspace` with no service root) and `--library` write nothing. The
|
|
1311
|
+
signal is a verdict about ONE service tree: the workspace run has no service root and
|
|
1312
|
+
reports every per-service row `NOT RUN` by construction, and a library has no deploy of its
|
|
1313
|
+
own (002 §10), so a file from either would be a partial verdict standing where a complete
|
|
1314
|
+
one belongs.
|
|
1315
|
+
|
|
1316
|
+
This package's own version is now read lazily, at the moment the signal is built, with
|
|
1317
|
+
`[oa-validate] This copy of the engine carries no package.json - … Fix: …` where the file
|
|
1318
|
+
is missing — a planted copy of the engine used to be a `MODULE_NOT_FOUND` stack trace
|
|
1319
|
+
before a single row ran.
|
|
1320
|
+
|
|
1321
|
+
### Fixed — a repository is who it DECLARES it is, not what its directory is called (d.229)
|
|
1322
|
+
|
|
1323
|
+
`F-RUNNER` and the three `G-SETUP-*` skeleton rows derived this repository's parameters
|
|
1324
|
+
from `path.basename(serviceRoot)`. Since d.229 those rows finally answer inside the image
|
|
1325
|
+
the service is built into — and that image has `WORKDIR /app`, so every service on the
|
|
1326
|
+
platform is called "app" there. Measured over the conforming fixture copied into a
|
|
1327
|
+
directory of that name: `F-RUNNER` reported a conformant compose file as drifted, quoting
|
|
1328
|
+
a line the substitution had mangled (`It __SERVICE_NAME__lies in dev and CI only`, because
|
|
1329
|
+
"app" is a substring of "applies"), and the production stage then fails the build of every
|
|
1330
|
+
conformant service. A directory called `My Service` did worse: the derivation threw, and
|
|
1331
|
+
`oa-validate` exited 2 with no table at all.
|
|
1332
|
+
|
|
1333
|
+
The identity is read from `config/service/config.json` → `service.name`, the key row
|
|
1334
|
+
`C-SERVICE` owns (`src/manifest/serviceIdentity.js`). Reading it back is the exact inverse
|
|
1335
|
+
of what `deriveParams` writes, which only ever ADDS the platform prefix — so no new rule
|
|
1336
|
+
about how a service must be named appears here as a side effect. A file that is absent,
|
|
1337
|
+
unreadable or declares no name is `C-SERVICE`'s finding and the rendered rows repeat
|
|
1338
|
+
nothing; a name no parameters can be derived from is reported by the row that needed it,
|
|
1339
|
+
because nothing else would say a word about it. The generator fails fast with
|
|
1340
|
+
`[ServiceIdentity] … Fix: …`.
|
|
1341
|
+
|
|
1342
|
+
### Fixed — the uniform sync needs a workspace only where a ROW needs one (d.229)
|
|
1343
|
+
|
|
1344
|
+
`oa-sync-template [path…] --target` demanded a workspace root before it planned anything,
|
|
1345
|
+
so the `fix` command of `F-INIT`, `F-JEST` and `F-RUNNER` could not be typed in the one
|
|
1346
|
+
place their finding is now raised: inside a service container. It asks the same predicate
|
|
1347
|
+
the manifest run asks (`rowNeedsWorkspace`, moved to `manifestShape.js` so both read one
|
|
1348
|
+
definition), and a row that does need the workspace is printed `NOT RUN` by name.
|
|
1349
|
+
`shared-env` still refuses without one — its owner is a platform file.
|
|
1350
|
+
|
|
1351
|
+
### Changed — `F-RUNNER`'s fix is a command: the sync writes the block (d.215d)
|
|
1352
|
+
|
|
1353
|
+
Confirmation 001 §3.2 asks that every finding's `fix` be a COMMAND. Seven of the eight biz
|
|
1354
|
+
repositories carry no `oa-test-runner v1` block at all, so the row's fix read "paste it in
|
|
1355
|
+
by hand once", which is advice. `npx oa-sync-template docker-compose.yml --target .` now
|
|
1356
|
+
writes it, in three states: a file carrying the markers is rewritten between them; a file
|
|
1357
|
+
carrying a runner but no markers has that runner's declaration REPLACED where it stands;
|
|
1358
|
+
a file carrying neither gets the block inserted as the first entry under `services:`.
|
|
1359
|
+
|
|
1360
|
+
The middle state is the one that matters and the one a first cut got wrong: measured
|
|
1361
|
+
2026-09-10 over a copy of `api_biz/converter`, inserting gave the file a SECOND
|
|
1362
|
+
`api_service_converter_tests:` key — a duplicate mapping key, which compose either refuses
|
|
1363
|
+
or resolves by taking the last one. The replacement spans the key to the next key at the
|
|
1364
|
+
same level, so a comment above it (in converter, the memory measurement taken inside that
|
|
1365
|
+
container) survives. Where the block goes is not a guess in a compose file — a mapping has
|
|
1366
|
+
one place its entries can start — which is why `init.sh` stays `BLOCKED`: there the
|
|
1367
|
+
position IS a decision about that service's own install steps.
|
|
1368
|
+
|
|
1369
|
+
The `wrote` / `would change` line names both sides of the first difference now. Naming only
|
|
1370
|
+
what the file already said reported `line 2: "api_service_converter:"` for an insertion —
|
|
1371
|
+
the line the block was pushed past, rather than the block.
|
|
1372
|
+
|
|
1373
|
+
### Added — the `Uniform:` region of a README has a gate, not only a generator (d.215e)
|
|
1374
|
+
|
|
1375
|
+
Owner decision 2026-09-09: that region is of the class `generated`, and 001 §2 makes such a
|
|
1376
|
+
class a generator PLUS a row carrying its `--check` (the precedent is `G-SHARED-ENV`).
|
|
1377
|
+
Until now the generator existed alone, so a README whose pointer had gone stale was reported
|
|
1378
|
+
by nothing anybody runs on a repository — the false guarantee of `automation-gates.md` §5.
|
|
1379
|
+
|
|
1380
|
+
* **`G-README`** (biz-service, severity `deploy`, scope `service`, `fix: npx
|
|
1381
|
+
oa-sync-template readme-uniform --target .`) — reads only the packaged manifest, so it
|
|
1382
|
+
answers in a container.
|
|
1383
|
+
* **`L-README-REGION`** (library, severity `publish`, scope `bearer`, `fix: npx
|
|
1384
|
+
oa-sync-template readme-uniform --all`) — a row of its own beside `L-README` rather than
|
|
1385
|
+
an extension of it, because the two halves of that file have different writers: the node
|
|
1386
|
+
header is written by a human, the region by a command. A checkout carrying no copy of
|
|
1387
|
+
this package reports it `NOT RUN` with that reason; a package declaring no category
|
|
1388
|
+
raises nothing, because `U-MISMATCH` already blocks it with its own fix.
|
|
1389
|
+
|
|
1390
|
+
Both render through `readmePointer.js` and compare through the same `checkUniformRegion`
|
|
1391
|
+
the `--check` run calls, and where each kind's region points is one module
|
|
1392
|
+
(`src/sync/readmeLocation.js`), read by the rows and by the CLI.
|
|
1393
|
+
|
|
1394
|
+
A forbidden row (`X-HOOKS`, `X-PREVAL`) now renders its path in the region like every other
|
|
1395
|
+
row. It did not, and the reason was a test rather than a reader:
|
|
1396
|
+
`api/tests/scripts/add-service.bats` grepped the WHOLE template for `hooks/pre-commit`, so
|
|
1397
|
+
a region correctly naming a banned path failed a case whose subject is whether `init.sh`
|
|
1398
|
+
still INSTALLS the hook. That case now asks it of the files that could.
|
|
1399
|
+
|
|
1400
|
+
### Changed — `C-LINT` moved into the `docs` block, whose budget it declares (d.210b)
|
|
1401
|
+
|
|
1402
|
+
The literal `config/biz-docs-lint.tree.json` stood in the manifest twice: on the row and
|
|
1403
|
+
again as the block's `tree_config`. The block names the ROW now (`tree_config_row`) and the
|
|
1404
|
+
row names the path, so a rename has one place to happen. The row id is unchanged, and so is
|
|
1405
|
+
every verdict: the block gained `applies_to: "*"`, without which `file-present` would have
|
|
1406
|
+
skipped the row in silence.
|
|
1407
|
+
|
|
1408
|
+
### Added — `D-NPM`, now that the rule it cites exists (L016)
|
|
1409
|
+
|
|
1410
|
+
Guidance until 2026-09-10, and true while it stood: 001 §2 asks that an npm script a live
|
|
1411
|
+
document cites still exist, and no rule of `lint-biz-docs.mjs` raised it, so a row would
|
|
1412
|
+
have reported nothing and read as a clean tree (001 §6). BIZ-DOCS wrote `L016`, which
|
|
1413
|
+
resolves a cited `npm run <name>` against the `package.json` above the tree — exactly one
|
|
1414
|
+
file for a service repository. Measured over the eight service trees: evaluated on seven of
|
|
1415
|
+
them with no finding, undecided on the eighth, which declares no tree config.
|
|
1416
|
+
|
|
1417
|
+
### Fixed — a rule the lint could not EVALUATE is NOT RUN, not a finding
|
|
1418
|
+
|
|
1419
|
+
An evidence probe reading `api_biz/*` cannot be evaluated in a checkout that has no
|
|
1420
|
+
siblings — the shape of the CI job. Measured 2026-09-10 over `git archive HEAD` into such a
|
|
1421
|
+
directory: the template mirror came back `NOT DEPLOYABLE` on `D-PORT` and `D-RETIRED`,
|
|
1422
|
+
whose bans nobody had violated, while `U-ORPHAN` in the same run said the honest thing. The
|
|
1423
|
+
linter's two channels map onto this uniform's two now: its `findings` are findings, its
|
|
1424
|
+
`skipped` list is `NOT RUN`. A tree the lint REFUSES outright — no tree config, a broken
|
|
1425
|
+
configuration — stays a finding, because that one has a fix somebody can carry out. A check
|
|
1426
|
+
may therefore return `{ findings, notRun }`; until now it could only return an array, so
|
|
1427
|
+
"I could not decide this" had to be dressed as a finding.
|
|
1428
|
+
|
|
1429
|
+
### Changed — `L-NO-FILE-RANGE` reads `file:` for every package, in every section (d.224d)
|
|
1430
|
+
|
|
1431
|
+
The row read `@onlineapps/*` only, so `lodash = file:../lodash` in `devDependencies` passed
|
|
1432
|
+
the uniform and the F10 publish gate had to keep rule **G2** of its own for exactly that
|
|
1433
|
+
case — one concern on two rails. A `file:` resolves to a path on the machine that wrote it
|
|
1434
|
+
and never reaches a published tarball, whoever owns the package, so the row now reads all
|
|
1435
|
+
four dependency sections for every name. RANGES (`^`, `~`, `latest`, `*`) stay scoped to
|
|
1436
|
+
what the SSOT owns: a caret on a third-party dependency is a judgement nothing here can
|
|
1437
|
+
make. G2 is deleted; the gate's rules are G4, G6 and G7.
|
|
1438
|
+
|
|
1439
|
+
### Fixed — Step 7 reads the reasons a service is undeployable from the whole manifest run
|
|
1440
|
+
|
|
1441
|
+
A `CONFIG_STEP_ROWS` row carrying `deploy` was filtered out of `deployFindings` together
|
|
1442
|
+
with its report (`4dcd61a2`).
|
|
1443
|
+
|
|
1444
|
+
|
|
1445
|
+
### Changed — the deploy signal says whether the run reached every blocking row (d.230)
|
|
1446
|
+
|
|
1447
|
+
`ci/deployability.json` carried one claim: `deployable`, meaning nothing blocked among the
|
|
1448
|
+
rows that RAN. Inside a service container the rows reading the workspace cannot run at all,
|
|
1449
|
+
so the file routinely said `deployable: true` about a tree whose other half nobody had
|
|
1450
|
+
opened — and a deploy gate reading only that key was honouring a `--skip` nobody had
|
|
1451
|
+
flagged (`automation-gates.md` §1.5). Confirmation 001 §4 knows one outcome, a finding
|
|
1452
|
+
refusing the deploy, and says nothing about NOT RUN.
|
|
1453
|
+
|
|
1454
|
+
The second half is **`complete`**: no row whose severity blocks this uniform was NOT RUN.
|
|
1455
|
+
It is derived in one place (`runManifest.js` § `incompleteRows`), so the verdict and the
|
|
1456
|
+
sentence a human reads name the same rows, and every `notRun` entry now carries the
|
|
1457
|
+
`severity` of the row that did not run — without it nothing can say whether a skipped row
|
|
1458
|
+
could have stopped anything (a `warn` row could not).
|
|
1459
|
+
|
|
1460
|
+
`deployable` is unchanged, and so is the registration contract of d.221. **No exit code
|
|
1461
|
+
changes**: an incomplete run must not stop a build image or a boot, because the partial run
|
|
1462
|
+
in the container is the intent (001 §4); completeness is the deploy gate's question, asked
|
|
1463
|
+
of the file. The gate takes a signal only when `deployable && complete`, and the complete
|
|
1464
|
+
run comes from the api checkout: `npx oa-validate --workspace <root>`.
|
|
1465
|
+
|
|
1466
|
+
The sentence a clear-but-incomplete run prints belongs to the manifest
|
|
1467
|
+
(`verdict.incomplete`, carrying `{count}` and `{ids}`), not to the renderer: a service says
|
|
1468
|
+
`deploy row(s)`, a library `publish row(s)`, and `manifestShape` refuses a manifest whose
|
|
1469
|
+
verdict lacks the sentence or whose sentence cannot name the rows.
|
|
1470
|
+
|
|
1471
|
+
```
|
|
1472
|
+
DEPLOYABLE — manifest conformance: no findings, INCOMPLETE: 6 deploy row(s) not run
|
|
1473
|
+
(U-ORPHAN, R-NODE, D-PORT, D-SCRIPT, D-RETIRED, D-HEADER). Fix: run the uniform from the
|
|
1474
|
+
api checkout — npx oa-validate --workspace <workspace root>
|
|
1475
|
+
```
|
|
1476
|
+
|
|
1477
|
+
### Changed — the template rows reference the package, not its output (d.229)
|
|
1478
|
+
|
|
1479
|
+
`F-INIT`, `F-JEST`, `F-RUNNER`, `G-SETUP-INSTALL`, `G-SETUP-MATRIX`, `G-SETUP-VALIDATION`
|
|
1480
|
+
and `G-SHARED-ENV` pointed at `api/templates/business-service/…` and
|
|
1481
|
+
`api/config/shared-env.json`. Since d.215a the OWNER of the template is this package and
|
|
1482
|
+
`api/templates/business-service` is its committed output (001 §5), so those rows
|
|
1483
|
+
referenced the output rather than the owner — which confirmation 004 point 2 forbids.
|
|
1484
|
+
|
|
1485
|
+
The measured cost was where it matters most: with no workspace root — a service container
|
|
1486
|
+
at boot, and a service's own CI checkout — every one of those rows was `NOT RUN`. Over
|
|
1487
|
+
`api_biz/converter` on 2026-09-09 that was 13 rows skipped and two real findings
|
|
1488
|
+
(`F-RUNNER`, `G-SHARED-ENV`) never raised. After the change the same run raises the same
|
|
1489
|
+
findings as the workspace run, and only `U-ORPHAN`, `R-NODE` and the four `D-*` rows stay
|
|
1490
|
+
`NOT RUN` — the rows that really do read something outside this package.
|
|
1491
|
+
|
|
1492
|
+
**A fourth `from:` shape**: `{ "package": "<path inside this package>", "text": true }`,
|
|
1493
|
+
resolved from the package's own location, so it travels with the pin. `manifestShape`
|
|
1494
|
+
describes it and refuses both a packaged reference without `text: true` and one naming a
|
|
1495
|
+
`path` and a `package` at once (two owners for one fact).
|
|
1496
|
+
|
|
1497
|
+
**Which rows need the workspace is now the ROW's property, not only the scope's.** A check
|
|
1498
|
+
answers `needsWorkspace({ row })`; the ones that read nothing but their own reference
|
|
1499
|
+
answer from the reference. Scope `bearer` stays — making these rows `service` would have
|
|
1500
|
+
stopped the workspace run from checking them across every service, the regression d.223
|
|
1501
|
+
removed. With no workspace there is one bearer and nothing to tell apart, so a finding is
|
|
1502
|
+
located the way the repository knows it (`init.sh`, not `../../elsewhere/init.sh`).
|
|
1503
|
+
|
|
1504
|
+
`G-SHARED-ENV` reads the rendered copy in the packaged template. The key set is still owned
|
|
1505
|
+
by `api/config/shared-env.json`; the two are kept equal by `tests/unit/sharedEnvTemplate.test.js`
|
|
1506
|
+
— an output with no gate would be a second copy of the fact (`doc-code-binding.md` §5).
|
|
1507
|
+
|
|
1508
|
+
### Fixed — one directory, one name: both roots of a run are canonical
|
|
1509
|
+
|
|
1510
|
+
The workspace root is derived from this package's own location, which node resolves
|
|
1511
|
+
through symlinks; a service or package root is an argument, which `path.resolve()` leaves
|
|
1512
|
+
in the spelling the caller typed. On macOS `/var` is a symlink to `/private/var`, so a run
|
|
1513
|
+
below the system temp got one root in each spelling — and "is the service inside the
|
|
1514
|
+
workspace", a string prefix, then answered no about a service plainly inside it. Measured
|
|
1515
|
+
over a `git archive` export of this repository: six rows reported `service root is outside
|
|
1516
|
+
the workspace root`, a sentence that was false, and the findings that export really
|
|
1517
|
+
carries were never raised. Every root now passes through `workspaceRoot.canonicalRoot()`
|
|
1518
|
+
before it is compared or printed.
|
|
1519
|
+
|
|
1520
|
+
### Added — the documentation rows of the biz-service uniform (`docs`)
|
|
1521
|
+
|
|
1522
|
+
The manifest gained a seventh section. Confirmation `biz-service-manifest` 001 §2 names
|
|
1523
|
+
five `D-*` rules about a service's own `docs/**`; four of them land as rows, and the
|
|
1524
|
+
fifth lands as `guidance` because the rule it needs does not exist yet.
|
|
1525
|
+
|
|
1526
|
+
Nothing here decides whether a document is wrong. The rules of `<service>/docs/**` are
|
|
1527
|
+
already written and already have an owner — `api/scripts/ci/lint-biz-docs.mjs`, held by
|
|
1528
|
+
BIZ-DOCS — so `src/manifest/checks/docsLintBridge.js` is a bridge in the same shape
|
|
1529
|
+
`contractBridge.js` has for the deploy and installation contracts: the row names the lint
|
|
1530
|
+
rule it stands for, the bridge runs the lint ONCE per service and hands each row the
|
|
1531
|
+
findings of the rules it cites, with the lint's own `file:line` and message.
|
|
1532
|
+
|
|
1533
|
+
`D-PORT` cites `F002:http-ports`, `D-SCRIPT` cites `L007`, `D-RETIRED` cites `F002` and
|
|
1534
|
+
`D-HEADER` cites `S001,S002` — written in the linter's own `--rules` syntax, so the row
|
|
1535
|
+
and the tool cannot grow a private dialect between them. A finding lands on exactly ONE
|
|
1536
|
+
row: the most specific citation wins, which is what keeps the port ban from being
|
|
1537
|
+
reported twice under two ids with two different `fix` sentences, and two rows claiming
|
|
1538
|
+
one rule equally are a fail-fast.
|
|
1539
|
+
|
|
1540
|
+
A rule NO row cites is not reported by this uniform. Measured 2026-09-09 over the eight
|
|
1541
|
+
repositories: the lint raises 80 findings, of which 16 fall to a row here. The rest is
|
|
1542
|
+
the documentation gate's own, landing in each repository's CI at zero findings
|
|
1543
|
+
(`biz-docs-foreign-tree-gate` 001) — said out loud in the module header rather than
|
|
1544
|
+
implied as coverage that is not there.
|
|
1545
|
+
|
|
1546
|
+
Where the rows do NOT answer, they say so. They are `bearer`-scoped and declare
|
|
1547
|
+
`api/scripts/ci` as a root they read, so a service image and a service's own CI report
|
|
1548
|
+
them `NOT RUN` with that reason. A tree carrying no `config/biz-docs-lint.tree.json` is
|
|
1549
|
+
undecided rather than clean — the file itself is row `C-LINT`, with its own fix — and a
|
|
1550
|
+
rule whose evidence probe reads a checkout that is not there is reported undecided too,
|
|
1551
|
+
never as a pass (`automation-gates.md` §5). An empty `docs/` raises nothing: the absence
|
|
1552
|
+
of the tree is `G-SETUP`'s finding, and a second row with a different fix would be a
|
|
1553
|
+
duplicate.
|
|
1554
|
+
|
|
1555
|
+
`D-NPM` is `guidance`, not a row. 001 §2 asks that an npm script a document cites still
|
|
1556
|
+
exists, and no rule of the lint raises it; a row citing a rule nobody raises reports
|
|
1557
|
+
nothing and reads as a clean tree ("no check, so not a rule", 001 §6). The interface such
|
|
1558
|
+
a rule would need is handed to BIZ-DOCS, and the row is one line of manifest away once it
|
|
1559
|
+
exists.
|
|
1560
|
+
|
|
1561
|
+
Measured on the eight repositories the day it landed: `invoicing` 17 `D-HEADER` findings,
|
|
1562
|
+
`ingest` 4 rows undecided for want of a tree config, the other six clean.
|
|
1563
|
+
|
|
1564
|
+
### Changed — `readme-uniform` renders the pointer of a biz service too
|
|
1565
|
+
|
|
1566
|
+
Confirmation 005 point 2 names three bearer kinds in one sentence — `library/<category>`,
|
|
1567
|
+
`biz-service`, `infra-service` — so `src/sync/readmePointer.js` is parameterised by kind
|
|
1568
|
+
rather than copied per kind; a second renderer would be two rails under one rule. What
|
|
1569
|
+
differs is declared as data: the region id, the regenerate command, the manifest a failed
|
|
1570
|
+
`--check` names, whether the README must carry a node header, and the body.
|
|
1571
|
+
|
|
1572
|
+
A service region carries `Uniform: [biz-service](<link>)`, the sentence saying which
|
|
1573
|
+
repositories wear the uniform (read from the `discovery` block and its SSOT, never typed),
|
|
1574
|
+
the row ids grouped by the manifest section that declares them, and which path falls under
|
|
1575
|
+
which row. A row taking its value from elsewhere renders the path that OWNS it, never the
|
|
1576
|
+
value (confirmation 004). A **forbidden** row contributes its id and not its path:
|
|
1577
|
+
`hooks/pre-commit` and `scripts/run-pre-validation.js` may not occur in a service
|
|
1578
|
+
repository as text at all — `api/tests/scripts/add-service.bats` greps the whole template
|
|
1579
|
+
for them, because `init.sh` used to install that hook — and a region naming them would
|
|
1580
|
+
write a retired path back into the template every service is created from. Measured: with
|
|
1581
|
+
the path in the region, that suite went 55/57.
|
|
1582
|
+
|
|
1583
|
+
The link points at the manifest as it lies in the service after `npm install`
|
|
1584
|
+
(`node_modules/@onlineapps/conn-orch-validator/manifests/biz-service.manifest.json`),
|
|
1585
|
+
derived from this package's name and the manifest's position inside it. A path, not a URL:
|
|
1586
|
+
a reader of the repository resolves it without a network. It resolves from the pin that
|
|
1587
|
+
ships `manifests/`, which is this wave.
|
|
1588
|
+
|
|
1589
|
+
`readme-uniform --target` now reads the KIND from the disk — a root carrying
|
|
1590
|
+
`config/service/operations.json` is a service, a root carrying `package.json` is a library,
|
|
1591
|
+
neither is a fail-fast naming both files. There is no `--kind` flag: it would be a second
|
|
1592
|
+
way to say what the directory already says. `--all` stays the library run, because the
|
|
1593
|
+
eight service repositories are separate checkouts and there is no list to walk; a service
|
|
1594
|
+
also needs no workspace above it, which is only consulted for what to call the target on
|
|
1595
|
+
stdout.
|
|
1596
|
+
|
|
1597
|
+
`templates/business-service/README.md` and its committed mirror
|
|
1598
|
+
`api/templates/business-service/README.md` carry the region rendered, so a service created
|
|
1599
|
+
by `--new` starts in sync. The pointer is the FIRST line of a service README: nothing
|
|
1600
|
+
requires a node header of a service, unlike a library, where its absence stays a fail-fast.
|
|
1601
|
+
The 27 library regions are byte-identical.
|
|
1602
|
+
|
|
1603
|
+
Not introduced: a manifest row asserting that a README region is current. The library
|
|
1604
|
+
manifest carries none (`L-README` checks the node header alone), so the service manifest
|
|
1605
|
+
gets none either — the pointer is a sync artefact with its own `--check`, and adding the
|
|
1606
|
+
row is the owner's decision rather than a side effect of this change.
|
|
1607
|
+
|
|
1608
|
+
|
|
1609
|
+
### Changed — `F-RUNNER` is a delimited block, and the sync renders it
|
|
1610
|
+
|
|
1611
|
+
The row named a compose file and compared it against nothing but itself, so the generator
|
|
1612
|
+
printed it `NOT RUN` and the runner stayed a copy kept in sync by review. The template's
|
|
1613
|
+
compose now carries `# --- oa-test-runner v1` … `# --- end oa-test-runner v1` (the two
|
|
1614
|
+
markers `F-INIT` already uses in `init.sh`, read trimmed so they may sit indented inside
|
|
1615
|
+
`services:`), the row declares `block:` and `from:`, and `src/manifest/checks/composeRunnerBlock.js`
|
|
1616
|
+
holds the one definition both halves read.
|
|
1617
|
+
|
|
1618
|
+
Normalized is not loosened: `build`, `env_file` and `networks` are what the same row
|
|
1619
|
+
already compares against the SERVICE the runner tests, so they are removed before the
|
|
1620
|
+
block is compared against the template — and grafted back from that service when the sync
|
|
1621
|
+
renders it. Seven of the eight biz repositories reach a second network in both nodes
|
|
1622
|
+
(measured 2026-09-09); that is the rule being kept, and neither half may report it as a
|
|
1623
|
+
difference. `where` now names the repository (`api_biz/<service>/docker-compose.yml`): the
|
|
1624
|
+
row runs per bearer, and five services all saying `docker-compose.yml` were five findings
|
|
1625
|
+
nobody could tell apart.
|
|
1626
|
+
|
|
1627
|
+
### Added — `G-SETUP-INSTALL`, `G-SETUP-MATRIX`, `G-SETUP-VALIDATION` (`skeleton_only`)
|
|
1628
|
+
|
|
1629
|
+
The three installation documents are compared by their set of `##` headings against the
|
|
1630
|
+
packaged template, never by their prose (confirmation 001 §2). The sync creates a missing
|
|
1631
|
+
document from the skeleton — header, title, sections, and no sentence of the template's
|
|
1632
|
+
prose, because what a section says is this service's own fact. An existing document is
|
|
1633
|
+
never rewritten: a missing section is `BLOCKED` with the heading named. That a document is
|
|
1634
|
+
absent at all remains `G-SETUP`'s finding through the installation contract's
|
|
1635
|
+
`SETUP_DOCS`, so one defect keeps one owner.
|
|
1636
|
+
|
|
1637
|
+
### Fixed — the `G-SHARED-ENV` fix sentence doubled the path
|
|
1638
|
+
|
|
1639
|
+
`npx oa-sync-template shared-env --target config/env-templates` wrote
|
|
1640
|
+
`config/env-templates/config/env-templates/shared.env`: the subcommand appends
|
|
1641
|
+
`config/env-templates/shared.env` to its target. Measured, then corrected to `--target .`
|
|
1642
|
+
(confirmation 001 §3.2: every `fix` sentence in the manifest is a command).
|
|
1643
|
+
|
|
1644
|
+
### Added — the template's production stage builds only a clean image
|
|
1645
|
+
|
|
1646
|
+
`templates/business-service/Dockerfile` runs `oa-validate` in the production stage, after
|
|
1647
|
+
`COPY . .` (confirmation 001 §3.1). The CLI is this package's, and all eight biz services
|
|
1648
|
+
declare this library under `dependencies`, so `npm ci --omit=dev` installs it. Rows that
|
|
1649
|
+
read a platform file print `NOT RUN` there — an image has no workspace above it — and one
|
|
1650
|
+
finding of severity `boot` or `deploy` fails the build.
|
|
1651
|
+
|
|
1652
|
+
### Changed — step 2 of the boot validation IS the manifest rows for the config documents
|
|
1653
|
+
|
|
1654
|
+
`ValidationOrchestrator.validateConfig()` checked `config.json`/`service.name` and a non-empty
|
|
1655
|
+
`operations.json` with sentences of its own, while rows `C-SERVICE`, `C-OPS` and `C-CONTRACT`
|
|
1656
|
+
demand the same documents with more coverage, an owner, a fix and a doc pointer. One contract,
|
|
1657
|
+
two rails — and the same defect was reported twice: a free sentence in step 2 and a table row in
|
|
1658
|
+
step 7. Step 2 keeps its place in the boot order (steps 3-6 must not run behind a broken
|
|
1659
|
+
configuration, and step 7 runs after them) and now runs those three rows; step 7 leaves them out
|
|
1660
|
+
of its table and its errors as already answered. `results.steps.config` gained `findings`, and its
|
|
1661
|
+
`errors` are the row messages (`[Manifest] C-SERVICE config/service/config.json - …`). The
|
|
1662
|
+
banner and `ci/deployability.json` still describe the whole uniform — the verdict is about the
|
|
1663
|
+
tree, not about which step read a row. The manifest is loaded and run ONCE per validation
|
|
1664
|
+
(`manifestRun()`); the wrapper builds a fresh orchestrator for every attempt, revalidation
|
|
1665
|
+
included (d.224).
|
|
1666
|
+
|
|
1667
|
+
### Changed — `installContract.js` exports the requirement ids it raises
|
|
1668
|
+
|
|
1669
|
+
`INSTALL_CONTRACT_REQUIREMENTS` joins `DEPLOY_CONTRACT_REQUIREMENTS` as an export, and `add()`
|
|
1670
|
+
refuses an id that is not on the list. The manifest bridge
|
|
1671
|
+
(`src/manifest/checks/contractBridge.js`) read a hand-written copy of those three ids, with a
|
|
1672
|
+
comment saying it was one; the deploy contract had already paid for that pattern, its copy naming
|
|
1673
|
+
one requirement fewer than the module enforced. The bridge now reads the export (d.224).
|
|
1674
|
+
|
|
1675
|
+
### Added — the documentation tree's descriptive lists are rendered, not typed
|
|
1676
|
+
|
|
1677
|
+
`oa-sync-template docs-region (--id <id> | --all) --file <document> [--check]` renders five
|
|
1678
|
+
regions from the two packaged manifests: the repository shape annotated with the row owning
|
|
1679
|
+
each path, the required and forbidden scripts with their bodies, the runtime parameters, the
|
|
1680
|
+
table of every row, and the library categories with their duty sections. Three documents were
|
|
1681
|
+
copying the repository tree by hand and two the script table — a descriptive fact kept true by
|
|
1682
|
+
review only. A region carries a row's observable and never its `why` or `fix`; a `from:`
|
|
1683
|
+
reference renders as the path that owns the value, never as the value. The run replaces a
|
|
1684
|
+
region and never inserts one: a document with no markers fails and prints the two lines to
|
|
1685
|
+
paste, because where a section belongs in another thread's document is that thread's decision.
|
|
1686
|
+
`--list` names the ids, `--check` exits 1 with the drift (d.226).
|
|
1687
|
+
|
|
1688
|
+
### Changed — one implementation of the generated-region markers
|
|
1689
|
+
|
|
1690
|
+
Finding, replacing, extracting and diffing a marked region moved to `src/sync/generatedRegion.js`,
|
|
1691
|
+
shared by `readme-uniform` and `docs-region`. `readme-uniform` renders byte-identical output
|
|
1692
|
+
(27 of 28 libraries in sync, exit 0, before and after). Damaged markers are now reported by
|
|
1693
|
+
`[GeneratedRegion]` rather than `[ReadmePointer]` — the module that rejects them names itself,
|
|
1694
|
+
and the message still names the file to repair (d.226).
|
|
1695
|
+
|
|
1696
|
+
### Fixed — the api checkout is found by where this package sits, not by the directory's name
|
|
1697
|
+
|
|
1698
|
+
The workspace root was resolved by looking upwards for a directory named `api`. A CI job
|
|
1699
|
+
that clones the repository under any other name — `infra-mono` — therefore resolved no
|
|
1700
|
+
workspace, and every row reading the platform SSOT printed `NOT RUN` in a checkout that
|
|
1701
|
+
had the SSOT all along. The checkout is now identified by the marker it carries
|
|
1702
|
+
(`config/services.json`) and located from this package's own path, so the name of the
|
|
1703
|
+
directory stops being part of the contract; `--workspace` remains the explicit override,
|
|
1704
|
+
and a copy installed in a service's `node_modules` still resolves nothing, which is the
|
|
1705
|
+
behaviour a run inside a service container needs (d.227).
|
|
1706
|
+
|
|
1707
|
+
|
|
1708
|
+
### Added — `config/service/*.json` is decided, and the keys nobody reads are refused
|
|
1709
|
+
|
|
1710
|
+
Confirmation `biz-service-manifest` 003 §18 asks for schemas over the three configuration
|
|
1711
|
+
files. Measurement first (d.217b): `operations.json` is already decided by
|
|
1712
|
+
`service-validator-core` `validateOperationsSchema`, and `integration-contract.json` by
|
|
1713
|
+
`utils/bizCiGateContract.js` — the validator the CI gate runs over it, which checks the
|
|
1714
|
+
connector booleans, the integration minimum, the database block and its agreement with
|
|
1715
|
+
`requiredConnectors.db`, and the env block with a `why` per name. Writing JSON Schema
|
|
1716
|
+
beside either would have been a second implementation of one rule, so `C-CONTRACT` now
|
|
1717
|
+
CALLS that validator (`check: config-contract`) instead of only asserting the file exists,
|
|
1718
|
+
and its message is the validator's own sentence. One rail, one wording, in the gate and in
|
|
1719
|
+
the table.
|
|
1720
|
+
|
|
1721
|
+
`config.json` had no owner, and `C-SERVICE` checked one key of it. It now demands the
|
|
1722
|
+
three that were measured to have a reader — `service.name`, `service.workspaceScoped` (an
|
|
1723
|
+
explicit boolean; the wrapper refuses to boot without one) and
|
|
1724
|
+
`service.specificationEndpoint` — and reports a present-but-wrongly-typed value with a
|
|
1725
|
+
different sentence than a missing one, because the two send the reader to different places.
|
|
1726
|
+
|
|
1727
|
+
`C-SERVICE-DEAD` (new row, severity `deploy`) refuses the four declarations the same
|
|
1728
|
+
measurement found no reader for anywhere in the platform: `service.version` (`ConfigLoader`
|
|
1729
|
+
assigns it from `package.json` on every load, so the value in the file is never read),
|
|
1730
|
+
`service.port` and `service.url` (confirmation `biz-service-port-url` 001) and
|
|
1731
|
+
`contractVersion` in the integration contract. Measured over the eight biz services: 22
|
|
1732
|
+
findings, and none for `C-SERVICE` or `C-CONTRACT` — the shape they already have is the
|
|
1733
|
+
shape the rows demand. A declaration nothing consumes is not harmless; it tells the next
|
|
1734
|
+
author something depends on it, and every new service copies it forward
|
|
1735
|
+
(`change-discipline.md` § Removing something removes its declaration).
|
|
1736
|
+
|
|
1737
|
+
### Removed — `D-DB-DECL`, whose rule `C-CONTRACT` now states
|
|
1738
|
+
|
|
1739
|
+
Once `C-CONTRACT` calls the contract validator, a missing or non-boolean
|
|
1740
|
+
`requiredConnectors.db` is reported by it as `[BizCiGate] Invalid contract field`.
|
|
1741
|
+
`D-DB-DECL` said the same thing in its own words, and two rows for one rule is the
|
|
1742
|
+
duplicate `change-discipline.md` § One rail per concern forbids — so the row, its
|
|
1743
|
+
`db-declared` check and its tests are gone in this change, not left to drift apart
|
|
1744
|
+
(lead decision, d.217b).
|
|
1745
|
+
|
|
1746
|
+
`D-DB-CONSISTENT` stays: `requiredConnectors.db` against a repository that has
|
|
1747
|
+
`migrations/` or a database client anyway is a comparison the contract validator cannot
|
|
1748
|
+
make, because it never looks at the repository.
|
|
1749
|
+
|
|
1750
|
+
|
|
1751
|
+
### Added — the business-service template ships in this package, and `oa-sync-template` renders it
|
|
1752
|
+
|
|
1753
|
+
Confirmation `biz-service-manifest` 001 §2 named three consumers of one declaration: the
|
|
1754
|
+
conformance check, the generator, and the documentation. Only the first existed. Every
|
|
1755
|
+
`fix` sentence about a file was therefore a hand-off — "copy the block from
|
|
1756
|
+
api/templates/business-service/init.sh" — and a hand-off is how nine copies of one
|
|
1757
|
+
`init.sh` block came to differ in the first place.
|
|
1758
|
+
|
|
1759
|
+
`templates/business-service/**` moved into this package (§2), so the shape a service is
|
|
1760
|
+
created from is pinned by the same version that pins the manifest it is measured against.
|
|
1761
|
+
`api/templates/business-service` is now that template rendered with its own placeholders
|
|
1762
|
+
as the parameters — generated output, committed for reading (§5) — and
|
|
1763
|
+
`tests/unit/templateMirror.test.js` measures §9's acceptance file by file, in both
|
|
1764
|
+
directions.
|
|
1765
|
+
|
|
1766
|
+
Two template files are packed under a name they are not written under, and both names are
|
|
1767
|
+
measurements rather than taste. `gitignore` has no dot because npm never packs a file
|
|
1768
|
+
called `.gitignore` — 19 of the template's 22 files reached `npm pack --dry-run` before,
|
|
1769
|
+
22 do now. `package.json.template` carries a suffix because a `package.json` inside a
|
|
1770
|
+
package is a second package to every tool that walks a package tree: under its written
|
|
1771
|
+
name it produced eight findings from `oa-validate --library --all` and two from
|
|
1772
|
+
`api/scripts/ci/verify-manifest-pins.mjs`, step 1 of the pre-push hook, all of them false
|
|
1773
|
+
— the template is not a published library, and its pins are deliberately not the
|
|
1774
|
+
platform's, because the generator writes the SSOT's versions when it creates a service.
|
|
1775
|
+
|
|
1776
|
+
Three runs joined `shared-env` and `readme-uniform`:
|
|
1777
|
+
|
|
1778
|
+
* `oa-sync-template --new <name> --into <dir>` — a whole service tree. It is what
|
|
1779
|
+
`api/scripts/add-service.sh --scaffold` now runs, instead of a `cp -R`, a `sed` loop
|
|
1780
|
+
and three `jq` calls that made the script a second writer of files the library already
|
|
1781
|
+
knew how to write. `--description` is optional: without it `__SERVICE_DESCRIPTION__` is
|
|
1782
|
+
left standing and the files carrying it are listed, because this run has no description
|
|
1783
|
+
and inventing one would write a fact nobody stated.
|
|
1784
|
+
* `oa-sync-template [path…] --target <serviceRoot>` — the uniform sync of §3.2. It walks
|
|
1785
|
+
the rows of classes `identical` and `generated` and renders each from the very `from:`
|
|
1786
|
+
reference that row points the CHECK at: one definition, read twice. It holds no list of
|
|
1787
|
+
files and no template path of its own. Files of the `own` class are never written.
|
|
1788
|
+
* `oa-sync-template template --target <dir>` — the committed template directory as output,
|
|
1789
|
+
with `--check` as its gate.
|
|
1790
|
+
|
|
1791
|
+
A row the manifest does not make renderable is printed `NOT RUN` with the reason, never
|
|
1792
|
+
filled from a guess: `F-RUNNER`, `G-PROD` and `G-SETUP` declare no `from:` reference, so
|
|
1793
|
+
nothing says what their content is rendered from (`.claude/rules/automation-gates.md` §5).
|
|
1794
|
+
Measured over the eight biz repositories on 2026-09-09, the files `--check` would change
|
|
1795
|
+
are exactly the `F-INIT`/`F-JEST` findings `oa-validate` reports on the same trees.
|
|
1796
|
+
|
|
1797
|
+
### Changed — the template gained `__REGISTRY_NAME__`, and the ignore list gained `ci/`
|
|
1798
|
+
|
|
1799
|
+
The template used to spend `__SERVICE_NAME__` on two facts — the registry identity
|
|
1800
|
+
(`biz-reporting`) and the directory-derived paths (`config/env-active/reporting.env`) — so
|
|
1801
|
+
the scaffold patched the identity back in with `jq` after copying, reformatting the JSON on
|
|
1802
|
+
the way. Each fact now has its own placeholder, which is what makes rendering a pure text
|
|
1803
|
+
substitution and the identity render comparable byte for byte. `ci/` joined the template's
|
|
1804
|
+
ignore list: `ci/deployability.json` is written on every start.
|
|
1805
|
+
|
|
1806
|
+
The `fix` sentences of `F-INIT` and `F-JEST` are now the command that fixes them. The other
|
|
1807
|
+
three file rows keep their prose fix, because the command would not do what it says.
|
|
1808
|
+
|
|
1809
|
+
|
|
1810
|
+
### Fixed — the workspace run stops printing "no findings" over a drifting workspace
|
|
1811
|
+
|
|
1812
|
+
A check declares what it needs in order to look, and until now the vocabulary had two
|
|
1813
|
+
words for three needs. `F-INIT`, `F-JEST`, `G-SHARED-ENV`, `R-NODE`, `L-ENGINES` and
|
|
1814
|
+
`L-PINS` are facts of ONE bearer that reach their reference file — the template,
|
|
1815
|
+
`api/config/shared-env.json`, `api/.nvmrc`, `api/config/libraries.json` — only through
|
|
1816
|
+
the workspace. There was no word for that, so they borrowed `scope: workspace`, whose
|
|
1817
|
+
contract is "there is no service to read", and each of them opened with
|
|
1818
|
+
`if (serviceRoot === null) return []`.
|
|
1819
|
+
|
|
1820
|
+
The consequence was a silent pass, which `.claude/rules/automation-gates.md` §5 counts as
|
|
1821
|
+
a defect of the same severity as a wrong result. Measured on 2026-09-09:
|
|
1822
|
+
`oa-validate --workspace <root>` over this workspace printed `DEPLOYABLE — no findings`,
|
|
1823
|
+
while the same eight services, checked one at a time, reported 28 findings of those four
|
|
1824
|
+
rows — six drifted `jest.config.js`, four drifted `init.sh`, eight stale `shared.env`, and
|
|
1825
|
+
five services building on Node 22 against an `api/.nvmrc` of 24.
|
|
1826
|
+
|
|
1827
|
+
`CHECK_SCOPES` now names three: `service` (the bearer's root is enough), `workspace` (the
|
|
1828
|
+
check reports about the workspace itself) and `bearer` (about one bearer, workspace needed
|
|
1829
|
+
only to read its `from:` reference). A `bearer` row runs once per bearer — the one given in
|
|
1830
|
+
service mode, every one the manifest's own `discovery` block finds in workspace mode. The
|
|
1831
|
+
service tables and `--library --all` are unchanged, byte for byte; only the workspace run
|
|
1832
|
+
gained what it had been quietly dropping.
|
|
1833
|
+
|
|
1834
|
+
An unknown scope is refused where the check registers, not where it would first be needed:
|
|
1835
|
+
a check the runner cannot place would produce a report indistinguishable from one in which
|
|
1836
|
+
it found nothing.
|
|
1837
|
+
|
|
1838
|
+
### Fixed — a clean verdict names how many rows did not run
|
|
1839
|
+
|
|
1840
|
+
`DEPLOYABLE — no findings` reads as a proven clean bill of health. Over a run where five
|
|
1841
|
+
rows could not look it is not one, and the NOT RUN lines above it do not undo the sentence
|
|
1842
|
+
a reader believes. Measured on 2026-09-09: the biz-service workspace run over a checkout
|
|
1843
|
+
carrying `api/` alone printed `DEPLOYABLE — no findings` with 29 rows not run.
|
|
1844
|
+
|
|
1845
|
+
The clear verdict is now `DEPLOYABLE — no findings (29 row(s) not run)` — and
|
|
1846
|
+
`PUBLISHABLE — no findings (6 row(s) not run)` for the library uniform — with the
|
|
1847
|
+
parenthesis appearing only when something really did not run. The outcome half of the
|
|
1848
|
+
sentence has one owner, `report.js` § `describeClearOutcome`, used by the init-log banner,
|
|
1849
|
+
the `oa-validate` report and the library report alike; before this the three described the
|
|
1850
|
+
same run in three different ways and two of them stopped at `no findings`. The blocked
|
|
1851
|
+
verdict and `ci/deployability.json` (which already carried `notRun[]`) are unchanged.
|
|
1852
|
+
|
|
1853
|
+
### Changed — `L-CONSUMER` and `L-TOOLING` are bearer rows
|
|
1854
|
+
|
|
1855
|
+
Both are facts of ONE package — who pins it, which service carries it — that reach the
|
|
1856
|
+
answer only through the workspace: the same shape as `L-PINS`. Carried as
|
|
1857
|
+
`scope: workspace` they read a null package root in a whole-set run and returned `[]`, so
|
|
1858
|
+
`runManifest({ workspaceRoot })` said nothing about them at all. As `bearer` rows they run
|
|
1859
|
+
once per package, and the whole-set run is now the union of what each package says: over
|
|
1860
|
+
the fixture workspace, four findings (`lib-bad-core` and `lib-orphan` pinned by nothing,
|
|
1861
|
+
`lib-tooling` carried by a service twice) where it previously reported none.
|
|
1862
|
+
`--library --all` is unchanged at 30, byte for byte — that path already ran them per
|
|
1863
|
+
package.
|
|
1864
|
+
|
|
1865
|
+
`scopeByRowId` in the CLI became `packageRelativeRowIds`: it asks the registry which rows
|
|
1866
|
+
report a path relative to the PACKAGE root — only `scope: service` does — instead of
|
|
1867
|
+
naming a scope in a comment that had not mentioned the third. Nothing there had to move
|
|
1868
|
+
when these two rows changed scope, which is the point.
|
|
1869
|
+
|
|
1870
|
+
### Fixed — a checkout without its siblings says NOT RUN instead of reporting a false blocker
|
|
1871
|
+
|
|
1872
|
+
CI checks out `api/` alone, so `<workspace>/api_biz` is not on disk at all. A check reading
|
|
1873
|
+
it then walked an absent directory, got the empty set, and reported it as an answer.
|
|
1874
|
+
Measured on 2026-09-09 against a `git archive` export of this repository: `--library --all`
|
|
1875
|
+
raised a blocking `L-CONSUMER` on `conn-base-db` — "pinned by nothing under
|
|
1876
|
+
api_biz/*/package.json, api/infra/*/package.json" — about files nobody opened, and
|
|
1877
|
+
`L-TOOLING` quietly answered "no service carries it" over the same absence. The
|
|
1878
|
+
biz-service workspace run printed `DEPLOYABLE — no findings` with `U-ORPHAN` passing over
|
|
1879
|
+
an `api_biz` that was not there.
|
|
1880
|
+
|
|
1881
|
+
A check now declares the directories it walks — `requiresSiblings({ row, block })`,
|
|
1882
|
+
derived from the row's own patterns so no name is written twice — and the RUNNER verifies
|
|
1883
|
+
them before running it, once, for every row alike: `NOT RUN — sibling root api_biz is not
|
|
1884
|
+
present in this checkout` (`sibling roots …, … are not present` when several are missing).
|
|
1885
|
+
Same rule `lint-biz-docs --allow-missing-siblings` already follows,
|
|
1886
|
+
`automation-gates.md` §5. A `bearer` row in workspace mode inherits the manifest's own
|
|
1887
|
+
discovery roots, because enumerating no bearers is how it would otherwise pass eight
|
|
1888
|
+
services nobody looked at.
|
|
1889
|
+
|
|
1890
|
+
Measured after: the same export raises 16 findings instead of 17 — the false one is gone —
|
|
1891
|
+
and names `L-CONSUMER` and `L-TOOLING` as NOT RUN. `--library --all` over the full
|
|
1892
|
+
workspace is unchanged at 30, byte for byte.
|
|
1893
|
+
|
|
1894
|
+
### Changed — the template `jest.config.js` is a clean norm, so `F-JEST` measures against one
|
|
1895
|
+
|
|
1896
|
+
`templates/business-service/jest.config.js` set `RABBITMQ_URL` and `REGISTRY_URL` from a
|
|
1897
|
+
`||` fallback onto a hardcoded address — two violations of
|
|
1898
|
+
`.claude/rules/architecture-principles.md` §2 and §3 in the file `F-JEST` holds up as the
|
|
1899
|
+
norm. None of the eight biz services had copied it (measured: zero `||`, zero
|
|
1900
|
+
`process.env` writes in all eight), and the environment those two names come from is
|
|
1901
|
+
already owned elsewhere — `.gitlab-ci.yml` variables and `config/env-templates/shared.env`
|
|
1902
|
+
— so nothing was orphaned by removing them.
|
|
1903
|
+
|
|
1904
|
+
The template is now byte-equal to `api_biz/converter/jest.config.js`, the shape two of the
|
|
1905
|
+
eight already share exactly and six share in substance (referential run, confirmation
|
|
1906
|
+
`biz-service-manifest` 001 §8.4). `F-JEST` consequently clears for `converter` and `meta`
|
|
1907
|
+
and keeps reporting the other six, which differ for reasons of their own.
|
|
1908
|
+
|
|
1909
|
+
### Added — the service uniform's rows: files, scripts, config, runtime, database
|
|
1910
|
+
|
|
1911
|
+
`manifests/biz-service.manifest.json` grows from three rows to twenty-nine, in six
|
|
1912
|
+
sections (confirmation `biz-service-manifest` 001 §2, §8 step 2). What each row decides
|
|
1913
|
+
is its `check`, and the checks fall into two kinds.
|
|
1914
|
+
|
|
1915
|
+
**Rows that cite a checker this package already runs**, and add no rule of their own
|
|
1916
|
+
(004 point 4): `G-PROD` and `R-PORTS` name requirements `R1` and `R4` of
|
|
1917
|
+
`utils/deployContract.js`; `G-SETUP`, `D-DB-PACKAGE` and `D-DB-HEADERS` name
|
|
1918
|
+
`SETUP_DOCS`, `SQL_PACKAGE` and `SQL_HEADERS` of `utils/installContract.js`;
|
|
1919
|
+
`C-ENV-READS` runs the completeness half of `utils/envContract.js`. One registry entry
|
|
1920
|
+
per checker, a `requirement` per row, and a row naming a requirement its checker does
|
|
1921
|
+
not raise fails fast instead of finding nothing.
|
|
1922
|
+
|
|
1923
|
+
**Rows that decide what nothing decided before**: `F-INIT` (the `oa-deps-guard v1` block
|
|
1924
|
+
of `init.sh`, byte-equal to the template — the same two markers
|
|
1925
|
+
`api/tests/scripts/infra-init-scripts.bats` matches on, reaching the biz repositories
|
|
1926
|
+
that bats file says are kept in sync by review only), `F-JEST`, `F-RUNNER` (the one-shot
|
|
1927
|
+
test runner's shape: one `test` profile, its own `1g` budget, no `restart`, the same
|
|
1928
|
+
build, env and networks as the service), `S-TEST`, `S-ALL-C`, `S-INT-C`, `S-COOKBOOKS`,
|
|
1929
|
+
`S-HOST`, `S-HOOKS`, `C-SERVICE`, `C-OPS`, `C-CONTRACT`, `C-ENV`, `C-LINT`,
|
|
1930
|
+
`G-SHARED-ENV`, `R-NODE`, `R-MEM`, `R-PORTS-DEV`, `D-DB-DECL`, `D-DB-CONSISTENT`.
|
|
1931
|
+
|
|
1932
|
+
Three of those fill a measured gap rather than repeating a gate: `R-NODE` compares the
|
|
1933
|
+
Node major against `api/.nvmrc`, where `R3` of the deploy contract only compares CI,
|
|
1934
|
+
Dockerfile and `engines.node` against each other — five of the eight services are
|
|
1935
|
+
internally consistent on Node 22 while the platform runs 24; `R-PORTS-DEV` reads the dev
|
|
1936
|
+
compose, which `R4` never does; and the memory norm had no gate at all.
|
|
1937
|
+
|
|
1938
|
+
A container script's body is parameterised by `${runner}`, resolved from the compose
|
|
1939
|
+
service carrying `profiles: [test]`, so seven spellings of one body are one body and not
|
|
1940
|
+
seven findings. `S-INT-C` carries `only_with: tests/integration`: a script pointing at an
|
|
1941
|
+
absent suite is a declaration with nothing behind it, and jest exits 1 on a path that
|
|
1942
|
+
matches no test.
|
|
1943
|
+
|
|
1944
|
+
### Changed — `templates/business-service` reconciled with the rows it is the reference for
|
|
1945
|
+
|
|
1946
|
+
The template is the reference bearer of every `identical` row, so a row that is red on it
|
|
1947
|
+
is a test of the row (lead decision, `api/shared/TODO.md` §0.2b-9 point 4). It gains
|
|
1948
|
+
`scripts["test:all:container"]`, the canonical `scripts.test` body, and
|
|
1949
|
+
`config/biz-docs-lint.tree.json`; its README no longer offers the bare host scripts as a
|
|
1950
|
+
second way to run a suite, because the runner is the only rail
|
|
1951
|
+
(`biz-test-container` 001). `npx oa-validate` over the template reports nothing.
|
|
1952
|
+
|
|
1953
|
+
|
|
1954
|
+
### Added — `oa-sync-template readme-uniform`, the rendered uniform pointer of a library README
|
|
1955
|
+
|
|
1956
|
+
Every library's `README.md` now carries a generated region between
|
|
1957
|
+
`<!-- BEGIN GENERATED: library-uniform -->` markers: the line
|
|
1958
|
+
`Uniform: [library/<category>](<link to manifests/library.manifest.json>)` and the duty
|
|
1959
|
+
sections that apply to that category, with the ids of their rows. The content is read
|
|
1960
|
+
from the library manifest and the package's own `oa.category`, the link is computed from
|
|
1961
|
+
the two paths, and `--check` exits 1 with a diff when a region on disk is not what the
|
|
1962
|
+
manifest renders. Marker shape and idea are `scripts/ci/sync-biz-facts.mjs`, so the
|
|
1963
|
+
workspace keeps one shape of generated region.
|
|
1964
|
+
|
|
1965
|
+
Discoverability of the uniform was a header per file in 003 §17; 005 point 2 replaced it
|
|
1966
|
+
with one rendered pointer per repository, and 0 of 28 library READMEs carried one. A
|
|
1967
|
+
library declaring no `oa.category` is announced `NOT RUN` and never guessed at — that
|
|
1968
|
+
absence is the blocking finding `U-MISMATCH` of `oa-validate --library`, which owns it.
|
|
1969
|
+
|
|
1970
|
+
### Changed — the verdict is a property of the uniform, not of the runner
|
|
1971
|
+
|
|
1972
|
+
Which severities stop a bearer, and the two words its verdict is printed with, are
|
|
1973
|
+
now declared by the manifest: `blocking_severities` and
|
|
1974
|
+
`verdict: { blocked, clear }`, required by the manifest's check on itself
|
|
1975
|
+
(`M-BLOCKING`, `M-VERDICT`). `runManifest` reads them instead of a global constant
|
|
1976
|
+
and carries them in the result; `report.js` prints them.
|
|
1977
|
+
|
|
1978
|
+
Before this, one list held `boot`, `deploy` and `publish`, so a service run ended
|
|
1979
|
+
with `NOT DEPLOYABLE — N finding(s) of severity boot|deploy|publish` — offering a
|
|
1980
|
+
consequence no service row can raise — and the library CLI kept its own second list
|
|
1981
|
+
to avoid the same sentence. That duplicate (`LIBRARY_BLOCKING` in
|
|
1982
|
+
`src/cli/oa-validate.js`) is gone. A service now reads `… of severity boot|deploy`
|
|
1983
|
+
and `DEPLOYABLE — no findings`; a library reads `… of severity publish` and
|
|
1984
|
+
`PUBLISHABLE — no findings`.
|
|
1985
|
+
|
|
1986
|
+
### Changed — `L-CONNECTOR-ENV` exempts the config module in either shape
|
|
1987
|
+
|
|
1988
|
+
The row's parameter is now `allowed: ["src/config/", "src/config.js"]` instead of
|
|
1989
|
+
`allowed_dir: "src/config"`: an entry ending in `/` exempts a directory, any other
|
|
1990
|
+
entry exempts that one file. The duty is about the concept — env reading lives in
|
|
1991
|
+
one config module — and a package small enough for a single `src/config.js` wears
|
|
1992
|
+
the same uniform as one with a directory. Measured over the workspace: the row falls
|
|
1993
|
+
from 2 findings to 1, and the one that remains
|
|
1994
|
+
(`conn-base-db/src/createSequelize.js`) is a real finding.
|
|
1995
|
+
|
|
1996
|
+
### Changed — `L-README` asks a library README for `> Owns:` and `> Status:`
|
|
1997
|
+
|
|
1998
|
+
The row used to demand `Parent:`, `Owns:` and a `Status` section, citing
|
|
1999
|
+
`api/docs/biz/DOC-STANDARD.md` — a header that standard reserves for the biz
|
|
2000
|
+
documentation tree, which `api/shared/**` is not part of. It now asks for the node
|
|
2001
|
+
header of `api/docs/standards/INFRA-DOC-STANDARD.md`: a `> Owns:` line carrying a
|
|
2002
|
+
value, and a `> Status:` line whose value is `current`, `draft` or `archived`.
|
|
2003
|
+
`Parent:` is neither required nor forbidden — a library's parent is the rendered
|
|
2004
|
+
category line, so a hand-written one would be a second owner of one fact.
|
|
2005
|
+
|
|
2006
|
+
### Added — the uniform manifest and `npx oa-validate` (towards 8.0.0)
|
|
2007
|
+
|
|
2008
|
+
`manifests/biz-service.manifest.json` is the machine-readable declaration of what a
|
|
2009
|
+
biz service is, shipped inside this package so that the manifest version IS the
|
|
2010
|
+
validator version — the pin decides the shape, and no version field exists in the
|
|
2011
|
+
file (confirmation `biz-service-manifest` 004).
|
|
2012
|
+
|
|
2013
|
+
The core lands here; the rows that describe the whole uniform follow in the same
|
|
2014
|
+
major:
|
|
2015
|
+
|
|
2016
|
+
- `src/manifest/` — loader, the manifest's check on ITSELF (required row fields,
|
|
2017
|
+
unique ids, valid severity, a registered check with its parameters, a non-empty
|
|
2018
|
+
`doc`, and no copied list of names), discovery of bearers from the disk with
|
|
2019
|
+
`from:` references resolved against the workspace, a registry of checks, and the
|
|
2020
|
+
report;
|
|
2021
|
+
- rows `X-HOOKS` and `X-PREVAL` (`files.forbidden`, severity `deploy`) — the hook
|
|
2022
|
+
the owner deleted and the per-service pre-validation script the library command
|
|
2023
|
+
replaced;
|
|
2024
|
+
- row `U-ORPHAN` — a directory under the discovery pattern that matches no pattern,
|
|
2025
|
+
or that `api/config/services.json` does not declare;
|
|
2026
|
+
- `bin.oa-validate` — one table `id · severity · where · what · fix · owner`, the
|
|
2027
|
+
`doc` pointer per reported row, `--json`, `--workspace`, exit `0` / `1` (`2` when
|
|
2028
|
+
the run cannot start).
|
|
2029
|
+
|
|
2030
|
+
Where the workspace is not reachable — inside a service container it is not — the
|
|
2031
|
+
rows that need it print `NOT RUN` with the reason, never a pass.
|
|
2032
|
+
|
|
2033
|
+
Measured on the eight biz repositories the day it landed: converter clean, the other
|
|
2034
|
+
seven carrying `hooks/pre-commit`, `scripts/run-pre-validation.js` or both; zero
|
|
2035
|
+
`U-ORPHAN`.
|
|
2036
|
+
|
|
2037
|
+
- **Step 7/7 "Manifest conformance", the banner and `ci/deployability.json`.** The
|
|
2038
|
+
manifest now runs at every boot, against the service root the orchestrator was
|
|
2039
|
+
constructed with (never `process.cwd()`). A `boot` finding fails the validation as
|
|
2040
|
+
before; a `deploy` finding lets the service start and sets `results.deployable =
|
|
2041
|
+
false` with the rows in `results.deployFindings`, prints the banner
|
|
2042
|
+
`NOT DEPLOYABLE — N finding(s)` plus the table through the injected logger, and
|
|
2043
|
+
writes the verdict to `ci/deployability.json` for `deploy-production` to refuse on.
|
|
2044
|
+
A clean run says `DEPLOYABLE — manifest conformance: 0 finding(s), K not run`; the
|
|
2045
|
+
signal carries the NOT RUN rows too, because a row that could not look is not a
|
|
2046
|
+
pass. No timestamp in the file — an unchanged tree writes an unchanged file. A run
|
|
2047
|
+
that stops at an earlier fail-fast step reports `deployable: null`, which is
|
|
2048
|
+
"unmeasured", not "fine". `oa-validate --workspace <root>` without a service root is
|
|
2049
|
+
the workspace run: every workspace-scoped finding, and the per-service rows NOT RUN.
|
|
2050
|
+
In a single service's run those rows report only what lies under that service — and
|
|
2051
|
+
where the service root does not lie under the workspace root at all, they are NOT RUN
|
|
2052
|
+
with that reason rather than silently filtered to nothing.
|
|
2053
|
+
Measured: hello-service 2 findings and `deployable: false`, converter 0 and
|
|
2054
|
+
`deployable: true`.
|
|
2055
|
+
|
|
2056
|
+
|
|
2057
|
+
### Added — the library uniform and `npx oa-validate --library`
|
|
2058
|
+
|
|
2059
|
+
`manifests/library.manifest.json` — the same concept for the 28 shared libraries, in
|
|
2060
|
+
the same package and therefore on the same version, so the two shapes cannot drift
|
|
2061
|
+
apart (confirmation `biz-service-manifest` 002 §10–§15). A library has no boot, so its
|
|
2062
|
+
rows carry one severity, `publish`: the choke point is `scripts/publish-library.sh`,
|
|
2063
|
+
and what fails there never becomes an artefact.
|
|
2064
|
+
|
|
2065
|
+
- **Discovery, not a list.** `api/shared/**/package.json` minus `**/node_modules/**`
|
|
2066
|
+
and `**/tests/fixtures/**`, bound to `api/config/libraries.json` by a `from:`
|
|
2067
|
+
reference. `U-ORPHAN` reports both directions: a package the SSOT does not know, and
|
|
2068
|
+
a name in the SSOT with no package. No library name appears in the manifest, and the
|
|
2069
|
+
validator's own fixtures are excluded by a manifest row rather than by a branch in
|
|
2070
|
+
the code (002 §13.1).
|
|
2071
|
+
- **Categories as definitions.** `core`, `connector`, `orchestration`, `runtime`,
|
|
2072
|
+
`tooling`, each with the categories it `may_depend_on` — itself included, explicitly,
|
|
2073
|
+
because within a layer is allowed and upward is not. The category is DECLARED
|
|
2074
|
+
(`"oa": { "category": … }`) and verified against the dependency graph; a missing
|
|
2075
|
+
declaration and a declaration the graph contradicts are both `U-MISMATCH` (005.3).
|
|
2076
|
+
`orchestration` gets no row of its own: "depends only downward" is what `U-MISMATCH`
|
|
2077
|
+
already decides.
|
|
2078
|
+
- **Rows.** Every library: `L-MAIN`, `L-ENGINES` (against the platform major,
|
|
2079
|
+
referenced from `api/.nvmrc`), `L-TESTS`, `L-TEST-SCRIPT`, `L-PACK-TESTS`, `L-PINS`,
|
|
2080
|
+
`L-NO-FILE-RANGE`, `L-CHANGELOG`, `L-README`, `L-CONSUMER`. Per category:
|
|
2081
|
+
`L-CORE-DEPS`, `L-CORE-EXPORTS`, `L-CONNECTOR-ENV`, `L-RUNTIME-CLIENT`, `L-TOOLING`.
|
|
2082
|
+
`L-PACK-TESTS` measures the effect (`files` allowlist, then `.npmignore`, then
|
|
2083
|
+
`.gitignore`), never the presence of a file — eight packages keep `tests/` out with
|
|
2084
|
+
an allowlist alone, and a row checking for `.npmignore` would report all eight as
|
|
2085
|
+
broken. Falsified against `npm pack --dry-run` over all 28: both say clean, 28/28.
|
|
2086
|
+
- **What is deliberately not a row** is written into the manifest's `guidance` block
|
|
2087
|
+
with its reason and its decision — `L-LINT` (002 §14), the logger/error contract, the
|
|
2088
|
+
boot-phase messages, "a removed export bumps the major", and the canonical body of
|
|
2089
|
+
the `test` script. A duty nothing can decide is guidance, not a rule (001 §6), and
|
|
2090
|
+
the shape check fails the build if a guidance entry grows a `check`.
|
|
2091
|
+
- **`oa-validate --library <packageDir>` and `--library --all`** — one table
|
|
2092
|
+
`id · category · where · what · fix`, the `doc` pointer per reported row, `--json`,
|
|
2093
|
+
exit `0` / `1` (`2` when the run cannot start). `--library` with neither a package
|
|
2094
|
+
nor `--all` is a usage error; nothing is guessed. `--all` runs every discovered
|
|
2095
|
+
package plus one whole-set run, because a name in the SSOT with no package on disk
|
|
2096
|
+
belongs to no package's root and no per-package run would ever show it.
|
|
2097
|
+
- `severity: publish` joins `boot` and `deploy` in the shape check; `from:` gained two
|
|
2098
|
+
shapes for the two SSOTs that are not arrays of objects — `names: "keys"` for an
|
|
2099
|
+
object map (`api/config/libraries.json`) and `text: true` for a file that is one
|
|
2100
|
+
value (`api/.nvmrc`). The service manifest is unchanged and its self-test unchanged.
|
|
2101
|
+
- **`ok` now answers for a library too.** `runManifest.BLOCKING_SEVERITIES` carries
|
|
2102
|
+
`publish` beside `boot` and `deploy`, so a run over a library returns `ok: false`
|
|
2103
|
+
when a `publish` row fires; before, `ok` was computed from the service severities
|
|
2104
|
+
alone and a library run reported `ok: true` with findings on the table. One fact,
|
|
2105
|
+
one owner — a consumer no longer has to recompute the verdict per uniform.
|
|
2106
|
+
`LIBRARY_MANIFEST_PATH` moved from the CLI to `src/manifest/loadManifest.js` beside
|
|
2107
|
+
`DEFAULT_MANIFEST_PATH`, for the same reason: where the manifest lives is a fact
|
|
2108
|
+
about the package, not about the CLI, and a boot step reads it without one.
|
|
2109
|
+
|
|
2110
|
+
Measured over the 28 libraries the day it landed: 119 findings — `U-MISMATCH` 28 (no
|
|
2111
|
+
package declares a category yet), `L-ENGINES` 28 (no package declares the platform
|
|
2112
|
+
major), `L-README` 28, `L-CHANGELOG` 21, `L-PINS` 13 (all one uncommitted
|
|
2113
|
+
`logger-contract` 1.0.0 → 1.1.0 wave, not a defect of the pins), `L-CONSUMER` 1
|
|
2114
|
+
(`conn-base-db`, which nothing in the workspace depends on), `U-ORPHAN` 0. Nothing in
|
|
2115
|
+
the libraries was changed by this entry; that is the next batch.
|
|
2116
|
+
|
|
2117
|
+
### Added — the platform env manifest and `bin.oa-sync-template` (towards 8.0.0)
|
|
2118
|
+
|
|
2119
|
+
- `src/sync/sharedEnv.js` — `renderSharedEnv(manifest)` and `checkSharedEnv(manifest, text)`,
|
|
2120
|
+
both pure: the file text `api/config/shared-env.json` describes, and whether a file on
|
|
2121
|
+
disk is still that text. Nothing here reads the environment or a path; the caller loads,
|
|
2122
|
+
reads and writes. `consumers` is deliberately not rendered — who reads a key is a fact
|
|
2123
|
+
the code owns.
|
|
2124
|
+
- `bin.oa-sync-template` — `oa-sync-template shared-env --target <dir> [--workspace <root>]
|
|
2125
|
+
[--check]` writes `<dir>/config/env-templates/shared.env` from that manifest, or exits 1
|
|
2126
|
+
with the diff under `--check` (2 when the run cannot start). No flag points it at another
|
|
2127
|
+
manifest: the shared key set has one owner, beside the SSOT.
|
|
2128
|
+
- Measured on the two copies this repository owns: the platform template and the
|
|
2129
|
+
business-service template are now rendered output, key-for-key identical to the platform
|
|
2130
|
+
file they came from. The business-service copy was 4 keys/values adrift (`LOG_LEVEL`
|
|
2131
|
+
absent, `guest:guest` and two `minioadmin` values where the platform file says
|
|
2132
|
+
`CHANGE_ME`).
|
|
2133
|
+
|
|
2134
|
+
Decision: confirmation `biz-service-manifest` 003 §18, 004.
|
|
2135
|
+
|
|
2136
|
+
### Changed — `verify-test-coverage` asks the DISK too, not only jest
|
|
2137
|
+
|
|
2138
|
+
**BREAKING.** The gate compared what jest matches against what the `test:all`
|
|
2139
|
+
chain and the declared stack tiers run — and all three asked the same jest. A
|
|
2140
|
+
`testMatch` narrowed to `tests/unit` therefore hid `tests/integration/` from
|
|
2141
|
+
every side at once, and the run printed `OK … 1 file(s) matched, 1 run by the
|
|
2142
|
+
test:all chain` with a whole suite on disk running nowhere (measured 2026-09-08).
|
|
2143
|
+
|
|
2144
|
+
`verifyTestCoverage` now also walks the repository for platform test files
|
|
2145
|
+
(`**/tests/**/*.test.js` — the `testMatch` `templates/business-service/jest.config.js`
|
|
2146
|
+
declares, used verbatim by all eight biz repositories) and reports every one the
|
|
2147
|
+
service's jest config does not match, as `TEST_NOT_MATCHED`. `node_modules` and
|
|
2148
|
+
`.git` are not walked; a file under `tests/` that is not a test file
|
|
2149
|
+
(`tests/helpers/*.js`, fixtures, `tests/setup-env.js`) is never reported.
|
|
2150
|
+
`TEST_COVERAGE_SCOPE` names the new side, so the printed scope still describes
|
|
2151
|
+
what the gate looked at.
|
|
2152
|
+
|
|
2153
|
+
Repositories that already keep the platform `testMatch` are unaffected: the eight
|
|
2154
|
+
biz repositories return exactly the verdicts they returned before (7 × OK,
|
|
2155
|
+
hello-service `TEST_RUNS_NOWHERE`).
|
|
2156
|
+
|
|
2157
|
+
### Changed — every error this package throws names the context that threw it
|
|
2158
|
+
|
|
2159
|
+
Each thrown message now carries `[Context] Problem - Expected/Fix`, chained failures
|
|
2160
|
+
pass their origin as `cause` instead of interpolating its text, and a static guard
|
|
2161
|
+
(`tests/unit/error-message-contract.test.js`) fails the suite if a bare message returns.
|
|
2162
|
+
|
|
2163
|
+
### Changed — `step.service` names a registered service, or the step fails
|
|
2164
|
+
|
|
2165
|
+
**BREAKING.** `CookbookTestRunner._resolveServiceRoot` no longer falls back to
|
|
2166
|
+
`servicePath` for an unregistered name, so a typo in a cookbook's `step.service`
|
|
2167
|
+
is refused (`Unknown step.service "<x>" - Expected one of: <registered names>`)
|
|
2168
|
+
instead of quietly running the runner's own handler and reporting PASS.
|
|
2169
|
+
|
|
2170
|
+
### Removed — the integration minimum no longer checks `test:all` by NAME
|
|
2171
|
+
|
|
2172
|
+
`verifyIntegrationMinimum` required the literal substring `test:integration` in
|
|
2173
|
+
`test:all` — a rule about a name, which passed for `echo test:integration` and
|
|
2174
|
+
failed for a chain reaching the same files through another script; the fact it
|
|
2175
|
+
was standing in for is now measured by `verify-test-coverage`, so the second rail
|
|
2176
|
+
is gone. The other three checks stay: `test:integration` must exist, it must not
|
|
2177
|
+
carry `--passWithNoTests`, and `test:all` must exist.
|
|
2178
|
+
|
|
2179
|
+
### Added — `verify-test-coverage`: every test runs somewhere, or says why it cannot
|
|
2180
|
+
|
|
2181
|
+
**BREAKING (the check is folded into `verify-contract`).** A new command, and a
|
|
2182
|
+
new optional contract field `stackTiers`. The gate compares three sets, all of
|
|
2183
|
+
them answered by the service's own jest rather than by parsing its scripts:
|
|
2184
|
+
|
|
2185
|
+
| | what | how |
|
|
2186
|
+
|---|---|---|
|
|
2187
|
+
| T | every file the service jest config matches | `jest --listTests` |
|
|
2188
|
+
| A | every file the `test:all` chain runs | the chain decomposed `npm run …` → jest, each invocation asked `--listTests` |
|
|
2189
|
+
| B | every file the declared stack tiers run | the same, per declared script |
|
|
2190
|
+
|
|
2191
|
+
A file in T that is in neither A nor B is a suite that runs nowhere: green
|
|
2192
|
+
whenever somebody runs it by hand, in no pipeline, and reported by nothing.
|
|
2193
|
+
Measured over the eight biz repositories on 2026-09-07 — biz-hello matched 17
|
|
2194
|
+
files and ran 14.
|
|
2195
|
+
|
|
2196
|
+
`stackTiers` is how a suite legitimately stands outside `test:all`:
|
|
2197
|
+
|
|
2198
|
+
```json
|
|
2199
|
+
"stackTiers": [
|
|
2200
|
+
{ "script": "test:e2e", "requires": "needs a live biz-hello consumer on biz-hello.workflow; CI has no service runtime" }
|
|
2201
|
+
]
|
|
2202
|
+
```
|
|
2203
|
+
|
|
2204
|
+
`requires` is mandatory, for the same reason `why` is mandatory in the `env`
|
|
2205
|
+
block: a tier nobody can justify is the one nobody dares delete. There is no
|
|
2206
|
+
exemption by directory name — `tests/e2e/**` is not self-evidently un-runnable,
|
|
2207
|
+
and a name cannot carry a reason.
|
|
2208
|
+
|
|
2209
|
+
Five violations, each naming the fix: a test that runs nowhere; a declared
|
|
2210
|
+
script `package.json` does not have; a declared script that lists no test file
|
|
2211
|
+
(a declaration with no consumer); a declared script whose tests `test:all`
|
|
2212
|
+
already runs (the declaration states something untrue); and — refused when the
|
|
2213
|
+
contract loads — an entry without `requires`. A step in the chain the gate
|
|
2214
|
+
cannot decompose is reported too, never skipped: an unreadable step would make
|
|
2215
|
+
every count a guess.
|
|
2216
|
+
|
|
2217
|
+
**BREAKING for repositories:** `verify-contract` now runs this check, so from
|
|
2218
|
+
8.0.0 a repo with a test file outside `test:all` and outside a declaration is
|
|
2219
|
+
RED. At the pin, that is biz-hello and only biz-hello
|
|
2220
|
+
(`tests/e2e/mq-invocation.test.js`, which needs a live consumer): it declares
|
|
2221
|
+
the tier, and goes green. The other seven repositories are already clean —
|
|
2222
|
+
measured 2026-09-07: converter 66/66, emailer 28/28, ingest 24/24, invoicing
|
|
2223
|
+
36/36, meta 51/51, pdfgen 17/17, property 163/163.
|
|
2224
|
+
|
|
2225
|
+
**What the gate does not measure, and says so on every run:** whether a declared
|
|
2226
|
+
stack tier is ever RUN. The declaration says why the suite cannot run in
|
|
2227
|
+
`test:all`; it never claims something else runs it.
|
|
2228
|
+
|
|
2229
|
+
### Added — `getForeignTestNamespace()`, and R8 accepts it
|
|
2230
|
+
|
|
2231
|
+
**Minor, additive.** Beside `getTestNamespace()` the package now exports
|
|
2232
|
+
`getForeignTestNamespace()`: a namespace an integration test must NOT see rows
|
|
2233
|
+
from, for the isolation tests that prove a handler filters on tenant_id AND
|
|
2234
|
+
workspace_id. It returns another member of the allowed environment classes,
|
|
2235
|
+
chosen by position in that list and wrapping at its end — deterministic per
|
|
2236
|
+
environment, with the caller's own workspace, and validated by the same
|
|
2237
|
+
whitelist, the same refusal message and the same missing-variable message as
|
|
2238
|
+
`getTestNamespace()`.
|
|
2239
|
+
|
|
2240
|
+
Why the library owns it (owner decision 2026-09-07, via BIZ-hello): the two
|
|
2241
|
+
alternatives a repo reaches for are both what deploy-contract R8 exists to
|
|
2242
|
+
refuse — a literal (biz-hello asserted against tenant 101, the live Meditest
|
|
2243
|
+
customer, so its isolation test wrote into real data) or arithmetic
|
|
2244
|
+
(`tenant + 1`, which turns the platform default 99 into 100, LIVE). Reading a
|
|
2245
|
+
second env variable per repo was rejected as a duplicate rail.
|
|
2246
|
+
|
|
2247
|
+
R8's permit in `utils/deployContract.js` now recognises a call to either helper,
|
|
2248
|
+
on a line that executes; a mention in a comment still earns nothing.
|
|
2249
|
+
|
|
2250
|
+
### Fixed — the deploy-contract gate reports the scope it actually checks
|
|
2251
|
+
|
|
2252
|
+
**Patch — messages only, no rule changed.** `verify-contract`,
|
|
2253
|
+
`verify-deploy-contract` and the green line
|
|
2254
|
+
`[BizCiGate] OK deploy-contract (<service>) — … satisfied` all announced
|
|
2255
|
+
`R1-R7`, hand-written in four places. R8 (one source for tenant / workspace
|
|
2256
|
+
identity) had been part of the run for weeks, and R6 has never been part of it —
|
|
2257
|
+
it is the library-compatibility gate `verify-lib-compat`. So the gate claimed one
|
|
2258
|
+
check it does not run and hid one it does.
|
|
2259
|
+
|
|
2260
|
+
The scope now has a single owner: `DEPLOY_CONTRACT_REQUIREMENTS` in
|
|
2261
|
+
`utils/deployContract.js`, the module's own list of the ids it can raise, with
|
|
2262
|
+
`DEPLOY_CONTRACT_SCOPE` (`R1-R5, R7-R8`) rendered from it. Both are exported for
|
|
2263
|
+
callers that need to name the scope. `verifyDeployContract` refuses an id that is
|
|
2264
|
+
not on the list, so the list and the code cannot drift apart again.
|
|
2265
|
+
|
|
2266
|
+
Reported by BIZ-hello (`api/shared/TODO.md`, "biz-ci-gate hlásí menší pokrytí,
|
|
2267
|
+
než jaké odbaví").
|
|
2268
|
+
|
|
2269
|
+
### BREAKING — `SETUP_DOCS` moves from `docs/setup/` to `docs/80-setup/`
|
|
2270
|
+
|
|
2271
|
+
**Major, behavioural.** The installation contract now requires
|
|
2272
|
+
`docs/80-setup/{INSTALL,PLATFORM_MATRIX,VALIDATION}.md`. It required
|
|
2273
|
+
`docs/setup/{…}.md` before; the file names are unchanged, the branch is not.
|
|
2274
|
+
There is no tolerance of the old path and no fallback — a repo that still
|
|
2275
|
+
carries `docs/setup/` fails `verify-install-contract` (and `verify-contract`,
|
|
2276
|
+
which folds it in) with three `SETUP_DOCS` violations
|
|
2277
|
+
(`architecture-principles.md` §3, §11).
|
|
2278
|
+
|
|
2279
|
+
The contract had been the reason seven biz repos violated `docs/biz/DOC-STANDARD.md`
|
|
2280
|
+
rule 7, which requires numbered branches `NN-name`: converter measured that
|
|
2281
|
+
renaming to a numbered branch satisfied lint S006 and immediately broke the CI
|
|
2282
|
+
gate. The owner decided the contract yields to the standard, not the other way
|
|
2283
|
+
round — `api/docs/governance/confirmations/docs-setup-branch-naming.md`
|
|
2284
|
+
confirmation 001, verdict "B — kontrakt: SETUP_DOCS přejmenovat na `NN-setup/`".
|
|
2285
|
+
The number `80` is BIZ-general's: property, the only numbered biz tree today,
|
|
2286
|
+
already carries `docs/80-setup/`; `30` would collide with property's
|
|
2287
|
+
`30-recurring`; and setup is an operational appendix sitting behind the contract
|
|
2288
|
+
nodes, in the same band as the platform tree's `80-decisions` / `90-migration`.
|
|
2289
|
+
|
|
2290
|
+
**How a repo adopts it.** The rename and the pin of this package's new version
|
|
2291
|
+
land in the SAME commit of each repo (`git mv docs/setup docs/80-setup` plus the
|
|
2292
|
+
`@onlineapps/conn-orch-validator` bump). Between the publish and that commit the
|
|
2293
|
+
repo's CI is red, so the two halves are never split across commits
|
|
2294
|
+
(`automation-gates.md` §3 — a gate lands with its compliance, not ahead of it).
|
|
2295
|
+
The `Fix:` half of the violation message says so on every run: *"add
|
|
2296
|
+
docs/80-setup/INSTALL.md to the repository; a repo carrying the older
|
|
2297
|
+
docs/setup/ branch renames it to docs/80-setup/."*
|
|
2298
|
+
|
|
2299
|
+
### Removed — the documented `url` tolerance in `ServiceReadinessValidator`
|
|
2300
|
+
|
|
2301
|
+
**Patch — no behaviour changes.** The class docblock carried a paragraph
|
|
2302
|
+
explaining that the retired probe's `url` may still arrive from a stale caller,
|
|
2303
|
+
and a test asserted that tolerance. Neither had a subject, and the paragraph said
|
|
2304
|
+
so itself: the only caller that ever passed a url was Tier-1's readiness step,
|
|
2305
|
+
retired 2026-08-22, and the one consumer left —
|
|
2306
|
+
`helpers/createServiceReadinessTests`, and through it every biz
|
|
2307
|
+
`tests/bootstrap/` suite — passes name, version, operations, testCookbook and
|
|
2308
|
+
registry. A tolerance nobody exercises still tells the reader that some caller
|
|
2309
|
+
depends on it (`change-discipline.md` § Removing something removes its
|
|
2310
|
+
declaration).
|
|
2311
|
+
|
|
2312
|
+
The docblock now states what `validateReadiness(service)` reads: `name`,
|
|
2313
|
+
`version`, `operations`, `testCookbook`, `registry`. Nothing replaced the
|
|
2314
|
+
tolerance — the method destructures the fields it needs and looks at no others,
|
|
2315
|
+
the same decision as `ValidationOrchestrator` for the same reasons (a
|
|
2316
|
+
per-class unknown-key rejection would be a second rail and a gate without its
|
|
2317
|
+
compliance sweep, `automation-gates.md` §3). A caller that still passes a `url`
|
|
2318
|
+
is unaffected, and the test that now stands asserts exactly that alongside the
|
|
2319
|
+
removal.
|
|
2320
|
+
|
|
2321
|
+
### Removed — the documented `serviceUrl` tolerance in `ValidationOrchestrator`
|
|
2322
|
+
|
|
2323
|
+
**Patch — no behaviour changes.** The class docblock promised that
|
|
2324
|
+
`options.serviceUrl` is "accepted-and-ignored: existing callers (ServiceWrapper,
|
|
2325
|
+
the biz `scripts/run-pre-validation.js`) still pass one". That sentence had gone
|
|
2326
|
+
false in both halves: `ServiceWrapper._createValidationOrchestrator()` says in
|
|
2327
|
+
its own comment that `serviceUrl` is NOT passed, and the biz
|
|
2328
|
+
`scripts/run-pre-validation.js` hand theirs to `CookbookTestRunner`, never to
|
|
2329
|
+
this class. A tolerance nobody exercises still tells every reader that some
|
|
2330
|
+
caller depends on it, which is the declaration `change-discipline.md` § Removing
|
|
2331
|
+
something removes its declaration exists to delete. The two tests asserting the
|
|
2332
|
+
tolerance, and the `serviceUrl` key in two test fixtures, went with it.
|
|
2333
|
+
|
|
2334
|
+
**Nothing was added in its place, and that is a decision.** The constructor reads
|
|
2335
|
+
`serviceRoot`, `serviceName`, `serviceVersion` and `logger`, and looks at no
|
|
2336
|
+
other key — the same way `CookbookTestRunner` and every other component in this
|
|
2337
|
+
package treat their options. Rejecting unknown keys would put a new mechanism on
|
|
2338
|
+
one class only (a second rail), and would be a gate landing without the
|
|
2339
|
+
compliance sweep `automation-gates.md` §3 requires. A caller that still passes a
|
|
2340
|
+
`serviceUrl` is therefore unaffected, exactly as before.
|
|
2341
|
+
|
|
2342
|
+
### Removed — `run-prevalidation` no longer loads the service config to fetch a URL nobody reads
|
|
2343
|
+
|
|
2344
|
+
**Minor, behavioural.** `resolveServiceUrl()` is gone from `src/cli/biz-ci-gate.js`
|
|
2345
|
+
and the `serviceUrl` parameter is gone from `runPreValidation`
|
|
2346
|
+
(`src/utils/preValidation.js`).
|
|
2347
|
+
|
|
2348
|
+
It resolved the service's own `@onlineapps/service-wrapper`, ran
|
|
2349
|
+
`ConfigLoader.loadAll()` and handed `config.service.url` to `CookbookTestRunner`
|
|
2350
|
+
— which never read it (`this.serviceUrl` appears nowhere in that class). And the
|
|
2351
|
+
value had stopped existing: the owner removed `port` and `url` from the service
|
|
2352
|
+
shape (`docs/governance/confirmations/biz-service-port-url.md` 001), so
|
|
2353
|
+
`loadAll()` does not return `service.url` any more. Measured against the
|
|
2354
|
+
installed loader with a `config.json` that still declares the field:
|
|
2355
|
+
|
|
2356
|
+
```
|
|
2357
|
+
WITH url in config.json -> config.service.url = undefined
|
|
2358
|
+
service keys = name,version,specificationEndpoint,description,workspaceScoped,env
|
|
2359
|
+
```
|
|
2360
|
+
|
|
2361
|
+
So the resolution had been yielding `undefined` for every service on the
|
|
2362
|
+
platform. What it still did was make an installed wrapper, a readable
|
|
2363
|
+
`package.json`, a declared `specificationEndpoint` and a resolvable `${ENV}`
|
|
2364
|
+
placeholder into preconditions of a cookbook run that depends on none of them —
|
|
2365
|
+
a required declaration with no consumer, which is worse than none
|
|
2366
|
+
(`change-discipline.md` § Removing something removes its declaration).
|
|
2367
|
+
|
|
2368
|
+
Running cookbooks needs `tests/cookbooks/`, `operations.json` and the handler
|
|
2369
|
+
modules. A service that ships none of the rest now runs its gate; a service that
|
|
2370
|
+
still declares `service.url` is unaffected, because the field is ignored rather
|
|
2371
|
+
than refused. The test that asserted the old refusal (`@onlineapps/service-wrapper
|
|
2372
|
+
is not installed`) went with the code that produced it.
|
|
2373
|
+
|
|
2374
|
+
### Added — `utils/handlerRef`: one definition of what a handler ref is and what resolving it means
|
|
2375
|
+
|
|
2376
|
+
**Minor.** New exports on the package index: `HANDLER_REF_PATTERN`,
|
|
2377
|
+
`parseHandlerRef(ref)` and `resolveHandlerModule({ serviceRoot, handlerRef })`.
|
|
2378
|
+
|
|
2379
|
+
The rule lived in five places. Four were copies of the same regex literal
|
|
2380
|
+
checking only the SHAPE — `ServiceReadinessValidator`, `ValidationOrchestrator`,
|
|
2381
|
+
`ServiceStructureValidator`, `helpers/createServiceReadinessTests`. The fifth,
|
|
2382
|
+
`CookbookTestRunner.resolveOperation`, actually resolved a ref, and did it more
|
|
2383
|
+
weakly than production. Measured against the new fixture before the change:
|
|
2384
|
+
|
|
2385
|
+
```
|
|
2386
|
+
double-hash -> { modulePath: '…/src/handlers/v3/good.js', exportName: 'run' }
|
|
2387
|
+
escapes-src -> { modulePath: '…/v3-service-broken-handler/outside.js', exportName: 'run' }
|
|
2388
|
+
missing-export -> { modulePath: '…/src/handlers/v3/good.js', exportName: 'nope' }
|
|
2389
|
+
```
|
|
2390
|
+
|
|
2391
|
+
`handlers/v3/good#run#extra` was silently truncated and dispatched;
|
|
2392
|
+
`../outside#run` loaded and ran a module from OUTSIDE the service source root;
|
|
2393
|
+
an export that does not exist got a descriptor. Cookbooks run at every biz
|
|
2394
|
+
service boot, so that was the loosest resolution on the platform sitting on the
|
|
2395
|
+
hottest path.
|
|
2396
|
+
|
|
2397
|
+
The two functions are separate on purpose, and that is what lets
|
|
2398
|
+
`service-wrapper`'s `HandlerLoader` adopt them as an internal swap rather than
|
|
2399
|
+
an API change: `resolveHandlerModule` states exactly the rule the loader
|
|
2400
|
+
implements today (containment on the resolved location, `require`, the export is
|
|
2401
|
+
a function), while the stricter declared form — `handlers/<path>#<export>`, owned
|
|
2402
|
+
by `api/docs/biz/30-operations/schema-v3.md` § handler — stays in
|
|
2403
|
+
`parseHandlerRef`. Adopting the first narrows nothing the wrapper accepts.
|
|
2404
|
+
|
|
2405
|
+
### Changed — the cookbook runner resolves handlers by that rule, which makes it stricter
|
|
2406
|
+
|
|
2407
|
+
**Minor, behavioural.** `CookbookTestRunner.resolveOperation()` now refuses what
|
|
2408
|
+
it used to accept: more than one `#`, a ref outside the declared form (including
|
|
2409
|
+
anything reaching out of `src/`), a module that does not load, and an export that
|
|
2410
|
+
is missing or is not a function. The failures arrive at resolution instead of at
|
|
2411
|
+
the first invocation, and each names the ref, the resolved path and the fix.
|
|
2412
|
+
|
|
2413
|
+
A service whose cookbooks pass today keeps passing — every conformant ref
|
|
2414
|
+
resolves exactly as before. One whose `operations.json` carries a malformed ref
|
|
2415
|
+
now fails its cookbook step where it previously dispatched something the author
|
|
2416
|
+
did not write. The descriptor gained `handler` (the resolved function), and the
|
|
2417
|
+
dispatch no longer requires the module a second time.
|
|
2418
|
+
|
|
2419
|
+
### Added — the readiness suite verifies that every declared handler RESOLVES
|
|
2420
|
+
|
|
2421
|
+
**Minor.** `createServiceReadinessTests` generates one more test: *every declared
|
|
2422
|
+
handler resolves to an exported function*, through `resolveDeclaredHandlers`,
|
|
2423
|
+
which is exported from the helper module. Matching the ref against a regex said
|
|
2424
|
+
nothing about whether the module is in the image, so an operation naming a module
|
|
2425
|
+
nobody shipped passed readiness in CI and failed at boot, where
|
|
2426
|
+
`HandlerRegistry.validate()` resolves every declared operation and fail-fasts.
|
|
2427
|
+
The verdict was right in the end; it arrived one environment too late. The check
|
|
2428
|
+
fail-fasts on the first unusable handler and names the operation.
|
|
2429
|
+
|
|
2430
|
+
### Changed — the `getTestNamespace()` refusals name the CI half of the fix, not only the local file
|
|
2431
|
+
|
|
2432
|
+
**Patch.** All three fail-fast messages of `utils/testNamespace` (missing value,
|
|
2433
|
+
non-integer value, refused tenant class) pointed at `config/env-active/shared.env`
|
|
2434
|
+
and nothing else. That file exists on a developer's machine and in no CI job: CI
|
|
2435
|
+
takes the testing pair from the pipeline variables, not from a file in the
|
|
2436
|
+
repository (`api/docs/standards/tenant-allocation.md` § Env variables). So the
|
|
2437
|
+
one place the message is read by somebody who cannot act on it — a red pipeline —
|
|
2438
|
+
was the one place its `Fix:` was unusable, which is the defect
|
|
2439
|
+
`architecture-principles.md` §5 names.
|
|
2440
|
+
|
|
2441
|
+
Each message now names both: `locally in config/env-active/shared.env`, `in CI in
|
|
2442
|
+
the pipeline variables`, and the section of the standard that owns the rule. The
|
|
2443
|
+
verdicts are untouched — the same values are accepted and refused as before, and
|
|
2444
|
+
only the wording moved.
|
|
2445
|
+
|
|
2446
|
+
### Added — an `expect.output` field name is a dot path into the output
|
|
2447
|
+
|
|
2448
|
+
**Minor.** `CookbookTestRunner.validateOutput()` resolves a field name such as
|
|
2449
|
+
`"pdf.size"` through nested objects instead of looking up `actual["pdf.size"]`
|
|
2450
|
+
flat. Every matcher works over a path — `equals`, `contains`, `type`, `pattern`,
|
|
2451
|
+
`min`, `max`, `length`, `exists`.
|
|
2452
|
+
|
|
2453
|
+
Why it had to change: a file descriptor lives under its own key inside the
|
|
2454
|
+
output (`{ pdf: <descriptor> }`), never at the root
|
|
2455
|
+
(`api/docs/biz/40-cookbooks/variable-references.md` § "File descriptors in
|
|
2456
|
+
outputs"). A cookbook that may only name root keys could therefore assert
|
|
2457
|
+
nothing at all about a generated file: `"pdf.size": { "min": 1000 }` came back
|
|
2458
|
+
as `Field pdf.size: missing in output` and failed the step — and, at Tier-1, the
|
|
2459
|
+
boot of a service returning exactly what the norm prescribes.
|
|
2460
|
+
|
|
2461
|
+
Two rules, both decisions:
|
|
2462
|
+
|
|
2463
|
+
- **The exact key wins, at every level.** `{ "a.b": 1 }` is legal output, so
|
|
2464
|
+
`"a.b"` is looked up verbatim before the name is split; splitting is what
|
|
2465
|
+
happens when the name does not exist as written. Nothing has to be escaped.
|
|
2466
|
+
- **Objects only.** A segment never indexes an array — `"items.0"` does not
|
|
2467
|
+
reach the first element — and never reads a property off a scalar. No cookbook
|
|
2468
|
+
asks for indices today, and the syntax would be this library's invention
|
|
2469
|
+
rather than the norm's.
|
|
2470
|
+
|
|
2471
|
+
A path that cannot be resolved reports the whole path AND the first segment that
|
|
2472
|
+
failed, because "no `pdf` at all" and "a `pdf` without a `size`" are different
|
|
2473
|
+
defects: `Field pdf.size: missing in output - segment "pdf" not found. Fix: …`.
|
|
2474
|
+
A name without a dot keeps its original wording, unchanged.
|
|
2475
|
+
|
|
2476
|
+
Backwards compatible: a flat key resolves exactly as before, including the
|
|
2477
|
+
`Field <name>: missing in output` message it produced.
|
|
2478
|
+
|
|
2479
|
+
### BREAKING — `getTestNamespace()` refuses tenant 97; the allowlist is 96 / 98 / 99
|
|
2480
|
+
|
|
2481
|
+
**Major.** `ALLOWED_TENANT_CLASSES` in `utils/testNamespace` drops **97**. A run
|
|
2482
|
+
configured with `TESTING_TENANT_ID=97` now throws instead of returning a
|
|
2483
|
+
namespace, and the refusal message lists the classes that remain.
|
|
2484
|
+
|
|
2485
|
+
97 was the `TESTING` environment class. The allocation retired it on 2026-08-31
|
|
2486
|
+
and the whitelist kept it, so the guard accepted at run time a class no row of
|
|
2487
|
+
`api/docs/standards/tenant-allocation.md` claimed — the document itself recorded
|
|
2488
|
+
the gap as "a residue, not a permission", waiting for exactly this change. A
|
|
2489
|
+
whitelist states the allocation; it does not outlive it.
|
|
2490
|
+
|
|
2491
|
+
**Nothing in the workspace sets that value.** Every `TESTING_TENANT_ID` in `api`
|
|
2492
|
+
and `api_biz/*` is `99` in the env templates and `96` in CI, verified before the
|
|
2493
|
+
narrowing — compliance precedes the gate (`automation-gates.md` §3). Dependents
|
|
2494
|
+
that pin an older version keep the wider list until they bump; a repo that has
|
|
2495
|
+
adopted this version and still configures 97 fails fast at the first integration
|
|
2496
|
+
run, naming the value, the allowed classes and the document.
|
|
2497
|
+
|
|
2498
|
+
Unchanged: 96, 98 and 99 are still accepted, 100 (LIVE) and 101+ (CUSTOMER) are
|
|
2499
|
+
still refused, and the workspace id is still not class-checked.
|
|
2500
|
+
|
|
2501
|
+
|
|
2502
|
+
### Added — R8 exempts an integration test that owns its database, and reports the half-done case
|
|
2503
|
+
|
|
2504
|
+
**Minor.** `utils/deployContract` R8 gains a third permit for `tests/integration/`:
|
|
2505
|
+
a file that builds its own schema (`CREATE DATABASE` / `CREATE SCHEMA`) on a line
|
|
2506
|
+
that executes AND redirects `DB_NAME` to it (`process.env.DB_NAME = …`, bracket
|
|
2507
|
+
form included) no longer has to take its namespace from the shared test-namespace
|
|
2508
|
+
module. It targets no shared namespace, so the helper protects nothing it could
|
|
2509
|
+
reach, and the only assertion it could offer instead cannot fail —
|
|
2510
|
+
`getTestNamespace()` returns a value from `ALLOWED_TENANT_CLASSES` and nothing
|
|
2511
|
+
else. Reporting a state whose only compliance is an assertion that cannot fail is
|
|
2512
|
+
what `automation-gates.md` §3 forbids.
|
|
2513
|
+
|
|
2514
|
+
The gate is not softened by it. A file that builds its own schema and does NOT
|
|
2515
|
+
redirect `DB_NAME` is still reported, now under a message of its own: its queries
|
|
2516
|
+
reach the live database while the file appears to own a throwaway. Both halves of
|
|
2517
|
+
the permit are read on lines that execute — a `CREATE DATABASE` or a redirect
|
|
2518
|
+
sitting in a comment earns nothing. Every file R8 reported before this change is
|
|
2519
|
+
still reported, and every file it reported for having no schema of its own keeps
|
|
2520
|
+
the message it had.
|
|
2521
|
+
|
|
2522
|
+
The permit asks for the redirect rather than banning the live database's name:
|
|
2523
|
+
a test may legitimately name the live schema to assert it never touches it, and
|
|
2524
|
+
a ban would demand the deletion of that guard — the same reason the runtime guard
|
|
2525
|
+
is an allowlist rather than a comparison against a declared live id.
|
|
2526
|
+
Rule: `api/docs/standards/tenant-allocation.md` § Enforcement.
|
|
2527
|
+
|
|
2528
|
+
### Changed — the logger check is `@onlineapps/logger-contract`, and the message case follows it
|
|
2529
|
+
|
|
2530
|
+
`ServiceReadinessValidator`, `ValidationOrchestrator` and `CookbookTestRunner`
|
|
2531
|
+
each carried their own `LOGGER_METHODS` constant and their own validation loop —
|
|
2532
|
+
three byte-identical copies of one contract, which
|
|
2533
|
+
`change-discipline.md` § One rail per concern calls a defect. All three now call
|
|
2534
|
+
`assertLogger(context, logger, reason)` from `@onlineapps/logger-contract`
|
|
2535
|
+
(exact pin `1.0.0`), per owner confirmation
|
|
2536
|
+
`api/docs/governance/confirmations/connector-logger-contract.md` 004.
|
|
2537
|
+
|
|
2538
|
+
What a caller sees: the same two states, refused at the same point in the same
|
|
2539
|
+
constructor, with the same `[Context] …` prefix and the same `Fix:` line. The
|
|
2540
|
+
only difference is the word after the prefix — `logger is required` /
|
|
2541
|
+
`logger is incomplete` where these three used to write `Logger` with a capital
|
|
2542
|
+
L. The other four adopters (`content-resolver`, `cookbook-template-helpers`,
|
|
2543
|
+
`conn-orch-registry`, `error-handler-core`) already wrote it lowercase, so this
|
|
2544
|
+
removes a divergence rather than creating one.
|
|
2545
|
+
|
|
2546
|
+
### BREAKING — cookbook `steps` is an ARRAY, and the object shape is refused (DÁVKA 95a)
|
|
2547
|
+
|
|
2548
|
+
**Major.** `utils/cookbookFormat.normalizeCookbookSteps` is renamed to
|
|
2549
|
+
`readCookbookSteps` and no longer normalises anything: a `steps` value that is
|
|
2550
|
+
present but not an array THROWS, with the object→array migration recipe in the
|
|
2551
|
+
message. `CookbookTestRunner.normalizeSteps()` is gone — the runner calls the one
|
|
2552
|
+
owner of the rule directly. `CookbookTestRunner.validateCookbook` throws
|
|
2553
|
+
`[CookbookTestRunner] Cookbook has no steps - …` where it used to throw
|
|
2554
|
+
`Cookbook must have steps (array or object)`.
|
|
2555
|
+
|
|
2556
|
+
Owner decision, fully authoritative:
|
|
2557
|
+
`api/docs/governance/confirmations/cookbook-steps-shape.md` 001 (2026-09-05) —
|
|
2558
|
+
`steps` is an array of step objects carrying `step_id`, the only allowed shape;
|
|
2559
|
+
the object variant must not be permitted or used anywhere. It supersedes the
|
|
2560
|
+
2026-08-17 object directive, which never landed in any runtime.
|
|
2561
|
+
|
|
2562
|
+
Why the tolerance had to go rather than stay as a convenience: the offline runner
|
|
2563
|
+
converted the object into an array, so Tier-1 passed a cookbook the production
|
|
2564
|
+
WorkflowOrchestrator — which only ever read arrays — threw on. That is a false
|
|
2565
|
+
green, and it is what kept object-shaped cookbooks alive in the tree. Order is
|
|
2566
|
+
the reason the shape matters at all: JS reorders numeric object keys and Postgres
|
|
2567
|
+
`jsonb` does not preserve key order either (measured M9,
|
|
2568
|
+
`infra/api_monitoring/src/consumer/cookbookSplit.js:32-37`).
|
|
2569
|
+
|
|
2570
|
+
### BREAKING — the cookbook version needs all three components (DÁVKA 95a)
|
|
2571
|
+
|
|
2572
|
+
**Major.** `checkCookbookFormatVersion` accepted 1 to 3 numeric components
|
|
2573
|
+
(`/^\d+(\.\d+){0,2}$/`); it now requires `major.minor.patch`
|
|
2574
|
+
(`/^\d+\.\d+\.\d+$/`). `"version": "2.1"` was accepted by this Tier-1 gate and
|
|
2575
|
+
rejected by the machine SSOT — `shared/cookbook/cookbook-core/schemas/cookbook.v2.schema.json:16`,
|
|
2576
|
+
`"pattern": "^2\\.\\d+\\.\\d+$"` — so the offline rail was the lenient one, which
|
|
2577
|
+
is the same false green as the object steps. Five fixtures in this package
|
|
2578
|
+
declared `"2.1"` and now declare `"2.1.0"`.
|
|
2579
|
+
|
|
2580
|
+
### REMOVED — `WorkflowTestRunner` (DÁVKA 95a)
|
|
2581
|
+
|
|
2582
|
+
**Major.** `WorkflowTestRunner` and its export are deleted. It was a second,
|
|
2583
|
+
parallel cookbook-execution rail with no consumer anywhere in the workspace
|
|
2584
|
+
(measured 2026-09-05: `api`, `api_biz/*`, `fe_adminui`, outside this package's
|
|
2585
|
+
own test and export), and it read `step.id` — the spelling `format.md` § Steps
|
|
2586
|
+
bans — so every key it wrote into the workflow context was `undefined`. A
|
|
2587
|
+
duplicate rail with no written justification is a defect, not redundancy
|
|
2588
|
+
(`change-discipline.md` § One rail per concern).
|
|
9
2589
|
|
|
10
2590
|
### BREAKING — standard level v1.1 "Multitenancy Standard" is removed (DÁVKA 88j)
|
|
11
2591
|
|