@onlineapps/conn-orch-validator 6.0.1 → 8.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 +2591 -2
- package/README.md +1075 -7
- package/docs/DESIGN.md +3 -1
- package/manifests/biz-service.manifest.json +658 -0
- package/manifests/library.manifest.json +324 -0
- package/package.json +12 -6
- package/src/CookbookTestRunner.js +422 -104
- package/src/CookbookTestUtils.js +7 -8
- package/src/ServiceReadinessValidator.js +78 -42
- package/src/ValidationOrchestrator.js +298 -75
- package/src/cli/biz-ci-gate.js +176 -33
- package/src/cli/oa-lint-scripts.js +221 -0
- package/src/cli/oa-sync-template.js +1020 -0
- package/src/cli/oa-validate.js +474 -0
- package/src/helpers/README.md +12 -2
- package/src/helpers/createServiceReadinessTests.js +75 -6
- package/src/index.js +33 -3
- package/src/lint/scripts/lintScripts.js +298 -0
- package/src/manifest/checks/composeRunnerBlock.js +222 -0
- package/src/manifest/checks/composeShape.js +165 -0
- package/src/manifest/checks/contractBridge.js +181 -0
- package/src/manifest/checks/discoveryOrphan.js +50 -0
- package/src/manifest/checks/docsLintBridge.js +553 -0
- package/src/manifest/checks/fileAbsent.js +35 -0
- package/src/manifest/checks/gitTracked.js +204 -0
- package/src/manifest/checks/index.js +111 -0
- package/src/manifest/checks/libraryContext.js +226 -0
- package/src/manifest/checks/libraryDocs.js +75 -0
- package/src/manifest/checks/libraryPackage.js +272 -0
- package/src/manifest/checks/librarySource.js +274 -0
- package/src/manifest/checks/libraryTests.js +121 -0
- package/src/manifest/checks/libraryWorkspace.js +293 -0
- package/src/manifest/checks/readmeRegion.js +135 -0
- package/src/manifest/checks/scriptHeaders.js +79 -0
- package/src/manifest/checks/serviceConfig.js +390 -0
- package/src/manifest/checks/serviceConnectors.js +81 -0
- package/src/manifest/checks/serviceDb.js +388 -0
- package/src/manifest/checks/serviceFiles.js +754 -0
- package/src/manifest/checks/serviceIdentityRows.js +351 -0
- package/src/manifest/checks/serviceRuntime.js +295 -0
- package/src/manifest/checks/serviceScripts.js +213 -0
- package/src/manifest/deployabilitySignal.js +121 -0
- package/src/manifest/discovery.js +386 -0
- package/src/manifest/loadManifest.js +62 -0
- package/src/manifest/manifestShape.js +446 -0
- package/src/manifest/report.js +245 -0
- package/src/manifest/runManifest.js +449 -0
- package/src/manifest/serviceIdentity.js +140 -0
- package/src/manifest/walk.js +74 -0
- package/src/manifest/workspaceRoot.js +242 -0
- package/src/mocks/MockMQClient.js +13 -30
- package/src/mocks/MockRegistry.js +4 -2
- package/src/mocks/MockStorage.js +4 -2
- package/src/sync/docsRegion.js +463 -0
- package/src/sync/generatedRegion.js +228 -0
- package/src/sync/readmeLocation.js +182 -0
- package/src/sync/readmePointer.js +477 -0
- package/src/sync/serviceTemplate.js +583 -0
- package/src/sync/sharedEnv.js +162 -0
- package/src/sync/uniformFiles.js +474 -0
- package/src/utils/bizCiGateContract.js +131 -7
- package/src/utils/connectorContract.js +97 -7
- package/src/utils/cookbookFormat.js +81 -40
- package/src/utils/deployContract.js +213 -13
- package/src/utils/envContract.js +57 -1
- package/src/utils/handlerRef.js +181 -0
- package/src/utils/installContract.js +287 -41
- package/src/utils/libCompat.js +29 -7
- package/src/utils/migrationOrder.js +163 -0
- package/src/utils/preValidation.js +20 -7
- package/src/utils/setupDatabase.js +194 -13
- package/src/utils/testCoverageContract.js +539 -0
- package/src/utils/testNamespace.js +247 -23
- package/src/utils/throwawaySchema.js +207 -0
- package/src/validators/ServiceStructureValidator.js +21 -20
- package/templates/business-service/.dockerignore +42 -0
- package/templates/business-service/.gitlab-ci.yml +290 -0
- package/templates/business-service/Dockerfile +27 -0
- package/templates/business-service/README.md +213 -0
- package/templates/business-service/config/biz-docs-lint.tree.json +10 -0
- package/templates/business-service/config/env-templates/__SERVICE_NAME__.env +4 -0
- package/templates/business-service/config/env-templates/shared.env +65 -0
- package/templates/business-service/config/service/config.json +14 -0
- package/templates/business-service/config/service/integration-contract.json +12 -0
- package/templates/business-service/config/service/operations.json +41 -0
- package/templates/business-service/docker-compose.production.yml +60 -0
- package/templates/business-service/docker-compose.yml +93 -0
- package/templates/business-service/docs/80-setup/INSTALL.md +101 -0
- package/templates/business-service/docs/80-setup/PLATFORM_MATRIX.md +65 -0
- package/templates/business-service/docs/80-setup/README.md +18 -0
- package/templates/business-service/docs/80-setup/VALIDATION.md +78 -0
- package/templates/business-service/docs/README.md +18 -0
- package/templates/business-service/gitignore +42 -0
- package/templates/business-service/index.js +10 -0
- package/templates/business-service/init.sh +54 -0
- package/templates/business-service/jest.config.js +6 -0
- package/templates/business-service/package.json.template +31 -0
- package/templates/business-service/scripts/verify-deploy-uniform.sh +180 -0
- package/templates/business-service/src/handlers/v3/echo.js +39 -0
- package/templates/business-service/tests/cookbooks/echo.json +36 -0
- package/templates/business-service/tests/unit/handler.test.js +78 -0
- package/src/WorkflowTestRunner.js +0 -402
|
@@ -4,21 +4,45 @@ 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');
|
|
11
17
|
const CookbookTestRunner = require('./CookbookTestRunner');
|
|
18
|
+
const { assertLogger } = require('@onlineapps/logger-contract');
|
|
19
|
+
const { HANDLER_REF_PATTERN } = require('./utils/handlerRef');
|
|
20
|
+
const { loadManifest, DEFAULT_MANIFEST_PATH } = require('./manifest/loadManifest');
|
|
21
|
+
const { runManifest } = require('./manifest/runManifest');
|
|
22
|
+
const { resolveWorkspaceRoot } = require('./manifest/workspaceRoot');
|
|
23
|
+
const { renderBanner, describeFinding } = require('./manifest/report');
|
|
24
|
+
const { buildDeployabilitySignal, writeDeployabilitySignal } = require('./manifest/deployabilitySignal');
|
|
12
25
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
26
|
+
/**
|
|
27
|
+
* The manifest rows step 2 answers: the configuration documents a biz service is
|
|
28
|
+
* configured by. Named here once, and the ONLY place the split between step 2
|
|
29
|
+
* and step 7 is stated.
|
|
30
|
+
*
|
|
31
|
+
* Step 2 used to check `config.json`/`service.name` and `operations.json` with
|
|
32
|
+
* hand-written sentences of its own, while these rows demand the same documents
|
|
33
|
+
* with more coverage, an owner, a fix and a doc pointer — and reported the same
|
|
34
|
+
* defect a second time in step 7. One concern, one rail
|
|
35
|
+
* (`.claude/rules/change-discipline.md`); the step keeps its PLACE in the boot
|
|
36
|
+
* order, because steps 3-6 must not run behind a broken configuration and step 7
|
|
37
|
+
* runs after them.
|
|
38
|
+
*/
|
|
39
|
+
const CONFIG_STEP_ROWS = Object.freeze(['C-SERVICE', 'C-OPS', 'C-CONTRACT']);
|
|
16
40
|
|
|
17
41
|
/**
|
|
18
42
|
* ValidationOrchestrator
|
|
19
43
|
*
|
|
20
44
|
* Orchestrates complete service validation (Tier 1 Pre-Validation)
|
|
21
|
-
* Runs a
|
|
45
|
+
* Runs a 7-step validation process and generates validation proof.
|
|
22
46
|
*
|
|
23
47
|
* Called automatically by ServiceWrapper during initialization.
|
|
24
48
|
*
|
|
@@ -28,12 +52,8 @@ const LOGGER_METHODS = ['info', 'warn', 'error', 'debug'];
|
|
|
28
52
|
* different place: a service now fails on a variable it declared it needs
|
|
29
53
|
* before any handler runs, instead of several layers later inside the wrapper.
|
|
30
54
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* It is not stored and not required. It fed the readiness step's
|
|
34
|
-
* `fetch(url + '/health')`, retired in 3.3.1 under ADR 0005; after that it was
|
|
35
|
-
* a *required* constructor option no line of code read, so a service whose
|
|
36
|
-
* SERVICE_URL was unset failed to boot on account of a value nobody wanted.
|
|
55
|
+
* Constructor options: `serviceRoot`, `serviceName`, `serviceVersion`, `logger`.
|
|
56
|
+
* Those four are what it reads, and it reads no others.
|
|
37
57
|
*/
|
|
38
58
|
class ValidationOrchestrator {
|
|
39
59
|
constructor(options = {}) {
|
|
@@ -46,26 +66,11 @@ class ValidationOrchestrator {
|
|
|
46
66
|
// An incomplete logger therefore reached the runner one line later and threw
|
|
47
67
|
// in its name instead of this one. Owner confirmation:
|
|
48
68
|
// docs/governance/confirmations/connector-logger-contract.md 001.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
+ 'Fix: pass options.logger (e.g. the logger your service already built).'
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const missingLoggerMethods = LOGGER_METHODS.filter(
|
|
58
|
-
(method) => typeof options.logger[method] !== 'function'
|
|
69
|
+
this.logger = assertLogger(
|
|
70
|
+
'ValidationOrchestrator',
|
|
71
|
+
options.logger,
|
|
72
|
+
'validation narrates where the service writes'
|
|
59
73
|
);
|
|
60
|
-
if (missingLoggerMethods.length > 0) {
|
|
61
|
-
throw new Error(
|
|
62
|
-
'[ValidationOrchestrator] Logger is incomplete - Expected: info, warn, error, debug as functions; '
|
|
63
|
-
+ `missing: ${missingLoggerMethods.join(', ')}. `
|
|
64
|
-
+ 'Fix: pass a logger implementing all four.'
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
this.logger = options.logger;
|
|
69
74
|
|
|
70
75
|
// config/service/ is the ONLY configuration path (owner decision
|
|
71
76
|
// 2026-09-03). The legacy conn-config/ branch is gone: zero such
|
|
@@ -77,6 +82,12 @@ class ValidationOrchestrator {
|
|
|
77
82
|
this.runtimePath = path.join(this.serviceRoot, 'conn-runtime');
|
|
78
83
|
this.proofPath = path.join(this.runtimePath, 'validation-proof.json');
|
|
79
84
|
|
|
85
|
+
// The manifest run of THIS validation, computed on first use (manifestRun()).
|
|
86
|
+
// One orchestrator is one run: `ServiceWrapper._createValidationOrchestrator()`
|
|
87
|
+
// builds a fresh one for every attempt, including the revalidation path, so a
|
|
88
|
+
// value cached here is never handed to a second measurement of the tree.
|
|
89
|
+
this._manifestRun = null;
|
|
90
|
+
|
|
80
91
|
// Validators
|
|
81
92
|
this.structureValidator = new ServiceStructureValidator(this.serviceRoot);
|
|
82
93
|
this.cookbookRunner = new CookbookTestRunner({
|
|
@@ -115,26 +126,83 @@ class ValidationOrchestrator {
|
|
|
115
126
|
|
|
116
127
|
|
|
117
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Read and parse the service's package.json.
|
|
131
|
+
*
|
|
132
|
+
* It is the single source of truth for the service version — the same one
|
|
133
|
+
* validateConfig() names below, and the same one the biz pre-validation entry
|
|
134
|
+
* point (`src/utils/preValidation.js`) already reads. `config/service/config.json`
|
|
135
|
+
* cannot supply it: the wrapper's ConfigLoader
|
|
136
|
+
* (`api/shared/connector/service-wrapper/src/ConfigLoader.js`) assigns
|
|
137
|
+
* `config.service.version` from package.json unconditionally on every load, so
|
|
138
|
+
* the key in the file is dead — and where it is declared at all, biz services
|
|
139
|
+
* declare it as the unresolved literal `${npm_package_version}`.
|
|
140
|
+
*
|
|
141
|
+
* A missing, unreadable, unparseable or version-less package.json is a contract
|
|
142
|
+
* violation, not a computation hiccup, so it is raised here in full §5 shape
|
|
143
|
+
* rather than through calculateFingerprint()'s generic wrapper.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} packageFile absolute path to the service's package.json
|
|
146
|
+
* @returns {object} the parsed package.json, `version` guaranteed non-empty
|
|
147
|
+
*/
|
|
148
|
+
readServicePackageJson(packageFile) {
|
|
149
|
+
let raw;
|
|
150
|
+
try {
|
|
151
|
+
raw = fs.readFileSync(packageFile, 'utf8');
|
|
152
|
+
} catch (error) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`[ValidationOrchestrator] Service package.json is unreadable - Expected: a readable ${packageFile}, `
|
|
155
|
+
+ 'the single source of truth for the service version. '
|
|
156
|
+
+ `Read failed with: ${error.code || error.message}. `
|
|
157
|
+
+ 'Fix: point options.serviceRoot at the service directory that contains package.json.'
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let pkg;
|
|
162
|
+
try {
|
|
163
|
+
pkg = JSON.parse(raw);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`[ValidationOrchestrator] Service package.json is not valid JSON - Expected: parseable JSON at ${packageFile}. `
|
|
167
|
+
+ `Parse failed with: ${error.message}. `
|
|
168
|
+
+ 'Fix: repair the file.'
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (typeof pkg.version !== 'string' || pkg.version.length === 0) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`[ValidationOrchestrator] Service package.json has no version - Expected: a non-empty "version" string in ${packageFile}, `
|
|
175
|
+
+ 'the single source of truth for the service version. '
|
|
176
|
+
+ `Got: ${JSON.stringify(pkg.version)}. `
|
|
177
|
+
+ 'Fix: set "version" in the service package.json.'
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return pkg;
|
|
182
|
+
}
|
|
183
|
+
|
|
118
184
|
/**
|
|
119
185
|
* Calculate service fingerprint
|
|
120
186
|
* Based on: version + operations.json + dependencies + config
|
|
121
187
|
*/
|
|
122
188
|
async calculateFingerprint() {
|
|
189
|
+
const packageFile = path.join(this.serviceRoot, 'package.json');
|
|
190
|
+
const pkg = this.readServicePackageJson(packageFile);
|
|
191
|
+
|
|
123
192
|
try {
|
|
124
193
|
const configFile = path.join(this.configPath, 'config.json');
|
|
125
194
|
const operationsFile = path.join(this.configPath, 'operations.json');
|
|
126
|
-
const packageFile = path.join(this.serviceRoot, 'package.json');
|
|
127
195
|
const dockerFile = path.join(this.serviceRoot, 'Dockerfile');
|
|
128
196
|
const dockerComposeFile = path.join(this.serviceRoot, 'docker-compose.yml');
|
|
129
197
|
const envTemplateFile = path.join(this.serviceRoot, '..', '..', 'config', 'env-templates', `${path.basename(this.serviceRoot)}.env`);
|
|
130
198
|
|
|
131
199
|
if (!fs.existsSync(configFile)) {
|
|
132
|
-
throw new Error(
|
|
200
|
+
throw new Error(`[ValidationOrchestrator] config.json not found - Expected the service `
|
|
201
|
+
+ `configuration at ${configFile}, which the fingerprint hashes. Fix: create that file.`);
|
|
133
202
|
}
|
|
134
203
|
|
|
135
204
|
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
136
205
|
const operations = JSON.parse(fs.readFileSync(operationsFile, 'utf8'));
|
|
137
|
-
const pkg = JSON.parse(fs.readFileSync(packageFile, 'utf8'));
|
|
138
206
|
|
|
139
207
|
// Include infra file contents in fingerprint to detect changes in environment/setup
|
|
140
208
|
const infra = {};
|
|
@@ -187,8 +255,21 @@ class ValidationOrchestrator {
|
|
|
187
255
|
}
|
|
188
256
|
|
|
189
257
|
// Generate fingerprint using FingerprintUtils (handles deep sorting)
|
|
258
|
+
//
|
|
259
|
+
// The version comes from package.json alone. It used to read
|
|
260
|
+
// `config.service?.version || pkg.version`, which hashed a value that was
|
|
261
|
+
// not a version at all: `config.service.version` is overwritten by the
|
|
262
|
+
// wrapper's ConfigLoader on every load (see readServicePackageJson above),
|
|
263
|
+
// and the services that declare it leave the unresolved literal
|
|
264
|
+
// `${npm_package_version}` in that key. The `||` then hid it — for those
|
|
265
|
+
// services the package.json branch was never reached, so publishing a new
|
|
266
|
+
// version of one did not move its fingerprint by one bit
|
|
267
|
+
// (package.json is otherwise represented here only by its @onlineapps
|
|
268
|
+
// dependency pins). config.json's own content is still hashed below, so
|
|
269
|
+
// editing that key is still detected — as a config change, which is what
|
|
270
|
+
// it is.
|
|
190
271
|
const fingerprintData = {
|
|
191
|
-
serviceVersion:
|
|
272
|
+
serviceVersion: pkg.version,
|
|
192
273
|
operations: operations,
|
|
193
274
|
dependencies: deps,
|
|
194
275
|
config: config,
|
|
@@ -197,14 +278,20 @@ class ValidationOrchestrator {
|
|
|
197
278
|
|
|
198
279
|
return FingerprintUtils.generate(fingerprintData);
|
|
199
280
|
} catch (error) {
|
|
200
|
-
throw new Error(
|
|
281
|
+
throw new Error('[ValidationOrchestrator] Failed to calculate the service fingerprint - Expected '
|
|
282
|
+
+ 'config/service and the infra files to be readable and valid JSON. Fix: repair whatever the '
|
|
283
|
+
+ `cause names (${error.message})`,
|
|
284
|
+
{ cause: error });
|
|
201
285
|
}
|
|
202
286
|
}
|
|
203
287
|
|
|
204
288
|
/**
|
|
205
|
-
* Run the full
|
|
289
|
+
* Run the full 7-step validation process. Step 7 joined the six below when the
|
|
290
|
+
* uniform manifest landed (confirmation `biz-service-manifest` 001 §3.1); it is
|
|
291
|
+
* the only step whose findings can leave the run successful and the service
|
|
292
|
+
* undeployable.
|
|
206
293
|
*
|
|
207
|
-
* There
|
|
294
|
+
* There was a different sixth step until 2026-08-22. Step 5, "Service Readiness",
|
|
208
295
|
* delegated to `ServiceReadinessValidator.validateReadiness({name, url,
|
|
209
296
|
* operations})` and reported a sixth-of-the-run verdict it never
|
|
210
297
|
* independently reached:
|
|
@@ -246,12 +333,19 @@ class ValidationOrchestrator {
|
|
|
246
333
|
warnings: [],
|
|
247
334
|
totalTests: 0,
|
|
248
335
|
passedTests: 0,
|
|
249
|
-
failedTests: 0
|
|
336
|
+
failedTests: 0,
|
|
337
|
+
// Step 7 fills these. `null` is not "deployable": it says the run stopped
|
|
338
|
+
// at a fail-fast step before conformance was measured, and a consumer
|
|
339
|
+
// that cannot tell those apart would read an unmeasured tree as a clean
|
|
340
|
+
// one (confirmation biz-service-manifest 001 §4).
|
|
341
|
+
deployable: null,
|
|
342
|
+
deployFindings: [],
|
|
343
|
+
notRun: []
|
|
250
344
|
};
|
|
251
345
|
|
|
252
346
|
try {
|
|
253
347
|
// Step 1: Service Structure
|
|
254
|
-
this.logger.info('[ValidationOrchestrator] Step 1/
|
|
348
|
+
this.logger.info('[ValidationOrchestrator] Step 1/7: Service Structure');
|
|
255
349
|
results.steps.structure = await this.validateStructure();
|
|
256
350
|
if (!results.steps.structure.valid) {
|
|
257
351
|
results.success = false;
|
|
@@ -261,7 +355,7 @@ class ValidationOrchestrator {
|
|
|
261
355
|
}
|
|
262
356
|
|
|
263
357
|
// Step 2: Config Files
|
|
264
|
-
this.logger.info('[ValidationOrchestrator] Step 2/
|
|
358
|
+
this.logger.info('[ValidationOrchestrator] Step 2/7: Config Files');
|
|
265
359
|
results.steps.config = await this.validateConfig();
|
|
266
360
|
if (!results.steps.config.valid) {
|
|
267
361
|
results.success = false;
|
|
@@ -275,7 +369,7 @@ class ValidationOrchestrator {
|
|
|
275
369
|
// start without must fail here, where its name and the reason it exists
|
|
276
370
|
// are both at hand. SECRETS_MASTER_KEY used to fail after MQ
|
|
277
371
|
// registration, several layers away from the declaration (defect D2).
|
|
278
|
-
this.logger.info('[ValidationOrchestrator] Step 3/
|
|
372
|
+
this.logger.info('[ValidationOrchestrator] Step 3/7: Environment Contract');
|
|
279
373
|
results.steps.env = this.validateEnvContract();
|
|
280
374
|
if (!results.steps.env.valid) {
|
|
281
375
|
results.success = false;
|
|
@@ -286,7 +380,7 @@ class ValidationOrchestrator {
|
|
|
286
380
|
}
|
|
287
381
|
|
|
288
382
|
// Step 4: Operations Compliance
|
|
289
|
-
this.logger.info('[ValidationOrchestrator] Step 4/
|
|
383
|
+
this.logger.info('[ValidationOrchestrator] Step 4/7: Operations Compliance');
|
|
290
384
|
results.steps.operations = await this.validateOperations();
|
|
291
385
|
results.warnings.push(...(results.steps.operations.warnings || []));
|
|
292
386
|
if (!results.steps.operations.valid) {
|
|
@@ -295,7 +389,7 @@ class ValidationOrchestrator {
|
|
|
295
389
|
}
|
|
296
390
|
|
|
297
391
|
// Step 5: Cookbook Tests
|
|
298
|
-
this.logger.info('[ValidationOrchestrator] Step 5/
|
|
392
|
+
this.logger.info('[ValidationOrchestrator] Step 5/7: Cookbook Tests');
|
|
299
393
|
results.steps.cookbooks = await this.runCookbookTests();
|
|
300
394
|
results.totalTests += results.steps.cookbooks.total || 0;
|
|
301
395
|
results.passedTests += results.steps.cookbooks.passed || 0;
|
|
@@ -306,7 +400,7 @@ class ValidationOrchestrator {
|
|
|
306
400
|
}
|
|
307
401
|
|
|
308
402
|
// Step 6: Connector Integration
|
|
309
|
-
this.logger.info('[ValidationOrchestrator] Step 6/
|
|
403
|
+
this.logger.info('[ValidationOrchestrator] Step 6/7: Connector Integration');
|
|
310
404
|
results.steps.connectors = this.validateConnectors();
|
|
311
405
|
if (!results.steps.connectors.valid) {
|
|
312
406
|
// Severity is unchanged: the connector contract stays non-critical and
|
|
@@ -322,6 +416,27 @@ class ValidationOrchestrator {
|
|
|
322
416
|
results.warnings.push(...results.steps.connectors.errors);
|
|
323
417
|
}
|
|
324
418
|
|
|
419
|
+
// Step 7: Manifest conformance
|
|
420
|
+
//
|
|
421
|
+
// The uniform of a biz service, measured against the manifest that ships
|
|
422
|
+
// inside this package — so the service's pin decides which shape it is
|
|
423
|
+
// measured against (confirmation biz-service-manifest 004 point 2).
|
|
424
|
+
//
|
|
425
|
+
// Two severities, two consequences (001 §4): a `boot` finding fails the
|
|
426
|
+
// validation like every other step above, while a `deploy` finding leaves
|
|
427
|
+
// the service running and marks it undeployable — the banner says so at
|
|
428
|
+
// the end of this log and `ci/deployability.json` says so to
|
|
429
|
+
// `deploy-production`.
|
|
430
|
+
this.logger.info('[ValidationOrchestrator] Step 7/7: Manifest conformance');
|
|
431
|
+
results.steps.manifest = this.validateManifestConformance();
|
|
432
|
+
results.deployable = results.steps.manifest.deployable;
|
|
433
|
+
results.deployFindings = results.steps.manifest.deployFindings;
|
|
434
|
+
results.notRun = results.steps.manifest.notRun;
|
|
435
|
+
if (!results.steps.manifest.valid) {
|
|
436
|
+
results.success = false;
|
|
437
|
+
results.errors.push(...results.steps.manifest.errors);
|
|
438
|
+
}
|
|
439
|
+
|
|
325
440
|
// Finalize and generate proof if successful
|
|
326
441
|
return await this.finalizeResults(results, startTime);
|
|
327
442
|
|
|
@@ -351,46 +466,63 @@ class ValidationOrchestrator {
|
|
|
351
466
|
}
|
|
352
467
|
|
|
353
468
|
/**
|
|
354
|
-
* Step 2:
|
|
469
|
+
* Step 2: the configuration documents, as the manifest rows CONFIG_STEP_ROWS
|
|
470
|
+
* demand them.
|
|
471
|
+
*
|
|
472
|
+
* The step decides nothing of its own: it runs the manifest (once per
|
|
473
|
+
* validation — manifestRun() below) and reports the findings of those three
|
|
474
|
+
* rows, each with the id, the owner and the fix its row carries. Step 7 then
|
|
475
|
+
* leaves them out, because they are answered, not because they stopped
|
|
476
|
+
* mattering.
|
|
477
|
+
*
|
|
478
|
+
* Which severity fails the boot is the uniform's own list, never a constant
|
|
479
|
+
* here — a service raises `boot`, a library `publish` (`api/shared/TODO.md`
|
|
480
|
+
* §0.2b-15).
|
|
481
|
+
*
|
|
482
|
+
* @returns {{valid: boolean, errors: string[], findings: object[]}}
|
|
355
483
|
*/
|
|
356
484
|
async validateConfig() {
|
|
357
|
-
const errors = [];
|
|
358
|
-
|
|
359
485
|
try {
|
|
360
|
-
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
errors.push('config.json not found');
|
|
364
|
-
} else {
|
|
365
|
-
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
366
|
-
if (!config.service?.name) errors.push('config.json missing service.name');
|
|
367
|
-
// Note: service.version is now read from package.json (Single Source of Truth)
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Validate operations.json
|
|
371
|
-
const operationsFile = path.join(this.configPath, 'operations.json');
|
|
372
|
-
if (!fs.existsSync(operationsFile)) {
|
|
373
|
-
errors.push('operations.json not found');
|
|
374
|
-
} else {
|
|
375
|
-
const operations = JSON.parse(fs.readFileSync(operationsFile, 'utf8'));
|
|
376
|
-
if (!operations.operations || Object.keys(operations.operations).length === 0) {
|
|
377
|
-
errors.push('operations.json has no operations defined');
|
|
378
|
-
}
|
|
379
|
-
}
|
|
486
|
+
const run = this.manifestRun();
|
|
487
|
+
const findings = run.findings.filter((finding) => CONFIG_STEP_ROWS.includes(finding.id));
|
|
488
|
+
const blocking = findings.filter((finding) => run.blockingSeverities.includes(finding.severity));
|
|
380
489
|
|
|
381
|
-
this.logger.info(`[ValidationOrchestrator] ✓ Config files: ${
|
|
490
|
+
this.logger.info(`[ValidationOrchestrator] ✓ Config files: ${blocking.length === 0 ? 'PASS' : 'FAIL'}`);
|
|
382
491
|
return {
|
|
383
|
-
valid:
|
|
384
|
-
errors:
|
|
492
|
+
valid: blocking.length === 0,
|
|
493
|
+
errors: blocking.map(describeFinding),
|
|
494
|
+
findings
|
|
385
495
|
};
|
|
386
496
|
} catch (error) {
|
|
497
|
+
// A broken manifest or an unreadable service root fails this step by its
|
|
498
|
+
// own name, the way step 7 does — never as "no findings".
|
|
499
|
+
this.logger.error(`[ValidationOrchestrator] Step 2/7 could not run: ${error.message}`);
|
|
387
500
|
return {
|
|
388
501
|
valid: false,
|
|
389
|
-
errors: [
|
|
502
|
+
errors: [error.message],
|
|
503
|
+
findings: []
|
|
390
504
|
};
|
|
391
505
|
}
|
|
392
506
|
}
|
|
393
507
|
|
|
508
|
+
/**
|
|
509
|
+
* The manifest run of this validation, computed once.
|
|
510
|
+
*
|
|
511
|
+
* Two steps read it — step 2 the configuration rows, step 7 every other row —
|
|
512
|
+
* and running the manifest twice would walk the repository twice and, worse,
|
|
513
|
+
* could answer the two steps differently if the tree changed between them.
|
|
514
|
+
*
|
|
515
|
+
* @returns {object} the value `runManifest` returned
|
|
516
|
+
*/
|
|
517
|
+
manifestRun() {
|
|
518
|
+
if (this._manifestRun === null) {
|
|
519
|
+
const workspaceRoot = resolveWorkspaceRoot({ startDir: this.serviceRoot });
|
|
520
|
+
const manifest = loadManifest(DEFAULT_MANIFEST_PATH);
|
|
521
|
+
this._manifestRun = runManifest({ manifest, serviceRoot: this.serviceRoot, workspaceRoot });
|
|
522
|
+
}
|
|
523
|
+
return this._manifestRun;
|
|
524
|
+
}
|
|
525
|
+
|
|
394
526
|
/**
|
|
395
527
|
* Step 3: Validate operations compliance (v3 — handler registry dispatch).
|
|
396
528
|
* Required per operation: handler ('handlers/<path>#<export>'), bundle_scope, input, output.
|
|
@@ -407,7 +539,6 @@ class ValidationOrchestrator {
|
|
|
407
539
|
const warnings = [];
|
|
408
540
|
|
|
409
541
|
const validScopes = ['platform', 'tenant', 'workspace'];
|
|
410
|
-
const handlerPattern = /^handlers\/[a-zA-Z0-9_\/-]+#[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
411
542
|
const forbiddenV2Fields = ['endpoint', 'method', 'path'];
|
|
412
543
|
|
|
413
544
|
for (const [opName, opDef] of Object.entries(operations.operations || {})) {
|
|
@@ -417,7 +548,7 @@ class ValidationOrchestrator {
|
|
|
417
548
|
|
|
418
549
|
if (!opDef.handler) {
|
|
419
550
|
errors.push(`Operation ${opName}: missing handler (v3 — 'handlers/<path>#<export>')`);
|
|
420
|
-
} else if (!
|
|
551
|
+
} else if (!HANDLER_REF_PATTERN.test(opDef.handler)) {
|
|
421
552
|
errors.push(`Operation ${opName}: invalid handler ref '${opDef.handler}' — expected 'handlers/<path>#<exportName>'`);
|
|
422
553
|
}
|
|
423
554
|
|
|
@@ -474,13 +605,21 @@ class ValidationOrchestrator {
|
|
|
474
605
|
total: 0,
|
|
475
606
|
passed: 0,
|
|
476
607
|
failed: 0,
|
|
608
|
+
cookbooks: { total: 0, passed: 0, failed: 0 },
|
|
477
609
|
warnings: ['No cookbook tests found']
|
|
478
610
|
};
|
|
479
611
|
}
|
|
480
612
|
|
|
481
613
|
const result = await this.cookbookRunner.runCookbooks(cookbooksPath);
|
|
482
614
|
|
|
483
|
-
|
|
615
|
+
// BOTH units, each named. The line used to read "Cookbook tests: X/Y"
|
|
616
|
+
// about STEP counts, so three one-step cookbooks reported "3/3" and the
|
|
617
|
+
// day one of them gained a second step the number rose with no cookbook
|
|
618
|
+
// added (BIZ-converter, 2026-08-27). A label that disagrees with its
|
|
619
|
+
// measurement is the false guarantee `automation-gates.md` §5 names.
|
|
620
|
+
this.logger.info(`[ValidationOrchestrator] ✓ Cookbooks: `
|
|
621
|
+
+ `${result.cookbooks.passed}/${result.cookbooks.total} passed `
|
|
622
|
+
+ `(${result.passed}/${result.total} steps)`);
|
|
484
623
|
|
|
485
624
|
// `N cookbook test(s) failed` was the whole error list until 2026-08-29:
|
|
486
625
|
// the count without a single name, so `Validation failed: 1 cookbook
|
|
@@ -498,6 +637,7 @@ class ValidationOrchestrator {
|
|
|
498
637
|
total: result.total,
|
|
499
638
|
passed: result.passed,
|
|
500
639
|
failed: result.failed,
|
|
640
|
+
cookbooks: result.cookbooks,
|
|
501
641
|
errors: result.failed > 0 ? [`${result.failed} cookbook test(s) failed`, ...details] : []
|
|
502
642
|
};
|
|
503
643
|
} catch (error) {
|
|
@@ -506,6 +646,7 @@ class ValidationOrchestrator {
|
|
|
506
646
|
total: 0,
|
|
507
647
|
passed: 0,
|
|
508
648
|
failed: 0,
|
|
649
|
+
cookbooks: { total: 0, passed: 0, failed: 0 },
|
|
509
650
|
errors: [`Cookbook tests failed: ${error.message}`]
|
|
510
651
|
};
|
|
511
652
|
}
|
|
@@ -594,6 +735,85 @@ class ValidationOrchestrator {
|
|
|
594
735
|
return { valid: result.valid, errors: result.errors };
|
|
595
736
|
}
|
|
596
737
|
|
|
738
|
+
/**
|
|
739
|
+
* Step 7: Manifest conformance — the uniform of a biz service.
|
|
740
|
+
*
|
|
741
|
+
* Reads the service root this orchestrator was CONSTRUCTED with, never
|
|
742
|
+
* `process.cwd()`: the wrapper starts a service from wherever the container's
|
|
743
|
+
* entrypoint happens to stand, and a check that measured the current
|
|
744
|
+
* directory would silently measure the wrong tree.
|
|
745
|
+
*
|
|
746
|
+
* The workspace root is resolved upwards from that service root and is absent
|
|
747
|
+
* inside a container — the rows that need it are then reported NOT RUN, and
|
|
748
|
+
* the signal carries them (`automation-gates.md` §5). There is no switch that
|
|
749
|
+
* skips this step and none that moves the signal: one path, unbypassable.
|
|
750
|
+
*
|
|
751
|
+
* Order inside the step is deliberate: the banner is printed BEFORE the file
|
|
752
|
+
* is written, so a service root that cannot be written to still leaves the
|
|
753
|
+
* human-readable verdict in the log.
|
|
754
|
+
*
|
|
755
|
+
* @see api/docs/governance/confirmations/biz-service-manifest.md §4
|
|
756
|
+
* @returns {{valid: boolean, deployable: boolean, errors: string[], findings: object[],
|
|
757
|
+
* deployFindings: object[], notRun: object[], signalPath: string|null}}
|
|
758
|
+
*/
|
|
759
|
+
validateManifestConformance() {
|
|
760
|
+
try {
|
|
761
|
+
const result = this.manifestRun();
|
|
762
|
+
|
|
763
|
+
// The banner and `ci/deployability.json` describe the WHOLE uniform,
|
|
764
|
+
// CONFIG_STEP_ROWS included: the verdict is about the tree, and a reader
|
|
765
|
+
// (or `deploy-production`) must not conclude it is clean because the boot
|
|
766
|
+
// log answered three of its rows one step earlier. What this step drops is
|
|
767
|
+
// only the second REPORT of those rows in its own table and errors.
|
|
768
|
+
for (const line of renderBanner(result)) {
|
|
769
|
+
this.logger.info(`[ValidationOrchestrator] ${line}`);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const signalPath = writeDeployabilitySignal({
|
|
773
|
+
serviceRoot: this.serviceRoot,
|
|
774
|
+
signal: buildDeployabilitySignal({
|
|
775
|
+
result,
|
|
776
|
+
validatorVersion: require('../package.json').version
|
|
777
|
+
})
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
const findings = result.findings.filter((finding) => !CONFIG_STEP_ROWS.includes(finding.id));
|
|
781
|
+
const bootFindings = findings.filter((finding) => finding.severity === 'boot');
|
|
782
|
+
|
|
783
|
+
return {
|
|
784
|
+
valid: bootFindings.length === 0,
|
|
785
|
+
deployable: result.ok,
|
|
786
|
+
errors: bootFindings.map(describeFinding),
|
|
787
|
+
findings,
|
|
788
|
+
// Read from the WHOLE run, for the same reason the banner and the signal
|
|
789
|
+
// are: `deployable` is a verdict about the tree, and these are the
|
|
790
|
+
// reasons behind it — not a second report of a row step 2 already
|
|
791
|
+
// showed. CONFIG_STEP_ROWS are declared `boot` today; the day the
|
|
792
|
+
// manifest gives one `deploy`, filtering here would print NOT DEPLOYABLE
|
|
793
|
+
// with nothing named as the cause (`automation-gates.md` §5).
|
|
794
|
+
deployFindings: result.findings.filter((finding) => finding.severity === 'deploy'),
|
|
795
|
+
notRun: result.notRun,
|
|
796
|
+
signalPath
|
|
797
|
+
};
|
|
798
|
+
} catch (error) {
|
|
799
|
+
// A broken manifest, an unreadable service root or an unwritable ci/ are
|
|
800
|
+
// contract violations, and they fail the validation like a `boot` finding
|
|
801
|
+
// — they are NOT reported as "no findings". They are returned rather than
|
|
802
|
+
// thrown so this step names itself, instead of surfacing as the generic
|
|
803
|
+
// "Validation error" of the catch in runFullValidation().
|
|
804
|
+
this.logger.error(`[ValidationOrchestrator] Step 7/7 could not run: ${error.message}`);
|
|
805
|
+
return {
|
|
806
|
+
valid: false,
|
|
807
|
+
deployable: false,
|
|
808
|
+
errors: [error.message],
|
|
809
|
+
findings: [],
|
|
810
|
+
deployFindings: [],
|
|
811
|
+
notRun: [],
|
|
812
|
+
signalPath: null
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
597
817
|
/**
|
|
598
818
|
* Finalize validation results and generate proof if successful
|
|
599
819
|
*/
|
|
@@ -665,7 +885,10 @@ class ValidationOrchestrator {
|
|
|
665
885
|
|
|
666
886
|
this.logger.info(`[ValidationOrchestrator] Proof saved: ${this.proofPath}`);
|
|
667
887
|
} catch (error) {
|
|
668
|
-
throw new Error(`Failed to save proof
|
|
888
|
+
throw new Error(`[ValidationOrchestrator] Failed to save the validation proof to ${this.proofPath} `
|
|
889
|
+
+ `- Expected the conn-runtime directory to be writable. Fix: repair whatever the cause names `
|
|
890
|
+
+ `(${error.message})`,
|
|
891
|
+
{ cause: error });
|
|
669
892
|
}
|
|
670
893
|
}
|
|
671
894
|
}
|