@onlineapps/conn-orch-validator 7.0.0 → 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.
Files changed (102) hide show
  1. package/CHANGELOG.md +2558 -2
  2. package/README.md +1038 -4
  3. package/docs/DESIGN.md +3 -1
  4. package/manifests/biz-service.manifest.json +658 -0
  5. package/manifests/library.manifest.json +324 -0
  6. package/package.json +12 -6
  7. package/src/CookbookTestRunner.js +408 -101
  8. package/src/CookbookTestUtils.js +7 -8
  9. package/src/ServiceReadinessValidator.js +10 -35
  10. package/src/ValidationOrchestrator.js +219 -71
  11. package/src/cli/biz-ci-gate.js +176 -33
  12. package/src/cli/oa-lint-scripts.js +221 -0
  13. package/src/cli/oa-sync-template.js +1020 -0
  14. package/src/cli/oa-validate.js +474 -0
  15. package/src/helpers/README.md +2 -1
  16. package/src/helpers/createServiceReadinessTests.js +60 -4
  17. package/src/index.js +33 -3
  18. package/src/lint/scripts/lintScripts.js +298 -0
  19. package/src/manifest/checks/composeRunnerBlock.js +222 -0
  20. package/src/manifest/checks/composeShape.js +165 -0
  21. package/src/manifest/checks/contractBridge.js +181 -0
  22. package/src/manifest/checks/discoveryOrphan.js +50 -0
  23. package/src/manifest/checks/docsLintBridge.js +553 -0
  24. package/src/manifest/checks/fileAbsent.js +35 -0
  25. package/src/manifest/checks/gitTracked.js +204 -0
  26. package/src/manifest/checks/index.js +111 -0
  27. package/src/manifest/checks/libraryContext.js +226 -0
  28. package/src/manifest/checks/libraryDocs.js +75 -0
  29. package/src/manifest/checks/libraryPackage.js +272 -0
  30. package/src/manifest/checks/librarySource.js +274 -0
  31. package/src/manifest/checks/libraryTests.js +121 -0
  32. package/src/manifest/checks/libraryWorkspace.js +293 -0
  33. package/src/manifest/checks/readmeRegion.js +135 -0
  34. package/src/manifest/checks/scriptHeaders.js +79 -0
  35. package/src/manifest/checks/serviceConfig.js +390 -0
  36. package/src/manifest/checks/serviceConnectors.js +81 -0
  37. package/src/manifest/checks/serviceDb.js +388 -0
  38. package/src/manifest/checks/serviceFiles.js +754 -0
  39. package/src/manifest/checks/serviceIdentityRows.js +351 -0
  40. package/src/manifest/checks/serviceRuntime.js +295 -0
  41. package/src/manifest/checks/serviceScripts.js +213 -0
  42. package/src/manifest/deployabilitySignal.js +121 -0
  43. package/src/manifest/discovery.js +386 -0
  44. package/src/manifest/loadManifest.js +62 -0
  45. package/src/manifest/manifestShape.js +446 -0
  46. package/src/manifest/report.js +245 -0
  47. package/src/manifest/runManifest.js +449 -0
  48. package/src/manifest/serviceIdentity.js +140 -0
  49. package/src/manifest/walk.js +74 -0
  50. package/src/manifest/workspaceRoot.js +242 -0
  51. package/src/mocks/MockMQClient.js +13 -30
  52. package/src/mocks/MockRegistry.js +4 -2
  53. package/src/mocks/MockStorage.js +4 -2
  54. package/src/sync/docsRegion.js +463 -0
  55. package/src/sync/generatedRegion.js +228 -0
  56. package/src/sync/readmeLocation.js +182 -0
  57. package/src/sync/readmePointer.js +477 -0
  58. package/src/sync/serviceTemplate.js +583 -0
  59. package/src/sync/sharedEnv.js +162 -0
  60. package/src/sync/uniformFiles.js +474 -0
  61. package/src/utils/bizCiGateContract.js +131 -7
  62. package/src/utils/connectorContract.js +97 -7
  63. package/src/utils/cookbookFormat.js +81 -40
  64. package/src/utils/deployContract.js +140 -9
  65. package/src/utils/envContract.js +57 -1
  66. package/src/utils/handlerRef.js +181 -0
  67. package/src/utils/installContract.js +287 -41
  68. package/src/utils/libCompat.js +29 -7
  69. package/src/utils/migrationOrder.js +163 -0
  70. package/src/utils/preValidation.js +20 -7
  71. package/src/utils/setupDatabase.js +194 -13
  72. package/src/utils/testCoverageContract.js +539 -0
  73. package/src/utils/testNamespace.js +247 -23
  74. package/src/utils/throwawaySchema.js +207 -0
  75. package/src/validators/ServiceStructureValidator.js +2 -1
  76. package/templates/business-service/.dockerignore +42 -0
  77. package/templates/business-service/.gitlab-ci.yml +290 -0
  78. package/templates/business-service/Dockerfile +27 -0
  79. package/templates/business-service/README.md +213 -0
  80. package/templates/business-service/config/biz-docs-lint.tree.json +10 -0
  81. package/templates/business-service/config/env-templates/__SERVICE_NAME__.env +4 -0
  82. package/templates/business-service/config/env-templates/shared.env +65 -0
  83. package/templates/business-service/config/service/config.json +14 -0
  84. package/templates/business-service/config/service/integration-contract.json +12 -0
  85. package/templates/business-service/config/service/operations.json +41 -0
  86. package/templates/business-service/docker-compose.production.yml +60 -0
  87. package/templates/business-service/docker-compose.yml +93 -0
  88. package/templates/business-service/docs/80-setup/INSTALL.md +101 -0
  89. package/templates/business-service/docs/80-setup/PLATFORM_MATRIX.md +65 -0
  90. package/templates/business-service/docs/80-setup/README.md +18 -0
  91. package/templates/business-service/docs/80-setup/VALIDATION.md +78 -0
  92. package/templates/business-service/docs/README.md +18 -0
  93. package/templates/business-service/gitignore +42 -0
  94. package/templates/business-service/index.js +10 -0
  95. package/templates/business-service/init.sh +54 -0
  96. package/templates/business-service/jest.config.js +6 -0
  97. package/templates/business-service/package.json.template +31 -0
  98. package/templates/business-service/scripts/verify-deploy-uniform.sh +180 -0
  99. package/templates/business-service/src/handlers/v3/echo.js +39 -0
  100. package/templates/business-service/tests/cookbooks/echo.json +36 -0
  101. package/templates/business-service/tests/unit/handler.test.js +78 -0
  102. package/src/WorkflowTestRunner.js +0 -402
