@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,293 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The rows a library cannot answer alone: whether the SSOT knows it, whether its
|
|
5
|
+
* declared category survives contact with the dependency graph, whether its pins
|
|
6
|
+
* are the versions the SSOT declares, whether anybody uses it at all, and whether
|
|
7
|
+
* a tooling package has stayed out of the services.
|
|
8
|
+
*
|
|
9
|
+
* None of them can answer from the package root alone, and they split by WHAT
|
|
10
|
+
* they are about. `U-ORPHAN` and `U-MISMATCH` are about the whole set, so they
|
|
11
|
+
* are `scope: 'workspace'`. `L-PINS`, `L-CONSUMER` and `L-TOOLING` are facts of
|
|
12
|
+
* ONE package — its pins against the SSOT, who pins it, which service carries
|
|
13
|
+
* it — that reach the answer only through the workspace, so they are
|
|
14
|
+
* `scope: 'bearer'` and run once per package. Carried as `workspace` they had to
|
|
15
|
+
* read a null package root and return `[]`, and the whole-set run then said
|
|
16
|
+
* nothing about them at all.
|
|
17
|
+
* Inside a container none of these files is present, and a row that cannot look
|
|
18
|
+
* is reported NOT RUN and never as passing
|
|
19
|
+
* (`.claude/rules/automation-gates.md` §5). The same holds one level down: a
|
|
20
|
+
* row reading a directory this checkout does not carry declares it in
|
|
21
|
+
* `requiresSiblings`, and the runner reports NOT RUN before the check runs.
|
|
22
|
+
*
|
|
23
|
+
* @see api/docs/governance/confirmations/biz-service-manifest.md §11
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const path = require('path');
|
|
27
|
+
|
|
28
|
+
const {
|
|
29
|
+
discoverBearers, resolveFromReference, resolveFromMap, expandPattern, rootOfPattern
|
|
30
|
+
} = require('../discovery');
|
|
31
|
+
const { resolveWorkspacePath } = require('../workspaceRoot');
|
|
32
|
+
const {
|
|
33
|
+
readJson, readPackage, whereOf, declaredCategory, appliesTo, scopedDeps, SCOPE
|
|
34
|
+
} = require('./libraryContext');
|
|
35
|
+
|
|
36
|
+
/** The section that decides the layer: what the package pulls into a runtime. */
|
|
37
|
+
const RUNTIME_SECTIONS = Object.freeze(['dependencies']);
|
|
38
|
+
|
|
39
|
+
/** The sections `scripts/ci/verify-manifest-pins.mjs` compares against the SSOT. */
|
|
40
|
+
const PINNED_SECTIONS = Object.freeze(['dependencies', 'devDependencies']);
|
|
41
|
+
|
|
42
|
+
/** Why a workspace-scoped row prints nothing instead of a verdict. */
|
|
43
|
+
const notRunBecause = (what) => `the workspace root is not reachable, so ${what} cannot be read`;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Every package the uniform's discovery pattern finds, indexed by the name its
|
|
47
|
+
* own `package.json` carries. That name — not the directory — is what the SSOT
|
|
48
|
+
* owns and what a dependency writes.
|
|
49
|
+
*
|
|
50
|
+
* @param {{ block: object, workspaceRoot: string }} params
|
|
51
|
+
* @returns {Map<string, {relativeDir: string, dir: string, category: string|null}>}
|
|
52
|
+
*/
|
|
53
|
+
function indexBearers({ block, workspaceRoot }) {
|
|
54
|
+
const { bearers } = discoverBearers({ block, workspaceRoot });
|
|
55
|
+
const index = new Map();
|
|
56
|
+
|
|
57
|
+
for (const bearer of bearers) {
|
|
58
|
+
const json = readJson(resolveWorkspacePath(workspaceRoot, bearer.file));
|
|
59
|
+
if (json === null || typeof json.name !== 'string') continue;
|
|
60
|
+
index.set(json.name, {
|
|
61
|
+
relativeDir: bearer.relativeDir,
|
|
62
|
+
dir: bearer.dir,
|
|
63
|
+
category: declaredCategory(json)
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return index;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const libraryOrphan = Object.freeze({
|
|
70
|
+
scope: 'workspace',
|
|
71
|
+
requires: Object.freeze([]),
|
|
72
|
+
|
|
73
|
+
requiresSiblings({ block }) {
|
|
74
|
+
return [rootOfPattern(block.pattern)];
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
describeNotRun({ block }) {
|
|
78
|
+
return notRunBecause(block && block.from ? block.from.path : 'the referenced SSOT');
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
run({ block, workspaceRoot }) {
|
|
82
|
+
const onDisk = indexBearers({ block, workspaceRoot });
|
|
83
|
+
const declared = new Set(resolveFromReference({ from: block.from, workspaceRoot }));
|
|
84
|
+
const findings = [];
|
|
85
|
+
|
|
86
|
+
for (const [name, bearer] of onDisk) {
|
|
87
|
+
if (declared.has(name)) continue;
|
|
88
|
+
findings.push({
|
|
89
|
+
where: bearer.relativeDir,
|
|
90
|
+
what: `${name} is not declared in ${block.from.path} (${block.from.list}) — nothing pins it and nothing publishes it`
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const name of declared) {
|
|
95
|
+
if (onDisk.has(name)) continue;
|
|
96
|
+
findings.push({
|
|
97
|
+
where: block.from.path,
|
|
98
|
+
what: `${name} is declared with a version but no package matches ${block.pattern}`
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return findings;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const libraryCategory = Object.freeze({
|
|
107
|
+
scope: 'workspace',
|
|
108
|
+
requires: Object.freeze([]),
|
|
109
|
+
|
|
110
|
+
requiresSiblings({ block }) {
|
|
111
|
+
return [rootOfPattern(block.pattern)];
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
describeNotRun() {
|
|
115
|
+
return notRunBecause('the categories of the packages this one depends on');
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
run({ block, serviceRoot, workspaceRoot }) {
|
|
119
|
+
const { json } = readPackage(serviceRoot);
|
|
120
|
+
if (json === null) return [];
|
|
121
|
+
const where = whereOf({ scope: 'workspace', serviceRoot, workspaceRoot, relative: 'package.json' });
|
|
122
|
+
|
|
123
|
+
const declared = declaredCategory(json);
|
|
124
|
+
if (declared === null) {
|
|
125
|
+
return [{
|
|
126
|
+
where,
|
|
127
|
+
what: 'declares no "oa"."category" — the layer a package is meant to sit in cannot be derived from anything else'
|
|
128
|
+
}];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const definition = block.categories[declared];
|
|
132
|
+
if (definition === undefined) {
|
|
133
|
+
return [{
|
|
134
|
+
where,
|
|
135
|
+
what: `declares category "${declared}", which this uniform does not define `
|
|
136
|
+
+ `(defined: ${Object.keys(block.categories).join(', ')})`
|
|
137
|
+
}];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const index = indexBearers({ block, workspaceRoot });
|
|
141
|
+
const allowed = new Set(definition.may_depend_on);
|
|
142
|
+
|
|
143
|
+
return scopedDeps(json, RUNTIME_SECTIONS).map(({ name }) => {
|
|
144
|
+
const dependency = index.get(name);
|
|
145
|
+
if (dependency === undefined) {
|
|
146
|
+
return {
|
|
147
|
+
where,
|
|
148
|
+
what: `depends on ${name}, which no package under ${block.pattern} provides — its category cannot be read`
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (dependency.category === null) {
|
|
152
|
+
return {
|
|
153
|
+
where,
|
|
154
|
+
what: `depends on ${name}, which declares no category — the layer of "${declared}" cannot be verified`
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (!allowed.has(dependency.category)) {
|
|
158
|
+
return {
|
|
159
|
+
where,
|
|
160
|
+
what: `declares category "${declared}" but depends on ${name}, which is "${dependency.category}" `
|
|
161
|
+
+ `(may_depend_on: ${definition.may_depend_on.join(', ') || 'nothing'})`
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}).filter((finding) => finding !== null);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const libraryPins = Object.freeze({
|
|
170
|
+
scope: 'bearer',
|
|
171
|
+
requires: Object.freeze(['from']),
|
|
172
|
+
|
|
173
|
+
describeNotRun({ row }) {
|
|
174
|
+
return notRunBecause(row.from.path);
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
run({ row, block, serviceRoot, workspaceRoot }) {
|
|
178
|
+
const { json } = readPackage(serviceRoot);
|
|
179
|
+
if (json === null || !appliesTo(block, json)) return [];
|
|
180
|
+
|
|
181
|
+
const ssot = resolveFromMap({ from: row.from, workspaceRoot });
|
|
182
|
+
const where = whereOf({ scope: 'bearer', serviceRoot, workspaceRoot, relative: 'package.json' });
|
|
183
|
+
|
|
184
|
+
return scopedDeps(json, PINNED_SECTIONS)
|
|
185
|
+
.map(({ name, spec, section }) => {
|
|
186
|
+
const owned = ssot[name];
|
|
187
|
+
if (owned === undefined) {
|
|
188
|
+
return { where, what: `${section} pins ${name}, which ${row.from.path} does not declare` };
|
|
189
|
+
}
|
|
190
|
+
if (spec !== owned) {
|
|
191
|
+
return { where, what: `${section} pins ${name} at "${spec}" but ${row.from.path} declares ${owned}` };
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
})
|
|
195
|
+
.filter((finding) => finding !== null);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const libraryConsumer = Object.freeze({
|
|
200
|
+
scope: 'bearer',
|
|
201
|
+
requires: Object.freeze(['consumer_patterns']),
|
|
202
|
+
|
|
203
|
+
// The row already says where the consumers live; the roots it needs are read
|
|
204
|
+
// off that, never restated. In a checkout carrying `api/` alone, `api_biz` is
|
|
205
|
+
// absent and "pinned by nothing" would be a lie about a file nobody opened.
|
|
206
|
+
requiresSiblings({ row }) {
|
|
207
|
+
return row.consumer_patterns.map(rootOfPattern);
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
describeNotRun({ row }) {
|
|
211
|
+
return notRunBecause(`the consumers under ${row.consumer_patterns.join(', ')}`);
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
run({ row, block, serviceRoot, workspaceRoot }) {
|
|
215
|
+
const { json, file } = readPackage(serviceRoot);
|
|
216
|
+
if (json === null || !appliesTo(block, json)) return [];
|
|
217
|
+
|
|
218
|
+
const own = path.resolve(file);
|
|
219
|
+
const consumers = [];
|
|
220
|
+
|
|
221
|
+
for (const pattern of row.consumer_patterns) {
|
|
222
|
+
for (const relative of expandPattern(workspaceRoot, pattern)) {
|
|
223
|
+
const absolute = resolveWorkspacePath(workspaceRoot, relative);
|
|
224
|
+
if (path.resolve(absolute) === own) continue;
|
|
225
|
+
const consumer = readJson(absolute);
|
|
226
|
+
if (consumer === null) continue;
|
|
227
|
+
const declared = { ...(consumer.dependencies || {}), ...(consumer.devDependencies || {}) };
|
|
228
|
+
if (Object.prototype.hasOwnProperty.call(declared, json.name)) consumers.push(relative);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (consumers.length > 0) return [];
|
|
233
|
+
return [{
|
|
234
|
+
where: whereOf({ scope: 'bearer', serviceRoot, workspaceRoot, relative: 'package.json' }),
|
|
235
|
+
what: `${json.name} is pinned by nothing under ${row.consumer_patterns.join(', ')} — it is unwired or dead`
|
|
236
|
+
}];
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const libraryTooling = Object.freeze({
|
|
241
|
+
scope: 'bearer',
|
|
242
|
+
requires: Object.freeze(['service_pattern']),
|
|
243
|
+
|
|
244
|
+
requiresSiblings({ row }) {
|
|
245
|
+
return [rootOfPattern(row.service_pattern)];
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
describeNotRun({ row }) {
|
|
249
|
+
return notRunBecause(`the services under ${row.service_pattern}`);
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
run({ row, block, serviceRoot, workspaceRoot }) {
|
|
253
|
+
const { json } = readPackage(serviceRoot);
|
|
254
|
+
if (json === null || !appliesTo(block, json)) return [];
|
|
255
|
+
|
|
256
|
+
const where = whereOf({ scope: 'bearer', serviceRoot, workspaceRoot, relative: 'package.json' });
|
|
257
|
+
const findings = [];
|
|
258
|
+
|
|
259
|
+
const bin = json.bin;
|
|
260
|
+
const hasBin = typeof bin === 'string' ? bin.length > 0 : (bin !== null && typeof bin === 'object' && Object.keys(bin).length > 0);
|
|
261
|
+
if (!hasBin) {
|
|
262
|
+
findings.push({ where, what: 'declares no "bin" — tooling is reached by a command, not by an import' });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
for (const relative of expandPattern(workspaceRoot, row.service_pattern)) {
|
|
266
|
+
const service = readJson(resolveWorkspacePath(workspaceRoot, relative));
|
|
267
|
+
if (service === null) continue;
|
|
268
|
+
if (!Object.prototype.hasOwnProperty.call(service.dependencies || {}, json.name)) continue;
|
|
269
|
+
// The finding is reported at the LIBRARY, not at the service: it is the
|
|
270
|
+
// library's duty and the library's table, and a finding placed under a
|
|
271
|
+
// neighbour's root is one a service-mode run would drop entirely
|
|
272
|
+
// (`runManifest.js` § The two modes).
|
|
273
|
+
findings.push({
|
|
274
|
+
where,
|
|
275
|
+
what: `${relative} carries ${json.name} in dependencies — tooling never runs inside a service`
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return findings;
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
module.exports = {
|
|
284
|
+
checks: [
|
|
285
|
+
{ name: 'library-orphan', check: libraryOrphan },
|
|
286
|
+
{ name: 'library-category', check: libraryCategory },
|
|
287
|
+
{ name: 'library-pins', check: libraryPins },
|
|
288
|
+
{ name: 'library-consumer', check: libraryConsumer },
|
|
289
|
+
{ name: 'library-tooling', check: libraryTooling }
|
|
290
|
+
],
|
|
291
|
+
indexBearers,
|
|
292
|
+
SCOPE
|
|
293
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The `Uniform:` region of a README, as a ROW rather than only as a `--check`.
|
|
5
|
+
*
|
|
6
|
+
* Owner decision 2026-09-09 (answering the kontrolor's d.215c note): that region
|
|
7
|
+
* is of the class `generated` — confirmation `biz-service-manifest` 001 §2 makes
|
|
8
|
+
* such a class a GENERATOR plus a row carrying the `--check` as its gate, the
|
|
9
|
+
* precedent being `G-SHARED-ENV`. Until this module the generator existed and
|
|
10
|
+
* the gate did not, so a README whose pointer had gone stale was reported by
|
|
11
|
+
* nothing anybody runs on a repository — the false guarantee of
|
|
12
|
+
* `.claude/rules/automation-gates.md` §5.
|
|
13
|
+
*
|
|
14
|
+
* ONE IMPLEMENTATION, TWO READERS. Nothing here decides what the region says:
|
|
15
|
+
* it renders through `readmePointer.js` and compares through the very
|
|
16
|
+
* `checkUniformRegion` the `--check` run calls, so the row and the command
|
|
17
|
+
* cannot disagree about the same file (`.claude/rules/change-discipline.md`
|
|
18
|
+
* § One rail per concern). Where the region's link points is
|
|
19
|
+
* `readmeLocation.js`, for the same reason.
|
|
20
|
+
*
|
|
21
|
+
* WHAT THE FINDING SAYS. The `--check` run prints the whole region twice, as a
|
|
22
|
+
* `-`/`+` block; a row's `what` is a table cell, so it names the first line that
|
|
23
|
+
* differs and what the file says there — the locator shape `F-INIT` and the sync
|
|
24
|
+
* already use. The full diff is one command away, and that command is the row's
|
|
25
|
+
* `fix`.
|
|
26
|
+
*
|
|
27
|
+
* @see api/docs/governance/confirmations/biz-service-manifest.md § Confirmation 20260909-biz-service-manifest-005
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const { KINDS, extractRegion, checkUniformRegion } = require('../../sync/readmePointer');
|
|
31
|
+
const {
|
|
32
|
+
readReadme, serviceRegion, libraryRegion, PACKAGE_IN_WORKSPACE
|
|
33
|
+
} = require('../../sync/readmeLocation');
|
|
34
|
+
const { whereOf, declaredCategory, readPackage } = require('./libraryContext');
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The first line at which the region on disk stops being the rendered one, and
|
|
38
|
+
* what the file says there.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} expected the rendered region
|
|
41
|
+
* @param {string} actual the region on disk
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
function firstDifference(expected, actual) {
|
|
45
|
+
const want = expected.split('\n');
|
|
46
|
+
const have = actual.split('\n');
|
|
47
|
+
const length = Math.max(want.length, have.length);
|
|
48
|
+
for (let index = 0; index < length; index += 1) {
|
|
49
|
+
if (want[index] === have[index]) continue;
|
|
50
|
+
return `at region line ${index + 1}: ${JSON.stringify(have[index] === undefined ? '<end of region>' : have[index].trim())}`;
|
|
51
|
+
}
|
|
52
|
+
return 'nowhere — the region matches line for line';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* One README measured against the region its manifest renders.
|
|
57
|
+
*
|
|
58
|
+
* @param {{ kind: string, dir: string, region: string, where: string, path: string }} params
|
|
59
|
+
* @returns {Array<{where: string, what: string}>}
|
|
60
|
+
*/
|
|
61
|
+
function compare({ kind, dir, region, where, path: relative }) {
|
|
62
|
+
const text = readReadme(dir);
|
|
63
|
+
if (text === null) {
|
|
64
|
+
return [{ where, what: `absent — the uniform pointer is a generated region of ${relative}` }];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const result = checkUniformRegion(text, region, { kind });
|
|
68
|
+
if (result.ok) return [];
|
|
69
|
+
|
|
70
|
+
const actual = extractRegion(text, { kind });
|
|
71
|
+
if (actual === null) {
|
|
72
|
+
return [{
|
|
73
|
+
where,
|
|
74
|
+
what: 'carries no generated uniform region — which uniform a repository wears is rendered from the '
|
|
75
|
+
+ 'manifest, never typed and never absent'
|
|
76
|
+
}];
|
|
77
|
+
}
|
|
78
|
+
return [{ where, what: `the generated uniform region is not what the manifest renders, ${firstDifference(region, actual)}` }];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const readmeUniformCurrent = Object.freeze({
|
|
82
|
+
scope: 'service',
|
|
83
|
+
requires: Object.freeze(['path', 'from']),
|
|
84
|
+
|
|
85
|
+
run({ row, serviceRoot }) {
|
|
86
|
+
return compare({
|
|
87
|
+
kind: KINDS.service,
|
|
88
|
+
dir: serviceRoot,
|
|
89
|
+
path: row.path,
|
|
90
|
+
where: row.path,
|
|
91
|
+
region: serviceRegion(serviceRoot)
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const libraryReadmeRegion = Object.freeze({
|
|
97
|
+
scope: 'bearer',
|
|
98
|
+
requires: Object.freeze(['path']),
|
|
99
|
+
|
|
100
|
+
// The link this region carries points at the library manifest as it lies in
|
|
101
|
+
// the checkout being read, so the row can only answer where that checkout
|
|
102
|
+
// holds a copy of this package.
|
|
103
|
+
requiresSiblings: () => (PACKAGE_IN_WORKSPACE === null ? [] : [PACKAGE_IN_WORKSPACE]),
|
|
104
|
+
|
|
105
|
+
describeNotRun() {
|
|
106
|
+
return 'the workspace root is not reachable, so the manifest the region links at cannot be located';
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
run({ row, serviceRoot, workspaceRoot }) {
|
|
110
|
+
const { json } = readPackage(serviceRoot);
|
|
111
|
+
|
|
112
|
+
// A package declaring no category is `U-MISMATCH`, blocking, with its own
|
|
113
|
+
// fix — and the region cannot be rendered without one, because the duty
|
|
114
|
+
// sections it lists ARE the category's. Saying so twice would give one
|
|
115
|
+
// defect two owners; this is the same silence `oa-sync-template
|
|
116
|
+
// readme-uniform --all` keeps, and for the same reason.
|
|
117
|
+
if (json === null || declaredCategory(json) === null) return [];
|
|
118
|
+
|
|
119
|
+
return compare({
|
|
120
|
+
kind: KINDS.library,
|
|
121
|
+
dir: serviceRoot,
|
|
122
|
+
path: row.path,
|
|
123
|
+
where: whereOf({ scope: 'bearer', serviceRoot, workspaceRoot, relative: row.path }),
|
|
124
|
+
region: libraryRegion({ packageDir: serviceRoot, pkg: json, workspaceRoot })
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
checks: [
|
|
131
|
+
{ name: 'readme-uniform-current', check: readmeUniformCurrent },
|
|
132
|
+
{ name: 'library-readme-region', check: libraryReadmeRegion }
|
|
133
|
+
],
|
|
134
|
+
firstDifference
|
|
135
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `S-SCRIPTS` — what a reader can learn about a script before running it.
|
|
5
|
+
*
|
|
6
|
+
* Confirmation `biz-service-manifest` 003 §19 makes the repository's own
|
|
7
|
+
* `scripts/` a duty of the uniform, checked by the lint "shipped in the
|
|
8
|
+
* validator package". That last clause is the whole design: the rules used to
|
|
9
|
+
* be a file of the `api` checkout, and a service has no such directory in its
|
|
10
|
+
* own CI and none at all inside its image, so the row could only ever have been
|
|
11
|
+
* NOT RUN where it is meant to answer. `src/lint/scripts/lintScripts.js` holds
|
|
12
|
+
* them now; this module is the bridge from the row to that module and decides
|
|
13
|
+
* nothing itself — the same shape `contractBridge.js` and `docsLintBridge.js`
|
|
14
|
+
* keep (004 point 4: a rule that exists is cited, never restated).
|
|
15
|
+
*
|
|
16
|
+
* ONE THING NEEDS THE WORKSPACE, AND ONLY WHEN IT IS USED. The header rules
|
|
17
|
+
* read the repository alone, so the row is `scope: service` and answers in a
|
|
18
|
+
* container. The exception is `S006`: a script may cite `api/docs/…`, and that
|
|
19
|
+
* path names the api checkout beside this repository, not a `docs/` of its own.
|
|
20
|
+
* With the workspace reachable the citation is resolved there; without it the
|
|
21
|
+
* lint returns the target as UNRESOLVED and this module reports the row NOT RUN
|
|
22
|
+
* with the targets named — a run that could not look must not report a pass
|
|
23
|
+
* (`.claude/rules/automation-gates.md` §5). A repository citing no such path is
|
|
24
|
+
* decided in full wherever it lies, which is the common case: measured
|
|
25
|
+
* 2026-09-10, 4 of the 47 `@see` lines under the eight services open with the
|
|
26
|
+
* prefix.
|
|
27
|
+
*
|
|
28
|
+
* @see api/docs/standards/SCRIPTS-STANDARD.md
|
|
29
|
+
* @see api/docs/governance/confirmations/biz-service-manifest.md
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
|
|
34
|
+
const { lintScripts, SCRIPT_SCOPE } = require('../../lint/scripts/lintScripts');
|
|
35
|
+
const { resolveWorkspacePath, WORKSPACE_MARKER } = require('../workspaceRoot');
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The api checkout, by the name the workspace marker already spells. Written
|
|
39
|
+
* once, from the marker, so "which directory is the api one" has one owner
|
|
40
|
+
* (`api/.claude/rules/single-source-of-truth.md`).
|
|
41
|
+
*/
|
|
42
|
+
const API_CHECKOUT = WORKSPACE_MARKER.split('/')[0];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Where the citations of one repository can be resolved, or null.
|
|
46
|
+
*
|
|
47
|
+
* @param {string|null} workspaceRoot
|
|
48
|
+
* @returns {string|null}
|
|
49
|
+
*/
|
|
50
|
+
function apiCheckout(workspaceRoot) {
|
|
51
|
+
if (workspaceRoot === null || workspaceRoot === undefined) return null;
|
|
52
|
+
const target = resolveWorkspacePath(workspaceRoot, API_CHECKOUT);
|
|
53
|
+
return fs.existsSync(target) && fs.statSync(target).isDirectory() ? target : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const scriptsLint = Object.freeze({
|
|
57
|
+
scope: 'service',
|
|
58
|
+
requires: Object.freeze(['path']),
|
|
59
|
+
|
|
60
|
+
run({ serviceRoot, workspaceRoot }) {
|
|
61
|
+
const result = lintScripts({ root: serviceRoot, apiRoot: apiCheckout(workspaceRoot) });
|
|
62
|
+
|
|
63
|
+
const findings = result.findings.map((finding) => ({
|
|
64
|
+
where: `${finding.file}:${finding.line}`,
|
|
65
|
+
what: `${finding.id} — ${finding.message}`
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
if (result.unresolved.length === 0) return findings;
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
findings,
|
|
72
|
+
notRun: `${result.unresolved.length} @see target(s) name the ${API_CHECKOUT} checkout, which this run `
|
|
73
|
+
+ `cannot reach: ${result.unresolved.join(', ')}. Run the uniform from a workspace that carries `
|
|
74
|
+
+ `${API_CHECKOUT}/.`
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
module.exports = { checks: [{ name: 'scripts-lint', check: scriptsLint }], SCRIPT_SCOPE, API_CHECKOUT };
|