@onlineapps/conn-orch-validator 8.1.0 → 10.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 +412 -0
- package/README.md +83 -9
- package/docs/DESIGN.md +21 -7
- package/manifests/biz-service.manifest.json +28 -5
- package/package.json +3 -3
- package/src/CookbookTestRunner.js +84 -16
- package/src/ValidationOrchestrator.js +73 -20
- package/src/cli/biz-ci-gate.js +28 -14
- package/src/cli/oa-sync-template.js +23 -8
- package/src/cli/oa-validate.js +7 -1
- package/src/index.js +21 -13
- package/src/lint/scripts/lintScripts.js +65 -18
- package/src/manifest/checks/composeRunnerBlock.js +37 -20
- package/src/manifest/checks/discoveryOrphan.js +2 -1
- package/src/manifest/checks/docsLintBridge.js +79 -21
- package/src/manifest/checks/gitTracked.js +12 -1
- package/src/manifest/checks/libraryPackage.js +3 -1
- package/src/manifest/checks/libraryWorkspace.js +18 -3
- package/src/manifest/checks/readmeRegion.js +9 -1
- package/src/manifest/checks/serviceConfig.js +29 -12
- package/src/manifest/checks/serviceFiles.js +34 -7
- package/src/manifest/checks/serviceIdentityRows.js +3 -1
- package/src/manifest/checks/serviceRuntime.js +3 -1
- package/src/manifest/discovery.js +25 -7
- package/src/manifest/runManifest.js +58 -7
- package/src/manifest/workspaceRoot.js +91 -5
- package/src/sync/serviceTemplate.js +76 -7
- package/src/sync/sharedEnv.js +11 -4
- package/src/sync/uniformFiles.js +91 -21
- package/src/utils/bizCiGateContract.js +25 -1
- package/src/utils/installContract.js +46 -5
- package/src/utils/libCompat.js +39 -19
- package/src/utils/preValidation.js +56 -11
- package/src/utils/stepFailure.js +106 -19
- package/src/utils/testCoverageContract.js +60 -2
- package/src/utils/throwawaySchema.js +92 -7
- package/src/validatorIdentity.js +31 -0
- package/src/validators/ServiceStructureValidator.js +41 -15
- package/src/validators/ValidationProofGenerator.js +73 -34
- package/templates/business-service/.dockerignore +9 -1
- package/templates/business-service/.gitlab-ci.yml +97 -30
- package/templates/business-service/README.md +14 -5
- package/templates/business-service/config/env-templates/__SERVICE_NAME__.env +17 -5
- package/templates/business-service/config/env-templates/shared.env +7 -1
- package/templates/business-service/docs/80-setup/INSTALL.md +13 -6
- package/templates/business-service/docs/80-setup/PLATFORM_MATRIX.md +1 -1
- package/templates/business-service/docs/80-setup/VALIDATION.md +1 -1
- package/templates/business-service/jest.config.js +9 -1
- package/templates/business-service/package.json.template +1 -1
- package/src/mocks/MockStorage.js +0 -188
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"check": "template-block",
|
|
50
50
|
"path": "init.sh",
|
|
51
51
|
"block": "oa-deps-guard v1",
|
|
52
|
+
"insert_after_function": "oa_npm_install",
|
|
52
53
|
"from": { "package": "templates/business-service/init.sh", "text": true },
|
|
53
54
|
"severity": "deploy",
|
|
54
55
|
"owner": "BIZ-general",
|
|
@@ -100,7 +101,7 @@
|
|
|
100
101
|
"from": { "package": "templates/business-service/gitignore", "text": true },
|
|
101
102
|
"severity": "deploy",
|
|
102
103
|
"owner": "BIZ-general",
|
|
103
|
-
"why": "Dockerfile line COPY . . takes everything .dockerignore does not exclude, and being ignored by git excludes nothing: measured by BIZ-hello 2026-09-11, a LOCAL production image of hello came to 6.6 GB - 5.2 GB of it logs/ - and carried config/env-active/*.env with DB_PASSWORD, JWT_SECRET, the MinIO keys and RABBITMQ_URL. A CI image is clean by accident, because a fresh checkout has no ignored file to copy, so the defect is invisible exactly where the image is built. The entries are DERIVED from the declaration .gitignore already reads rather than written a second time (change-discipline.md \u00a7 One rail per concern), and translated: a slash-less .gitignore pattern matches at every depth while a .dockerignore one is matched from the context root, so *.log there would catch debug.log and not logs/debug.log. The criterion is a comparison and never a list of directories - the files in /app of an image built from the working tree, node_modules aside, are the files of an image built from git archive HEAD",
|
|
104
|
+
"why": "Dockerfile line COPY . . takes everything .dockerignore does not exclude, and being ignored by git excludes nothing: measured by BIZ-hello 2026-09-11, a LOCAL production image of hello came to 6.6 GB - 5.2 GB of it logs/ - and carried config/env-active/*.env with DB_PASSWORD, JWT_SECRET, the MinIO keys and RABBITMQ_URL. A CI image is clean by accident, because a fresh checkout has no ignored file to copy, so the defect is invisible exactly where the image is built. The entries are DERIVED from the declaration .gitignore already reads rather than written a second time (change-discipline.md \u00a7 One rail per concern), and translated: a slash-less .gitignore pattern matches at every depth while a .dockerignore one is matched from the context root, so *.log there would catch debug.log and not logs/debug.log. The criterion is a comparison and never a list of directories - the files in /app of an image built from the working tree, node_modules aside, are the files of an image built from git archive HEAD MINUS the test tree (d.560): tests belong in the repository and never in the artefact that runs, which is the decision the library uniform makes one manifest over as L-PACK-TESTS. The single exception is tests/cookbooks, and it is not a test at all but a declaration the RUNTIME reads - Tier-1 of the boot runs those cookbooks at phase 0.2, and an image without the directory measures zero of them, so ValidationProofGenerator refuses the proof as NO_TESTS and the service never starts",
|
|
104
105
|
"fix": "npx oa-sync-template .dockerignore --target . - it writes the derived entries under one labelled block and rewrites nothing this repository wrote",
|
|
105
106
|
"doc": "api/docs/governance/confirmations/biz-service-manifest.md \u00a72"
|
|
106
107
|
}
|
|
@@ -138,9 +139,9 @@
|
|
|
138
139
|
"from": { "package": "templates/business-service/.gitlab-ci.yml", "text": true },
|
|
139
140
|
"severity": "deploy",
|
|
140
141
|
"owner": "BIZ-general",
|
|
141
|
-
"why": "the platform half of a biz pipeline belongs to the platform, not to the service: it builds the same way, identifies the image by the FULL commit sha (R5), hands the deploy the digest CI itself pushed (R1/R2), and
|
|
142
|
+
"why": "the platform half of a biz pipeline belongs to the platform, not to the service: it builds the same way, identifies the image by the FULL commit sha (R5), hands the deploy the digest CI itself pushed (R1/R2), and runs the uniform twice - continuously, in job validate-uniform on every pipeline including the devel mirror (confirmation 010), and again before the SSH step as the binding last instance (confirmation 008). A service that edits any of that edits the gate that judges it. It is a BLOCK and not the whole file because the other half is genuinely the service's: which database, which ci:gate:* steps and which artefacts its integration needs is a fact about the service, and so - since the block cannot own it - is whether that job's own rules name the devel branch the block's jobs now name. Measured over the eight repositories on 2026-09-11: the template was 151 lines and 7 root keys, the services 176 to 198 lines and 9, and every difference was theirs - the test job that stands up MariaDB, Redis and RabbitMQ as service containers, runs five ci:gate:* steps and publishes ci/integration-signal.json, plus workflow: and verify-installation-contract:. Of those two the first became the block's in d.251b; the second is the template's no longer - d.470 stopped declaring it, its work moved into the rows G-SETUP, D-DB-PACKAGE and D-DB-HEADERS that the uniform gate runs, and each repository keeps its own copy until its release retires it (d.320). A whole-file fix would have deleted all of it and left eight integration tiers running against no database, which is a finding nobody can carry out (automation-gates.md §3, last bullet; controller finding 20)",
|
|
142
143
|
"fix": "npx oa-sync-template .gitlab-ci.yml --target .",
|
|
143
|
-
"doc": "api/docs/governance/confirmations/biz-service-manifest.md \u00a7 Confirmation
|
|
144
|
+
"doc": "api/docs/governance/confirmations/biz-service-manifest.md \u00a7 Confirmation 20260916-biz-service-manifest-010"
|
|
144
145
|
},
|
|
145
146
|
{
|
|
146
147
|
"id": "G-PROD-IMAGE",
|
|
@@ -293,6 +294,17 @@
|
|
|
293
294
|
"fix": "add scripts[\"test:integration:container\"] wrapping the runner this repository declares",
|
|
294
295
|
"doc": "api/docs/governance/confirmations/biz-test-container.md"
|
|
295
296
|
},
|
|
297
|
+
{
|
|
298
|
+
"id": "S-UNIT-C",
|
|
299
|
+
"check": "script-body",
|
|
300
|
+
"name": "test:unit:container",
|
|
301
|
+
"body": "docker compose run --rm ${runner} npm run test:unit",
|
|
302
|
+
"severity": "deploy",
|
|
303
|
+
"owner": "BIZ-general",
|
|
304
|
+
"why": "the unit suite is the measured reason the runner exists (biz-test-container 001: unit 341 MiB beside a 96 MiB service in a 384 MB budget, SIGKILL for both), so it has a wrapper like every other suite; the NAME says which suite runs, the way test:all:container and test:integration:container do, and the five repositories reaching it through test:container were a second spelling of one rail that script-body cannot see",
|
|
305
|
+
"fix": "rename scripts[\"test:container\"] to scripts[\"test:unit:container\"], wrapping the runner this repository declares",
|
|
306
|
+
"doc": "api/docs/governance/confirmations/biz-test-container.md"
|
|
307
|
+
},
|
|
296
308
|
{
|
|
297
309
|
"id": "S-COOKBOOKS",
|
|
298
310
|
"check": "script-body",
|
|
@@ -304,6 +316,17 @@
|
|
|
304
316
|
"why": "the cookbook run is the library's command; a per-service copy of it drifts from the library that owns the dispatch",
|
|
305
317
|
"fix": "set scripts[\"test:cookbooks\"] to the library command and delete the per-service script",
|
|
306
318
|
"doc": "api/docs/governance/confirmations/biz-service-manifest.md §5"
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
"id": "S-COOK-C",
|
|
322
|
+
"check": "script-body",
|
|
323
|
+
"name": "test:cookbooks:container",
|
|
324
|
+
"body": "docker compose run --rm ${runner} npm run test:cookbooks",
|
|
325
|
+
"severity": "deploy",
|
|
326
|
+
"owner": "BIZ-general",
|
|
327
|
+
"why": "the cookbook run dispatches real handlers and reads TESTING_TENANT_ID / TESTING_WORKSPACE_ID, which only the runner has from env_file; without the wrapper the documented path to it is a host command that fails on missing env rather than on the cookbook",
|
|
328
|
+
"fix": "add scripts[\"test:cookbooks:container\"] wrapping the runner this repository declares",
|
|
329
|
+
"doc": "api/docs/governance/confirmations/biz-test-container.md"
|
|
307
330
|
}
|
|
308
331
|
],
|
|
309
332
|
"forbidden": [
|
|
@@ -420,10 +443,10 @@
|
|
|
420
443
|
"id": "G-SHARED-ENV",
|
|
421
444
|
"check": "shared-env-generated",
|
|
422
445
|
"path": "config/env-templates/shared.env",
|
|
423
|
-
"from": { "
|
|
446
|
+
"from": { "path": "api/config/shared-env.json", "text": true },
|
|
424
447
|
"severity": "deploy",
|
|
425
448
|
"owner": "BIZ-general",
|
|
426
|
-
"why": "the shared key set is one fact with one owner; measured on 2026-09-09, none of the nine copies matched the platform file and one key existed in no copy at all. The owner is
|
|
449
|
+
"why": "the shared key set is one fact with one owner; measured on 2026-09-09, none of the nine copies matched the platform file and one key existed in no copy at all. The owner is api/config/shared-env.json and this row renders from it with the renderer the sync command writes with, so a service cannot be synced and reported drifted in the same hour; a run that cannot reach the workspace says NOT RUN and names the command, because a render published with this package answers about the day it was published, not about the SSOT",
|
|
427
450
|
"fix": "npx oa-sync-template shared-env --target .",
|
|
428
451
|
"doc": "api/docs/governance/confirmations/biz-service-manifest.md \u00a718"
|
|
429
452
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/conn-orch-validator",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.0.0",
|
|
4
4
|
"description": "Validation orchestrator for OA Drive microservices - coordinates validation across all layers (base, infra, orch, business)",
|
|
5
5
|
"oa": {
|
|
6
6
|
"category": "orchestration"
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"author": "OnlineApps",
|
|
29
29
|
"license": "PROPRIETARY",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@onlineapps/logger-contract": "
|
|
32
|
-
"@onlineapps/service-validator-core": "2.
|
|
31
|
+
"@onlineapps/logger-contract": "2.0.0",
|
|
32
|
+
"@onlineapps/service-validator-core": "2.1.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"jest": "^29.5.0"
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const crypto = require('crypto');
|
|
6
|
-
const MockMQClient = require('./mocks/MockMQClient');
|
|
7
|
-
const MockRegistry = require('./mocks/MockRegistry');
|
|
8
6
|
const { resolveHeaders } = require('./utils/resolveHeaders');
|
|
9
7
|
const { checkCookbookFormatVersion, readCookbookSteps } = require('./utils/cookbookFormat');
|
|
10
8
|
const { getTestNamespace, assertWorkspaceId } = require('./utils/testNamespace');
|
|
11
|
-
const {
|
|
9
|
+
const {
|
|
10
|
+
describeStepFailure, describeStepIdentity, KIND_STEP, KIND_COOKBOOK_LOAD_FAILURE
|
|
11
|
+
} = require('./utils/stepFailure');
|
|
12
12
|
const { parseHandlerRef, resolveHandlerModule } = require('./utils/handlerRef');
|
|
13
13
|
const { assertLogger } = require('@onlineapps/logger-contract');
|
|
14
14
|
|
|
@@ -76,7 +76,14 @@ function resolveOutputField(container, fieldPath) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
|
-
* CookbookTestRunner — executes cookbook tests
|
|
79
|
+
* CookbookTestRunner — executes cookbook tests in this process, without a transport.
|
|
80
|
+
*
|
|
81
|
+
* There is no infrastructure double. `mockInfrastructure` built a MockMQClient
|
|
82
|
+
* and a MockRegistry for the days when a step was dispatched through a
|
|
83
|
+
* transport; since v3 in-process dispatch there is nothing for them to stand in
|
|
84
|
+
* for, nothing read them, and the database a handler reaches is the SERVICE's
|
|
85
|
+
* real one. The option is retired and refused by name in the constructor and in
|
|
86
|
+
* a cookbook's `test` block — see README § Pre-validation runs in-process.
|
|
80
87
|
*
|
|
81
88
|
* Dispatch is exclusively v3 handler-registry: each operation MUST declare
|
|
82
89
|
* `handler: "path#exportName"` in operations.json. The runner loads the
|
|
@@ -109,9 +116,20 @@ class CookbookTestRunner {
|
|
|
109
116
|
// docs/governance/confirmations/connector-logger-contract.md 001.
|
|
110
117
|
assertLogger('CookbookTestRunner', options.logger, 'the runner writes where the service writes');
|
|
111
118
|
|
|
119
|
+
// A RETIRED option is refused, never ignored: a caller that still writes it
|
|
120
|
+
// believes it buys something, and silently doing nothing is the implicit
|
|
121
|
+
// behaviour `.claude/rules/architecture-principles.md` §8 forbids. Both
|
|
122
|
+
// values are refused — `false` asked for the same absent mechanism as
|
|
123
|
+
// `true`. Why the concept went: `tests/unit/mockInfrastructureRetired.test.js`.
|
|
124
|
+
if (options.mockInfrastructure !== undefined) {
|
|
125
|
+
throw new Error('[CookbookTestRunner] mockInfrastructure is not an option - a cookbook step is '
|
|
126
|
+
+ 'dispatched in-process against the service\'s own v3 handler, with no transport to '
|
|
127
|
+
+ 'stand in for; the database a handler reaches is the real one. '
|
|
128
|
+
+ 'Fix: drop mockInfrastructure from the constructor options.');
|
|
129
|
+
}
|
|
130
|
+
|
|
112
131
|
this.serviceName = options.serviceName;
|
|
113
132
|
this.servicePath = options.servicePath;
|
|
114
|
-
this.mockInfrastructure = options.mockInfrastructure !== false;
|
|
115
133
|
this.timeout = options.timeout;
|
|
116
134
|
if (!this.timeout || typeof this.timeout !== 'number' || this.timeout <= 0) {
|
|
117
135
|
throw new Error('[CookbookTestRunner] timeout is required — Expected positive number (ms)');
|
|
@@ -122,12 +140,6 @@ class CookbookTestRunner {
|
|
|
122
140
|
this._servicePathByName.set(this.serviceName, this.servicePath);
|
|
123
141
|
}
|
|
124
142
|
|
|
125
|
-
// Initialize mocked infrastructure
|
|
126
|
-
if (this.mockInfrastructure) {
|
|
127
|
-
this.mqClient = new MockMQClient();
|
|
128
|
-
this.registry = new MockRegistry();
|
|
129
|
-
}
|
|
130
|
-
|
|
131
143
|
// Test results
|
|
132
144
|
this.results = this._emptyResults();
|
|
133
145
|
}
|
|
@@ -278,6 +290,13 @@ class CookbookTestRunner {
|
|
|
278
290
|
this.results.cookbooks.total += 1;
|
|
279
291
|
this.results.cookbooks.failed += 1;
|
|
280
292
|
this.results.steps.push({
|
|
293
|
+
// WHAT this record is, said here rather than inferred downstream from
|
|
294
|
+
// the absence of `step_id`. It is not a step: this cookbook produced
|
|
295
|
+
// none. Every consumer of `results.steps` reads the field
|
|
296
|
+
// (`utils/stepFailure.js`), and until d.516b the aggregate error list
|
|
297
|
+
// called this `step "step #1"` — a step that does not exist
|
|
298
|
+
// (`.claude/rules/architecture-principles.md` §8).
|
|
299
|
+
kind: KIND_COOKBOOK_LOAD_FAILURE,
|
|
281
300
|
cookbook: file,
|
|
282
301
|
passed: false,
|
|
283
302
|
error: error.message
|
|
@@ -380,6 +399,9 @@ class CookbookTestRunner {
|
|
|
380
399
|
this.logger.info(`Executing step: ${stepLabel} (${step.operation})`);
|
|
381
400
|
|
|
382
401
|
const result = {
|
|
402
|
+
// The other half of the pair: this record IS a step, and says so beside
|
|
403
|
+
// the cookbook-load failure that is not (d.516b).
|
|
404
|
+
kind: KIND_STEP,
|
|
383
405
|
// The identifier the format requires, plus the position, so the aggregate
|
|
384
406
|
// error list can name the step without re-deriving anything. `id` was
|
|
385
407
|
// emitted alongside it until 2026-08-30; one thing, one name.
|
|
@@ -556,6 +578,18 @@ class CookbookTestRunner {
|
|
|
556
578
|
correlation_id: correlationId,
|
|
557
579
|
person_id: personId,
|
|
558
580
|
operation_name: step.operation,
|
|
581
|
+
// WHICH STEP is running, not merely which operation. Production
|
|
582
|
+
// `OperationContext` carries it among its 16 fields
|
|
583
|
+
// (`@onlineapps/service-wrapper` src/OperationContext.js), and a handler
|
|
584
|
+
// that stores a file cannot do without it:
|
|
585
|
+
// `@onlineapps/conn-orch-content-resolver` keys every stored object on
|
|
586
|
+
// `content/<workflow_id>/<step_id>` and refuses to invent either half.
|
|
587
|
+
// Tier-1 left it out, so a service moving off `operation_name` onto
|
|
588
|
+
// `ctx.step_id` passed in production and died in ServiceWrapper phase 0.2
|
|
589
|
+
// (BIZ-pdfgen, measured 2026-09-16 against 9.0.0 and HEAD). It is per
|
|
590
|
+
// STEP on purpose: one cookbook may call the same operation twice, and
|
|
591
|
+
// those two runs must not share a storage prefix.
|
|
592
|
+
step_id: step.step_id,
|
|
559
593
|
service_name: step.service,
|
|
560
594
|
db: null,
|
|
561
595
|
cache: null,
|
|
@@ -591,7 +625,8 @@ class CookbookTestRunner {
|
|
|
591
625
|
workspace_id: ctx.workspace_id,
|
|
592
626
|
workflow_id: ctx.workflow_id,
|
|
593
627
|
correlation_id: ctx.correlation_id,
|
|
594
|
-
person_id: ctx.person_id
|
|
628
|
+
person_id: ctx.person_id,
|
|
629
|
+
step_id: ctx.step_id
|
|
595
630
|
}
|
|
596
631
|
};
|
|
597
632
|
result.request = request;
|
|
@@ -1041,6 +1076,22 @@ class CookbookTestRunner {
|
|
|
1041
1076
|
throw new Error(`[CookbookTestRunner] ${versionProblem}`);
|
|
1042
1077
|
}
|
|
1043
1078
|
|
|
1079
|
+
// The retired option, in the other place a caller could still write it. The
|
|
1080
|
+
// cookbook schema admits additional properties inside `test`, so nothing
|
|
1081
|
+
// refuses the key at load time — and a key nobody reads taught every reader
|
|
1082
|
+
// that a cookbook chooses its own infrastructure, which it never did
|
|
1083
|
+
// (`change-discipline.md` § Removing something removes its declaration).
|
|
1084
|
+
// Refused BY NAME rather than dropped in silence, for the reason the
|
|
1085
|
+
// constructor refuses it one screen up.
|
|
1086
|
+
if (cookbook.test && typeof cookbook.test === 'object'
|
|
1087
|
+
&& cookbook.test.mockInfrastructure !== undefined) {
|
|
1088
|
+
throw new Error('[CookbookTestRunner] Cookbook declares test.mockInfrastructure - the option is '
|
|
1089
|
+
+ 'retired; a cookbook never chose its own infrastructure and the runner dispatches '
|
|
1090
|
+
+ 'in-process against the real service. '
|
|
1091
|
+
+ 'Fix: remove "mockInfrastructure" from the cookbook\'s "test" block '
|
|
1092
|
+
+ '(api/docs/biz/40-cookbooks/format.md § Optional top-level fields).');
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1044
1095
|
if (cookbook.steps === undefined || cookbook.steps === null) {
|
|
1045
1096
|
throw new Error('[CookbookTestRunner] Cookbook has no steps - Expected "steps": [ … ] with at '
|
|
1046
1097
|
+ 'least one step object carrying "step_id". '
|
|
@@ -1055,18 +1106,35 @@ class CookbookTestRunner {
|
|
|
1055
1106
|
+ 'steps array. Fix: add at least one step (api/docs/biz/40-cookbooks/format.md § Required fields).');
|
|
1056
1107
|
}
|
|
1057
1108
|
|
|
1058
|
-
|
|
1109
|
+
// The three fields `@onlineapps/cookbook-core` 5.0.0 requires of a task step
|
|
1110
|
+
// beyond `type` (`schemas/cookbook.v2.schema.json`
|
|
1111
|
+
// definitions.TaskStep.required). `step_id` comes FIRST, because it is what
|
|
1112
|
+
// the other two messages name the step by: until d.516b this guard checked
|
|
1113
|
+
// only the last two and wrote `Step ${step.step_id || 'unknown'}`, so a
|
|
1114
|
+
// cookbook missing the field the whole format addresses steps by was
|
|
1115
|
+
// accepted here and its diagnostics said `Step unknown`. A stand-in name is
|
|
1116
|
+
// the fallback `.claude/rules/architecture-principles.md` §3 forbids; with
|
|
1117
|
+
// the field required, the case it stood in for cannot occur.
|
|
1118
|
+
stepsArray.forEach((step, index) => {
|
|
1119
|
+
const position = index + 1;
|
|
1120
|
+
if (typeof step.step_id !== 'string' || step.step_id.length === 0) {
|
|
1121
|
+
throw new Error(`[CookbookTestRunner] Step #${position} has no step_id - every step of a v2.1 `
|
|
1122
|
+
+ 'cookbook is addressed by its own step_id (@onlineapps/cookbook-core '
|
|
1123
|
+
+ 'schemas/cookbook.v2.schema.json definitions.TaskStep.required). '
|
|
1124
|
+
+ `Fix: add "step_id" to step #${position} `
|
|
1125
|
+
+ '(api/docs/biz/40-cookbooks/format.md § Required fields).');
|
|
1126
|
+
}
|
|
1059
1127
|
if (!step.service) {
|
|
1060
|
-
throw new Error(`[CookbookTestRunner] Step ${step.step_id
|
|
1128
|
+
throw new Error(`[CookbookTestRunner] Step ${step.step_id} must have service - `
|
|
1061
1129
|
+ 'Expected the name of the service that runs the step. Fix: add "service" to that step '
|
|
1062
1130
|
+ '(api/docs/biz/40-cookbooks/format.md § Step definition).');
|
|
1063
1131
|
}
|
|
1064
1132
|
if (!step.operation) {
|
|
1065
|
-
throw new Error(`[CookbookTestRunner] Step ${step.step_id
|
|
1133
|
+
throw new Error(`[CookbookTestRunner] Step ${step.step_id} must have operation - `
|
|
1066
1134
|
+ 'Expected the operation that service exposes. Fix: add "operation" to that step '
|
|
1067
1135
|
+ '(api/docs/biz/40-cookbooks/format.md § Step definition).');
|
|
1068
1136
|
}
|
|
1069
|
-
}
|
|
1137
|
+
});
|
|
1070
1138
|
|
|
1071
1139
|
return true;
|
|
1072
1140
|
}
|
|
@@ -8,10 +8,13 @@ const { normalizeEnvDeclaration, collectEnvCoverage, verifyEnvPresence } = requi
|
|
|
8
8
|
// called. It verified a cached proof before booting; the proof cache was
|
|
9
9
|
// removed (see validate()) and the verifier's last call site went with it,
|
|
10
10
|
// leaving a binding whose only remaining effect was to tell a reader that this
|
|
11
|
-
// class still checks proofs. `ValidationProofCodec`
|
|
12
|
-
// encodes
|
|
13
|
-
|
|
11
|
+
// class still checks proofs. `ValidationProofCodec` went the same way in d.524:
|
|
12
|
+
// finalizeResults() no longer encodes a proof of its own — it asks
|
|
13
|
+
// `ValidationProofGenerator`, which is the one author of one, and the codec is
|
|
14
|
+
// that generator's business.
|
|
14
15
|
const FingerprintUtils = require('@onlineapps/service-validator-core').FingerprintUtils;
|
|
16
|
+
const ValidationProofGenerator = require('./validators/ValidationProofGenerator');
|
|
17
|
+
const { VALIDATOR_VERSION } = require('./validatorIdentity');
|
|
15
18
|
const { ServiceStructureValidator } = require('./validators/ServiceStructureValidator');
|
|
16
19
|
const { describeStepFailureWithContext } = require('./utils/stepFailure');
|
|
17
20
|
const CookbookTestRunner = require('./CookbookTestRunner');
|
|
@@ -38,6 +41,41 @@ const { buildDeployabilitySignal, writeDeployabilitySignal } = require('./manife
|
|
|
38
41
|
*/
|
|
39
42
|
const CONFIG_STEP_ROWS = Object.freeze(['C-SERVICE', 'C-OPS', 'C-CONTRACT']);
|
|
40
43
|
|
|
44
|
+
/**
|
|
45
|
+
* One number a step MEASURED — or a refusal.
|
|
46
|
+
*
|
|
47
|
+
* The run's `totalTests`/`passedTests`/`failedTests` used to be summed with
|
|
48
|
+
* `|| 0`, so a step that measured nothing contributed a zero and the aggregate
|
|
49
|
+
* could not tell "no tests ran" from "the step did not say". The proof's author
|
|
50
|
+
* refuses an unmeasured field (`ValidationProofGenerator.readMeasurement`,
|
|
51
|
+
* d.524), but it never saw one: the fallback had already turned it into a
|
|
52
|
+
* number. That is `.claude/rules/architecture-principles.md` §3 (No Fallbacks)
|
|
53
|
+
* standing exactly where §4 wants a refusal, and the far end paid for it —
|
|
54
|
+
* `ValidationProofCodec.decode()` rejects the resulting proof as NO_TESTS, days
|
|
55
|
+
* later and nowhere near the cause.
|
|
56
|
+
*
|
|
57
|
+
* A measured 0 is untouched: zero failures is the ordinary case. What is
|
|
58
|
+
* refused is the ABSENCE of a measurement.
|
|
59
|
+
*
|
|
60
|
+
* @param {Object} step the step result the aggregate is summing
|
|
61
|
+
* @param {string} field `total`, `passed` or `failed`
|
|
62
|
+
* @param {string} stepName the step's name, for the refusal
|
|
63
|
+
* @returns {number} the measured value
|
|
64
|
+
*/
|
|
65
|
+
function readStepMeasurement(step, field, stepName) {
|
|
66
|
+
const value = step === null || step === undefined ? undefined : step[field];
|
|
67
|
+
|
|
68
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
69
|
+
throw new Error(`[ValidationOrchestrator] Missing measurement - step "${stepName}" reported `
|
|
70
|
+
+ `${JSON.stringify(value) === undefined ? 'undefined' : JSON.stringify(value)} for "${field}", `
|
|
71
|
+
+ 'and the run counts measured values only - a zero standing in for an absent measurement is '
|
|
72
|
+
+ 'what makes a proof of nothing look like a passing validation. '
|
|
73
|
+
+ `Fix: return a finite number as "${field}" from step "${stepName}".`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
|
|
41
79
|
/**
|
|
42
80
|
* ValidationOrchestrator
|
|
43
81
|
*
|
|
@@ -391,9 +429,11 @@ class ValidationOrchestrator {
|
|
|
391
429
|
// Step 5: Cookbook Tests
|
|
392
430
|
this.logger.info('[ValidationOrchestrator] Step 5/7: Cookbook Tests');
|
|
393
431
|
results.steps.cookbooks = await this.runCookbookTests();
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
results.
|
|
432
|
+
// Measured values only — see readStepMeasurement() for why `|| 0` here
|
|
433
|
+
// was the reason the generator's refusal never fired.
|
|
434
|
+
results.totalTests += readStepMeasurement(results.steps.cookbooks, 'total', 'cookbooks');
|
|
435
|
+
results.passedTests += readStepMeasurement(results.steps.cookbooks, 'passed', 'cookbooks');
|
|
436
|
+
results.failedTests += readStepMeasurement(results.steps.cookbooks, 'failed', 'cookbooks');
|
|
397
437
|
if (!results.steps.cookbooks.success) {
|
|
398
438
|
results.success = false;
|
|
399
439
|
results.errors.push(...(results.steps.cookbooks.errors || []));
|
|
@@ -773,7 +813,7 @@ class ValidationOrchestrator {
|
|
|
773
813
|
serviceRoot: this.serviceRoot,
|
|
774
814
|
signal: buildDeployabilitySignal({
|
|
775
815
|
result,
|
|
776
|
-
validatorVersion:
|
|
816
|
+
validatorVersion: VALIDATOR_VERSION
|
|
777
817
|
})
|
|
778
818
|
});
|
|
779
819
|
|
|
@@ -825,21 +865,34 @@ class ValidationOrchestrator {
|
|
|
825
865
|
try {
|
|
826
866
|
// Generate validation proof
|
|
827
867
|
const fingerprint = await this.calculateFingerprint();
|
|
828
|
-
|
|
868
|
+
|
|
869
|
+
// Through the generator, which is the ONE author of a proof. Built here
|
|
870
|
+
// by hand and handed straight to the codec, this proof escaped both of
|
|
871
|
+
// the generator's refusals: an unmeasured field (`readMeasurement`) and
|
|
872
|
+
// a run that executed nothing
|
|
873
|
+
// (`.claude/rules/change-discipline.md` § One rail per concern).
|
|
874
|
+
//
|
|
875
|
+
// The numbers reaching it are measured ones: the aggregate above sums
|
|
876
|
+
// through `readStepMeasurement()`, so an absent measurement fails the
|
|
877
|
+
// run here rather than arriving as a zero the generator cannot tell
|
|
878
|
+
// from a real one (d.576b).
|
|
879
|
+
const generator = new ValidationProofGenerator({
|
|
829
880
|
serviceName: this.serviceName,
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
validator: '@onlineapps/conn-orch-validator',
|
|
833
|
-
validatorVersion: require('../package.json').version,
|
|
834
|
-
validatedAt: new Date().toISOString(),
|
|
835
|
-
testsRun: results.totalTests,
|
|
836
|
-
testsPassed: results.passedTests,
|
|
837
|
-
testsFailed: results.failedTests,
|
|
838
|
-
durationMs: duration
|
|
839
|
-
};
|
|
881
|
+
serviceVersion: this.serviceVersion
|
|
882
|
+
});
|
|
840
883
|
|
|
841
|
-
|
|
842
|
-
|
|
884
|
+
const pkg = this.readServicePackageJson(path.join(this.serviceRoot, 'package.json'));
|
|
885
|
+
|
|
886
|
+
const encodedProof = generator.generateProof(
|
|
887
|
+
{
|
|
888
|
+
total: results.totalTests,
|
|
889
|
+
passed: results.passedTests,
|
|
890
|
+
failed: results.failedTests,
|
|
891
|
+
duration
|
|
892
|
+
},
|
|
893
|
+
pkg.dependencies || {},
|
|
894
|
+
{ contractFingerprint: fingerprint }
|
|
895
|
+
);
|
|
843
896
|
|
|
844
897
|
// Save proof to conn-runtime/
|
|
845
898
|
await this.saveProof(encodedProof);
|
package/src/cli/biz-ci-gate.js
CHANGED
|
@@ -33,6 +33,7 @@ const {
|
|
|
33
33
|
PLATFORM_TEST_FILE_PATTERN,
|
|
34
34
|
} = require('../utils/testCoverageContract');
|
|
35
35
|
const { runPreValidation } = require('../utils/preValidation');
|
|
36
|
+
const { describeRecordIdentity } = require('../utils/stepFailure');
|
|
36
37
|
const { checkLibCompat, loadLibrarySet } = require('../utils/libCompat');
|
|
37
38
|
const { buildSchema } = require('../utils/setupDatabase');
|
|
38
39
|
|
|
@@ -89,7 +90,7 @@ Commands:
|
|
|
89
90
|
contract's closed vocabularies on every live migrations/**/*.sql
|
|
90
91
|
(archive/ and superseded/ excluded), and each BASELINE manifest
|
|
91
92
|
naming exactly the files the installer applies, in that order.
|
|
92
|
-
run-prevalidation Run the service's cookbooks
|
|
93
|
+
run-prevalidation Run the service's cookbooks in-process (no transport, real database)
|
|
93
94
|
and write conn-runtime/validation-proof.json. Replaces the
|
|
94
95
|
per-service scripts/run-pre-validation.js.
|
|
95
96
|
verify-lib-compat Compare @onlineapps/* pins against the platform library SSOT (R6).
|
|
@@ -382,18 +383,6 @@ async function runVerifyContract(options) {
|
|
|
382
383
|
}
|
|
383
384
|
|
|
384
385
|
|
|
385
|
-
/**
|
|
386
|
-
* Running cookbooks needs `tests/cookbooks/`, `operations.json` and the handler
|
|
387
|
-
* modules — nothing else. This command used to resolve the service's own
|
|
388
|
-
* `@onlineapps/service-wrapper`, run `ConfigLoader.loadAll()` and pass
|
|
389
|
-
* `service.url` to the runner, which never read it (`this.serviceUrl` appears
|
|
390
|
-
* nowhere in CookbookTestRunner). `ConfigLoader.loadAll` no longer returns that
|
|
391
|
-
* field either — the owner removed `port` and `url` from the service shape
|
|
392
|
-
* (docs/governance/confirmations/biz-service-port-url.md 001), so the value had
|
|
393
|
-
* been `undefined` for every service on the platform. What the load still did
|
|
394
|
-
* was make a readable config, an installed wrapper and a resolvable `${ENV}`
|
|
395
|
-
* placeholder preconditions of a cookbook run that depends on none of them.
|
|
396
|
-
*/
|
|
397
386
|
/**
|
|
398
387
|
* End the process with this code, once everything written has left the buffers.
|
|
399
388
|
*
|
|
@@ -434,6 +423,27 @@ function exitWhenFlushed(code) {
|
|
|
434
423
|
done();
|
|
435
424
|
}
|
|
436
425
|
|
|
426
|
+
/**
|
|
427
|
+
* What a cookbook run reads, and what it does NOT.
|
|
428
|
+
*
|
|
429
|
+
* It reads `tests/cookbooks/`, `config/service/operations.json` and the
|
|
430
|
+
* service's `package.json`, and it dispatches the service's own v3 handler
|
|
431
|
+
* modules IN THIS PROCESS. So it needs whatever those handlers need: a handler
|
|
432
|
+
* that reaches its schema opens the service's real pool here, which is the
|
|
433
|
+
* subject of `exitWhenFlushed` above. "Nothing else" is what this sentence used
|
|
434
|
+
* to say, and the paragraph below it said the opposite (d.523).
|
|
435
|
+
*
|
|
436
|
+
* What it does not do is load the service CONFIG. This command used to resolve
|
|
437
|
+
* the service's own `@onlineapps/service-wrapper`, run `ConfigLoader.loadAll()`
|
|
438
|
+
* and pass `service.url` to the runner, which never read it (`this.serviceUrl`
|
|
439
|
+
* appears nowhere in CookbookTestRunner). `ConfigLoader.loadAll` no longer
|
|
440
|
+
* returns that field either — the owner removed `port` and `url` from the
|
|
441
|
+
* service shape (docs/governance/confirmations/biz-service-port-url.md 001), so
|
|
442
|
+
* the value had been `undefined` for every service on the platform. What the
|
|
443
|
+
* load still did was make a readable config, an installed wrapper and a
|
|
444
|
+
* resolvable `${ENV}` placeholder preconditions of a cookbook run that depends
|
|
445
|
+
* on none of them.
|
|
446
|
+
*/
|
|
437
447
|
async function runRunPreValidation(options) {
|
|
438
448
|
const outcome = await runPreValidation({
|
|
439
449
|
serviceRoot: path.resolve(options.serviceRoot),
|
|
@@ -451,7 +461,11 @@ async function runRunPreValidation(options) {
|
|
|
451
461
|
|
|
452
462
|
if (!outcome.ok) {
|
|
453
463
|
for (const step of outcome.failedSteps) {
|
|
454
|
-
|
|
464
|
+
// The record names itself: `results.steps` carries two kinds and this
|
|
465
|
+
// line printed `step_id` for both, so a rejected cookbook read as
|
|
466
|
+
// `undefined` (d.523). The identity comes from the one owner of it,
|
|
467
|
+
// through `runPreValidation`'s outcome.
|
|
468
|
+
process.stderr.write(`[BizCiGate] FAIL run-prevalidation — ${describeRecordIdentity(step)}: ${step.error}\n`);
|
|
455
469
|
for (const validationError of step.validationErrors) {
|
|
456
470
|
process.stderr.write(`[BizCiGate] * ${validationError}\n`);
|
|
457
471
|
}
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* * bare paths - the uniform sync of confirmation 001 §3.2. It writes the
|
|
12
12
|
* files of the manifest classes `identical` and `generated` from the very
|
|
13
13
|
* `from:` reference those rows already point the conformance CHECK at, and
|
|
14
|
-
* never touches a file of the `own` class. A row
|
|
15
|
-
*
|
|
14
|
+
* never touches a file of the `own` class. A row that names no `from:`
|
|
15
|
+
* reference is not a row of this run — it is a requirement about the
|
|
16
|
+
* repository, and `npx oa-validate` is what answers it (d.507).
|
|
16
17
|
* * `--new <name>` - the whole service tree, from the template that ships
|
|
17
18
|
* inside this package (001 §2). It is what `api/scripts/add-service.sh
|
|
18
19
|
* --scaffold` runs; that script keeps the platform facts (the registry
|
|
@@ -48,7 +49,7 @@ const {
|
|
|
48
49
|
SSOT_PIN,
|
|
49
50
|
TEMPLATE_ROOT
|
|
50
51
|
} = require('../sync/serviceTemplate');
|
|
51
|
-
const { planSync, uniformRows } = require('../sync/uniformFiles');
|
|
52
|
+
const { planSync, uniformRows, isSyncRow } = require('../sync/uniformFiles');
|
|
52
53
|
const { loadManifest: loadUniformManifest, DEFAULT_MANIFEST_PATH } = require('../manifest/loadManifest');
|
|
53
54
|
const {
|
|
54
55
|
applyUniformRegion,
|
|
@@ -107,10 +108,11 @@ Usage:
|
|
|
107
108
|
is: generated output, committed for reading (001 §5). --check is the gate.
|
|
108
109
|
|
|
109
110
|
With bare paths (or none, meaning all of them) the run rewrites the files the
|
|
110
|
-
manifest declares in the classes "identical" and "generated"
|
|
111
|
-
reference the conformance check reads. Files
|
|
112
|
-
tests/**, the content of docs/** - are never
|
|
113
|
-
|
|
111
|
+
manifest declares in the classes "identical" and "generated" that name a
|
|
112
|
+
"from" reference, from that same reference the conformance check reads. Files
|
|
113
|
+
of the "own" class - src/**, tests/**, the content of docs/** - are never
|
|
114
|
+
written. A row naming no reference is a requirement about the repository
|
|
115
|
+
rather than a file, and npx oa-validate <serviceRoot> is what answers it.
|
|
114
116
|
|
|
115
117
|
--new <name> create a service tree from the packaged template
|
|
116
118
|
--into <dir> where that tree is written; it must not exist yet
|
|
@@ -721,9 +723,22 @@ function runSync(options) {
|
|
|
721
723
|
// The positionals are judged FIRST, against the manifest, and before anything
|
|
722
724
|
// that touches the disk: a mistyped subcommand must be named as what it is,
|
|
723
725
|
// not reported as a missing workspace three steps later.
|
|
724
|
-
const
|
|
726
|
+
const rows = uniformRows(loadUniformManifest(DEFAULT_MANIFEST_PATH));
|
|
727
|
+
const declared = rows.filter(isSyncRow).map((row) => row.path);
|
|
725
728
|
for (const wanted of options.paths) {
|
|
726
729
|
if (declared.includes(wanted)) continue;
|
|
730
|
+
|
|
731
|
+
// A path the uniform DOES declare, as a requirement rather than as a file
|
|
732
|
+
// this run renders, is refused by what it is and not as a typo: the reader
|
|
733
|
+
// named a real row, and what they need is the run that answers it.
|
|
734
|
+
const requirement = rows.find((row) => row.path === wanted);
|
|
735
|
+
if (requirement !== undefined) {
|
|
736
|
+
throw new Error(`[oa-sync-template] "${wanted}" is declared by ${requirement.id}, which names no "from" `
|
|
737
|
+
+ 'reference - it is a requirement about the repository, not a file this run renders. '
|
|
738
|
+
+ 'Fix: check it with npx oa-validate <serviceRoot>; the paths this run writes are '
|
|
739
|
+
+ `(${declared.join(', ')}).`);
|
|
740
|
+
}
|
|
741
|
+
|
|
727
742
|
throw new Error(`[oa-sync-template] Unknown argument "${wanted}" - it is neither a subcommand this run `
|
|
728
743
|
+ `has (${SUBCOMMAND_NAMES.join(', ')}) nor a path the uniform declares (${declared.join(', ')}). `
|
|
729
744
|
+ 'Fix: name one of those, or leave the paths out to write them all.');
|
package/src/cli/oa-validate.js
CHANGED
|
@@ -433,7 +433,13 @@ function main(argv) {
|
|
|
433
433
|
}
|
|
434
434
|
const serviceRoot = canonicalRoot(given);
|
|
435
435
|
|
|
436
|
-
|
|
436
|
+
// The workspace this SERVICE lies in, not the one this package lies in
|
|
437
|
+
// (d.540): the copy a service runs is the one it installed, and an installed
|
|
438
|
+
// copy is part of no checkout — `oa-validate .` inside a complete workspace
|
|
439
|
+
// printed NOT RESOLVED and reported every workspace row NOT RUN. The root is
|
|
440
|
+
// the one the caller named, never `process.cwd()`
|
|
441
|
+
// (`workspaceRoot.js` § The service a run is ABOUT).
|
|
442
|
+
const workspaceRoot = resolveWorkspaceRoot({ explicit: options.workspace, serviceRoot });
|
|
437
443
|
const result = runManifest({ manifest, serviceRoot, workspaceRoot });
|
|
438
444
|
|
|
439
445
|
process.stdout.write(options.json ? renderJson(result) : renderReport(result));
|
package/src/index.js
CHANGED
|
@@ -12,10 +12,6 @@
|
|
|
12
12
|
* @see /api/docs/architecture/validator.md (validator architecture)
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
const MockMQClient = require('./mocks/MockMQClient');
|
|
16
|
-
const MockRegistry = require('./mocks/MockRegistry');
|
|
17
|
-
const MockStorage = require('./mocks/MockStorage');
|
|
18
|
-
|
|
19
15
|
const CookbookTestUtils = require('./CookbookTestUtils');
|
|
20
16
|
const { ServiceStructureValidator } = require('./validators/ServiceStructureValidator');
|
|
21
17
|
const ValidationProofGenerator = require('./validators/ValidationProofGenerator');
|
|
@@ -34,11 +30,16 @@ const ValidationOrchestrator = require('./ValidationOrchestrator');
|
|
|
34
30
|
|
|
35
31
|
const { createServiceReadinessTests } = require('./helpers/createServiceReadinessTests');
|
|
36
32
|
|
|
33
|
+
// No infrastructure doubles are published. `MockMQClient`, `MockRegistry` and
|
|
34
|
+
// `MockStorage` were the furniture of `mockInfrastructure`, retired in d.576:
|
|
35
|
+
// pre-validation dispatches a step in-process against the service's own v3
|
|
36
|
+
// handler, so there is no transport to stand in for, and the database a handler
|
|
37
|
+
// reaches is the real one. `MockRegistry` stays inside the package as the
|
|
38
|
+
// registry stand-in of `helpers/createServiceReadinessTests`, which is a
|
|
39
|
+
// different concern and keeps its own consumer; `MockMQClient` as the double of
|
|
40
|
+
// `tests/unit/RegistrationFlow.test.js`. `MockStorage` had neither, so d.583
|
|
41
|
+
// deleted the file as well as the export (`tests/unit/mockInfrastructureRetired.test.js`).
|
|
37
42
|
module.exports = {
|
|
38
|
-
get MockMQClient() { return MockMQClient; },
|
|
39
|
-
get MockRegistry() { return MockRegistry; },
|
|
40
|
-
get MockStorage() { return MockStorage; },
|
|
41
|
-
|
|
42
43
|
get CookbookTestUtils() { return CookbookTestUtils; },
|
|
43
44
|
get ServiceReadinessValidator() { return ServiceReadinessValidator; },
|
|
44
45
|
get CookbookTestRunner() { return CookbookTestRunner; },
|
|
@@ -82,11 +83,18 @@ module.exports = {
|
|
|
82
83
|
get parseHandlerRef() { return require('./utils/handlerRef').parseHandlerRef; },
|
|
83
84
|
get resolveHandlerModule() { return require('./utils/handlerRef').resolveHandlerModule; },
|
|
84
85
|
|
|
86
|
+
// The other half of what a service's integration harness needs to build its
|
|
87
|
+
// throwaway schema: which .sql files make up the set, and in which order.
|
|
88
|
+
// `createThrowawaySchema` above resolves the plan itself; a harness that
|
|
89
|
+
// builds its own schema step by step reads the plan directly, and had to
|
|
90
|
+
// reach into `src/utils/setupDatabase` to get it — a path inside the package
|
|
91
|
+
// is not a contract, and every move of `src/utils/` would break a consumer
|
|
92
|
+
// that never agreed to one. Exported as the identical function, not a
|
|
93
|
+
// wrapper: one definition, one behaviour (`change-discipline.md` § One rail
|
|
94
|
+
// per concern).
|
|
95
|
+
get resolveMigrationPlan() { return require('./utils/setupDatabase').resolveMigrationPlan; },
|
|
96
|
+
|
|
85
97
|
get createServiceReadinessTests() { return createServiceReadinessTests; },
|
|
86
98
|
get BizCiGateContract() { return BizCiGateContract; },
|
|
87
|
-
get IntegrationRun() { return require('./utils/integrationRun'); }
|
|
88
|
-
|
|
89
|
-
createMockMQ: () => new MockMQClient(),
|
|
90
|
-
createMockRegistry: () => new MockRegistry(),
|
|
91
|
-
createMockStorage: () => new MockStorage()
|
|
99
|
+
get IntegrationRun() { return require('./utils/integrationRun'); }
|
|
92
100
|
};
|