@onlineapps/conn-orch-validator 7.0.0 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2582 -2
- package/README.md +1038 -4
- package/docs/DESIGN.md +3 -1
- package/manifests/biz-service.manifest.json +658 -0
- package/manifests/library.manifest.json +324 -0
- package/package.json +12 -6
- package/src/CookbookTestRunner.js +408 -101
- package/src/CookbookTestUtils.js +7 -8
- package/src/ServiceReadinessValidator.js +10 -35
- package/src/ValidationOrchestrator.js +219 -71
- package/src/cli/biz-ci-gate.js +176 -33
- package/src/cli/oa-lint-scripts.js +221 -0
- package/src/cli/oa-sync-template.js +1020 -0
- package/src/cli/oa-validate.js +474 -0
- package/src/helpers/README.md +2 -1
- package/src/helpers/createServiceReadinessTests.js +60 -4
- package/src/index.js +33 -3
- package/src/lint/scripts/lintScripts.js +298 -0
- package/src/manifest/checks/composeRunnerBlock.js +222 -0
- package/src/manifest/checks/composeShape.js +165 -0
- package/src/manifest/checks/contractBridge.js +181 -0
- package/src/manifest/checks/discoveryOrphan.js +50 -0
- package/src/manifest/checks/docsLintBridge.js +553 -0
- package/src/manifest/checks/fileAbsent.js +35 -0
- package/src/manifest/checks/gitTracked.js +204 -0
- package/src/manifest/checks/index.js +111 -0
- package/src/manifest/checks/libraryContext.js +226 -0
- package/src/manifest/checks/libraryDocs.js +75 -0
- package/src/manifest/checks/libraryPackage.js +272 -0
- package/src/manifest/checks/librarySource.js +274 -0
- package/src/manifest/checks/libraryTests.js +121 -0
- package/src/manifest/checks/libraryWorkspace.js +293 -0
- package/src/manifest/checks/readmeRegion.js +135 -0
- package/src/manifest/checks/scriptHeaders.js +79 -0
- package/src/manifest/checks/serviceConfig.js +390 -0
- package/src/manifest/checks/serviceConnectors.js +81 -0
- package/src/manifest/checks/serviceDb.js +388 -0
- package/src/manifest/checks/serviceFiles.js +754 -0
- package/src/manifest/checks/serviceIdentityRows.js +351 -0
- package/src/manifest/checks/serviceRuntime.js +295 -0
- package/src/manifest/checks/serviceScripts.js +213 -0
- package/src/manifest/deployabilitySignal.js +121 -0
- package/src/manifest/discovery.js +386 -0
- package/src/manifest/loadManifest.js +62 -0
- package/src/manifest/manifestShape.js +446 -0
- package/src/manifest/report.js +245 -0
- package/src/manifest/runManifest.js +449 -0
- package/src/manifest/serviceIdentity.js +140 -0
- package/src/manifest/walk.js +74 -0
- package/src/manifest/workspaceRoot.js +242 -0
- package/src/mocks/MockMQClient.js +13 -30
- package/src/mocks/MockRegistry.js +4 -2
- package/src/mocks/MockStorage.js +4 -2
- package/src/sync/docsRegion.js +463 -0
- package/src/sync/generatedRegion.js +228 -0
- package/src/sync/readmeLocation.js +182 -0
- package/src/sync/readmePointer.js +477 -0
- package/src/sync/serviceTemplate.js +583 -0
- package/src/sync/sharedEnv.js +162 -0
- package/src/sync/uniformFiles.js +474 -0
- package/src/utils/bizCiGateContract.js +131 -7
- package/src/utils/connectorContract.js +97 -7
- package/src/utils/cookbookFormat.js +81 -40
- package/src/utils/deployContract.js +140 -9
- package/src/utils/envContract.js +57 -1
- package/src/utils/handlerRef.js +181 -0
- package/src/utils/installContract.js +287 -41
- package/src/utils/libCompat.js +29 -7
- package/src/utils/migrationOrder.js +163 -0
- package/src/utils/preValidation.js +20 -7
- package/src/utils/setupDatabase.js +194 -13
- package/src/utils/testCoverageContract.js +539 -0
- package/src/utils/testNamespace.js +247 -23
- package/src/utils/throwawaySchema.js +207 -0
- package/src/validators/ServiceStructureValidator.js +2 -1
- package/templates/business-service/.dockerignore +42 -0
- package/templates/business-service/.gitlab-ci.yml +409 -0
- package/templates/business-service/Dockerfile +27 -0
- package/templates/business-service/README.md +213 -0
- package/templates/business-service/config/biz-docs-lint.tree.json +10 -0
- package/templates/business-service/config/env-templates/__SERVICE_NAME__.env +22 -0
- package/templates/business-service/config/env-templates/shared.env +65 -0
- package/templates/business-service/config/service/config.json +14 -0
- package/templates/business-service/config/service/integration-contract.json +12 -0
- package/templates/business-service/config/service/operations.json +41 -0
- package/templates/business-service/docker-compose.production.yml +60 -0
- package/templates/business-service/docker-compose.yml +93 -0
- package/templates/business-service/docs/80-setup/INSTALL.md +123 -0
- package/templates/business-service/docs/80-setup/PLATFORM_MATRIX.md +65 -0
- package/templates/business-service/docs/80-setup/README.md +18 -0
- package/templates/business-service/docs/80-setup/VALIDATION.md +78 -0
- package/templates/business-service/docs/README.md +18 -0
- package/templates/business-service/gitignore +42 -0
- package/templates/business-service/index.js +10 -0
- package/templates/business-service/init.sh +54 -0
- package/templates/business-service/jest.config.js +6 -0
- package/templates/business-service/package.json.template +31 -0
- package/templates/business-service/scripts/verify-deploy-uniform.sh +180 -0
- package/templates/business-service/src/handlers/v3/echo.js +39 -0
- package/templates/business-service/tests/cookbooks/echo.json +36 -0
- package/templates/business-service/tests/unit/handler.test.js +78 -0
- package/src/WorkflowTestRunner.js +0 -402
package/src/CookbookTestUtils.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const {
|
|
4
4
|
MIN_COOKBOOK_FORMAT_VERSION,
|
|
5
5
|
checkCookbookFormatVersion,
|
|
6
|
-
|
|
6
|
+
readCookbookSteps,
|
|
7
7
|
stepIdentityOf
|
|
8
8
|
} = require('./utils/cookbookFormat');
|
|
9
9
|
|
|
@@ -85,13 +85,12 @@ class CookbookTestUtils {
|
|
|
85
85
|
errors.push(versionProblem);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
// that was 100 % of the tree.
|
|
88
|
+
// The one shape the format allows, through the one owner of that rule
|
|
89
|
+
// (`utils/cookbookFormat`), so this entry point and the Tier-1 runner cannot
|
|
90
|
+
// disagree about what a cookbook's steps are.
|
|
92
91
|
let steps;
|
|
93
92
|
try {
|
|
94
|
-
steps =
|
|
93
|
+
steps = readCookbookSteps(cookbook.steps);
|
|
95
94
|
} catch (error) {
|
|
96
95
|
// The refusal is the finding. This entry point answers with a list, not
|
|
97
96
|
// an exception, so the message becomes one more error in that list.
|
|
@@ -137,8 +136,8 @@ class CookbookTestUtils {
|
|
|
137
136
|
}
|
|
138
137
|
|
|
139
138
|
// Compare steps
|
|
140
|
-
const steps1 =
|
|
141
|
-
const steps2 =
|
|
139
|
+
const steps1 = readCookbookSteps(cookbook1.steps);
|
|
140
|
+
const steps2 = readCookbookSteps(cookbook2.steps);
|
|
142
141
|
|
|
143
142
|
if (steps1.length !== steps2.length) {
|
|
144
143
|
differences.push({
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { assertLogger } = require('@onlineapps/logger-contract');
|
|
3
4
|
const CookbookTestUtils = require('./CookbookTestUtils');
|
|
4
|
-
|
|
5
|
-
// The logger methods this validator requires — the order is the order the
|
|
6
|
-
// error message lists them in.
|
|
7
|
-
const LOGGER_METHODS = ['info', 'warn', 'error', 'debug'];
|
|
5
|
+
const { HANDLER_REF_PATTERN } = require('./utils/handlerRef');
|
|
8
6
|
|
|
9
7
|
/**
|
|
10
8
|
* The semver 2.0.0 grammar, verbatim from semver.org's own published regular
|
|
@@ -38,16 +36,9 @@ const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]
|
|
|
38
36
|
* previously spent on `health` fold into `operations` so total stays
|
|
39
37
|
* at 100.
|
|
40
38
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* naming an endpoint that does not exist. The only caller that still
|
|
45
|
-
* passed one was Tier-1's readiness step, and that step is gone
|
|
46
|
-
* (see ValidationOrchestrator: its verdict was a strict function of
|
|
47
|
-
* steps 2 and 3). What is left has exactly one consumer:
|
|
48
|
-
* `helpers/createServiceReadinessTests`, and through it the
|
|
49
|
-
* `tests/bootstrap/` suites of the biz repos — which pass name, version,
|
|
50
|
-
* operations, testCookbook and registry, and never passed a url.
|
|
39
|
+
* `validateReadiness(service)` reads `name`, `version`, `operations`,
|
|
40
|
+
* `testCookbook` and `registry`. Those five are what it reads, and it
|
|
41
|
+
* reads no others.
|
|
51
42
|
*
|
|
52
43
|
* @see api/docs/biz/30-operations/schema-v3.md
|
|
53
44
|
* @see /api/docs/biz/40-cookbooks/test-runner-flow.md (input probe contract)
|
|
@@ -61,26 +52,11 @@ class ServiceReadinessValidator {
|
|
|
61
52
|
// caller that did not print the returned object learned nothing about why a
|
|
62
53
|
// service was refused. Owner confirmation:
|
|
63
54
|
// docs/governance/confirmations/connector-logger-contract.md 001.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
+ 'Fix: pass options.logger (e.g. the logger your service already built).'
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const missingLoggerMethods = LOGGER_METHODS.filter(
|
|
73
|
-
(method) => typeof options.logger[method] !== 'function'
|
|
55
|
+
this.logger = assertLogger(
|
|
56
|
+
'ServiceReadinessValidator',
|
|
57
|
+
options.logger,
|
|
58
|
+
'the readiness verdict leaves the process'
|
|
74
59
|
);
|
|
75
|
-
if (missingLoggerMethods.length > 0) {
|
|
76
|
-
throw new Error(
|
|
77
|
-
'[ServiceReadinessValidator] Logger is incomplete - Expected: info, warn, error, debug as functions; '
|
|
78
|
-
+ `missing: ${missingLoggerMethods.join(', ')}. `
|
|
79
|
-
+ 'Fix: pass a logger implementing all four.'
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
this.logger = options.logger;
|
|
84
60
|
|
|
85
61
|
// Readiness checks: core (80 points) + optional (20 points) = 100 points max
|
|
86
62
|
// Core checks ALWAYS run, optional checks run if testCookbook/registry provided
|
|
@@ -253,7 +229,6 @@ class ServiceReadinessValidator {
|
|
|
253
229
|
}
|
|
254
230
|
|
|
255
231
|
const validScopes = ['platform', 'tenant', 'workspace'];
|
|
256
|
-
const handlerPattern = /^handlers\/[a-zA-Z0-9_\/-]+#[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
257
232
|
const forbiddenV2Fields = ['endpoint', 'method', 'path'];
|
|
258
233
|
|
|
259
234
|
for (const [name, operation] of Object.entries(operations)) {
|
|
@@ -262,7 +237,7 @@ class ServiceReadinessValidator {
|
|
|
262
237
|
}
|
|
263
238
|
if (!operation.handler) {
|
|
264
239
|
errors.push(`Operation '${name}' missing handler (v3 — 'handlers/<path>#<export>')`);
|
|
265
|
-
} else if (!
|
|
240
|
+
} else if (!HANDLER_REF_PATTERN.test(operation.handler)) {
|
|
266
241
|
errors.push(`Operation '${name}' has invalid handler ref: '${operation.handler}'`);
|
|
267
242
|
}
|
|
268
243
|
if (!operation.bundle_scope) {
|
|
@@ -15,16 +15,34 @@ const FingerprintUtils = require('@onlineapps/service-validator-core').Fingerpri
|
|
|
15
15
|
const { ServiceStructureValidator } = require('./validators/ServiceStructureValidator');
|
|
16
16
|
const { describeStepFailureWithContext } = require('./utils/stepFailure');
|
|
17
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');
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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']);
|
|
22
40
|
|
|
23
41
|
/**
|
|
24
42
|
* ValidationOrchestrator
|
|
25
43
|
*
|
|
26
44
|
* Orchestrates complete service validation (Tier 1 Pre-Validation)
|
|
27
|
-
* Runs a
|
|
45
|
+
* Runs a 7-step validation process and generates validation proof.
|
|
28
46
|
*
|
|
29
47
|
* Called automatically by ServiceWrapper during initialization.
|
|
30
48
|
*
|
|
@@ -34,12 +52,8 @@ const LOGGER_METHODS = ['info', 'warn', 'error', 'debug'];
|
|
|
34
52
|
* different place: a service now fails on a variable it declared it needs
|
|
35
53
|
* before any handler runs, instead of several layers later inside the wrapper.
|
|
36
54
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* It is not stored and not required. It fed the readiness step's
|
|
40
|
-
* `fetch(url + '/health')`, retired in 3.3.1 under ADR 0005; after that it was
|
|
41
|
-
* a *required* constructor option no line of code read, so a service whose
|
|
42
|
-
* 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.
|
|
43
57
|
*/
|
|
44
58
|
class ValidationOrchestrator {
|
|
45
59
|
constructor(options = {}) {
|
|
@@ -52,26 +66,11 @@ class ValidationOrchestrator {
|
|
|
52
66
|
// An incomplete logger therefore reached the runner one line later and threw
|
|
53
67
|
// in its name instead of this one. Owner confirmation:
|
|
54
68
|
// docs/governance/confirmations/connector-logger-contract.md 001.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
+ 'Fix: pass options.logger (e.g. the logger your service already built).'
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const missingLoggerMethods = LOGGER_METHODS.filter(
|
|
64
|
-
(method) => typeof options.logger[method] !== 'function'
|
|
69
|
+
this.logger = assertLogger(
|
|
70
|
+
'ValidationOrchestrator',
|
|
71
|
+
options.logger,
|
|
72
|
+
'validation narrates where the service writes'
|
|
65
73
|
);
|
|
66
|
-
if (missingLoggerMethods.length > 0) {
|
|
67
|
-
throw new Error(
|
|
68
|
-
'[ValidationOrchestrator] Logger is incomplete - Expected: info, warn, error, debug as functions; '
|
|
69
|
-
+ `missing: ${missingLoggerMethods.join(', ')}. `
|
|
70
|
-
+ 'Fix: pass a logger implementing all four.'
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
this.logger = options.logger;
|
|
75
74
|
|
|
76
75
|
// config/service/ is the ONLY configuration path (owner decision
|
|
77
76
|
// 2026-09-03). The legacy conn-config/ branch is gone: zero such
|
|
@@ -83,6 +82,12 @@ class ValidationOrchestrator {
|
|
|
83
82
|
this.runtimePath = path.join(this.serviceRoot, 'conn-runtime');
|
|
84
83
|
this.proofPath = path.join(this.runtimePath, 'validation-proof.json');
|
|
85
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
|
+
|
|
86
91
|
// Validators
|
|
87
92
|
this.structureValidator = new ServiceStructureValidator(this.serviceRoot);
|
|
88
93
|
this.cookbookRunner = new CookbookTestRunner({
|
|
@@ -192,7 +197,8 @@ class ValidationOrchestrator {
|
|
|
192
197
|
const envTemplateFile = path.join(this.serviceRoot, '..', '..', 'config', 'env-templates', `${path.basename(this.serviceRoot)}.env`);
|
|
193
198
|
|
|
194
199
|
if (!fs.existsSync(configFile)) {
|
|
195
|
-
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.`);
|
|
196
202
|
}
|
|
197
203
|
|
|
198
204
|
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
@@ -272,14 +278,20 @@ class ValidationOrchestrator {
|
|
|
272
278
|
|
|
273
279
|
return FingerprintUtils.generate(fingerprintData);
|
|
274
280
|
} catch (error) {
|
|
275
|
-
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 });
|
|
276
285
|
}
|
|
277
286
|
}
|
|
278
287
|
|
|
279
288
|
/**
|
|
280
|
-
* 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.
|
|
281
293
|
*
|
|
282
|
-
* There
|
|
294
|
+
* There was a different sixth step until 2026-08-22. Step 5, "Service Readiness",
|
|
283
295
|
* delegated to `ServiceReadinessValidator.validateReadiness({name, url,
|
|
284
296
|
* operations})` and reported a sixth-of-the-run verdict it never
|
|
285
297
|
* independently reached:
|
|
@@ -321,12 +333,19 @@ class ValidationOrchestrator {
|
|
|
321
333
|
warnings: [],
|
|
322
334
|
totalTests: 0,
|
|
323
335
|
passedTests: 0,
|
|
324
|
-
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: []
|
|
325
344
|
};
|
|
326
345
|
|
|
327
346
|
try {
|
|
328
347
|
// Step 1: Service Structure
|
|
329
|
-
this.logger.info('[ValidationOrchestrator] Step 1/
|
|
348
|
+
this.logger.info('[ValidationOrchestrator] Step 1/7: Service Structure');
|
|
330
349
|
results.steps.structure = await this.validateStructure();
|
|
331
350
|
if (!results.steps.structure.valid) {
|
|
332
351
|
results.success = false;
|
|
@@ -336,7 +355,7 @@ class ValidationOrchestrator {
|
|
|
336
355
|
}
|
|
337
356
|
|
|
338
357
|
// Step 2: Config Files
|
|
339
|
-
this.logger.info('[ValidationOrchestrator] Step 2/
|
|
358
|
+
this.logger.info('[ValidationOrchestrator] Step 2/7: Config Files');
|
|
340
359
|
results.steps.config = await this.validateConfig();
|
|
341
360
|
if (!results.steps.config.valid) {
|
|
342
361
|
results.success = false;
|
|
@@ -350,7 +369,7 @@ class ValidationOrchestrator {
|
|
|
350
369
|
// start without must fail here, where its name and the reason it exists
|
|
351
370
|
// are both at hand. SECRETS_MASTER_KEY used to fail after MQ
|
|
352
371
|
// registration, several layers away from the declaration (defect D2).
|
|
353
|
-
this.logger.info('[ValidationOrchestrator] Step 3/
|
|
372
|
+
this.logger.info('[ValidationOrchestrator] Step 3/7: Environment Contract');
|
|
354
373
|
results.steps.env = this.validateEnvContract();
|
|
355
374
|
if (!results.steps.env.valid) {
|
|
356
375
|
results.success = false;
|
|
@@ -361,7 +380,7 @@ class ValidationOrchestrator {
|
|
|
361
380
|
}
|
|
362
381
|
|
|
363
382
|
// Step 4: Operations Compliance
|
|
364
|
-
this.logger.info('[ValidationOrchestrator] Step 4/
|
|
383
|
+
this.logger.info('[ValidationOrchestrator] Step 4/7: Operations Compliance');
|
|
365
384
|
results.steps.operations = await this.validateOperations();
|
|
366
385
|
results.warnings.push(...(results.steps.operations.warnings || []));
|
|
367
386
|
if (!results.steps.operations.valid) {
|
|
@@ -370,7 +389,7 @@ class ValidationOrchestrator {
|
|
|
370
389
|
}
|
|
371
390
|
|
|
372
391
|
// Step 5: Cookbook Tests
|
|
373
|
-
this.logger.info('[ValidationOrchestrator] Step 5/
|
|
392
|
+
this.logger.info('[ValidationOrchestrator] Step 5/7: Cookbook Tests');
|
|
374
393
|
results.steps.cookbooks = await this.runCookbookTests();
|
|
375
394
|
results.totalTests += results.steps.cookbooks.total || 0;
|
|
376
395
|
results.passedTests += results.steps.cookbooks.passed || 0;
|
|
@@ -381,7 +400,7 @@ class ValidationOrchestrator {
|
|
|
381
400
|
}
|
|
382
401
|
|
|
383
402
|
// Step 6: Connector Integration
|
|
384
|
-
this.logger.info('[ValidationOrchestrator] Step 6/
|
|
403
|
+
this.logger.info('[ValidationOrchestrator] Step 6/7: Connector Integration');
|
|
385
404
|
results.steps.connectors = this.validateConnectors();
|
|
386
405
|
if (!results.steps.connectors.valid) {
|
|
387
406
|
// Severity is unchanged: the connector contract stays non-critical and
|
|
@@ -397,6 +416,27 @@ class ValidationOrchestrator {
|
|
|
397
416
|
results.warnings.push(...results.steps.connectors.errors);
|
|
398
417
|
}
|
|
399
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
|
+
|
|
400
440
|
// Finalize and generate proof if successful
|
|
401
441
|
return await this.finalizeResults(results, startTime);
|
|
402
442
|
|
|
@@ -426,46 +466,63 @@ class ValidationOrchestrator {
|
|
|
426
466
|
}
|
|
427
467
|
|
|
428
468
|
/**
|
|
429
|
-
* 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[]}}
|
|
430
483
|
*/
|
|
431
484
|
async validateConfig() {
|
|
432
|
-
const errors = [];
|
|
433
|
-
|
|
434
485
|
try {
|
|
435
|
-
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
errors.push('config.json not found');
|
|
439
|
-
} else {
|
|
440
|
-
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
441
|
-
if (!config.service?.name) errors.push('config.json missing service.name');
|
|
442
|
-
// Note: service.version is now read from package.json (Single Source of Truth)
|
|
443
|
-
}
|
|
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));
|
|
444
489
|
|
|
445
|
-
|
|
446
|
-
const operationsFile = path.join(this.configPath, 'operations.json');
|
|
447
|
-
if (!fs.existsSync(operationsFile)) {
|
|
448
|
-
errors.push('operations.json not found');
|
|
449
|
-
} else {
|
|
450
|
-
const operations = JSON.parse(fs.readFileSync(operationsFile, 'utf8'));
|
|
451
|
-
if (!operations.operations || Object.keys(operations.operations).length === 0) {
|
|
452
|
-
errors.push('operations.json has no operations defined');
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
this.logger.info(`[ValidationOrchestrator] ✓ Config files: ${errors.length === 0 ? 'PASS' : 'FAIL'}`);
|
|
490
|
+
this.logger.info(`[ValidationOrchestrator] ✓ Config files: ${blocking.length === 0 ? 'PASS' : 'FAIL'}`);
|
|
457
491
|
return {
|
|
458
|
-
valid:
|
|
459
|
-
errors:
|
|
492
|
+
valid: blocking.length === 0,
|
|
493
|
+
errors: blocking.map(describeFinding),
|
|
494
|
+
findings
|
|
460
495
|
};
|
|
461
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}`);
|
|
462
500
|
return {
|
|
463
501
|
valid: false,
|
|
464
|
-
errors: [
|
|
502
|
+
errors: [error.message],
|
|
503
|
+
findings: []
|
|
465
504
|
};
|
|
466
505
|
}
|
|
467
506
|
}
|
|
468
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
|
+
|
|
469
526
|
/**
|
|
470
527
|
* Step 3: Validate operations compliance (v3 — handler registry dispatch).
|
|
471
528
|
* Required per operation: handler ('handlers/<path>#<export>'), bundle_scope, input, output.
|
|
@@ -482,7 +539,6 @@ class ValidationOrchestrator {
|
|
|
482
539
|
const warnings = [];
|
|
483
540
|
|
|
484
541
|
const validScopes = ['platform', 'tenant', 'workspace'];
|
|
485
|
-
const handlerPattern = /^handlers\/[a-zA-Z0-9_\/-]+#[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
486
542
|
const forbiddenV2Fields = ['endpoint', 'method', 'path'];
|
|
487
543
|
|
|
488
544
|
for (const [opName, opDef] of Object.entries(operations.operations || {})) {
|
|
@@ -492,7 +548,7 @@ class ValidationOrchestrator {
|
|
|
492
548
|
|
|
493
549
|
if (!opDef.handler) {
|
|
494
550
|
errors.push(`Operation ${opName}: missing handler (v3 — 'handlers/<path>#<export>')`);
|
|
495
|
-
} else if (!
|
|
551
|
+
} else if (!HANDLER_REF_PATTERN.test(opDef.handler)) {
|
|
496
552
|
errors.push(`Operation ${opName}: invalid handler ref '${opDef.handler}' — expected 'handlers/<path>#<exportName>'`);
|
|
497
553
|
}
|
|
498
554
|
|
|
@@ -549,13 +605,21 @@ class ValidationOrchestrator {
|
|
|
549
605
|
total: 0,
|
|
550
606
|
passed: 0,
|
|
551
607
|
failed: 0,
|
|
608
|
+
cookbooks: { total: 0, passed: 0, failed: 0 },
|
|
552
609
|
warnings: ['No cookbook tests found']
|
|
553
610
|
};
|
|
554
611
|
}
|
|
555
612
|
|
|
556
613
|
const result = await this.cookbookRunner.runCookbooks(cookbooksPath);
|
|
557
614
|
|
|
558
|
-
|
|
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)`);
|
|
559
623
|
|
|
560
624
|
// `N cookbook test(s) failed` was the whole error list until 2026-08-29:
|
|
561
625
|
// the count without a single name, so `Validation failed: 1 cookbook
|
|
@@ -573,6 +637,7 @@ class ValidationOrchestrator {
|
|
|
573
637
|
total: result.total,
|
|
574
638
|
passed: result.passed,
|
|
575
639
|
failed: result.failed,
|
|
640
|
+
cookbooks: result.cookbooks,
|
|
576
641
|
errors: result.failed > 0 ? [`${result.failed} cookbook test(s) failed`, ...details] : []
|
|
577
642
|
};
|
|
578
643
|
} catch (error) {
|
|
@@ -581,6 +646,7 @@ class ValidationOrchestrator {
|
|
|
581
646
|
total: 0,
|
|
582
647
|
passed: 0,
|
|
583
648
|
failed: 0,
|
|
649
|
+
cookbooks: { total: 0, passed: 0, failed: 0 },
|
|
584
650
|
errors: [`Cookbook tests failed: ${error.message}`]
|
|
585
651
|
};
|
|
586
652
|
}
|
|
@@ -669,6 +735,85 @@ class ValidationOrchestrator {
|
|
|
669
735
|
return { valid: result.valid, errors: result.errors };
|
|
670
736
|
}
|
|
671
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
|
+
|
|
672
817
|
/**
|
|
673
818
|
* Finalize validation results and generate proof if successful
|
|
674
819
|
*/
|
|
@@ -740,7 +885,10 @@ class ValidationOrchestrator {
|
|
|
740
885
|
|
|
741
886
|
this.logger.info(`[ValidationOrchestrator] Proof saved: ${this.proofPath}`);
|
|
742
887
|
} catch (error) {
|
|
743
|
-
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 });
|
|
744
892
|
}
|
|
745
893
|
}
|
|
746
894
|
}
|