@onlineapps/conn-orch-validator 5.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -94,7 +94,7 @@ legitimate state until every repo adopts the declaration, and never silent.
94
94
 
95
95
  ```
96
96
  services/my-service/
97
- ├── conn-config/ ← Static (gitignored: NO)
97
+ ├── config/service/ ← Static (gitignored: NO)
98
98
  │ ├── config.json
99
99
  │ └── operations.json
100
100
  ├── conn-runtime/ ← Runtime (gitignored: YES)
@@ -140,7 +140,7 @@ The validator evaluates each service against cumulative implementation standards
140
140
 
141
141
  | Level | Name | Checks | Since |
142
142
  |-------|------|--------|-------|
143
- | **v1.0** | Base Service Standard | `conn-config/`, `src/app.js`, `index.js`, valid `config.json` + `operations.json`, `@onlineapps/service-wrapper` dep | 2025-06 |
143
+ | **v1.0** | Base Service Standard | `config/service/`, `src/app.js`, `index.js`, valid `config.json` + `operations.json`, `@onlineapps/service-wrapper` dep | 2025-06 |
144
144
  | **v1.1** | Multitenancy Standard | `wrapper.tenantContext` configured in `config.json` | 2026-03 |
145
145
  | **v1.2** | Business Error Handling | `business_error_contract` (some file under `src/**` imports a `BusinessError` family from `@onlineapps/service-wrapper` or declares the brand `onlineapps.businessError`) + `no_retired_error_hierarchy` (no error class imported from `@onlineapps/service-common` anywhere under `src/`) | 2026-03 |
146
146
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/conn-orch-validator",
3
- "version": "5.0.0",
3
+ "version": "6.0.0",
4
4
  "description": "Validation orchestrator for OA Drive microservices - coordinates validation across all layers (base, infra, orch, business)",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -9,7 +9,6 @@
9
9
  "scripts": {
10
10
  "test": "jest",
11
11
  "test:unit": "jest tests/unit",
12
- "test:component": "jest tests/component",
13
12
  "test:integration": "jest tests/integration",
14
13
  "test:coverage": "jest --coverage"
15
14
  },
@@ -10,6 +10,10 @@ const { checkCookbookFormatVersion, normalizeCookbookSteps } = require('./utils/
10
10
  const { getTestNamespace } = require('./utils/testNamespace');
11
11
  const { describeStepFailure, describeStepIdentity } = require('./utils/stepFailure');
12
12
 
13
+ // The logger methods this runner requires — the order is the order the error
14
+ // message lists them in.
15
+ const LOGGER_METHODS = ['info', 'warn', 'error', 'debug'];
16
+
13
17
  /**
14
18
  * The step's stopwatch, spelled out. A single total hid the fact that
15
19
  * biz-hello's `count_stub` spent 2264 ms loading sequelize and ~0 ms doing its
@@ -37,7 +41,7 @@ function describeStepTiming(result) {
37
41
  * httpClient: null, secrets: null, state: null, stream: null,
38
42
  * abortSignal: null }
39
43
  * matching the shape ServiceWrapper.ContextBuilder produces in production
40
- * (see api/docs/architecture/biz-service-invocation-model.md §5). Handlers
44
+ * (see api/docs/biz/10-invocation/handler-dispatch.md). Handlers
41
45
  * that need DB access import sequelize directly from their own
42
46
  * src/config/database.js module (see biz/60-templates/onboarding-checklist.md §9a);
43
47
  * connector slots stay null and the handler surfaces its own failure if
@@ -50,8 +54,29 @@ class CookbookTestRunner {
50
54
  if (!options.serviceName) {
51
55
  throw new Error('[CookbookTestRunner] serviceName is required');
52
56
  }
53
- if (!options.logger || typeof options.logger.warn !== 'function') {
54
- throw new Error('[CookbookTestRunner] Logger is required Expected object with warn() method');
57
+ // The logger is INJECTED and validated in full, HERE. Until 2026-09-03 only
58
+ // `warn()` was checkedthe one method this runner never calls, while it
59
+ // calls info() and error() throughout a run. A logger without them therefore
60
+ // constructed fine and died on the first log line: delayed validation, which
61
+ // architecture-principles.md §4 forbids. Owner confirmation:
62
+ // docs/governance/confirmations/connector-logger-contract.md 001.
63
+ if (!options.logger) {
64
+ throw new Error(
65
+ '[CookbookTestRunner] Logger is required - Expected: a logger with info/warn/error/debug, '
66
+ + 'so the runner writes where the service writes. '
67
+ + 'Fix: pass options.logger (e.g. the logger your service already built).'
68
+ );
69
+ }
70
+
71
+ const missingLoggerMethods = LOGGER_METHODS.filter(
72
+ (method) => typeof options.logger[method] !== 'function'
73
+ );
74
+ if (missingLoggerMethods.length > 0) {
75
+ throw new Error(
76
+ '[CookbookTestRunner] Logger is incomplete - Expected: info, warn, error, debug as functions; '
77
+ + `missing: ${missingLoggerMethods.join(', ')}. `
78
+ + 'Fix: pass a logger implementing all four.'
79
+ );
55
80
  }
56
81
  this.serviceName = options.serviceName;
57
82
  this.servicePath = options.servicePath;
@@ -310,7 +335,7 @@ class CookbookTestRunner {
310
335
  /**
311
336
  * Handler dispatch: require handler module, build minimal real ctx, call
312
337
  * handler(input, ctx). Mirrors the production ContextBuilder shape
313
- * (see api/docs/architecture/biz-service-invocation-model.md §5) with
338
+ * (see api/docs/biz/10-invocation/handler-dispatch.md) with
314
339
  * all connector slots set to null — handlers that need DB access use
315
340
  * direct sequelize import per biz/60-templates/onboarding-checklist.md §9a.
316
341
  */
