@onlineapps/conn-orch-validator 9.0.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 +373 -0
- package/README.md +83 -9
- package/docs/DESIGN.md +21 -7
- package/manifests/biz-service.manifest.json +28 -5
- package/package.json +2 -2
- 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 +91 -25
- 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/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
|
@@ -30,12 +30,22 @@
|
|
|
30
30
|
* biz repositories: 47 `@see` lines under their scripts directories, of which 4
|
|
31
31
|
* open with the `api/` prefix.
|
|
32
32
|
*
|
|
33
|
-
* Two scopes,
|
|
34
|
-
* `scripts/`. S009
|
|
35
|
-
* nothing else
|
|
36
|
-
* mechanism keeping it true (`doc-code-binding.md`
|
|
37
|
-
* a CODE line is a fixture assertion whose target
|
|
38
|
-
* which therefore cannot rot.
|
|
33
|
+
* Two scopes, three rules. S001-S008 read the HEADER of every script under
|
|
34
|
+
* `scripts/`. S009 and S010 read the tests, and they read opposite halves of
|
|
35
|
+
* them. S009 reads the COMMENTS and nothing else: a `<path>.<ext>:<line>`
|
|
36
|
+
* citation in prose has no mechanism keeping it true (`doc-code-binding.md`
|
|
37
|
+
* §1), while the same shape on a CODE line is a fixture assertion whose target
|
|
38
|
+
* the test creates itself and which therefore cannot rot. S010 reads the CODE
|
|
39
|
+
* lines and nothing else: an inverted command is exempt from `errexit`, so an
|
|
40
|
+
* assertion written that way asserts nothing unless it happens to be the last
|
|
41
|
+
* command of the body.
|
|
42
|
+
*
|
|
43
|
+
* The test scope is `tests/`, not the one directory S009 was born in. A rule
|
|
44
|
+
* that reads `tests/scripts/` and not `tests/scripts-docker/` beside it has a
|
|
45
|
+
* hole nobody can see: the run reports `0 finding(s)` and never says which
|
|
46
|
+
* files it did not open. Widened in d.482 (2026-09-15) together with S010, over
|
|
47
|
+
* a scope measured clean for both rules first — no baseline
|
|
48
|
+
* (`automation-gates.md` §3).
|
|
39
49
|
*
|
|
40
50
|
* @see api/docs/standards/SCRIPTS-STANDARD.md
|
|
41
51
|
* @see api/docs/governance/confirmations/biz-service-manifest.md
|
|
@@ -54,8 +64,8 @@ const SHELL_VOCAB = new Set(['bash>=4', 'bash>=3.2', 'sh', 'node>=18', 'node>=24
|
|
|
54
64
|
/** The directory whose scripts carry the header. */
|
|
55
65
|
const SCRIPT_SCOPE = 'scripts';
|
|
56
66
|
|
|
57
|
-
/** The directory whose
|
|
58
|
-
const CITATION_SCOPE = 'tests
|
|
67
|
+
/** The directory whose tests S009 and S010 read, each one half of them. */
|
|
68
|
+
const CITATION_SCOPE = 'tests';
|
|
59
69
|
|
|
60
70
|
/**
|
|
61
71
|
* The directories whose files are libraries rather than entry points.
|
|
@@ -111,6 +121,27 @@ const LINE_CITATION = /[A-Za-z0-9_/.-]+\.(?:js|mjs|cjs|ts|mts|sh|bash|bats|yml|j
|
|
|
111
121
|
/** A comment line of a shell-family test. */
|
|
112
122
|
const COMMENT_LINE = /^\s*#/;
|
|
113
123
|
|
|
124
|
+
/**
|
|
125
|
+
* An assertion written as an inverted command, in the two shapes bats carries.
|
|
126
|
+
*
|
|
127
|
+
* Measured on bats 1.13.0: under `set -e` a command whose exit code is inverted
|
|
128
|
+
* is exempt from errexit, so `! cmd` in the middle of a test body fails nothing
|
|
129
|
+
* — the test stays green whatever `cmd` returns. Only the LAST command of the
|
|
130
|
+
* body decides the verdict, which is why the defect survives review: the line
|
|
131
|
+
* reads like an assertion and is one exactly when it happens to sit last.
|
|
132
|
+
*
|
|
133
|
+
* The rule does not try to tell the last command from the others. It reports
|
|
134
|
+
* every occurrence, because the position is a property of the file's current
|
|
135
|
+
* shape rather than of the assertion's intent: a line inserted below turns a
|
|
136
|
+
* working assertion into an inert one with no edit to the assertion itself.
|
|
137
|
+
* INFRA rewrote all 32 occurrences this platform carried before the rule landed
|
|
138
|
+
* (`572488c4`, `6100477e`, `d212d3c6`), so the stricter reading costs nothing.
|
|
139
|
+
*
|
|
140
|
+
* `if ! cmd; then … fi` is NOT this defect and matches neither pattern: there
|
|
141
|
+
* the `if` consumes the exit code on purpose, which is the shape the fix uses.
|
|
142
|
+
*/
|
|
143
|
+
const INERT_ASSERTION = Object.freeze([/^\s*!\s/, /\|\|\s*!\s/]);
|
|
144
|
+
|
|
114
145
|
/** The prefix a citation of the api checkout opens with, as every document writes it. */
|
|
115
146
|
const API_PREFIX = 'api/';
|
|
116
147
|
|
|
@@ -265,22 +296,38 @@ function lintScripts({ root, apiRoot = null }) {
|
|
|
265
296
|
);
|
|
266
297
|
}
|
|
267
298
|
|
|
268
|
-
// S009
|
|
269
|
-
//
|
|
299
|
+
// S009 on the comment lines, S010 on the code lines. One pass, because they
|
|
300
|
+
// partition the same file: a line is prose or it is a command, never both.
|
|
270
301
|
for (const relative of citationScanned) {
|
|
271
302
|
const lines = fs.readFileSync(path.join(root, ...relative.split('/')), 'utf8').split('\n');
|
|
272
303
|
for (let n = 0; n < lines.length; n += 1) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (
|
|
304
|
+
// S009: a comment cites a symbol or a literal string, and a covered
|
|
305
|
+
// defect cites the commit hash — never a line number.
|
|
306
|
+
if (COMMENT_LINE.test(lines[n])) {
|
|
307
|
+
const hit = lines[n].match(LINE_CITATION);
|
|
308
|
+
if (hit === null) continue;
|
|
309
|
+
findings.push({
|
|
310
|
+
id: 'S009',
|
|
311
|
+
file: relative,
|
|
312
|
+
line: n + 1,
|
|
313
|
+
message: `Comment cites a line number - "${hit[0]}". A line number has no mechanism keeping it true: `
|
|
314
|
+
+ 'the file moves and the number stays (doc-code-binding.md §1). Fix: cite a symbol or a literal '
|
|
315
|
+
+ 'string; a covered defect cites the commit hash that introduced or repaired it '
|
|
316
|
+
+ "(git log -S'<string>' --format=%h -- <file>) - DOC-STANDARD rule 11."
|
|
317
|
+
});
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// S010: an assertion whose exit code is inverted is exempt from errexit.
|
|
322
|
+
if (!INERT_ASSERTION.some((shape) => shape.test(lines[n]))) continue;
|
|
276
323
|
findings.push({
|
|
277
|
-
id: '
|
|
324
|
+
id: 'S010',
|
|
278
325
|
file: relative,
|
|
279
326
|
line: n + 1,
|
|
280
|
-
message: `
|
|
281
|
-
+ '
|
|
282
|
-
+ '
|
|
283
|
-
+
|
|
327
|
+
message: `Inert assertion - "${lines[n].trim()}". Under set -e a command with an inverted exit code `
|
|
328
|
+
+ 'is exempt from errexit, so it fails nothing unless it is the last command of the test body. '
|
|
329
|
+
+ 'Fix: if <what must not hold>; then echo "[ctx] problem - found: …" >&2; return 1; fi '
|
|
330
|
+
+ '— SCRIPTS-STANDARD.md § Enforcement'
|
|
284
331
|
});
|
|
285
332
|
}
|
|
286
333
|
}
|
|
@@ -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
|
|
|
@@ -165,8 +165,19 @@ const check = Object.freeze({
|
|
|
165
165
|
if (listed === null) {
|
|
166
166
|
return {
|
|
167
167
|
findings: [],
|
|
168
|
+
// The remedy of this row is NOT the one every other NOT RUN of this
|
|
169
|
+
// package carries. `workspaceRoot.js` § describeWorkspaceFix names
|
|
170
|
+
// `--workspace`, which points at a tree of checkouts; no value of it
|
|
171
|
+
// turns an export into a checkout, and this row asks git, not the
|
|
172
|
+
// workspace. So the sentence is this row's own — one remedy, one owner,
|
|
173
|
+
// and neither borrowed from the other
|
|
174
|
+
// (`.claude/rules/change-discipline.md` § One rail per concern).
|
|
175
|
+
// Until d.518 it stopped at the absence, which is the dead end
|
|
176
|
+
// `.claude/rules/automation-gates.md` §1 requirement 4 forbids.
|
|
168
177
|
notRun: 'this tree is not a git checkout (git ls-files could not answer), so what a clone '
|
|
169
|
-
+ 'would receive cannot be read — a container and an exported tarball are in exactly this state'
|
|
178
|
+
+ 'would receive cannot be read — a container and an exported tarball are in exactly this state. '
|
|
179
|
+
+ 'Fix: run this row over a git checkout — a clone of the repository, never an export, a tarball '
|
|
180
|
+
+ 'or a built image.'
|
|
170
181
|
};
|
|
171
182
|
}
|
|
172
183
|
|
|
@@ -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;
|