@onlineapps/conn-orch-validator 9.0.0 → 11.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 +546 -0
- package/README.md +337 -19
- package/docs/DESIGN.md +32 -9
- package/manifests/biz-service.manifest.json +56 -6
- package/package.json +3 -2
- package/src/CookbookTestRunner.js +134 -22
- package/src/ValidationOrchestrator.js +312 -73
- package/src/cli/biz-ci-gate.js +191 -15
- package/src/cli/oa-sync-template.js +23 -8
- package/src/cli/oa-validate.js +70 -2
- 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 +14 -28
- 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/serviceDb.js +176 -7
- package/src/manifest/checks/serviceFiles.js +34 -7
- package/src/manifest/checks/serviceIdentityRows.js +3 -1
- package/src/manifest/checks/serviceRuntime.js +126 -1
- package/src/manifest/discovery.js +25 -7
- package/src/manifest/gitCheckout.js +84 -0
- 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/dbAccountGrants.js +126 -0
- package/src/utils/envContract.js +36 -6
- package/src/utils/envReads.js +102 -0
- 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/stepReferences.js +278 -0
- 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 +47 -15
- package/src/validators/ValidationProofGenerator.js +73 -34
- package/templates/business-service/.dockerignore +9 -1
- package/templates/business-service/.gitlab-ci.yml +199 -35
- package/templates/business-service/Dockerfile +49 -16
- package/templates/business-service/README.md +56 -9
- package/templates/business-service/config/env-templates/__SERVICE_NAME__.env +17 -5
- package/templates/business-service/config/env-templates/shared.env +8 -2
- package/templates/business-service/docker-compose.production.yml +9 -0
- package/templates/business-service/docker-compose.yml +17 -0
- 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
|
@@ -24,19 +24,24 @@
|
|
|
24
24
|
* (here) — one concern, one rail (`.claude/rules/change-discipline.md` § One rail
|
|
25
25
|
* per concern). Everything else in the block is compared byte for byte.
|
|
26
26
|
*
|
|
27
|
-
* The service's own identity is the other
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
27
|
+
* The service's own identity is the other half, and it is settled in ONE
|
|
28
|
+
* direction: the reference is RENDERED with this repository's names and the
|
|
29
|
+
* result compared against the file, never the file read back into placeholders
|
|
30
|
+
* (d.531). The runner's key is `<container>_tests` and the container name is per
|
|
31
|
+
* repository by construction, so a render is exact; reading a name back is not,
|
|
32
|
+
* because a name is also an ordinary word — for `api_biz/meta` it rewrote the
|
|
33
|
+
* template's own citation of that path and reported a conformant block as
|
|
34
|
+
* drifted. `renderRunnerIdentity` below is that one direction, and both the
|
|
35
|
+
* check and the generator go through it.
|
|
32
36
|
*
|
|
33
37
|
* ## The same definition renders
|
|
34
38
|
*
|
|
35
|
-
* `renderRunnerBlock`
|
|
36
|
-
* this repository's
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
39
|
+
* `renderRunnerBlock` is the generator's half: the same identity render, plus
|
|
40
|
+
* this repository's three shared declarations grafted in. That is what makes
|
|
41
|
+
* `npx oa-sync-template docker-compose.yml` produce a block the check then finds
|
|
42
|
+
* clean — a generator whose output its own check rejects is the defect this
|
|
43
|
+
* module exists to avoid, and it is measured over a service called `meta`
|
|
44
|
+
* (`tests/unit/manifestServiceFiles.test.js`).
|
|
40
45
|
*
|
|
41
46
|
* @see api/docs/governance/confirmations/biz-service-manifest.md §2, §3.2
|
|
42
47
|
* @see api/docs/governance/confirmations/biz-test-container.md
|
|
@@ -156,8 +161,22 @@ function stripDeclarations(lines, keys) {
|
|
|
156
161
|
}
|
|
157
162
|
|
|
158
163
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
164
|
+
* The template's block with one repository's identity in it — the ONE direction
|
|
165
|
+
* this module substitutes (d.531).
|
|
166
|
+
*
|
|
167
|
+
* The other direction, reading a repository's block back into placeholders, was
|
|
168
|
+
* a second rail under the same rule and it could not be made right: a
|
|
169
|
+
* substitution of the service's NAME rewrites every occurrence of that string,
|
|
170
|
+
* including one in the template's own prose. Measured over `api_biz/meta`: the
|
|
171
|
+
* block cites `api_biz/meta/docker-compose.yml` as the file the memory numbers
|
|
172
|
+
* were taken in, so that service's conformant block was normalized into
|
|
173
|
+
* `api_biz/__SERVICE_NAME__/docker-compose.yml` and reported as drifted, while
|
|
174
|
+
* `oa-sync-template docker-compose.yml --check` — which renders — called the
|
|
175
|
+
* same file unchanged.
|
|
176
|
+
*
|
|
177
|
+
* A placeholder is a name nothing else can be, so rendering is total; a name is
|
|
178
|
+
* a word, and reading it back is a guess. One rail, and it is this one
|
|
179
|
+
* (`.claude/rules/change-discipline.md` § One rail per concern).
|
|
161
180
|
*
|
|
162
181
|
* The container name is substituted BEFORE the service name on purpose: the
|
|
163
182
|
* container name normally contains the service name (`api_service_converter`
|
|
@@ -167,10 +186,10 @@ function stripDeclarations(lines, keys) {
|
|
|
167
186
|
* @param {{lines: string[], containerName: string, serviceName: string}} args
|
|
168
187
|
* @returns {string[]}
|
|
169
188
|
*/
|
|
170
|
-
function
|
|
171
|
-
return
|
|
172
|
-
.split(
|
|
173
|
-
.split(
|
|
189
|
+
function renderRunnerIdentity({ lines, containerName, serviceName }) {
|
|
190
|
+
return lines.map((line) => line
|
|
191
|
+
.split(CONTAINER_PLACEHOLDER).join(containerName)
|
|
192
|
+
.split(SERVICE_PLACEHOLDER).join(serviceName));
|
|
174
193
|
}
|
|
175
194
|
|
|
176
195
|
/**
|
|
@@ -188,9 +207,7 @@ function normalizeRunnerBlock({ lines, containerName, serviceName }) {
|
|
|
188
207
|
* @returns {string[]} the block's lines, markers included
|
|
189
208
|
*/
|
|
190
209
|
function renderRunnerBlock({ reference, composeText, containerName, serviceName }) {
|
|
191
|
-
let rendered =
|
|
192
|
-
.split(CONTAINER_PLACEHOLDER).join(containerName)
|
|
193
|
-
.split(SERVICE_PLACEHOLDER).join(serviceName));
|
|
210
|
+
let rendered = renderRunnerIdentity({ lines: reference, containerName, serviceName });
|
|
194
211
|
|
|
195
212
|
const service = serviceLines(composeText, containerName);
|
|
196
213
|
|
|
@@ -217,6 +234,6 @@ module.exports = {
|
|
|
217
234
|
serviceLines,
|
|
218
235
|
replaceServiceNode,
|
|
219
236
|
stripDeclarations,
|
|
220
|
-
|
|
237
|
+
renderRunnerIdentity,
|
|
221
238
|
renderRunnerBlock
|
|
222
239
|
};
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
const { discoverBearers, rootOfPattern } = require('../discovery');
|
|
21
|
+
const { describeWorkspaceFix } = require('../workspaceRoot');
|
|
21
22
|
|
|
22
23
|
const check = Object.freeze({
|
|
23
24
|
scope: 'workspace',
|
|
@@ -31,7 +32,7 @@ const check = Object.freeze({
|
|
|
31
32
|
/** What the run prints instead of a verdict when the workspace is unreachable. */
|
|
32
33
|
describeNotRun({ block }) {
|
|
33
34
|
const owner = block && block.from ? block.from.path : 'the referenced SSOT';
|
|
34
|
-
return `the workspace root is not reachable, so ${owner} cannot be read`;
|
|
35
|
+
return `the workspace root is not reachable, so ${owner} cannot be read. ${describeWorkspaceFix(owner)}`;
|
|
35
36
|
},
|
|
36
37
|
|
|
37
38
|
/**
|
|
@@ -54,7 +54,7 @@ const path = require('path');
|
|
|
54
54
|
const { spawnSync } = require('child_process');
|
|
55
55
|
|
|
56
56
|
const { whereOf } = require('./libraryContext');
|
|
57
|
-
const { resolveWorkspacePath } = require('../workspaceRoot');
|
|
57
|
+
const { resolveWorkspacePath, describeWorkspaceFix, AS_THE_REASON_NAMES } = require('../workspaceRoot');
|
|
58
58
|
|
|
59
59
|
/** The documentation lint, by the path the manifest writes every api path in. */
|
|
60
60
|
const LINT_SCRIPT = 'api/scripts/ci/lint-biz-docs.mjs';
|
|
@@ -210,6 +210,55 @@ const ERROR_SEVERITY = 'error';
|
|
|
210
210
|
*/
|
|
211
211
|
const errorGraded = (findings) => findings.filter((finding) => finding.severity === ERROR_SEVERITY);
|
|
212
212
|
|
|
213
|
+
/**
|
|
214
|
+
* The NOT RUN sentence of a row the linter could not decide — the rules it could
|
|
215
|
+
* not evaluate, the linter's own reason for the first of them, and the command
|
|
216
|
+
* that turns this checkout into one where the question has an answer.
|
|
217
|
+
*
|
|
218
|
+
* Three things, and each has exactly one owner. The rule ids and the reason are
|
|
219
|
+
* the LINTER'S: this module cites the tool, it does not restate it, which is the
|
|
220
|
+
* whole point of the bridge (docblock at the top of this file). The remedy is
|
|
221
|
+
* `workspaceRoot.js` § describeWorkspaceFix, shared with
|
|
222
|
+
* `runManifest.js` § describeMissingRoots — the other place a row goes NOT RUN
|
|
223
|
+
* because a sibling was not there. Until d.516 this end of it stopped at the
|
|
224
|
+
* linter's reason: measured 2026-09-15 over a `git archive HEAD` export placed
|
|
225
|
+
* beside no `api_biz`, the run printed
|
|
226
|
+
*
|
|
227
|
+
* NOT RUN D-PORT — F002:http-ports could not be decided — api_biz/*/docker-compose.yml
|
|
228
|
+
* lives in a sibling checkout this run does not have, so the probe could not
|
|
229
|
+
* be evaluated
|
|
230
|
+
*
|
|
231
|
+
* while `U-ORPHAN`, in the SAME run and about the SAME absent directory, named
|
|
232
|
+
* the command. True, and nothing the reader could act on
|
|
233
|
+
* (`.claude/rules/automation-gates.md` §1 requirement 4).
|
|
234
|
+
*
|
|
235
|
+
* What the fix asks the reader to carry is left as the phrase pointing back at
|
|
236
|
+
* the reason, rather than a root resolved here. The linter's skipped channel
|
|
237
|
+
* names the absent THING — a probe target, a scan root, a manifest — and it
|
|
238
|
+
* carries no structured root to read (`api/scripts/ci/lint-biz-docs.mjs`
|
|
239
|
+
* § markSkipped: `{ rule, reason }` and nothing else). Deriving one would mean
|
|
240
|
+
* parsing that sentence, which is the private dialect between row and tool this
|
|
241
|
+
* bridge exists to avoid; naming the siblings here instead would make this
|
|
242
|
+
* module a second owner of the lint's own probe configuration.
|
|
243
|
+
*
|
|
244
|
+
* Collapsed into one reason per row: a checkout without siblings makes a dozen
|
|
245
|
+
* bans undecidable at once, and twelve identical sentences say nothing the first
|
|
246
|
+
* one does not.
|
|
247
|
+
*
|
|
248
|
+
* @param {Array<{rule: string, reason: string}>} undecided at least one entry
|
|
249
|
+
* @returns {string}
|
|
250
|
+
*/
|
|
251
|
+
function describeUndecided(undecided) {
|
|
252
|
+
if (!Array.isArray(undecided) || undecided.length === 0) {
|
|
253
|
+
throw new Error('[DocsLintBridge] Undecided rules are required - describeUndecided() was given '
|
|
254
|
+
+ `${JSON.stringify(undecided)}, and a row with nothing undecided is not NOT RUN. `
|
|
255
|
+
+ 'Fix: call it only when the linter reported at least one rule it could not evaluate.');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return `${undecided.map((entry) => entry.rule).join(', ')} could not be decided — `
|
|
259
|
+
+ `${undecided[0].reason}. ${describeWorkspaceFix(AS_THE_REASON_NAMES)}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
213
262
|
/**
|
|
214
263
|
* Which row owns a lint finding: the one whose citation is the most specific.
|
|
215
264
|
*
|
|
@@ -250,7 +299,11 @@ function claimantOf(ruleId, rows) {
|
|
|
250
299
|
* @returns {{findings: Array<object>, skipped: Array<object>}|{problem: string}}
|
|
251
300
|
*/
|
|
252
301
|
function lintOnce({ lintScript, docsDir, treeConfig }) {
|
|
253
|
-
|
|
302
|
+
// `\0` as the escape, never the raw byte: written literally it makes the
|
|
303
|
+
// whole file binary to grep, diff and every reader that asks what changed
|
|
304
|
+
// (measured 2026-09-16 — `grep -n` answered "Binary file … matches" and
|
|
305
|
+
// printed nothing). The value is identical; the source stays readable.
|
|
306
|
+
const key = [lintScript, docsDir, treeConfig].join('\0');
|
|
254
307
|
if (answers.has(key)) return answers.get(key);
|
|
255
308
|
|
|
256
309
|
const answer = runLint({ lintScript, docsDir, treeConfig });
|
|
@@ -394,15 +447,31 @@ function askLint({ block, serviceRoot, workspaceRoot }) {
|
|
|
394
447
|
return { answer };
|
|
395
448
|
}
|
|
396
449
|
|
|
450
|
+
/**
|
|
451
|
+
* The absence BOTH documentation rows answer, in one sentence.
|
|
452
|
+
*
|
|
453
|
+
* `describeMissingRoots` answers "the workspace is there and the root is not";
|
|
454
|
+
* this one answers "there is no workspace at all". Until d.516b it stopped at
|
|
455
|
+
* the absence — the defect `.claude/rules/automation-gates.md` §1 requirement 4
|
|
456
|
+
* names, one channel over from the one d.516 closed. Until d.523 the sentence
|
|
457
|
+
* was then written TWICE, once per row, byte for byte: two rails for one
|
|
458
|
+
* concern (`.claude/rules/change-discipline.md` § One rail per concern), and
|
|
459
|
+
* the shape in which the first of the two would one day be improved alone.
|
|
460
|
+
*
|
|
461
|
+
* @returns {string}
|
|
462
|
+
*/
|
|
463
|
+
function describeLintNotRun() {
|
|
464
|
+
return `the workspace root is not reachable, so ${LINT_SCRIPT} cannot be run. `
|
|
465
|
+
+ describeWorkspaceFix(LINT_SCRIPT);
|
|
466
|
+
}
|
|
467
|
+
|
|
397
468
|
const docsLint = Object.freeze({
|
|
398
469
|
scope: 'bearer',
|
|
399
470
|
requires: Object.freeze(['rule']),
|
|
400
471
|
|
|
401
472
|
requiresSiblings: () => [LINT_DIR],
|
|
402
473
|
|
|
403
|
-
describeNotRun
|
|
404
|
-
return `the workspace root is not reachable, so ${LINT_SCRIPT} cannot be run`;
|
|
405
|
-
},
|
|
474
|
+
describeNotRun: describeLintNotRun,
|
|
406
475
|
|
|
407
476
|
/**
|
|
408
477
|
* @param {{ row: object, block: object, serviceRoot: string, workspaceRoot: string }} params
|
|
@@ -446,20 +515,15 @@ const docsLint = Object.freeze({
|
|
|
446
515
|
// is `answer.problem` above, and the absent tree config before it, both of
|
|
447
516
|
// which stay findings with a fix somebody can carry out.
|
|
448
517
|
//
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
// the first one does not.
|
|
518
|
+
// The sentence itself is `describeUndecided` above — shared with `D-LINT`,
|
|
519
|
+
// which reports the same absence about the rules no row claims.
|
|
452
520
|
const undecided = answer.skipped
|
|
453
521
|
.filter((entry) => citations.some((citation) => covers(citation, entry.rule)))
|
|
454
522
|
.filter((entry) => claimantOf(entry.rule, rows) === row.id);
|
|
455
523
|
|
|
456
524
|
if (undecided.length === 0) return found;
|
|
457
525
|
|
|
458
|
-
return {
|
|
459
|
-
findings: found,
|
|
460
|
-
notRun: `${undecided.map((entry) => entry.rule).join(', ')} could not be decided — `
|
|
461
|
-
+ `${undecided[0].reason}`
|
|
462
|
-
};
|
|
526
|
+
return { findings: found, notRun: describeUndecided(undecided) };
|
|
463
527
|
}
|
|
464
528
|
});
|
|
465
529
|
|
|
@@ -497,9 +561,7 @@ const docsLintClean = Object.freeze({
|
|
|
497
561
|
|
|
498
562
|
requiresSiblings: () => [LINT_DIR],
|
|
499
563
|
|
|
500
|
-
describeNotRun
|
|
501
|
-
return `the workspace root is not reachable, so ${LINT_SCRIPT} cannot be run`;
|
|
502
|
-
},
|
|
564
|
+
describeNotRun: describeLintNotRun,
|
|
503
565
|
|
|
504
566
|
/**
|
|
505
567
|
* @param {{ block: object, serviceRoot: string, workspaceRoot: string }} params
|
|
@@ -531,11 +593,7 @@ const docsLintClean = Object.freeze({
|
|
|
531
593
|
const undecided = answer.skipped.filter((entry) => claimantOf(entry.rule, rows) === null);
|
|
532
594
|
if (undecided.length === 0) return found;
|
|
533
595
|
|
|
534
|
-
return {
|
|
535
|
-
findings: found,
|
|
536
|
-
notRun: `${undecided.map((entry) => entry.rule).join(', ')} could not be decided — `
|
|
537
|
-
+ `${undecided[0].reason}`
|
|
538
|
-
};
|
|
596
|
+
return { findings: found, notRun: describeUndecided(undecided) };
|
|
539
597
|
}
|
|
540
598
|
});
|
|
541
599
|
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
|
|
46
46
|
const fs = require('fs');
|
|
47
47
|
const path = require('path');
|
|
48
|
-
const {
|
|
48
|
+
const { gitCommand, listTrackedFiles, NOT_A_CHECKOUT } = require('../gitCheckout');
|
|
49
49
|
|
|
50
50
|
/** Never walked: neither is part of a repository's declared shape. */
|
|
51
51
|
const NEVER_WALKED = Object.freeze(['node_modules', '.git']);
|
|
@@ -53,29 +53,6 @@ const NEVER_WALKED = Object.freeze(['node_modules', '.git']);
|
|
|
53
53
|
/** The file classes whose rows REQUIRE a path to be there. */
|
|
54
54
|
const REQUIRING_CLASSES = Object.freeze(['identical', 'contains', 'generated']);
|
|
55
55
|
|
|
56
|
-
/**
|
|
57
|
-
* Run one git command in the repository, or return null when git itself could
|
|
58
|
-
* not answer. Nothing here guesses: the caller turns a null into NOT RUN.
|
|
59
|
-
*
|
|
60
|
-
* @param {string} serviceRoot
|
|
61
|
-
* @param {string[]} args
|
|
62
|
-
* @param {string} [input]
|
|
63
|
-
* @returns {string|null}
|
|
64
|
-
*/
|
|
65
|
-
function git(serviceRoot, args, input = undefined) {
|
|
66
|
-
try {
|
|
67
|
-
return execFileSync('git', ['-C', serviceRoot, ...args], {
|
|
68
|
-
encoding: 'utf8',
|
|
69
|
-
input,
|
|
70
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
71
|
-
});
|
|
72
|
-
} catch (error) {
|
|
73
|
-
// check-ignore exits 1 when nothing matched, which is an ANSWER.
|
|
74
|
-
if (error.status === 1 && typeof error.stdout === 'string') return error.stdout;
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
56
|
/**
|
|
80
57
|
* Does the path exist under EXACTLY this spelling?
|
|
81
58
|
*
|
|
@@ -161,12 +138,21 @@ const check = Object.freeze({
|
|
|
161
138
|
* @returns {{findings: Array<{where: string, what: string}>, notRun: string|null}}
|
|
162
139
|
*/
|
|
163
140
|
run({ block, serviceRoot }) {
|
|
164
|
-
const listed =
|
|
141
|
+
const listed = listTrackedFiles(serviceRoot);
|
|
165
142
|
if (listed === null) {
|
|
166
143
|
return {
|
|
167
144
|
findings: [],
|
|
168
|
-
|
|
169
|
-
|
|
145
|
+
// The remedy of this row is NOT the one every other NOT RUN of this
|
|
146
|
+
// package carries. `workspaceRoot.js` § describeWorkspaceFix names
|
|
147
|
+
// `--workspace`, which points at a tree of checkouts; no value of it
|
|
148
|
+
// turns an export into a checkout, and this row asks git, not the
|
|
149
|
+
// workspace. So the sentence is this row's own — one remedy, one owner,
|
|
150
|
+
// and neither borrowed from the other
|
|
151
|
+
// (`.claude/rules/change-discipline.md` § One rail per concern).
|
|
152
|
+
// Until d.518 it stopped at the absence, which is the dead end
|
|
153
|
+
// `.claude/rules/automation-gates.md` §1 requirement 4 forbids.
|
|
154
|
+
notRun: `${NOT_A_CHECKOUT} Fix: run this row over a git checkout — a clone of the repository, `
|
|
155
|
+
+ 'never an export, a tarball or a built image.'
|
|
170
156
|
};
|
|
171
157
|
}
|
|
172
158
|
|
|
@@ -180,7 +166,7 @@ const check = Object.freeze({
|
|
|
180
166
|
|
|
181
167
|
// Only now, and only for those: which of them a .gitignore rule covers is
|
|
182
168
|
// what turns the finding into an edit the reader can make.
|
|
183
|
-
const answered =
|
|
169
|
+
const answered = gitCommand(serviceRoot, ['check-ignore', '--stdin', '-z'], missing.map(({ file }) => file).join('\0'));
|
|
184
170
|
const ignored = new Set((answered === null ? '' : answered).split('\0').filter((entry) => entry.length > 0));
|
|
185
171
|
|
|
186
172
|
return {
|
|
@@ -17,6 +17,7 @@ const fs = require('fs');
|
|
|
17
17
|
const path = require('path');
|
|
18
18
|
|
|
19
19
|
const { resolveFromValue } = require('../discovery');
|
|
20
|
+
const { describeWorkspaceFix } = require('../workspaceRoot');
|
|
20
21
|
const {
|
|
21
22
|
readPackage, whereOf, appliesTo, allDeps, scopedDeps, nodeMajorOf
|
|
22
23
|
} = require('./libraryContext');
|
|
@@ -91,7 +92,8 @@ const libraryEngines = Object.freeze({
|
|
|
91
92
|
requires: Object.freeze(['from']),
|
|
92
93
|
|
|
93
94
|
describeNotRun({ row }) {
|
|
94
|
-
return `the workspace root is not reachable, so ${row.from.path} cannot be read
|
|
95
|
+
return `the workspace root is not reachable, so ${row.from.path} cannot be read. `
|
|
96
|
+
+ describeWorkspaceFix(row.from.path);
|
|
95
97
|
},
|
|
96
98
|
|
|
97
99
|
run({ row, block, serviceRoot, workspaceRoot }) {
|
|
@@ -28,7 +28,7 @@ const path = require('path');
|
|
|
28
28
|
const {
|
|
29
29
|
discoverBearers, resolveFromReference, resolveFromMap, expandPattern, rootOfPattern
|
|
30
30
|
} = require('../discovery');
|
|
31
|
-
const { resolveWorkspacePath } = require('../workspaceRoot');
|
|
31
|
+
const { resolveWorkspacePath, describeWorkspaceFix, AS_THE_REASON_NAMES } = require('../workspaceRoot');
|
|
32
32
|
const {
|
|
33
33
|
readJson, readPackage, whereOf, declaredCategory, appliesTo, scopedDeps, SCOPE
|
|
34
34
|
} = require('./libraryContext');
|
|
@@ -39,8 +39,23 @@ const RUNTIME_SECTIONS = Object.freeze(['dependencies']);
|
|
|
39
39
|
/** The sections `scripts/ci/verify-manifest-pins.mjs` compares against the SSOT. */
|
|
40
40
|
const PINNED_SECTIONS = Object.freeze(['dependencies', 'devDependencies']);
|
|
41
41
|
|
|
42
|
-
/**
|
|
43
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Why a workspace-scoped row prints nothing instead of a verdict — and, since
|
|
44
|
+
* d.518, what to run so that it can.
|
|
45
|
+
*
|
|
46
|
+
* Five rows of the library uniform are reported through this one line
|
|
47
|
+
* (`U-ORPHAN`, `U-MISMATCH`, `L-PINS`, `L-CONSUMER`, `L-TOOLING`), so the
|
|
48
|
+
* command belongs here rather than at each of them: one remedy, one sentence,
|
|
49
|
+
* one owner (`workspaceRoot.js` § describeWorkspaceFix,
|
|
50
|
+
* `.claude/rules/change-discipline.md` § One rail per concern). Until d.518 all
|
|
51
|
+
* five stopped at the absence — true, and nothing a reader of a CI log can act
|
|
52
|
+
* on (`.claude/rules/automation-gates.md` §1 requirement 4).
|
|
53
|
+
*
|
|
54
|
+
* WHAT the checkout has to carry is already named by `what`, immediately before
|
|
55
|
+
* the fix, so the fix points back at it instead of repeating it in other words.
|
|
56
|
+
*/
|
|
57
|
+
const notRunBecause = (what) => `the workspace root is not reachable, so ${what} cannot be read. `
|
|
58
|
+
+ describeWorkspaceFix(AS_THE_REASON_NAMES);
|
|
44
59
|
|
|
45
60
|
/**
|
|
46
61
|
* Every package the uniform's discovery pattern finds, indexed by the name its
|
|
@@ -32,6 +32,7 @@ const {
|
|
|
32
32
|
readReadme, serviceRegion, libraryRegion, PACKAGE_IN_WORKSPACE
|
|
33
33
|
} = require('../../sync/readmeLocation');
|
|
34
34
|
const { whereOf, declaredCategory, readPackage } = require('./libraryContext');
|
|
35
|
+
const { describeWorkspaceFix, AS_THE_REASON_NAMES } = require('../workspaceRoot');
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* The first line at which the region on disk stops being the rendered one, and
|
|
@@ -102,8 +103,15 @@ const libraryReadmeRegion = Object.freeze({
|
|
|
102
103
|
// holds a copy of this package.
|
|
103
104
|
requiresSiblings: () => (PACKAGE_IN_WORKSPACE === null ? [] : [PACKAGE_IN_WORKSPACE]),
|
|
104
105
|
|
|
106
|
+
// The absence, and then the command that removes it, from the one owner of
|
|
107
|
+
// that command (`workspaceRoot.js` § describeWorkspaceFix). What the checkout
|
|
108
|
+
// must carry is named by the sentence in front of it, so the fix points back
|
|
109
|
+
// at it rather than saying it a second time in other words — which is what
|
|
110
|
+
// `AS_THE_REASON_NAMES` is for. Until d.518 this ended at the absence
|
|
111
|
+
// (`.claude/rules/automation-gates.md` §1 requirement 4).
|
|
105
112
|
describeNotRun() {
|
|
106
|
-
return 'the workspace root is not reachable, so the manifest the region links at cannot be located'
|
|
113
|
+
return 'the workspace root is not reachable, so the manifest the region links at cannot be located. '
|
|
114
|
+
+ describeWorkspaceFix(AS_THE_REASON_NAMES);
|
|
107
115
|
},
|
|
108
116
|
|
|
109
117
|
run({ row, serviceRoot, workspaceRoot }) {
|
|
@@ -25,9 +25,10 @@
|
|
|
25
25
|
const fs = require('fs');
|
|
26
26
|
const path = require('path');
|
|
27
27
|
|
|
28
|
-
const {
|
|
28
|
+
const { resolveFromDocument, isPackageReference, referenceOwner } = require('../discovery');
|
|
29
|
+
const { describeWorkspaceFix } = require('../workspaceRoot');
|
|
29
30
|
const { whereOf } = require('./libraryContext');
|
|
30
|
-
const { diffAgainst } = require('../../sync/sharedEnv');
|
|
31
|
+
const { diffAgainst, renderSharedEnv } = require('../../sync/sharedEnv');
|
|
31
32
|
const { loadAndValidateIntegrationContract } = require('../../utils/bizCiGateContract');
|
|
32
33
|
|
|
33
34
|
/** The generated platform file every service carries a copy of. */
|
|
@@ -333,14 +334,29 @@ const envTemplates = Object.freeze({
|
|
|
333
334
|
});
|
|
334
335
|
|
|
335
336
|
/**
|
|
336
|
-
* `shared.env` is
|
|
337
|
-
* the
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
* fix
|
|
337
|
+
* `shared.env` is generated, never written, and this row reads the file that
|
|
338
|
+
* OWNS the key set — `api/config/shared-env.json` in the workspace — through the
|
|
339
|
+
* same renderer `npx oa-sync-template shared-env` writes with (`src/sync/sharedEnv.js`).
|
|
340
|
+
*
|
|
341
|
+
* Until d.536 it read the render this package carries (d.229), so the verdict
|
|
342
|
+
* depended on the day the validator was published rather than on the SSOT: the
|
|
343
|
+
* three services synced after the 9.0.0 publish were told "10 line(s) differ" by
|
|
344
|
+
* a row whose own fix command had just written the file, because the SSOT had
|
|
345
|
+
* gained `LOG_MAX_SIZE_BYTES`, `LOG_MAX_FILES` and a comment since. One fact
|
|
346
|
+
* cannot be read off two files (`.claude/rules/change-discipline.md` § One rail
|
|
347
|
+
* per concern), and a gate whose answer moves with a publication date is not
|
|
348
|
+
* predictable (`.claude/rules/automation-gates.md` §1.1).
|
|
349
|
+
*
|
|
350
|
+
* The reason d.229 gave for the packaged copy — "a service container has no
|
|
351
|
+
* `api/config/`" — no longer buys anything the row needs: the deploy job the
|
|
352
|
+
* uniform runs in clones the api checkout (confirmation `biz-service-manifest`
|
|
353
|
+
* 008), and where the workspace really is out of reach the row says NOT RUN and
|
|
354
|
+
* names the command, which is what every other workspace-reading row does. A
|
|
355
|
+
* silent pass measured against yesterday's key set is the false guarantee
|
|
356
|
+
* `automation-gates.md` §5 calls a defect.
|
|
357
|
+
*
|
|
358
|
+
* `templates/business-service/config/env-templates/shared.env` stays as the
|
|
359
|
+
* scaffold's own output, and `sharedEnvTemplate.test.js` stays the gate on it.
|
|
344
360
|
*/
|
|
345
361
|
const sharedEnvGenerated = Object.freeze({
|
|
346
362
|
scope: 'bearer',
|
|
@@ -349,7 +365,8 @@ const sharedEnvGenerated = Object.freeze({
|
|
|
349
365
|
needsWorkspace: ({ row }) => !isPackageReference(row.from),
|
|
350
366
|
|
|
351
367
|
describeNotRun({ row }) {
|
|
352
|
-
|
|
368
|
+
const owner = referenceOwner(row.from);
|
|
369
|
+
return `the workspace root is not reachable, so ${owner} cannot be read. ${describeWorkspaceFix(owner)}`;
|
|
353
370
|
},
|
|
354
371
|
|
|
355
372
|
run({ row, serviceRoot, workspaceRoot }) {
|
|
@@ -360,7 +377,7 @@ const sharedEnvGenerated = Object.freeze({
|
|
|
360
377
|
return [{ where, what: `absent — it is rendered from ${owner}` }];
|
|
361
378
|
}
|
|
362
379
|
|
|
363
|
-
const rendered =
|
|
380
|
+
const rendered = renderSharedEnv(resolveFromDocument({ from: row.from, workspaceRoot }));
|
|
364
381
|
if (rendered === text) return [];
|
|
365
382
|
|
|
366
383
|
const differing = diffAgainst(rendered, text).split('\n').filter((line) => /^[-+]/.test(line)).length - 2;
|