@@ -3,7 +3,7 @@
3
3
  const {
4
4
  MIN_COOKBOOK_FORMAT_VERSION,
5
5
  checkCookbookFormatVersion,
6
- normalizeCookbookSteps,
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
- // Both documented shapes, through the one owner of that rule. Requiring an
89
- // array here rejected every V2 object-shaped cookbook, and requiring `id`
90
- // rejected every cookbook that follows format.md — measured 2026-08-30,
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 = normalizeCookbookSteps(cookbook.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 = normalizeCookbookSteps(cookbook1.steps);
141
- const steps2 = normalizeCookbookSteps(cookbook2.steps);
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
- * 2026-08-22: the `url` the probe consumed is gone too. It outlived the
42
- * probe by five months as an accepted-and-ignored argument echoed into
43
- * `results.serviceUrl` and printed as a `URL:` line — a report field
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
- if (!options.logger) {
65
- throw new Error(
66
- '[ServiceReadinessValidator] Logger is required - Expected: a logger with info/warn/error/debug, '
67
- + 'so the readiness verdict leaves the process. '
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 (!handlerPattern.test(operation.handler)) {
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
- // The logger methods this orchestrator requires — the order is the order the
20
- // error message lists them in.
21
- const LOGGER_METHODS = ['info', 'warn', 'error', 'debug'];
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 6-step validation process and generates validation proof.
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
- * `options.serviceUrl` is accepted-and-ignored: existing callers
38
- * (ServiceWrapper, the biz `scripts/run-pre-validation.js`) still pass one.
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
- if (!options.logger) {
56
- throw new Error(
57
- '[ValidationOrchestrator] Logger is required - Expected: a logger with info/warn/error/debug, '
58
- + 'so validation narrates where the service writes. '
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('config.json not found');
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(`Failed to calculate fingerprint: ${error.message}`);
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 6-step validation process.
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 were six steps until 2026-08-22. Step 5, "Service Readiness",
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/6: Service Structure');
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/6: Config Files');
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/6: Environment Contract');
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/6: Operations Compliance');
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/6: Cookbook Tests');
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/6: Connector Integration');
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: Validate config files
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
- // Validate config.json
436
- const configFile = path.join(this.configPath, 'config.json');
437
- if (!fs.existsSync(configFile)) {
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
- // Validate operations.json
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: errors.length === 0,
459
- errors: 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: [`Config validation failed: ${error.message}`]
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 (!handlerPattern.test(opDef.handler)) {
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
- this.logger.info(`[ValidationOrchestrator] ✓ Cookbook tests: ${result.passed}/${result.total} passed`);
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: ${error.message}`);
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
  }