@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/README.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
> Status: current
|
|
2
|
+
> Owns: the validation a service runs over its own readiness — the uniform manifests, the cookbook test runner, and the mocks that stand in for infrastructure
|
|
3
|
+
|
|
4
|
+
<!-- BEGIN GENERATED: library-uniform — regenerate: npx oa-sync-template readme-uniform --all -->
|
|
5
|
+
Uniform: [library/orchestration](./manifests/library.manifest.json)
|
|
6
|
+
|
|
7
|
+
Duty sections that apply:
|
|
8
|
+
|
|
9
|
+
- `all`: L-MAIN, L-ENGINES, L-TESTS, L-TEST-SCRIPT, L-PACK-TESTS, L-PINS, L-NO-FILE-RANGE, L-CHANGELOG, L-README, L-README-REGION, L-CONSUMER
|
|
10
|
+
<!-- END GENERATED: library-uniform -->
|
|
11
|
+
|
|
1
12
|
# @onlineapps/conn-orch-validator
|
|
2
13
|
|
|
3
14
|
**Validation Orchestrator for OA Drive Microservices**
|
|
@@ -9,7 +20,7 @@ Coordinates validation across ALL layers (base, infra, orch, business) to ensure
|
|
|
9
20
|
This is **NOT** a development testing tool. This is a **production validation orchestrator** that:
|
|
10
21
|
|
|
11
22
|
1. **Validates service structure** - directories, files, configuration
|
|
12
|
-
2. **Validates configuration** - `config.json`, `operations.json`
|
|
23
|
+
2. **Validates configuration** - `config.json`, `operations.json` and `integration-contract.json`, through the manifest rows that own them
|
|
13
24
|
3. **Validates the environment contract** - every `env.required` name is set
|
|
14
25
|
4. **Validates operations** - the v3 handler-registry rules
|
|
15
26
|
5. **Validates business logic** - cookbook tests with mocked infrastructure
|
|
@@ -38,16 +49,21 @@ runs Tier-1 validation in phase 0.2 — before MQ connects, before registration.
|
|
|
38
49
|
|
|
39
50
|
---
|
|
40
51
|
|
|
41
|
-
## Validation Process (
|
|
52
|
+
## Validation Process (7 Steps)
|
|
42
53
|
|
|
43
54
|
1. **Service Structure** - directories and files exist
|
|
44
|
-
2. **Config Files** -
|
|
55
|
+
2. **Config Files** - rows `C-SERVICE`, `C-OPS` and `C-CONTRACT` of the uniform manifest, run here so a
|
|
56
|
+
broken configuration stops the boot before steps 3-6; step 7 leaves them out because they are already
|
|
57
|
+
answered. The step decides nothing of its own — until d.224 it re-implemented two of those rules with
|
|
58
|
+
sentences of its own, and the same defect was reported twice
|
|
45
59
|
3. **Environment Contract** - every variable the contract declares `env.required` is set
|
|
46
60
|
4. **Operations Compliance** - every operation declares `handler`, `bundle_scope`, `input`, `output`, and none carries a retired v2 field (`endpoint`, `method`, `path`); a missing `description` is a warning
|
|
47
61
|
5. **Cookbook Tests** - business logic + integration (MOCKED infra)
|
|
48
62
|
6. **Connector Integration** - the connector declarations agree and the environment backs them
|
|
63
|
+
7. **Manifest conformance** - the repository against the uniform manifest shipped in this package (see below)
|
|
49
64
|
|
|
50
|
-
**Output:** Validation proof saved to `conn-runtime/validation-proof.json
|
|
65
|
+
**Output:** Validation proof saved to `conn-runtime/validation-proof.json`, deployability
|
|
66
|
+
signal saved to `ci/deployability.json`
|
|
51
67
|
|
|
52
68
|
> The list said "Service Readiness — HTTP API works" until 2026-08-27. That step
|
|
53
69
|
> was removed on 2026-08-22 under ADR 0005 and this README kept describing it.
|
|
@@ -90,6 +106,970 @@ legitimate state until every repo adopts the declaration, and never silent.
|
|
|
90
106
|
|
|
91
107
|
---
|
|
92
108
|
|
|
109
|
+
## Test-coverage contract (`stackTiers` block)
|
|
110
|
+
|
|
111
|
+
Every test file the service's jest config matches must be RUN somewhere: by the
|
|
112
|
+
`test:all` chain, or by a script the contract declares as a stack tier with the
|
|
113
|
+
reason it cannot run in `test:all`.
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
oa-biz-ci-gate verify-test-coverage [--service-root <path>]
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Also folded into `verify-contract`, so a version bump activates it.
|
|
120
|
+
|
|
121
|
+
| Set | What | How it is measured |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| T | files the service jest config matches | `jest --listTests` |
|
|
124
|
+
| A | files the `test:all` chain runs | the chain decomposed `npm run …` → jest, each invocation asked `--listTests` |
|
|
125
|
+
| B | files the declared stack tiers run | the same, per declared script |
|
|
126
|
+
|
|
127
|
+
The question is always put to **jest**, and to the service's own jest resolved
|
|
128
|
+
from its `node_modules` — a parse of `jest --config jest.config.js tests/unit`
|
|
129
|
+
answers with the shape of the command, and a different jest version resolves a
|
|
130
|
+
different `testMatch`. The gate therefore needs the dependencies installed:
|
|
131
|
+
run it after `npm ci`.
|
|
132
|
+
|
|
133
|
+
A file in T that is in neither A nor B is the defect: a suite that runs nowhere
|
|
134
|
+
fails nowhere, so it looks like coverage and proves nothing.
|
|
135
|
+
|
|
136
|
+
### Declaring a stack tier
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
"stackTiers": [
|
|
140
|
+
{ "script": "test:e2e", "requires": "needs a live biz-hello consumer on biz-hello.workflow; CI has no service runtime" }
|
|
141
|
+
]
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`requires` is mandatory — a tier nobody can justify is the one nobody dares
|
|
145
|
+
delete, and a reader of a pipeline that does not contain the suite must be able
|
|
146
|
+
to see why. There is no exemption by directory name: `tests/e2e/**` is not
|
|
147
|
+
self-evidently un-runnable, and a name cannot carry a reason.
|
|
148
|
+
|
|
149
|
+
| Violation | Reported as |
|
|
150
|
+
|---|---|
|
|
151
|
+
| a matched file that neither `test:all` nor a declaration reaches | `TEST_RUNS_NOWHERE` |
|
|
152
|
+
| a declared script `package.json` does not define | `STACK_TIER_SCRIPT` |
|
|
153
|
+
| a declared script that lists 0 test files | `STACK_TIER_EMPTY` |
|
|
154
|
+
| a declared script whose tests `test:all` already runs | `STACK_TIER_IN_TEST_ALL` |
|
|
155
|
+
| a step in the chain that is neither `npm run <script>` nor a jest command | `TEST_ALL_CHAIN` |
|
|
156
|
+
| an entry without `requires` | refused when the contract loads |
|
|
157
|
+
|
|
158
|
+
**Measurement boundary, printed on every run:** the gate never checks whether a
|
|
159
|
+
declared stack tier is actually RUN anywhere. The declaration says why the suite
|
|
160
|
+
cannot run in `test:all`; it does not claim something else runs it.
|
|
161
|
+
|
|
162
|
+
A contract with no `stackTiers` block is the ordinary case — it means every test
|
|
163
|
+
file is reachable from `test:all`.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Manifest uniformy (`oa-validate`)
|
|
168
|
+
|
|
169
|
+
`manifests/biz-service.manifest.json` is the machine-readable declaration of what a
|
|
170
|
+
biz service is: sections by concern, one row per rule, each row carrying `id`,
|
|
171
|
+
`severity`, `owner`, `fix`, `doc` and the name of the `check` that decides it. The
|
|
172
|
+
manifest ships inside this package, so **the manifest version is the validator
|
|
173
|
+
version** — a service pins the validator exactly and thereby pins the shape it is
|
|
174
|
+
measured against. There is no version field in the file.
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
npx oa-validate # the current directory, workspace derived from this package
|
|
178
|
+
npx oa-validate ../converter --workspace /path/to/workspace
|
|
179
|
+
npx oa-validate . --json # the same run as data
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
One table (`id · severity · where · what · fix · owner`), the `doc` pointer of every
|
|
183
|
+
reported row, and exit `0` / `1`; `2` when the run could not start. Rows that resolve
|
|
184
|
+
a `from:` reference into the workspace (`api/config/services.json`, `api/.nvmrc`) are
|
|
185
|
+
printed `NOT RUN` where that reference cannot be read, and never counted as passing.
|
|
186
|
+
|
|
187
|
+
**A service run also RECORDS its verdict**, in `<serviceRoot>/ci/deployability.json` —
|
|
188
|
+
the machine half described under [Step 7 and the deployability signal](#step-7-and-the-deployability-signal),
|
|
189
|
+
written by the same builder and the same writer that step uses. That is what makes the
|
|
190
|
+
run from the api checkout usable by a deploy gate: until d.232 the only writer was step 7,
|
|
191
|
+
which runs inside the service container where the workspace SSOTs are absent, so every
|
|
192
|
+
signal that existed said `complete: false` — and a gate asking for `deployable && complete`
|
|
193
|
+
could never be satisfied, because the run that IS complete left no file behind. The
|
|
194
|
+
workspace mode and `--library` write nothing: the signal is a verdict about one service
|
|
195
|
+
tree, and neither run has one. An unwritable service root ends the run with exit `2`,
|
|
196
|
+
after the table has been printed.
|
|
197
|
+
|
|
198
|
+
**A rendered template directory is compared minus what the template ignores.** The
|
|
199
|
+
template's own `.gitignore` is the one place saying which paths of a service tree are
|
|
200
|
+
generated output, and `oa-sync-template template --check` reads it on both sides
|
|
201
|
+
(`sync/serviceTemplate.js` § `listOutputFiles`). `oa-validate` records its verdict in the
|
|
202
|
+
tree it measured (d.232), so a run over `api/templates/business-service` leaves
|
|
203
|
+
`ci/deployability.json` there; git does not see it, and since d.237 neither does the
|
|
204
|
+
mirror check.
|
|
205
|
+
|
|
206
|
+
**A reference into this package runs everywhere.** The template ships INSIDE the
|
|
207
|
+
validator (001 §2, §5), so the rows comparing a service against it reference the packaged
|
|
208
|
+
file — `from: { "package": "templates/business-service/jest.config.js", "text": true }` —
|
|
209
|
+
and resolve it from the package's own location. Those rows therefore answer in a service
|
|
210
|
+
container and in a service's own CI checkout, which is exactly where the uniform is for.
|
|
211
|
+
Measured over `api_biz/converter` on 2026-09-09: a run with no workspace used to report 13
|
|
212
|
+
rows `NOT RUN`, F-RUNNER and G-SHARED-ENV among them, and raised neither of the two real
|
|
213
|
+
findings they carry; it now reports the same findings as the workspace run, with
|
|
214
|
+
`U-ORPHAN` and the five `D-*` rows the only ones left `NOT RUN`. What still needs the
|
|
215
|
+
workspace is what reads something outside this package. `R-NODE` left that list in d.238:
|
|
216
|
+
the platform Node major now comes from this package's own `engines.node`, which the
|
|
217
|
+
library uniform's `L-ENGINES` holds equal to `api/.nvmrc`, so it is a checked projection
|
|
218
|
+
of the SSOT that travels with the pin rather than a second copy of the number
|
|
219
|
+
(confirmation `biz-service-manifest` 006 point 2).
|
|
220
|
+
|
|
221
|
+
**Where the workspace comes from.** The run derives it from where THIS package sits:
|
|
222
|
+
the checkout carrying `config/services.json` is the api checkout, and the workspace is
|
|
223
|
+
its parent. What that checkout directory is CALLED does not matter — a CI job that
|
|
224
|
+
clones into `infra-mono` resolves the same workspace as a developer with `api`.
|
|
225
|
+
`--workspace` is the explicit override and always wins. The copy installed in a
|
|
226
|
+
service's `node_modules` sits inside that service rather than beside the api checkout,
|
|
227
|
+
so it resolves no workspace of its own and the rows reading the SSOT print `NOT RUN`
|
|
228
|
+
with the reason — which is what a run inside a service container does.
|
|
229
|
+
|
|
230
|
+
Rows, categories and discovery patterns are the manifest's own facts; every other
|
|
231
|
+
fact is a `from:` reference to the file that owns it, and the suite
|
|
232
|
+
`tests/unit/manifestShape.test.js` fails the build on a copied list.
|
|
233
|
+
|
|
234
|
+
The service uniform is declared in eight sections, and what each one is for — with the
|
|
235
|
+
reason every row exists — is confirmation
|
|
236
|
+
[`biz-service-manifest`](../../../docs/governance/confirmations/biz-service-manifest.md) §2
|
|
237
|
+
and §19:
|
|
238
|
+
`discovery` (which directories wear the uniform, and who says they exist),
|
|
239
|
+
`files` (byte-identical to the template, generated from it, carrying the platform's
|
|
240
|
+
entries among its own, or forbidden outright),
|
|
241
|
+
`scripts` (the bodies a suite is run with, and what may never run without being named),
|
|
242
|
+
`tooling` (the repository's own `scripts/`, and what a reader can learn about one before
|
|
243
|
+
running it), `config` (the files a service is configured by), `runtime` (the Node major,
|
|
244
|
+
the memory norm and the ports it may not publish), `db` (whether it has a database, what
|
|
245
|
+
its SQL package is called and which account reaches it) and `docs` (what the service's own
|
|
246
|
+
documentation tree may not say). A row that cites a checker this package already runs — the deploy contract, the
|
|
247
|
+
installation contract, the environment contract, the documentation lint — names its
|
|
248
|
+
`requirement` or its `rule` and adds no rule of its own: the manifest cites, it never
|
|
249
|
+
restates (confirmation 004 point 4).
|
|
250
|
+
|
|
251
|
+
Beside the sections sits `guidance`: a duty the confirmations name for which nothing
|
|
252
|
+
can decide the answer yet. It carries a `why` and a `doc` and never a `check` or a
|
|
253
|
+
`severity`, because it produces no finding — "no check, so not a rule" (001 §6), said
|
|
254
|
+
out loud rather than left as a row that reports nothing and reads as a pass.
|
|
255
|
+
|
|
256
|
+
#### The script rows (`tooling`)
|
|
257
|
+
|
|
258
|
+
*(The word is used twice in this package for two different things, and they never meet:
|
|
259
|
+
here it is a SECTION of the service uniform, and in the library uniform below it is a
|
|
260
|
+
`category` a package declares. Different manifests, different axes.)*
|
|
261
|
+
|
|
262
|
+
`S-SCRIPTS` runs the SCRIPTS-STANDARD header rules over `scripts/**` of the repository:
|
|
263
|
+
the five fields in order with the right comment marker, the closed `Status` and `Shell`
|
|
264
|
+
vocabularies, the blank comment line, every `@see` target existing, and the
|
|
265
|
+
`sourced`/`required` words in the two library directories. What an `Owns:` sentence SAYS
|
|
266
|
+
is true no linter can check; content accuracy stays review, said plainly.
|
|
267
|
+
|
|
268
|
+
Which of those two words a library owes is decided by its EXTENSION, not by which of the
|
|
269
|
+
two directories it lies in (`S007`, d.249). The directory says one thing only — "a library,
|
|
270
|
+
not an entry point", so the `Usage:` line carries a word instead of a command line — and
|
|
271
|
+
`.sh` is `sourced` while a Node module is `required`. Until d.249 the rule read
|
|
272
|
+
`scripts/lib/` as shell and `scripts/ci/lib/` as Node, which is what the tree looked like
|
|
273
|
+
when it was written and is not what the tree is: BIZ-ingest measured a Node library under
|
|
274
|
+
`scripts/lib/` in their repository and seven in meta's, none of which could go green
|
|
275
|
+
without writing `sourced` about a file nothing sources. A rule satisfied only by an untrue
|
|
276
|
+
sentence is the one `architecture-principles.md` §10 forbids weakening a test to meet, so
|
|
277
|
+
the rule moved and the files stayed. Measured 2026-09-11, before and after: ingest 1
|
|
278
|
+
finding → 0, meta 7 → 0, `api/scripts` 0 → 0 across 105 files.
|
|
279
|
+
|
|
280
|
+
The rules ship in this package — `src/lint/scripts/lintScripts.js`, with
|
|
281
|
+
`oa-lint-scripts` as its command — and that is the whole reason the row can exist.
|
|
282
|
+
They used to be a file of the `api` checkout, and a file of one checkout cannot decide a
|
|
283
|
+
rule about eight repositories: a service has no `api/` directory in its own CI and none
|
|
284
|
+
at all inside its image. `api/scripts/ci/lint-scripts.mjs` now calls this module and holds
|
|
285
|
+
no rule of its own, so `npm run scripts:lint`, the CI job `validate-scripts` and this row
|
|
286
|
+
are one implementation (confirmation `biz-service-manifest` 003 §19).
|
|
287
|
+
|
|
288
|
+
One thing in those rules needs a second root, and only when it is used: a script may cite
|
|
289
|
+
`api/docs/…`, which names the api checkout beside the repository rather than a `docs/` of
|
|
290
|
+
its own. With the workspace reachable the citation is resolved there; without it the lint
|
|
291
|
+
returns the target as UNRESOLVED and the row is `NOT RUN` with the targets named — a run
|
|
292
|
+
that could not look must not report a pass. Measured 2026-09-10 over the eight services:
|
|
293
|
+
47 `@see` lines under their scripts directories, 4 of them with that prefix — and 86
|
|
294
|
+
scripts of which not one carried a header.
|
|
295
|
+
|
|
296
|
+
#### The SQL rows (`db`)
|
|
297
|
+
|
|
298
|
+
Beside `D-DB-CONSISTENT` (does the repository agree with what it declared) and the two
|
|
299
|
+
rows citing the installation contract, three rows say what the package is CALLED and who
|
|
300
|
+
reaches it. All three ask their question only of a repository that DECLARES a database, so
|
|
301
|
+
a stateless service is silent — `pdfgen` is the measured case.
|
|
302
|
+
|
|
303
|
+
* **`D-DB-NAMING`** — `migrations/*.sql` reads the shape
|
|
304
|
+
`api/docs/biz/70-contracts/database-contract.md` §5 prescribes: three digits, an
|
|
305
|
+
underscore, a lowercase description. The row reads that node back rather than inventing
|
|
306
|
+
a stricter rule of its own, so `000_baseline.sql` — §5's own example, and an applied
|
|
307
|
+
migration by §8 — is a name and not a finding. No leading verb is demanded either,
|
|
308
|
+
because the measured counter-example is right (`009_cnv_batch_drop_pending_status.sql`
|
|
309
|
+
opens with a table prefix), and the three digits are not decoration (`999a_` sorts after
|
|
310
|
+
`999_` only by accident of the byte after the digits). The convention reaches one level
|
|
311
|
+
and stops: what `BASELINE/` and `SEED/**` must CARRY is the installation contract's §4,
|
|
312
|
+
which `D-DB-HEADERS` already checks, and that contract names no shape for their file
|
|
313
|
+
names at all.
|
|
314
|
+
* **`D-DB-README`** — the installation contract §3 asks a repository with a database for
|
|
315
|
+
four things and `SQL_PACKAGE` checks three of them. The fourth is `migrations/README.md`,
|
|
316
|
+
the documented execution order that reproduces a working instance, and until d.218
|
|
317
|
+
nothing looked for it.
|
|
318
|
+
* **`D-DB-ACCOUNT`** — `DB_USER` in this service's env template reads
|
|
319
|
+
`oagen_<shortname>`, from the same derivation `C-IDENTITY` holds `database.schema` to
|
|
320
|
+
(confirmation `db-accounts-per-service` 001: root stops being an operational identity —
|
|
321
|
+
it was the identity of nine containers across eight repositories). A repository that
|
|
322
|
+
declares a database and no `DB_USER` is a finding and not a silence.
|
|
323
|
+
|
|
324
|
+
One more row about the database sits outside this section on purpose. **`X-DB-CONFIG`**
|
|
325
|
+
is in `files.forbidden`, beside `X-HOOKS` and `X-PREVAL`, and it forbids one path:
|
|
326
|
+
`src/config/database.js`. Seven of the eight services carried that file — a module
|
|
327
|
+
singleton building its own Sequelize — beside `@onlineapps/conn-base-db`, whose
|
|
328
|
+
`createSequelize` exists to be the one connection factory
|
|
329
|
+
(`.claude/rules/change-discipline.md` § One rail per concern); the 8.0.0 cascade commit of
|
|
330
|
+
each service switches its importers and deletes the file, and this row is what keeps it
|
|
331
|
+
deleted (confirmation `biz-service-manifest` 009). Unlike the three rows above it does NOT
|
|
332
|
+
ask `requiredConnectors.db` first: 009 names the path with no condition on it, and a
|
|
333
|
+
service that declared no database has even less business carrying a connection factory. A
|
|
334
|
+
repository without the file is silent either way — `pdfgen` is the measured case here too.
|
|
335
|
+
|
|
336
|
+
#### What `.gitignore` may NOT hide: the `tracked` class
|
|
337
|
+
|
|
338
|
+
`F-GITIGNORE` above says what the file must CONTAIN. **`X-IGNORED`** (`files.tracked`,
|
|
339
|
+
check `git-tracked`) says what it may not cover, and the two together are the answer to the
|
|
340
|
+
owner's sentence: a biz service has no right to ignore something the uniform requires.
|
|
341
|
+
|
|
342
|
+
Every file row reads the WORKING TREE. What a clone, a CI checkout and the production image
|
|
343
|
+
receive is what git TRACKS, and those are not the same thing — measured 2026-09-10 on a copy
|
|
344
|
+
of converter outside the repository:
|
|
345
|
+
|
|
346
|
+
| run | result |
|
|
347
|
+
|---|---|
|
|
348
|
+
| `init.sh` and `jest.config.js` in `.gitignore`, both files on disk | `F-INIT`, `F-JEST`: **no findings** |
|
|
349
|
+
| the same two files deleted — what a clean clone has | both fire, NOT DEPLOYABLE |
|
|
350
|
+
|
|
351
|
+
Developer green, CI and image red. The row reads **no list of its own**: the paths are the
|
|
352
|
+
ones `files.identical`, `files.contains` and `files.generated` already name, read from the
|
|
353
|
+
block it sits in, so a new file row extends this one too (004 point 2). A directory among
|
|
354
|
+
them covers its whole tree — `scripts/` is the case that matters, because `S-SCRIPTS` lints
|
|
355
|
+
the headers of scripts the clone may never receive. `files.forbidden` is excluded on
|
|
356
|
+
purpose (demanding a banned file be tracked is a contradiction), and so is `files.own`.
|
|
357
|
+
|
|
358
|
+
The question is TRACKED, not IGNORED: a tracked file matching an ignore line is harmless,
|
|
359
|
+
an untracked one is gone from the clone whether a rule covers it or not. `git check-ignore`
|
|
360
|
+
is asked only about the files that already failed, because that is what turns the finding
|
|
361
|
+
into an edit. Outside a git checkout — a container, an exported tarball — the row is NOT RUN
|
|
362
|
+
with that reason, never a pass.
|
|
363
|
+
|
|
364
|
+
#### The template's own completeness: the `own` class
|
|
365
|
+
|
|
366
|
+
`files.own` is not an exemption list; it is where the manifest says, for each file the
|
|
367
|
+
template carries, that the SERVICE decides its content. `tests/unit/templateCoverage.test.js`
|
|
368
|
+
walks `templates/business-service/**` and demands, for every file, either a row naming its
|
|
369
|
+
path or membership of that class. Adding a file to the template is therefore a decision
|
|
370
|
+
about who guards it, taken in the manifest — and the list is in the manifest rather than in
|
|
371
|
+
the test precisely so that the next file cannot be exempted by editing a test
|
|
372
|
+
(`.claude/rules/change-discipline.md` § One rail per concern). It was the missing check
|
|
373
|
+
behind two files that had no row at all: `gitignore` (now `F-GITIGNORE`) and
|
|
374
|
+
`.gitlab-ci.yml` (now `G-CI`).
|
|
375
|
+
|
|
376
|
+
#### `.gitignore`: the `contains` class
|
|
377
|
+
|
|
378
|
+
`F-GITIGNORE` compares the ignore ENTRIES the packaged template declares against the
|
|
379
|
+
entries the repository declares — any order, every other line left alone. The class exists
|
|
380
|
+
because this file is neither of the other two: it cannot be `identical` (a repository
|
|
381
|
+
ignores its own scratch directories, and nine different files were measured across the
|
|
382
|
+
eight services), and nothing renders it from a declaration. What must be true of it is a
|
|
383
|
+
SET: every path this platform WRITES into a repository is a path git must not see, and
|
|
384
|
+
`ci/deployability.json` is the measured case — `oa-validate` and the pre-push hook of
|
|
385
|
+
confirmation 006 both write it, and in four of the eight repositories every run left it
|
|
386
|
+
untracked. A narrower pattern does not satisfy the entry it narrows:
|
|
387
|
+
`config/env-active/*.env` leaves every non-`.env` file in a directory of live secrets
|
|
388
|
+
committable.
|
|
389
|
+
|
|
390
|
+
The generator still writes a labelled `# --- oa-ignore v1` block, so a reader can tell the
|
|
391
|
+
platform's lines from the repository's own — but the block is the FIX's shape and never
|
|
392
|
+
the check's, and the renderer removes an existing one before it computes what is missing,
|
|
393
|
+
so a second run adds no second block.
|
|
394
|
+
|
|
395
|
+
#### The documentation rows (`docs`)
|
|
396
|
+
|
|
397
|
+
The rules of `<service>/docs/**` already exist and already have an owner:
|
|
398
|
+
`api/scripts/ci/lint-biz-docs.mjs`, held by BIZ-DOCS. So the rows here decide nothing —
|
|
399
|
+
each citing row names the lint rule it stands for, in the lint's own `--rules` syntax,
|
|
400
|
+
and `src/manifest/checks/docsLintBridge.js` runs the lint ONCE per service and hands each
|
|
401
|
+
row the findings of the rules it cites, with the lint's own `file:line`:
|
|
402
|
+
|
|
403
|
+
| row | cites | what it stands for |
|
|
404
|
+
|---|---|---|
|
|
405
|
+
| `D-PORT` | `F002:http-ports` | no document sends an operator to a port nothing listens on |
|
|
406
|
+
| `D-NPM` | `L016` | a cited npm script is one this repository declares |
|
|
407
|
+
| `D-SCRIPT` | `L007` | a cited script path exists in this repository |
|
|
408
|
+
| `D-RETIRED` | `F002` | no live document teaches a retired concept |
|
|
409
|
+
| `D-HEADER` | `S001,S002` | every node opens with `Parent:` and `Owns:` |
|
|
410
|
+
| `D-LINT` | — | the lint reports nothing else over this tree |
|
|
411
|
+
|
|
412
|
+
A finding lands on exactly ONE row: the most specific citation wins, so the ban on
|
|
413
|
+
localhost ports goes to `D-PORT` rather than twice, and two rows citing one rule
|
|
414
|
+
equally are a fail-fast.
|
|
415
|
+
|
|
416
|
+
`D-LINT` is the catch-all and cites nothing — deliberately. What the rows above buy is
|
|
417
|
+
their own `fix` sentence, not coverage: measured 2026-09-09 over the eight repositories,
|
|
418
|
+
80 lint findings of which 16 fell to a citing row, so a tree the documentation gate called
|
|
419
|
+
broken came back DEPLOYABLE here. A citation list on the catch-all would have to be held
|
|
420
|
+
equal to the linter's and would fall behind it the first rule BIZ-DOCS wrote, so the row
|
|
421
|
+
names none and takes every error-graded finding the rows above did not claim.
|
|
422
|
+
|
|
423
|
+
It counts what the LINT graded `error` — the set its own `--severity error` prints. That
|
|
424
|
+
is the whole of the safeguard: grading belongs to BIZ-DOCS
|
|
425
|
+
(`api/config/biz-docs-lint.json` § severities), so a rule they add at `warn` is visible in
|
|
426
|
+
their run and blocks no deploy until they raise it on their own dated transition
|
|
427
|
+
(`automation-gates.md` §3). The documentation gate itself is unchanged and still lands in
|
|
428
|
+
each repository's CI at zero findings (confirmation
|
|
429
|
+
[`biz-docs-foreign-tree-gate`](../../../docs/governance/confirmations/biz-docs-foreign-tree-gate.md)).
|
|
430
|
+
|
|
431
|
+
The rows are `bearer`-scoped and declare `api/scripts/ci` as a root they read, so in a
|
|
432
|
+
service image and in a service's own CI — where there is no api checkout — the runner
|
|
433
|
+
reports them `NOT RUN` with that reason. `C-LINT`, the row owning
|
|
434
|
+
`config/biz-docs-lint.tree.json`, lives in this block too: the rows are INVOKED with the
|
|
435
|
+
budget that file declares, so the path is written once, on the row, and the block names
|
|
436
|
+
the row (`tree_config_row`) rather than repeating the path. A tree with no such file is
|
|
437
|
+
reported as undecided rather than clean; an empty `docs/` raises nothing, because the
|
|
438
|
+
absence of the tree is `G-SETUP`'s finding, not a second one with a different fix.
|
|
439
|
+
|
|
440
|
+
**A rule the lint could not EVALUATE is `NOT RUN`, not a finding.** The linter has its own
|
|
441
|
+
NOT RUN channel — that is what `--allow-missing-siblings` fills — and a ban whose evidence
|
|
442
|
+
probe reads `api_biz/*` is undecidable in a checkout that has no siblings. Measured
|
|
443
|
+
2026-09-10 over `git archive HEAD` into such a directory: the template mirror came back
|
|
444
|
+
`NOT DEPLOYABLE` on `D-PORT` and `D-RETIRED`, whose bans nobody had violated, while
|
|
445
|
+
`U-ORPHAN` in the same run said the honest thing. The two channels of the lint now map onto
|
|
446
|
+
the two of this uniform: its `findings` are findings, its `skipped` list is `NOT RUN`. A
|
|
447
|
+
tree the lint REFUSES outright (no tree config, a broken configuration) stays a finding,
|
|
448
|
+
because that one has a fix somebody can carry out.
|
|
449
|
+
|
|
450
|
+
`D-NPM` was guidance until 2026-09-10, and the guidance was true while it stood: no rule of
|
|
451
|
+
the lint raised it, and a row citing a rule nobody raises reports nothing and reads as a
|
|
452
|
+
clean tree (001 §6). BIZ-DOCS wrote `L016`, which resolves a cited `npm run <name>` against
|
|
453
|
+
the `package.json` above the tree — exactly one file for a service repository — so the row
|
|
454
|
+
exists now.
|
|
455
|
+
|
|
456
|
+
#### The three configuration files, and who decides them
|
|
457
|
+
|
|
458
|
+
Confirmation
|
|
459
|
+
[`biz-service-manifest`](../../../docs/governance/confirmations/biz-service-manifest.md)
|
|
460
|
+
§18 asks that `config/service/config.json`, `operations.json` and
|
|
461
|
+
`integration-contract.json` be decided by schemas shipped with the validator. Two of
|
|
462
|
+
the three already had an owner, so no second notation was introduced for them
|
|
463
|
+
(`change-discipline.md` § One rail per concern):
|
|
464
|
+
|
|
465
|
+
| file | row | decided by |
|
|
466
|
+
|---|---|---|
|
|
467
|
+
| `operations.json` | `C-OPS` | `service-validator-core` `validateOperationsSchema` — the mirror of the fifteen rules the Registry applies on registration |
|
|
468
|
+
| `integration-contract.json` | `C-CONTRACT` | `utils/bizCiGateContract.js` — the validator the CI gate already runs over this document, so the gate and the table can never disagree about it |
|
|
469
|
+
| `config.json` | `C-SERVICE` | `src/manifest/checks/serviceConfig.js`, which had no owner before |
|
|
470
|
+
|
|
471
|
+
`C-IDENTITY` is the row about the NAME rather than about one file. `naming.md` derives
|
|
472
|
+
every spelling of a biz service from a single short name, and says why that is not
|
|
473
|
+
tidiness: the gateway compares `service_code` from the entitlement data against the
|
|
474
|
+
service name in a cookbook step character by character, so a second spelling is a request
|
|
475
|
+
that gets refused. The row takes the short name from `service.name` and holds the npm
|
|
476
|
+
`name`, `container_name` in both compose files, `config/env-templates/<shortname>.env`
|
|
477
|
+
with the `env_file` entries loading it, and `database.schema` where the repository
|
|
478
|
+
declares one, to that one name. It reads nothing outside the repository, so it answers in
|
|
479
|
+
a container too. Its workspace half is `U-IDENTITY`, beside `U-ORPHAN`: `directory`,
|
|
480
|
+
`repo` (last path segment) and `container` in `api/config/services.json` against the same
|
|
481
|
+
short name. Measured 2026-09-10 over the eight live services: seven wear one name each and
|
|
482
|
+
`hello-service` wore four.
|
|
483
|
+
|
|
484
|
+
`C-SERVICE` demands the three keys that were **measured** to have a reader:
|
|
485
|
+
`service.name` (the identity the wrapper registers under), `service.workspaceScoped`
|
|
486
|
+
(an explicit boolean — the wrapper refuses to boot without one, and it decides whether
|
|
487
|
+
`list-workspaces` must exist) and `service.specificationEndpoint`. Everything else the
|
|
488
|
+
file carries is the service's own business and passes untouched.
|
|
489
|
+
|
|
490
|
+
The same measurement found declarations with **no reader anywhere**, and
|
|
491
|
+
`C-SERVICE-DEAD` (severity `deploy`) refuses them rather than letting each new service
|
|
492
|
+
copy them forward: `service.version` (`ConfigLoader` overwrites it from `package.json`
|
|
493
|
+
on every load), `service.port` (a biz service binds none), `wrapper.registry.url`
|
|
494
|
+
(registration travels over MQ to `registry.register`), `wrapper.tenantContext` (its only
|
|
495
|
+
consumer `createTenantContextMiddleware` and the runtime default were deleted
|
|
496
|
+
together, confirmation
|
|
497
|
+
[`wrapper-tenant-middleware`](../../../docs/governance/confirmations/wrapper-tenant-middleware.md)
|
|
498
|
+
001) and `contractVersion` in the integration contract. The keys themselves are listed
|
|
499
|
+
once, in `DEAD_KEYS` (`src/manifest/checks/serviceConfig.js`), and the manifest row's
|
|
500
|
+
`why` is held equal to that list by its own test — so this paragraph names the class,
|
|
501
|
+
never the count. The fix is always the same sentence — delete the key.
|
|
502
|
+
|
|
503
|
+
`service.url` is the one entry carrying a **date**: it is dead from wrapper **8.0.0**,
|
|
504
|
+
not today. The published 7.0.0 still throws
|
|
505
|
+
`Missing configuration - service.url is required`, so the key travels WITH the pin, which
|
|
506
|
+
is what confirmation
|
|
507
|
+
[`biz-service-port-url`](../../../docs/governance/confirmations/biz-service-port-url.md)
|
|
508
|
+
001 already said. A rule claiming the key has no reader today would send a service pinned
|
|
509
|
+
to 7.0.0 into a failed boot; the row names the work the cascade carries, and its `deploy`
|
|
510
|
+
severity means it never stops one.
|
|
511
|
+
|
|
512
|
+
### Step 7 and the deployability signal
|
|
513
|
+
|
|
514
|
+
The same check is step **7/7** of the boot validation (`ValidationOrchestrator`), so a
|
|
515
|
+
service measures itself at every start against the manifest its pin selected. The two
|
|
516
|
+
blocking severities have different consequences, and that difference is the point
|
|
517
|
+
(confirmation `biz-service-manifest` 001 §4):
|
|
518
|
+
|
|
519
|
+
| Severity | Boot | `results` | What the operator sees |
|
|
520
|
+
|---|---|---|---|
|
|
521
|
+
| `boot` | refused | `success: false`, the finding in `errors` | the service does not start |
|
|
522
|
+
| `deploy` | **succeeds** | `success: true`, `deployable: false`, the finding in `deployFindings` | the banner below, at every start, until it is gone |
|
|
523
|
+
| `warn` | succeeds | table only | the row in the banner's table |
|
|
524
|
+
|
|
525
|
+
```
|
|
526
|
+
[ValidationOrchestrator] NOT DEPLOYABLE — 2 finding(s)
|
|
527
|
+
[ValidationOrchestrator] id severity where what … fix owner
|
|
528
|
+
[ValidationOrchestrator] X-HOOKS deploy hooks/pre-commit … rm -f hooks/pre-commit BIZ-general
|
|
529
|
+
[ValidationOrchestrator] X-PREVAL deploy scripts/run-pre-validation.js … rm -f scripts/run-pre-validation.js BIZ-general
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
A clean run says so in one line, and says what it could not look at, because silence is
|
|
533
|
+
the one outcome a reader cannot interpret and `no findings` alone reads as a proven clean
|
|
534
|
+
bill of health over rows nobody opened. There are three of those lines, and which one is
|
|
535
|
+
printed is decided by WHAT did not run (`report.js` § `describeClearOutcome`, the one
|
|
536
|
+
owner this banner, the `oa-validate` report and the library report all use):
|
|
537
|
+
|
|
538
|
+
```
|
|
539
|
+
DEPLOYABLE — manifest conformance: no findings
|
|
540
|
+
DEPLOYABLE — manifest conformance: no findings (1 row(s) not run)
|
|
541
|
+
DEPLOYABLE — manifest conformance: no findings, INCOMPLETE: 6 deploy row(s) not run
|
|
542
|
+
(U-ORPHAN, U-IDENTITY, D-PORT, D-SCRIPT, D-RETIRED, D-HEADER). Fix: run the uniform from
|
|
543
|
+
the api checkout — npx oa-validate --workspace <workspace root>
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
The first is a run that answered every row. The second is a run that missed a row which
|
|
547
|
+
could not have blocked anything (`warn`). The third is a run that missed a row which
|
|
548
|
+
COULD have — and then the verdict is true only of the half that ran. The sentence itself
|
|
549
|
+
belongs to the uniform (`verdict.incomplete` in the manifest, with `{count}` and `{ids}`
|
|
550
|
+
in it), never to the renderer: a library says `publish row(s)` there, and
|
|
551
|
+
`PUBLISHABLE — no findings` where nothing was skipped.
|
|
552
|
+
|
|
553
|
+
The same verdict is written, in the same step, to **`ci/deployability.json`** of the
|
|
554
|
+
service root, which `deploy-production` reads and refuses on a single row. The `oa-validate`
|
|
555
|
+
CLI writes the same file, through the same builder and writer
|
|
556
|
+
(`src/manifest/deployabilitySignal.js`), for the service run it just printed — one file
|
|
557
|
+
shape with two producers, never two shapes:
|
|
558
|
+
|
|
559
|
+
```json
|
|
560
|
+
{
|
|
561
|
+
"validator": "@onlineapps/conn-orch-validator",
|
|
562
|
+
"validatorVersion": "<the version this service pins>",
|
|
563
|
+
"uniform": "biz-service",
|
|
564
|
+
"serviceRoot": "/srv/biz-hello",
|
|
565
|
+
"deployable": false,
|
|
566
|
+
"complete": false,
|
|
567
|
+
"findings": [{ "id": "X-HOOKS", "severity": "deploy", "where": "…", "what": "…", "fix": "…", "owner": "…", "doc": "…" }],
|
|
568
|
+
"notRun": [{ "id": "U-ORPHAN", "severity": "deploy", "reason": "…" }]
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
**Two claims, not one.** `deployable` says nothing blocked among the rows that RAN;
|
|
573
|
+
`complete` says no row that CAN block this bearer was skipped. Inside a service container
|
|
574
|
+
the rows reading the workspace cannot run at all, so a signal is routinely deployable and
|
|
575
|
+
incomplete at once — and a gate reading only the first was honouring a `--skip` nobody had
|
|
576
|
+
flagged (`automation-gates.md` §1.5). **The deploy gate takes a signal only when both are
|
|
577
|
+
true**; the complete run is produced from the api checkout with
|
|
578
|
+
`npx oa-validate --workspace <root> <service root>`, and since d.232 that run writes the
|
|
579
|
+
file it is asked for. A service root OUTSIDE the workspace root cannot produce a complete
|
|
580
|
+
signal at all — the rows reading the SSOTs report `NOT RUN` for exactly that reason, so
|
|
581
|
+
the gate's complete run measures the checkout, never a copy of it. Neither field changes an exit code: a partial run in
|
|
582
|
+
the container is the intent, and completeness is the gate's question, not the boot's.
|
|
583
|
+
|
|
584
|
+
It is the shape of `ci/integration-signal.json` with three deliberate differences:
|
|
585
|
+
no `generatedAt` (an unchanged tree must produce an unchanged file, or nothing can tell
|
|
586
|
+
a re-run from a change), `validatorVersion` instead of a version of its own (the
|
|
587
|
+
manifest version IS the validator version), and `deployable` instead of a
|
|
588
|
+
`gate: {verdict, reason}` block (one fact, one owner). `notRun` is carried on purpose:
|
|
589
|
+
rows that could not look are not passes, and each entry carries the `severity` of the row
|
|
590
|
+
that did not run — which is what decides `complete`.
|
|
591
|
+
|
|
592
|
+
There is no switch that skips the step and none that moves the file — one path,
|
|
593
|
+
unbypassable.
|
|
594
|
+
|
|
595
|
+
### Two run modes, three scopes
|
|
596
|
+
|
|
597
|
+
Every check declares what it needs in order to look, and that word alone decides where
|
|
598
|
+
the runner runs it. There are three of them (`src/manifest/manifestShape.js`
|
|
599
|
+
§ `CHECK_SCOPES`):
|
|
600
|
+
|
|
601
|
+
| Scope | What the check needs | Example row |
|
|
602
|
+
|---|---|---|
|
|
603
|
+
| `service` | the bearer's own root — all there is inside a container | `S-TEST`, `R-MEM` |
|
|
604
|
+
| `workspace` | the workspace, because the check is ABOUT it | `U-ORPHAN` |
|
|
605
|
+
| `bearer` | the bearer's root, plus whatever its `from:` reference names — the workspace (`L-PINS` reads `api/config/libraries.json`) or this package itself (`F-JEST` reads the packaged template, `R-NODE` its own `package.json`) | `F-JEST`, `R-NODE`, `L-PINS` |
|
|
606
|
+
|
|
607
|
+
| Mode | Command | `service` rows | `workspace` rows (`U-ORPHAN`) | `bearer` rows |
|
|
608
|
+
|---|---|---|---|---|
|
|
609
|
+
| service | `oa-validate [serviceRoot]` | run | only findings under that service's root | run, for that one bearer |
|
|
610
|
+
| workspace | `oa-validate --workspace <root>` (no service root) | `NOT RUN` | **all** of them | run, **once per discovered bearer** |
|
|
611
|
+
|
|
612
|
+
A neighbour's orphan is the neighbour's blocker, so it does not appear in this service's
|
|
613
|
+
table; the complete list is the workspace run, which is that check's home.
|
|
614
|
+
|
|
615
|
+
`bearer` exists because `F-JEST` is neither of the other two: it is a fact of ONE service,
|
|
616
|
+
and it reaches the template only through the workspace. Made `workspace` — which it was
|
|
617
|
+
until 7.0.0 — it had to answer "no findings" in workspace mode, and that answer is
|
|
618
|
+
indistinguishable from a clean workspace. Measured over the `service-workspace` fixture on
|
|
619
|
+
2026-09-09: the workspace run printed `DEPLOYABLE — no findings` while two of its four
|
|
620
|
+
services violated four of those rows between them. Who the bearers are is the manifest's
|
|
621
|
+
own `discovery` block, never a second walk.
|
|
622
|
+
|
|
623
|
+
**A checkout that does not carry its siblings.** CI checks out `api/` alone, so
|
|
624
|
+
`<workspace>/api_biz` is not there. A check that reads it — `L-CONSUMER` asks who pins this
|
|
625
|
+
package, `L-TOOLING` which service carries it, `U-ORPHAN` which directory nobody declared —
|
|
626
|
+
would walk an absent directory and report the empty set as an answer. So a check declares
|
|
627
|
+
the directories it walks (`requiresSiblings({ row, block })`, derived from the row's own
|
|
628
|
+
patterns), and the runner verifies them before running it:
|
|
629
|
+
|
|
630
|
+
```
|
|
631
|
+
NOT RUN L-CONSUMER — sibling root api_biz is not present in this checkout
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
Measured against a `git archive` export of this repository on 2026-09-09: without the gate,
|
|
635
|
+
`--library --all` raised a blocking `L-CONSUMER` on `conn-base-db` about files nobody
|
|
636
|
+
opened. Never a finding, never a pass (`automation-gates.md` §5).
|
|
637
|
+
|
|
638
|
+
One case is neither mode: a service root that does not lie under the given workspace root.
|
|
639
|
+
Filtering there would drop every finding and the empty table would read as "the row
|
|
640
|
+
looked and found nothing", so each row that needs the workspace — `workspace` and `bearer`
|
|
641
|
+
alike — is reported `NOT RUN — service root is outside the workspace root <root>` instead,
|
|
642
|
+
in the banner and in the signal alike.
|
|
643
|
+
|
|
644
|
+
### Uniforma knihoven (`oa-validate --library`)
|
|
645
|
+
|
|
646
|
+
`manifests/library.manifest.json` is the same concept for the shared libraries, in the
|
|
647
|
+
same package, carrying the same version — so the two shapes cannot drift apart
|
|
648
|
+
(confirmation 002 §12). A library has no boot, so its rows carry one severity:
|
|
649
|
+
`publish`. The choke point is `scripts/publish-library.sh`, and what fails there never
|
|
650
|
+
becomes an artefact and therefore never reaches a service.
|
|
651
|
+
|
|
652
|
+
```bash
|
|
653
|
+
npx oa-validate --library ../../logger-contract # one package
|
|
654
|
+
npx oa-validate --library --all # every package the uniform finds
|
|
655
|
+
npx oa-validate --library --all --workspace /path/to/workspace --json
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
One table (`id · category · where · what · fix`), the `doc` pointer of every reported
|
|
659
|
+
row, exit `0` / `1`; `2` when the run could not start. `--library` with neither a
|
|
660
|
+
package directory nor `--all` is a usage error — nothing is guessed.
|
|
661
|
+
|
|
662
|
+
**Who wears the uniform** is discovery, not a list: `api/shared/**/package.json` minus
|
|
663
|
+
`**/node_modules/**` and `**/tests/fixtures/**`, bound to `api/config/libraries.json`.
|
|
664
|
+
A package the SSOT does not know, and a name in the SSOT with no package, are both
|
|
665
|
+
`U-ORPHAN`. No library name appears in the manifest (004 point 3); the exclusion of the
|
|
666
|
+
validator's own fixtures is a row of the manifest, never a branch in the code.
|
|
667
|
+
|
|
668
|
+
**The category** is the one fact about a library that cannot be derived, so it is
|
|
669
|
+
declared — `"oa": { "category": "core" }` — and checked against the dependency graph
|
|
670
|
+
(005 point 3). A missing declaration and a declaration the graph contradicts are both
|
|
671
|
+
`U-MISMATCH`.
|
|
672
|
+
|
|
673
|
+
| category | layer | may depend on |
|
|
674
|
+
|---|---|---|
|
|
675
|
+
| `core` | L1 | nothing under `@onlineapps` |
|
|
676
|
+
| `connector` | L2 | `core`, `connector` |
|
|
677
|
+
| `orchestration` | L3 | `core`, `connector`, `orchestration` |
|
|
678
|
+
| `runtime` | L4 | `core`, `connector`, `orchestration`, `runtime` |
|
|
679
|
+
| `tooling` | — | any of the above |
|
|
680
|
+
|
|
681
|
+
Within a layer is allowed; upward is not (`architecture-principles.md` §7 forbids a
|
|
682
|
+
reverse dependency, not a neighbouring one). The list a category may depend on includes
|
|
683
|
+
itself, explicitly, so no rule is implied anywhere.
|
|
684
|
+
|
|
685
|
+
**The rows.** `all`: `L-MAIN`, `L-ENGINES` (the platform major, referenced from
|
|
686
|
+
`api/.nvmrc`), `L-TESTS`, `L-TEST-SCRIPT`, `L-PACK-TESTS`, `L-PINS`, `L-NO-FILE-RANGE`,
|
|
687
|
+
`L-CHANGELOG`, `L-README`, `L-README-REGION`, `L-CONSUMER`. Per category: `L-CORE-DEPS`, `L-CORE-EXPORTS`,
|
|
688
|
+
`L-CONNECTOR-ENV`, `L-RUNTIME-CLIENT`, `L-TOOLING`. `orchestration` needs no row of its
|
|
689
|
+
own — "depends only downward" is exactly what `U-MISMATCH` decides, and a second row
|
|
690
|
+
would be a second owner of one rule.
|
|
691
|
+
|
|
692
|
+
`L-CONNECTOR-ENV` asks its question of CODE. It reads each `src/**.js` with comments, the
|
|
693
|
+
text of string and template literals, and regular-expression literals blanked out
|
|
694
|
+
(`checks/librarySource.js` § `codeOnly`); what a template literal interpolates is code
|
|
695
|
+
again, because `${process.env.X}` really does read the environment. Until d.239 it matched
|
|
696
|
+
the raw file text, so a module documenting the duty it obeys — "this module reads no
|
|
697
|
+
`process.env`" — was reported for the sentence in its own header.
|
|
698
|
+
|
|
699
|
+
`L-PACK-TESTS` measures the **effect**, not the file: eight packages keep `tests/` out of
|
|
700
|
+
the tarball with a `files` allowlist and no `.npmignore` at all, and a row checking for
|
|
701
|
+
the file would report eight packages that are already right.
|
|
702
|
+
|
|
703
|
+
**What is deliberately NOT a row** is written down as such, in the manifest's `guidance`
|
|
704
|
+
block, with the reason and the decision it rests on — a duty nothing can decide is
|
|
705
|
+
guidance, never a rule (001 §6). Today: `L-LINT` (002 §14 — one shared config or none,
|
|
706
|
+
and "leave as is" is not an option), the logger/error contract, the boot-phase messages,
|
|
707
|
+
"a removed export bumps the major", and the canonical body of the `test` script. The
|
|
708
|
+
shape check fails the build if a guidance entry grows a `check`.
|
|
709
|
+
|
|
710
|
+
Decision and specification: [`api/docs/governance/confirmations/biz-service-manifest.md`](../../../docs/governance/confirmations/biz-service-manifest.md)
|
|
711
|
+
(entries 001–005).
|
|
712
|
+
|
|
713
|
+
---
|
|
714
|
+
|
|
715
|
+
## Generated files (`oa-sync-template`)
|
|
716
|
+
|
|
717
|
+
### The business-service template lives here
|
|
718
|
+
|
|
719
|
+
`templates/business-service/**` ships **inside this package**, so the shape a service is
|
|
720
|
+
created from is pinned by the same version that pins the manifest it is measured against
|
|
721
|
+
(confirmation 001 §2). `api/templates/business-service` is no longer that source: it is
|
|
722
|
+
this generator's output, committed for reading (§5), and the acceptance §9 asks for —
|
|
723
|
+
"differs from the generator's output by nothing" — is measured by
|
|
724
|
+
`tests/unit/templateMirror.test.js`, file by file, in both directions.
|
|
725
|
+
|
|
726
|
+
Two files are packed under a different name than they are written under, and the map is
|
|
727
|
+
in `src/sync/serviceTemplate.js` (`PACKED_NAMES`). `gitignore` has no dot because npm
|
|
728
|
+
never packs a file called `.gitignore`. `package.json.template` carries a suffix because a
|
|
729
|
+
`package.json` inside a package is a second package to every tool that walks a package
|
|
730
|
+
tree: measured 2026-09-09 under its written name, `oa-validate --library --all` reported
|
|
731
|
+
eight findings about the template and `api/scripts/ci/verify-manifest-pins.mjs` — step 1
|
|
732
|
+
of the pre-push hook — reported two more, all false. The template's pins are deliberately
|
|
733
|
+
NOT the platform's; the generator writes the SSOT's versions when it creates a service.
|
|
734
|
+
Every other file is packed and written under the name it has.
|
|
735
|
+
|
|
736
|
+
`F-DOCKERIGNORE` is `F-GITIGNORE`'s row one step further along, over the SAME
|
|
737
|
+
declaration. `Dockerfile` line `COPY . .` takes everything `.dockerignore` does not
|
|
738
|
+
exclude, and being ignored by git excludes nothing at all: measured by BIZ-hello on
|
|
739
|
+
2026-09-11, a LOCAL production image of hello came to 6.6 GB — 5.2 GB of it `logs/` — and
|
|
740
|
+
carried `config/env-active/*.env` with `DB_PASSWORD`, `JWT_SECRET`, the MinIO keys and
|
|
741
|
+
`RABBITMQ_URL`. The image CI builds is clean by accident, because a fresh checkout has no
|
|
742
|
+
ignored file to copy, so the defect is invisible exactly where the image is built.
|
|
743
|
+
|
|
744
|
+
The entries are DERIVED from `templates/business-service/gitignore` rather than written a
|
|
745
|
+
second time, and the derivation is a translation, not a copy: a slash-less `.gitignore`
|
|
746
|
+
pattern matches at every depth while a `.dockerignore` one is matched from the context
|
|
747
|
+
root, so `*.log` there catches `debug.log` and not `logs/debug.log`. Every slash-less entry
|
|
748
|
+
is therefore emitted in both shapes, an entry carrying a slash travels unchanged, a
|
|
749
|
+
negation keeps its `!` in front and its place in the order, and `.git` is added — the one
|
|
750
|
+
exclusion git can never declare about itself. What the derivation cannot do is exclude
|
|
751
|
+
something `.gitignore` does not: `tests/` is the trap on that side, because Tier-1 of the
|
|
752
|
+
boot reads `tests/cookbooks` and an image without it fails at phase 0.2.
|
|
753
|
+
|
|
754
|
+
The criterion is a comparison and never a list of directories, which is how hello found it:
|
|
755
|
+
the files in `/app` of an image built from the WORKING TREE, `node_modules` aside, are the
|
|
756
|
+
files of an image built from `git archive HEAD`. `tests/unit/dockerignoreBuildContext.integration.test.js`
|
|
757
|
+
measures exactly that through docker itself — `FROM scratch` + `COPY . /app` exported
|
|
758
|
+
locally, so the matcher under test is BuildKit's own — and says NOT RUN with the reason
|
|
759
|
+
when there is no daemon to ask.
|
|
760
|
+
|
|
761
|
+
`G-CI` holds ONE delimited block of `.gitlab-ci.yml` — `oa-ci v1` — to the packaged
|
|
762
|
+
template, rendered for this service. The block carries `include:`, `workflow:`, `stages:`,
|
|
763
|
+
`variables:`, `verify-installation-contract:`, `build:`, `secret_detection:` and
|
|
764
|
+
`deploy-production:`: the pipeline's platform half, which decides WHICH pipelines run at
|
|
765
|
+
all, builds the same way everywhere, holds the installation document and the migrations to
|
|
766
|
+
each other, pins the deploy to the digest CI pushed (`R1`, `R2` and `R5` read this file)
|
|
767
|
+
and carries the step below. It is RENDERED rather than identical because that half names
|
|
768
|
+
the service once, in `GIT_CLONE_PATH`.
|
|
769
|
+
|
|
770
|
+
One key is outside the markers, and the sync never rewrites a line of it: `test:`. What
|
|
771
|
+
separates it from the rest is not where it happened to sit but who decides it — which
|
|
772
|
+
database an integration needs, which `ci:gate:*` steps run and which artefact it publishes
|
|
773
|
+
are facts about the service. `workflow:` and `verify-installation-contract:` looked like
|
|
774
|
+
the same kind of thing until they were measured: on 2026-09-11 all eight repositories
|
|
775
|
+
carried each of them BYTE identical, one variant apiece, and what they say is the
|
|
776
|
+
platform's, so they joined the block (d.251b). `test:` was the only key that actually
|
|
777
|
+
differed, in all eight.
|
|
778
|
+
|
|
779
|
+
The one thing the migration does move is position: a repository whose platform keys sit
|
|
780
|
+
between its own keys — all eight, today — gets the block where the first platform key
|
|
781
|
+
stood, so `test:` ends up after it, TEXT UNCHANGED. YAML mappings are unordered, and the
|
|
782
|
+
alternative would be a block that is not one region. The reason the row is a block at all
|
|
783
|
+
is a measurement and a rule: on 2026-09-11 the template was 151 lines and 7 root keys while
|
|
784
|
+
the eight services carried 176 to 198 lines and 9, and the difference that mattered was the
|
|
785
|
+
`test` job standing up MariaDB, Redis and RabbitMQ as service containers, five `ci:gate:*`
|
|
786
|
+
steps and the `ci/integration-signal.json` artefact. A whole-file row would have had a fix
|
|
787
|
+
that deleted eight integration tiers — a finding nobody can carry out
|
|
788
|
+
(`.claude/rules/automation-gates.md` §3). `stages:` stays in the block and declares all
|
|
789
|
+
four stages, the service's own jobs included; no biz service declares a fifth.
|
|
790
|
+
|
|
791
|
+
The fix is therefore `npx oa-sync-template .gitlab-ci.yml --target .`, in three states: a
|
|
792
|
+
file carrying the block is rewritten between its markers; a file carrying the platform's
|
|
793
|
+
keys UNMARKED — all eight, today — has exactly those keys replaced by the block where the
|
|
794
|
+
first of them stood, so no file ends up with two `build:` keys; a file that is not there is
|
|
795
|
+
created whole, which is what `--new` writes. Which keys the block claims is read from the
|
|
796
|
+
block itself (`src/sync/serviceTemplate.js` § `topLevelKeys`), so adding a job to the
|
|
797
|
+
platform's half is one edit to the template.
|
|
798
|
+
|
|
799
|
+
The template's `.gitlab-ci.yml` runs the conformance check once more in its
|
|
800
|
+
`deploy-production` job, before the SSH step — deploy is confirmation, never discovery
|
|
801
|
+
(confirmation 006 point 3, placed by 008). The step is one file,
|
|
802
|
+
`scripts/verify-deploy-uniform.sh`, called with three arguments: the checkout GitLab made,
|
|
803
|
+
the api repository URL carrying the job token, and the api ref production follows. It
|
|
804
|
+
clones `api` read-only beside the checkout, runs `oa-validate <serviceRoot> --workspace
|
|
805
|
+
<root>` with the engine this repository's pin installed, and refuses the deploy on one
|
|
806
|
+
blocking finding or on `complete: false`. Nothing turns it off.
|
|
807
|
+
|
|
808
|
+
The layout is the whole of why it can be complete: the rows reading a platform SSOT answer
|
|
809
|
+
only for a service at `<root>/api_biz/<service>` beside `<root>/api`. Measured 2026-09-11
|
|
810
|
+
over a converter export — in that layout `complete: true` in 1.1 s; as unrelated siblings,
|
|
811
|
+
seven blocking rows NOT RUN and `complete: false`. So the job sets `GIT_CLONE_PATH` to put
|
|
812
|
+
its own checkout there (no symlink, no second copy) and the script refuses any other
|
|
813
|
+
layout by name. Whether a real pipeline reaches `complete: true` is not measured here: it
|
|
814
|
+
also needs the runner's custom build directory and the api project's job-token allowlist,
|
|
815
|
+
which is the owner's step (008 point 2).
|
|
816
|
+
|
|
817
|
+
The same job carries the OTHER end of that sentence: after the SSH step it runs the
|
|
818
|
+
platform's post-deploy gate over the target it declares —
|
|
819
|
+
`DEPLOY_GATE_TARGETS: "biz:<registry name>"`, because the gate has no default and the job
|
|
820
|
+
says which checks judge it (confirmation `deploy-gate-targets` 002). The gate is
|
|
821
|
+
`api/scripts/run-post-deploy-gate.sh` out of the read-only `api` clone the uniform step
|
|
822
|
+
already made, run in REMOTE mode: the container and its `RestartCount` are read on the biz
|
|
823
|
+
box, the heartbeat projection Registry writes into Redis on the infra box, because those
|
|
824
|
+
two facts live on two machines and this runner is a third (confirmation
|
|
825
|
+
`deploy-gate-targets` 003, `server-topology` 004/005). The infra box is reached as the
|
|
826
|
+
gate-reader account — `GATE_INFRA_HOST`, `GATE_INFRA_USER`, `GATE_INFRA_SSH_PRIVATE_KEY`
|
|
827
|
+
as a second SSH identity — and the job REFUSES before it installs or deploys anything when
|
|
828
|
+
that identity is not configured, naming the missing variables: a service deployed and then
|
|
829
|
+
not verified is the false guarantee `automation-gates.md` §5 is about. A service whose
|
|
830
|
+
contract declares `workflowChecks` needs its `DEPLOY_SMOKE_*` variables too; the gate says
|
|
831
|
+
which one is missing.
|
|
832
|
+
|
|
833
|
+
The template's `Dockerfile` runs the conformance check in its **production stage**, after
|
|
834
|
+
the sources arrive and before the image is finished (confirmation 001 §3.1). It is this
|
|
835
|
+
package's own CLI, and every biz service declares this library under `dependencies`
|
|
836
|
+
(measured across all eight on 2026-09-09), so the `npm ci --omit=dev` above it installs
|
|
837
|
+
what the line calls. An image has no workspace above it, so the rows reading a platform
|
|
838
|
+
file print `NOT RUN` by name there and everything the repository answers for itself runs:
|
|
839
|
+
one finding of severity `boot` or `deploy` exits non-zero and the image is never built.
|
|
840
|
+
|
|
841
|
+
### `oa-sync-template --new` — a service
|
|
842
|
+
|
|
843
|
+
```bash
|
|
844
|
+
npx oa-sync-template --new <name> --into <serviceRoot> [--description <text>] [--workspace <root>]
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
`<name>` is the DIRECTORY name under `api_biz/`; everything else is derived from it and
|
|
848
|
+
substituted — `__SERVICE_NAME__`, `__CONTAINER_NAME__` (`api_service_<name>`),
|
|
849
|
+
`__REPO_NAME__` and `__REGISTRY_NAME__` (both `biz-<name>`). Three things are added to
|
|
850
|
+
the render, and each is a fact about ONE service rather than about the shape: the env
|
|
851
|
+
template is named after the service, `config/env-templates/shared.env` comes from the
|
|
852
|
+
platform env manifest, and every `@onlineapps` pin comes from
|
|
853
|
+
`api/config/libraries.json` — a dependency the SSOT does not know fails the run by name.
|
|
854
|
+
|
|
855
|
+
`--description` is optional. Without it `__SERVICE_DESCRIPTION__` is **left standing**
|
|
856
|
+
and the files carrying it are listed: this run has no description, and writing a
|
|
857
|
+
plausible one would put a fact in the repository that nobody stated. `--into` must not
|
|
858
|
+
exist yet, so nothing anybody else wrote is overwritten.
|
|
859
|
+
|
|
860
|
+
`api/scripts/add-service.sh --scaffold` runs exactly this. What stays in that script is
|
|
861
|
+
what is not the shape of a service: the entry in `api/config/services.json` and the port.
|
|
862
|
+
|
|
863
|
+
### `oa-sync-template [path…]` — an existing service
|
|
864
|
+
|
|
865
|
+
```bash
|
|
866
|
+
npx oa-sync-template [path...] --target <serviceRoot> [--workspace <root>] [--check]
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
Rewrites the files the manifest declares in the classes `identical`, `generated` and
|
|
870
|
+
`contains`, from the same `from:` reference those rows already point the conformance CHECK
|
|
871
|
+
at — one definition, read twice. With no paths it plans every such row. Files of the `own`
|
|
872
|
+
class — `src/**`, `tests/**`, the content of `docs/**` — are never written.
|
|
873
|
+
|
|
874
|
+
**No workspace is required of the run itself** — only of the ROWS that read a workspace
|
|
875
|
+
file, and since d.229 most of them read a file this package carries. The predicate is the
|
|
876
|
+
one the manifest run uses (`rowNeedsWorkspace`), so the `fix` command of `F-INIT`,
|
|
877
|
+
`F-JEST` and `F-RUNNER` can be typed where their finding is now raised: inside the service
|
|
878
|
+
image. A row that does need one is printed `NOT RUN` by name, never skipped in silence.
|
|
879
|
+
|
|
880
|
+
A row the manifest does not make renderable is printed `NOT RUN` with the reason instead
|
|
881
|
+
of being filled from a guess. Today that is `G-PROD-IMAGE` and `G-SETUP`: they declare no
|
|
882
|
+
`from:` reference, so nothing says what their content is rendered from — the first is a
|
|
883
|
+
requirement ABOUT a file, the second a directory that must exist. A row whose file
|
|
884
|
+
blocks the write — an `init.sh` carrying no block marker, where inserting one would mean
|
|
885
|
+
choosing a position among somebody's own install steps; an installation document missing a
|
|
886
|
+
section, where writing one would mean rewriting somebody's prose — is printed `BLOCKED`
|
|
887
|
+
and exits 1.
|
|
888
|
+
|
|
889
|
+
Three rows need more than a splice, and the module that owns their CHECK renders them, so
|
|
890
|
+
the generator cannot produce a file its own check rejects:
|
|
891
|
+
|
|
892
|
+
* **`F-RUNNER`** — the `oa-test-runner v1` block is the template's, except for `build`,
|
|
893
|
+
`env_file` and `networks`, which the same row requires to equal the service the runner
|
|
894
|
+
tests. The check removes those three before comparing against the template; the sync
|
|
895
|
+
grafts this repository's own into the rendered block. Seven of the eight biz services
|
|
896
|
+
reach a second network in both nodes on 2026-09-09 — that is the rule being kept, not
|
|
897
|
+
broken, so neither half may report it.
|
|
898
|
+
|
|
899
|
+
Unlike `init.sh`, a compose file is never `BLOCKED`, because a mapping has one place its
|
|
900
|
+
entries can start. **Three states**: a file carrying the markers is rewritten between
|
|
901
|
+
them; a file carrying a runner but NO markers has that runner's declaration REPLACED
|
|
902
|
+
where it stands — measured 2026-09-10, seven of the eight repositories are in that state
|
|
903
|
+
(d.220), and a run that only inserted gave them a second `<container>_tests:` key, which
|
|
904
|
+
compose either refuses or resolves by taking the last one; a file carrying neither gets
|
|
905
|
+
the block inserted under `services:`. The replacement spans the KEY to the next key at
|
|
906
|
+
the same level, so a comment above it — in `api_biz/converter` the memory measurement
|
|
907
|
+
taken inside that container — survives untouched.
|
|
908
|
+
* **`G-PROD`** — `docker-compose.production.yml` is the template rendered with this
|
|
909
|
+
repository's DECLARED name (`config/service/config.json` → `service.name`), whole file
|
|
910
|
+
and nothing else. The class is `generated` rather than `identical` because the file
|
|
911
|
+
carries an identity — the container, the image repository, the service's own env file —
|
|
912
|
+
and the render is what puts it there; owner decision `server-topology` 005, on the
|
|
913
|
+
measurement that the seven old-shape copies were byte-identical once the name was
|
|
914
|
+
normalised. There is no prose to protect here, so an existing file is REWRITTEN rather
|
|
915
|
+
than spliced or blocked, and the R1 bridge `G-PROD-IMAGE` keeps answering about the pin
|
|
916
|
+
beside it.
|
|
917
|
+
* **`G-README`** — the generated uniform pointer of `README.md`, rendered from the packaged
|
|
918
|
+
manifest and applied to the region between the markers; everything outside them is left
|
|
919
|
+
as it was. The file itself is never created: a README is prose somebody wrote, and an
|
|
920
|
+
absent one is what the row reports.
|
|
921
|
+
* **`G-SETUP-INSTALL` / `-MATRIX` / `-VALIDATION`** — `skeleton_only`: the check compares
|
|
922
|
+
the set of `##` headings against the template document and never the prose. A missing
|
|
923
|
+
document is created from the skeleton (header, title, sections, no prose); an existing
|
|
924
|
+
one is never rewritten, and a missing section is `BLOCKED` with the heading named. That
|
|
925
|
+
a document is absent at all stays `G-SETUP`'s finding, through the installation
|
|
926
|
+
contract's `SETUP_DOCS`, so one defect keeps one owner.
|
|
927
|
+
|
|
928
|
+
`--check` writes nothing and exits 1, naming the first line that differs and what the run
|
|
929
|
+
would put there — both sides, because one of them alone lies about half the runs: an
|
|
930
|
+
INSERTION is not described by what the file already said at that line. Measured over the
|
|
931
|
+
eight biz repositories on 2026-09-09, the set of files this run would change is exactly the
|
|
932
|
+
set of `F-INIT`/`F-JEST` findings `oa-validate` reports on the same trees, which is what
|
|
933
|
+
"one definition" has to mean to be worth anything. On the same trees `F-RUNNER` used to
|
|
934
|
+
report `BLOCKED`, and the row's `fix` accordingly read "paste the block once by hand" —
|
|
935
|
+
advice, where confirmation 001 §3.2 asks for a command. It is a command now.
|
|
936
|
+
|
|
937
|
+
### `oa-sync-template template` — the committed template directory
|
|
938
|
+
|
|
939
|
+
```bash
|
|
940
|
+
npx oa-sync-template template --target <dir> [--check]
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
Renders the packaged template with its own placeholders as the parameters, which is what
|
|
944
|
+
`api/templates/business-service` is. `--check` is the gate: it exits 1 both on a file that
|
|
945
|
+
differs and on a file the packaged template has no source for.
|
|
946
|
+
|
|
947
|
+
### `oa-sync-template shared-env`
|
|
948
|
+
|
|
949
|
+
`config/env-templates/shared.env` is generated, never edited, never edited. The platform env manifest
|
|
950
|
+
`api/config/shared-env.json` owns the shared key set — name, template value, why the
|
|
951
|
+
platform shares it — and every bearer's copy is rendered from it, so a service can only
|
|
952
|
+
add keys of its own in `service.env`.
|
|
953
|
+
|
|
954
|
+
```bash
|
|
955
|
+
npx oa-sync-template shared-env --target <bearer dir> [--workspace <root>] [--check]
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
`--target` is the bearer: a service root, `api/` itself, or the business-service
|
|
959
|
+
template; `--workspace` is the directory holding `api/` and `api_biz/`, found upwards
|
|
960
|
+
from the target when it is not given. `--check` writes nothing and exits 1 with the diff
|
|
961
|
+
when the file on disk is not what the manifest renders; a run that cannot start at all
|
|
962
|
+
exits 2. There is no flag pointing the run at another manifest — the shared key set has
|
|
963
|
+
one owner.
|
|
964
|
+
|
|
965
|
+
Decision and specification: [`api/docs/governance/confirmations/biz-service-manifest.md`](../../../docs/governance/confirmations/biz-service-manifest.md) §18.
|
|
966
|
+
|
|
967
|
+
### `oa-sync-template readme-uniform`
|
|
968
|
+
|
|
969
|
+
Discoverability of a uniform is one rendered pointer per repository, not a header per
|
|
970
|
+
file: a generated region of the repository's `README.md`, everything in it read from the
|
|
971
|
+
manifest that ships in this package — the link included, which is computed from the two
|
|
972
|
+
paths rather than written down. Nothing outside the markers is touched.
|
|
973
|
+
|
|
974
|
+
```bash
|
|
975
|
+
npx oa-sync-template readme-uniform (--target <dir> | --all) [--workspace <root>] [--check]
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
`--target` reads the KIND from the disk, because the kind is a fact of the directory and a
|
|
979
|
+
`--kind` flag would be a second way to state it:
|
|
980
|
+
|
|
981
|
+
| the root carries | kind | the region between markers |
|
|
982
|
+
|---|---|---|
|
|
983
|
+
| `config/service/operations.json` | service | `biz-service-uniform` — `Uniform: [biz-service](<link>)`, which repositories wear the uniform (the `discovery` block and its SSOT), the row ids grouped by the manifest section that declares them, and which path falls under which row |
|
|
984
|
+
| `package.json` (and no such file) | library | `library-uniform` — `Uniform: [library/<category>](<link>)` and the duty sections the declared category wears, with the ids of their rows |
|
|
985
|
+
| neither | — | fail-fast naming both files it looked for |
|
|
986
|
+
|
|
987
|
+
`--all` is the library run and stays one: the eight service repositories are separate
|
|
988
|
+
checkouts, not siblings under one workspace, so there is no list to walk — a service is
|
|
989
|
+
written one `--target` at a time. A service needs no workspace above it either; its region
|
|
990
|
+
is the packaged manifest and its link is the copy npm installs into its own
|
|
991
|
+
`node_modules`, which a reader resolves without a network.
|
|
992
|
+
|
|
993
|
+
A service region renders a row's path, and a row taking its value from elsewhere renders
|
|
994
|
+
the path that OWNS the value rather than the value (`` `api/.nvmrc` ``, not `Node 24`).
|
|
995
|
+
Never a `why` or a `fix` — that prose has an owner, and the region is not it. A FORBIDDEN
|
|
996
|
+
row contributes its id and not its path: `hooks/pre-commit` and
|
|
997
|
+
`scripts/run-pre-validation.js` may not occur in a service repository as text at all
|
|
998
|
+
(`api/tests/scripts/add-service.bats` greps the template for them, because `init.sh` used
|
|
999
|
+
to install the hook), so a region naming them would write a retired path back into every
|
|
1000
|
+
service.
|
|
1001
|
+
|
|
1002
|
+
The region carries no date, no version and no count. The version a README is checked
|
|
1003
|
+
against IS the pin on this package, because the manifest ships inside it; a date would
|
|
1004
|
+
make two runs render different bytes, and a generator whose output varies cannot be a
|
|
1005
|
+
gate. A library that declares no `oa.category` is reported `NOT RUN` under `--all` and
|
|
1006
|
+
counted out of the coverage line — its missing declaration is already the blocking
|
|
1007
|
+
finding `U-MISMATCH` of `oa-validate --library`, and this run never guesses one. Asked
|
|
1008
|
+
about such a package by name with `--target`, the run fails fast instead. A service
|
|
1009
|
+
declares nothing of the sort: the uniform it wears follows from its location (005 point 1).
|
|
1010
|
+
|
|
1011
|
+
**The region is gated, not only generated** (owner decision 2026-09-09). A `generated` class
|
|
1012
|
+
is a generator PLUS a row carrying its `--check` (001 §2, the precedent being
|
|
1013
|
+
`G-SHARED-ENV`), and until d.215e this one had the generator alone: a README whose pointer
|
|
1014
|
+
had gone stale was reported by nothing anybody runs on a repository. Two rows say it now —
|
|
1015
|
+
`G-README` of the biz-service uniform (severity `deploy`) and `L-README-REGION` of the
|
|
1016
|
+
library one (severity `publish`) — and they render through this very module and compare
|
|
1017
|
+
through the same `checkUniformRegion` the `--check` run calls, so a row and the command
|
|
1018
|
+
cannot disagree about one file. A missing region, a stale one and an absent README are three
|
|
1019
|
+
sentences with three fixes; the row names the first line that differs, and the full diff is
|
|
1020
|
+
one command away, which is the row's own `fix`.
|
|
1021
|
+
|
|
1022
|
+
`L-README` is untouched beside it: it asks whether the node header is there, which a human
|
|
1023
|
+
writes, while `L-README-REGION` asks whether the rendered half still says what the manifest
|
|
1024
|
+
says. Two writers, two fixes, two rows. A library declaring no category raises neither —
|
|
1025
|
+
that is `U-MISMATCH`, blocking, with its own fix — and a checkout holding no copy of this
|
|
1026
|
+
package reports `L-README-REGION` `NOT RUN` with that reason, because the link the region
|
|
1027
|
+
carries points at the manifest as it lies in the checkout being read.
|
|
1028
|
+
|
|
1029
|
+
Decision and specification: [`api/docs/governance/confirmations/biz-service-manifest.md`](../../../docs/governance/confirmations/biz-service-manifest.md) § Confirmation 20260909-biz-service-manifest-005, point 2.
|
|
1030
|
+
|
|
1031
|
+
### `oa-sync-template docs-region`
|
|
1032
|
+
|
|
1033
|
+
The documentation tree copied the same descriptive lists by hand: the repository shape in
|
|
1034
|
+
three nodes, the `package.json` scripts in two, the runtime numbers in several. A copy is
|
|
1035
|
+
kept true by review only, which is what `.claude/rules/doc-code-binding.md` §1 forbids for
|
|
1036
|
+
a descriptive fact. Five regions render those lists from the two packaged manifests:
|
|
1037
|
+
|
|
1038
|
+
| id | renders |
|
|
1039
|
+
|---|---|
|
|
1040
|
+
| `biz-service-tree` | the packaged template's files, each annotated with the row that owns the path; the forbidden paths, and a path a row owns that the template does not carry |
|
|
1041
|
+
| `biz-service-scripts` | `scripts.required` with the body of each row, and `scripts.forbidden` |
|
|
1042
|
+
| `biz-service-runtime` | the runtime rows and every row carrying a memory limit, with their own parameters |
|
|
1043
|
+
| `biz-service-alignment` | every row of the service manifest — id, severity, what it checks |
|
|
1044
|
+
| `library-duties` | the library categories with their layer and `may_depend_on`, the duty sections by row id, and the guidance entries marked apart as named non-rules |
|
|
1045
|
+
|
|
1046
|
+
```bash
|
|
1047
|
+
npx oa-sync-template docs-region (--id <id> | --all) --file <document> [--workspace <root>] [--check]
|
|
1048
|
+
npx oa-sync-template docs-region --list
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
What a region carries is a row's observable — its id, its check, the paths and values its
|
|
1052
|
+
own parameters name. Never its `why` or `fix`: that prose has an owner, and a copy of it in
|
|
1053
|
+
five documents is the duplication the mechanism exists to remove. A `from:` reference
|
|
1054
|
+
renders as the path that OWNS the value, never as the value itself, so `` `api/.nvmrc` ``
|
|
1055
|
+
stays true when the platform moves to the next Node major. `${runner}` stays unexpanded,
|
|
1056
|
+
because the name of the one-shot test runner is per repository.
|
|
1057
|
+
|
|
1058
|
+
The run REPLACES a region and never inserts one. These documents live in `api/docs/**`, a
|
|
1059
|
+
tree owned by other threads: where a section belongs in them is their owner's decision, and
|
|
1060
|
+
a generator writing into somebody else's document is the unsafe side effect
|
|
1061
|
+
`.claude/rules/automation-gates.md` §1 requirement 3 forbids. A document carrying no
|
|
1062
|
+
markers is therefore a failure that prints the two lines to paste; `--all` covers exactly
|
|
1063
|
+
the regions a document already declares. Like every region in this package, the output
|
|
1064
|
+
carries no date, no version and no count.
|
|
1065
|
+
|
|
1066
|
+
Finding, replacing, extracting and diffing a marked region is `src/sync/generatedRegion.js`,
|
|
1067
|
+
shared with `readme-uniform` — one implementation of the marker shape in the package.
|
|
1068
|
+
|
|
1069
|
+
Decision and specification: [`api/docs/governance/confirmations/biz-service-manifest.md`](../../../docs/governance/confirmations/biz-service-manifest.md) § Confirmation 20260909-biz-service-manifest-002, §16.2.
|
|
1070
|
+
|
|
1071
|
+
---
|
|
1072
|
+
|
|
93
1073
|
## Runtime Directory Structure
|
|
94
1074
|
|
|
95
1075
|
```
|
|
@@ -239,6 +1219,60 @@ ServiceStructureValidator.getStandardLevels();
|
|
|
239
1219
|
|
|
240
1220
|
**Related:** [Biz Service Canonical Shape — Implementation Standard Levels](/docs/biz/00-model/service-shape.md#implementation-standard-levels), [Biz Error Handling Contract](/docs/biz/70-contracts/error-handling.md)
|
|
241
1221
|
|
|
1222
|
+
## Test namespace (`getTestNamespace`, `getForeignTestNamespace`, `assertAllowedTenant`)
|
|
1223
|
+
|
|
1224
|
+
An integration test reaches a real database, so the tenant / workspace it writes
|
|
1225
|
+
into is a safety boundary — never a value a test file picks for itself. Both
|
|
1226
|
+
helpers read the platform env (`TESTING_TENANT_ID`, `TESTING_WORKSPACE_ID`) and
|
|
1227
|
+
refuse anything outside the environment classes
|
|
1228
|
+
[tenant-allocation.md](/docs/standards/tenant-allocation.md) allocates to
|
|
1229
|
+
non-production; a missing or malformed value throws rather than defaulting.
|
|
1230
|
+
|
|
1231
|
+
```javascript
|
|
1232
|
+
const { getTestNamespace, getForeignTestNamespace } = require('@onlineapps/conn-orch-validator');
|
|
1233
|
+
|
|
1234
|
+
const ctx = getTestNamespace(); // the namespace this test owns
|
|
1235
|
+
const other = getForeignTestNamespace(); // one it must NOT see rows from
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1238
|
+
`getForeignTestNamespace()` exists for isolation tests: a handler filters on
|
|
1239
|
+
tenant_id AND workspace_id, and proving it does needs a second namespace. It
|
|
1240
|
+
returns another of the same allowed classes, chosen by position in that list and
|
|
1241
|
+
wrapping at its end — deterministic for a given environment, and never a literal
|
|
1242
|
+
customer tenant or a derived `tenant + 1` (the platform default 99 + 1 is the
|
|
1243
|
+
LIVE tenant). The workspace half is the caller's own.
|
|
1244
|
+
|
|
1245
|
+
Deploy-contract R8 accepts a call to either helper as the one legitimate source
|
|
1246
|
+
of a namespace inside `tests/integration/`.
|
|
1247
|
+
|
|
1248
|
+
### `assertAllowedTenant(tenantId, { purpose, setBy })`
|
|
1249
|
+
|
|
1250
|
+
The same boundary asked about an id the caller already holds. An operational
|
|
1251
|
+
script — clone a namespace, wipe one, backfill one — is handed a tenant on the
|
|
1252
|
+
command line, and the question it must answer is not "which namespace do I own"
|
|
1253
|
+
but "may I touch this one at all".
|
|
1254
|
+
|
|
1255
|
+
```javascript
|
|
1256
|
+
const { assertAllowedTenant } = require('@onlineapps/conn-orch-validator');
|
|
1257
|
+
|
|
1258
|
+
const target = assertAllowedTenant(args.tenant, {
|
|
1259
|
+
purpose: '--to-tenant', // what the caller calls the value
|
|
1260
|
+
setBy: 'on the command line' // where that name is given a value
|
|
1261
|
+
});
|
|
1262
|
+
// 98 — an integer, whatever shape came in ("98" and 98 both arrive this way)
|
|
1263
|
+
```
|
|
1264
|
+
|
|
1265
|
+
It returns the id normalized to an integer, and throws otherwise: absent, not an
|
|
1266
|
+
integer, or not one of the environment classes the standard allocates to
|
|
1267
|
+
non-production. The refusal is the sentence `getTestNamespace()` throws, rendered
|
|
1268
|
+
from the same place — `purpose` and `setBy` are the only parts that differ, so an
|
|
1269
|
+
operator who typed `--to-tenant 101` is told to change that flag rather than an
|
|
1270
|
+
env file they never touched.
|
|
1271
|
+
|
|
1272
|
+
The class list itself is deliberately **not** exported: a copy of a boundary is a
|
|
1273
|
+
second boundary, and the two drift. The refusal names the allowed classes, so a
|
|
1274
|
+
caller that has to show them never enumerates them itself.
|
|
1275
|
+
|
|
242
1276
|
## Related Documentation
|
|
243
1277
|
|
|
244
1278
|
- [/docs/architecture/validator.md](/docs/architecture/validator.md)
|