@onlineapps/conn-orch-validator 7.0.0 → 8.1.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 +2582 -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 +409 -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 +22 -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 +123 -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,446 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The manifest's test on itself.
|
|
5
|
+
*
|
|
6
|
+
* Confirmation `biz-service-manifest` 004:
|
|
7
|
+
* - a row may carry only facts the manifest owns — categories, duties, row
|
|
8
|
+
* ids, severities, `fix` commands, discovery patterns, `doc` pointers;
|
|
9
|
+
* - every other fact is a `from:` reference to the file that owns it;
|
|
10
|
+
* - bearers are discovered, never enumerated;
|
|
11
|
+
* - the version of the manifest is the validator pin, never a field;
|
|
12
|
+
* - point 5: every array of names is either a row/category definition or a
|
|
13
|
+
* `from:` reference — a copied list cannot be committed.
|
|
14
|
+
* 002 §16.1: a row without `doc` is a finding of the manifest itself.
|
|
15
|
+
*
|
|
16
|
+
* The result is data, not a throw: `manifestShape.test.js` asserts the exact
|
|
17
|
+
* violations, and `runManifest` turns them into a fail-fast error.
|
|
18
|
+
*
|
|
19
|
+
* @see api/docs/governance/confirmations/biz-service-manifest.md § Confirmation 20260909-biz-service-manifest-004
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const { walkManifest, collectRows, isPlainObject } = require('./walk');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The consequences a row may carry, and no others. `boot` and `deploy` are the
|
|
26
|
+
* service uniform's (001 §4); `publish` is the library uniform's — a library has
|
|
27
|
+
* no boot, and the choke point nothing reaches a service past is
|
|
28
|
+
* `scripts/publish-library.sh` (002 §10).
|
|
29
|
+
*/
|
|
30
|
+
const SEVERITIES = Object.freeze(['boot', 'deploy', 'publish', 'warn']);
|
|
31
|
+
|
|
32
|
+
/** The one severity that stops nothing: it is printed in the table and nowhere else. */
|
|
33
|
+
const WARN_SEVERITY = 'warn';
|
|
34
|
+
|
|
35
|
+
/** Which severities a uniform may declare as blocking — every consequence except `warn`. */
|
|
36
|
+
const BLOCKING_CANDIDATES = Object.freeze(SEVERITIES.filter((severity) => severity !== WARN_SEVERITY));
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The three sentences every verdict is printed with, whichever uniform prints
|
|
40
|
+
* it: the word for a run something blocked, the word for a clear one, and — the
|
|
41
|
+
* third since d.230 — what a clear run says when it could not look at every row
|
|
42
|
+
* that CAN block. That last one is a sentence rather than a word because it
|
|
43
|
+
* names a count and a list, so it carries the two placeholders below.
|
|
44
|
+
*
|
|
45
|
+
* It belongs to the uniform for the same reason the first two do: a service
|
|
46
|
+
* skips `deploy` rows and a library `publish` ones, and the renderer knows
|
|
47
|
+
* neither word (`api/shared/TODO.md` §0.2b-15).
|
|
48
|
+
*/
|
|
49
|
+
const VERDICT_WORDS = Object.freeze(['blocked', 'clear', 'incomplete']);
|
|
50
|
+
|
|
51
|
+
/** What the `incomplete` sentence must be able to say: how many rows, and which. */
|
|
52
|
+
const VERDICT_PLACEHOLDERS = Object.freeze(['{count}', '{ids}']);
|
|
53
|
+
|
|
54
|
+
/** Every row states these, whatever its check. */
|
|
55
|
+
const REQUIRED_ROW_FIELDS = Object.freeze(['id', 'check', 'severity', 'owner', 'fix', 'doc']);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* What a check needs in order to look, and therefore where it runs. Three
|
|
59
|
+
* words, because there are three genuinely different needs — and the third one
|
|
60
|
+
* was missing until d.223, with a measured cost.
|
|
61
|
+
*
|
|
62
|
+
* - `service` — the bearer's own root is enough. Inside a container that is
|
|
63
|
+
* all there is, and these rows are the ones that still work
|
|
64
|
+
* there.
|
|
65
|
+
* - `workspace` — the check reports ABOUT the workspace: `U-ORPHAN` names a
|
|
66
|
+
* directory that is nobody's service. It has no bearer, so a
|
|
67
|
+
* run without a service root is its HOME, not its handicap.
|
|
68
|
+
* - `bearer` — the check reports about ONE bearer, and needs the workspace
|
|
69
|
+
* only to READ the file its `from:` reference names (the
|
|
70
|
+
* template, `config/shared-env.json`, `api/.nvmrc`). It runs
|
|
71
|
+
* once per bearer: for the one given in service mode, for
|
|
72
|
+
* every discovered one in workspace mode.
|
|
73
|
+
*
|
|
74
|
+
* Before the third word existed, `F-INIT`, `F-JEST`, `G-SHARED-ENV`, `R-NODE`,
|
|
75
|
+
* `L-ENGINES` and `L-PINS` had to borrow `workspace`, whose contract is "there
|
|
76
|
+
* is no service to read" — so each of them opened with `if (serviceRoot ===
|
|
77
|
+
* null) return []` and the workspace run printed "no findings" over a workspace
|
|
78
|
+
* with five real violations in it (measured over the `service-workspace`
|
|
79
|
+
* fixture, 2026-09-09). A silent pass is the false guarantee of
|
|
80
|
+
* `automation-gates.md` §5, and it came from a vocabulary that could not say
|
|
81
|
+
* what those rows needed.
|
|
82
|
+
*/
|
|
83
|
+
const CHECK_SCOPES = Object.freeze(['service', 'workspace', 'bearer']);
|
|
84
|
+
|
|
85
|
+
/** The scopes whose check cannot look without the workspace root resolved. */
|
|
86
|
+
const WORKSPACE_DEPENDENT_SCOPES = Object.freeze(['workspace', 'bearer']);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Whether THIS row can be answered without the workspace.
|
|
90
|
+
*
|
|
91
|
+
* The scope says it for most rows: a `workspace` check reports about the
|
|
92
|
+
* workspace, and a `bearer` check needs it to READ the file its `from:`
|
|
93
|
+
* reference names. But which file that is, is the row's business, and since
|
|
94
|
+
* d.229 a row may reference a file this package carries itself
|
|
95
|
+
* (`manifestShape.js` § describeFromProblem, the fourth shape). Such a row needs
|
|
96
|
+
* no workspace at all: the reference travels with the pin.
|
|
97
|
+
*
|
|
98
|
+
* The check answers, because the check is what reads — a rule written here
|
|
99
|
+
* would have to know which rows reference what, which is the manifest's fact,
|
|
100
|
+
* not the runner's (`change-discipline.md` § One rail per concern). A check that
|
|
101
|
+
* does not answer keeps the scope's default, so nothing had to change for the
|
|
102
|
+
* rows whose reference is still a workspace file (`R-NODE` reads `api/.nvmrc`)
|
|
103
|
+
* or which read the workspace for something other than a reference (the `D-*`
|
|
104
|
+
* rows run the documentation lint out of `api/scripts`).
|
|
105
|
+
*
|
|
106
|
+
* @param {{check: object, row: object, block: object}} params
|
|
107
|
+
* @returns {boolean}
|
|
108
|
+
*/
|
|
109
|
+
function rowNeedsWorkspace({ check, row, block }) {
|
|
110
|
+
if (!WORKSPACE_DEPENDENT_SCOPES.includes(check.scope)) return false;
|
|
111
|
+
return check.needsWorkspace ? check.needsWorkspace({ row, block }) : true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The only keys under which an array of strings is a fact the manifest OWNS
|
|
116
|
+
* (004 point 1: categories, duties, discovery patterns). An array of strings
|
|
117
|
+
* anywhere else is a copied list, whoever wrote it and however true it is today.
|
|
118
|
+
*
|
|
119
|
+
* - `exclude`, `consumer_patterns` — discovery patterns;
|
|
120
|
+
* - `may_depend_on` — the category graph, which is the duty itself;
|
|
121
|
+
* - `forbidden_packages` — the packages a duty forbids by name. These have no
|
|
122
|
+
* SSOT in this repository to reference: the rule is prose
|
|
123
|
+
* (`api/.claude/rules/package-management.md` § Storage Architecture) and the
|
|
124
|
+
* row is its only machine-readable form;
|
|
125
|
+
* - `blocking_severities` — which consequences stop this uniform's bearer. The
|
|
126
|
+
* uniform is the only thing that knows: a service raises no `publish`
|
|
127
|
+
* finding and a library no `boot` one;
|
|
128
|
+
* - `allowed` — the paths a duty exempts, each either a directory prefix
|
|
129
|
+
* ending in `/` or one file. The duty IS the exemption, so nothing else owns
|
|
130
|
+
* the list.
|
|
131
|
+
*/
|
|
132
|
+
const OWNED_STRING_ARRAY_KEYS = Object.freeze([
|
|
133
|
+
'exclude',
|
|
134
|
+
'may_depend_on',
|
|
135
|
+
'consumer_patterns',
|
|
136
|
+
'forbidden_packages',
|
|
137
|
+
'blocking_severities',
|
|
138
|
+
'allowed'
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
/** A version written into the manifest would be a second owner of the pin (004 point 2). */
|
|
142
|
+
const VERSION_KEY = /^(manifest_)?version$/;
|
|
143
|
+
|
|
144
|
+
/** The `applies_to` value that means every bearer of the uniform. */
|
|
145
|
+
const ALL_CATEGORIES = '*';
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The four shapes a `from:` reference may take.
|
|
149
|
+
*
|
|
150
|
+
* The first three point OUT of this package, at files owned by other concerns
|
|
151
|
+
* that are not going to be rewritten for the manifest:
|
|
152
|
+
*
|
|
153
|
+
* - `{ path, list, key }` an array of objects (api/config/services.json)
|
|
154
|
+
* - `{ path, list, names: "keys" }` an object map (api/config/libraries.json)
|
|
155
|
+
* - `{ path, text: true }` the whole file as one value (api/.nvmrc)
|
|
156
|
+
*
|
|
157
|
+
* The fourth points INTO it:
|
|
158
|
+
*
|
|
159
|
+
* - `{ package, text: true }` a file this package carries itself
|
|
160
|
+
* (templates/business-service/jest.config.js)
|
|
161
|
+
*
|
|
162
|
+
* It exists because for some facts the owner IS this package. The template moved
|
|
163
|
+
* inside it in d.215a — `api/templates/business-service` is the generator's
|
|
164
|
+
* OUTPUT, committed for reading (001 §5) — so a row referencing that directory
|
|
165
|
+
* referenced the output rather than the owner, which 004 point 2 forbids. The
|
|
166
|
+
* practical consequence was measured on 2026-09-09: inside a service container
|
|
167
|
+
* and in the service's own CI there is no workspace, so every row whose
|
|
168
|
+
* reference was `api/templates/…` reported NOT RUN — precisely the rows the
|
|
169
|
+
* uniform exists for. A packaged reference travels with the pin, so those rows
|
|
170
|
+
* run wherever the package is installed.
|
|
171
|
+
*
|
|
172
|
+
* A reference names one owner: `path` or `package`, never both.
|
|
173
|
+
*
|
|
174
|
+
* @param {*} from the reference as written
|
|
175
|
+
* @returns {string|null} what is wrong with it, or null when it is usable
|
|
176
|
+
*/
|
|
177
|
+
function describeFromProblem(from) {
|
|
178
|
+
if (!isPlainObject(from)) return 'it is not an object.';
|
|
179
|
+
|
|
180
|
+
const packaged = typeof from.package === 'string' && from.package.length > 0;
|
|
181
|
+
if (packaged && typeof from.path === 'string') {
|
|
182
|
+
return 'it names both a workspace "path" and a "package" file, so the fact would have two owners.';
|
|
183
|
+
}
|
|
184
|
+
if (packaged) {
|
|
185
|
+
return from.text === true
|
|
186
|
+
? null
|
|
187
|
+
: 'it names a "package" file without text: true — a file this package carries is read whole.';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (typeof from.path !== 'string' || from.path.length === 0) {
|
|
191
|
+
return 'it names neither a workspace "path" nor a "package" file.';
|
|
192
|
+
}
|
|
193
|
+
if (from.text === true) return null;
|
|
194
|
+
if (typeof from.list !== 'string' || from.list.length === 0) {
|
|
195
|
+
return 'it names no "list", and is not a { path, text: true } reference.';
|
|
196
|
+
}
|
|
197
|
+
if (from.names === 'keys') return null;
|
|
198
|
+
if (typeof from.key === 'string' && from.key.length > 0) return null;
|
|
199
|
+
return 'it names a "list" but neither a "key" (array of objects) nor names: "keys" (object map).';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Every category any discovery block of this manifest defines.
|
|
204
|
+
*
|
|
205
|
+
* @param {object} manifest parsed manifest
|
|
206
|
+
* @returns {Set<string>}
|
|
207
|
+
*/
|
|
208
|
+
function collectCategories(manifest) {
|
|
209
|
+
const names = new Set();
|
|
210
|
+
for (const block of Object.values(manifest.discovery || {})) {
|
|
211
|
+
for (const category of Object.keys(block.categories || {})) names.add(category);
|
|
212
|
+
}
|
|
213
|
+
return names;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The consequence of the uniform, which only the uniform knows.
|
|
219
|
+
*
|
|
220
|
+
* `boot` and `deploy` stop a service; `publish` stops a library (002 §10). A
|
|
221
|
+
* single global list made every verdict name all three, so a service run offered
|
|
222
|
+
* `publish` as a reason it was not deployable — a consequence no service row can
|
|
223
|
+
* raise (`api/shared/TODO.md` §0.2b-15). The severities and the two words the
|
|
224
|
+
* verdict is printed with are therefore declared by the manifest, and required
|
|
225
|
+
* here: a manifest that does not say what stops its bearer cannot be run at all.
|
|
226
|
+
*
|
|
227
|
+
* @param {object} manifest parsed manifest
|
|
228
|
+
* @param {(id: string, message: string) => void} add collector of violations
|
|
229
|
+
* @returns {void}
|
|
230
|
+
*/
|
|
231
|
+
function verifyConsequence(manifest, add) {
|
|
232
|
+
const blocking = manifest.blocking_severities;
|
|
233
|
+
if (!Array.isArray(blocking) || blocking.length === 0) {
|
|
234
|
+
add('M-BLOCKING', 'The manifest does not declare "blocking_severities". '
|
|
235
|
+
+ `Fix: state which severities stop this uniform's bearer, e.g. ["boot", "deploy"] for a service `
|
|
236
|
+
+ 'and ["publish"] for a library (confirmation biz-service-manifest 001 §4, 002 §10).');
|
|
237
|
+
} else {
|
|
238
|
+
const unusable = blocking.filter((severity) => !BLOCKING_CANDIDATES.includes(severity));
|
|
239
|
+
if (unusable.length > 0) {
|
|
240
|
+
add('M-BLOCKING', `"blocking_severities" names ${unusable.join(', ')}. `
|
|
241
|
+
+ `Fix: use one or more of ${BLOCKING_CANDIDATES.join(', ')} — "${WARN_SEVERITY}" is table-only and `
|
|
242
|
+
+ 'stops nothing (confirmation biz-service-manifest 001 §4).');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const verdict = manifest.verdict;
|
|
247
|
+
if (!isPlainObject(verdict)) {
|
|
248
|
+
add('M-VERDICT', 'The manifest does not declare "verdict". '
|
|
249
|
+
+ 'Fix: state the two words this uniform is judged with, e.g. '
|
|
250
|
+
+ '{ "blocked": "NOT DEPLOYABLE", "clear": "DEPLOYABLE" } — the word belongs to the uniform, '
|
|
251
|
+
+ 'never to an if in the renderer.');
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const missing = VERDICT_WORDS.filter((word) => typeof verdict[word] !== 'string' || verdict[word].length === 0);
|
|
256
|
+
if (missing.length > 0) {
|
|
257
|
+
add('M-VERDICT', `"verdict" is missing: ${missing.join(', ')}. `
|
|
258
|
+
+ `Fix: every uniform states all three — ${VERDICT_WORDS.join(', ')}; "incomplete" is the sentence a `
|
|
259
|
+
+ 'clear run adds when a row that can block it did not run (d.230).');
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const withoutPlaceholder = VERDICT_PLACEHOLDERS.filter((token) => !verdict.incomplete.includes(token));
|
|
264
|
+
if (withoutPlaceholder.length > 0) {
|
|
265
|
+
add('M-VERDICT', `"verdict"."incomplete" carries no ${withoutPlaceholder.join(', ')}. `
|
|
266
|
+
+ 'Fix: the sentence names HOW MANY rows did not run and WHICH — a reader who cannot see the ids '
|
|
267
|
+
+ 'cannot run the missing half.');
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* @param {object} manifest parsed manifest
|
|
273
|
+
* @param {{ checkRegistry: object }} deps the registered checks, injected
|
|
274
|
+
* @returns {{ ok: boolean, violations: Array<{id: string, message: string}>, rowIds: string[] }}
|
|
275
|
+
*/
|
|
276
|
+
function verifyManifestShape(manifest, { checkRegistry } = {}) {
|
|
277
|
+
if (!isPlainObject(manifest)) {
|
|
278
|
+
throw new Error('[ManifestShape] Manifest object is required - verifyManifestShape(manifest, deps) '
|
|
279
|
+
+ `got ${JSON.stringify(manifest)}. Fix: pass the result of loadManifest().`);
|
|
280
|
+
}
|
|
281
|
+
if (!isPlainObject(checkRegistry)) {
|
|
282
|
+
throw new Error('[ManifestShape] Check registry is required - verifyManifestShape(manifest, { checkRegistry }) '
|
|
283
|
+
+ 'got none. Fix: pass CHECK_REGISTRY from src/manifest/checks.');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const violations = [];
|
|
287
|
+
const add = (id, message) => violations.push({ id, message });
|
|
288
|
+
|
|
289
|
+
const { arrays, scalars } = walkManifest(manifest);
|
|
290
|
+
|
|
291
|
+
for (const scalar of scalars) {
|
|
292
|
+
if (VERSION_KEY.test(scalar.key)) {
|
|
293
|
+
add('M-VERSION', `"${scalar.at}" states a version. The manifest version IS the validator pin `
|
|
294
|
+
+ '(SSOT api/config/libraries.json, gate R6). Fix: remove the field.');
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for (const entry of arrays) {
|
|
299
|
+
const rowArray = entry.value.length > 0
|
|
300
|
+
&& entry.value.every((element) => isPlainObject(element) && 'id' in element);
|
|
301
|
+
if (rowArray) continue;
|
|
302
|
+
|
|
303
|
+
const objectArray = entry.value.length > 0 && entry.value.every(isPlainObject);
|
|
304
|
+
if (objectArray) {
|
|
305
|
+
add('M-ROW-FIELD', `"${entry.at}" is an array of objects that are not rows — an element has no "id". `
|
|
306
|
+
+ 'Fix: give every row an id, or move the data to the file that owns it and reference it with "from".');
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (OWNED_STRING_ARRAY_KEYS.includes(entry.key) && entry.value.every((e) => typeof e === 'string')) {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
add('M-COPY', `"${entry.at}" is a list of names the manifest does not own. `
|
|
315
|
+
+ 'Fix: replace it with a "from" reference to the file that owns the list, or make it a row definition.');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
verifyConsequence(manifest, add);
|
|
319
|
+
|
|
320
|
+
const rows = collectRows(manifest);
|
|
321
|
+
const seen = new Map();
|
|
322
|
+
|
|
323
|
+
for (const { row, at } of rows) {
|
|
324
|
+
const missing = REQUIRED_ROW_FIELDS.filter((field) => typeof row[field] !== 'string' || row[field].length === 0);
|
|
325
|
+
if (missing.length > 0) {
|
|
326
|
+
add('M-ROW-FIELD', `Row ${row.id || at} is missing: ${missing.join(', ')}. `
|
|
327
|
+
+ `Fix: every row states ${REQUIRED_ROW_FIELDS.join(', ')} — "doc" is where its "why" lives.`);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (seen.has(row.id)) {
|
|
332
|
+
add('M-ROW-ID', `Row id ${row.id} is used twice (${seen.get(row.id)} and ${at}). `
|
|
333
|
+
+ 'Fix: a finding is named by its row; give the second row its own id.');
|
|
334
|
+
}
|
|
335
|
+
seen.set(row.id, at);
|
|
336
|
+
|
|
337
|
+
if (!SEVERITIES.includes(row.severity)) {
|
|
338
|
+
add('M-SEVERITY', `Row ${row.id} has severity "${row.severity}". `
|
|
339
|
+
+ `Fix: use one of ${SEVERITIES.join(', ')} (confirmation biz-service-manifest 001 §4).`);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const check = checkRegistry[row.check];
|
|
344
|
+
if (!check) {
|
|
345
|
+
add('M-CHECK', `Row ${row.id} names check "${row.check}", which nothing registers. `
|
|
346
|
+
+ `Fix: register it in src/manifest/checks, or use one of: ${Object.keys(checkRegistry).join(', ')}.`);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (!CHECK_SCOPES.includes(check.scope)) {
|
|
351
|
+
add('M-SCOPE', `Row ${row.id} names check "${row.check}", which declares scope `
|
|
352
|
+
+ `${JSON.stringify(check.scope)}. `
|
|
353
|
+
+ `Fix: a check declares one of ${CHECK_SCOPES.join(', ')} — the runner decides where to run it `
|
|
354
|
+
+ 'from that word alone, and an unknown one would run nowhere and say nothing.');
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const missingParams = (check.requires || []).filter((param) => row[param] === undefined);
|
|
359
|
+
if (missingParams.length > 0) {
|
|
360
|
+
add('M-PARAM', `Row ${row.id} does not give check "${row.check}" its parameters: ${missingParams.join(', ')}. `
|
|
361
|
+
+ 'Fix: add them to the row.');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
for (const { row, at } of rows) {
|
|
366
|
+
if (row.from === undefined) continue;
|
|
367
|
+
const problem = describeFromProblem(row.from);
|
|
368
|
+
if (problem !== null) add('M-FROM', `Row ${row.id || at} has an unusable "from" reference — ${problem} `
|
|
369
|
+
+ 'Fix: reference the file that owns the fact; the manifest never copies it.');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const categories = collectCategories(manifest);
|
|
373
|
+
|
|
374
|
+
for (const [name, block] of Object.entries(manifest.discovery || {})) {
|
|
375
|
+
if (typeof block.pattern !== 'string' || block.pattern.length === 0) {
|
|
376
|
+
add('M-DISCOVERY', `Discovery block "${name}" has no "pattern". `
|
|
377
|
+
+ 'Fix: bearers are discovered, never enumerated — state the pattern that finds them.');
|
|
378
|
+
}
|
|
379
|
+
const problem = describeFromProblem(block.from);
|
|
380
|
+
if (problem !== null) {
|
|
381
|
+
add('M-DISCOVERY', `Discovery block "${name}" has no usable "from" reference — ${problem} `
|
|
382
|
+
+ 'Fix: name the file that owns the list of bearers; the manifest never copies it.');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
for (const [category, definition] of Object.entries(block.categories || {})) {
|
|
386
|
+
const missing = ['layer', 'concern', 'doc'].filter((field) => typeof definition[field] !== 'string');
|
|
387
|
+
if (missing.length > 0) {
|
|
388
|
+
add('M-CATEGORY', `Category "${category}" is missing: ${missing.join(', ')}. `
|
|
389
|
+
+ 'Fix: a category states which layer it is, what it is for, and where its why lives.');
|
|
390
|
+
}
|
|
391
|
+
if (!Array.isArray(definition.may_depend_on)) {
|
|
392
|
+
add('M-CATEGORY', `Category "${category}" has no "may_depend_on" list. `
|
|
393
|
+
+ 'Fix: state the categories it may depend on — an empty list is a decision, an absent one is not.');
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
for (const target of definition.may_depend_on) {
|
|
397
|
+
if (!categories.has(target)) {
|
|
398
|
+
add('M-CATEGORY', `Category "${category}" may_depend_on "${target}", which this manifest does not define. `
|
|
399
|
+
+ `Fix: use one of ${[...categories].join(', ')}.`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
for (const [name, block] of Object.entries(manifest.duties || {})) {
|
|
406
|
+
const target = block.applies_to;
|
|
407
|
+
if (target === ALL_CATEGORIES) continue;
|
|
408
|
+
if (typeof target !== 'string' || !categories.has(target)) {
|
|
409
|
+
add('M-DUTY', `Duty section "${name}" applies_to ${JSON.stringify(target)}, which is neither `
|
|
410
|
+
+ `"${ALL_CATEGORIES}" nor a defined category. Fix: use one of ${[...categories].join(', ')}.`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
for (const [id, entry] of Object.entries(manifest.guidance || {})) {
|
|
415
|
+
if (!isPlainObject(entry)) {
|
|
416
|
+
add('M-GUIDANCE', `Guidance "${id}" is not an object. Fix: state why it is not a row, and where its why lives.`);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const missing = ['why', 'doc'].filter((field) => typeof entry[field] !== 'string' || entry[field].length === 0);
|
|
420
|
+
if (missing.length > 0) {
|
|
421
|
+
add('M-GUIDANCE', `Guidance "${id}" is missing: ${missing.join(', ')}. `
|
|
422
|
+
+ 'Fix: guidance says WHY there is no row and points at the decision — that is the whole of it.');
|
|
423
|
+
}
|
|
424
|
+
if (entry.check !== undefined || entry.severity !== undefined) {
|
|
425
|
+
add('M-GUIDANCE', `Guidance "${id}" carries "check" or "severity". `
|
|
426
|
+
+ 'Fix: guidance never produces a finding; if it can be decided, make it a row.');
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return { ok: violations.length === 0, violations, rowIds: rows.map(({ row }) => row.id) };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
module.exports = {
|
|
434
|
+
verifyManifestShape,
|
|
435
|
+
rowNeedsWorkspace,
|
|
436
|
+
describeFromProblem,
|
|
437
|
+
SEVERITIES,
|
|
438
|
+
CHECK_SCOPES,
|
|
439
|
+
WORKSPACE_DEPENDENT_SCOPES,
|
|
440
|
+
BLOCKING_CANDIDATES,
|
|
441
|
+
VERDICT_WORDS,
|
|
442
|
+
VERDICT_PLACEHOLDERS,
|
|
443
|
+
REQUIRED_ROW_FIELDS,
|
|
444
|
+
OWNED_STRING_ARRAY_KEYS,
|
|
445
|
+
ALL_CATEGORIES
|
|
446
|
+
};
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Presentation of a manifest run: the table a human reads and the JSON a gate
|
|
5
|
+
* reads. One table, the columns confirmation `biz-service-manifest` 001 §3.1
|
|
6
|
+
* fixes: `id · severity · where · what · fix · owner`.
|
|
7
|
+
*
|
|
8
|
+
* Two things this module refuses to hide:
|
|
9
|
+
* - the `doc` pointer of every reported row, so the reader reaches the *why*
|
|
10
|
+
* without asking anybody (002 §16.1);
|
|
11
|
+
* - a row that did NOT run, printed above the table. A check that could not
|
|
12
|
+
* look is never silence and never a pass (`automation-gates.md` §5).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { incompleteRows } = require('./runManifest');
|
|
16
|
+
|
|
17
|
+
const COLUMNS = Object.freeze(['id', 'severity', 'where', 'what', 'fix', 'owner']);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The blocking findings of a run, judged by the severities THIS run's uniform
|
|
21
|
+
* declared. Nothing here knows which those are — a service has no `publish` row
|
|
22
|
+
* and a library no `boot` one, so a constant in this module would print a
|
|
23
|
+
* consequence the uniform cannot raise (`api/shared/TODO.md` §0.2b-15).
|
|
24
|
+
*
|
|
25
|
+
* A result that does not carry them is named, never guessed at
|
|
26
|
+
* (`.claude/rules/architecture-principles.md` §3, §5).
|
|
27
|
+
*
|
|
28
|
+
* @param {object} result the value `runManifest` returned
|
|
29
|
+
* @returns {object[]} the findings whose severity stops the bearer
|
|
30
|
+
*/
|
|
31
|
+
function blockingOf(result) {
|
|
32
|
+
if (!Array.isArray(result.blockingSeverities) || result.blockingSeverities.length === 0) {
|
|
33
|
+
throw new Error('[Report] The run does not declare its blocking severities - a verdict would have to '
|
|
34
|
+
+ 'guess what stops this bearer. Fix: render the value runManifest() returned; it carries '
|
|
35
|
+
+ 'blockingSeverities from the manifest.');
|
|
36
|
+
}
|
|
37
|
+
return result.findings.filter((finding) => result.blockingSeverities.includes(finding.severity));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The two words this uniform is judged with — `NOT DEPLOYABLE`/`DEPLOYABLE` for
|
|
42
|
+
* a service, `NOT PUBLISHABLE`/`PUBLISHABLE` for a library. The word is a
|
|
43
|
+
* property of the uniform, never an `if` in the renderer.
|
|
44
|
+
*
|
|
45
|
+
* @param {object} result the value `runManifest` returned
|
|
46
|
+
* @returns {{blocked: string, clear: string}}
|
|
47
|
+
*/
|
|
48
|
+
function verdictOf(result) {
|
|
49
|
+
const verdict = result.verdict;
|
|
50
|
+
if (verdict === null || typeof verdict !== 'object'
|
|
51
|
+
|| typeof verdict.blocked !== 'string' || typeof verdict.clear !== 'string'
|
|
52
|
+
|| typeof verdict.incomplete !== 'string') {
|
|
53
|
+
throw new Error('[Report] The run does not declare its verdict words - the sentence a human reads '
|
|
54
|
+
+ 'belongs to the uniform. Fix: render the value runManifest() returned; it carries verdict '
|
|
55
|
+
+ '{ blocked, clear, incomplete } from the manifest.');
|
|
56
|
+
}
|
|
57
|
+
return verdict;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* What a CLEAR run adds when it could not look at every row that can block: the
|
|
62
|
+
* uniform's own sentence, with the count and the ids of those rows in it.
|
|
63
|
+
*
|
|
64
|
+
* The sentence is the manifest's (`manifestShape.js` § VERDICT_WORDS) and the
|
|
65
|
+
* rows are `runManifest`'s (§ incompleteRows); this function only fills the two
|
|
66
|
+
* placeholders. Nothing here knows the word `deploy` — a library says `publish`,
|
|
67
|
+
* and a renderer that knew either would be a second owner of the consequence
|
|
68
|
+
* (`api/shared/TODO.md` §0.2b-15).
|
|
69
|
+
*
|
|
70
|
+
* @param {object} result the value `runManifest` returned
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
function describeIncomplete(result) {
|
|
74
|
+
const rows = incompleteRows(result);
|
|
75
|
+
return verdictOf(result).incomplete
|
|
76
|
+
.split('{count}').join(String(rows.length))
|
|
77
|
+
.split('{ids}').join(rows.map((one) => one.id).join(', '));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The outcome half of a CLEAR verdict — what was found, and always what could
|
|
82
|
+
* not be looked at.
|
|
83
|
+
*
|
|
84
|
+
* One owner, three callers: the banner an init log ends with, the report
|
|
85
|
+
* `oa-validate` prints, and the library report beside it. Before this, the three
|
|
86
|
+
* described the same run three ways, and two of them stopped at
|
|
87
|
+
* `— no findings`. A reader believes that sentence: it reads as a proven clean
|
|
88
|
+
* bill of health, and over a run where five rows could not look it is not one
|
|
89
|
+
* (`.claude/rules/automation-gates.md` §5 — a silent NOT RUN is a false
|
|
90
|
+
* guarantee; `change-discipline.md` § One rail per concern — one sentence, one
|
|
91
|
+
* owner). Lead decision, d.223 follow-up.
|
|
92
|
+
*
|
|
93
|
+
* The count is appended only when there IS one: a run that looked at everything
|
|
94
|
+
* keeps the short sentence, so the parenthesis always means something.
|
|
95
|
+
*
|
|
96
|
+
* @param {object} result the value `runManifest` returned
|
|
97
|
+
* @returns {string} e.g. `no findings`, `no findings (5 row(s) not run)`,
|
|
98
|
+
* `0 blocking finding(s), 1 warning(s) (1 row(s) not run)`
|
|
99
|
+
*/
|
|
100
|
+
function describeClearOutcome(result) {
|
|
101
|
+
const warnings = result.findings.length - blockingOf(result).length;
|
|
102
|
+
const found = warnings > 0
|
|
103
|
+
? `0 blocking finding(s), ${warnings} warning(s)`
|
|
104
|
+
: 'no findings';
|
|
105
|
+
|
|
106
|
+
if (result.notRun.length === 0) return found;
|
|
107
|
+
|
|
108
|
+
// Three outcomes, because there are three: everything was looked at; a row
|
|
109
|
+
// that could not have blocked anything was skipped (the count says so, and
|
|
110
|
+
// nothing more is owed); a row that CAN block was skipped, and then the
|
|
111
|
+
// verdict is only true of the half that ran (d.230).
|
|
112
|
+
return incompleteRows(result).length > 0
|
|
113
|
+
? `${found}, ${describeIncomplete(result)}`
|
|
114
|
+
: `${found} (${result.notRun.length} row(s) not run)`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function renderTable(findings) {
|
|
118
|
+
const widths = COLUMNS.map((column) => Math.max(
|
|
119
|
+
column.length,
|
|
120
|
+
...findings.map((finding) => String(finding[column]).length)
|
|
121
|
+
));
|
|
122
|
+
const line = (cells) => cells
|
|
123
|
+
.map((cell, index) => (index === cells.length - 1 ? String(cell) : String(cell).padEnd(widths[index])))
|
|
124
|
+
.join(' ')
|
|
125
|
+
.trimEnd();
|
|
126
|
+
|
|
127
|
+
return [line(COLUMNS), ...findings.map((finding) => line(COLUMNS.map((column) => finding[column])))];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const notRunLine = (skipped) => `NOT RUN ${skipped.id} — ${skipped.reason}`;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The banner of confirmation `biz-service-manifest` 001 §4 — what the service
|
|
134
|
+
* prints at the end of its init log, and what `oa-validate` could print without
|
|
135
|
+
* the surrounding report.
|
|
136
|
+
*
|
|
137
|
+
* A clean run says so in one line INCLUDING how many rows did not run: silence
|
|
138
|
+
* is the one outcome a reader cannot interpret, and a row that could not look is
|
|
139
|
+
* not a pass (`.claude/rules/automation-gates.md` §5).
|
|
140
|
+
*
|
|
141
|
+
* The table is `renderTable` — the same one `renderReport` prints, because one
|
|
142
|
+
* concern gets one rail (`.claude/rules/change-discipline.md`).
|
|
143
|
+
*
|
|
144
|
+
* @param {object} result the value `runManifest` returned
|
|
145
|
+
* @returns {string[]} the banner, line by line, unterminated
|
|
146
|
+
*/
|
|
147
|
+
function renderBanner(result) {
|
|
148
|
+
const blocking = blockingOf(result);
|
|
149
|
+
const verdict = verdictOf(result);
|
|
150
|
+
const lines = [];
|
|
151
|
+
|
|
152
|
+
if (blocking.length > 0) {
|
|
153
|
+
lines.push(`${verdict.blocked} — ${blocking.length} finding(s)`);
|
|
154
|
+
} else {
|
|
155
|
+
// The banner keeps its subject — it is one line in a long init log — and
|
|
156
|
+
// takes the outcome from the one place that owns it.
|
|
157
|
+
lines.push(`${verdict.clear} — manifest conformance: ${describeClearOutcome(result)}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (result.findings.length > 0) lines.push(...renderTable(result.findings));
|
|
161
|
+
lines.push(...result.notRun.map(notRunLine));
|
|
162
|
+
|
|
163
|
+
return lines;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* One finding as a single sentence in the `[Context] Problem - Expected/Fix`
|
|
168
|
+
* shape of `architecture-principles.md` §5 — what a `boot` finding reads like in
|
|
169
|
+
* `results.errors`, where there is no table to carry the columns.
|
|
170
|
+
*
|
|
171
|
+
* @param {object} finding one entry of `result.findings`
|
|
172
|
+
* @returns {string}
|
|
173
|
+
*/
|
|
174
|
+
function describeFinding(finding) {
|
|
175
|
+
return `[Manifest] ${finding.id} ${finding.where} - ${finding.what}. `
|
|
176
|
+
+ `Fix: ${finding.fix} (owner: ${finding.owner}, doc: ${finding.doc})`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @param {object} result the value `runManifest` returned
|
|
181
|
+
* @returns {string} the whole report, newline-terminated
|
|
182
|
+
*/
|
|
183
|
+
function renderReport(result) {
|
|
184
|
+
const blocking = blockingOf(result);
|
|
185
|
+
const verdict = verdictOf(result);
|
|
186
|
+
|
|
187
|
+
const lines = [
|
|
188
|
+
`Manifest conformance — uniform ${result.uniform}`,
|
|
189
|
+
`Service root: ${result.serviceRoot === null ? 'NOT SET — this is a workspace run' : result.serviceRoot}`,
|
|
190
|
+
`Workspace root: ${result.workspaceRoot === null ? 'NOT RESOLVED' : result.workspaceRoot}`,
|
|
191
|
+
''
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
for (const skipped of result.notRun) {
|
|
195
|
+
lines.push(notRunLine(skipped));
|
|
196
|
+
}
|
|
197
|
+
if (result.notRun.length > 0) lines.push('');
|
|
198
|
+
|
|
199
|
+
if (result.findings.length > 0) {
|
|
200
|
+
lines.push(...renderTable(result.findings), '');
|
|
201
|
+
lines.push('Doc:');
|
|
202
|
+
const seen = new Set();
|
|
203
|
+
for (const finding of result.findings) {
|
|
204
|
+
if (seen.has(finding.id)) continue;
|
|
205
|
+
seen.add(finding.id);
|
|
206
|
+
lines.push(` ${finding.id} ${finding.doc}`);
|
|
207
|
+
}
|
|
208
|
+
lines.push('');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (blocking.length > 0) {
|
|
212
|
+
lines.push(`${verdict.blocked} — ${blocking.length} finding(s) of severity `
|
|
213
|
+
+ `${result.blockingSeverities.join('|')}`);
|
|
214
|
+
} else {
|
|
215
|
+
lines.push(`${verdict.clear} — ${describeClearOutcome(result)}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return `${lines.join('\n')}\n`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* @param {object} result the value `runManifest` returned
|
|
223
|
+
* @returns {string} the same run as data, newline-terminated
|
|
224
|
+
*/
|
|
225
|
+
function renderJson(result) {
|
|
226
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** 001 §3.1: exit 0 or 1. A blocking finding is the only thing that fails a run. */
|
|
230
|
+
function exitCodeFor(result) {
|
|
231
|
+
return result.ok ? 0 : 1;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = {
|
|
235
|
+
renderReport,
|
|
236
|
+
renderBanner,
|
|
237
|
+
renderJson,
|
|
238
|
+
describeFinding,
|
|
239
|
+
describeClearOutcome,
|
|
240
|
+
describeIncomplete,
|
|
241
|
+
exitCodeFor,
|
|
242
|
+
blockingOf,
|
|
243
|
+
verdictOf,
|
|
244
|
+
COLUMNS
|
|
245
|
+
};
|