@onlineapps/conn-orch-validator 12.2.0 → 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 +597 -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/README.md +3 -2
- package/templates/business-service/config/env-templates/shared.env +1 -0
- package/templates/business-service/src/config/index.js +15 -0
- package/TESTING_STRATEGY.md +0 -92
- package/jest.config.js +0 -37
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The rules about the `config/service/operations.json` DOCUMENT — one
|
|
5
|
+
* definition, asked by every rail, so one question gets one answer.
|
|
6
|
+
*
|
|
7
|
+
* Three rules live here:
|
|
8
|
+
*
|
|
9
|
+
* 1. the document declares `schema_version: "3.0"` — the version of the
|
|
10
|
+
* operation schema it is written in;
|
|
11
|
+
* 2. the map it wraps declares at least one operation — a dispatch table with
|
|
12
|
+
* nothing in it is a service with no reason to boot;
|
|
13
|
+
* 3. each declaration is complete enough for a cookbook to be generated from
|
|
14
|
+
* it — `input` and `output` (errors), `description` (a warning).
|
|
15
|
+
*
|
|
16
|
+
* ## Why these are NOT in `@onlineapps/service-validator-core`
|
|
17
|
+
*
|
|
18
|
+
* Because the Registry never sees this document. Registration sends
|
|
19
|
+
* `doc.operations` alone — the map, not the file — so `schema_version` and a
|
|
20
|
+
* sibling key of it never reach the Registry, and the Registry has no rule
|
|
21
|
+
* about either. The core package holds the MIRROR of the Registry's rules
|
|
22
|
+
* (owner decision `api/docs/governance/confirmations/operations-schema-mirror.md`
|
|
23
|
+
* 001), and its own header promises it "adds no rule and drops no rule".
|
|
24
|
+
* Putting a rule the Registry does not have into that mirror would make the
|
|
25
|
+
* promise false and the mirror unreadable as a mirror.
|
|
26
|
+
*
|
|
27
|
+
* Rule 3 is the same case seen from the other side: the Registry generates no
|
|
28
|
+
* cookbook, so it needs none of `input`, `output`, `description`; the two rails
|
|
29
|
+
* that DO generate one cannot boot a service without them.
|
|
30
|
+
*
|
|
31
|
+
* These three rules belong to the tool that reads REPOSITORIES — this package —
|
|
32
|
+
* and their home here is the owner-approved answer, not a waypoint: do not move
|
|
33
|
+
* them into the core because they look like they belong beside the per-operation
|
|
34
|
+
* rules. `src/utils/operationsRules.js` is the adapter to those; this module is
|
|
35
|
+
* the owner of these. Lead decision 2026-09-20 (batch d.465b), on the measured
|
|
36
|
+
* alternative of putting them in the core, which no rail could have called.
|
|
37
|
+
*
|
|
38
|
+
* ## What it replaced (measured 2026-09-20)
|
|
39
|
+
*
|
|
40
|
+
* An empty operations map had FOUR answers: an error
|
|
41
|
+
* (`ServiceReadinessValidator`), a warning (`ServiceStructureValidator`
|
|
42
|
+
* `NO_OPERATIONS`), silence — a clean PASS — (`ValidationOrchestrator` step 4)
|
|
43
|
+
* and a finding (the uniform row `C-OPS`). A missing `schema_version` had none:
|
|
44
|
+
* `ServiceStructureValidator` compared the value only `if` it was there.
|
|
45
|
+
* Now every rail asks this module, and the answer is an error everywhere — the
|
|
46
|
+
* stricter of the four, because a service that dispatches nothing cannot serve
|
|
47
|
+
* a single cookbook step.
|
|
48
|
+
*
|
|
49
|
+
* Layer: L3 (Orchestration). No filesystem, no environment, no dependencies.
|
|
50
|
+
*
|
|
51
|
+
* @see api/docs/biz/30-operations/schema-v3.md § File shape
|
|
52
|
+
* @see api/docs/governance/confirmations/operations-schema-mirror.md
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/** The one document these rules are about, named the same way in every finding. */
|
|
56
|
+
const OPERATIONS_FILE = 'config/service/operations.json';
|
|
57
|
+
|
|
58
|
+
/** The only operation-schema version this platform dispatches. */
|
|
59
|
+
const OPERATIONS_SCHEMA_VERSION = '3.0';
|
|
60
|
+
|
|
61
|
+
/** How every finding of this module says what to do about it. */
|
|
62
|
+
const SET_VERSION_FIX = `set "schema_version": "${OPERATIONS_SCHEMA_VERSION}" in ${OPERATIONS_FILE}`;
|
|
63
|
+
|
|
64
|
+
/** The two severities a finding of this module carries, named once. */
|
|
65
|
+
const SEVERITY_ERROR = 'error';
|
|
66
|
+
const SEVERITY_WARNING = 'warning';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Whether a value is a plain object — the shape both rules presuppose.
|
|
70
|
+
*
|
|
71
|
+
* @param {*} value
|
|
72
|
+
* @returns {boolean}
|
|
73
|
+
*/
|
|
74
|
+
function isPlainObject(value) {
|
|
75
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Rule 2, on the map alone — for the rail that never holds the document.
|
|
80
|
+
*
|
|
81
|
+
* A map that is missing, null or not a plain object is NOT this rule's business:
|
|
82
|
+
* that one the Registry does have, so `utils/operationsRules.js` answers it and
|
|
83
|
+
* this module stays silent rather than giving the same defect a second voice.
|
|
84
|
+
*
|
|
85
|
+
* @param {*} operations - the `operations` map
|
|
86
|
+
* @returns {Array<{field: string, what: string, fix: string}>}
|
|
87
|
+
*/
|
|
88
|
+
function operationsMapFindings(operations) {
|
|
89
|
+
if (!isPlainObject(operations)) return [];
|
|
90
|
+
if (Object.keys(operations).length > 0) return [];
|
|
91
|
+
|
|
92
|
+
return [{
|
|
93
|
+
field: 'operations',
|
|
94
|
+
what: 'declares no operations — a service that dispatches nothing has no reason to boot',
|
|
95
|
+
fix: `declare at least one operation in ${OPERATIONS_FILE}`,
|
|
96
|
+
severity: SEVERITY_ERROR
|
|
97
|
+
}];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Both rules, on the parsed document — for every rail that holds the file.
|
|
102
|
+
*
|
|
103
|
+
* @param {*} document - parsed contents of `config/service/operations.json`
|
|
104
|
+
* @returns {Array<{field: string, what: string, fix: string}>}
|
|
105
|
+
*/
|
|
106
|
+
function operationsDocumentFindings(document) {
|
|
107
|
+
const findings = [];
|
|
108
|
+
const declared = isPlainObject(document) ? document.schema_version : undefined;
|
|
109
|
+
|
|
110
|
+
if (declared === undefined || declared === null) {
|
|
111
|
+
findings.push({
|
|
112
|
+
field: 'schema_version',
|
|
113
|
+
what: 'absent — the document does not say which operation schema it is written in, '
|
|
114
|
+
+ 'so nothing can tell a v3 declaration from an older one',
|
|
115
|
+
fix: SET_VERSION_FIX,
|
|
116
|
+
severity: SEVERITY_ERROR
|
|
117
|
+
});
|
|
118
|
+
} else if (declared !== OPERATIONS_SCHEMA_VERSION) {
|
|
119
|
+
findings.push({
|
|
120
|
+
field: 'schema_version',
|
|
121
|
+
what: `is ${JSON.stringify(declared)} — this platform dispatches operation schema `
|
|
122
|
+
+ `${OPERATIONS_SCHEMA_VERSION} and no other`,
|
|
123
|
+
fix: SET_VERSION_FIX,
|
|
124
|
+
severity: SEVERITY_ERROR
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
findings.push(...operationsMapFindings(isPlainObject(document) ? document.operations : undefined));
|
|
129
|
+
return findings;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Rule 3 — is each declaration complete enough for a cookbook to be generated
|
|
134
|
+
* from it?
|
|
135
|
+
*
|
|
136
|
+
* This is a document rule for the same reason as the other two: the Registry
|
|
137
|
+
* has none of it. It never generates a cookbook, so it never needs `input`,
|
|
138
|
+
* `output` or `description`; the two rails that DO generate one —
|
|
139
|
+
* `ValidationOrchestrator` step 4 and the readiness score — need all three.
|
|
140
|
+
*
|
|
141
|
+
* The severity is not a new decision. Both rails already answered `input` and
|
|
142
|
+
* `output` with an error and `description` with a warning, and agreed
|
|
143
|
+
* (measured d.465c); what they disagreed about was every sentence — `Operation
|
|
144
|
+
* x: missing input schema` against `Operation 'x' missing input schema`. One
|
|
145
|
+
* rule stated in two texts is still two rails, because the next edit moves one
|
|
146
|
+
* of them and nothing notices.
|
|
147
|
+
*
|
|
148
|
+
* A value that is not an object is not this rule's business: the Registry owns
|
|
149
|
+
* that one, and `utils/operationsRules.js` reports it.
|
|
150
|
+
*
|
|
151
|
+
* @param {*} operations - the `operations` map
|
|
152
|
+
* @returns {Array<{operation: string, field: string, what: string, fix: string, severity: string}>}
|
|
153
|
+
*/
|
|
154
|
+
function operationsDeclarationFindings(operations) {
|
|
155
|
+
if (!isPlainObject(operations)) return [];
|
|
156
|
+
|
|
157
|
+
const findings = [];
|
|
158
|
+
for (const [name, operation] of Object.entries(operations)) {
|
|
159
|
+
if (!isPlainObject(operation)) continue;
|
|
160
|
+
|
|
161
|
+
for (const field of ['input', 'output']) {
|
|
162
|
+
if (operation[field]) continue;
|
|
163
|
+
findings.push({
|
|
164
|
+
operation: name,
|
|
165
|
+
field,
|
|
166
|
+
what: `no ${field} schema — a cookbook step generated from this declaration `
|
|
167
|
+
+ `has nothing to shape its ${field} by`,
|
|
168
|
+
fix: `add "${field}" (a JSON Schema object) to the operation in ${OPERATIONS_FILE}`,
|
|
169
|
+
severity: SEVERITY_ERROR
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!operation.description) {
|
|
174
|
+
findings.push({
|
|
175
|
+
operation: name,
|
|
176
|
+
field: 'description',
|
|
177
|
+
what: 'no description — the operation catalogue and the generated cookbook step '
|
|
178
|
+
+ 'have nothing to call it',
|
|
179
|
+
fix: `add "description" to the operation in ${OPERATIONS_FILE}`,
|
|
180
|
+
severity: SEVERITY_WARNING
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return findings;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The findings of one severity, in declaration order.
|
|
190
|
+
*
|
|
191
|
+
* @param {Array<{severity: string}>} findings
|
|
192
|
+
* @param {string} severity - `SEVERITY_ERROR` or `SEVERITY_WARNING`
|
|
193
|
+
* @returns {Array<object>}
|
|
194
|
+
*/
|
|
195
|
+
function ofSeverity(findings, severity) {
|
|
196
|
+
return findings.filter((finding) => finding.severity === severity);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* One document finding as a single sentence, for a rail that collects strings.
|
|
201
|
+
*
|
|
202
|
+
* The `Fix:` sentence is part of it: a rail reporting strings has nowhere else
|
|
203
|
+
* to carry the remedy (`architecture-principles.md` §5).
|
|
204
|
+
*
|
|
205
|
+
* @param {{field: string, what: string, fix: string}} finding
|
|
206
|
+
* @returns {string}
|
|
207
|
+
*/
|
|
208
|
+
function documentFindingSentence(finding) {
|
|
209
|
+
const subject = finding.operation === undefined
|
|
210
|
+
? `${OPERATIONS_FILE} —`
|
|
211
|
+
: `${OPERATIONS_FILE} — operation '${finding.operation}',`;
|
|
212
|
+
return `${subject} ${finding.field}: ${finding.what}. Fix: ${finding.fix}.`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* One document finding as a record, for a rail that collects objects.
|
|
217
|
+
*
|
|
218
|
+
* @param {{field: string, what: string, fix: string}} finding
|
|
219
|
+
* @returns {{type: string, path: string, field: string, message: string, fix: string}}
|
|
220
|
+
*/
|
|
221
|
+
function documentFindingRecord(finding) {
|
|
222
|
+
return {
|
|
223
|
+
type: 'INVALID_OPERATIONS_DOCUMENT',
|
|
224
|
+
path: OPERATIONS_FILE,
|
|
225
|
+
field: finding.field,
|
|
226
|
+
message: `${OPERATIONS_FILE} — ${finding.field}: ${finding.what}`,
|
|
227
|
+
fix: finding.fix
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
module.exports = {
|
|
232
|
+
OPERATIONS_FILE,
|
|
233
|
+
OPERATIONS_SCHEMA_VERSION,
|
|
234
|
+
SEVERITY_ERROR,
|
|
235
|
+
SEVERITY_WARNING,
|
|
236
|
+
ofSeverity,
|
|
237
|
+
operationsDeclarationFindings,
|
|
238
|
+
operationsMapFindings,
|
|
239
|
+
operationsDocumentFindings,
|
|
240
|
+
documentFindingSentence,
|
|
241
|
+
documentFindingRecord
|
|
242
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The per-operation rules of `config/service/operations.json`, read from the one
|
|
5
|
+
* place that owns them, and rendered in the two shapes the rails of this package
|
|
6
|
+
* report in.
|
|
7
|
+
*
|
|
8
|
+
* The rules themselves are NOT here and must never be: they are the rules the
|
|
9
|
+
* Registry applies when a service registers, and the owner decided where they
|
|
10
|
+
* live — `@onlineapps/service-validator-core`, `validateOperationsSchema`
|
|
11
|
+
* (`api/docs/governance/confirmations/operations-schema-mirror.md` 001, which
|
|
12
|
+
* rejected a copy in this package in so many words). This module is the adapter
|
|
13
|
+
* that lets a rail here ask that one question and print the answer in its own
|
|
14
|
+
* voice.
|
|
15
|
+
*
|
|
16
|
+
* What it replaced (measured 2026-09-20, d.465): three rails restated the rules
|
|
17
|
+
* from memory — `ValidationOrchestrator.validateOperations` (boot step 4),
|
|
18
|
+
* `ServiceStructureValidator.validateOperation` (boot step 1) and
|
|
19
|
+
* `ServiceReadinessValidator.checkOperationsCompliance` (the readiness score),
|
|
20
|
+
* with a fourth copy in the jest suite `createServiceReadinessTests` generates
|
|
21
|
+
* for every biz repository. All four knew `handler`, `bundle_scope` and the
|
|
22
|
+
* retired v2 fields; NONE of them knew `mutates`, `resource_type` or the
|
|
23
|
+
* kebab-case key rule, which are the three the Registry refuses a registration
|
|
24
|
+
* on. An operation violating them therefore passed every local check and
|
|
25
|
+
* surfaced as a crash loop after restart — the defect the confirmation exists to
|
|
26
|
+
* remove (`change-discipline.md` § One rail per concern).
|
|
27
|
+
*
|
|
28
|
+
* Layer: L3 (Orchestration). No filesystem, no environment.
|
|
29
|
+
*
|
|
30
|
+
* @see api/docs/governance/confirmations/operations-schema-mirror.md
|
|
31
|
+
* @see api/docs/biz/30-operations/schema-v3.md
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The owning definition, required at CALL time rather than at load time.
|
|
36
|
+
*
|
|
37
|
+
* `oa-validate` is started from a copy of this engine's `src/` tree — that is
|
|
38
|
+
* how an installed package runs, and how three acceptance suites plant one —
|
|
39
|
+
* and the engine's own guards (no `package.json`, no workspace root) speak
|
|
40
|
+
* before any row runs. A top-level `require` of a sibling package here loads
|
|
41
|
+
* before those guards get a chance, so a copy with nothing installed beside it
|
|
42
|
+
* died with `MODULE_NOT_FOUND` and a stack instead of the one actionable
|
|
43
|
+
* sentence `architecture-principles.md` §5 demands
|
|
44
|
+
* (`tests/unit/oaValidateCli.integration.test.js` § an engine copy carrying no
|
|
45
|
+
* package.json). Asking at call time keeps the dependency exactly as hard as it
|
|
46
|
+
* was — the row cannot answer without it — while leaving the order of the
|
|
47
|
+
* engine's messages intact. The package's own `index.js` defers for the same
|
|
48
|
+
* reason.
|
|
49
|
+
*
|
|
50
|
+
* @returns {Function} `validateOperationsSchema` of `@onlineapps/service-validator-core`
|
|
51
|
+
*/
|
|
52
|
+
function owningDefinition() {
|
|
53
|
+
return require('@onlineapps/service-validator-core').validateOperationsSchema;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The one document these rules are about, named the same way in every finding. */
|
|
57
|
+
const OPERATIONS_FILE = 'config/service/operations.json';
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Split a mirror reason into the problem and the fix it already carries.
|
|
61
|
+
*
|
|
62
|
+
* Several of the Registry's reasons end in a `Fix:` sentence and the rest do
|
|
63
|
+
* not. A rail that reports `{ message, fix }` needs both halves, so the halves
|
|
64
|
+
* are taken from the reason where it has them, and composed from the field it
|
|
65
|
+
* names where it does not. Nothing is invented about the RULE either way — the
|
|
66
|
+
* sentence that states the rule is always the Registry's own.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} reason - `reason` of one mirror finding
|
|
69
|
+
* @returns {{ what: string, fix: string|null }}
|
|
70
|
+
*/
|
|
71
|
+
function splitReason(reason) {
|
|
72
|
+
const marker = reason.indexOf('Fix:');
|
|
73
|
+
if (marker === -1) return { what: reason.trim(), fix: null };
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
what: reason.slice(0, marker).replace(/\s*[-–—]\s*$/, '').trim(),
|
|
77
|
+
fix: reason.slice(marker + 'Fix:'.length).trim()
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* One mirror finding as a single sentence, for a rail that collects strings.
|
|
83
|
+
*
|
|
84
|
+
* @param {{operation: string, field: string, reason: string}} finding
|
|
85
|
+
* @returns {string}
|
|
86
|
+
*/
|
|
87
|
+
function findingSentence(finding) {
|
|
88
|
+
if (finding.operation === '(root)') {
|
|
89
|
+
return `${OPERATIONS_FILE}: ${finding.reason}`;
|
|
90
|
+
}
|
|
91
|
+
return `Operation '${finding.operation}' — ${finding.field}: ${finding.reason}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* One mirror finding as a record, for a rail that collects objects.
|
|
96
|
+
*
|
|
97
|
+
* `type` is the Registry's own error code for the whole verdict, not a taxonomy
|
|
98
|
+
* of this package: a caller reading `INVALID_OPERATIONS_SCHEMA` here and in the
|
|
99
|
+
* rejection message is reading the same word about the same rule.
|
|
100
|
+
*
|
|
101
|
+
* @param {{operation: string, field: string, reason: string}} finding
|
|
102
|
+
* @param {string} errorCode - `errorCode` of the verdict the finding came from
|
|
103
|
+
* @returns {{type: string, path: string, operation: string, field: string, message: string, fix: string}}
|
|
104
|
+
*/
|
|
105
|
+
function findingRecord(finding, errorCode) {
|
|
106
|
+
const { what, fix } = splitReason(finding.reason);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
type: errorCode,
|
|
110
|
+
path: OPERATIONS_FILE,
|
|
111
|
+
operation: finding.operation,
|
|
112
|
+
field: finding.field,
|
|
113
|
+
message: `Operation "${finding.operation}" — ${finding.field}: ${what}`,
|
|
114
|
+
fix: fix === null
|
|
115
|
+
? `Correct "${finding.field}" of operation "${finding.operation}" in ${OPERATIONS_FILE} `
|
|
116
|
+
+ '— see api/docs/biz/30-operations/schema-v3.md.'
|
|
117
|
+
: fix
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Ask the owning definition, and get the answer as sentences.
|
|
123
|
+
*
|
|
124
|
+
* @param {object|null|undefined} operations - the `operations` map
|
|
125
|
+
* @returns {{ valid: boolean, errorCode: string|null, sentences: string[] }}
|
|
126
|
+
*/
|
|
127
|
+
function operationsRuleSentences(operations) {
|
|
128
|
+
const verdict = owningDefinition()(operations);
|
|
129
|
+
return {
|
|
130
|
+
valid: verdict.valid,
|
|
131
|
+
errorCode: verdict.errorCode,
|
|
132
|
+
sentences: verdict.errors.map(findingSentence)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Ask the owning definition, and get the answer as records.
|
|
138
|
+
*
|
|
139
|
+
* @param {object|null|undefined} operations - the `operations` map
|
|
140
|
+
* @returns {{ valid: boolean, errorCode: string|null, records: object[] }}
|
|
141
|
+
*/
|
|
142
|
+
function operationsRuleRecords(operations) {
|
|
143
|
+
const verdict = owningDefinition()(operations);
|
|
144
|
+
return {
|
|
145
|
+
valid: verdict.valid,
|
|
146
|
+
errorCode: verdict.errorCode,
|
|
147
|
+
records: verdict.errors.map((finding) => findingRecord(finding, verdict.errorCode))
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = {
|
|
152
|
+
OPERATIONS_FILE,
|
|
153
|
+
findingSentence,
|
|
154
|
+
findingRecord,
|
|
155
|
+
operationsRuleSentences,
|
|
156
|
+
operationsRuleRecords
|
|
157
|
+
};
|
|
@@ -26,7 +26,18 @@ function resolveHeaders(headers, options = {}) {
|
|
|
26
26
|
const resolvedValue = valueStr.replace(/\$\{([A-Z0-9_]+)\}/g, (_match, varName) => {
|
|
27
27
|
const envValue = env[varName];
|
|
28
28
|
if (envValue === undefined || envValue === null || String(envValue).trim() === '') {
|
|
29
|
-
|
|
29
|
+
// The `Fix:` half is what `architecture-principles.md` §5 asks of every
|
|
30
|
+
// config error, in the wording
|
|
31
|
+
// `api/docs/standards/ARCHITECTURE_PRINCIPLES.md` § Config Contract
|
|
32
|
+
// Exceptions owns: a key whose schema declares no owning env file is
|
|
33
|
+
// sent to `env-active/*.env`, and a location nobody declared is never
|
|
34
|
+
// invented. A header placeholder has no schema, so that is the location
|
|
35
|
+
// it gets — plus the other way out this rail really has, a literal
|
|
36
|
+
// value in the cookbook step.
|
|
37
|
+
throw new Error(
|
|
38
|
+
`[conn-orch-validator][Headers] Missing environment variable - Expected ${varName} for header ${key}. `
|
|
39
|
+
+ `Fix: set ${varName} in env-active/*.env, or replace the placeholder with a literal value in the cookbook step's headers.`
|
|
40
|
+
);
|
|
30
41
|
}
|
|
31
42
|
return String(envValue);
|
|
32
43
|
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* The service declares WHAT its database is in integration-contract.json; this
|
|
7
7
|
* decides HOW it gets built, identically for all of them
|
|
8
|
-
* (docs/biz/00-model/uniformity-principle.md).
|
|
8
|
+
* (api/docs/biz/00-model/uniformity-principle.md).
|
|
9
9
|
*
|
|
10
10
|
* It replaces six per-repo ci-setup-db.js scripts that had each solved the same
|
|
11
11
|
* problem differently — 113, 69, 56, 54, 22 and 20 lines, in three strategies,
|
package/src/utils/stepFailure.js
CHANGED
|
@@ -52,8 +52,8 @@ const KIND_COOKBOOK_LOAD_FAILURE = 'cookbook-load-failure';
|
|
|
52
52
|
* alternative on the day was `undefined`. It is the wrong answer now. Since
|
|
53
53
|
* `@onlineapps/cookbook-core` 5.0.0 the schema requires `step_id` on every task
|
|
54
54
|
* step (`schemas/cookbook.v2.schema.json` definitions.TaskStep.required lists
|
|
55
|
-
* step_id, type, service, operation)
|
|
56
|
-
*
|
|
55
|
+
* step_id, type, service, operation) and forbids a step key `id` (d.983),
|
|
56
|
+
* `CookbookTestRunner.validateCookbook` refuses a cookbook
|
|
57
57
|
* whose step omits it, and the orchestrator will not run a task step that fails
|
|
58
58
|
* to name its operation either (`@onlineapps/conn-orch-orchestrator`
|
|
59
59
|
* § _requireStepOperation, d.460). So the only step that can still arrive here
|
|
@@ -78,7 +78,7 @@ function describeStepIdentity(step, index) {
|
|
|
78
78
|
throw new Error('[stepFailure] step is required - Expected the cookbook step (or its result) to name in a message');
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
// `step_id` only.
|
|
81
|
+
// `step_id` only. cookbook-core refuses a step spelling it `id` (d.983), so a
|
|
82
82
|
// step reaching here can no longer carry the retired name.
|
|
83
83
|
if (typeof step.step_id === 'string' && step.step_id.length > 0) return step.step_id;
|
|
84
84
|
|
|
@@ -82,33 +82,29 @@ function resolveStepInput(step, runContext) {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
|
-
* WHAT A TIER-1 RECIPE MAY WRITE IN `input`, checked before the run starts
|
|
85
|
+
* WHAT A TIER-1 RECIPE MAY WRITE IN `input`, checked before the run starts —
|
|
86
|
+
* the ONE rule of that kind the format's owner does not hold.
|
|
86
87
|
*
|
|
87
88
|
* Tier-1 is the service's own startup proof, so a recipe it passes must be a
|
|
88
|
-
* recipe production would accept.
|
|
89
|
-
* QUIET where production is
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* The literal outcome is right for a reference whose VALUE is missing at that
|
|
108
|
-
* moment — a step that has not run, or one that failed
|
|
109
|
-
* (`variable-references.md` § Unresolved references), and those two stay literal
|
|
110
|
-
* below. It is wrong for a name that does not exist in the recipe at all: no
|
|
111
|
-
* run can ever make it resolve.
|
|
89
|
+
* recipe production would accept. A helper call — `{{webalizeString(…)}}`,
|
|
90
|
+
* `{{string2file(…)}}` — broke that symmetry by going QUIET where production is
|
|
91
|
+
* loud: the orchestrator resolves it through its helper registry and throws on
|
|
92
|
+
* a name the registry does not carry
|
|
93
|
+
* (`WorkflowOrchestrator._executeTemplateHelper`). This runner has no registry —
|
|
94
|
+
* deliberately: a helper reaches its own runtime (ContentResolver, files,
|
|
95
|
+
* storage), which is not what a boot probe may do. So the expression resolved
|
|
96
|
+
* to nothing and survived as literal text, and the step ran with
|
|
97
|
+
* `{{webalizeString(…)}}` where a value belonged. `@onlineapps/cookbook-core`
|
|
98
|
+
* accepts a helper call — it is valid in a recipe production runs — so the
|
|
99
|
+
* refusal is Tier-1's own and lives here.
|
|
100
|
+
*
|
|
101
|
+
* WHAT IS NOT HERE ANY MORE. A `{{steps.<id>}}` naming no step of the recipe,
|
|
102
|
+
* a positional `steps.0` / `steps[0]`, and a `depends_on` naming no step are
|
|
103
|
+
* refused by `@onlineapps/cookbook-core` `validateCookbook`, which
|
|
104
|
+
* `CookbookTestRunner.validateCookbook` runs FIRST. This module held its own
|
|
105
|
+
* copy of the first and the last with its own messages until d.980 — one
|
|
106
|
+
* concern on two rails (`.claude/rules/change-discipline.md` § One rail per
|
|
107
|
+
* concern).
|
|
112
108
|
*
|
|
113
109
|
* NO SECOND WALK. The traversal and the expression extraction are the same
|
|
114
110
|
* `resolveReferencesWith()` that resolves the input for real one moment later,
|
|
@@ -127,8 +123,7 @@ function resolveStepInput(step, runContext) {
|
|
|
127
123
|
* another package's manifest.
|
|
128
124
|
*
|
|
129
125
|
* SCOPE. `input` only, which is the single expansion site a Tier-1 cookbook has
|
|
130
|
-
* (`resolveStepInput` above says why)
|
|
131
|
-
* names by definition.
|
|
126
|
+
* (`resolveStepInput` above says why).
|
|
132
127
|
*
|
|
133
128
|
* @see api/docs/biz/40-cookbooks/variable-references.md
|
|
134
129
|
* @see api/docs/governance/confirmations/cookbook-validation-placement.md
|
|
@@ -137,9 +132,6 @@ function resolveStepInput(step, runContext) {
|
|
|
137
132
|
/** `helperName(...)` — the shape `WorkflowOrchestrator` routes to its registry. */
|
|
138
133
|
const HELPER_CALL = /^([a-zA-Z_][a-zA-Z0-9_]*)\(([\s\S]*)\)$/;
|
|
139
134
|
|
|
140
|
-
/** Root of the step namespace; the only root whose names a recipe can be checked against. */
|
|
141
|
-
const STEPS_ROOT = 'steps';
|
|
142
|
-
|
|
143
135
|
function helperCallProblem(stepId, helperName, expression) {
|
|
144
136
|
return `Step ${stepId} input calls the template helper "${helperName}" - `
|
|
145
137
|
+ `Tier-1 runs no helper registry, so "{{${expression}}}" would reach the handler as literal `
|
|
@@ -149,76 +141,25 @@ function helperCallProblem(stepId, helperName, expression) {
|
|
|
149
141
|
+ 'earlier steps (api/docs/biz/40-cookbooks/variable-references.md).';
|
|
150
142
|
}
|
|
151
143
|
|
|
152
|
-
function undefinedStepProblem(stepId, referencedId, expression, knownIds) {
|
|
153
|
-
return `Step ${stepId} references an undefined step - "{{${expression}}}" names "${referencedId}", `
|
|
154
|
-
+ 'which is not a step_id of this cookbook. '
|
|
155
|
-
+ `Expected one of: ${knownIds.join(', ')}. `
|
|
156
|
-
+ 'Fix: correct the step_id in the reference, or add the missing step '
|
|
157
|
-
+ '(api/docs/biz/40-cookbooks/variable-references.md § Unresolved references).';
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function undefinedDependencyProblem(stepId, dependencyId, knownIds) {
|
|
161
|
-
return `Step ${stepId} depends on an undefined step - "depends_on" names "${dependencyId}", `
|
|
162
|
-
+ 'which is not a step_id of this cookbook. '
|
|
163
|
-
+ `Expected one of: ${knownIds.join(', ')}. `
|
|
164
|
-
+ 'Fix: correct the step_id in "depends_on", or add the missing step '
|
|
165
|
-
+ '(api/docs/biz/40-cookbooks/format.md § Step definition).';
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* What ONE expression is, measured against the names this cookbook defines.
|
|
170
|
-
*
|
|
171
|
-
* A path whose root is not `steps` — `api_input.…`, `context.…`, `current.…` —
|
|
172
|
-
* is left alone: its value comes into being at run time and a recipe says
|
|
173
|
-
* nothing about whether it will. The BRACKET form `steps[0]` is not a step
|
|
174
|
-
* reference either, in this runner or at the gateway; the dotted `steps.0` is,
|
|
175
|
-
* and it names a step no recipe can define.
|
|
176
|
-
*
|
|
177
|
-
* @returns {string|null} the problem, or `null` when the expression is fine
|
|
178
|
-
*/
|
|
179
|
-
function checkExpression(expression, stepId, known, knownIds) {
|
|
180
|
-
const helperCall = expression.match(HELPER_CALL);
|
|
181
|
-
if (helperCall) {
|
|
182
|
-
return helperCallProblem(stepId, helperCall[1], expression);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const parts = expression.split('.');
|
|
186
|
-
if (parts[0] !== STEPS_ROOT || parts.length < 2) return null;
|
|
187
|
-
|
|
188
|
-
const referencedId = parts[1];
|
|
189
|
-
if (known.has(referencedId)) return null;
|
|
190
|
-
|
|
191
|
-
return undefinedStepProblem(stepId, referencedId, expression, knownIds);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
144
|
/**
|
|
195
145
|
* The problem this cookbook's step inputs carry, or `null` when they carry none.
|
|
196
146
|
*
|
|
197
|
-
* Returns the FIRST problem rather than throwing
|
|
198
|
-
* `utils/cookbookFormat.checkCookbookFormatVersion` does: the caller owns the
|
|
147
|
+
* Returns the FIRST problem rather than throwing: the caller owns the
|
|
199
148
|
* `[Context]` of the message it throws, and the caller here is the runner
|
|
200
149
|
* (`.claude/rules/architecture-principles.md` §5).
|
|
201
150
|
*
|
|
202
|
-
* @param {Array<Object>} steps - the cookbook's steps, already
|
|
151
|
+
* @param {Array<Object>} steps - the cookbook's steps, already validated by cookbook-core
|
|
203
152
|
* @returns {Promise<string|null>} the problem text without its context prefix
|
|
204
153
|
*/
|
|
205
154
|
async function checkStepInputExpressions(steps) {
|
|
206
|
-
const knownIds = steps
|
|
207
|
-
.map((step) => step && step.step_id)
|
|
208
|
-
.filter((stepId) => typeof stepId === 'string' && stepId.length > 0);
|
|
209
|
-
const known = new Set(knownIds);
|
|
210
|
-
|
|
211
155
|
for (const step of steps) {
|
|
212
|
-
const dependencies = Array.isArray(step.depends_on) ? step.depends_on : [];
|
|
213
|
-
for (const dependency of dependencies) {
|
|
214
|
-
if (typeof dependency !== 'string' || known.has(dependency)) continue;
|
|
215
|
-
return undefinedDependencyProblem(step.step_id, dependency, knownIds);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
156
|
let problem = null;
|
|
219
157
|
await resolveReferencesWith(step.input, (expression) => {
|
|
220
158
|
if (problem === null) {
|
|
221
|
-
|
|
159
|
+
const helperCall = expression.match(HELPER_CALL);
|
|
160
|
+
if (helperCall) {
|
|
161
|
+
problem = helperCallProblem(step.step_id, helperCall[1], expression);
|
|
162
|
+
}
|
|
222
163
|
}
|
|
223
164
|
return undefined;
|
|
224
165
|
});
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* (`change-discipline.md` § One rail per concern), and the concern here is not
|
|
13
13
|
* service-specific at all: the service declares WHAT its database is in
|
|
14
14
|
* `config/service/integration-contract.json`, and HOW a schema gets built from
|
|
15
|
-
* that declaration is uniform (`docs/biz/00-model/uniformity-principle.md`).
|
|
15
|
+
* that declaration is uniform (`api/docs/biz/00-model/uniformity-principle.md`).
|
|
16
16
|
*
|
|
17
17
|
* WHAT IT SHARES WITH THE CI BUILD. Everything except one decision. The
|
|
18
18
|
* migration set and its order (`resolveMigrationPlan`, `migrationOrder.js`), the
|