@onlineapps/conn-orch-validator 7.0.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2558 -2
- package/README.md +1038 -4
- package/docs/DESIGN.md +3 -1
- package/manifests/biz-service.manifest.json +658 -0
- package/manifests/library.manifest.json +324 -0
- package/package.json +12 -6
- package/src/CookbookTestRunner.js +408 -101
- package/src/CookbookTestUtils.js +7 -8
- package/src/ServiceReadinessValidator.js +10 -35
- package/src/ValidationOrchestrator.js +219 -71
- package/src/cli/biz-ci-gate.js +176 -33
- package/src/cli/oa-lint-scripts.js +221 -0
- package/src/cli/oa-sync-template.js +1020 -0
- package/src/cli/oa-validate.js +474 -0
- package/src/helpers/README.md +2 -1
- package/src/helpers/createServiceReadinessTests.js +60 -4
- package/src/index.js +33 -3
- package/src/lint/scripts/lintScripts.js +298 -0
- package/src/manifest/checks/composeRunnerBlock.js +222 -0
- package/src/manifest/checks/composeShape.js +165 -0
- package/src/manifest/checks/contractBridge.js +181 -0
- package/src/manifest/checks/discoveryOrphan.js +50 -0
- package/src/manifest/checks/docsLintBridge.js +553 -0
- package/src/manifest/checks/fileAbsent.js +35 -0
- package/src/manifest/checks/gitTracked.js +204 -0
- package/src/manifest/checks/index.js +111 -0
- package/src/manifest/checks/libraryContext.js +226 -0
- package/src/manifest/checks/libraryDocs.js +75 -0
- package/src/manifest/checks/libraryPackage.js +272 -0
- package/src/manifest/checks/librarySource.js +274 -0
- package/src/manifest/checks/libraryTests.js +121 -0
- package/src/manifest/checks/libraryWorkspace.js +293 -0
- package/src/manifest/checks/readmeRegion.js +135 -0
- package/src/manifest/checks/scriptHeaders.js +79 -0
- package/src/manifest/checks/serviceConfig.js +390 -0
- package/src/manifest/checks/serviceConnectors.js +81 -0
- package/src/manifest/checks/serviceDb.js +388 -0
- package/src/manifest/checks/serviceFiles.js +754 -0
- package/src/manifest/checks/serviceIdentityRows.js +351 -0
- package/src/manifest/checks/serviceRuntime.js +295 -0
- package/src/manifest/checks/serviceScripts.js +213 -0
- package/src/manifest/deployabilitySignal.js +121 -0
- package/src/manifest/discovery.js +386 -0
- package/src/manifest/loadManifest.js +62 -0
- package/src/manifest/manifestShape.js +446 -0
- package/src/manifest/report.js +245 -0
- package/src/manifest/runManifest.js +449 -0
- package/src/manifest/serviceIdentity.js +140 -0
- package/src/manifest/walk.js +74 -0
- package/src/manifest/workspaceRoot.js +242 -0
- package/src/mocks/MockMQClient.js +13 -30
- package/src/mocks/MockRegistry.js +4 -2
- package/src/mocks/MockStorage.js +4 -2
- package/src/sync/docsRegion.js +463 -0
- package/src/sync/generatedRegion.js +228 -0
- package/src/sync/readmeLocation.js +182 -0
- package/src/sync/readmePointer.js +477 -0
- package/src/sync/serviceTemplate.js +583 -0
- package/src/sync/sharedEnv.js +162 -0
- package/src/sync/uniformFiles.js +474 -0
- package/src/utils/bizCiGateContract.js +131 -7
- package/src/utils/connectorContract.js +97 -7
- package/src/utils/cookbookFormat.js +81 -40
- package/src/utils/deployContract.js +140 -9
- package/src/utils/envContract.js +57 -1
- package/src/utils/handlerRef.js +181 -0
- package/src/utils/installContract.js +287 -41
- package/src/utils/libCompat.js +29 -7
- package/src/utils/migrationOrder.js +163 -0
- package/src/utils/preValidation.js +20 -7
- package/src/utils/setupDatabase.js +194 -13
- package/src/utils/testCoverageContract.js +539 -0
- package/src/utils/testNamespace.js +247 -23
- package/src/utils/throwawaySchema.js +207 -0
- package/src/validators/ServiceStructureValidator.js +2 -1
- package/templates/business-service/.dockerignore +42 -0
- package/templates/business-service/.gitlab-ci.yml +290 -0
- package/templates/business-service/Dockerfile +27 -0
- package/templates/business-service/README.md +213 -0
- package/templates/business-service/config/biz-docs-lint.tree.json +10 -0
- package/templates/business-service/config/env-templates/__SERVICE_NAME__.env +4 -0
- package/templates/business-service/config/env-templates/shared.env +65 -0
- package/templates/business-service/config/service/config.json +14 -0
- package/templates/business-service/config/service/integration-contract.json +12 -0
- package/templates/business-service/config/service/operations.json +41 -0
- package/templates/business-service/docker-compose.production.yml +60 -0
- package/templates/business-service/docker-compose.yml +93 -0
- package/templates/business-service/docs/80-setup/INSTALL.md +101 -0
- package/templates/business-service/docs/80-setup/PLATFORM_MATRIX.md +65 -0
- package/templates/business-service/docs/80-setup/README.md +18 -0
- package/templates/business-service/docs/80-setup/VALIDATION.md +78 -0
- package/templates/business-service/docs/README.md +18 -0
- package/templates/business-service/gitignore +42 -0
- package/templates/business-service/index.js +10 -0
- package/templates/business-service/init.sh +54 -0
- package/templates/business-service/jest.config.js +6 -0
- package/templates/business-service/package.json.template +31 -0
- package/templates/business-service/scripts/verify-deploy-uniform.sh +180 -0
- package/templates/business-service/src/handlers/v3/echo.js +39 -0
- package/templates/business-service/tests/cookbooks/echo.json +36 -0
- package/templates/business-service/tests/unit/handler.test.js +78 -0
- package/src/WorkflowTestRunner.js +0 -402
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Discovery — who wears the uniform.
|
|
5
|
+
*
|
|
6
|
+
* Confirmation `biz-service-manifest` 005 point 1: completeness is discovery.
|
|
7
|
+
* The manifest names a pattern; the check walks the disk and binds every bearer
|
|
8
|
+
* it finds to the SSOT that owns the names (004 point 2 — a reference, never a
|
|
9
|
+
* copy). A directory that matches no pattern, or one whose SSOT does not know
|
|
10
|
+
* it, is `U-ORPHAN`. No header is read and none is needed: a bearer's uniform
|
|
11
|
+
* is its location.
|
|
12
|
+
*
|
|
13
|
+
* Disagreement between the SSOT and the disk in the OTHER direction (a name
|
|
14
|
+
* declared with no directory) is a finding of the SSOT or of the repository,
|
|
15
|
+
* not of the manifest (004 point 4); `api/tests/scripts/repository-inventory.bats`
|
|
16
|
+
* already owns it and nothing here reimplements it.
|
|
17
|
+
*
|
|
18
|
+
* @see api/docs/governance/confirmations/biz-service-manifest.md § Confirmation 20260909-biz-service-manifest-005
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
const { resolveWorkspacePath } = require('./workspaceRoot');
|
|
25
|
+
|
|
26
|
+
/** Never walked: neither is part of any repository's declared shape. */
|
|
27
|
+
const NEVER_WALKED = Object.freeze(['node_modules', '.git']);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* This package's own root — the directory carrying `manifests/` and
|
|
31
|
+
* `templates/`. Derived from this file's location, so it is the SAME copy the
|
|
32
|
+
* pin decides: a checkout's `api/shared/connector/conn-orch-validator`, or the
|
|
33
|
+
* `node_modules/@onlineapps/conn-orch-validator` a service installed.
|
|
34
|
+
*/
|
|
35
|
+
const PACKAGE_ROOT = path.join(__dirname, '..', '..');
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Whether a `from:` reference names a file this package carries itself.
|
|
39
|
+
*
|
|
40
|
+
* The distinction decides where the file is read from AND whether the row can
|
|
41
|
+
* run at all without a workspace, so it is asked in one place and never by
|
|
42
|
+
* looking at a path prefix (`change-discipline.md` § One rail per concern).
|
|
43
|
+
*
|
|
44
|
+
* @param {*} from the reference as written
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
function isPackageReference(from) {
|
|
48
|
+
return from !== null && typeof from === 'object' && typeof from.package === 'string' && from.package.length > 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The file a reference names, as a reader is told where to find it: the
|
|
53
|
+
* workspace-relative path, or the package-relative one. Every message about a
|
|
54
|
+
* reference goes through this, so a row that moved its fact from the workspace
|
|
55
|
+
* into the package does not leave a message pointing at the old owner.
|
|
56
|
+
*
|
|
57
|
+
* @param {*} from the reference as written
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
function referenceOwner(from) {
|
|
61
|
+
if (isPackageReference(from)) return from.package;
|
|
62
|
+
if (from !== null && typeof from === 'object' && typeof from.path === 'string') return from.path;
|
|
63
|
+
throw new Error('[ManifestDiscovery] Reference names no file - referenceOwner() got '
|
|
64
|
+
+ `${JSON.stringify(from)}. Fix: a from: reference names either a workspace "path" or a "package" file.`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** `**` matches any number of directories, `*` matches within one segment. */
|
|
68
|
+
function globToRegExp(pattern) {
|
|
69
|
+
const segments = pattern.split('/');
|
|
70
|
+
let source = '^';
|
|
71
|
+
segments.forEach((segment, index) => {
|
|
72
|
+
const last = index === segments.length - 1;
|
|
73
|
+
if (segment === '**') {
|
|
74
|
+
source += last ? '.*' : '(?:[^/]+/)*';
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
source += segment.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*');
|
|
78
|
+
if (!last) source += '/';
|
|
79
|
+
});
|
|
80
|
+
return new RegExp(`${source}$`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {string} relativePath path relative to the workspace root, `/`-separated
|
|
85
|
+
* @param {string[]} [patterns] the manifest's exclude patterns
|
|
86
|
+
* @returns {boolean}
|
|
87
|
+
*/
|
|
88
|
+
function isExcluded(relativePath, patterns) {
|
|
89
|
+
if (!Array.isArray(patterns) || patterns.length === 0) return false;
|
|
90
|
+
return patterns.some((pattern) => globToRegExp(pattern).test(relativePath));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const listEntries = (dir) => {
|
|
94
|
+
try {
|
|
95
|
+
return fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
96
|
+
} catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Every file under `root` matching `pattern`, as workspace-relative paths.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} root workspace root
|
|
105
|
+
* @param {string} pattern the manifest's discovery pattern
|
|
106
|
+
* @returns {string[]}
|
|
107
|
+
*/
|
|
108
|
+
function expandPattern(root, pattern) {
|
|
109
|
+
const segments = pattern.split('/');
|
|
110
|
+
const found = [];
|
|
111
|
+
|
|
112
|
+
const walk = (relativeDir, index) => {
|
|
113
|
+
const segment = segments[index];
|
|
114
|
+
const last = index === segments.length - 1;
|
|
115
|
+
const absoluteDir = relativeDir ? resolveWorkspacePath(root, relativeDir) : root;
|
|
116
|
+
const join = (name) => (relativeDir ? `${relativeDir}/${name}` : name);
|
|
117
|
+
|
|
118
|
+
if (segment === '**') {
|
|
119
|
+
walk(relativeDir, index + 1);
|
|
120
|
+
for (const entry of listEntries(absoluteDir)) {
|
|
121
|
+
if (!entry.isDirectory() || NEVER_WALKED.includes(entry.name)) continue;
|
|
122
|
+
walk(join(entry.name), index);
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (segment.includes('*')) {
|
|
128
|
+
const matcher = globToRegExp(segment);
|
|
129
|
+
for (const entry of listEntries(absoluteDir)) {
|
|
130
|
+
if (!matcher.test(entry.name)) continue;
|
|
131
|
+
if (last && entry.isFile()) found.push(join(entry.name));
|
|
132
|
+
else if (!last && entry.isDirectory()) walk(join(entry.name), index + 1);
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const candidate = resolveWorkspacePath(root, join(segment));
|
|
138
|
+
if (last) {
|
|
139
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) found.push(join(segment));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) walk(join(segment), index + 1);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
walk('', 0);
|
|
146
|
+
return found;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The directory a pattern starts from: its longest prefix carrying no wildcard.
|
|
151
|
+
*
|
|
152
|
+
* That directory is what has to EXIST before the pattern can answer anything.
|
|
153
|
+
* `api_biz/*/package.json` starts at `api_biz`, `api/infra/*/package.json`
|
|
154
|
+
* at `api/infra`, `api/shared/**/package.json` at `api/shared`. In a checkout
|
|
155
|
+
* that carries `api/` alone — CI's `git archive` export — the first is absent and
|
|
156
|
+
* the other two are not, and a check reading it must say NOT RUN rather than
|
|
157
|
+
* report the empty set as an answer (`automation-gates.md` §5).
|
|
158
|
+
*
|
|
159
|
+
* @param {string} pattern a workspace-relative discovery pattern
|
|
160
|
+
* @returns {string} the workspace-relative directory it starts from
|
|
161
|
+
*/
|
|
162
|
+
function rootOfPattern(pattern) {
|
|
163
|
+
const segments = pattern.split('/');
|
|
164
|
+
const wildcard = segments.findIndex((segment) => segment.includes('*'));
|
|
165
|
+
const directory = wildcard === -1 ? segments.slice(0, -1) : segments.slice(0, wildcard);
|
|
166
|
+
return directory.join('/');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The container a `<dir>/*/<file>` pattern enumerates, so that a directory
|
|
171
|
+
* carrying no matching file can still be seen. A pattern of another shape has
|
|
172
|
+
* no container, and orphan detection over directories does not apply to it.
|
|
173
|
+
*
|
|
174
|
+
* @param {string} pattern
|
|
175
|
+
* @returns {string|null}
|
|
176
|
+
*/
|
|
177
|
+
function containerOf(pattern) {
|
|
178
|
+
const segments = pattern.split('/');
|
|
179
|
+
if (segments.length < 3) return null;
|
|
180
|
+
if (segments[1] !== '*') return null;
|
|
181
|
+
if (segments.includes('**')) return null;
|
|
182
|
+
return segments[0];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Read the file a `from:` reference points at. Nothing is interpreted here — the
|
|
187
|
+
* three resolvers below decide what the reference means.
|
|
188
|
+
*
|
|
189
|
+
* @param {{ from: {path: string}, workspaceRoot: string }} params
|
|
190
|
+
* @returns {string} the file's contents
|
|
191
|
+
*/
|
|
192
|
+
function readReferencedFile({ from, workspaceRoot }) {
|
|
193
|
+
if (!from || (typeof from.path !== 'string' && !isPackageReference(from))) {
|
|
194
|
+
throw new Error('[ManifestDiscovery] Row or discovery block has no "from" reference - the fact has an '
|
|
195
|
+
+ 'owner and the manifest never copies it. Fix: add from { path, ... } naming the file that owns it, '
|
|
196
|
+
+ 'or from { package, text: true } for a file this package carries.');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (isPackageReference(from)) {
|
|
200
|
+
const packaged = path.join(PACKAGE_ROOT, ...from.package.split('/'));
|
|
201
|
+
try {
|
|
202
|
+
return fs.readFileSync(packaged, 'utf8');
|
|
203
|
+
} catch (cause) {
|
|
204
|
+
throw new Error(`[ManifestDiscovery] Packaged file not found - ${from.package} is missing from `
|
|
205
|
+
+ `${PACKAGE_ROOT}, and this package is what owns it. `
|
|
206
|
+
+ 'Fix: reinstall @onlineapps/conn-orch-validator; the file ships inside it.', { cause });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const file = resolveWorkspacePath(workspaceRoot, from.path);
|
|
211
|
+
try {
|
|
212
|
+
return fs.readFileSync(file, 'utf8');
|
|
213
|
+
} catch (cause) {
|
|
214
|
+
throw new Error(`[ManifestDiscovery] Referenced file not found - ${from.path} under ${workspaceRoot}. `
|
|
215
|
+
+ 'Fix: run with --workspace pointing at the directory that holds api/ and api_biz/.', { cause });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The whole referenced file as one value: `{ path, text: true }`.
|
|
221
|
+
*
|
|
222
|
+
* The shape exists because not every SSOT is a JSON document. `api/.nvmrc`
|
|
223
|
+
* carries the platform Node major as its only line (confirmation
|
|
224
|
+
* `node-runtime-version` 003), and a row that needs that major references the
|
|
225
|
+
* file rather than restating the number.
|
|
226
|
+
*
|
|
227
|
+
* @param {{ from: {path: string, text: boolean}, workspaceRoot: string }} params
|
|
228
|
+
* @returns {string} the file's contents, trimmed
|
|
229
|
+
*/
|
|
230
|
+
function resolveFromValue({ from, workspaceRoot }) {
|
|
231
|
+
if (!from || from.text !== true) {
|
|
232
|
+
throw new Error('[ManifestDiscovery] Reference is not a text reference - resolveFromValue needs '
|
|
233
|
+
+ `from { path, text: true } and got ${JSON.stringify(from)}. `
|
|
234
|
+
+ 'Fix: use resolveFromReference for a list of names.');
|
|
235
|
+
}
|
|
236
|
+
return readReferencedFile({ from, workspaceRoot }).trim();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The node a `from.list` path points at, as the owner file writes it.
|
|
241
|
+
*
|
|
242
|
+
* @param {{ from: {path: string, list: string}, workspaceRoot: string }} params
|
|
243
|
+
* @returns {Array|object} the array or the object map the reference names
|
|
244
|
+
*/
|
|
245
|
+
function resolveFromMap({ from, workspaceRoot }) {
|
|
246
|
+
const raw = readReferencedFile({ from, workspaceRoot });
|
|
247
|
+
|
|
248
|
+
let document;
|
|
249
|
+
try {
|
|
250
|
+
document = JSON.parse(raw);
|
|
251
|
+
} catch (cause) {
|
|
252
|
+
throw new Error(`[ManifestDiscovery] Referenced file is not valid JSON - ${from.path}. `
|
|
253
|
+
+ 'Fix: repair the file; it is the SSOT this row reads.', { cause });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const node = from.list.split('.').reduce((current, key) => (current == null ? undefined : current[key]), document);
|
|
257
|
+
if (node === null || node === undefined || typeof node !== 'object') {
|
|
258
|
+
throw new Error(`[ManifestDiscovery] Referenced list not found - "${from.list}" in ${from.path}. `
|
|
259
|
+
+ 'Fix: point the reference at the array or object that owns the names.');
|
|
260
|
+
}
|
|
261
|
+
return node;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Read the list a `from:` reference points at, and return the names it owns.
|
|
266
|
+
*
|
|
267
|
+
* Two shapes, because the two SSOTs are written differently and neither is going
|
|
268
|
+
* to be rewritten for the manifest's convenience:
|
|
269
|
+
* - `{ path, list, key }` — an array of objects; the name is `entry[key]`
|
|
270
|
+
* (`api/config/services.json`, `businessServices.services[].directory`);
|
|
271
|
+
* - `{ path, list, names: "keys" }` — an object map; the names ARE the keys
|
|
272
|
+
* (`api/config/libraries.json`, `libraries`).
|
|
273
|
+
*
|
|
274
|
+
* @param {{ from: {path: string, list: string, key?: string, names?: string}, workspaceRoot: string }} params
|
|
275
|
+
* @returns {string[]}
|
|
276
|
+
*/
|
|
277
|
+
function resolveFromReference({ from, workspaceRoot }) {
|
|
278
|
+
const list = resolveFromMap({ from, workspaceRoot });
|
|
279
|
+
|
|
280
|
+
if (from.names === 'keys') {
|
|
281
|
+
if (Array.isArray(list)) {
|
|
282
|
+
throw new Error(`[ManifestDiscovery] Referenced list is an array, not a map - "${from.list}" in ${from.path} `
|
|
283
|
+
+ 'was referenced with names: "keys". Fix: use { list, key } for an array of objects.');
|
|
284
|
+
}
|
|
285
|
+
return Object.keys(list);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!Array.isArray(list)) {
|
|
289
|
+
throw new Error(`[ManifestDiscovery] Referenced list is not an array - "${from.list}" in ${from.path}. `
|
|
290
|
+
+ 'Fix: reference an object map with names: "keys", or point at the array that owns the names.');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// An entry that does not carry the key declares no bearer of this uniform, and
|
|
294
|
+
// that is a real shape of the SSOT rather than damage: `services.json` holds
|
|
295
|
+
// `evidence-service` with `enabled: false` and no `directory` (measured
|
|
296
|
+
// 2026-09-09), a service that exists as a plan and not as a repository. The
|
|
297
|
+
// reference reads the set of names the owner file DECLARES; a name it does not
|
|
298
|
+
// declare cannot bind a directory, and a directory nobody declares is exactly
|
|
299
|
+
// what U-ORPHAN reports. A key that IS there but is not a string is damage,
|
|
300
|
+
// and it stops the run rather than being coerced.
|
|
301
|
+
const names = [];
|
|
302
|
+
list.forEach((entry, index) => {
|
|
303
|
+
const value = entry == null ? undefined : entry[from.key];
|
|
304
|
+
if (value === undefined) return;
|
|
305
|
+
if (typeof value !== 'string') {
|
|
306
|
+
throw new Error(`[ManifestDiscovery] Referenced list entry has a non-string "${from.key}" - `
|
|
307
|
+
+ `${from.list}[${index}] in ${from.path} carries ${JSON.stringify(value)}. `
|
|
308
|
+
+ 'Fix: repair the SSOT; the reference reads names, not values of another kind.');
|
|
309
|
+
}
|
|
310
|
+
names.push(value);
|
|
311
|
+
});
|
|
312
|
+
return names;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* @param {{ block: object, workspaceRoot: string }} params the discovery block and where to look
|
|
317
|
+
* @returns {{ bearers: Array<{name: string, relativeDir: string, dir: string, file: string}>,
|
|
318
|
+
* excluded: string[],
|
|
319
|
+
* orphans: Array<{name: string, relativeDir: string, dir: string, reason: string}> }}
|
|
320
|
+
*/
|
|
321
|
+
function discoverBearers({ block, workspaceRoot }) {
|
|
322
|
+
if (!block || typeof block.pattern !== 'string') {
|
|
323
|
+
throw new Error('[ManifestDiscovery] Discovery block needs a "pattern" - discoverBearers({ block }) got none. '
|
|
324
|
+
+ 'Fix: state the pattern that finds the bearers of this uniform.');
|
|
325
|
+
}
|
|
326
|
+
if (typeof workspaceRoot !== 'string' || workspaceRoot.length === 0) {
|
|
327
|
+
throw new Error('[ManifestDiscovery] Workspace root is required - discoverBearers({ workspaceRoot }) got none. '
|
|
328
|
+
+ 'Fix: resolve it first (resolveWorkspaceRoot) and report NOT RUN when it is unreachable.');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const matched = expandPattern(workspaceRoot, block.pattern);
|
|
332
|
+
const excluded = matched.filter((relative) => isExcluded(relative, block.exclude));
|
|
333
|
+
const bearers = matched
|
|
334
|
+
.filter((relative) => !excluded.includes(relative))
|
|
335
|
+
.map((relative) => {
|
|
336
|
+
const relativeDir = path.posix.dirname(relative);
|
|
337
|
+
return {
|
|
338
|
+
name: path.posix.basename(relativeDir),
|
|
339
|
+
relativeDir,
|
|
340
|
+
dir: resolveWorkspacePath(workspaceRoot, relativeDir),
|
|
341
|
+
file: relative
|
|
342
|
+
};
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const orphans = [];
|
|
346
|
+
const container = containerOf(block.pattern);
|
|
347
|
+
if (container && block.from) {
|
|
348
|
+
const known = resolveFromReference({ from: block.from, workspaceRoot });
|
|
349
|
+
for (const entry of listEntries(resolveWorkspacePath(workspaceRoot, container))) {
|
|
350
|
+
if (!entry.isDirectory()) continue;
|
|
351
|
+
const relativeDir = `${container}/${entry.name}`;
|
|
352
|
+
if (isExcluded(relativeDir, block.exclude)) continue;
|
|
353
|
+
|
|
354
|
+
const isBearer = bearers.some((bearer) => bearer.relativeDir === relativeDir);
|
|
355
|
+
const reason = !isBearer
|
|
356
|
+
? `matches no discovery pattern of this uniform (${block.pattern})`
|
|
357
|
+
: (known.includes(entry.name)
|
|
358
|
+
? null
|
|
359
|
+
: `not declared in ${block.from.path} (${block.from.list}[].${block.from.key})`);
|
|
360
|
+
|
|
361
|
+
if (reason) {
|
|
362
|
+
orphans.push({
|
|
363
|
+
name: entry.name, relativeDir, dir: resolveWorkspacePath(workspaceRoot, relativeDir), reason
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return { bearers, excluded, orphans };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
module.exports = {
|
|
373
|
+
PACKAGE_ROOT,
|
|
374
|
+
isPackageReference,
|
|
375
|
+
referenceOwner,
|
|
376
|
+
discoverBearers,
|
|
377
|
+
resolveFromReference,
|
|
378
|
+
resolveFromValue,
|
|
379
|
+
resolveFromMap,
|
|
380
|
+
readReferencedFile,
|
|
381
|
+
expandPattern,
|
|
382
|
+
rootOfPattern,
|
|
383
|
+
containerOf,
|
|
384
|
+
isExcluded,
|
|
385
|
+
globToRegExp
|
|
386
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The packaged uniform manifest, read from disk.
|
|
5
|
+
*
|
|
6
|
+
* The manifest ships INSIDE this package, so the manifest version IS the
|
|
7
|
+
* validator version: a service pins the validator exactly (SSOT
|
|
8
|
+
* `api/config/libraries.json`, gate R6) and thereby pins the shape it is
|
|
9
|
+
* checked against. That is why no version field exists in the file — a field
|
|
10
|
+
* would be a second owner of the same fact.
|
|
11
|
+
*
|
|
12
|
+
* @see api/docs/governance/confirmations/biz-service-manifest.md §2
|
|
13
|
+
* @see api/docs/governance/confirmations/biz-service-manifest.md § Confirmation 20260909-biz-service-manifest-004
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
/** Where the packaged manifest lives. The only default in this module family. */
|
|
20
|
+
const DEFAULT_MANIFEST_PATH = path.join(__dirname, '..', '..', 'manifests', 'biz-service.manifest.json');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Where the library uniform lives. It ships beside the service one, so the same
|
|
24
|
+
* pin decides both shapes (002 §12). Both paths belong to this module for the
|
|
25
|
+
* same reason: the manifest location is a fact about the package, not about the
|
|
26
|
+
* CLI — a boot step reads it without a CLI in sight.
|
|
27
|
+
*/
|
|
28
|
+
const LIBRARY_MANIFEST_PATH = path.join(__dirname, '..', '..', 'manifests', 'library.manifest.json');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read and parse a manifest. Nothing is validated here beyond "it is JSON" —
|
|
32
|
+
* the shape is `manifestShape.verifyManifestShape`, so a caller can inspect the
|
|
33
|
+
* violations instead of catching a throw.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} manifestPath absolute path to the manifest file
|
|
36
|
+
* @returns {object} the parsed manifest
|
|
37
|
+
*/
|
|
38
|
+
function loadManifest(manifestPath) {
|
|
39
|
+
if (typeof manifestPath !== 'string' || manifestPath.length === 0) {
|
|
40
|
+
throw new Error('[Manifest] Manifest path is required - loadManifest(manifestPath) got '
|
|
41
|
+
+ `${JSON.stringify(manifestPath)}. Fix: pass DEFAULT_MANIFEST_PATH or an explicit path.`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let raw;
|
|
45
|
+
try {
|
|
46
|
+
raw = fs.readFileSync(manifestPath, 'utf8');
|
|
47
|
+
} catch (cause) {
|
|
48
|
+
throw new Error(`[Manifest] Manifest file not found - ${manifestPath}. `
|
|
49
|
+
+ 'Fix: reinstall @onlineapps/conn-orch-validator; the manifest ships with the package.',
|
|
50
|
+
{ cause });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(raw);
|
|
55
|
+
} catch (cause) {
|
|
56
|
+
throw new Error(`[Manifest] Manifest is not valid JSON - ${manifestPath}. `
|
|
57
|
+
+ 'Fix: repair the file; it is the machine-readable declaration of the uniform.',
|
|
58
|
+
{ cause });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { loadManifest, DEFAULT_MANIFEST_PATH, LIBRARY_MANIFEST_PATH };
|