@onlineapps/conn-orch-validator 6.0.1 → 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.
Files changed (102) hide show
  1. package/CHANGELOG.md +2591 -2
  2. package/README.md +1075 -7
  3. package/docs/DESIGN.md +3 -1
  4. package/manifests/biz-service.manifest.json +658 -0
  5. package/manifests/library.manifest.json +324 -0
  6. package/package.json +12 -6
  7. package/src/CookbookTestRunner.js +422 -104
  8. package/src/CookbookTestUtils.js +7 -8
  9. package/src/ServiceReadinessValidator.js +78 -42
  10. package/src/ValidationOrchestrator.js +298 -75
  11. package/src/cli/biz-ci-gate.js +176 -33
  12. package/src/cli/oa-lint-scripts.js +221 -0
  13. package/src/cli/oa-sync-template.js +1020 -0
  14. package/src/cli/oa-validate.js +474 -0
  15. package/src/helpers/README.md +12 -2
  16. package/src/helpers/createServiceReadinessTests.js +75 -6
  17. package/src/index.js +33 -3
  18. package/src/lint/scripts/lintScripts.js +298 -0
  19. package/src/manifest/checks/composeRunnerBlock.js +222 -0
  20. package/src/manifest/checks/composeShape.js +165 -0
  21. package/src/manifest/checks/contractBridge.js +181 -0
  22. package/src/manifest/checks/discoveryOrphan.js +50 -0
  23. package/src/manifest/checks/docsLintBridge.js +553 -0
  24. package/src/manifest/checks/fileAbsent.js +35 -0
  25. package/src/manifest/checks/gitTracked.js +204 -0
  26. package/src/manifest/checks/index.js +111 -0
  27. package/src/manifest/checks/libraryContext.js +226 -0
  28. package/src/manifest/checks/libraryDocs.js +75 -0
  29. package/src/manifest/checks/libraryPackage.js +272 -0
  30. package/src/manifest/checks/librarySource.js +274 -0
  31. package/src/manifest/checks/libraryTests.js +121 -0
  32. package/src/manifest/checks/libraryWorkspace.js +293 -0
  33. package/src/manifest/checks/readmeRegion.js +135 -0
  34. package/src/manifest/checks/scriptHeaders.js +79 -0
  35. package/src/manifest/checks/serviceConfig.js +390 -0
  36. package/src/manifest/checks/serviceConnectors.js +81 -0
  37. package/src/manifest/checks/serviceDb.js +388 -0
  38. package/src/manifest/checks/serviceFiles.js +754 -0
  39. package/src/manifest/checks/serviceIdentityRows.js +351 -0
  40. package/src/manifest/checks/serviceRuntime.js +295 -0
  41. package/src/manifest/checks/serviceScripts.js +213 -0
  42. package/src/manifest/deployabilitySignal.js +121 -0
  43. package/src/manifest/discovery.js +386 -0
  44. package/src/manifest/loadManifest.js +62 -0
  45. package/src/manifest/manifestShape.js +446 -0
  46. package/src/manifest/report.js +245 -0
  47. package/src/manifest/runManifest.js +449 -0
  48. package/src/manifest/serviceIdentity.js +140 -0
  49. package/src/manifest/walk.js +74 -0
  50. package/src/manifest/workspaceRoot.js +242 -0
  51. package/src/mocks/MockMQClient.js +13 -30
  52. package/src/mocks/MockRegistry.js +4 -2
  53. package/src/mocks/MockStorage.js +4 -2
  54. package/src/sync/docsRegion.js +463 -0
  55. package/src/sync/generatedRegion.js +228 -0
  56. package/src/sync/readmeLocation.js +182 -0
  57. package/src/sync/readmePointer.js +477 -0
  58. package/src/sync/serviceTemplate.js +583 -0
  59. package/src/sync/sharedEnv.js +162 -0
  60. package/src/sync/uniformFiles.js +474 -0
  61. package/src/utils/bizCiGateContract.js +131 -7
  62. package/src/utils/connectorContract.js +97 -7
  63. package/src/utils/cookbookFormat.js +81 -40
  64. package/src/utils/deployContract.js +213 -13
  65. package/src/utils/envContract.js +57 -1
  66. package/src/utils/handlerRef.js +181 -0
  67. package/src/utils/installContract.js +287 -41
  68. package/src/utils/libCompat.js +29 -7
  69. package/src/utils/migrationOrder.js +163 -0
  70. package/src/utils/preValidation.js +20 -7
  71. package/src/utils/setupDatabase.js +194 -13
  72. package/src/utils/testCoverageContract.js +539 -0
  73. package/src/utils/testNamespace.js +247 -23
  74. package/src/utils/throwawaySchema.js +207 -0
  75. package/src/validators/ServiceStructureValidator.js +21 -20
  76. package/templates/business-service/.dockerignore +42 -0
  77. package/templates/business-service/.gitlab-ci.yml +290 -0
  78. package/templates/business-service/Dockerfile +27 -0
  79. package/templates/business-service/README.md +213 -0
  80. package/templates/business-service/config/biz-docs-lint.tree.json +10 -0
  81. package/templates/business-service/config/env-templates/__SERVICE_NAME__.env +4 -0
  82. package/templates/business-service/config/env-templates/shared.env +65 -0
  83. package/templates/business-service/config/service/config.json +14 -0
  84. package/templates/business-service/config/service/integration-contract.json +12 -0
  85. package/templates/business-service/config/service/operations.json +41 -0
  86. package/templates/business-service/docker-compose.production.yml +60 -0
  87. package/templates/business-service/docker-compose.yml +93 -0
  88. package/templates/business-service/docs/80-setup/INSTALL.md +101 -0
  89. package/templates/business-service/docs/80-setup/PLATFORM_MATRIX.md +65 -0
  90. package/templates/business-service/docs/80-setup/README.md +18 -0
  91. package/templates/business-service/docs/80-setup/VALIDATION.md +78 -0
  92. package/templates/business-service/docs/README.md +18 -0
  93. package/templates/business-service/gitignore +42 -0
  94. package/templates/business-service/index.js +10 -0
  95. package/templates/business-service/init.sh +54 -0
  96. package/templates/business-service/jest.config.js +6 -0
  97. package/templates/business-service/package.json.template +31 -0
  98. package/templates/business-service/scripts/verify-deploy-uniform.sh +180 -0
  99. package/templates/business-service/src/handlers/v3/echo.js +39 -0
  100. package/templates/business-service/tests/cookbooks/echo.json +36 -0
  101. package/templates/business-service/tests/unit/handler.test.js +78 -0
  102. 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>/*&#47;<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 };