@@ -621,12 +646,12 @@ class CookbookTestRunner {
621
646
  */
622
647
  async resolveOperation(serviceName, operationName) {
623
648
  const serviceRoot = this._resolveServiceRoot(serviceName);
624
- const newPath = path.join(serviceRoot, 'config', 'service', 'operations.json');
625
- const legacyPath = path.join(serviceRoot, 'conn-config', 'operations.json');
626
- const operationsPath = fs.existsSync(newPath) ? newPath : (fs.existsSync(legacyPath) ? legacyPath : null);
649
+ // config/service/ is the ONLY configuration path (owner decision
650
+ // 2026-09-03) the legacy conn-config/ layout exists in no repository.
651
+ const operationsPath = path.join(serviceRoot, 'config', 'service', 'operations.json');
627
652
 
628
- if (!operationsPath) {
629
- throw new Error(`Operations file not found for service: ${serviceName} (checked config/service/ and conn-config/)`);
653
+ if (!fs.existsSync(operationsPath)) {
654
+ throw new Error(`Operations file not found for service: ${serviceName} (expected config/service/operations.json)`);
630
655
  }
631
656
 
632
657
  const operations = JSON.parse(fs.readFileSync(operationsPath, 'utf8'));
@@ -2,6 +2,10 @@
2
2
 
3
3
  const CookbookTestUtils = require('./CookbookTestUtils');
4
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'];
8
+
5
9
  /**
6
10
  * ServiceReadinessValidator - Orchestrates complete service validation.
7
11
  *
@@ -32,15 +36,37 @@ const CookbookTestUtils = require('./CookbookTestUtils');
32
36
  * `tests/bootstrap/` suites of the biz repos — which pass name, version,
33
37
  * operations, testCookbook and registry, and never passed a url.
34
38
  *
35
- * @see /api/docs/architecture/biz-service-invocation-model.md §5.3
39
+ * @see api/docs/biz/30-operations/schema-v3.md
36
40
  * @see /api/docs/biz/40-cookbooks/test-runner-flow.md (input probe contract)
37
41
  * @see /api/docs/biz/80-decisions/0005-no-http-in-biz-containers.md
38
42
  */
39
43
  class ServiceReadinessValidator {
40
44
  constructor(options = {}) {
41
- if (!options.logger || typeof options.logger.warn !== 'function') {
42
- throw new Error('[ServiceReadinessValidator] Logger is required Expected object with warn() method');
45
+ // The logger is INJECTED and validated in full, HERE — and, since
46
+ // 2026-09-03, actually used: validateReadiness() writes the verdict through
47
+ // it. Until then this class demanded a logger and never wrote a line, so a
48
+ // caller that did not print the returned object learned nothing about why a
49
+ // service was refused. Owner confirmation:
50
+ // docs/governance/confirmations/connector-logger-contract.md 001.
51
+ if (!options.logger) {
52
+ throw new Error(
53
+ '[ServiceReadinessValidator] Logger is required - Expected: a logger with info/warn/error/debug, '
54
+ + 'so the readiness verdict leaves the process. '
55
+ + 'Fix: pass options.logger (e.g. the logger your service already built).'
56
+ );
43
57
  }
58
+
59
+ const missingLoggerMethods = LOGGER_METHODS.filter(
60
+ (method) => typeof options.logger[method] !== 'function'
61
+ );
62
+ if (missingLoggerMethods.length > 0) {
63
+ throw new Error(
64
+ '[ServiceReadinessValidator] Logger is incomplete - Expected: info, warn, error, debug as functions; '
65
+ + `missing: ${missingLoggerMethods.join(', ')}. `
66
+ + 'Fix: pass a logger implementing all four.'
67
+ );
68
+ }
69
+
44
70
  this.logger = options.logger;
45
71
 
46
72
  // Readiness checks: core (80 points) + optional (20 points) = 100 points max
@@ -149,6 +175,42 @@ class ServiceReadinessValidator {
149
175
  results.ready = requiredPassed && results.score >= 60;
150
176
  results.recommendation = this.getRecommendation(results);
151
177
 
178
+ // ONE line per evaluation, carrying the whole verdict as a structured
179
+ // object: what was checked, what each check contributed, and why the answer
180
+ // is what it is. Per check there is nothing — a line per check says the same
181
+ // thing in more places, and this runs on every biz service boot.
182
+ //
183
+ // `score` is read off the check rather than assumed: a check that never
184
+ // reached award() (operations absent entirely) carries no score field, and
185
+ // the log says 0 instead of `undefined`.
186
+ const checkSummary = {};
187
+ for (const [name, check] of Object.entries(results.checks)) {
188
+ checkSummary[name] = {
189
+ passed: check.passed === true,
190
+ score: typeof check.score === 'number' ? check.score : 0
191
+ };
192
+ }
193
+
194
+ this.logger.info('[ServiceReadinessValidator] Readiness evaluated', {
195
+ serviceName: results.serviceName,
196
+ score: results.score,
197
+ maxScore: results.maxScore,
198
+ ready: results.ready,
199
+ checks: checkSummary,
200
+ errors: results.errors,
201
+ warnings: results.warnings
202
+ });
203
+
204
+ // A refusal is an error-level event wherever logs are filtered by level;
205
+ // the info line above still carries the detail.
206
+ if (results.ready === false) {
207
+ this.logger.error('[ServiceReadinessValidator] Service not ready', {
208
+ serviceName: results.serviceName,
209
+ score: results.score,
210
+ errors: results.errors
211
+ });
212
+ }
213
+
152
214
  return results;
153
215
  }
154
216
 
@@ -10,6 +10,10 @@ const { ServiceStructureValidator } = require('./validators/ServiceStructureVali
10
10
  const { describeStepFailureWithContext } = require('./utils/stepFailure');
11
11
  const CookbookTestRunner = require('./CookbookTestRunner');
12
12
 
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'];
16
+
13
17
  /**
14
18
  * ValidationOrchestrator
15
19
  *
@@ -36,15 +40,39 @@ class ValidationOrchestrator {
36
40
  this.serviceRoot = options.serviceRoot;
37
41
  this.serviceName = options.serviceName;
38
42
  this.serviceVersion = options.serviceVersion;
39
- if (!options.logger || typeof options.logger.warn !== 'function') {
40
- throw new Error('[ValidationOrchestrator] Logger is required Expected object with warn() method');
43
+ // The logger is INJECTED and validated in full, HERE. Until 2026-09-03 only
44
+ // `warn()` was checked, while this class narrates every step of a run and
45
+ // hands the same object to CookbookTestRunner below, which demands all four.
46
+ // An incomplete logger therefore reached the runner one line later and threw
47
+ // in its name instead of this one. Owner confirmation:
48
+ // 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'
59
+ );
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
+ );
41
66
  }
67
+
42
68
  this.logger = options.logger;
43
69
 
44
- // Paths — prefer new config/service/ layout, fall back to legacy conn-config/
45
- const newConfigDir = path.join(this.serviceRoot, 'config', 'service');
46
- const legacyConfigDir = path.join(this.serviceRoot, 'conn-config');
47
- this.configPath = fs.existsSync(newConfigDir) ? newConfigDir : legacyConfigDir;
70
+ // config/service/ is the ONLY configuration path (owner decision
71
+ // 2026-09-03). The legacy conn-config/ branch is gone: zero such
72
+ // directories existed across api_biz/* and infra/* when it was removed, so
73
+ // it could only ever resolve to a path that does not exist and report the
74
+ // failure one layer away from the cause.
75
+ this.configPath = path.join(this.serviceRoot, 'config', 'service');
48
76
 
49
77
  this.runtimePath = path.join(this.serviceRoot, 'conn-runtime');
50
78
  this.proofPath = path.join(this.runtimePath, 'validation-proof.json');
@@ -64,7 +92,7 @@ class ValidationOrchestrator {
64
92
  * Checks if proof exists and is valid, or runs full validation
65
93
  */
66
94
  async validate() {
67
- console.log('[ValidationOrchestrator] Starting validation...');
95
+ this.logger.info('[ValidationOrchestrator] Starting validation...');
68
96
 
69
97
  // Check if proof exists and is valid
70
98
  // No proof cache. It skipped steps 1-3 and 5-6 — measured at 6 ms in total
@@ -79,9 +107,9 @@ class ValidationOrchestrator {
79
107
  // Dockerfile either. Removing it gives up no capability.
80
108
  //
81
109
  // Its original justification — an HTTP probe with retries in step 5 — was
82
- // removed in 3.3.1 under ADR 0005. Audit: docs/archive/validation-proof-audit-2026-08.md
110
+ // removed in 3.3.1 under ADR 0005.
83
111
  // Run full validation
84
- console.log('[ValidationOrchestrator] No valid proof, running validation...');
112
+ this.logger.info('[ValidationOrchestrator] No valid proof, running validation...');
85
113
  return await this.runFullValidation();
86
114
  }
87
115
 
@@ -223,7 +251,7 @@ class ValidationOrchestrator {
223
251
 
224
252
  try {
225
253
  // Step 1: Service Structure
226
- console.log('[ValidationOrchestrator] Step 1/6: Service Structure');
254
+ this.logger.info('[ValidationOrchestrator] Step 1/6: Service Structure');
227
255
  results.steps.structure = await this.validateStructure();
228
256
  if (!results.steps.structure.valid) {
229
257
  results.success = false;
@@ -233,7 +261,7 @@ class ValidationOrchestrator {
233
261
  }
234
262
 
235
263
  // Step 2: Config Files
236
- console.log('[ValidationOrchestrator] Step 2/6: Config Files');
264
+ this.logger.info('[ValidationOrchestrator] Step 2/6: Config Files');
237
265
  results.steps.config = await this.validateConfig();
238
266
  if (!results.steps.config.valid) {
239
267
  results.success = false;
@@ -247,7 +275,7 @@ class ValidationOrchestrator {
247
275
  // start without must fail here, where its name and the reason it exists
248
276
  // are both at hand. SECRETS_MASTER_KEY used to fail after MQ
249
277
  // registration, several layers away from the declaration (defect D2).
250
- console.log('[ValidationOrchestrator] Step 3/6: Environment Contract');
278
+ this.logger.info('[ValidationOrchestrator] Step 3/6: Environment Contract');
251
279
  results.steps.env = this.validateEnvContract();
252
280
  if (!results.steps.env.valid) {
253
281
  results.success = false;
@@ -258,7 +286,7 @@ class ValidationOrchestrator {
258
286
  }
259
287
 
260
288
  // Step 4: Operations Compliance
261
- console.log('[ValidationOrchestrator] Step 4/6: Operations Compliance');
289
+ this.logger.info('[ValidationOrchestrator] Step 4/6: Operations Compliance');
262
290
  results.steps.operations = await this.validateOperations();
263
291
  results.warnings.push(...(results.steps.operations.warnings || []));
264
292
  if (!results.steps.operations.valid) {
@@ -267,7 +295,7 @@ class ValidationOrchestrator {
267
295
  }
268
296
 
269
297
  // Step 5: Cookbook Tests
270
- console.log('[ValidationOrchestrator] Step 5/6: Cookbook Tests');
298
+ this.logger.info('[ValidationOrchestrator] Step 5/6: Cookbook Tests');
271
299
  results.steps.cookbooks = await this.runCookbookTests();
272
300
  results.totalTests += results.steps.cookbooks.total || 0;
273
301
  results.passedTests += results.steps.cookbooks.passed || 0;
@@ -278,7 +306,7 @@ class ValidationOrchestrator {
278
306
  }
279
307
 
280
308
  // Step 6: Connector Integration
281
- console.log('[ValidationOrchestrator] Step 6/6: Connector Integration');
309
+ this.logger.info('[ValidationOrchestrator] Step 6/6: Connector Integration');
282
310
  results.steps.connectors = this.validateConnectors();
283
311
  if (!results.steps.connectors.valid) {
284
312
  // Severity is unchanged: the connector contract stays non-critical and
@@ -298,7 +326,7 @@ class ValidationOrchestrator {
298
326
  return await this.finalizeResults(results, startTime);
299
327
 
300
328
  } catch (error) {
301
- console.error(`[ValidationOrchestrator] Validation failed: ${error.message}`);
329
+ this.logger.error(`[ValidationOrchestrator] Validation failed: ${error.message}`);
302
330
  results.success = false;
303
331
  results.errors.push(`Validation error: ${error.message}`);
304
332
  return this.finalizeResults(results, startTime);
@@ -312,7 +340,7 @@ class ValidationOrchestrator {
312
340
  try {
313
341
  const result = this.structureValidator.validate();
314
342
 
315
- console.log(`[ValidationOrchestrator] ✓ Service structure: ${result.valid ? 'PASS' : 'FAIL'}`);
343
+ this.logger.info(`[ValidationOrchestrator] ✓ Service structure: ${result.valid ? 'PASS' : 'FAIL'}`);
316
344
  return result;
317
345
  } catch (error) {
318
346
  return {
@@ -350,7 +378,7 @@ class ValidationOrchestrator {
350
378
  }
351
379
  }
352
380
 
353
- console.log(`[ValidationOrchestrator] ✓ Config files: ${errors.length === 0 ? 'PASS' : 'FAIL'}`);
381
+ this.logger.info(`[ValidationOrchestrator] ✓ Config files: ${errors.length === 0 ? 'PASS' : 'FAIL'}`);
354
382
  return {
355
383
  valid: errors.length === 0,
356
384
  errors: errors
@@ -413,7 +441,7 @@ class ValidationOrchestrator {
413
441
  }
414
442
  }
415
443
 
416
- console.log(`[ValidationOrchestrator] ✓ Operations compliance: ${errors.length === 0 ? 'PASS' : 'FAIL'}`
444
+ this.logger.info(`[ValidationOrchestrator] ✓ Operations compliance: ${errors.length === 0 ? 'PASS' : 'FAIL'}`
417
445
  + `${warnings.length > 0 ? ` (${warnings.length} warning(s))` : ''}`);
418
446
  return {
419
447
  valid: errors.length === 0,
@@ -440,7 +468,7 @@ class ValidationOrchestrator {
440
468
  const cookbooksPath = path.join(this.serviceRoot, 'tests', 'cookbooks');
441
469
 
442
470
  if (!fs.existsSync(cookbooksPath)) {
443
- console.warn('[ValidationOrchestrator] No cookbook tests found (tests/cookbooks/ missing)');
471
+ this.logger.warn('[ValidationOrchestrator] No cookbook tests found (tests/cookbooks/ missing)');
444
472
  return {
445
473
  success: true,
446
474
  total: 0,
@@ -452,7 +480,7 @@ class ValidationOrchestrator {
452
480
 
453
481
  const result = await this.cookbookRunner.runCookbooks(cookbooksPath);
454
482
 
455
- console.log(`[ValidationOrchestrator] ✓ Cookbook tests: ${result.passed}/${result.total} passed`);
483
+ this.logger.info(`[ValidationOrchestrator] ✓ Cookbook tests: ${result.passed}/${result.total} passed`);
456
484
 
457
485
  // `N cookbook test(s) failed` was the whole error list until 2026-08-29:
458
486
  // the count without a single name, so `Validation failed: 1 cookbook
@@ -462,7 +490,7 @@ class ValidationOrchestrator {
462
490
  const failedSteps = (result.steps || []).filter((step) => step.passed === false);
463
491
  const details = failedSteps.map(describeStepFailureWithContext);
464
492
  for (const detail of details) {
465
- console.error(`[ValidationOrchestrator] ✗ ${detail}`);
493
+ this.logger.error(`[ValidationOrchestrator] ✗ ${detail}`);
466
494
  }
467
495
 
468
496
  return {
@@ -517,7 +545,7 @@ class ValidationOrchestrator {
517
545
  // connector step; what is new here is a declaration that exists and is
518
546
  // wrong, and that must stop the boot rather than be worked around.
519
547
  if (error.code === 'ENOENT') {
520
- console.log('[ValidationOrchestrator] ⊘ Environment contract: SKIPPED — '
548
+ this.logger.info('[ValidationOrchestrator] ⊘ Environment contract: SKIPPED — '
521
549
  + `no ${path.relative(this.serviceRoot, contractFile)} in this service`);
522
550
  return { valid: true, skipped: true, errors: [] };
523
551
  }
@@ -525,14 +553,14 @@ class ValidationOrchestrator {
525
553
  }
526
554
 
527
555
  if (declaration === null) {
528
- console.log('[ValidationOrchestrator] ⊘ Environment contract: SKIPPED — '
556
+ this.logger.info('[ValidationOrchestrator] ⊘ Environment contract: SKIPPED — '
529
557
  + 'config/service/integration-contract.json declares no "env" block '
530
558
  + '(F16 adoption pending; nothing is being checked here)');
531
559
  return { valid: true, skipped: true, errors: [] };
532
560
  }
533
561
 
534
562
  const result = verifyEnvPresence({ declaration, env: process.env });
535
- console.log(`[ValidationOrchestrator] ${result.valid ? '✓' : '✗'} Environment contract: `
563
+ this.logger.info(`[ValidationOrchestrator] ${result.valid ? '✓' : '✗'} Environment contract: `
536
564
  + `${result.valid ? 'PASS' : 'FAIL'} (${result.checked.length} required name(s) checked: `
537
565
  + `${result.checked.join(', ') || 'none'})`);
538
566
  return { valid: result.valid, errors: result.errors };
@@ -561,7 +589,7 @@ class ValidationOrchestrator {
561
589
  }
562
590
 
563
591
  const result = verifyConnectorContract({ config, requiredConnectors, env: process.env });
564
- console.log(`[ValidationOrchestrator] ${result.valid ? '✓' : '✗'} Connector contract: `
592
+ this.logger.info(`[ValidationOrchestrator] ${result.valid ? '✓' : '✗'} Connector contract: `
565
593
  + `${result.valid ? 'PASS' : 'FAIL'} (${result.checked.length} checked: ${result.checked.join(', ') || 'none'})`);
566
594
  return { valid: result.valid, errors: result.errors };
567
595
  }
@@ -599,20 +627,20 @@ class ValidationOrchestrator {
599
627
  results.proof = encodedProof;
600
628
  results.fingerprint = fingerprint;
601
629
 
602
- console.log(`[ValidationOrchestrator] ✅ Validation PASSED (${duration}ms)`);
603
- console.log(`[ValidationOrchestrator] Proof saved to: ${this.proofPath}`);
630
+ this.logger.info(`[ValidationOrchestrator] ✅ Validation PASSED (${duration}ms)`);
631
+ this.logger.info(`[ValidationOrchestrator] Proof saved to: ${this.proofPath}`);
604
632
  } catch (error) {
605
- console.error(`[ValidationOrchestrator] Failed to generate proof: ${error.message}`);
633
+ this.logger.error(`[ValidationOrchestrator] Failed to generate proof: ${error.message}`);
606
634
  results.success = false;
607
635
  results.errors.push(`Proof generation failed: ${error.message}`);
608
636
  }
609
637
  } else {
610
- console.error(`[ValidationOrchestrator] ❌ Validation FAILED (${duration}ms)`);
638
+ this.logger.error(`[ValidationOrchestrator] ❌ Validation FAILED (${duration}ms)`);
611
639
  // The trailing `(', ')}` was a leftover from a half-finished edit to a
612
640
  // template literal: it printed on every failed validation as if it were
613
641
  // part of the data. Found while reading this line for the biz-hello
614
642
  // diagnosis (2026-08-29).
615
- console.error(`[ValidationOrchestrator] Errors: ${JSON.stringify(results.errors, null, 2)}`);
643
+ this.logger.error(`[ValidationOrchestrator] Errors: ${JSON.stringify(results.errors, null, 2)}`);
616
644
  }
617
645
 
618
646
  return results;
@@ -635,7 +663,7 @@ class ValidationOrchestrator {
635
663
  // Save proof
636
664
  fs.writeFileSync(this.proofPath, JSON.stringify(proof, null, 2));
637
665
 
638
- console.log(`[ValidationOrchestrator] Proof saved: ${this.proofPath}`);
666
+ this.logger.info(`[ValidationOrchestrator] Proof saved: ${this.proofPath}`);
639
667
  } catch (error) {
640
668
  throw new Error(`Failed to save proof: ${error.message}`);
641
669
  }
@@ -150,7 +150,13 @@ class WorkflowTestRunner extends EventEmitter {
150
150
  const resolvedInput = this.resolveInput(input, workflow.context);
151
151
 
152
152
  if (this.debug) {
153
- console.log(`Executing task: ${service}.${operation}`, resolvedInput);
153
+ // WHICH operation ran, never WHAT it carried: the resolved input is the
154
+ // workflow's payload, and printing it to stdout put whatever the cookbook
155
+ // held — identifiers, amounts, anything — into the terminal and every log
156
+ // collector behind it. This class takes no logger (constructor above), so
157
+ // the trace stays on console; giving it one is an API change and the
158
+ // owner's call, not a side effect of this fix.
159
+ console.log(`Executing task: ${service}.${operation}`);
154
160
  }
155
161
 
156
162
  // Get service from registry
@@ -20,7 +20,7 @@ Instead of **copying test code** between services, we provide **reusable test he
20
20
  **File:** `createServiceReadinessTests.js`
21
21
 
22
22
  **What it does:**
23
- - Validates service structure (conn-config/, src/app.js, package.json)
23
+ - Validates service structure (config/service/, src/handlers/, package.json)
24
24
  - Validates configuration files (config.json, operations.json)
25
25
  - Tests all operations endpoints
26
26
  - Validates health endpoint
@@ -70,7 +70,7 @@ The helper uses `ServiceStructureValidator` to validate service structure BEFORE
70
70
 
71
71
  ✅ ALL CHECKS PASSED
72
72
 
73
- ✓ Found Configuration directory: conn-config
73
+ ✓ Found Configuration directory: config/service
74
74
  ✓ Found Source code directory: src
75
75
  ✓ Found Tests directory: tests
76
76
  ✓ Found valid config.json
@@ -86,14 +86,14 @@ If validation fails, clear error messages are shown:
86
86
  ```
87
87
  ❌ ERRORS (2):
88
88
 
89
- ✗ Required directory missing: conn-config
89
+ ✗ Required directory missing: config/service/
90
90
  Type: MISSING_DIRECTORY
91
- File: conn-config
92
- Fix: Create directory: mkdir -p conn-config
91
+ File: config/service/
92
+ Fix: Create directory: mkdir -p config/service
93
93
 
94
- ✗ Service configuration missing: conn-config/config.json
94
+ ✗ Service configuration missing: config/service/config.json
95
95
  Type: MISSING_CONFIG
96
- File: conn-config/config.json
96
+ File: config/service/config.json
97
97
  Fix: Create config.json with service metadata. See: /docs/biz/60-templates/service-template.md
98
98
 
99
99
  ⚠️ WARNINGS (1):
@@ -111,8 +111,8 @@ The helper automatically detects and loads:
111
111
  const serviceRoot = path.resolve(__dirname, '../..');
112
112
 
113
113
  // Standard file locations
114
- const config = require(path.join(serviceRoot, 'conn-config/config.json'));
115
- const operations = require(path.join(serviceRoot, 'conn-config/operations.json'));
114
+ const config = require(path.join(serviceRoot, 'config/service/config.json'));
115
+ const operations = require(path.join(serviceRoot, 'config/service/operations.json'));
116
116
  const app = require(path.join(serviceRoot, 'src/app.js'));
117
117
 
118
118
  // Extract metadata
@@ -157,7 +157,7 @@ function createMyTests(testsDir, options = {}) {
157
157
  }
158
158
 
159
159
  // 2. Load config
160
- const config = require(path.join(serviceRoot, 'conn-config/config.json'));
160
+ const config = require(path.join(serviceRoot, 'config/service/config.json'));
161
161
 
162
162
  // 3. Create test suite
163
163
  describe('My Test Suite @integration', () => {
@@ -40,6 +40,10 @@ const { MIN_COOKBOOK_FORMAT_VERSION } = require('../utils/cookbookFormat');
40
40
  * @param {Object} [options] - Optional configuration
41
41
  * @param {boolean} [options.includeOptionalChecks=true] - Include cookbook & registry checks
42
42
  * @param {number} [options.timeout=15000] - Test timeout in ms
43
+ * @param {Object} [options.logger=console] - Logger handed to
44
+ * ServiceReadinessValidator (info/warn/error/debug). Defaults to `console`,
45
+ * which is the report channel of a jest bootstrap suite — see the comment at
46
+ * the call site and FALLBACKS_INVENTORY.md §5.6.
43
47
  *
44
48
  * @example
45
49
  * // In services/my-service/tests/bootstrap/service-readiness.test.js
@@ -71,14 +75,11 @@ function createServiceReadinessTests(testsDir, options = {}) {
71
75
  );
72
76
  }
73
77
 
74
- // Load configuration files prefer config/service/, fall back to conn-config/
75
- const resolveConfig = (filename) => {
76
- const newPath = path.join(serviceRoot, 'config', 'service', filename);
77
- const legacyPath = path.join(serviceRoot, 'conn-config', filename);
78
- return fs.existsSync(newPath) ? newPath : legacyPath;
79
- };
80
- const configPath = resolveConfig('config.json');
81
- const operationsPath = resolveConfig('operations.json');
78
+ // config/service/ is the ONLY configuration path (owner decision 2026-09-03).
79
+ // The legacy conn-config/ branch is gone — it existed in no repository, so it
80
+ // could only turn a missing file into a read error naming a retired layout.
81
+ const configPath = path.join(serviceRoot, 'config', 'service', 'config.json');
82
+ const operationsPath = path.join(serviceRoot, 'config', 'service', 'operations.json');
82
83
 
83
84
  const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
84
85
  const operations = JSON.parse(fs.readFileSync(operationsPath, 'utf-8'));
@@ -113,7 +114,15 @@ function createServiceReadinessTests(testsDir, options = {}) {
113
114
  };
114
115
  }
115
116
 
116
- const validator = new ServiceReadinessValidator({ logger: console });
117
+ // `console` is the intended logger here, not a stand-in for one that is
118
+ // unavailable: this helper builds a jest suite in a service's
119
+ // tests/bootstrap, where stdout IS the report the developer reads, and
120
+ // every one of the eight biz services calls it as
121
+ // `createServiceReadinessTests(__dirname)` from its own repository.
122
+ // A caller that has a real logger passes one; nobody is forced to invent
123
+ // one to run their tests.
124
+ // @see api/docs/standards/FALLBACKS_INVENTORY.md §5.6
125
+ const validator = new ServiceReadinessValidator({ logger: options.logger ?? console });
117
126
  const result = await validator.validateReadiness({
118
127
  name: serviceName,
119
128
  version: serviceVersion,
package/src/index.js CHANGED
@@ -20,12 +20,13 @@ const CookbookTestUtils = require('./CookbookTestUtils');
20
20
  const { ServiceStructureValidator } = require('./validators/ServiceStructureValidator');
21
21
  const ValidationProofGenerator = require('./validators/ValidationProofGenerator');
22
22
 
23
- let CookbookTestRunner;
24
- try {
25
- CookbookTestRunner = require('./CookbookTestRunner');
26
- } catch (error) {
27
- console.error('[conn-orch-validator] WARNING: Failed to load CookbookTestRunner:', error.message);
28
- }
23
+ // Required like every other export: a broken require fails the load, here,
24
+ // with its own stack. The try/catch that used to guard it printed a WARNING and
25
+ // left `CookbookTestRunner: undefined` on the exports — a fallback (§3) and
26
+ // delayed validation (§4) that could not even do what it promised, since
27
+ // ./ValidationOrchestrator below requires the same module unguarded and took
28
+ // the load down anyway. All it added was a misleading line before the real one.
29
+ const CookbookTestRunner = require('./CookbookTestRunner');
29
30
  const WorkflowTestRunner = require('./WorkflowTestRunner');
30
31
  const BizCiGateContract = require('./utils/bizCiGateContract');
31
32
 
@@ -16,25 +16,26 @@ const fs = require('fs');
16
16
  const path = require('path');
17
17
 
18
18
  /**
19
- * Try reading from new path first (config/service/), fall back to legacy (conn-config/).
19
+ * Read a configuration file from the ONE place a service may keep it:
20
+ * `config/service/`. The legacy `conn-config/` alternative was removed on
21
+ * 2026-09-03 (owner decision) — no repository carried such a directory any
22
+ * more, so accepting it only taught a shape nobody may build and let every
23
+ * finding offer a fix pointing at a retired layout.
24
+ *
20
25
  * @param {string} root - Service root
21
26
  * @param {string} filename - e.g. 'config.json'
22
27
  * @returns {string|null} File contents or null
23
28
  */
24
29
  function readConfigFile(root, filename) {
25
- const candidates = [
26
- path.join(root, 'config', 'service', filename),
27
- path.join(root, 'conn-config', filename)
28
- ];
29
- for (const p of candidates) {
30
- try { return fs.readFileSync(p, 'utf-8'); } catch { /* try next */ }
30
+ try {
31
+ return fs.readFileSync(path.join(root, 'config', 'service', filename), 'utf-8');
32
+ } catch {
33
+ return null;
31
34
  }
32
- return null;
33
35
  }
34
36
 
35
37
  function hasConfigDir(root) {
36
- return fs.existsSync(path.join(root, 'config', 'service')) ||
37
- fs.existsSync(path.join(root, 'conn-config'));
38
+ return fs.existsSync(path.join(root, 'config', 'service'));
38
39
  }
39
40
 
40
41
 
@@ -225,7 +226,7 @@ const STANDARD_LEVELS = [
225
226
  const has = (p) => fs.existsSync(path.join(root, p));
226
227
  const read = (p) => { try { return fs.readFileSync(path.join(root, p), 'utf-8'); } catch { return null; } };
227
228
 
228
- results.push({ passed: hasConfigDir(root), id: 'dir_conn_config', message: 'config/service/ or conn-config/ directory' });
229
+ results.push({ passed: hasConfigDir(root), id: 'dir_config_service', message: 'config/service/ directory' });
229
230
  results.push({ passed: has('src'), id: 'dir_src', message: 'src/ directory' });
230
231
  results.push({ passed: has('tests'), id: 'dir_tests', message: 'tests/ directory' });
231
232
  // ADR 0005: src/handlers/ replaces src/app.js. Flat handlers/ or
@@ -449,8 +450,8 @@ class ServiceStructureValidator {
449
450
  if (!hasConfigDir(this.serviceRoot)) {
450
451
  this.errors.push({
451
452
  type: 'MISSING_DIRECTORY',
452
- path: 'config/service/ (or conn-config/)',
453
- message: 'Required directory missing: config/service/ (or legacy conn-config/)',
453
+ path: 'config/service/',
454
+ message: 'Required directory missing: config/service/',
454
455
  description: 'Configuration directory',
455
456
  fix: 'Create directory: mkdir -p config/service'
456
457
  });
@@ -501,8 +502,8 @@ class ServiceStructureValidator {
501
502
  if (!configRaw) {
502
503
  this.errors.push({
503
504
  type: 'MISSING_CONFIG',
504
- path: 'config/service/config.json (or conn-config/config.json)',
505
- message: 'Service configuration missing: config/service/config.json (or conn-config/config.json)',
505
+ path: 'config/service/config.json',
506
+ message: 'Service configuration missing: config/service/config.json',
506
507
  fix: 'Create config.json with service metadata. See: /docs/biz/60-templates/service-template.md'
507
508
  });
508
509
  } else {
@@ -524,8 +525,8 @@ class ServiceStructureValidator {
524
525
  if (!opsRaw) {
525
526
  this.errors.push({
526
527
  type: 'MISSING_OPERATIONS',
527
- path: 'config/service/operations.json (or conn-config/operations.json)',
528
- message: 'Operations specification missing: config/service/operations.json (or conn-config/operations.json)',
528
+ path: 'config/service/operations.json',
529
+ message: 'Operations specification missing: config/service/operations.json',
529
530
  fix: 'Create operations.json. See: /docs/biz/30-operations/schema-v3.md'
530
531
  });
531
532
  } else {