@onlineapps/conn-orch-validator 9.0.0 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +546 -0
- package/README.md +337 -19
- package/docs/DESIGN.md +32 -9
- package/manifests/biz-service.manifest.json +56 -6
- package/package.json +3 -2
- package/src/CookbookTestRunner.js +134 -22
- package/src/ValidationOrchestrator.js +312 -73
- package/src/cli/biz-ci-gate.js +191 -15
- package/src/cli/oa-sync-template.js +23 -8
- package/src/cli/oa-validate.js +70 -2
- package/src/index.js +21 -13
- package/src/lint/scripts/lintScripts.js +65 -18
- package/src/manifest/checks/composeRunnerBlock.js +37 -20
- package/src/manifest/checks/discoveryOrphan.js +2 -1
- package/src/manifest/checks/docsLintBridge.js +79 -21
- package/src/manifest/checks/gitTracked.js +14 -28
- package/src/manifest/checks/libraryPackage.js +3 -1
- package/src/manifest/checks/libraryWorkspace.js +18 -3
- package/src/manifest/checks/readmeRegion.js +9 -1
- package/src/manifest/checks/serviceConfig.js +29 -12
- package/src/manifest/checks/serviceDb.js +176 -7
- package/src/manifest/checks/serviceFiles.js +34 -7
- package/src/manifest/checks/serviceIdentityRows.js +3 -1
- package/src/manifest/checks/serviceRuntime.js +126 -1
- package/src/manifest/discovery.js +25 -7
- package/src/manifest/gitCheckout.js +84 -0
- package/src/manifest/runManifest.js +58 -7
- package/src/manifest/workspaceRoot.js +91 -5
- package/src/sync/serviceTemplate.js +76 -7
- package/src/sync/sharedEnv.js +11 -4
- package/src/sync/uniformFiles.js +91 -21
- package/src/utils/bizCiGateContract.js +25 -1
- package/src/utils/dbAccountGrants.js +126 -0
- package/src/utils/envContract.js +36 -6
- package/src/utils/envReads.js +102 -0
- package/src/utils/installContract.js +46 -5
- package/src/utils/libCompat.js +39 -19
- package/src/utils/preValidation.js +56 -11
- package/src/utils/stepFailure.js +106 -19
- package/src/utils/stepReferences.js +278 -0
- package/src/utils/testCoverageContract.js +60 -2
- package/src/utils/throwawaySchema.js +92 -7
- package/src/validatorIdentity.js +31 -0
- package/src/validators/ServiceStructureValidator.js +47 -15
- package/src/validators/ValidationProofGenerator.js +73 -34
- package/templates/business-service/.dockerignore +9 -1
- package/templates/business-service/.gitlab-ci.yml +199 -35
- package/templates/business-service/Dockerfile +49 -16
- package/templates/business-service/README.md +56 -9
- package/templates/business-service/config/env-templates/__SERVICE_NAME__.env +17 -5
- package/templates/business-service/config/env-templates/shared.env +8 -2
- package/templates/business-service/docker-compose.production.yml +9 -0
- package/templates/business-service/docker-compose.yml +17 -0
- package/templates/business-service/docs/80-setup/PLATFORM_MATRIX.md +1 -1
- package/templates/business-service/docs/80-setup/VALIDATION.md +1 -1
- package/templates/business-service/jest.config.js +9 -1
- package/templates/business-service/package.json.template +1 -1
- package/src/mocks/MockStorage.js +0 -188
|
@@ -8,10 +8,13 @@ const { normalizeEnvDeclaration, collectEnvCoverage, verifyEnvPresence } = requi
|
|
|
8
8
|
// called. It verified a cached proof before booting; the proof cache was
|
|
9
9
|
// removed (see validate()) and the verifier's last call site went with it,
|
|
10
10
|
// leaving a binding whose only remaining effect was to tell a reader that this
|
|
11
|
-
// class still checks proofs. `ValidationProofCodec`
|
|
12
|
-
// encodes
|
|
13
|
-
|
|
11
|
+
// class still checks proofs. `ValidationProofCodec` went the same way in d.524:
|
|
12
|
+
// finalizeResults() no longer encodes a proof of its own — it asks
|
|
13
|
+
// `ValidationProofGenerator`, which is the one author of one, and the codec is
|
|
14
|
+
// that generator's business.
|
|
14
15
|
const FingerprintUtils = require('@onlineapps/service-validator-core').FingerprintUtils;
|
|
16
|
+
const ValidationProofGenerator = require('./validators/ValidationProofGenerator');
|
|
17
|
+
const { VALIDATOR_VERSION } = require('./validatorIdentity');
|
|
15
18
|
const { ServiceStructureValidator } = require('./validators/ServiceStructureValidator');
|
|
16
19
|
const { describeStepFailureWithContext } = require('./utils/stepFailure');
|
|
17
20
|
const CookbookTestRunner = require('./CookbookTestRunner');
|
|
@@ -22,6 +25,7 @@ const { runManifest } = require('./manifest/runManifest');
|
|
|
22
25
|
const { resolveWorkspaceRoot } = require('./manifest/workspaceRoot');
|
|
23
26
|
const { renderBanner, describeFinding } = require('./manifest/report');
|
|
24
27
|
const { buildDeployabilitySignal, writeDeployabilitySignal } = require('./manifest/deployabilitySignal');
|
|
28
|
+
const { isGitCheckout, NOT_A_CHECKOUT } = require('./manifest/gitCheckout');
|
|
25
29
|
|
|
26
30
|
/**
|
|
27
31
|
* The manifest rows step 2 answers: the configuration documents a biz service is
|
|
@@ -38,6 +42,94 @@ const { buildDeployabilitySignal, writeDeployabilitySignal } = require('./manife
|
|
|
38
42
|
*/
|
|
39
43
|
const CONFIG_STEP_ROWS = Object.freeze(['C-SERVICE', 'C-OPS', 'C-CONTRACT']);
|
|
40
44
|
|
|
45
|
+
/**
|
|
46
|
+
* One number a step MEASURED — or a refusal.
|
|
47
|
+
*
|
|
48
|
+
* The run's `totalTests`/`passedTests`/`failedTests` used to be summed with
|
|
49
|
+
* `|| 0`, so a step that measured nothing contributed a zero and the aggregate
|
|
50
|
+
* could not tell "no tests ran" from "the step did not say". The proof's author
|
|
51
|
+
* refuses an unmeasured field (`ValidationProofGenerator.readMeasurement`,
|
|
52
|
+
* d.524), but it never saw one: the fallback had already turned it into a
|
|
53
|
+
* number. That is `.claude/rules/architecture-principles.md` §3 (No Fallbacks)
|
|
54
|
+
* standing exactly where §4 wants a refusal, and the far end paid for it —
|
|
55
|
+
* `ValidationProofCodec.decode()` rejects the resulting proof as NO_TESTS, days
|
|
56
|
+
* later and nowhere near the cause.
|
|
57
|
+
*
|
|
58
|
+
* A measured 0 is untouched: zero failures is the ordinary case. What is
|
|
59
|
+
* refused is the ABSENCE of a measurement.
|
|
60
|
+
*
|
|
61
|
+
* @param {Object} step the step result the aggregate is summing
|
|
62
|
+
* @param {string} field `total`, `passed` or `failed`
|
|
63
|
+
* @param {string} stepName the step's name, for the refusal
|
|
64
|
+
* @returns {number} the measured value
|
|
65
|
+
*/
|
|
66
|
+
function readStepMeasurement(step, field, stepName) {
|
|
67
|
+
const value = step === null || step === undefined ? undefined : step[field];
|
|
68
|
+
|
|
69
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
70
|
+
throw new Error(`[ValidationOrchestrator] Missing measurement - step "${stepName}" reported `
|
|
71
|
+
+ `${JSON.stringify(value) === undefined ? 'undefined' : JSON.stringify(value)} for "${field}", `
|
|
72
|
+
+ 'and the run counts measured values only - a zero standing in for an absent measurement is '
|
|
73
|
+
+ 'what makes a proof of nothing look like a passing validation. '
|
|
74
|
+
+ `Fix: return a finite number as "${field}" from step "${stepName}".`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The step that produced a finding, carried ON the finding.
|
|
82
|
+
*
|
|
83
|
+
* `results.errors` is one flat array: step 1 pushes objects
|
|
84
|
+
* `{ type, path, message, fix }` and steps 2-7 push sentences they have already
|
|
85
|
+
* written in the `[Context] Problem - Expected/Fix` shape. Nothing in it said
|
|
86
|
+
* WHICH step a finding came from, so the only reader that needs to know — the
|
|
87
|
+
* wrapper, which retries a transient step and refuses a static one — had to read
|
|
88
|
+
* `results.steps` instead and could say nothing about an individual finding.
|
|
89
|
+
*
|
|
90
|
+
* The marker is ADDED, never substituted: a sentence stays the same sentence and
|
|
91
|
+
* an object keeps every field it had, because `describeValidationFinding()` of
|
|
92
|
+
* `@onlineapps/service-wrapper` 9.0.x renders a string verbatim and an object
|
|
93
|
+
* only when it carries `type` and `message` — anything else reaches the operator
|
|
94
|
+
* as raw JSON. Rewriting the sentences into `{ step, message }` objects would
|
|
95
|
+
* therefore break the released wrapper, so the sentences keep their shape until
|
|
96
|
+
* a wrapper that reads `step` is published (d.602; the report to BIZ-general).
|
|
97
|
+
*
|
|
98
|
+
* The findings are COPIED rather than stamped in place: the step's own
|
|
99
|
+
* `results.steps.<step>.errors` is its answer and stays untouched.
|
|
100
|
+
*
|
|
101
|
+
* @param {Array<(string|object)>} findings what a step reported
|
|
102
|
+
* @param {string} step the step's key in `results.steps`
|
|
103
|
+
* @returns {Array<(string|object)>} the same findings, objects carrying `step`
|
|
104
|
+
*/
|
|
105
|
+
function attributeToStep(findings, step) {
|
|
106
|
+
if (!Array.isArray(findings)) {
|
|
107
|
+
// The TypeError this replaces reported itself instead of the finding: step 6
|
|
108
|
+
// spread a field the step does not have and aborted the whole run with
|
|
109
|
+
// "results.steps.connectors.warnings is not iterable" (F12, 2026-08-22).
|
|
110
|
+
throw new Error(`[ValidationOrchestrator] Step "${step}" reported findings that are not an array - `
|
|
111
|
+
+ `got ${JSON.stringify(findings)}, expected an array. `
|
|
112
|
+
+ `Fix: return "errors" as an array from step "${step}".`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return findings.map((finding) => (
|
|
116
|
+
finding !== null && typeof finding === 'object' ? { ...finding, step } : finding
|
|
117
|
+
));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The finding of a step whose body THREW — an object, so that it carries `step`,
|
|
122
|
+
* and with `type` so the released wrapper renders it as a sentence rather than
|
|
123
|
+
* as JSON.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} step the step's key in `results.steps`
|
|
126
|
+
* @param {string} message the failure, in the `[Context] Problem - Fix` shape
|
|
127
|
+
* @returns {{type: string, step: string, message: string}}
|
|
128
|
+
*/
|
|
129
|
+
function stepThrewFinding(step, message) {
|
|
130
|
+
return { type: 'STEP_THREW', step, message };
|
|
131
|
+
}
|
|
132
|
+
|
|
41
133
|
/**
|
|
42
134
|
* ValidationOrchestrator
|
|
43
135
|
*
|
|
@@ -345,22 +437,27 @@ class ValidationOrchestrator {
|
|
|
345
437
|
|
|
346
438
|
try {
|
|
347
439
|
// Step 1: Service Structure
|
|
348
|
-
this.
|
|
349
|
-
|
|
350
|
-
|
|
440
|
+
const structureFailed = await this.runStep(results, 'structure', async () => {
|
|
441
|
+
this.logger.info('[ValidationOrchestrator] Step 1/7: Service Structure');
|
|
442
|
+
results.steps.structure = await this.validateStructure();
|
|
443
|
+
if (results.steps.structure.valid) return false;
|
|
444
|
+
|
|
351
445
|
results.success = false;
|
|
352
|
-
results.errors.push(...results.steps.structure.errors);
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
446
|
+
results.errors.push(...attributeToStep(results.steps.structure.errors, 'structure'));
|
|
447
|
+
return true;
|
|
448
|
+
});
|
|
449
|
+
// Fail fast - can't continue without proper structure
|
|
450
|
+
if (structureFailed) return this.finalizeResults(results, startTime);
|
|
356
451
|
|
|
357
452
|
// Step 2: Config Files
|
|
358
|
-
this.
|
|
359
|
-
|
|
360
|
-
|
|
453
|
+
await this.runStep(results, 'config', async () => {
|
|
454
|
+
this.logger.info('[ValidationOrchestrator] Step 2/7: Config Files');
|
|
455
|
+
results.steps.config = await this.validateConfig();
|
|
456
|
+
if (results.steps.config.valid) return;
|
|
457
|
+
|
|
361
458
|
results.success = false;
|
|
362
|
-
results.errors.push(...results.steps.config.errors);
|
|
363
|
-
}
|
|
459
|
+
results.errors.push(...attributeToStep(results.steps.config.errors, 'config'));
|
|
460
|
+
});
|
|
364
461
|
|
|
365
462
|
// Step 3: Environment Contract
|
|
366
463
|
//
|
|
@@ -369,40 +466,53 @@ class ValidationOrchestrator {
|
|
|
369
466
|
// start without must fail here, where its name and the reason it exists
|
|
370
467
|
// are both at hand. SECRETS_MASTER_KEY used to fail after MQ
|
|
371
468
|
// registration, several layers away from the declaration (defect D2).
|
|
372
|
-
this.
|
|
373
|
-
|
|
374
|
-
|
|
469
|
+
const envFailed = await this.runStep(results, 'env', () => {
|
|
470
|
+
this.logger.info('[ValidationOrchestrator] Step 3/7: Environment Contract');
|
|
471
|
+
results.steps.env = this.validateEnvContract();
|
|
472
|
+
if (results.steps.env.valid) return false;
|
|
473
|
+
|
|
375
474
|
results.success = false;
|
|
376
|
-
results.errors.push(...results.steps.env.errors);
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
475
|
+
results.errors.push(...attributeToStep(results.steps.env.errors, 'env'));
|
|
476
|
+
return true;
|
|
477
|
+
});
|
|
478
|
+
// Fail fast — running handlers without the environment they declared
|
|
479
|
+
// produces a second, less honest error somewhere further in.
|
|
480
|
+
if (envFailed) return this.finalizeResults(results, startTime);
|
|
381
481
|
|
|
382
482
|
// Step 4: Operations Compliance
|
|
383
|
-
this.
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
483
|
+
await this.runStep(results, 'operations', async () => {
|
|
484
|
+
this.logger.info('[ValidationOrchestrator] Step 4/7: Operations Compliance');
|
|
485
|
+
results.steps.operations = await this.validateOperations();
|
|
486
|
+
results.warnings.push(...attributeToStep(results.steps.operations.warnings || [], 'operations'));
|
|
487
|
+
if (results.steps.operations.valid) return;
|
|
488
|
+
|
|
387
489
|
results.success = false;
|
|
388
|
-
results.errors.push(...results.steps.operations.errors);
|
|
389
|
-
}
|
|
490
|
+
results.errors.push(...attributeToStep(results.steps.operations.errors, 'operations'));
|
|
491
|
+
});
|
|
390
492
|
|
|
391
493
|
// Step 5: Cookbook Tests
|
|
392
|
-
this.
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
494
|
+
await this.runStep(results, 'cookbooks', async () => {
|
|
495
|
+
this.logger.info('[ValidationOrchestrator] Step 5/7: Cookbook Tests');
|
|
496
|
+
results.steps.cookbooks = await this.runCookbookTests();
|
|
497
|
+
// Measured values only — see readStepMeasurement() for why `|| 0` here
|
|
498
|
+
// was the reason the generator's refusal never fired. The refusal now
|
|
499
|
+
// fails THIS step by name instead of surfacing as a step-less
|
|
500
|
+
// "Validation error" (d.602).
|
|
501
|
+
results.totalTests += readStepMeasurement(results.steps.cookbooks, 'total', 'cookbooks');
|
|
502
|
+
results.passedTests += readStepMeasurement(results.steps.cookbooks, 'passed', 'cookbooks');
|
|
503
|
+
results.failedTests += readStepMeasurement(results.steps.cookbooks, 'failed', 'cookbooks');
|
|
504
|
+
if (results.steps.cookbooks.success) return;
|
|
505
|
+
|
|
398
506
|
results.success = false;
|
|
399
|
-
results.errors.push(...(results.steps.cookbooks.errors || []));
|
|
400
|
-
}
|
|
507
|
+
results.errors.push(...attributeToStep(results.steps.cookbooks.errors || [], 'cookbooks'));
|
|
508
|
+
});
|
|
401
509
|
|
|
402
510
|
// Step 6: Connector Integration
|
|
403
|
-
this.
|
|
404
|
-
|
|
405
|
-
|
|
511
|
+
await this.runStep(results, 'connectors', () => {
|
|
512
|
+
this.logger.info('[ValidationOrchestrator] Step 6/7: Connector Integration');
|
|
513
|
+
results.steps.connectors = this.validateConnectors();
|
|
514
|
+
if (results.steps.connectors.valid) return;
|
|
515
|
+
|
|
406
516
|
// Severity is unchanged: the connector contract stays non-critical and
|
|
407
517
|
// its findings are reported as warnings, not as validation errors.
|
|
408
518
|
//
|
|
@@ -413,8 +523,8 @@ class ValidationOrchestrator {
|
|
|
413
523
|
// — threw "results.steps.connectors.warnings is not iterable", aborted
|
|
414
524
|
// the whole run, and reported that TypeError in place of every finding.
|
|
415
525
|
// Reproduced against biz-pdfgen and biz-property on 2026-08-22.
|
|
416
|
-
results.warnings.push(...results.steps.connectors.errors);
|
|
417
|
-
}
|
|
526
|
+
results.warnings.push(...attributeToStep(results.steps.connectors.errors, 'connectors'));
|
|
527
|
+
});
|
|
418
528
|
|
|
419
529
|
// Step 7: Manifest conformance
|
|
420
530
|
//
|
|
@@ -427,15 +537,17 @@ class ValidationOrchestrator {
|
|
|
427
537
|
// the service running and marks it undeployable — the banner says so at
|
|
428
538
|
// the end of this log and `ci/deployability.json` says so to
|
|
429
539
|
// `deploy-production`.
|
|
430
|
-
this.
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
540
|
+
await this.runStep(results, 'manifest', () => {
|
|
541
|
+
this.logger.info('[ValidationOrchestrator] Step 7/7: Manifest conformance');
|
|
542
|
+
results.steps.manifest = this.validateManifestConformance();
|
|
543
|
+
results.deployable = results.steps.manifest.deployable;
|
|
544
|
+
results.deployFindings = results.steps.manifest.deployFindings;
|
|
545
|
+
results.notRun = results.steps.manifest.notRun;
|
|
546
|
+
if (results.steps.manifest.valid) return;
|
|
547
|
+
|
|
436
548
|
results.success = false;
|
|
437
|
-
results.errors.push(...results.steps.manifest.errors);
|
|
438
|
-
}
|
|
549
|
+
results.errors.push(...attributeToStep(results.steps.manifest.errors, 'manifest'));
|
|
550
|
+
});
|
|
439
551
|
|
|
440
552
|
// Finalize and generate proof if successful
|
|
441
553
|
return await this.finalizeResults(results, startTime);
|
|
@@ -443,11 +555,74 @@ class ValidationOrchestrator {
|
|
|
443
555
|
} catch (error) {
|
|
444
556
|
this.logger.error(`[ValidationOrchestrator] Validation failed: ${error.message}`);
|
|
445
557
|
results.success = false;
|
|
446
|
-
|
|
558
|
+
// A failure that belongs to NO step — `finalizeResults` is the only code
|
|
559
|
+
// left out here. Everything raised inside a step is already recorded under
|
|
560
|
+
// that step's name by runStep(), which marks the error on its way out, so
|
|
561
|
+
// adding this sentence as well would report one failure twice and the
|
|
562
|
+
// second copy would carry no step at all (d.602).
|
|
563
|
+
if (typeof error.validationStep !== 'string') {
|
|
564
|
+
results.errors.push(`Validation error: ${error.message}`);
|
|
565
|
+
}
|
|
447
566
|
return this.finalizeResults(results, startTime);
|
|
448
567
|
}
|
|
449
568
|
}
|
|
450
569
|
|
|
570
|
+
/**
|
|
571
|
+
* Run one step of the validation and GUARANTEE its record in `results.steps`.
|
|
572
|
+
*
|
|
573
|
+
* Each step method catches what its own work raises and answers
|
|
574
|
+
* `{ valid: false, … }` by its own name. This is the net under all of them: an
|
|
575
|
+
* exception from the logger, from the aggregate that reads a step's
|
|
576
|
+
* measurements, or from a step's findings being something other than an array
|
|
577
|
+
* used to escape to the catch of runFullValidation(), which pushed
|
|
578
|
+
* `Validation error: <message>` and left `results.steps.<step>` ABSENT.
|
|
579
|
+
*
|
|
580
|
+
* That absence is not a cosmetic gap. `@onlineapps/service-wrapper` 9.0.x
|
|
581
|
+
* decides whether a failed FÁZE 0.2 may be retried by reading
|
|
582
|
+
* `result.steps.<step>.valid` (its `STATIC_VALIDATION_STEPS`), and a step with
|
|
583
|
+
* no record reads as "the validator threw" — transient — so a structural
|
|
584
|
+
* defect bought the full six-attempt budget: 18,5 minutes of a boot that
|
|
585
|
+
* cannot succeed (measured on biz-converter, W591).
|
|
586
|
+
*
|
|
587
|
+
* The run still STOPS where it stopped before: the error is re-thrown, marked,
|
|
588
|
+
* and the outer catch finalizes exactly as it did.
|
|
589
|
+
*
|
|
590
|
+
* @param {object} results the run being assembled
|
|
591
|
+
* @param {string} step the step's key in `results.steps`
|
|
592
|
+
* @param {function(): *} block the step, its harvest included
|
|
593
|
+
* @returns {Promise<*>} whatever the block returned
|
|
594
|
+
*/
|
|
595
|
+
async runStep(results, step, block) {
|
|
596
|
+
try {
|
|
597
|
+
return await block();
|
|
598
|
+
} catch (error) {
|
|
599
|
+
const finding = stepThrewFinding(step,
|
|
600
|
+
`[ValidationOrchestrator] Step "${step}" threw - ${error.message} `
|
|
601
|
+
+ `Fix: repair what that message names; step "${step}" reached no verdict of its own.`);
|
|
602
|
+
|
|
603
|
+
// What the step had already measured is KEPT: a throw during the harvest
|
|
604
|
+
// must not erase the counts or findings the step did produce.
|
|
605
|
+
const measured = results.steps[step];
|
|
606
|
+
const partial = measured !== null && typeof measured === 'object' ? measured : {};
|
|
607
|
+
|
|
608
|
+
results.steps[step] = {
|
|
609
|
+
...partial,
|
|
610
|
+
// Both fields, because this net does not know which one the step
|
|
611
|
+
// answers with: steps 1-4, 6 and 7 report `valid`, step 5 reports
|
|
612
|
+
// `success`, and a reader of either must see the failure.
|
|
613
|
+
valid: false,
|
|
614
|
+
success: false,
|
|
615
|
+
threw: true,
|
|
616
|
+
errors: [...(Array.isArray(partial.errors) ? partial.errors : []), finding]
|
|
617
|
+
};
|
|
618
|
+
results.success = false;
|
|
619
|
+
results.errors.push(finding);
|
|
620
|
+
|
|
621
|
+
error.validationStep = step;
|
|
622
|
+
throw error;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
451
626
|
/**
|
|
452
627
|
* Step 1: Validate service structure
|
|
453
628
|
*/
|
|
@@ -460,7 +635,8 @@ class ValidationOrchestrator {
|
|
|
460
635
|
} catch (error) {
|
|
461
636
|
return {
|
|
462
637
|
valid: false,
|
|
463
|
-
|
|
638
|
+
threw: true,
|
|
639
|
+
errors: [stepThrewFinding('structure', `Structure validation failed: ${error.message}`)]
|
|
464
640
|
};
|
|
465
641
|
}
|
|
466
642
|
}
|
|
@@ -499,7 +675,8 @@ class ValidationOrchestrator {
|
|
|
499
675
|
this.logger.error(`[ValidationOrchestrator] Step 2/7 could not run: ${error.message}`);
|
|
500
676
|
return {
|
|
501
677
|
valid: false,
|
|
502
|
-
|
|
678
|
+
threw: true,
|
|
679
|
+
errors: [stepThrewFinding('config', error.message)],
|
|
503
680
|
findings: []
|
|
504
681
|
};
|
|
505
682
|
}
|
|
@@ -582,7 +759,8 @@ class ValidationOrchestrator {
|
|
|
582
759
|
} catch (error) {
|
|
583
760
|
return {
|
|
584
761
|
valid: false,
|
|
585
|
-
|
|
762
|
+
threw: true,
|
|
763
|
+
errors: [stepThrewFinding('operations', `Operations validation failed: ${error.message}`)],
|
|
586
764
|
warnings: []
|
|
587
765
|
};
|
|
588
766
|
}
|
|
@@ -643,11 +821,12 @@ class ValidationOrchestrator {
|
|
|
643
821
|
} catch (error) {
|
|
644
822
|
return {
|
|
645
823
|
success: false,
|
|
824
|
+
threw: true,
|
|
646
825
|
total: 0,
|
|
647
826
|
passed: 0,
|
|
648
827
|
failed: 0,
|
|
649
828
|
cookbooks: { total: 0, passed: 0, failed: 0 },
|
|
650
|
-
errors: [`Cookbook tests failed: ${error.message}`]
|
|
829
|
+
errors: [stepThrewFinding('cookbooks', `Cookbook tests failed: ${error.message}`)]
|
|
651
830
|
};
|
|
652
831
|
}
|
|
653
832
|
}
|
|
@@ -690,7 +869,7 @@ class ValidationOrchestrator {
|
|
|
690
869
|
+ `no ${path.relative(this.serviceRoot, contractFile)} in this service`);
|
|
691
870
|
return { valid: true, skipped: true, errors: [] };
|
|
692
871
|
}
|
|
693
|
-
return { valid: false, errors: [error.message] };
|
|
872
|
+
return { valid: false, threw: true, errors: [stepThrewFinding('env', error.message)] };
|
|
694
873
|
}
|
|
695
874
|
|
|
696
875
|
if (declaration === null) {
|
|
@@ -726,7 +905,12 @@ class ValidationOrchestrator {
|
|
|
726
905
|
config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
727
906
|
requiredConnectors = JSON.parse(fs.readFileSync(contractFile, 'utf8')).requiredConnectors || {};
|
|
728
907
|
} catch (error) {
|
|
729
|
-
return {
|
|
908
|
+
return {
|
|
909
|
+
valid: false,
|
|
910
|
+
threw: true,
|
|
911
|
+
errors: [stepThrewFinding('connectors',
|
|
912
|
+
`[ConnectorContract] Cannot read the connector declarations - ${error.message}`)]
|
|
913
|
+
};
|
|
730
914
|
}
|
|
731
915
|
|
|
732
916
|
const result = verifyConnectorContract({ config, requiredConnectors, env: process.env });
|
|
@@ -748,18 +932,59 @@ class ValidationOrchestrator {
|
|
|
748
932
|
* the signal carries them (`automation-gates.md` §5). There is no switch that
|
|
749
933
|
* skips this step and none that moves the signal: one path, unbypassable.
|
|
750
934
|
*
|
|
935
|
+
* OUTSIDE A GIT CHECKOUT THE STEP DOES NOT MEASURE. Owner decision 2026-09-17
|
|
936
|
+
* (confirmation `biz-service-manifest` 011): a production image is not a
|
|
937
|
+
* checkout and carries neither `docker-compose.yml` nor `README.md`, so the
|
|
938
|
+
* uniform measured there reports absences that say nothing about the
|
|
939
|
+
* repository — 12 findings for a healthy service. A verdict reached that way
|
|
940
|
+
* is not a strict check, it is a wrong answer, and `NOT DEPLOYABLE` printed
|
|
941
|
+
* from it discredits the rows that ARE true. What proves deployability is the
|
|
942
|
+
* CI job `validate-uniform`, from every commit, over the checkout
|
|
943
|
+
* (confirmation 010); a running container cannot prove it and no longer
|
|
944
|
+
* pretends to. The step then says NOT RUN in one line and `deployable` stays
|
|
945
|
+
* `null` — NOT MEASURED, which is a different statement from `false` and is
|
|
946
|
+
* why that field has three states.
|
|
947
|
+
*
|
|
751
948
|
* Order inside the step is deliberate: the banner is printed BEFORE the file
|
|
752
949
|
* is written, so a service root that cannot be written to still leaves the
|
|
753
950
|
* human-readable verdict in the log.
|
|
754
951
|
*
|
|
755
952
|
* @see api/docs/governance/confirmations/biz-service-manifest.md §4
|
|
756
|
-
* @returns {{valid: boolean, deployable: boolean, errors: string[], findings: object[],
|
|
953
|
+
* @returns {{valid: boolean, deployable: (boolean|null), errors: string[], findings: object[],
|
|
757
954
|
* deployFindings: object[], notRun: object[], signalPath: string|null}}
|
|
758
955
|
*/
|
|
759
956
|
validateManifestConformance() {
|
|
760
957
|
try {
|
|
958
|
+
// The run happens first and costs nothing extra: step 2 has already asked
|
|
959
|
+
// for it (`manifestRun()` is computed once per validation), and a service
|
|
960
|
+
// root that vanished must still fail by its own name rather than be
|
|
961
|
+
// reported as "not a checkout".
|
|
761
962
|
const result = this.manifestRun();
|
|
762
963
|
|
|
964
|
+
if (!isGitCheckout(this.serviceRoot)) {
|
|
965
|
+
// One line, and no table: naming the findings of a tree this run cannot
|
|
966
|
+
// read would be the false guarantee of `automation-gates.md` §5 in
|
|
967
|
+
// reverse — a reader acting on `README.md missing` inside a container
|
|
968
|
+
// would go looking for a file the image is not supposed to carry.
|
|
969
|
+
this.logger.info(`[ValidationOrchestrator] NOT RUN manifest conformance — ${NOT_A_CHECKOUT} `
|
|
970
|
+
+ 'Deployability is proven per commit by the CI job `validate-uniform` over the checkout '
|
|
971
|
+
+ '(confirmation biz-service-manifest 010).');
|
|
972
|
+
|
|
973
|
+
// No signal either: `ci/deployability.json` IS a verdict, and this run
|
|
974
|
+
// reached none. Writing one would hand `deploy-production` a file that
|
|
975
|
+
// looks like an answer. The checkout path writes it
|
|
976
|
+
// (`src/cli/oa-validate.js`), which is where the answer exists.
|
|
977
|
+
return {
|
|
978
|
+
valid: true,
|
|
979
|
+
deployable: null,
|
|
980
|
+
errors: [],
|
|
981
|
+
findings: [],
|
|
982
|
+
deployFindings: [],
|
|
983
|
+
notRun: [],
|
|
984
|
+
signalPath: null
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
763
988
|
// The banner and `ci/deployability.json` describe the WHOLE uniform,
|
|
764
989
|
// CONFIG_STEP_ROWS included: the verdict is about the tree, and a reader
|
|
765
990
|
// (or `deploy-production`) must not conclude it is clean because the boot
|
|
@@ -773,7 +998,7 @@ class ValidationOrchestrator {
|
|
|
773
998
|
serviceRoot: this.serviceRoot,
|
|
774
999
|
signal: buildDeployabilitySignal({
|
|
775
1000
|
result,
|
|
776
|
-
validatorVersion:
|
|
1001
|
+
validatorVersion: VALIDATOR_VERSION
|
|
777
1002
|
})
|
|
778
1003
|
});
|
|
779
1004
|
|
|
@@ -804,8 +1029,9 @@ class ValidationOrchestrator {
|
|
|
804
1029
|
this.logger.error(`[ValidationOrchestrator] Step 7/7 could not run: ${error.message}`);
|
|
805
1030
|
return {
|
|
806
1031
|
valid: false,
|
|
1032
|
+
threw: true,
|
|
807
1033
|
deployable: false,
|
|
808
|
-
errors: [error.message],
|
|
1034
|
+
errors: [stepThrewFinding('manifest', error.message)],
|
|
809
1035
|
findings: [],
|
|
810
1036
|
deployFindings: [],
|
|
811
1037
|
notRun: [],
|
|
@@ -825,21 +1051,34 @@ class ValidationOrchestrator {
|
|
|
825
1051
|
try {
|
|
826
1052
|
// Generate validation proof
|
|
827
1053
|
const fingerprint = await this.calculateFingerprint();
|
|
828
|
-
|
|
1054
|
+
|
|
1055
|
+
// Through the generator, which is the ONE author of a proof. Built here
|
|
1056
|
+
// by hand and handed straight to the codec, this proof escaped both of
|
|
1057
|
+
// the generator's refusals: an unmeasured field (`readMeasurement`) and
|
|
1058
|
+
// a run that executed nothing
|
|
1059
|
+
// (`.claude/rules/change-discipline.md` § One rail per concern).
|
|
1060
|
+
//
|
|
1061
|
+
// The numbers reaching it are measured ones: the aggregate above sums
|
|
1062
|
+
// through `readStepMeasurement()`, so an absent measurement fails the
|
|
1063
|
+
// run here rather than arriving as a zero the generator cannot tell
|
|
1064
|
+
// from a real one (d.576b).
|
|
1065
|
+
const generator = new ValidationProofGenerator({
|
|
829
1066
|
serviceName: this.serviceName,
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
validator: '@onlineapps/conn-orch-validator',
|
|
833
|
-
validatorVersion: require('../package.json').version,
|
|
834
|
-
validatedAt: new Date().toISOString(),
|
|
835
|
-
testsRun: results.totalTests,
|
|
836
|
-
testsPassed: results.passedTests,
|
|
837
|
-
testsFailed: results.failedTests,
|
|
838
|
-
durationMs: duration
|
|
839
|
-
};
|
|
1067
|
+
serviceVersion: this.serviceVersion
|
|
1068
|
+
});
|
|
840
1069
|
|
|
841
|
-
|
|
842
|
-
|
|
1070
|
+
const pkg = this.readServicePackageJson(path.join(this.serviceRoot, 'package.json'));
|
|
1071
|
+
|
|
1072
|
+
const encodedProof = generator.generateProof(
|
|
1073
|
+
{
|
|
1074
|
+
total: results.totalTests,
|
|
1075
|
+
passed: results.passedTests,
|
|
1076
|
+
failed: results.failedTests,
|
|
1077
|
+
duration
|
|
1078
|
+
},
|
|
1079
|
+
pkg.dependencies || {},
|
|
1080
|
+
{ contractFingerprint: fingerprint }
|
|
1081
|
+
);
|
|
843
1082
|
|
|
844
1083
|
// Save proof to conn-runtime/
|
|
845
1084
|
await this.saveProof(encodedProof);
|