@onlineapps/conn-orch-validator 12.1.1 → 13.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 +607 -0
- package/README.md +126 -19
- package/manifests/biz-service.manifest.json +15 -2
- package/manifests/library.manifest.json +4 -4
- package/package.json +11 -3
- package/src/CookbookTestRunner.js +275 -105
- package/src/CookbookTestUtils.js +79 -68
- package/src/ServiceReadinessValidator.js +42 -52
- package/src/ValidationOrchestrator.js +65 -44
- package/src/cli/biz-ci-gate.js +2 -2
- package/src/cli/oa-sync-template.js +97 -47
- package/src/cli/oa-validate.js +44 -10
- package/src/helpers/README.md +6 -6
- package/src/helpers/createServiceReadinessTests.js +87 -33
- package/src/index.js +14 -5
- package/src/lint/scripts/lintScripts.js +11 -4
- package/src/manifest/checks/libraryContext.js +6 -3
- package/src/manifest/checks/libraryDocs.js +174 -4
- package/src/manifest/checks/libraryTests.js +200 -19
- package/src/manifest/checks/scriptHeaders.js +6 -13
- package/src/manifest/checks/serviceConfig.js +36 -16
- package/src/manifest/checks/serviceConnectors.js +180 -2
- package/src/manifest/checks/serviceDb.js +0 -3
- package/src/manifest/checks/serviceScripts.js +3 -20
- package/src/manifest/runManifest.js +90 -13
- package/src/manifest/workspaceRoot.js +133 -4
- package/src/mocks/MockMQClient.js +2 -2
- package/src/sync/docsRegion.js +2 -2
- package/src/sync/readmeFile.js +30 -0
- package/src/sync/readmeLocation.js +2 -12
- package/src/sync/readmePointer.js +10 -4
- package/src/sync/serviceTemplate.js +9 -11
- package/src/sync/sharedEnv.js +59 -3
- package/src/sync/uniformFiles.js +81 -8
- package/src/utils/bizCiGateContract.js +2 -2
- package/src/utils/connectorContract.js +54 -2
- package/src/utils/cookbookFormat.js +25 -115
- package/src/utils/dbAccountGrants.js +5 -3
- package/src/utils/deployContract.js +153 -28
- package/src/utils/envContract.js +2 -2
- package/src/utils/handlerRef.js +8 -10
- package/src/utils/integrationRun.js +1 -1
- package/src/utils/operationsDocumentRules.js +242 -0
- package/src/utils/operationsRules.js +157 -0
- package/src/utils/resolveHeaders.js +12 -1
- package/src/utils/setupDatabase.js +1 -1
- package/src/utils/stepFailure.js +3 -3
- package/src/utils/stepReferences.js +28 -87
- package/src/utils/throwawaySchema.js +1 -1
- package/src/utils/yamlTopLevel.js +105 -0
- package/src/validators/ServiceStructureValidator.js +67 -152
- package/templates/business-service/.gitlab-ci.yml +203 -37
- package/templates/business-service/README.md +7 -4
- package/templates/business-service/config/env-templates/shared.env +1 -0
- package/templates/business-service/docs/80-setup/INSTALL.md +31 -3
- package/templates/business-service/docs/80-setup/PLATFORM_MATRIX.md +5 -2
- package/templates/business-service/src/config/index.js +15 -0
- package/TESTING_STRATEGY.md +0 -92
- package/jest.config.js +0 -37
package/src/CookbookTestUtils.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const {
|
|
4
|
+
validateCookbook: validateCookbookFormat,
|
|
5
|
+
CookbookValidationError
|
|
6
|
+
} = require('@onlineapps/cookbook-core');
|
|
3
7
|
const {
|
|
4
8
|
MIN_COOKBOOK_FORMAT_VERSION,
|
|
5
|
-
checkCookbookFormatVersion,
|
|
6
9
|
readCookbookSteps,
|
|
7
10
|
stepIdentityOf
|
|
8
11
|
} = require('./utils/cookbookFormat');
|
|
@@ -10,6 +13,9 @@ const {
|
|
|
10
13
|
/**
|
|
11
14
|
* CookbookTestUtils - Utilities for cookbook testing
|
|
12
15
|
*/
|
|
16
|
+
/** The control-flow step types the V2.1 schema knows. */
|
|
17
|
+
const CONTROL_FLOW_TYPES = ['foreach', 'switch', 'fork_join', 'steps'];
|
|
18
|
+
|
|
13
19
|
class CookbookTestUtils {
|
|
14
20
|
/**
|
|
15
21
|
* Create a mock workflow message
|
|
@@ -65,59 +71,38 @@ class CookbookTestUtils {
|
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
/**
|
|
68
|
-
* Validate cookbook
|
|
74
|
+
* Validate a cookbook for the readiness-wrapper path
|
|
75
|
+
* (`ServiceReadinessValidator.checkCookbookExecution`).
|
|
76
|
+
*
|
|
77
|
+
* FIRST, `@onlineapps/cookbook-core` `validateCookbook` — the same function
|
|
78
|
+
* the Tier-1 runner (`CookbookTestRunner.validateCookbook`, d.980) and the
|
|
79
|
+
* receiving side run. It owns every structural rule this entry point used to
|
|
80
|
+
* copy: the object itself, `version`, the `steps` array, a step's `step_id`,
|
|
81
|
+
* `type` and `service`, and a step carrying `id` (d.983). Until d.984 this
|
|
82
|
+
* method held its own copy of each, with its own messages, and the `id` ban
|
|
83
|
+
* stayed behind cookbook-core until d.985 — one concern on two rails
|
|
84
|
+
* (`.claude/rules/change-discipline.md` § One rail per concern).
|
|
85
|
+
*
|
|
86
|
+
* This entry point answers with a list, not an exception, so a refusal
|
|
87
|
+
* becomes the list's one error. From cookbook-core only a
|
|
88
|
+
* `CookbookValidationError` is a finding about the cookbook; anything else it
|
|
89
|
+
* throws is a defect and propagates (the caller,
|
|
90
|
+
* `checkCookbookExecution`, reports it as a failed check).
|
|
69
91
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* Presence alone used to be enough here, so a cookbook stuck on an outdated
|
|
73
|
-
* format passed the readiness wrapper unnoticed.
|
|
92
|
+
* @param {*} cookbook - the parsed cookbook
|
|
93
|
+
* @returns {{valid: boolean, errors: string[]}}
|
|
74
94
|
*/
|
|
75
95
|
static validateCookbook(cookbook) {
|
|
76
|
-
const errors = [];
|
|
77
|
-
|
|
78
|
-
if (!cookbook) {
|
|
79
|
-
errors.push('Cookbook is required');
|
|
80
|
-
return { valid: false, errors };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const versionProblem = checkCookbookFormatVersion(cookbook.version);
|
|
84
|
-
if (versionProblem) {
|
|
85
|
-
errors.push(versionProblem);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// The one shape the format allows, through the one owner of that rule
|
|
89
|
-
// (`utils/cookbookFormat`), so this entry point and the Tier-1 runner cannot
|
|
90
|
-
// disagree about what a cookbook's steps are.
|
|
91
|
-
let steps;
|
|
92
96
|
try {
|
|
93
|
-
|
|
97
|
+
validateCookbookFormat(cookbook);
|
|
94
98
|
} catch (error) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return { valid: false, errors };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (steps.length === 0) {
|
|
102
|
-
errors.push('Steps array is required');
|
|
103
|
-
} else {
|
|
104
|
-
steps.forEach((step, index) => {
|
|
105
|
-
if (!stepIdentityOf(step)) {
|
|
106
|
-
errors.push(`Step ${index}: step_id is required`);
|
|
107
|
-
}
|
|
108
|
-
if (!step.type) {
|
|
109
|
-
errors.push(`Step ${index}: type is required`);
|
|
110
|
-
}
|
|
111
|
-
if (step.type === 'task' && !step.service) {
|
|
112
|
-
errors.push(`Step ${index}: service is required for task steps`);
|
|
113
|
-
}
|
|
114
|
-
});
|
|
99
|
+
if (!(error instanceof CookbookValidationError)) {
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
return { valid: false, errors: [error.message] };
|
|
115
103
|
}
|
|
116
104
|
|
|
117
|
-
return {
|
|
118
|
-
valid: errors.length === 0,
|
|
119
|
-
errors
|
|
120
|
-
};
|
|
105
|
+
return { valid: true, errors: [] };
|
|
121
106
|
}
|
|
122
107
|
|
|
123
108
|
/**
|
|
@@ -212,50 +197,76 @@ class CookbookTestUtils {
|
|
|
212
197
|
}
|
|
213
198
|
|
|
214
199
|
/**
|
|
215
|
-
* Generate control
|
|
200
|
+
* Generate a control-flow step (`foreach`, `switch`, `fork_join`, `steps`) in
|
|
201
|
+
* the shape the V2.1 schema of `@onlineapps/cookbook-core` accepts.
|
|
202
|
+
*
|
|
203
|
+
* The step names the service that runs it, and every task it nests by default
|
|
204
|
+
* runs in that same service
|
|
205
|
+
* (api/docs/governance/confirmations/cookbook-execution-owner.md 002,
|
|
206
|
+
* api/docs/biz/40-cookbooks/format.md). Nested steps passed in `options`
|
|
207
|
+
* (`body`, `cases`, `default`, `branches`, `steps`) are taken as given.
|
|
208
|
+
*
|
|
209
|
+
* @param {'foreach'|'switch'|'fork_join'|'steps'} type
|
|
210
|
+
* @param {object} options
|
|
211
|
+
* @param {string} options.service - the service that runs the step (required)
|
|
212
|
+
* @param {string} [options.step_id] - defaults to `<type>_step_<timestamp>`
|
|
213
|
+
* @returns {object} one control-flow step
|
|
214
|
+
* @throws {Error} on a missing `service`, a V1 `id` option or an unknown type
|
|
216
215
|
*/
|
|
217
216
|
static createControlFlowStep(type, options = {}) {
|
|
217
|
+
if (!CONTROL_FLOW_TYPES.includes(type)) {
|
|
218
|
+
throw new Error(`[CookbookTestUtils] createControlFlowStep('${type}') - unknown control-flow type '${type}' - Expected: one of ${CONTROL_FLOW_TYPES.join(', ')}. Fix: pass one of them as the first argument`);
|
|
219
|
+
}
|
|
220
|
+
if (typeof options.service !== 'string' || options.service.length === 0) {
|
|
221
|
+
throw new Error(`[CookbookTestUtils] createControlFlowStep('${type}') - \`service\` is required - Expected: the name of the service that runs this control-flow step. Fix: pass { service: '<name>' }`);
|
|
222
|
+
}
|
|
223
|
+
if (Object.prototype.hasOwnProperty.call(options, 'id')) {
|
|
224
|
+
throw new Error(`[CookbookTestUtils] createControlFlowStep('${type}') - \`id\` is not a step identity - Expected: \`step_id\` (the schema refuses \`id\` on every step). Fix: pass { step_id: '<id>' }`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const { service } = options;
|
|
228
|
+
const task = (stepId, operation) => ({ step_id: stepId, type: 'task', service, operation });
|
|
218
229
|
const baseStep = {
|
|
219
|
-
step_id: options.step_id ||
|
|
220
|
-
type
|
|
230
|
+
step_id: options.step_id || `${type}_step_${Date.now()}`,
|
|
231
|
+
type,
|
|
232
|
+
service
|
|
221
233
|
};
|
|
222
234
|
|
|
223
235
|
switch (type) {
|
|
224
236
|
case 'foreach':
|
|
225
237
|
return {
|
|
226
238
|
...baseStep,
|
|
227
|
-
|
|
228
|
-
body: options.body ||
|
|
229
|
-
step_id: 'foreach-body',
|
|
230
|
-
type: 'task',
|
|
231
|
-
service: 'test-service',
|
|
232
|
-
operation: 'processItem'
|
|
233
|
-
}
|
|
239
|
+
iterator: options.iterator || '{{api_input.items}}',
|
|
240
|
+
body: options.body || [task('foreach_body', 'processItem')]
|
|
234
241
|
};
|
|
235
242
|
|
|
236
243
|
case 'switch':
|
|
237
244
|
return {
|
|
238
245
|
...baseStep,
|
|
239
|
-
|
|
240
|
-
cases: options.cases ||
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
default: options.default ||
|
|
246
|
+
expression: options.expression || '{{api_input.type}}',
|
|
247
|
+
cases: options.cases || {
|
|
248
|
+
type1: task('case1', 'handleType1'),
|
|
249
|
+
type2: task('case2', 'handleType2')
|
|
250
|
+
},
|
|
251
|
+
default: options.default || task('default_case', 'handleDefault')
|
|
245
252
|
};
|
|
246
253
|
|
|
247
254
|
case 'fork_join':
|
|
248
255
|
return {
|
|
249
256
|
...baseStep,
|
|
250
|
-
branches: options.branches ||
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
257
|
+
branches: options.branches || {
|
|
258
|
+
branch1: task('branch1', 'runBranch1'),
|
|
259
|
+
branch2: task('branch2', 'runBranch2')
|
|
260
|
+
},
|
|
254
261
|
join: options.join || { strategy: 'merge' }
|
|
255
262
|
};
|
|
256
263
|
|
|
264
|
+
case 'steps':
|
|
257
265
|
default:
|
|
258
|
-
return
|
|
266
|
+
return {
|
|
267
|
+
...baseStep,
|
|
268
|
+
steps: options.steps || [task('steps_first', 'runFirst')]
|
|
269
|
+
};
|
|
259
270
|
}
|
|
260
271
|
}
|
|
261
272
|
}
|
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
const { assertLogger } = require('@onlineapps/logger-contract');
|
|
4
4
|
const CookbookTestUtils = require('./CookbookTestUtils');
|
|
5
|
-
const {
|
|
5
|
+
const { operationsRuleSentences } = require('./utils/operationsRules');
|
|
6
|
+
const {
|
|
7
|
+
operationsMapFindings,
|
|
8
|
+
operationsDeclarationFindings,
|
|
9
|
+
documentFindingSentence,
|
|
10
|
+
ofSeverity,
|
|
11
|
+
SEVERITY_ERROR,
|
|
12
|
+
SEVERITY_WARNING
|
|
13
|
+
} = require('./utils/operationsDocumentRules');
|
|
6
14
|
|
|
7
15
|
/**
|
|
8
16
|
* The semver 2.0.0 grammar, verbatim from semver.org's own published regular
|
|
@@ -41,8 +49,8 @@ const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]
|
|
|
41
49
|
* reads no others.
|
|
42
50
|
*
|
|
43
51
|
* @see api/docs/biz/30-operations/schema-v3.md
|
|
44
|
-
* @see
|
|
45
|
-
* @see
|
|
52
|
+
* @see api/docs/biz/40-cookbooks/test-runner-flow.md (input probe contract)
|
|
53
|
+
* @see api/docs/biz/80-decisions/0005-no-http-in-biz-containers.md
|
|
46
54
|
*/
|
|
47
55
|
class ServiceReadinessValidator {
|
|
48
56
|
constructor(options = {}) {
|
|
@@ -51,7 +59,7 @@ class ServiceReadinessValidator {
|
|
|
51
59
|
// it. Until then this class demanded a logger and never wrote a line, so a
|
|
52
60
|
// caller that did not print the returned object learned nothing about why a
|
|
53
61
|
// service was refused. Owner confirmation:
|
|
54
|
-
// docs/governance/confirmations/connector-logger-contract.md 001.
|
|
62
|
+
// api/docs/governance/confirmations/connector-logger-contract.md 001.
|
|
55
63
|
this.logger = assertLogger(
|
|
56
64
|
'ServiceReadinessValidator',
|
|
57
65
|
options.logger,
|
|
@@ -60,7 +68,7 @@ class ServiceReadinessValidator {
|
|
|
60
68
|
|
|
61
69
|
// Readiness checks: core (80 points) + optional (20 points) = 100 points max
|
|
62
70
|
// Core checks ALWAYS run, optional checks run if testCookbook/registry provided
|
|
63
|
-
// See: /shared/connector/conn-orch-validator/README.md for usage pattern
|
|
71
|
+
// See: api/shared/connector/conn-orch-validator/README.md for usage pattern
|
|
64
72
|
//
|
|
65
73
|
// Weight allocation post-ADR-0005:
|
|
66
74
|
// operations: 80 — was 60; absorbed the 20 pts from retired `health`.
|
|
@@ -205,59 +213,41 @@ class ServiceReadinessValidator {
|
|
|
205
213
|
|
|
206
214
|
/**
|
|
207
215
|
* Check operations.json compliance (v3 — handler registry).
|
|
208
|
-
*
|
|
209
|
-
*
|
|
216
|
+
*
|
|
217
|
+
* The per-operation rules are the Registry's own, read from the one
|
|
218
|
+
* definition that owns them (`utils/operationsRules.js` →
|
|
219
|
+
* `@onlineapps/service-validator-core`); `input` and `output` are this
|
|
220
|
+
* check's own requirement, because the readiness run synthesises a cookbook
|
|
221
|
+
* from them, and a missing `description` is a warning.
|
|
222
|
+
*
|
|
223
|
+
* @see api/docs/governance/confirmations/operations-schema-mirror.md
|
|
210
224
|
*/
|
|
211
225
|
async checkOperationsCompliance(operations) {
|
|
212
226
|
try {
|
|
213
227
|
const errors = [];
|
|
214
228
|
const warnings = [];
|
|
215
229
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
if (!operation.handler) {
|
|
239
|
-
errors.push(`Operation '${name}' missing handler (v3 — 'handlers/<path>#<export>')`);
|
|
240
|
-
} else if (!HANDLER_REF_PATTERN.test(operation.handler)) {
|
|
241
|
-
errors.push(`Operation '${name}' has invalid handler ref: '${operation.handler}'`);
|
|
242
|
-
}
|
|
243
|
-
if (!operation.bundle_scope) {
|
|
244
|
-
errors.push(`Operation '${name}' missing bundle_scope (platform|tenant|workspace)`);
|
|
245
|
-
} else if (!validScopes.includes(operation.bundle_scope)) {
|
|
246
|
-
errors.push(`Operation '${name}' has invalid bundle_scope: '${operation.bundle_scope}'`);
|
|
247
|
-
}
|
|
248
|
-
if (!operation.input) {
|
|
249
|
-
errors.push(`Operation '${name}' missing input schema`);
|
|
250
|
-
}
|
|
251
|
-
if (!operation.output) {
|
|
252
|
-
errors.push(`Operation '${name}' missing output schema`);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
for (const forbidden of forbiddenV2Fields) {
|
|
256
|
-
if (forbidden in operation) {
|
|
257
|
-
errors.push(`Operation '${name}' has retired v2 field '${forbidden}' — remove per RFC §5.3`);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
230
|
+
// A map with nothing in it — asked of the one definition that owns that
|
|
231
|
+
// rule. This rail was the strict one of four before d.465b; it still
|
|
232
|
+
// refuses, but now it refuses in the same sentence as every other rail
|
|
233
|
+
// instead of one it wrote itself.
|
|
234
|
+
errors.push(...operationsMapFindings(operations).map(documentFindingSentence));
|
|
235
|
+
|
|
236
|
+
// The Registry's rules, asked of the one definition that owns them —
|
|
237
|
+
// including "the map must be a plain object", which this method used to
|
|
238
|
+
// answer on its own.
|
|
239
|
+
errors.push(...operationsRuleSentences(operations).sentences);
|
|
240
|
+
|
|
241
|
+
const isMap = operations !== null && typeof operations === 'object' && !Array.isArray(operations);
|
|
242
|
+
const operationCount = isMap ? Object.keys(operations).length : 0;
|
|
243
|
+
|
|
244
|
+
// A declaration complete enough for the cookbook this validator
|
|
245
|
+
// synthesises from it — the same question step 4 of the boot validation
|
|
246
|
+
// asks, asked of the same definition since d.465c, so the two can no
|
|
247
|
+
// longer answer it in two different sentences.
|
|
248
|
+
const declaration = operationsDeclarationFindings(operations);
|
|
249
|
+
errors.push(...ofSeverity(declaration, SEVERITY_ERROR).map(documentFindingSentence));
|
|
250
|
+
warnings.push(...ofSeverity(declaration, SEVERITY_WARNING).map(documentFindingSentence));
|
|
261
251
|
|
|
262
252
|
return {
|
|
263
253
|
passed: errors.length === 0,
|
|
@@ -17,9 +17,24 @@ const ValidationProofGenerator = require('./validators/ValidationProofGenerator'
|
|
|
17
17
|
const { VALIDATOR_VERSION } = require('./validatorIdentity');
|
|
18
18
|
const { ServiceStructureValidator } = require('./validators/ServiceStructureValidator');
|
|
19
19
|
const { describeStepFailureWithContext } = require('./utils/stepFailure');
|
|
20
|
+
// Where the proof file lives is ONE fact of this package, and its owner is the
|
|
21
|
+
// module that writes it in the CI gate. Until d.758 this class built the same
|
|
22
|
+
// path from a literal of its own, so the boot path and the gate path each
|
|
23
|
+
// carried a copy — and two copies of one fact agree only while somebody is
|
|
24
|
+
// watching (`.claude/rules/doc-code-binding.md` §1). The wrapper's pair ended
|
|
25
|
+
// the same way in d.715 (`ConfigLoader.PROOF_RELATIVE_PATH`).
|
|
26
|
+
const { PROOF_RELATIVE_PATH } = require('./utils/preValidation');
|
|
20
27
|
const CookbookTestRunner = require('./CookbookTestRunner');
|
|
21
28
|
const { assertLogger } = require('@onlineapps/logger-contract');
|
|
22
|
-
const {
|
|
29
|
+
const { operationsRuleSentences } = require('./utils/operationsRules');
|
|
30
|
+
const {
|
|
31
|
+
operationsDocumentFindings,
|
|
32
|
+
operationsDeclarationFindings,
|
|
33
|
+
documentFindingSentence,
|
|
34
|
+
ofSeverity,
|
|
35
|
+
SEVERITY_ERROR,
|
|
36
|
+
SEVERITY_WARNING
|
|
37
|
+
} = require('./utils/operationsDocumentRules');
|
|
23
38
|
const { loadManifest, DEFAULT_MANIFEST_PATH } = require('./manifest/loadManifest');
|
|
24
39
|
const { runManifest } = require('./manifest/runManifest');
|
|
25
40
|
const { resolveWorkspaceRoot } = require('./manifest/workspaceRoot');
|
|
@@ -172,7 +187,7 @@ class ValidationOrchestrator {
|
|
|
172
187
|
// hands the same object to CookbookTestRunner below, which demands all four.
|
|
173
188
|
// An incomplete logger therefore reached the runner one line later and threw
|
|
174
189
|
// in its name instead of this one. Owner confirmation:
|
|
175
|
-
// docs/governance/confirmations/connector-logger-contract.md 001.
|
|
190
|
+
// api/docs/governance/confirmations/connector-logger-contract.md 001.
|
|
176
191
|
this.logger = assertLogger(
|
|
177
192
|
'ValidationOrchestrator',
|
|
178
193
|
options.logger,
|
|
@@ -186,8 +201,8 @@ class ValidationOrchestrator {
|
|
|
186
201
|
// failure one layer away from the cause.
|
|
187
202
|
this.configPath = path.join(this.serviceRoot, 'config', 'service');
|
|
188
203
|
|
|
189
|
-
this.
|
|
190
|
-
this.
|
|
204
|
+
this.proofPath = path.join(this.serviceRoot, PROOF_RELATIVE_PATH);
|
|
205
|
+
this.runtimePath = path.dirname(this.proofPath);
|
|
191
206
|
|
|
192
207
|
// The manifest run of THIS validation, computed on first use (manifestRun()).
|
|
193
208
|
// One orchestrator is one run: `ServiceWrapper._createValidationOrchestrator()`
|
|
@@ -455,6 +470,15 @@ class ValidationOrchestrator {
|
|
|
455
470
|
const structureFailed = await this.runStep(results, 'structure', async () => {
|
|
456
471
|
this.logger.info('[ValidationOrchestrator] Step 1/7: Service Structure');
|
|
457
472
|
results.steps.structure = await this.validateStructure();
|
|
473
|
+
// Step 1 collects its warnings like step 4 does. Until d.791c it was the
|
|
474
|
+
// only step that did not: twelve `this.warnings.push(…)` in
|
|
475
|
+
// `ServiceStructureValidator` filled a field the summary never read, so
|
|
476
|
+
// findings like STANDARD_LEVEL_GAP reached no operator. Measured over all
|
|
477
|
+
// eight `api_biz/*` before the change: exactly ONE warning appears
|
|
478
|
+
// (ingest), so the symmetry arrived with compliance rather than with a
|
|
479
|
+
// flood (`automation-gates.md` §3). Before the push, because a failing
|
|
480
|
+
// structure returns below and its warnings are worth just as much.
|
|
481
|
+
results.warnings.push(...attributeToStep(results.steps.structure.warnings || [], 'structure'));
|
|
458
482
|
if (results.steps.structure.valid) return false;
|
|
459
483
|
|
|
460
484
|
results.success = false;
|
|
@@ -721,11 +745,20 @@ class ValidationOrchestrator {
|
|
|
721
745
|
|
|
722
746
|
/**
|
|
723
747
|
* Step 4: Validate operations compliance (v3 — handler registry dispatch).
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
748
|
+
*
|
|
749
|
+
* The per-operation rules are the Registry's, asked of the one definition
|
|
750
|
+
* that owns them (`utils/operationsRules.js` →
|
|
751
|
+
* `@onlineapps/service-validator-core`), so this step refuses exactly what a
|
|
752
|
+
* registration would refuse — including `mutates`, `resource_type` and the
|
|
753
|
+
* kebab-case key rule, which the copy that used to live here did not know.
|
|
754
|
+
*
|
|
755
|
+
* Two things this step checks beyond them, because a boot needs them and a
|
|
756
|
+
* registration does not: `input` and `output` must be declared (error), and a
|
|
757
|
+
* missing `description` is warned about — inherited from the removed
|
|
758
|
+
* readiness step, which was the only place that noticed it and then threw the
|
|
759
|
+
* notice away.
|
|
760
|
+
*
|
|
761
|
+
* @see api/docs/governance/confirmations/operations-schema-mirror.md
|
|
729
762
|
*/
|
|
730
763
|
async validateOperations() {
|
|
731
764
|
try {
|
|
@@ -734,39 +767,27 @@ class ValidationOrchestrator {
|
|
|
734
767
|
const errors = [];
|
|
735
768
|
const warnings = [];
|
|
736
769
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
errors.push(`Operation ${opName}: missing input schema`);
|
|
759
|
-
}
|
|
760
|
-
if (!opDef.output) {
|
|
761
|
-
errors.push(`Operation ${opName}: missing output schema`);
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
for (const forbidden of forbiddenV2Fields) {
|
|
765
|
-
if (forbidden in opDef) {
|
|
766
|
-
errors.push(`Operation ${opName}: retired v2 field '${forbidden}' present — remove per RFC §5.3`);
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
+
// The two DOCUMENT rules (`schema_version`, an operations map with
|
|
771
|
+
// nothing in it), asked of the one definition that owns them. Until
|
|
772
|
+
// d.465b this step was the rail that answered an empty map with silence
|
|
773
|
+
// — a clean PASS — while three other rails called the same document
|
|
774
|
+
// broken.
|
|
775
|
+
errors.push(...operationsDocumentFindings(operations).map(documentFindingSentence));
|
|
776
|
+
|
|
777
|
+
// Every rule the Registry refuses a registration on, asked once, of the
|
|
778
|
+
// one definition that owns them (`utils/operationsRules.js`). A missing
|
|
779
|
+
// `operations` key is one of them — it used to be read as `|| {}` here,
|
|
780
|
+
// which turned the Registry's refusal into a silent PASS.
|
|
781
|
+
const map = operations.operations;
|
|
782
|
+
errors.push(...operationsRuleSentences(map).sentences);
|
|
783
|
+
|
|
784
|
+
// What the Registry does NOT check, and this step does: a declaration
|
|
785
|
+
// complete enough for a cookbook to be generated from it. The readiness
|
|
786
|
+
// score asks the same question, and until d.465c the two worded every
|
|
787
|
+
// answer differently, which is two rails however identical the severity.
|
|
788
|
+
const declaration = operationsDeclarationFindings(map);
|
|
789
|
+
errors.push(...ofSeverity(declaration, SEVERITY_ERROR).map(documentFindingSentence));
|
|
790
|
+
warnings.push(...ofSeverity(declaration, SEVERITY_WARNING).map(documentFindingSentence));
|
|
770
791
|
|
|
771
792
|
this.logger.info(`[ValidationOrchestrator] ✓ Operations compliance: ${errors.length === 0 ? 'PASS' : 'FAIL'}`
|
|
772
793
|
+ `${warnings.length > 0 ? ` (${warnings.length} warning(s))` : ''}`);
|
|
@@ -953,7 +974,7 @@ class ValidationOrchestrator {
|
|
|
953
974
|
*
|
|
954
975
|
* OUTSIDE A GIT CHECKOUT THE STEP DOES NOT MEASURE. Owner decision 2026-09-17
|
|
955
976
|
* (confirmation `biz-service-manifest` 011): a production image is not a
|
|
956
|
-
* checkout and carries neither
|
|
977
|
+
* checkout and carries neither a compose file nor a README, so the
|
|
957
978
|
* uniform measured there reports absences that say nothing about the
|
|
958
979
|
* repository — 12 findings for a healthy service. A verdict reached that way
|
|
959
980
|
* is not a strict check, it is a wrong answer, and `NOT DEPLOYABLE` printed
|
|
@@ -983,7 +1004,7 @@ class ValidationOrchestrator {
|
|
|
983
1004
|
if (!isGitCheckout(this.serviceRoot)) {
|
|
984
1005
|
// One line, and no table: naming the findings of a tree this run cannot
|
|
985
1006
|
// read would be the false guarantee of `automation-gates.md` §5 in
|
|
986
|
-
// reverse — a reader acting on
|
|
1007
|
+
// reverse — a reader acting on a missing-README finding inside a container
|
|
987
1008
|
// would go looking for a file the image is not supposed to carry.
|
|
988
1009
|
this.logger.info(`[ValidationOrchestrator] NOT RUN manifest conformance — ${NOT_A_CHECKOUT} `
|
|
989
1010
|
+ 'Deployability is proven per commit by the CI job `validate-uniform` over the checkout '
|
package/src/cli/biz-ci-gate.js
CHANGED
|
@@ -418,7 +418,7 @@ function runSetupDbAccount(options) {
|
|
|
418
418
|
|
|
419
419
|
/**
|
|
420
420
|
* Build the schema the contract declares. One implementation for every service:
|
|
421
|
-
* the repo declares WHAT, this decides HOW (docs/biz/00-model/uniformity-principle.md).
|
|
421
|
+
* the repo declares WHAT, this decides HOW (api/docs/biz/00-model/uniformity-principle.md).
|
|
422
422
|
*/
|
|
423
423
|
function runSetupDb(options) {
|
|
424
424
|
const contractInfo = loadAndValidateIntegrationContract(options.serviceRoot, options.contractPath);
|
|
@@ -608,7 +608,7 @@ function exitWhenFlushed(code) {
|
|
|
608
608
|
* and pass `service.url` to the runner, which never read it (`this.serviceUrl`
|
|
609
609
|
* appears nowhere in CookbookTestRunner). `ConfigLoader.loadAll` no longer
|
|
610
610
|
* returns that field either — the owner removed `port` and `url` from the
|
|
611
|
-
* service shape (docs/governance/confirmations/biz-service-port-url.md 001), so
|
|
611
|
+
* service shape (api/docs/governance/confirmations/biz-service-port-url.md 001), so
|
|
612
612
|
* the value had been `undefined` for every service on the platform. What the
|
|
613
613
|
* load still did was make a readable config, an installed wrapper and a
|
|
614
614
|
* resolvable `${ENV}` placeholder preconditions of a cookbook run that depends
|