@onlineapps/conn-orch-validator 6.0.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,39 @@ All notable changes to this package. Follows [Keep a Changelog](https://keepacha
7
7
  > ⚠ Neither this entry nor the connector-step work of `dcddb862` is published.
8
8
  > The last version on NPM is 3.3.2, so **no biz CI run sees any of it yet**.
9
9
 
10
+ ### BREAKING — standard level v1.1 "Multitenancy Standard" is removed (DÁVKA 88j)
11
+
12
+ **Major.** `getStandardLevels()` now returns three levels, `v1.0, v1.2, v1.3`, and
13
+ `determineStandardLevel().details` has one entry fewer. Anything indexing that
14
+ array by position, or expecting a `tenant_context` check, reads something else now.
15
+
16
+ Owner decision: `api/docs/governance/confirmations/service-shape-v11-retirement.md`.
17
+
18
+ v1.1 held a single check, `tenant_context`, and it asserted nothing but the
19
+ EXISTENCE of `wrapper.tenantContext` in `config/service/config.json`. Nothing
20
+ reads that key: the wrapper runtime reads `cache`, `infrastructureGate`,
21
+ `health`, `validation`, `mq`, `registry`, `monitoring`, `state`, `secrets` and
22
+ `heartbeat`; `createTenantContextMiddleware` had had no caller since ADR 0005
23
+ removed the HTTP surface (and was itself deleted on 2026-09-05,
24
+ `docs/governance/confirmations/wrapper-tenant-middleware.md`); at the time,
25
+ `service-wrapper/config/runtime-defaults.json` still supplied the key regardless
26
+ of what a service declared. The gate was enforcing a
27
+ dead declaration — `change-discipline.md` § *Removing something removes its
28
+ declaration*.
29
+
30
+ - **Removed** the level, not its checks. An empty level with `checks: () => []`
31
+ would pass for every service while asserting nothing, which is the false
32
+ guarantee `automation-gates.md` §5 names.
33
+ - **The number is not reused and the gap stays.** v1.2 and v1.3 keep their names,
34
+ so a level recorded in an earlier proof still means what it meant. Cumulative
35
+ now reads *"v1.2 requires v1.0"*.
36
+ - **A service is free to drop `wrapper.tenantContext`** and reach the same level
37
+ it reaches with it. Measured over `api_biz/*` (8 services, all of which still
38
+ declare the key): every verdict is `v1.3` before the change and `v1.3` after —
39
+ the removal takes a requirement away without moving a single live verdict.
40
+ - The package's own conformant fixture (`tests/fixtures/v3-cookbook-service`)
41
+ drops the key in the same change; it carried it only to satisfy v1.1.
42
+
10
43
  ### BREAKING — a cookbook step spelling its identifier `id` is refused (DÁVKA 77 A3)
11
44
 
12
45
  `format.md` § Steps has banned `id` since v2.1; the code accepted it anyway, which
package/README.md CHANGED
@@ -9,11 +9,16 @@ Coordinates validation across ALL layers (base, infra, orch, business) to ensure
9
9
  This is **NOT** a development testing tool. This is a **production validation orchestrator** that:
10
10
 
11
11
  1. **Validates service structure** - directories, files, configuration
12
- 2. **Validates configuration** - config.json, operations.json compliance
13
- 3. **Validates business logic** - cookbook tests with mocked infrastructure
14
- 4. **Validates HTTP API** - endpoints respond correctly
15
- 5. **Validates connector integration** - all connectors work with service
16
- 6. **Generates validation proof** - cryptographic SHA256 proof for registry
12
+ 2. **Validates configuration** - `config.json`, `operations.json` compliance
13
+ 3. **Validates the environment contract** - every `env.required` name is set
14
+ 4. **Validates operations** - the v3 handler-registry rules
15
+ 5. **Validates business logic** - cookbook tests with mocked infrastructure
16
+ 6. **Validates connector integration** - the two declarations agree and the environment backs them
17
+ 7. **Generates validation proof** - SHA256 proof carried into registration
18
+
19
+ There is no HTTP step. Biz containers expose no HTTP surface (ADR 0005), so
20
+ nothing here probes an endpoint — the per-operation endpoint probe and the
21
+ `/health` probe were both removed with it.
17
22
 
18
23
  Used in **Tier 1 Pre-Validation** (offline, before registration) and invoked automatically by ServiceWrapper.
19
24
 
@@ -22,18 +27,13 @@ Used in **Tier 1 Pre-Validation** (offline, before registration) and invoked aut
22
27
  **You don't use this directly!** ServiceWrapper handles validation automatically:
23
28
 
24
29
  ```javascript
25
- // services/my-service/index.js
26
- const { ServiceWrapper } = require('@onlineapps/service-wrapper');
27
-
28
- const wrapper = new ServiceWrapper({
29
- service: app,
30
- serviceRoot: __dirname
31
- });
32
-
33
- // Validation happens automatically
34
- await wrapper.initialize();
30
+ // index.js of a biz service — the whole file
31
+ require('@onlineapps/service-wrapper').bootstrap(__dirname);
35
32
  ```
36
33
 
34
+ `bootstrap()` builds the wrapper from `src/config`, and `wrapper.initialize()`
35
+ runs Tier-1 validation in phase 0.2 — before MQ connects, before registration.
36
+
37
37
  **No test files needed in business service!**
38
38
 
39
39
  ---
@@ -43,7 +43,7 @@ await wrapper.initialize();
43
43
  1. **Service Structure** - directories and files exist
44
44
  2. **Config Files** - valid JSON, required fields
45
45
  3. **Environment Contract** - every variable the contract declares `env.required` is set
46
- 4. **Operations Compliance** - follows OPERATIONS.md standard
46
+ 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
47
  5. **Cookbook Tests** - business logic + integration (MOCKED infra)
48
48
  6. **Connector Integration** - the connector declarations agree and the environment backs them
49
49
 
@@ -116,7 +116,7 @@ services/my-service/
116
116
  "serviceName": "hello-service",
117
117
  "version": "1.0.0",
118
118
  "validator": "@onlineapps/conn-orch-validator",
119
- "validatorVersion": "2.0.6",
119
+ "validatorVersion": "<this package's version, read from its package.json>",
120
120
  "validatedAt": "2025-10-22T10:30:45.123Z",
121
121
  "testsRun": 15,
122
122
  "testsPassed": 15,
@@ -127,29 +127,102 @@ services/my-service/
127
127
  ```
128
128
 
129
129
  **Proof Lifecycle:**
130
- - **Valid for:** 7 days OR until service fingerprint changes
131
- - **Fingerprint:** SHA256 hash of (version + operations + dependencies + config)
132
- - **Revalidation:** Automatic if proof missing/invalid/expired
133
- - **Registry:** Accepts service with valid proof (skips Tier 2 validation)
130
+ - **Written:** on every successful validation, i.e. on every boot. There is no
131
+ proof cache the one that existed skipped steps 1-3 and 5-6 to save 6 ms and
132
+ bought a window of up to seven days in which validation asserted something no
133
+ longer true. It is gone, and with it the 7-day expiry it needed.
134
+ - **Fingerprint:** SHA256 over (service version + operations + `@onlineapps/*`
135
+ dependencies + config + infra files + cookbooks + seeds), stored in the proof
136
+ as `contractFingerprint`. Nothing reads it back to skip work; it records what
137
+ the run covered. The **service version comes from the service `package.json`,
138
+ and from nowhere else** — the same single source of truth the readiness
139
+ section below describes. It used to be `config.service?.version ||
140
+ pkg.version`, which for a service carrying `"${npm_package_version}"` in
141
+ `config/service/config.json` hashed that literal and never consulted
142
+ package.json at all, so a version bump moved no bit of the fingerprint. A
143
+ missing, unparseable or version-less package.json now fails the run instead of
144
+ falling back.
145
+ - **Registry:** the proof travels in the registration payload
146
+ (`ServiceWrapper.js`, `validationProof` field) — a service that fails
147
+ validation never reaches registration, because phase 0.2 throws.
134
148
 
135
149
  ---
136
150
 
151
+ ## Readiness scoring (`ServiceReadinessValidator`)
152
+
153
+ A separate scorer from the six-step Tier-1 run above. Tier-1 stopped calling it
154
+ in 2026-08 — its verdict was a strict function of steps 2 and 3, so it announced
155
+ a result it never independently reached. What consumes it today is
156
+ `helpers/createServiceReadinessTests`, and through it every biz repo's
157
+ `tests/bootstrap/` suite.
158
+
159
+ | Check | Weight | Required | What it means |
160
+ |---|---|---|---|
161
+ | `operations` | 80 | yes | the v3 rules — `handler`, `bundle_scope`, `input`, `output`, no retired v2 field |
162
+ | `cookbook` | 15 | no | runs only when a `testCookbook` is passed; structural validation of it |
163
+ | `registry` | 5 | no | runs only when a `registry` is passed; the service can register and its version does not clash |
164
+
165
+ `ready` is true when every required check passed AND the score is at least 60.
166
+ The retired `health` check's 20 points folded into `operations` (ADR 0005), so
167
+ the maximum is still 100.
168
+
169
+ **"Can register" is not truthiness.** `canRegister` requires a `name`, an
170
+ `operations` object, and a `version` matching the semver 2.0.0 grammar — the
171
+ last one because the line below it compares that value against what the registry
172
+ already holds, and comparing two strings that are not versions decides nothing.
173
+ The string that made this measurable is `"${npm_package_version}"`: every biz
174
+ `config/service/config.json` declares it under `service.version`, it is truthy,
175
+ and the gate used to accept it (measured by BIZ-ingest). Each failing part
176
+ contributes one `[Context] Problem - Expected/Fix` line to `checks.registry.errors`.
177
+
178
+ The version a caller passes comes from the service **`package.json`**, never from
179
+ `config.json`. That is the single source of truth the runtime uses:
180
+ `ConfigLoader.loadServiceConfig()` overwrites `config.service.version` from
181
+ package.json before `${VAR}` placeholders are resolved, so the config.json key
182
+ never reaches a running service. `helpers/createServiceReadinessTests` reads
183
+ package.json for exactly that reason.
184
+
185
+ **Every evaluation writes exactly one line** through the injected logger:
186
+ `[ServiceReadinessValidator] Readiness evaluated` with a structured object
187
+ (`serviceName`, `score`, `maxScore`, `ready`, `checks` as `{name: {passed,
188
+ score}}`, `errors`, `warnings`). A refusal adds one `error`-level line,
189
+ `[ServiceReadinessValidator] Service not ready`. There is no per-check
190
+ narration: one evaluation, one complete, machine-readable record.
191
+
192
+ The constructor requires a logger with all four of `info`, `warn`, `error`,
193
+ `debug`, and says which one is missing when it is not — see
194
+ `docs/governance/confirmations/connector-logger-contract.md`.
195
+
137
196
  ## Implementation Standard Levels
138
197
 
139
198
  The validator evaluates each service against cumulative implementation standards. Levels are ordered — each requires all previous to pass:
140
199
 
141
200
  | Level | Name | Checks | Since |
142
201
  |-------|------|--------|-------|
143
- | **v1.0** | Base Service Standard | `config/service/`, `src/app.js`, `index.js`, valid `config.json` + `operations.json`, `@onlineapps/service-wrapper` dep | 2025-06 |
144
- | **v1.1** | Multitenancy Standard | `wrapper.tenantContext` configured in `config.json` | 2026-03 |
145
- | **v1.2** | Business Error Handling | `business_error_contract` (some file under `src/**` imports a `BusinessError` family from `@onlineapps/service-wrapper` or declares the brand `onlineapps.businessError`) + `no_retired_error_hierarchy` (no error class imported from `@onlineapps/service-common` anywhere under `src/`) | 2026-03 |
202
+ | **v1.0** | Base Service Standard | `config/service/`, `src/`, `tests/`, `src/handlers/` (v3 handler modules — ADR 0005 replaced `src/app.js`), `index.js`, `config.json` with `service.name`, `operations.json` with an `operations` object, `@onlineapps/service-wrapper` dep | 2025-06 |
203
+ | **v1.2** | Business Error Handling Standard | `business_error_contract` (some file under `src/**` imports a `BusinessError` family from `@onlineapps/service-wrapper` or declares the brand `onlineapps.businessError`) + `no_retired_error_hierarchy` (no error class imported from `@onlineapps/service-common` anywhere under `src/`) | 2026-03 |
204
+ | **v1.3** | Zero-HTTP Shape (ADR 0005) | `src/app.js` absent, `src/routes/` absent, `src/middlewares/` absent, `express` not in dependencies | 2026-08 |
205
+
206
+ **v1.1 "Multitenancy Standard" was retired on 2026-09-04** (owner decision,
207
+ `/docs/governance/confirmations/service-shape-v11-retirement.md`). Its only check
208
+ asserted that `wrapper.tenantContext` was present in `config.json`, and nothing
209
+ reads that key — the wrapper runtime never looks it up,
210
+ `createTenantContextMiddleware` was deleted outright on 2026-09-05
211
+ (`/docs/governance/confirmations/wrapper-tenant-middleware.md`), and
212
+ `runtime-defaults.json` no longer carries the key either. The level is removed,
213
+ not emptied: a level with no checks passes for everyone while asserting nothing
214
+ (`automation-gates.md` §5). The number is not reused and the gap in the numbering
215
+ stays, so a level recorded earlier still means what it meant.
146
216
 
147
217
  **Key properties:**
148
- - **Cumulative** — v1.2 requires v1.0 + v1.1 to also pass
218
+ - **Cumulative** — each level requires every level listed above it (so v1.2
219
+ requires v1.0); the table is the authority, not the version number
149
220
  - **Baked into validator** — older validator versions naturally know fewer levels (backward compatible)
150
221
  - **Warnings** — next unsatisfied level generates `STANDARD_LEVEL_GAP` warnings with specific missing checks
151
- - **Exposed in `/info`**ServiceWrapper's `GET /info` endpoint returns the highest satisfied level
152
- - **In validation results** `validate()` returns `standardLevel` and `standardDetails`
222
+ - **In validation results** — `ServiceStructureValidator.validate()` returns
223
+ `standardLevel` and `standardDetails`. That is the only place the level
224
+ surfaces: `GET /info` used to carry it and was retired with the rest of the
225
+ HTTP surface (ADR 0005), and nothing has taken its place
153
226
 
154
227
  ```javascript
155
228
  // Programmatic access
@@ -168,12 +241,7 @@ ServiceStructureValidator.getStandardLevels();
168
241
 
169
242
  ## Related Documentation
170
243
 
171
- - [SERVICE_REGISTRATION_FLOW.md](/services/hello-service/docs/SERVICE_REGISTRATION_FLOW.md)
172
244
  - [/docs/architecture/validator.md](/docs/architecture/validator.md)
173
245
  - [/docs/biz/30-operations/schema-v3.md](/docs/biz/30-operations/schema-v3.md)
174
246
  - [/docs/standards/ERROR_HANDLING.md](/docs/standards/ERROR_HANDLING.md)
175
247
  - [@onlineapps/service-validator-core](/shared/service-validator-core/README.md)
176
-
177
- ---
178
-
179
- *Last updated: 2026-03-24*
package/docs/DESIGN.md CHANGED
@@ -5,13 +5,14 @@
5
5
  Validation framework for OA Drive microservices. Drives two flows:
6
6
 
7
7
  1. **Pre-validation** (Tier 1) — runs during `service-wrapper` startup. Verifies
8
- structure (config files, package.json), readiness (endpoints respond, health
9
- works) and produces a signed `validation-proof.json` stored under
10
- `conn-runtime/`.
8
+ structure (config files, package.json), the environment contract, the v3
9
+ operations rules and the cookbook tests, then produces a signed
10
+ `validation-proof.json` stored under `conn-runtime/`. Nothing is probed over
11
+ HTTP: biz containers expose no endpoints (ADR 0005).
11
12
  2. **Readiness checks** — a reusable probe (`createServiceReadinessTests`)
12
13
  consumable from the `tests/bootstrap/` suites of individual biz services.
13
14
 
14
- The single source of truth for service endpoint metadata is
15
+ The single source of truth for service operation metadata is
15
16
  [`operations.json`](../../../docs/biz/30-operations/registration-wire.md).
16
17
  OpenAPI iteration (`paths`/`operationId`) is NOT supported — that legacy
17
18
  surface was removed together with the now-retired `ServiceValidator` /
@@ -38,8 +39,9 @@ surface was removed together with the now-retired `ServiceValidator` /
38
39
 
39
40
  ### Production Validation
40
41
 
41
- - `ValidationOrchestrator` — 5-step pre-validation pipeline (structure, config,
42
- operations, cookbooks, connectors), emits proof
42
+ - `ValidationOrchestrator` — six-step pre-validation pipeline (structure,
43
+ config, environment contract, operations, cookbooks, connectors), emits proof.
44
+ Each step announces itself as `Step N/6` through the injected logger
43
45
  - `ServiceReadinessValidator` — score-based checks (operations 80 / cookbook 15
44
46
  / registry 5). Not used by the orchestrator: its only consumer is
45
47
  `createServiceReadinessTests`, and through it the biz repos'
@@ -52,27 +54,31 @@ surface was removed together with the now-retired `ServiceValidator` /
52
54
 
53
55
  ### Test Suite Helpers
54
56
 
55
- - `createServiceReadinessTests(options)` — Jest suite generator
57
+ - `createServiceReadinessTests(testsDir, options)` — Jest suite generator
56
58
 
57
59
  ## Integration Points
58
60
 
59
61
  - `@onlineapps/service-wrapper` instantiates `ValidationOrchestrator` during
60
- wrapper startup. Proof is cached under `conn-runtime/validation-proof.json`
61
- (30-day validity, invalidated by config fingerprint change includes
62
- operations map, see
63
- [operations-registry-contract.md §3](../../../docs/biz/30-operations/registration-wire.md)).
64
- - The biz repos import `createServiceReadinessTests` from this package in their
65
- `tests/bootstrap/service-readiness.test.js` (`converter`, `ingest`,
66
- `hello-service`).
62
+ wrapper startup, in phase 0.2. **There is no proof cache**: the full
63
+ validation runs on every boot, and `conn-runtime/validation-proof.json` is its
64
+ output, not its shortcut. The cache that once lived here skipped most steps to
65
+ save milliseconds and bought a window in which validation asserted something
66
+ no longer true; the fingerprint it keyed on covered less than it claimed. The
67
+ proof travels into registration (see
68
+ [registration-wire.md](../../../docs/biz/30-operations/registration-wire.md)).
69
+ - Biz repos import `createServiceReadinessTests` from this package in their
70
+ `tests/bootstrap/service-readiness.test.js`. Which repos do so changes with
71
+ the service headcount, so grep for the symbol rather than trusting a list
72
+ written here.
67
73
  - Pre-validation is driven by the `oa-biz-ci-gate run-prevalidation` subcommand,
68
74
  not by a Jest suite generator.
69
75
 
70
76
  ## What We Test
71
77
 
72
- 1. **Service contract** — operations.json structure + endpoint reachability
73
- 2. **Workflow capability** — can process cookbooks
74
- 3. **Infrastructure** — registry / MQ / storage connectivity
75
- 4. **Health** — `/health` returns 200
78
+ 1. **Service contract** — structure, config files and the v3 operations rules
79
+ 2. **Environment contract** — every name declared `env.required` is set
80
+ 3. **Workflow capability** — can process cookbooks, in-process, with mocks
81
+ 4. **Connector contract** — the two declarations agree and the environment backs them
76
82
 
77
83
  ## What We DO NOT Test
78
84
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/conn-orch-validator",
3
- "version": "6.0.0",
3
+ "version": "7.0.0",
4
4
  "description": "Validation orchestrator for OA Drive microservices - coordinates validation across all layers (base, infra, orch, business)",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -441,11 +441,22 @@ class CookbookTestRunner {
441
441
  // Synthesize an HTTP-like response so existing expect validators
442
442
  // continue to work (expect.status: 'error', expect.error.code, ...).
443
443
  const errorCode = handlerError.code || handlerError.name || 'HANDLER_ERROR';
444
- const statusCode = typeof handlerError.statusCode === 'number'
445
- ? handlerError.statusCode
444
+ // `status` is the platform's ONE name for the number a thrown error
445
+ // carries: `BusinessError` sets it, `ErrorMapper` reads it
446
+ // (@onlineapps/service-wrapper, src/ErrorMapper.js), and a service that
447
+ // throws its own class declares the same shape.
448
+ // @see api/docs/biz/70-contracts/error-handling.md §1
449
+ //
450
+ // No `statusCode || status` chain. The retired second name was read here
451
+ // and nowhere else, which is why three biz services mirrored the value
452
+ // under both names to satisfy this line; a fallback would keep both alive
453
+ // and let them drift. An error carrying no usable status is a 500 — the
454
+ // handler declared no outcome, so it gets the server fault it earned.
455
+ const status = typeof handlerError.status === 'number'
456
+ ? handlerError.status
446
457
  : 500;
447
458
  result.response = {
448
- status: statusCode,
459
+ status,
449
460
  statusText: errorCode,
450
461
  data: { code: errorCode, message: handlerError.message }
451
462
  };
@@ -6,6 +6,19 @@ const CookbookTestUtils = require('./CookbookTestUtils');
6
6
  // error message lists them in.
7
7
  const LOGGER_METHODS = ['info', 'warn', 'error', 'debug'];
8
8
 
9
+ /**
10
+ * The semver 2.0.0 grammar, verbatim from semver.org's own published regular
11
+ * expression (anchored, no leading `v`, no surrounding whitespace, no leading
12
+ * zeroes in a numeric identifier).
13
+ *
14
+ * Written out rather than pulled from the `semver` package: this is the only
15
+ * version rule in the package, one anchored pattern expresses it completely,
16
+ * and a dependency added for a single `test()` call is a supply chain the gate
17
+ * does not need. If a second version rule ever appears here, that trade changes
18
+ * — and the change is then visible, because it is this comment that has to go.
19
+ */
20
+ const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
21
+
9
22
  /**
10
23
  * ServiceReadinessValidator - Orchestrates complete service validation.
11
24
  *
@@ -311,16 +324,63 @@ class ServiceReadinessValidator {
311
324
  }
312
325
 
313
326
  /**
314
- * Check registry compatibility
327
+ * Check registry compatibility.
328
+ *
329
+ * `canRegister` used to be `!!(name && version && operations)`. Truthiness is
330
+ * the wrong question about a version: the line below it compares this value
331
+ * against what the registry already holds, and a comparison between two
332
+ * strings that are not versions decides nothing. Measured (BIZ-ingest): every
333
+ * biz `config/service/config.json` declares
334
+ * `"service": { "version": "${npm_package_version}" }`, the placeholder was
335
+ * handed here unexpanded, and — being a non-empty string — it passed. The
336
+ * gate reported `canRegister: true` about a value that is not a version.
337
+ *
338
+ * The grammar is semver 2.0.0, checked here and nowhere else: this is the one
339
+ * place that decides `canRegister`. Callers supply the value (the `version`
340
+ * field of the service package.json, which is what ConfigLoader reads at
341
+ * runtime); they do not re-check it.
315
342
  */
316
343
  async checkRegistryCompatibility(service, registry) {
317
344
  try {
318
- // Check if service can be registered
319
- const canRegister = !!(
320
- service.name &&
321
- service.version &&
322
- service.operations
323
- );
345
+ const errors = [];
346
+
347
+ if (!service.name) {
348
+ errors.push(
349
+ '[ServiceReadinessValidator] Service name is missing - '
350
+ + 'validateReadiness() received no `name`. '
351
+ + 'Expected: the service name, e.g. biz-ingest. '
352
+ + 'Fix: pass it in the object handed to validateReadiness().'
353
+ );
354
+ }
355
+
356
+ if (service.version === undefined || service.version === null) {
357
+ errors.push(
358
+ '[ServiceReadinessValidator] Service version is missing - '
359
+ + 'validateReadiness() received no `version`. '
360
+ + 'Expected: the `version` field of the service package.json, e.g. 1.4.2. '
361
+ + 'Fix: pass it in the object handed to validateReadiness().'
362
+ );
363
+ } else if (!SEMVER_PATTERN.test(service.version)) {
364
+ errors.push(
365
+ '[ServiceReadinessValidator] Service version is not a semantic version - '
366
+ + `received "${service.version}". `
367
+ + 'Expected: MAJOR.MINOR.PATCH per semver 2.0.0, e.g. 1.4.2 or 1.4.2-rc.1. '
368
+ + 'Fix: pass the `version` field of the service package.json — the single source of '
369
+ + 'truth ConfigLoader reads at runtime. An unresolved "${...}" means the value came '
370
+ + 'from config.json instead, where the key is never expanded.'
371
+ );
372
+ }
373
+
374
+ if (!service.operations) {
375
+ errors.push(
376
+ '[ServiceReadinessValidator] Operations are missing - '
377
+ + 'validateReadiness() received no `operations`. '
378
+ + 'Expected: the flat operations object from config/service/operations.json. '
379
+ + 'Fix: pass it in the object handed to validateReadiness().'
380
+ );
381
+ }
382
+
383
+ const canRegister = errors.length === 0;
324
384
 
325
385
  // Check if service operations match registry expectations
326
386
  const registeredService = registry.getService(service.name);
@@ -331,7 +391,8 @@ class ServiceReadinessValidator {
331
391
  passed: canRegister && compatible,
332
392
  canRegister,
333
393
  compatible,
334
- existingVersion: registeredService?.version
394
+ existingVersion: registeredService?.version,
395
+ errors
335
396
  };
336
397
  } catch (error) {
337
398
  return {
@@ -4,7 +4,13 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { verifyConnectorContract } = require('./utils/connectorContract');
6
6
  const { normalizeEnvDeclaration, collectEnvCoverage, verifyEnvPresence } = require('./utils/envContract');
7
- const { ValidationProofCodec, ValidationProofVerifier } = require('@onlineapps/service-validator-core');
7
+ // `ValidationProofVerifier` was destructured here alongside the codec and never
8
+ // called. It verified a cached proof before booting; the proof cache was
9
+ // removed (see validate()) and the verifier's last call site went with it,
10
+ // leaving a binding whose only remaining effect was to tell a reader that this
11
+ // class still checks proofs. `ValidationProofCodec` stays — finalizeResults()
12
+ // encodes the proof it writes.
13
+ const { ValidationProofCodec } = require('@onlineapps/service-validator-core');
8
14
  const FingerprintUtils = require('@onlineapps/service-validator-core').FingerprintUtils;
9
15
  const { ServiceStructureValidator } = require('./validators/ServiceStructureValidator');
10
16
  const { describeStepFailureWithContext } = require('./utils/stepFailure');
@@ -115,15 +121,72 @@ class ValidationOrchestrator {
115
121
 
116
122
 
117
123
 
124
+ /**
125
+ * Read and parse the service's package.json.
126
+ *
127
+ * It is the single source of truth for the service version — the same one
128
+ * validateConfig() names below, and the same one the biz pre-validation entry
129
+ * point (`src/utils/preValidation.js`) already reads. `config/service/config.json`
130
+ * cannot supply it: the wrapper's ConfigLoader
131
+ * (`api/shared/connector/service-wrapper/src/ConfigLoader.js`) assigns
132
+ * `config.service.version` from package.json unconditionally on every load, so
133
+ * the key in the file is dead — and where it is declared at all, biz services
134
+ * declare it as the unresolved literal `${npm_package_version}`.
135
+ *
136
+ * A missing, unreadable, unparseable or version-less package.json is a contract
137
+ * violation, not a computation hiccup, so it is raised here in full §5 shape
138
+ * rather than through calculateFingerprint()'s generic wrapper.
139
+ *
140
+ * @param {string} packageFile absolute path to the service's package.json
141
+ * @returns {object} the parsed package.json, `version` guaranteed non-empty
142
+ */
143
+ readServicePackageJson(packageFile) {
144
+ let raw;
145
+ try {
146
+ raw = fs.readFileSync(packageFile, 'utf8');
147
+ } catch (error) {
148
+ throw new Error(
149
+ `[ValidationOrchestrator] Service package.json is unreadable - Expected: a readable ${packageFile}, `
150
+ + 'the single source of truth for the service version. '
151
+ + `Read failed with: ${error.code || error.message}. `
152
+ + 'Fix: point options.serviceRoot at the service directory that contains package.json.'
153
+ );
154
+ }
155
+
156
+ let pkg;
157
+ try {
158
+ pkg = JSON.parse(raw);
159
+ } catch (error) {
160
+ throw new Error(
161
+ `[ValidationOrchestrator] Service package.json is not valid JSON - Expected: parseable JSON at ${packageFile}. `
162
+ + `Parse failed with: ${error.message}. `
163
+ + 'Fix: repair the file.'
164
+ );
165
+ }
166
+
167
+ if (typeof pkg.version !== 'string' || pkg.version.length === 0) {
168
+ throw new Error(
169
+ `[ValidationOrchestrator] Service package.json has no version - Expected: a non-empty "version" string in ${packageFile}, `
170
+ + 'the single source of truth for the service version. '
171
+ + `Got: ${JSON.stringify(pkg.version)}. `
172
+ + 'Fix: set "version" in the service package.json.'
173
+ );
174
+ }
175
+
176
+ return pkg;
177
+ }
178
+
118
179
  /**
119
180
  * Calculate service fingerprint
120
181
  * Based on: version + operations.json + dependencies + config
121
182
  */
122
183
  async calculateFingerprint() {
184
+ const packageFile = path.join(this.serviceRoot, 'package.json');
185
+ const pkg = this.readServicePackageJson(packageFile);
186
+
123
187
  try {
124
188
  const configFile = path.join(this.configPath, 'config.json');
125
189
  const operationsFile = path.join(this.configPath, 'operations.json');
126
- const packageFile = path.join(this.serviceRoot, 'package.json');
127
190
  const dockerFile = path.join(this.serviceRoot, 'Dockerfile');
128
191
  const dockerComposeFile = path.join(this.serviceRoot, 'docker-compose.yml');
129
192
  const envTemplateFile = path.join(this.serviceRoot, '..', '..', 'config', 'env-templates', `${path.basename(this.serviceRoot)}.env`);
@@ -134,7 +197,6 @@ class ValidationOrchestrator {
134
197
 
135
198
  const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
136
199
  const operations = JSON.parse(fs.readFileSync(operationsFile, 'utf8'));
137
- const pkg = JSON.parse(fs.readFileSync(packageFile, 'utf8'));
138
200
 
139
201
  // Include infra file contents in fingerprint to detect changes in environment/setup
140
202
  const infra = {};
@@ -187,8 +249,21 @@ class ValidationOrchestrator {
187
249
  }
188
250
 
189
251
  // Generate fingerprint using FingerprintUtils (handles deep sorting)
252
+ //
253
+ // The version comes from package.json alone. It used to read
254
+ // `config.service?.version || pkg.version`, which hashed a value that was
255
+ // not a version at all: `config.service.version` is overwritten by the
256
+ // wrapper's ConfigLoader on every load (see readServicePackageJson above),
257
+ // and the services that declare it leave the unresolved literal
258
+ // `${npm_package_version}` in that key. The `||` then hid it — for those
259
+ // services the package.json branch was never reached, so publishing a new
260
+ // version of one did not move its fingerprint by one bit
261
+ // (package.json is otherwise represented here only by its @onlineapps
262
+ // dependency pins). config.json's own content is still hashed below, so
263
+ // editing that key is still detected — as a config change, which is what
264
+ // it is.
190
265
  const fingerprintData = {
191
- serviceVersion: config.service?.version || pkg.version,
266
+ serviceVersion: pkg.version,
192
267
  operations: operations,
193
268
  dependencies: deps,
194
269
  config: config,
@@ -15,43 +15,53 @@ Instead of **copying test code** between services, we provide **reusable test he
15
15
 
16
16
  ### createServiceReadinessTests
17
17
 
18
- **Purpose:** Integration test for service HTTP API readiness
18
+ **Purpose:** a jest suite proving a service is ready — its shape on disk and its
19
+ v3 operations contract. Nothing is started and nothing is called over the
20
+ network: biz containers run zero HTTP (ADR 0005), so there is no port to bind,
21
+ no endpoint to probe and no `/health` to check.
19
22
 
20
23
  **File:** `createServiceReadinessTests.js`
21
24
 
22
25
  **What it does:**
23
- - Validates service structure (config/service/, src/handlers/, package.json)
24
- - Validates configuration files (config.json, operations.json)
25
- - Tests all operations endpoints
26
- - Validates health endpoint
27
- - Auto-generates cookbook tests (with mocks)
28
- - Tests registry compatibility (with mocks)
29
- - Scores 100/100 with all checks
26
+ - Validates service structure (config/service/, src/, tests/, src/handlers/, package.json) and THROWS before any test runs if it fails
27
+ - Loads `config/service/config.json` and `config/service/operations.json` — the only configuration path
28
+ - Scores readiness through `ServiceReadinessValidator` (operations, plus cookbook and registry when the optional checks are on)
29
+ - Synthesises a cookbook from the declared operations, with mock input generated from each `input` schema
30
+ - Uses `MockRegistry` for the registry check — nothing real is contacted
31
+ - Asserts every operation declares `handler` (matching `handlers/<path>#<export>`) and a valid `bundle_scope`, and carries no retired v2 `endpoint` / `method`
30
32
 
31
33
  **Usage:**
32
34
  ```javascript
33
- // services/my-service/tests/integration/service-readiness.test.js
35
+ // <service>/tests/bootstrap/service-readiness.test.js
34
36
  const { createServiceReadinessTests } = require('@onlineapps/conn-orch-validator');
35
37
 
36
38
  createServiceReadinessTests(__dirname);
37
39
  ```
38
40
 
41
+ The service root is resolved two levels up from the directory you pass, so the
42
+ file belongs in `tests/<something>/`.
43
+
39
44
  **Options:**
40
45
  ```javascript
41
46
  createServiceReadinessTests(__dirname, {
42
- testPort: 5556, // Test server port (default: 5556)
43
47
  includeOptionalChecks: true, // Cookbook & registry checks (default: true)
44
- timeout: 15000 // Test timeout in ms (default: 15000)
48
+ timeout: 15000, // Test timeout in ms (default: 15000)
49
+ logger: myLogger // Logger for ServiceReadinessValidator (default: console)
45
50
  });
46
51
  ```
47
52
 
48
- **Score Breakdown:**
49
- - operations: 30 points (required)
50
- - endpoints: 30 points (required)
51
- - health: 20 points (required)
52
- - cookbook: 15 points (optional, with mocks)
53
- - registry: 5 points (optional, with mocks)
54
- - **Total: 100/100**
53
+ `logger` defaults to `console` on purpose — in a bootstrap suite stdout is the
54
+ report the developer reads. See
55
+ [FALLBACKS_INVENTORY.md](/docs/standards/FALLBACKS_INVENTORY.md) §5.6.
56
+
57
+ **Score Breakdown** (owned by `ServiceReadinessValidator`):
58
+ - operations: 80 points (required)
59
+ - cookbook: 15 points (optional, structural validation of the synthesised cookbook)
60
+ - registry: 5 points (optional, MockRegistry)
61
+ - **Total: 100/100** — ready requires every required check to pass and a score of at least 60
62
+
63
+ With `includeOptionalChecks` on, the suite asserts the full 100; with it off, it
64
+ asserts at least 80 and a passing `operations` check.
55
65
 
56
66
  ---
57
67
 
@@ -70,12 +80,12 @@ The helper uses `ServiceStructureValidator` to validate service structure BEFORE
70
80
 
71
81
  ✅ ALL CHECKS PASSED
72
82
 
73
- ✓ Found Configuration directory: config/service
83
+ ✓ Found Configuration directory
74
84
  ✓ Found Source code directory: src
75
85
  ✓ Found Tests directory: tests
76
86
  ✓ Found valid config.json
77
87
  ✓ Found valid operations.json
78
- ✓ Found src/app.js
88
+ ✓ Found src/handlers/
79
89
  ✓ Found 3 cookbook test(s)
80
90
 
81
91
  ═══════════════════════════════════════════════════════════════
@@ -107,20 +117,30 @@ If validation fails, clear error messages are shown:
107
117
  The helper automatically detects and loads:
108
118
 
109
119
  ```javascript
110
- // Service root (2 levels up from tests/integration/)
111
- const serviceRoot = path.resolve(__dirname, '../..');
120
+ // Service root (2 levels up from the directory you passed)
121
+ const serviceRoot = path.resolve(testsDir, '../..');
112
122
 
113
- // Standard file locations
114
- const config = require(path.join(serviceRoot, 'config/service/config.json'));
115
- const operations = require(path.join(serviceRoot, 'config/service/operations.json'));
116
- const app = require(path.join(serviceRoot, 'src/app.js'));
123
+ // The only configuration path — the legacy conn-config/ layout is gone
124
+ const config = JSON.parse(fs.readFileSync(path.join(serviceRoot, 'config/service/config.json'), 'utf-8'));
125
+ const operations = JSON.parse(fs.readFileSync(path.join(serviceRoot, 'config/service/operations.json'), 'utf-8'));
117
126
 
118
127
  // Extract metadata
119
128
  const serviceName = config.service.name;
120
- const serviceVersion = config.service.version;
121
- const healthEndpoint = config.wrapper?.health?.endpoint || '/health';
129
+
130
+ // The version comes from package.json, NEVER from config.json. ConfigLoader
131
+ // (shared/connector/service-wrapper/src/ConfigLoader.js) assigns
132
+ // `config.service.version` from package.json unconditionally on every load, and
133
+ // does it BEFORE `${VAR}` placeholders are resolved — so `service.version` in
134
+ // config.json never reaches a running service under any value, and reading it
135
+ // here yielded the literal string `${npm_package_version}`.
136
+ const serviceVersion = JSON.parse(
137
+ fs.readFileSync(path.join(serviceRoot, 'package.json'), 'utf-8')
138
+ ).version;
122
139
  ```
123
140
 
141
+ Nothing loads `src/app.js` and nothing reads a health endpoint: both belonged to
142
+ the Express surface ADR 0005 removed.
143
+
124
144
  No service-specific code needed!
125
145
 
126
146
  ---
@@ -133,7 +153,7 @@ When creating new generic test helpers:
133
153
  2. **Accept testsDir as first parameter:** `function create...(testsDir, options)`
134
154
  3. **Calculate service root:** `const serviceRoot = path.resolve(testsDir, '../..');`
135
155
  4. **Validate structure first:** Use `ServiceStructureValidator`
136
- 5. **Load standard files:** config.json, operations.json, app.js
156
+ 5. **Load standard files:** `config/service/config.json`, `config/service/operations.json`
137
157
  6. **Provide clear output:** Log validation results, test progress
138
158
  7. **Export single function:** `module.exports = { create...Tests };`
139
159
  8. **Update index.js:** Add to exports
@@ -176,8 +196,3 @@ module.exports = { createMyTests };
176
196
  - [/tests/TESTING.md](/tests/TESTING.md) - SPOT principles
177
197
  - [/shared/connector/conn-orch-validator/README.md](/shared/connector/conn-orch-validator/README.md) - Package documentation
178
198
  - [/shared/connector/conn-orch-validator/docs/DESIGN.md](/shared/connector/conn-orch-validator/docs/DESIGN.md) - Design principles
179
-
180
- ---
181
-
182
- *Last updated: 2025-10-21*
183
- *Maintained by: OA Drive Core Team*
@@ -84,9 +84,22 @@ function createServiceReadinessTests(testsDir, options = {}) {
84
84
  const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
85
85
  const operations = JSON.parse(fs.readFileSync(operationsPath, 'utf-8'));
86
86
 
87
- // Extract metadata from config
88
87
  const serviceName = config.service.name;
89
- const serviceVersion = config.service.version;
88
+
89
+ // The version comes from package.json, never from config.json — the same
90
+ // single source of truth the runtime uses:
91
+ //
92
+ // shared/connector/service-wrapper/src/ConfigLoader.js:239
93
+ // config.service.version = this.loadPackageVersion(basePath);
94
+ //
95
+ // That assignment is unconditional and happens BEFORE `${VAR}` placeholders
96
+ // are resolved, so `service.version` in config.json never reaches a running
97
+ // service under any value. Reading it here was reading a dead key, and the
98
+ // value it yielded was the literal string `${npm_package_version}`: the
99
+ // placeholder that no expansion pass ever touches, handed to
100
+ // ServiceReadinessValidator, which used to accept it for being truthy.
101
+ const packagePath = path.join(serviceRoot, 'package.json');
102
+ const serviceVersion = JSON.parse(fs.readFileSync(packagePath, 'utf-8')).version;
90
103
 
91
104
  describe(`${serviceName} Service Readiness @integration`, () => {
92
105
  test('service passes readiness validation', async () => {
@@ -331,6 +331,77 @@ function isCommentLine(line) {
331
331
  return trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*');
332
332
  }
333
333
 
334
+ /**
335
+ * The other half of the same distinction. A comment is prose because of WHERE
336
+ * it sits; an error message is prose because of WHAT IT SAYS.
337
+ *
338
+ * "Inside quotes" alone is NOT the test, and must never become it: a query is
339
+ * a string too, and a query is exactly what reaches a database —
340
+ * `SELECT 1 FROM t WHERE tenant_id = 100` has to keep failing. What separates
341
+ * the two is the verb. Prose makes a CLAIM about the identifier ("workspace_id
342
+ * = 0 IS NOT a value"); code assigns a VALUE to it, and nothing follows the
343
+ * number but syntax.
344
+ *
345
+ * So the permit is granted only when the number is followed, inside the same
346
+ * string literal, by a finite verb from the closed list below. Everything else
347
+ * — end of string, `,`, `)`, ` AND `, ` order by ` — stays reported. The list
348
+ * is deliberately short and enumerated rather than clever: a gate whose verdict
349
+ * cannot be predicted from reading it is not a gate (`automation-gates.md` §1).
350
+ *
351
+ * Both conditions fail CLOSED. A string that spans several lines looks to this
352
+ * line-local scanner like no string at all, so its literal keeps its report.
353
+ */
354
+ const PROSE_PREDICATE =
355
+ /^[\s,;:—-]*\b(is|are|was|were|has|have|had|means|meant|carries|carry|cannot|must|never|does|do|would|will|remains|stays)\b/i;
356
+
357
+ /**
358
+ * The end offset of the CLOSED string literal containing `index`, or -1 when
359
+ * that position is code. Line-local and escape-aware; a quote opened on an
360
+ * earlier line is invisible here, which is the safe direction (no permit).
361
+ */
362
+ function enclosingStringEnd(line, index) {
363
+ let quote = null;
364
+ let start = -1;
365
+ for (let i = 0; i < line.length; i += 1) {
366
+ const ch = line[i];
367
+ if (quote !== null && ch === '\\') { i += 1; continue; }
368
+ if (quote === null) {
369
+ if (ch === "'" || ch === '"' || ch === '`') { quote = ch; start = i; }
370
+ } else if (ch === quote) {
371
+ if (index > start && index < i) return i;
372
+ quote = null;
373
+ start = -1;
374
+ }
375
+ }
376
+ return -1;
377
+ }
378
+
379
+ /**
380
+ * True when the literal at `index` is quoted inside an English sentence that
381
+ * talks ABOUT the identifier, rather than handing it a value.
382
+ */
383
+ function isProseLiteral(line, index, matchText) {
384
+ const end = enclosingStringEnd(line, index);
385
+ if (end === -1) return false;
386
+ return PROSE_PREDICATE.test(line.slice(index + matchText.length, end));
387
+ }
388
+
389
+ /**
390
+ * A tenant / workspace identity frozen into this line of executable code.
391
+ *
392
+ * One rail for both scan sites (src+scripts, and the shared test-namespace
393
+ * module), so the two permits — comment, prose — cannot drift apart.
394
+ */
395
+ function freezesNamespaceIdentity(line) {
396
+ if (isCommentLine(line)) return false;
397
+ for (const pattern of [LITERAL_NAMESPACE, LITERAL_NAMESPACE_CONST]) {
398
+ const match = pattern.exec(line);
399
+ if (!match) continue;
400
+ if (!isProseLiteral(line, match.index, match[0])) return true;
401
+ }
402
+ return false;
403
+ }
404
+
334
405
  function walkFiles(dir, base, out) {
335
406
  let entries;
336
407
  try {
@@ -384,8 +455,7 @@ function checkNamespaceLiterals(serviceRoot, add) {
384
455
  if (text === null) continue;
385
456
  const lines = text.split('\n');
386
457
  for (let i = 0; i < lines.length; i += 1) {
387
- if (isCommentLine(lines[i])) continue;
388
- if (!LITERAL_NAMESPACE.test(lines[i]) && !LITERAL_NAMESPACE_CONST.test(lines[i])) continue;
458
+ if (!freezesNamespaceIdentity(lines[i])) continue;
389
459
  add('R8', `${file.relative}:${i + 1} freezes a tenant / workspace id into code:\n`
390
460
  + ` ${lines[i].trim()}\n`
391
461
  + ' Fix: take it from ctx (handlers), from a CLI argument (scripts),\n'
@@ -409,8 +479,7 @@ function checkIntegrationNamespaceSource(serviceRoot, add) {
409
479
  if (isNamespaceModule(file.relative)) {
410
480
  const lines = text.split('\n');
411
481
  for (let i = 0; i < lines.length; i += 1) {
412
- if (isCommentLine(lines[i])) continue;
413
- if (!LITERAL_NAMESPACE.test(lines[i]) && !LITERAL_NAMESPACE_CONST.test(lines[i])) continue;
482
+ if (!freezesNamespaceIdentity(lines[i])) continue;
414
483
  add('R8', `${file.relative}:${i + 1} freezes the test namespace into a literal:\n`
415
484
  + ` ${lines[i].trim()}\n`
416
485
  + ' This module exists so the namespace has one source; that source must\n'
@@ -4,7 +4,10 @@
4
4
  * Validates that a business service has correct directory structure,
5
5
  * configuration files, and follows OA Drive standards.
6
6
  *
7
- * Each standard level is cumulative — v1.2 requires all checks from v1.0 + v1.1 + v1.2.
7
+ * Each standard level is cumulative — a level requires all checks from every level
8
+ * declared before it in STANDARD_LEVELS, in that order. The list is the authority,
9
+ * not the version number: v1.1 was retired on 2026-09-04 and its number is not
10
+ * reused, so today "v1.2" means v1.0 + v1.2.
8
11
  * Older validator versions naturally support fewer levels (they don't know about newer ones).
9
12
  *
10
13
  * @module validators/ServiceStructureValidator
@@ -268,24 +271,21 @@ const STANDARD_LEVELS = [
268
271
  return results;
269
272
  }
270
273
  },
271
- {
272
- level: 'v1.1',
273
- name: 'Multitenancy Standard',
274
- since: '2026-03-01',
275
- checks: (root) => {
276
- const configRaw = readConfigFile(root, 'config.json');
277
- let hasTenantContext = false;
278
- if (configRaw) {
279
- try {
280
- const cfg = JSON.parse(configRaw);
281
- hasTenantContext = cfg.wrapper?.tenantContext !== undefined;
282
- } catch { /* invalid JSON */ }
283
- }
284
- return [
285
- { passed: hasTenantContext, id: 'tenant_context', message: 'wrapper.tenantContext configured in config.json' }
286
- ];
287
- }
288
- },
274
+ // v1.1 "Multitenancy Standard" was retired on 2026-09-04 (owner decision,
275
+ // api/docs/governance/confirmations/service-shape-v11-retirement.md). Its single
276
+ // check `tenant_context` asserted only that `wrapper.tenantContext` EXISTED in
277
+ // config.json. Nothing reads that key: the wrapper runtime reads cache,
278
+ // infrastructureGate, health, validation, mq, registry, monitoring, state,
279
+ // secrets and heartbeat — not tenantContext; `createTenantContextMiddleware`
280
+ // was deleted on 2026-09-05 together with the runtime-defaults key
281
+ // (docs/governance/confirmations/wrapper-tenant-middleware.md). A
282
+ // level enforcing a dead declaration is the false guarantee automation-gates.md
283
+ // §5 names, so the level is GONE rather than emptied — an empty level would
284
+ // pass for every service while asserting nothing.
285
+ //
286
+ // The number is deliberately not reused and the gap is not closed up: v1.2 and
287
+ // v1.3 keep their names so that a level recorded in an older proof still means
288
+ // what it meant. Cumulative now reads "v1.2 requires v1.0".
289
289
  {
290
290
  level: 'v1.2',
291
291
  name: 'Business Error Handling Standard',