@onlineapps/conn-orch-validator 7.0.0 → 8.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 +2558 -2
- package/README.md +1038 -4
- package/docs/DESIGN.md +3 -1
- package/manifests/biz-service.manifest.json +658 -0
- package/manifests/library.manifest.json +324 -0
- package/package.json +12 -6
- package/src/CookbookTestRunner.js +408 -101
- package/src/CookbookTestUtils.js +7 -8
- package/src/ServiceReadinessValidator.js +10 -35
- package/src/ValidationOrchestrator.js +219 -71
- package/src/cli/biz-ci-gate.js +176 -33
- package/src/cli/oa-lint-scripts.js +221 -0
- package/src/cli/oa-sync-template.js +1020 -0
- package/src/cli/oa-validate.js +474 -0
- package/src/helpers/README.md +2 -1
- package/src/helpers/createServiceReadinessTests.js +60 -4
- package/src/index.js +33 -3
- package/src/lint/scripts/lintScripts.js +298 -0
- package/src/manifest/checks/composeRunnerBlock.js +222 -0
- package/src/manifest/checks/composeShape.js +165 -0
- package/src/manifest/checks/contractBridge.js +181 -0
- package/src/manifest/checks/discoveryOrphan.js +50 -0
- package/src/manifest/checks/docsLintBridge.js +553 -0
- package/src/manifest/checks/fileAbsent.js +35 -0
- package/src/manifest/checks/gitTracked.js +204 -0
- package/src/manifest/checks/index.js +111 -0
- package/src/manifest/checks/libraryContext.js +226 -0
- package/src/manifest/checks/libraryDocs.js +75 -0
- package/src/manifest/checks/libraryPackage.js +272 -0
- package/src/manifest/checks/librarySource.js +274 -0
- package/src/manifest/checks/libraryTests.js +121 -0
- package/src/manifest/checks/libraryWorkspace.js +293 -0
- package/src/manifest/checks/readmeRegion.js +135 -0
- package/src/manifest/checks/scriptHeaders.js +79 -0
- package/src/manifest/checks/serviceConfig.js +390 -0
- package/src/manifest/checks/serviceConnectors.js +81 -0
- package/src/manifest/checks/serviceDb.js +388 -0
- package/src/manifest/checks/serviceFiles.js +754 -0
- package/src/manifest/checks/serviceIdentityRows.js +351 -0
- package/src/manifest/checks/serviceRuntime.js +295 -0
- package/src/manifest/checks/serviceScripts.js +213 -0
- package/src/manifest/deployabilitySignal.js +121 -0
- package/src/manifest/discovery.js +386 -0
- package/src/manifest/loadManifest.js +62 -0
- package/src/manifest/manifestShape.js +446 -0
- package/src/manifest/report.js +245 -0
- package/src/manifest/runManifest.js +449 -0
- package/src/manifest/serviceIdentity.js +140 -0
- package/src/manifest/walk.js +74 -0
- package/src/manifest/workspaceRoot.js +242 -0
- package/src/mocks/MockMQClient.js +13 -30
- package/src/mocks/MockRegistry.js +4 -2
- package/src/mocks/MockStorage.js +4 -2
- package/src/sync/docsRegion.js +463 -0
- package/src/sync/generatedRegion.js +228 -0
- package/src/sync/readmeLocation.js +182 -0
- package/src/sync/readmePointer.js +477 -0
- package/src/sync/serviceTemplate.js +583 -0
- package/src/sync/sharedEnv.js +162 -0
- package/src/sync/uniformFiles.js +474 -0
- package/src/utils/bizCiGateContract.js +131 -7
- package/src/utils/connectorContract.js +97 -7
- package/src/utils/cookbookFormat.js +81 -40
- package/src/utils/deployContract.js +140 -9
- package/src/utils/envContract.js +57 -1
- package/src/utils/handlerRef.js +181 -0
- package/src/utils/installContract.js +287 -41
- package/src/utils/libCompat.js +29 -7
- package/src/utils/migrationOrder.js +163 -0
- package/src/utils/preValidation.js +20 -7
- package/src/utils/setupDatabase.js +194 -13
- package/src/utils/testCoverageContract.js +539 -0
- package/src/utils/testNamespace.js +247 -23
- package/src/utils/throwawaySchema.js +207 -0
- package/src/validators/ServiceStructureValidator.js +2 -1
- package/templates/business-service/.dockerignore +42 -0
- package/templates/business-service/.gitlab-ci.yml +290 -0
- package/templates/business-service/Dockerfile +27 -0
- package/templates/business-service/README.md +213 -0
- package/templates/business-service/config/biz-docs-lint.tree.json +10 -0
- package/templates/business-service/config/env-templates/__SERVICE_NAME__.env +4 -0
- package/templates/business-service/config/env-templates/shared.env +65 -0
- package/templates/business-service/config/service/config.json +14 -0
- package/templates/business-service/config/service/integration-contract.json +12 -0
- package/templates/business-service/config/service/operations.json +41 -0
- package/templates/business-service/docker-compose.production.yml +60 -0
- package/templates/business-service/docker-compose.yml +93 -0
- package/templates/business-service/docs/80-setup/INSTALL.md +101 -0
- package/templates/business-service/docs/80-setup/PLATFORM_MATRIX.md +65 -0
- package/templates/business-service/docs/80-setup/README.md +18 -0
- package/templates/business-service/docs/80-setup/VALIDATION.md +78 -0
- package/templates/business-service/docs/README.md +18 -0
- package/templates/business-service/gitignore +42 -0
- package/templates/business-service/index.js +10 -0
- package/templates/business-service/init.sh +54 -0
- package/templates/business-service/jest.config.js +6 -0
- package/templates/business-service/package.json.template +31 -0
- package/templates/business-service/scripts/verify-deploy-uniform.sh +180 -0
- package/templates/business-service/src/handlers/v3/echo.js +39 -0
- package/templates/business-service/tests/cookbooks/echo.json +36 -0
- package/templates/business-service/tests/unit/handler.test.js +78 -0
- package/src/WorkflowTestRunner.js +0 -402
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The npm scripts of a biz service: the ones the uniform requires, with their
|
|
5
|
+
* canonical body, and the ones it forbids (confirmation `biz-service-manifest`
|
|
6
|
+
* 001 §2, § scripts).
|
|
7
|
+
*
|
|
8
|
+
* Why a body and not just a name: measured on 2026-09-09, seven of the eight
|
|
9
|
+
* services declare `test:all:container` in seven different spellings — and the
|
|
10
|
+
* only thing that differs is the container name. A row that compared the text
|
|
11
|
+
* would report seven findings and mean none of them, so the canonical body is
|
|
12
|
+
* PARAMETERISED by `${runner}`, and the runner is read where it is declared: the
|
|
13
|
+
* compose service carrying `profiles: [test]` (lead decision `api/shared/TODO.md`
|
|
14
|
+
* §0.2b-9 point 2 — one owner for that name, the compose file).
|
|
15
|
+
*
|
|
16
|
+
* @see api/docs/governance/confirmations/biz-service-manifest.md §2
|
|
17
|
+
* @see api/docs/governance/confirmations/biz-test-container.md
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
|
|
23
|
+
const { readComposeServices } = require('./composeShape');
|
|
24
|
+
|
|
25
|
+
/** Where the runner's name is declared, and the profile that identifies it. */
|
|
26
|
+
const DEV_COMPOSE = 'docker-compose.yml';
|
|
27
|
+
const TEST_PROFILE = 'test';
|
|
28
|
+
|
|
29
|
+
/** The placeholder a canonical body spends on the one name that is per-repository. */
|
|
30
|
+
const RUNNER_PLACEHOLDER = '${runner}';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The npm scripts that run without anybody naming them. `pre<x>`/`post<x>` fire
|
|
34
|
+
* around `<x>`; the rest fire on an npm command (install, publish, pack).
|
|
35
|
+
* npm owns this list — the check cites it, and the row states the consequence.
|
|
36
|
+
*/
|
|
37
|
+
const NPM_LIFECYCLE = Object.freeze([
|
|
38
|
+
'preinstall', 'install', 'postinstall',
|
|
39
|
+
'prepublish', 'prepublishOnly', 'prepack', 'postpack', 'prepare'
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} serviceRoot repository root
|
|
44
|
+
* @returns {{ scripts: object, present: boolean }}
|
|
45
|
+
*/
|
|
46
|
+
function readScripts(serviceRoot) {
|
|
47
|
+
const file = path.join(serviceRoot, 'package.json');
|
|
48
|
+
if (!fs.existsSync(file)) return { scripts: {}, present: false };
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
51
|
+
return { scripts: parsed.scripts || {}, present: true };
|
|
52
|
+
} catch (cause) {
|
|
53
|
+
throw new Error(`[ManifestScripts] package.json is not valid JSON - ${file}. `
|
|
54
|
+
+ 'Fix: repair it; every row about a script reads it.', { cause });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The name of the one-shot test runner this repository declares, or null.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} serviceRoot repository root
|
|
62
|
+
* @returns {string|null}
|
|
63
|
+
*/
|
|
64
|
+
function runnerName(serviceRoot) {
|
|
65
|
+
const file = path.join(serviceRoot, DEV_COMPOSE);
|
|
66
|
+
if (!fs.existsSync(file)) return null;
|
|
67
|
+
const services = readComposeServices(fs.readFileSync(file, 'utf8'));
|
|
68
|
+
for (const [name, node] of services) {
|
|
69
|
+
if ((node.sequences.get('profiles') || []).includes(TEST_PROFILE)) return name;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The same command written the other way: `node <script> args…` is what a
|
|
76
|
+
* repository writes before the library's `bin` is on its PATH, `<bin> args…`
|
|
77
|
+
* after. Both spell one body, so a service switching between them at a version
|
|
78
|
+
* bump is not a finding (lead brief, d.210a).
|
|
79
|
+
*
|
|
80
|
+
* @param {string} body the canonical body
|
|
81
|
+
* @param {string} bin the bin name the row names
|
|
82
|
+
* @returns {string}
|
|
83
|
+
*/
|
|
84
|
+
function binSpelling(body, bin) {
|
|
85
|
+
const tokens = body.trim().split(/\s+/);
|
|
86
|
+
return [bin, ...tokens.slice(2)].join(' ');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Does this repository have an integration suite to run?
|
|
91
|
+
*
|
|
92
|
+
* The question decides whether `test:integration:container` is owed at all: a
|
|
93
|
+
* script pointing at an absent suite is the dead declaration
|
|
94
|
+
* `change-discipline.md` § "Removing something removes its declaration"
|
|
95
|
+
* forbids, and jest exits 1 on a path that matches no test — so requiring it
|
|
96
|
+
* unconditionally would ship a red script to every new service.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} serviceRoot repository root
|
|
99
|
+
* @param {string} relative the suite directory the row names
|
|
100
|
+
* @returns {boolean}
|
|
101
|
+
*/
|
|
102
|
+
function hasSuite(serviceRoot, relative) {
|
|
103
|
+
const dir = path.join(serviceRoot, ...relative.split('/'));
|
|
104
|
+
return fs.existsSync(dir) && fs.statSync(dir).isDirectory() && fs.readdirSync(dir).length > 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const scriptBody = Object.freeze({
|
|
108
|
+
scope: 'service',
|
|
109
|
+
requires: Object.freeze(['name', 'body']),
|
|
110
|
+
|
|
111
|
+
run({ row, serviceRoot }) {
|
|
112
|
+
if (row.only_with !== undefined && !hasSuite(serviceRoot, row.only_with)) return [];
|
|
113
|
+
|
|
114
|
+
const { scripts, present } = readScripts(serviceRoot);
|
|
115
|
+
if (!present) return [{ where: 'package.json', what: 'absent — nothing declares this directory a service' }];
|
|
116
|
+
|
|
117
|
+
const runner = row.body.includes(RUNNER_PLACEHOLDER) ? runnerName(serviceRoot) : null;
|
|
118
|
+
const expected = runner === null ? row.body : row.body.split(RUNNER_PLACEHOLDER).join(runner);
|
|
119
|
+
const unnamed = row.body.includes(RUNNER_PLACEHOLDER) && runner === null
|
|
120
|
+
? ', and this repository declares no runner to name'
|
|
121
|
+
: '';
|
|
122
|
+
const alsoSpelled = row.bin === undefined
|
|
123
|
+
? ''
|
|
124
|
+
: ` (the ${row.bin} bin spelling of it counts as the same body)`;
|
|
125
|
+
|
|
126
|
+
const declared = scripts[row.name];
|
|
127
|
+
if (declared === undefined) {
|
|
128
|
+
return [{
|
|
129
|
+
where: 'package.json',
|
|
130
|
+
what: `declares no script ${JSON.stringify(row.name)} — the uniform's body is `
|
|
131
|
+
+ `${JSON.stringify(expected)}${unnamed}${alsoSpelled}`
|
|
132
|
+
}];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const accepted = [expected];
|
|
136
|
+
if (row.bin !== undefined) accepted.push(binSpelling(expected, row.bin));
|
|
137
|
+
if (accepted.includes(declared.trim())) return [];
|
|
138
|
+
|
|
139
|
+
return [{
|
|
140
|
+
where: 'package.json',
|
|
141
|
+
what: `script ${JSON.stringify(row.name)} is ${JSON.stringify(declared)}, the uniform's body is `
|
|
142
|
+
+ `${JSON.stringify(expected)}${unnamed}${alsoSpelled}`
|
|
143
|
+
}];
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const scriptForbidden = Object.freeze({
|
|
148
|
+
scope: 'service',
|
|
149
|
+
requires: Object.freeze(['name']),
|
|
150
|
+
|
|
151
|
+
run({ row, serviceRoot }) {
|
|
152
|
+
const { scripts } = readScripts(serviceRoot);
|
|
153
|
+
if (scripts[row.name] === undefined) return [];
|
|
154
|
+
|
|
155
|
+
const because = row.why ? `: ${row.why}` : '';
|
|
156
|
+
return [{
|
|
157
|
+
where: 'package.json',
|
|
158
|
+
what: `script ${JSON.stringify(row.name)} is declared — this uniform forbids it${because}`
|
|
159
|
+
}];
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const scriptImplicit = Object.freeze({
|
|
164
|
+
scope: 'service',
|
|
165
|
+
requires: Object.freeze([]),
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* A script that fires because something else ran. Two shapes, one rule: npm's
|
|
169
|
+
* own lifecycle names, and a `pre`/`post` hook — including one whose target
|
|
170
|
+
* this package does not declare, which fires on nothing at all and is a
|
|
171
|
+
* declaration with nothing behind it.
|
|
172
|
+
*/
|
|
173
|
+
run({ serviceRoot }) {
|
|
174
|
+
const { scripts } = readScripts(serviceRoot);
|
|
175
|
+
const findings = [];
|
|
176
|
+
|
|
177
|
+
for (const name of Object.keys(scripts)) {
|
|
178
|
+
if (NPM_LIFECYCLE.includes(name)) {
|
|
179
|
+
findings.push({
|
|
180
|
+
where: 'package.json',
|
|
181
|
+
what: `script ${JSON.stringify(name)} is an npm lifecycle hook — it fires on an npm command, `
|
|
182
|
+
+ 'not on a name; a script runs when it is named, never as a side effect'
|
|
183
|
+
});
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const hook = /^(pre|post)(.+)$/.exec(name);
|
|
188
|
+
if (hook === null) continue;
|
|
189
|
+
const target = hook[2];
|
|
190
|
+
|
|
191
|
+
findings.push({
|
|
192
|
+
where: 'package.json',
|
|
193
|
+
what: scripts[target] === undefined
|
|
194
|
+
? `script ${JSON.stringify(name)} fires on "npm run ${target}", which this package does not declare — `
|
|
195
|
+
+ 'a script runs when it is named, never as a side effect'
|
|
196
|
+
: `script ${JSON.stringify(name)} fires as a side effect of "npm run ${target}" — `
|
|
197
|
+
+ 'a script runs when it is named, never as a side effect'
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return findings;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
module.exports = {
|
|
206
|
+
checks: [
|
|
207
|
+
{ name: 'script-body', check: scriptBody },
|
|
208
|
+
{ name: 'script-forbidden', check: scriptForbidden },
|
|
209
|
+
{ name: 'script-implicit', check: scriptImplicit }
|
|
210
|
+
],
|
|
211
|
+
binSpelling,
|
|
212
|
+
runnerName
|
|
213
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `ci/deployability.json` — the machine half of the manifest verdict.
|
|
5
|
+
*
|
|
6
|
+
* Confirmation `biz-service-manifest` 001 §4: a `deploy` finding lets the
|
|
7
|
+
* service start and makes it undeployable. The banner says that to a human; this
|
|
8
|
+
* file says it to `deploy-production`, which refuses on a single row and names
|
|
9
|
+
* it. Its shape follows the signal this package already writes,
|
|
10
|
+
* `ci/integration-signal.json` (`utils/bizCiGateContract.js`
|
|
11
|
+
* buildIntegrationSignalSummary), with three deliberate differences:
|
|
12
|
+
*
|
|
13
|
+
* - **no `generatedAt`.** The integration signal stamps every write, so two
|
|
14
|
+
* runs over an unchanged repository produce two different files. This one is
|
|
15
|
+
* a verdict about the tree, not about the moment, and an unchanged tree must
|
|
16
|
+
* produce an unchanged file — otherwise nothing can tell a re-run from a
|
|
17
|
+
* change.
|
|
18
|
+
* - **`validatorVersion` instead of a version of its own.** The manifest ships
|
|
19
|
+
* inside this package, so the manifest version IS the validator version and
|
|
20
|
+
* the service's pin decides which shape it was measured against
|
|
21
|
+
* (confirmation 004 § point 2, `loadManifest.js`).
|
|
22
|
+
* - **`deployable` instead of a `gate: {verdict, reason}` block.** The verdict
|
|
23
|
+
* is one boolean and its reasons are the findings; a second field restating
|
|
24
|
+
* it would be a second owner of one fact.
|
|
25
|
+
*
|
|
26
|
+
* `notRun` is carried on purpose: inside a service container the workspace SSOTs
|
|
27
|
+
* are not present, so workspace-scoped rows cannot look. A signal that stayed
|
|
28
|
+
* silent about them would read as "everything checked, nothing found"
|
|
29
|
+
* (`.claude/rules/automation-gates.md` §5). It does not change the verdict —
|
|
30
|
+
* a row that did not run is not a finding.
|
|
31
|
+
*
|
|
32
|
+
* **`complete` is what the gate reads beside `deployable`** (d.230).
|
|
33
|
+
* `deployable` is true when nothing blocked among the rows that RAN; `complete`
|
|
34
|
+
* is true when no row that CAN block was skipped. In a container the second is
|
|
35
|
+
* routinely false, and until it was written down the deploy gate had no way to
|
|
36
|
+
* tell a proven tree from an unexamined one — it was honouring a `--skip` nobody
|
|
37
|
+
* had flagged (`automation-gates.md` §1.5). The deploy gate takes a signal only
|
|
38
|
+
* when both are true; the complete run is produced from the api checkout, with
|
|
39
|
+
* `npx oa-validate --workspace <root>`. Neither field changes an exit code: a
|
|
40
|
+
* partial run in the container is the intent, and completeness is the gate's
|
|
41
|
+
* question, not the boot's.
|
|
42
|
+
*
|
|
43
|
+
* @see api/docs/governance/confirmations/biz-service-manifest.md §4
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
const fs = require('fs');
|
|
47
|
+
const path = require('path');
|
|
48
|
+
|
|
49
|
+
/** Where the signal lives, relative to the service root. One place, named once. */
|
|
50
|
+
const SIGNAL_RELATIVE_PATH = 'ci/deployability.json';
|
|
51
|
+
|
|
52
|
+
const PACKAGE_NAME = '@onlineapps/conn-orch-validator';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {object} params
|
|
56
|
+
* @param {object} params.result the value `runManifest` returned
|
|
57
|
+
* @param {string} params.validatorVersion the running package version — the manifest version
|
|
58
|
+
* @returns {{validator: string, validatorVersion: string, uniform: string, serviceRoot: string,
|
|
59
|
+
* deployable: boolean, complete: boolean, findings: Array<object>, notRun: Array<object>}}
|
|
60
|
+
*/
|
|
61
|
+
function buildDeployabilitySignal({ result, validatorVersion }) {
|
|
62
|
+
if (result === null || typeof result !== 'object') {
|
|
63
|
+
throw new Error('[DeployabilitySignal] Manifest result is required - buildDeployabilitySignal({ result }) got '
|
|
64
|
+
+ `${JSON.stringify(result)}. Fix: pass the value runManifest() returned.`);
|
|
65
|
+
}
|
|
66
|
+
if (typeof validatorVersion !== 'string' || validatorVersion.length === 0) {
|
|
67
|
+
throw new Error('[DeployabilitySignal] Validator version is required - buildDeployabilitySignal('
|
|
68
|
+
+ `{ validatorVersion }) got ${JSON.stringify(validatorVersion)}. `
|
|
69
|
+
+ 'Fix: pass require("../package.json").version; the manifest version IS the validator version.');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (typeof result.complete !== 'boolean') {
|
|
73
|
+
throw new Error('[DeployabilitySignal] Manifest result does not say whether it was complete - '
|
|
74
|
+
+ `buildDeployabilitySignal({ result }) got complete=${JSON.stringify(result.complete)}. `
|
|
75
|
+
+ 'Fix: pass the value runManifest() returned; a signal without it would let a gate read a '
|
|
76
|
+
+ 'partial run as a proven one.');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
validator: PACKAGE_NAME,
|
|
81
|
+
validatorVersion,
|
|
82
|
+
uniform: result.uniform,
|
|
83
|
+
serviceRoot: result.serviceRoot,
|
|
84
|
+
deployable: result.ok,
|
|
85
|
+
complete: result.complete,
|
|
86
|
+
findings: result.findings,
|
|
87
|
+
notRun: result.notRun
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Write the signal into `<serviceRoot>/ci/`, creating the directory.
|
|
93
|
+
*
|
|
94
|
+
* There is no switch that skips the write and no environment variable that
|
|
95
|
+
* moves it: the step that produces the verdict is the step that records it
|
|
96
|
+
* (`.claude/rules/automation-gates.md` §1 requirement 5).
|
|
97
|
+
*
|
|
98
|
+
* @param {{serviceRoot: string, signal: object}} params
|
|
99
|
+
* @returns {string} the absolute path written
|
|
100
|
+
*/
|
|
101
|
+
function writeDeployabilitySignal({ serviceRoot, signal }) {
|
|
102
|
+
if (typeof serviceRoot !== 'string' || serviceRoot.length === 0) {
|
|
103
|
+
throw new Error('[DeployabilitySignal] Service root is required - writeDeployabilitySignal({ serviceRoot }) got '
|
|
104
|
+
+ `${JSON.stringify(serviceRoot)}. Fix: pass the repository the signal describes.`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const target = path.join(path.resolve(serviceRoot), ...SIGNAL_RELATIVE_PATH.split('/'));
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
111
|
+
fs.writeFileSync(target, `${JSON.stringify(signal, null, 2)}\n`);
|
|
112
|
+
} catch (cause) {
|
|
113
|
+
throw new Error(`[DeployabilitySignal] Cannot write the deployability signal to ${target} `
|
|
114
|
+
+ '- Expected the service root to be writable. Fix: repair whatever the cause names '
|
|
115
|
+
+ `(${cause.message})`, { cause });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return target;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { buildDeployabilitySignal, writeDeployabilitySignal, SIGNAL_RELATIVE_PATH };
|