@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.
Files changed (102) hide show
  1. package/CHANGELOG.md +2591 -2
  2. package/README.md +1075 -7
  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 +422 -104
  8. package/src/CookbookTestUtils.js +7 -8
  9. package/src/ServiceReadinessValidator.js +78 -42
  10. package/src/ValidationOrchestrator.js +298 -75
  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 +12 -2
  16. package/src/helpers/createServiceReadinessTests.js +75 -6
  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 +213 -13
  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 +21 -20
  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
@@ -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
- const { ValidationProofCodec, ValidationProofVerifier } = require('@onlineapps/service-validator-core');
7
+ // `ValidationProofVerifier` was destructured here alongside the codec and never
8
+ // called. It verified a cached proof before booting; the proof cache was
9
+ // removed (see validate()) and the verifier's last call site went with it,
10
+ // leaving a binding whose only remaining effect was to tell a reader that this
11
+ // class still checks proofs. `ValidationProofCodec` stays — finalizeResults()
12
+ // encodes the proof it writes.
13
+ const { ValidationProofCodec } = require('@onlineapps/service-validator-core');
8
14
  const FingerprintUtils = require('@onlineapps/service-validator-core').FingerprintUtils;
9
15
  const { ServiceStructureValidator } = require('./validators/ServiceStructureValidator');
10
16
  const { describeStepFailureWithContext } = require('./utils/stepFailure');
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
- // The logger methods this orchestrator requires — the order is the order the
14
- // error message lists them in.
15
- 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']);
16
40
 
17
41
  /**
18
42
  * ValidationOrchestrator
19
43
  *
20
44
  * Orchestrates complete service validation (Tier 1 Pre-Validation)
21
- * Runs a 6-step validation process and generates validation proof.
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
- * `options.serviceUrl` is accepted-and-ignored: existing callers
32
- * (ServiceWrapper, the biz `scripts/run-pre-validation.js`) still pass one.
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
- if (!options.logger) {
50
- throw new Error(
51
- '[ValidationOrchestrator] Logger is required - Expected: a logger with info/warn/error/debug, '
52
- + 'so validation narrates where the service writes. '
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('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.`);
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: config.service?.version || pkg.version,
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(`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 });
201
285
  }
202
286
  }
203
287
 
204
288
  /**
205
- * 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.
206
293
  *
207
- * 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",
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/6: Service Structure');
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/6: Config Files');
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/6: Environment Contract');
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/6: Operations Compliance');
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/6: Cookbook Tests');
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/6: Connector Integration');
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: 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[]}}
355
483
  */
356
484
  async validateConfig() {
357
- const errors = [];
358
-
359
485
  try {
360
- // Validate config.json
361
- const configFile = path.join(this.configPath, 'config.json');
362
- if (!fs.existsSync(configFile)) {
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: ${errors.length === 0 ? 'PASS' : 'FAIL'}`);
490
+ this.logger.info(`[ValidationOrchestrator] ✓ Config files: ${blocking.length === 0 ? 'PASS' : 'FAIL'}`);
382
491
  return {
383
- valid: errors.length === 0,
384
- errors: 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: [`Config validation failed: ${error.message}`]
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 (!handlerPattern.test(opDef.handler)) {
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
- 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)`);
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: ${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 });
669
892
  }
670
893
  }
671
894
  }