@onlineapps/conn-orch-validator 4.0.1 → 5.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 +90 -0
- package/package.json +1 -1
- package/src/CookbookTestRunner.js +26 -31
- package/src/CookbookTestUtils.js +39 -20
- package/src/cli/biz-ci-gate.js +1 -1
- package/src/helpers/createServiceReadinessTests.js +1 -1
- package/src/utils/cookbookFormat.js +78 -1
- package/src/utils/preValidation.js +1 -1
- package/src/utils/stepFailure.js +38 -2
- package/src/validators/ServiceStructureValidator.js +24 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,96 @@ All notable changes to this package. Follows [Keep a Changelog](https://keepacha
|
|
|
7
7
|
> ⚠ Neither this entry nor the connector-step work of `dcddb862` is published.
|
|
8
8
|
> The last version on NPM is 3.3.2, so **no biz CI run sees any of it yet**.
|
|
9
9
|
|
|
10
|
+
### BREAKING — a cookbook step spelling its identifier `id` is refused (DÁVKA 77 A3)
|
|
11
|
+
|
|
12
|
+
`format.md` § Steps has banned `id` since v2.1; the code accepted it anyway, which
|
|
13
|
+
is the fallback `architecture-principles.md` §3 forbids. The whole tree was
|
|
14
|
+
measured compliant first (`automation-gates.md` §3 — the gate arrives with the
|
|
15
|
+
compliance, never ahead of it): 79 cookbook files, 78 pass, one does not.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
[CookbookFormat] step "<step_id|#N>" carries "id" - Expected: "step_id" only
|
|
19
|
+
(api/docs/biz/40-cookbooks/format.md § Steps). Fix: rename "id" to "step_id"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- `normalizeCookbookSteps` throws on any step carrying `id`, with or without a
|
|
23
|
+
`step_id` beside it, and the V2 object shape no longer writes `id` back onto
|
|
24
|
+
the step it normalises.
|
|
25
|
+
- `CookbookTestUtils.validateCookbook` answers with a list rather than an
|
|
26
|
+
exception, so the refusal arrives there as one more entry in `errors`.
|
|
27
|
+
- `stepIdentityOf` reads `step_id` only.
|
|
28
|
+
- `createServiceReadinessTests` synthesised steps with `id`, so the helper
|
|
29
|
+
rejected its own output; it now emits `step_id`.
|
|
30
|
+
|
|
31
|
+
The single file that fails today is `api_biz/meta/tests/cookbooks/list-tenants.json`
|
|
32
|
+
(step `test_list_tenants`, carries both). This package is not published until it
|
|
33
|
+
is fixed.
|
|
34
|
+
|
|
35
|
+
### BREAKING — a step result carries `step_id` only (DÁVKA 77 A)
|
|
36
|
+
|
|
37
|
+
`CookbookTestRunner` emitted both `id` and `step_id` on every step result. Two
|
|
38
|
+
names for one thing means every reader has to know both, and the one that was
|
|
39
|
+
`undefined` for conforming cookbooks is what reached a production log as
|
|
40
|
+
`Step undefined`. Measured before removing it: no run artefact carries a step
|
|
41
|
+
identifier at all — the validation proof, `conn-runtime/validation-failure.json`
|
|
42
|
+
and the integration-run artefact hold counts only — and the sole readers of
|
|
43
|
+
`result.id` in the workspace were inside this package.
|
|
44
|
+
|
|
45
|
+
- `result.id` is gone. Read `result.step_id`.
|
|
46
|
+
- `runPreValidation`'s `failedSteps[]` entries carry `step_id`, not `id`; the
|
|
47
|
+
`biz-ci-gate run-prevalidation` failure line reads it from there.
|
|
48
|
+
- `describeStepIdentity` no longer falls back to `id`: a step that carries one
|
|
49
|
+
is refused upstream, so reading it would only put the retired name back into
|
|
50
|
+
the diagnostics. Such a step is now named by position.
|
|
51
|
+
|
|
52
|
+
### Changed (minor) — `service.port` is no longer required (DÁVKA 75 B)
|
|
53
|
+
|
|
54
|
+
ADR 0005 removed the HTTP surface from biz containers. Nothing listens, so the
|
|
55
|
+
port has no consumer — yet `ServiceStructureValidator` step 1 failed a service
|
|
56
|
+
that did not declare one, forcing every repo to keep a number nobody reads. That
|
|
57
|
+
is the inverse of `change-discipline.md` § "Removing something removes its
|
|
58
|
+
declaration".
|
|
59
|
+
|
|
60
|
+
- `service.port` is dropped from `validateConfigStructure`'s required fields. A
|
|
61
|
+
config without it now passes step 1.
|
|
62
|
+
- A config that still declares one gets a **warning**, `ORPHANED_CONFIG_FIELD`,
|
|
63
|
+
naming ADR 0005 and the fix (delete the field). Silence would leave the eight
|
|
64
|
+
stale declarations recorded in `api/docs/biz/00-model/service-shape.md`
|
|
65
|
+
invisible, which `automation-gates.md` §5 calls a defect. It never fails a
|
|
66
|
+
build.
|
|
67
|
+
- The conformant fixture no longer declares a port, and its README says why.
|
|
68
|
+
|
|
69
|
+
A relaxation, so **minor**: a config that passed before still passes.
|
|
70
|
+
|
|
71
|
+
### Changed (minor) — one name for a step: `step_id` (DÁVKA 75 A)
|
|
72
|
+
|
|
73
|
+
`api/docs/biz/40-cookbooks/format.md` requires `step_id` and bans `id`. Measured
|
|
74
|
+
2026-08-30 across the whole workspace — 101 steps in 77 cookbook files: 94 carry
|
|
75
|
+
`step_id` only, 6 carry `id` only, 1 carries both. All six `id`-only steps were
|
|
76
|
+
this package's own fixtures; the one carrying both is
|
|
77
|
+
`api_biz/meta/tests/cookbooks/list-tenants.json`, another repo's file.
|
|
78
|
+
|
|
79
|
+
- The four fixture cookbooks and the inline cookbooks in four suites now use
|
|
80
|
+
`step_id`, and the assertions read `result.step_id`.
|
|
81
|
+
- `CookbookTestUtils.validateCookbook` **rejected 100 % of the real cookbooks**:
|
|
82
|
+
it demanded `id`, and it knew only the array shape, so every V2 object-shaped
|
|
83
|
+
cookbook came back `"Steps array is required"`. It is reachable from
|
|
84
|
+
`ServiceReadinessValidator.checkCookbookExecution`, so a service using the
|
|
85
|
+
readiness wrapper was told its correct cookbooks were broken. Raw proof before
|
|
86
|
+
the fix: `tester-helpers.json` → `["Step 0: id is required", …×5]`,
|
|
87
|
+
`property/list-statements.json` → `["Steps array is required"]`.
|
|
88
|
+
- Both shapes now go through one owner, `utils/cookbookFormat.normalizeCookbookSteps`,
|
|
89
|
+
which `CookbookTestRunner.normalizeSteps` also delegates to — the two entry
|
|
90
|
+
points can no longer disagree about what a cookbook's steps are.
|
|
91
|
+
- `stepIdentityOf()` reads `step_id`, then `id`. **`id` is still accepted**;
|
|
92
|
+
rejecting it is a separate decision for the owner.
|
|
93
|
+
- What this package synthesises (`generateTestCookbook`, `createWorkflowMessage`,
|
|
94
|
+
`createControlFlowStep`) emits `step_id` only, and `compareCookbooks` reports
|
|
95
|
+
differences as `steps[i].step_id`.
|
|
96
|
+
|
|
97
|
+
`result.id` is still emitted by `CookbookTestRunner` alongside `result.step_id`.
|
|
98
|
+
Whether it survives is a result-contract change and is the owner's call.
|
|
99
|
+
|
|
10
100
|
### Fixed — validator findings pointed at an archived document (DÁVKA 51)
|
|
11
101
|
|
|
12
102
|
`ServiceStructureValidator` sent readers of five findings to
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/conn-orch-validator",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "Validation orchestrator for OA Drive microservices - coordinates validation across all layers (base, infra, orch, business)",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -6,9 +6,9 @@ const crypto = require('crypto');
|
|
|
6
6
|
const MockMQClient = require('./mocks/MockMQClient');
|
|
7
7
|
const MockRegistry = require('./mocks/MockRegistry');
|
|
8
8
|
const { resolveHeaders } = require('./utils/resolveHeaders');
|
|
9
|
-
const { checkCookbookFormatVersion } = require('./utils/cookbookFormat');
|
|
9
|
+
const { checkCookbookFormatVersion, normalizeCookbookSteps } = require('./utils/cookbookFormat');
|
|
10
10
|
const { getTestNamespace } = require('./utils/testNamespace');
|
|
11
|
-
const { describeStepFailure } = require('./utils/stepFailure');
|
|
11
|
+
const { describeStepFailure, describeStepIdentity } = require('./utils/stepFailure');
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* The step's stopwatch, spelled out. A single total hid the fact that
|
|
@@ -124,8 +124,8 @@ class CookbookTestRunner {
|
|
|
124
124
|
// Execute steps
|
|
125
125
|
const cookbookName = cookbookData.description || 'Unnamed';
|
|
126
126
|
const stepResults = [];
|
|
127
|
-
for (const step of stepsArray) {
|
|
128
|
-
const stepResult = await this.executeStep(step, testConfig);
|
|
127
|
+
for (const [stepIndex, step] of stepsArray.entries()) {
|
|
128
|
+
const stepResult = await this.executeStep(step, testConfig, stepIndex);
|
|
129
129
|
// Which cookbook this step came from. Without it a failed step in the
|
|
130
130
|
// aggregate is an id with no file behind it, and a service can carry a
|
|
131
131
|
// dozen cookbooks (utils/stepFailure.js).
|
|
@@ -210,13 +210,23 @@ class CookbookTestRunner {
|
|
|
210
210
|
/**
|
|
211
211
|
* Execute single step
|
|
212
212
|
*/
|
|
213
|
-
async executeStep(step, testConfig) {
|
|
213
|
+
async executeStep(step, testConfig, stepIndex = 0) {
|
|
214
214
|
const startTime = Date.now();
|
|
215
215
|
|
|
216
|
-
this
|
|
216
|
+
// ONE label for this step, resolved once. `step.id` alone was undefined for
|
|
217
|
+
// every cookbook that follows the required v2.1 shape (`step_id`), so the
|
|
218
|
+
// live biz-converter log read `Step undefined: FAILED …` about a cookbook
|
|
219
|
+
// that is correct. Owner of the rule: utils/stepFailure.js.
|
|
220
|
+
const stepLabel = describeStepIdentity(step, stepIndex);
|
|
221
|
+
|
|
222
|
+
this.logger.info(`Executing step: ${stepLabel} (${step.operation})`);
|
|
217
223
|
|
|
218
224
|
const result = {
|
|
219
|
-
|
|
225
|
+
// The identifier the format requires, plus the position, so the aggregate
|
|
226
|
+
// error list can name the step without re-deriving anything. `id` was
|
|
227
|
+
// emitted alongside it until 2026-08-30; one thing, one name.
|
|
228
|
+
step_id: step.step_id,
|
|
229
|
+
stepIndex,
|
|
220
230
|
operation: step.operation,
|
|
221
231
|
passed: false,
|
|
222
232
|
// Three separate numbers, because they answer different questions.
|
|
@@ -273,13 +283,13 @@ class CookbookTestRunner {
|
|
|
273
283
|
});
|
|
274
284
|
|
|
275
285
|
if (result.passed) {
|
|
276
|
-
this.logger.info(`Step ${
|
|
286
|
+
this.logger.info(`Step ${stepLabel}: PASSED (${describeStepTiming(result)})`);
|
|
277
287
|
} else {
|
|
278
288
|
// A verdict without its reason is a silent gate (automation-gates.md
|
|
279
289
|
// §5). Until 2026-08-29 this line said only "FAILED (1101ms)" and the
|
|
280
290
|
// reason — held in result.validationErrors or result.error — reached
|
|
281
291
|
// nobody, which made a failed biz-hello boot undiagnosable from logs.
|
|
282
|
-
this.logger.error(`Step ${
|
|
292
|
+
this.logger.error(`Step ${stepLabel}: FAILED (${describeStepTiming(result)}) — ${describeStepFailure(result)}`);
|
|
283
293
|
}
|
|
284
294
|
|
|
285
295
|
} catch (error) {
|
|
@@ -291,7 +301,7 @@ class CookbookTestRunner {
|
|
|
291
301
|
result.handlerDurationMs = 0;
|
|
292
302
|
result.passed = false;
|
|
293
303
|
|
|
294
|
-
this.logger.error(`Step ${
|
|
304
|
+
this.logger.error(`Step ${stepLabel}: ERROR - ${error.message}\n${error.stack}`);
|
|
295
305
|
}
|
|
296
306
|
|
|
297
307
|
return result;
|
|
@@ -692,25 +702,10 @@ class CookbookTestRunner {
|
|
|
692
702
|
* Supports V1 (steps as array) and V2 (steps as object keyed by step_id)
|
|
693
703
|
*/
|
|
694
704
|
normalizeSteps(steps) {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
// V1 format: steps is already an array
|
|
700
|
-
if (Array.isArray(steps)) {
|
|
701
|
-
return steps;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// V2 format: steps is an object keyed by step_id
|
|
705
|
-
if (typeof steps === 'object') {
|
|
706
|
-
return Object.entries(steps).map(([stepId, step]) => ({
|
|
707
|
-
...step,
|
|
708
|
-
step_id: stepId,
|
|
709
|
-
id: stepId // Keep both for backward compatibility
|
|
710
|
-
}));
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
return [];
|
|
705
|
+
// The rule lives in utils/cookbookFormat — the same owner
|
|
706
|
+
// CookbookTestUtils.validateCookbook now reads, so the two entry points
|
|
707
|
+
// cannot disagree about what a cookbook's steps are.
|
|
708
|
+
return normalizeCookbookSteps(steps);
|
|
714
709
|
}
|
|
715
710
|
|
|
716
711
|
/**
|
|
@@ -748,8 +743,8 @@ class CookbookTestRunner {
|
|
|
748
743
|
}
|
|
749
744
|
|
|
750
745
|
for (const step of stepsArray) {
|
|
751
|
-
if (!step.service) throw new Error(`Step ${step.step_id ||
|
|
752
|
-
if (!step.operation) throw new Error(`Step ${step.step_id ||
|
|
746
|
+
if (!step.service) throw new Error(`Step ${step.step_id || 'unknown'} must have service`);
|
|
747
|
+
if (!step.operation) throw new Error(`Step ${step.step_id || 'unknown'} must have operation`);
|
|
753
748
|
}
|
|
754
749
|
|
|
755
750
|
return true;
|
package/src/CookbookTestUtils.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const {
|
|
4
|
+
MIN_COOKBOOK_FORMAT_VERSION,
|
|
5
|
+
checkCookbookFormatVersion,
|
|
6
|
+
normalizeCookbookSteps,
|
|
7
|
+
stepIdentityOf
|
|
8
|
+
} = require('./utils/cookbookFormat');
|
|
4
9
|
|
|
5
10
|
/**
|
|
6
11
|
* CookbookTestUtils - Utilities for cookbook testing
|
|
@@ -16,7 +21,7 @@ class CookbookTestUtils {
|
|
|
16
21
|
version: MIN_COOKBOOK_FORMAT_VERSION,
|
|
17
22
|
steps: options.steps || [
|
|
18
23
|
{
|
|
19
|
-
|
|
24
|
+
step_id: 'step1',
|
|
20
25
|
type: 'task',
|
|
21
26
|
service: options.serviceName || 'test-service',
|
|
22
27
|
operation: 'testOperation',
|
|
@@ -43,7 +48,7 @@ class CookbookTestUtils {
|
|
|
43
48
|
|
|
44
49
|
for (let i = 1; i <= stepCount; i++) {
|
|
45
50
|
steps.push({
|
|
46
|
-
|
|
51
|
+
step_id: `step${i}`,
|
|
47
52
|
type: 'task',
|
|
48
53
|
service: services[i % services.length],
|
|
49
54
|
operation: `operation${i}`,
|
|
@@ -80,12 +85,26 @@ class CookbookTestUtils {
|
|
|
80
85
|
errors.push(versionProblem);
|
|
81
86
|
}
|
|
82
87
|
|
|
83
|
-
|
|
88
|
+
// Both documented shapes, through the one owner of that rule. Requiring an
|
|
89
|
+
// array here rejected every V2 object-shaped cookbook, and requiring `id`
|
|
90
|
+
// rejected every cookbook that follows format.md — measured 2026-08-30,
|
|
91
|
+
// that was 100 % of the tree.
|
|
92
|
+
let steps;
|
|
93
|
+
try {
|
|
94
|
+
steps = normalizeCookbookSteps(cookbook.steps);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
// The refusal is the finding. This entry point answers with a list, not
|
|
97
|
+
// an exception, so the message becomes one more error in that list.
|
|
98
|
+
errors.push(error.message);
|
|
99
|
+
return { valid: false, errors };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (steps.length === 0) {
|
|
84
103
|
errors.push('Steps array is required');
|
|
85
104
|
} else {
|
|
86
|
-
|
|
87
|
-
if (!step
|
|
88
|
-
errors.push(`Step ${index}:
|
|
105
|
+
steps.forEach((step, index) => {
|
|
106
|
+
if (!stepIdentityOf(step)) {
|
|
107
|
+
errors.push(`Step ${index}: step_id is required`);
|
|
89
108
|
}
|
|
90
109
|
if (!step.type) {
|
|
91
110
|
errors.push(`Step ${index}: type is required`);
|
|
@@ -118,8 +137,8 @@ class CookbookTestUtils {
|
|
|
118
137
|
}
|
|
119
138
|
|
|
120
139
|
// Compare steps
|
|
121
|
-
const steps1 = cookbook1.steps
|
|
122
|
-
const steps2 = cookbook2.steps
|
|
140
|
+
const steps1 = normalizeCookbookSteps(cookbook1.steps);
|
|
141
|
+
const steps2 = normalizeCookbookSteps(cookbook2.steps);
|
|
123
142
|
|
|
124
143
|
if (steps1.length !== steps2.length) {
|
|
125
144
|
differences.push({
|
|
@@ -135,11 +154,11 @@ class CookbookTestUtils {
|
|
|
135
154
|
const step1 = steps1[i];
|
|
136
155
|
const step2 = steps2[i];
|
|
137
156
|
|
|
138
|
-
if (step1
|
|
157
|
+
if (stepIdentityOf(step1) !== stepIdentityOf(step2)) {
|
|
139
158
|
differences.push({
|
|
140
|
-
field: `steps[${i}].
|
|
141
|
-
cookbook1: step1
|
|
142
|
-
cookbook2: step2
|
|
159
|
+
field: `steps[${i}].step_id`,
|
|
160
|
+
cookbook1: stepIdentityOf(step1),
|
|
161
|
+
cookbook2: stepIdentityOf(step2)
|
|
143
162
|
});
|
|
144
163
|
}
|
|
145
164
|
|
|
@@ -198,7 +217,7 @@ class CookbookTestUtils {
|
|
|
198
217
|
*/
|
|
199
218
|
static createControlFlowStep(type, options = {}) {
|
|
200
219
|
const baseStep = {
|
|
201
|
-
|
|
220
|
+
step_id: options.step_id || options.id || `${type}-step-${Date.now()}`,
|
|
202
221
|
type
|
|
203
222
|
};
|
|
204
223
|
|
|
@@ -208,7 +227,7 @@ class CookbookTestUtils {
|
|
|
208
227
|
...baseStep,
|
|
209
228
|
items: options.items || '$api_input.items',
|
|
210
229
|
body: options.body || {
|
|
211
|
-
|
|
230
|
+
step_id: 'foreach-body',
|
|
212
231
|
type: 'task',
|
|
213
232
|
service: 'test-service',
|
|
214
233
|
operation: 'processItem'
|
|
@@ -220,18 +239,18 @@ class CookbookTestUtils {
|
|
|
220
239
|
...baseStep,
|
|
221
240
|
condition: options.condition || '$api_input.type',
|
|
222
241
|
cases: options.cases || [
|
|
223
|
-
{ value: 'type1', step: {
|
|
224
|
-
{ value: 'type2', step: {
|
|
242
|
+
{ value: 'type1', step: { step_id: 'case1', type: 'task', service: 'service1' } },
|
|
243
|
+
{ value: 'type2', step: { step_id: 'case2', type: 'task', service: 'service2' } }
|
|
225
244
|
],
|
|
226
|
-
default: options.default || {
|
|
245
|
+
default: options.default || { step_id: 'default', type: 'task', service: 'default-service' }
|
|
227
246
|
};
|
|
228
247
|
|
|
229
248
|
case 'fork_join':
|
|
230
249
|
return {
|
|
231
250
|
...baseStep,
|
|
232
251
|
branches: options.branches || [
|
|
233
|
-
{
|
|
234
|
-
{
|
|
252
|
+
{ step_id: 'branch1', type: 'task', service: 'service1' },
|
|
253
|
+
{ step_id: 'branch2', type: 'task', service: 'service2' }
|
|
235
254
|
],
|
|
236
255
|
join: options.join || { strategy: 'merge' }
|
|
237
256
|
};
|
package/src/cli/biz-ci-gate.js
CHANGED
|
@@ -334,7 +334,7 @@ async function runRunPreValidation(options) {
|
|
|
334
334
|
|
|
335
335
|
if (!outcome.ok) {
|
|
336
336
|
for (const step of outcome.failedSteps) {
|
|
337
|
-
process.stderr.write(`[BizCiGate] FAIL run-prevalidation — ${step.
|
|
337
|
+
process.stderr.write(`[BizCiGate] FAIL run-prevalidation — ${step.step_id}: ${step.error}\n`);
|
|
338
338
|
for (const validationError of step.validationErrors) {
|
|
339
339
|
process.stderr.write(`[BizCiGate] * ${validationError}\n`);
|
|
340
340
|
}
|
|
@@ -103,7 +103,7 @@ function createServiceReadinessTests(testsDir, options = {}) {
|
|
|
103
103
|
// § Required fields), otherwise this helper rejects its own output.
|
|
104
104
|
version: MIN_COOKBOOK_FORMAT_VERSION,
|
|
105
105
|
steps: Object.entries(operationsFlat).map(([name, op]) => ({
|
|
106
|
-
|
|
106
|
+
step_id: `test-${name}`,
|
|
107
107
|
type: 'task',
|
|
108
108
|
service: serviceName,
|
|
109
109
|
operation: name,
|
|
@@ -88,8 +88,85 @@ function checkCookbookFormatVersion(version) {
|
|
|
88
88
|
return null;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Normalise a cookbook's `steps` into the array shape the rest of the package
|
|
93
|
+
* works with, whichever of the two documented shapes it arrives in, and REFUSE
|
|
94
|
+
* a step that spells its identifier `id`.
|
|
95
|
+
*
|
|
96
|
+
* format.md § Steps documents an array of steps, each carrying `step_id`, and
|
|
97
|
+
* bans `id` outright. Accepting both is how a tree ends up with two names for
|
|
98
|
+
* one thing: measured 2026-08-30, 101 steps in 77 files carried `step_id` and
|
|
99
|
+
* six carried `id`, and every reader had to know about both. The V2 object
|
|
100
|
+
* shape keys the same steps by their `step_id`, so normalising it writes that
|
|
101
|
+
* key back onto the step — and nothing else.
|
|
102
|
+
*
|
|
103
|
+
* Refusal is loud, not lenient: silently reading `id` is the fallback
|
|
104
|
+
* `architecture-principles.md` §3 forbids, and it hides an authoring mistake
|
|
105
|
+
* until something downstream reports `Step undefined`.
|
|
106
|
+
*
|
|
107
|
+
* There is no `id` anywhere any more, at authoring time or at runtime. The
|
|
108
|
+
* adapter that used to write `id = step_id` (`normalizeStepForExecutor`) went
|
|
109
|
+
* with the executor it fed, and `@onlineapps/cookbook-executor` itself was
|
|
110
|
+
* deleted on 2026-08-31 — WorkflowOrchestrator is the single owner of cookbook
|
|
111
|
+
* execution and reads `step_id`. So this refusal is the whole rule, not one
|
|
112
|
+
* half of it: `id` is banned by format.md § Steps, full stop.
|
|
113
|
+
*
|
|
114
|
+
* @param {*} steps the raw value of the cookbook's top-level `steps`
|
|
115
|
+
* @returns {object[]} the steps as an array; `[]` for anything else
|
|
116
|
+
* @throws {Error} when any step carries `id`
|
|
117
|
+
*/
|
|
118
|
+
function normalizeCookbookSteps(steps) {
|
|
119
|
+
if (!steps) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let normalized;
|
|
124
|
+
if (Array.isArray(steps)) {
|
|
125
|
+
normalized = steps;
|
|
126
|
+
} else if (typeof steps === 'object') {
|
|
127
|
+
normalized = Object.entries(steps).map(([stepId, step]) => ({
|
|
128
|
+
...step,
|
|
129
|
+
step_id: stepId
|
|
130
|
+
}));
|
|
131
|
+
} else {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
normalized.forEach((step, index) => {
|
|
136
|
+
if (step && typeof step === 'object' && 'id' in step) {
|
|
137
|
+
const label = stepIdentityOf(step) || `#${index + 1}`;
|
|
138
|
+
throw new Error(
|
|
139
|
+
`[CookbookFormat] step "${label}" carries "id" - Expected: "step_id" only `
|
|
140
|
+
+ `(${DOC_REFERENCE.replace('§ Required fields', '§ Steps')}). Fix: rename "id" to "step_id"`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return normalized;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The identifier a step declares. `step_id` is the ONLY spelling the format
|
|
150
|
+
* allows; `normalizeCookbookSteps` refuses a step that carries `id`, so nothing
|
|
151
|
+
* downstream needs to know that name ever existed.
|
|
152
|
+
*
|
|
153
|
+
* @param {object} step one normalised step
|
|
154
|
+
* @returns {string|null} the declared identifier, or `null` when it declares none
|
|
155
|
+
*/
|
|
156
|
+
function stepIdentityOf(step) {
|
|
157
|
+
if (!step || typeof step !== 'object') {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
if (typeof step.step_id === 'string' && step.step_id.length > 0) {
|
|
161
|
+
return step.step_id;
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
91
166
|
module.exports = {
|
|
92
167
|
MIN_COOKBOOK_FORMAT_VERSION,
|
|
93
168
|
checkCookbookFormatVersion,
|
|
94
|
-
compareVersions
|
|
169
|
+
compareVersions,
|
|
170
|
+
normalizeCookbookSteps,
|
|
171
|
+
stepIdentityOf
|
|
95
172
|
};
|
|
@@ -110,7 +110,7 @@ async function runPreValidation({ serviceRoot, serviceUrl, RunnerClass, ProofGen
|
|
|
110
110
|
const failedSteps = results.steps
|
|
111
111
|
.filter((step) => !step.passed)
|
|
112
112
|
.map((step) => ({
|
|
113
|
-
|
|
113
|
+
step_id: step.step_id || step.operation,
|
|
114
114
|
error: describeStepError(step.error),
|
|
115
115
|
validationErrors: step.validationErrors || []
|
|
116
116
|
}));
|
package/src/utils/stepFailure.js
CHANGED
|
@@ -17,6 +17,40 @@
|
|
|
17
17
|
* (automation-gates.md §5 — silence is a defect).
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Which step this is — for a log line, an error list, anything a human reads.
|
|
22
|
+
*
|
|
23
|
+
* `api/docs/biz/40-cookbooks/format.md` § Required fields makes `step_id` THE
|
|
24
|
+
* identifier of a v2.1 step and bans `id` outright ("an entry carrying `id`
|
|
25
|
+
* instead of `step_id` throws"). CookbookTestRunner read `step.id` anyway, so
|
|
26
|
+
* every conforming cookbook logged `Step undefined: FAILED …` — measured in the
|
|
27
|
+
* live biz-converter log on 2026-08-29 against a cookbook that is correct.
|
|
28
|
+
*
|
|
29
|
+
* The position is the last resort and it is a real answer: `step #3 (operation:
|
|
30
|
+
* convert)` finds the step in the file, which `undefined` never did. A reader
|
|
31
|
+
* who cannot tell which step failed has the same problem as one who was told
|
|
32
|
+
* nothing (automation-gates.md §5).
|
|
33
|
+
*
|
|
34
|
+
* @param {Object} step - a cookbook step, or a step result carrying the same keys
|
|
35
|
+
* @param {number} index - zero-based position within the cookbook
|
|
36
|
+
* @returns {string} never empty, never "undefined"
|
|
37
|
+
*/
|
|
38
|
+
function describeStepIdentity(step, index) {
|
|
39
|
+
if (!step || typeof step !== 'object') {
|
|
40
|
+
throw new Error('[stepFailure] step is required - Expected the cookbook step (or its result) to name in a message');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// `step_id` only. `normalizeCookbookSteps` refuses a step spelling it `id`,
|
|
44
|
+
// so a step reaching here can no longer carry the retired name.
|
|
45
|
+
if (typeof step.step_id === 'string' && step.step_id.length > 0) return step.step_id;
|
|
46
|
+
|
|
47
|
+
const position = Number.isInteger(index) && index >= 0 ? index + 1 : 1;
|
|
48
|
+
const operation = typeof step.operation === 'string' && step.operation.length > 0
|
|
49
|
+
? ` (operation: ${step.operation})`
|
|
50
|
+
: '';
|
|
51
|
+
return `step #${position}${operation}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
20
54
|
/**
|
|
21
55
|
* Why this step failed, in one line, from whichever field holds it.
|
|
22
56
|
*
|
|
@@ -65,9 +99,11 @@ function describeStepFailure(stepResult) {
|
|
|
65
99
|
function describeStepFailureWithContext(stepResult) {
|
|
66
100
|
const reason = describeStepFailure(stepResult);
|
|
67
101
|
const cookbook = stepResult.cookbook || 'unknown cookbook';
|
|
68
|
-
|
|
102
|
+
// One owner for "which step": `unnamed step` used to be the answer for every
|
|
103
|
+
// conforming cookbook, because the result carried neither identifier.
|
|
104
|
+
const stepId = describeStepIdentity(stepResult, stepResult.stepIndex);
|
|
69
105
|
const operation = stepResult.operation ? ` (${stepResult.operation})` : '';
|
|
70
106
|
return `cookbook "${cookbook}" step "${stepId}"${operation}: ${reason}`;
|
|
71
107
|
}
|
|
72
108
|
|
|
73
|
-
module.exports = { describeStepFailure, describeStepFailureWithContext };
|
|
109
|
+
module.exports = { describeStepIdentity, describeStepFailure, describeStepFailureWithContext };
|
|
@@ -549,10 +549,16 @@ class ServiceStructureValidator {
|
|
|
549
549
|
* Note: service.version is now read from package.json (Single Source of Truth)
|
|
550
550
|
*/
|
|
551
551
|
validateConfigStructure(config) {
|
|
552
|
-
// Required fields (version comes from package.json, not config.json)
|
|
552
|
+
// Required fields (version comes from package.json, not config.json).
|
|
553
|
+
//
|
|
554
|
+
// `service.port` was required here until 2026-08-30. ADR 0005 removed the
|
|
555
|
+
// HTTP surface from biz containers, so nothing listens and the number has
|
|
556
|
+
// no consumer — requiring it forced every repo to keep a declaration that
|
|
557
|
+
// means nothing, which is what `change-discipline.md` § "Removing something
|
|
558
|
+
// removes its declaration" exists to prevent. Absence is now correct;
|
|
559
|
+
// presence is reported below as a warning, never as a failure.
|
|
553
560
|
const requiredFields = [
|
|
554
|
-
'service.name'
|
|
555
|
-
'service.port'
|
|
561
|
+
'service.name'
|
|
556
562
|
];
|
|
557
563
|
|
|
558
564
|
for (const field of requiredFields) {
|
|
@@ -573,6 +579,21 @@ class ServiceStructureValidator {
|
|
|
573
579
|
}
|
|
574
580
|
}
|
|
575
581
|
|
|
582
|
+
// A port that is still declared is a stale declaration, not a failure. Say
|
|
583
|
+
// so out loud: `automation-gates.md` §5 — a check that stays silent about a
|
|
584
|
+
// thing it can see is a false guarantee, and the eight services measured in
|
|
585
|
+
// api/docs/biz/00-model/service-shape.md still carry the field.
|
|
586
|
+
if (config.service?.port !== undefined) {
|
|
587
|
+
this.warnings.push({
|
|
588
|
+
type: 'ORPHANED_CONFIG_FIELD',
|
|
589
|
+
path: 'config/service/config.json',
|
|
590
|
+
field: 'service.port',
|
|
591
|
+
value: config.service.port,
|
|
592
|
+
message: 'service.port is declared but nothing listens: ADR 0005 removed the HTTP surface from biz containers',
|
|
593
|
+
fix: 'Remove "port" from the "service" section of config.json'
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
576
597
|
// Validate service name format
|
|
577
598
|
if (config.service?.name) {
|
|
578
599
|
if (!/^[a-z][a-z0-9-]*$/.test(config.service.name)) {
|