@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 +33 -0
- package/README.md +100 -32
- package/docs/DESIGN.md +24 -18
- package/package.json +1 -1
- package/src/CookbookTestRunner.js +14 -3
- package/src/ServiceReadinessValidator.js +69 -8
- package/src/ValidationOrchestrator.js +79 -4
- package/src/helpers/README.md +49 -34
- package/src/helpers/createServiceReadinessTests.js +15 -2
- package/src/utils/deployContract.js +73 -4
- package/src/validators/ServiceStructureValidator.js +19 -19
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
|
|
13
|
-
3. **Validates
|
|
14
|
-
4. **Validates
|
|
15
|
-
5. **Validates
|
|
16
|
-
6. **
|
|
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
|
-
//
|
|
26
|
-
|
|
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** -
|
|
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": "
|
|
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
|
-
- **
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
144
|
-
| **v1.
|
|
145
|
-
| **v1.
|
|
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** —
|
|
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
|
-
- **
|
|
152
|
-
|
|
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),
|
|
9
|
-
|
|
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
|
|
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` —
|
|
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.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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** —
|
|
73
|
-
2. **
|
|
74
|
-
3. **
|
|
75
|
-
4. **
|
|
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": "
|
|
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
|
-
|
|
445
|
-
|
|
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
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
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:
|
|
266
|
+
serviceVersion: pkg.version,
|
|
192
267
|
operations: operations,
|
|
193
268
|
dependencies: deps,
|
|
194
269
|
config: config,
|
package/src/helpers/README.md
CHANGED
|
@@ -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:**
|
|
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
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
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
|
-
//
|
|
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
|
|
48
|
+
timeout: 15000, // Test timeout in ms (default: 15000)
|
|
49
|
+
logger: myLogger // Logger for ServiceReadinessValidator (default: console)
|
|
45
50
|
});
|
|
46
51
|
```
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
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
|
|
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/
|
|
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
|
|
111
|
-
const serviceRoot = path.resolve(
|
|
120
|
+
// Service root (2 levels up from the directory you passed)
|
|
121
|
+
const serviceRoot = path.resolve(testsDir, '../..');
|
|
112
122
|
|
|
113
|
-
//
|
|
114
|
-
const config =
|
|
115
|
-
const operations =
|
|
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
|
-
|
|
121
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 —
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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',
|