@onlineapps/conn-orch-validator 12.2.0 → 13.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 (56) hide show
  1. package/CHANGELOG.md +597 -0
  2. package/README.md +126 -19
  3. package/manifests/biz-service.manifest.json +15 -2
  4. package/manifests/library.manifest.json +4 -4
  5. package/package.json +11 -3
  6. package/src/CookbookTestRunner.js +275 -105
  7. package/src/CookbookTestUtils.js +79 -68
  8. package/src/ServiceReadinessValidator.js +42 -52
  9. package/src/ValidationOrchestrator.js +65 -44
  10. package/src/cli/biz-ci-gate.js +2 -2
  11. package/src/cli/oa-sync-template.js +97 -47
  12. package/src/cli/oa-validate.js +44 -10
  13. package/src/helpers/README.md +6 -6
  14. package/src/helpers/createServiceReadinessTests.js +87 -33
  15. package/src/index.js +14 -5
  16. package/src/lint/scripts/lintScripts.js +11 -4
  17. package/src/manifest/checks/libraryContext.js +6 -3
  18. package/src/manifest/checks/libraryDocs.js +174 -4
  19. package/src/manifest/checks/libraryTests.js +200 -19
  20. package/src/manifest/checks/scriptHeaders.js +6 -13
  21. package/src/manifest/checks/serviceConfig.js +36 -16
  22. package/src/manifest/checks/serviceConnectors.js +180 -2
  23. package/src/manifest/checks/serviceDb.js +0 -3
  24. package/src/manifest/checks/serviceScripts.js +3 -20
  25. package/src/manifest/runManifest.js +90 -13
  26. package/src/manifest/workspaceRoot.js +133 -4
  27. package/src/mocks/MockMQClient.js +2 -2
  28. package/src/sync/docsRegion.js +2 -2
  29. package/src/sync/readmeFile.js +30 -0
  30. package/src/sync/readmeLocation.js +2 -12
  31. package/src/sync/readmePointer.js +10 -4
  32. package/src/sync/serviceTemplate.js +9 -11
  33. package/src/sync/sharedEnv.js +59 -3
  34. package/src/sync/uniformFiles.js +81 -8
  35. package/src/utils/bizCiGateContract.js +2 -2
  36. package/src/utils/connectorContract.js +54 -2
  37. package/src/utils/cookbookFormat.js +25 -115
  38. package/src/utils/dbAccountGrants.js +5 -3
  39. package/src/utils/deployContract.js +153 -28
  40. package/src/utils/envContract.js +2 -2
  41. package/src/utils/handlerRef.js +8 -10
  42. package/src/utils/integrationRun.js +1 -1
  43. package/src/utils/operationsDocumentRules.js +242 -0
  44. package/src/utils/operationsRules.js +157 -0
  45. package/src/utils/resolveHeaders.js +12 -1
  46. package/src/utils/setupDatabase.js +1 -1
  47. package/src/utils/stepFailure.js +3 -3
  48. package/src/utils/stepReferences.js +28 -87
  49. package/src/utils/throwawaySchema.js +1 -1
  50. package/src/utils/yamlTopLevel.js +105 -0
  51. package/src/validators/ServiceStructureValidator.js +67 -152
  52. package/templates/business-service/README.md +3 -2
  53. package/templates/business-service/config/env-templates/shared.env +1 -0
  54. package/templates/business-service/src/config/index.js +15 -0
  55. package/TESTING_STRATEGY.md +0 -92
  56. package/jest.config.js +0 -37
@@ -8,20 +8,36 @@
8
8
  *
9
9
  * Four runs, one template:
10
10
  *
11
- * * bare paths - the uniform sync of confirmation 001 §3.2. It writes the
12
- * files of the manifest classes `identical` and `generated` from the very
13
- * `from:` reference those rows already point the conformance CHECK at, and
14
- * never touches a file of the `own` class. A row that names no `from:`
15
- * reference is not a row of this run — it is a requirement about the
16
- * repository, and `npx oa-validate` is what answers it (d.507).
11
+ * * bare paths - the uniform sync of confirmation 001 §3.2, and since d.710
12
+ * the WHOLE of it: over a service root with no path named it writes every
13
+ * row the manifest declares in a syncable class - `identical`, `generated`,
14
+ * `contains` - from the very `from:` reference those rows already point the
15
+ * conformance CHECK at, and never touches a file of the `own` class. That
16
+ * is one run for "bring this service into line with everything oa-validate
17
+ * measures as generated", `config/env-templates/shared.env` included. A row
18
+ * that names no `from:` reference is not a row of this run — it is a
19
+ * requirement about the repository, and `npx oa-validate` is what answers
20
+ * it (d.507).
17
21
  * * `--new <name>` - the whole service tree, from the template that ships
18
22
  * inside this package (001 §2). It is what `api/scripts/add-service.sh
19
23
  * --scaffold` runs; that script keeps the platform facts (the registry
20
24
  * entry, the port), because those are not the shape of a service.
21
25
  * * `shared-env` - `config/env-templates/shared.env` from
22
26
  * `api/config/shared-env.json`, the one owner of the shared key set (003 §18).
27
+ * It stays, because its bearer is not always a service: `api/` itself and
28
+ * the business-service template carry that file too, and neither is a
29
+ * service root the bare run could be pointed at. Both of those paths are
30
+ * READ OFF the manifest row (`sharedEnvRow()` below) rather than written
31
+ * here, so this command and the row cannot disagree about one file.
23
32
  * * `readme-uniform` - the generated uniform pointer of a README, for a
24
- * library or for a biz service; the kind is read from the target (005).
33
+ * library or for a biz service; the kind is read from the target (005). It
34
+ * stays for the LIBRARY, which wears no service uniform; for a service the
35
+ * bare run covers the same region through row `G-README`.
36
+ *
37
+ * `template` and `docs-region` are deliberately NOT part of the bare run: their
38
+ * bearer is not a service. One renders `api/templates/business-service`, the
39
+ * other a region of the `api/docs/**` tree — two directories a service
40
+ * repository does not have and does not own.
25
41
  *
26
42
  * There is no flag that points the run at another manifest: the shape has one
27
43
  * owner beside the SSOT (004), and a `--manifest` flag would be a second one
@@ -57,8 +73,8 @@ const {
57
73
  declaredCategory,
58
74
  KINDS
59
75
  } = require('../sync/readmePointer');
76
+ const { README_FILE } = require('../sync/readmeFile');
60
77
  const {
61
- README_FILE,
62
78
  PACKAGE_ROOT,
63
79
  SERVICE_MANIFEST_IN_PACKAGE,
64
80
  LIBRARY_MANIFEST_IN_PACKAGE,
@@ -67,27 +83,30 @@ const {
67
83
  libraryRegion
68
84
  } = require('../sync/readmeLocation');
69
85
  const docsRegion = require('../sync/docsRegion');
70
- const { resolveWorkspaceRoot, canonicalRoot, WORKSPACE_MARKER } = require('../manifest/workspaceRoot');
86
+ const {
87
+ resolveWorkspaceRoot, resolveWorkspacePath, canonicalRoot, WORKSPACE_MARKER
88
+ } = require('../manifest/workspaceRoot');
71
89
  const { loadManifest: loadLibraryManifest, LIBRARY_MANIFEST_PATH } = require('../manifest/loadManifest');
72
- const { discoverBearers } = require('../manifest/discovery');
90
+ const { discoverBearers, resolveFromDocument } = require('../manifest/discovery');
73
91
 
74
92
  /** Exit code for a run that could not start at all — distinct from a finding. */
75
93
  const USAGE_EXIT = 2;
76
94
 
77
95
  /**
78
- * The manifest lives beside the SSOT the workspace marker names, so "where is
79
- * the workspace" is asked once and answered in one place (004: platform-level
80
- * facts live in `api/config/*.json` beside the SSOT).
96
+ * The row that owns `shared.env`, found by the CHECK that decides it.
97
+ *
98
+ * By the check and not by the id, because that is already this package's
99
+ * vocabulary for "which rule renders this row" (`../sync/uniformFiles.js` §
100
+ * `RENDERERS` is keyed the same way). A list of ids in a runner is the thing
101
+ * that module refuses to keep: a row is renderable because its rule says how,
102
+ * never because a caller recognised its name.
81
103
  */
82
- const MANIFEST_RELATIVE = path.join(path.dirname(WORKSPACE_MARKER), 'shared-env.json');
83
-
84
- /** Where a bearer keeps the file this subcommand generates. */
85
- const SHARED_ENV_RELATIVE = path.join('config', 'env-templates', 'shared.env');
86
-
87
- /** The same file as a tree key, which is always '/'-separated. */
88
- const SHARED_ENV_POSIX = 'config/env-templates/shared.env';
104
+ const SHARED_ENV_CHECK = 'shared-env-generated';
89
105
 
90
- /** The platform SSOT of library versions, relative to the workspace root. */
106
+ /**
107
+ * The platform SSOT of library versions, in the same convention as the manifest
108
+ * above and resolved the same way.
109
+ */
91
110
  const LIBRARIES_RELATIVE = path.join(path.dirname(WORKSPACE_MARKER), 'libraries.json');
92
111
 
93
112
  /** The scope prefix whose versions the SSOT owns. */
@@ -108,21 +127,31 @@ Usage:
108
127
  is: generated output, committed for reading (001 §5). --check is the gate.
109
128
 
110
129
  With bare paths (or none, meaning all of them) the run rewrites the files the
111
- manifest declares in the classes "identical" and "generated" that name a
112
- "from" reference, from that same reference the conformance check reads. Files
113
- of the "own" class - src/**, tests/**, the content of docs/** - are never
114
- written. A row naming no reference is a requirement about the repository
130
+ manifest declares in the classes "identical", "generated" and "contains" that
131
+ name a "from" reference, from that same reference the conformance check reads.
132
+ Files of the "own" class - src/**, tests/**, the content of docs/** - are
133
+ never written. A row naming no reference is a requirement about the repository
115
134
  rather than a file, and npx oa-validate <serviceRoot> is what answers it.
116
135
 
136
+ WHAT ONE BARE RUN COVERS: every generated row of the SERVICE uniform, which
137
+ since d.710 includes config/env-templates/shared.env and the README region -
138
+ so "sync everything that is generated" is npx oa-sync-template --target .,
139
+ one command, not a sequence each repository assembles for itself. WHAT IT
140
+ DOES NOT: template and docs-region, whose bearer is not a service -
141
+ api/templates/business-service and the api/docs/** tree have their own owners.
142
+
117
143
  --new <name> create a service tree from the packaged template
118
144
  --into <dir> where that tree is written; it must not exist yet
119
145
  --description <s> what the service is for; without it the template's
120
146
  __SERVICE_DESCRIPTION__ is LEFT standing and the files
121
147
  carrying it are listed
122
148
 
123
- Writes <dir>/config/env-templates/shared.env from the platform env manifest
124
- api/config/shared-env.json — the one owner of the shared key set. A service
125
- adds keys of its own in service.env, never here.
149
+ shared-env writes <dir>/config/env-templates/shared.env from the platform env
150
+ manifest api/config/shared-env.json — the one owner of the shared key set. A
151
+ service adds keys of its own in service.env, never here. For a SERVICE the
152
+ bare run writes the same file from the same manifest (row G-SHARED-ENV); this
153
+ subcommand is how the other two bearers get it — api/ itself and the
154
+ business-service template, neither of which is a service root.
126
155
 
127
156
  --target <dir> the bearer: a service root, api/ itself, or the business
128
157
  service template
@@ -131,7 +160,7 @@ Usage:
131
160
  --check write nothing; exit 1 with a diff when the file on disk is
132
161
  not what the manifest renders
133
162
 
134
- readme-uniform writes the generated region of a repository's README.md from
163
+ readme-uniform writes the generated region of a repository's ${README_FILE} from
135
164
  the manifest that ships with this package. --target reads the kind from the
136
165
  disk: a root carrying config/service/operations.json is a SERVICE and gets
137
166
  "Uniform: [biz-service](<link>)", the sections of the biz-service manifest and
@@ -257,18 +286,30 @@ function parseArgs(argv) {
257
286
  return options;
258
287
  }
259
288
 
260
- function loadManifest(workspaceRoot) {
261
- const manifestPath = path.join(workspaceRoot, MANIFEST_RELATIVE);
262
- if (!fs.existsSync(manifestPath)) {
263
- throw new Error(`[oa-sync-template] Missing platform env manifest - ${manifestPath} does not exist. `
264
- + 'Fix: the shared key set is declared there and nowhere else.');
265
- }
266
-
267
- try {
268
- return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
269
- } catch (error) {
270
- throw new Error(`[oa-sync-template] Invalid platform env manifest - ${manifestPath}: ${error.message}`);
289
+ /**
290
+ * The manifest row that owns `shared.env`: where a bearer keeps the file
291
+ * (`row.path`) and which file owns its content (`row.from`).
292
+ *
293
+ * BOTH facts come from the row, and that is the whole point of this function.
294
+ * Until d.710c this command carried its own pair — a `MANIFEST_RELATIVE` built
295
+ * from the workspace marker and a `SHARED_ENV_RELATIVE` literal — beside the
296
+ * row that says the same two things to `oa-validate` and, since d.710, to the
297
+ * bare run. One file, two declarations of where it lives: the day the row's
298
+ * `from.path` moves, the subcommand keeps reading the old place and the two
299
+ * halves of one uniform disagree in silence
300
+ * (`.claude/rules/change-discipline.md` § One rail per concern).
301
+ *
302
+ * @returns {object} the row
303
+ */
304
+ function sharedEnvRow() {
305
+ const manifest = loadUniformManifest(DEFAULT_MANIFEST_PATH);
306
+ const row = uniformRows(manifest).find((candidate) => candidate.check === SHARED_ENV_CHECK);
307
+ if (row === undefined) {
308
+ throw new Error(`[oa-sync-template] No manifest row is decided by "${SHARED_ENV_CHECK}" - this run writes `
309
+ + 'the file that row declares, and knows no path of its own. '
310
+ + `Fix: restore the row in ${DEFAULT_MANIFEST_PATH}.`);
271
311
  }
312
+ return row;
272
313
  }
273
314
 
274
315
  /**
@@ -287,8 +328,9 @@ function runSharedEnv(options) {
287
328
  + 'Fix: pass --workspace <root> pointing at the directory that holds api/ and api_biz/.');
288
329
  }
289
330
 
290
- const manifest = loadManifest(workspaceRoot);
291
- const file = path.join(target, SHARED_ENV_RELATIVE);
331
+ const row = sharedEnvRow();
332
+ const manifest = resolveFromDocument({ from: row.from, workspaceRoot });
333
+ const file = path.join(target, ...row.path.split('/'));
292
334
 
293
335
  if (options.check) {
294
336
  if (!fs.existsSync(file)) {
@@ -765,7 +807,14 @@ function runSync(options) {
765
807
  let blocked = 0;
766
808
 
767
809
  for (const entry of entries) {
768
- const where = `${entry.id} ${entry.path}`;
810
+ // WHAT THE LINE IS ABOUT, not merely which file it touched. A block row
811
+ // compares and writes the markers alone, so a line naming the path only
812
+ // reads as a claim about a file whose other half was never looked at —
813
+ // `.claude/rules/automation-gates.md` §5. `planRow` decides the scope, by
814
+ // the manifest row's own `block`; this loop only prints it.
815
+ const where = entry.block === undefined
816
+ ? `${entry.id} ${entry.path}`
817
+ : `${entry.id} ${entry.path} (block "${entry.block}")`;
769
818
  if (entry.outcome === 'not-run') {
770
819
  process.stdout.write(`[oa-sync-template] NOT RUN ${where} - ${entry.reason}\n`);
771
820
  } else if (entry.outcome === 'blocked') {
@@ -797,7 +846,7 @@ function runSync(options) {
797
846
  * @returns {object} the parsed `libraries` map
798
847
  */
799
848
  function loadLibraries(workspaceRoot) {
800
- const file = path.join(workspaceRoot, LIBRARIES_RELATIVE);
849
+ const file = resolveWorkspacePath(workspaceRoot, LIBRARIES_RELATIVE);
801
850
  if (!fs.existsSync(file)) {
802
851
  throw new Error(`[oa-sync-template] Missing library SSOT - ${file} does not exist. `
803
852
  + 'Fix: a new service pins every @onlineapps dependency to the platform SSOT, which is that file.');
@@ -899,7 +948,8 @@ function runNew(options) {
899
948
  }
900
949
 
901
950
  const workspaceRoot = requireWorkspaceRoot({ explicit: options.workspace, startDir: path.dirname(into) });
902
- const envManifest = loadManifest(workspaceRoot);
951
+ const sharedEnv = sharedEnvRow();
952
+ const envManifest = resolveFromDocument({ from: sharedEnv.from, workspaceRoot });
903
953
  const libraries = loadLibraries(workspaceRoot);
904
954
 
905
955
  const tree = renderTree({ params });
@@ -908,7 +958,7 @@ function runNew(options) {
908
958
  tree.delete(ENV_TEMPLATE_SOURCE);
909
959
  tree.set(envTemplateTarget(params.service_name), envTemplate);
910
960
 
911
- tree.set(SHARED_ENV_POSIX, renderSharedEnv(envManifest));
961
+ tree.set(sharedEnv.path, renderSharedEnv(envManifest));
912
962
  tree.set('package.json', pinLibraries(tree.get('package.json'), libraries));
913
963
 
914
964
  for (const [relative, text] of tree) writeServiceFile(into, relative, text);
@@ -1032,4 +1082,4 @@ if (require.main === module) {
1032
1082
  process.exitCode = main(process.argv.slice(2));
1033
1083
  }
1034
1084
 
1035
- module.exports = { main, parseArgs, USAGE_EXIT, MANIFEST_RELATIVE, SHARED_ENV_RELATIVE, README_FILE };
1085
+ module.exports = { main, parseArgs, USAGE_EXIT };
@@ -30,9 +30,9 @@ const fs = require('fs');
30
30
  const path = require('path');
31
31
 
32
32
  const { loadManifest, DEFAULT_MANIFEST_PATH, LIBRARY_MANIFEST_PATH } = require('../manifest/loadManifest');
33
- const { runManifest } = require('../manifest/runManifest');
33
+ const { runManifest, isTargetVersion } = require('../manifest/runManifest');
34
34
  const {
35
- resolveWorkspaceRoot, canonicalRoot, API_MARKER, PACKAGE_ROOT
35
+ resolveWorkspaceRoot, requirePackageInApiCheckout, canonicalRoot, API_MARKER, PACKAGE_ROOT
36
36
  } = require('../manifest/workspaceRoot');
37
37
  const {
38
38
  renderReport, renderJson, exitCodeFor, blockingOf, verdictOf, describeClearOutcome
@@ -83,7 +83,7 @@ const USAGE = `
83
83
  Usage:
84
84
  oa-validate [serviceRoot] [--workspace <root>] [--json]
85
85
  oa-validate --workspace <root> [--json]
86
- oa-validate --library <packageDir> [--workspace <root>] [--json]
86
+ oa-validate --library <packageDir> [--workspace <root>] [--as-version <X.Y.Z>] [--json]
87
87
  oa-validate --library --all [--workspace <root>] [--json]
88
88
  oa-validate --env-reads [<serviceRoot>]
89
89
 
@@ -121,6 +121,11 @@ Options:
121
121
  No such checkout (an installed copy, a service container)
122
122
  → the rows that need it are reported NOT RUN, never as
123
123
  passing.
124
+ --as-version <X.Y.Z> With --library <packageDir>: judge the package AS the version
125
+ being published, not the one package.json still carries —
126
+ what a publish dry run asks before the wave bumps the file.
127
+ The rows that judge a version (L-CHANGELOG) read it; the
128
+ finding names it and the package.json version beside it.
124
129
  --json Print the run as JSON instead of the table.
125
130
  --env-reads Print the environment names this service reads — one per
126
131
  line, sorted, nothing else — and exit 0. No table, no
@@ -148,7 +153,8 @@ Exit codes:
148
153
 
149
154
  function parseArgs(argv) {
150
155
  const parsed = {
151
- serviceRoot: null, workspace: null, json: false, help: false, library: false, all: false, envReads: false
156
+ serviceRoot: null, workspace: null, json: false, help: false, library: false, all: false, envReads: false,
157
+ asVersion: null
152
158
  };
153
159
  const args = [...argv];
154
160
 
@@ -171,6 +177,17 @@ function parseArgs(argv) {
171
177
  + 'api/ and api_biz/. Fix: oa-validate [serviceRoot] --workspace <root>');
172
178
  }
173
179
  parsed.workspace = value;
180
+ } else if (token === '--as-version') {
181
+ const value = args.shift();
182
+ if (!value || value.startsWith('--')) {
183
+ throw new Error('[oa-validate] Option --as-version has no value - it names the version being '
184
+ + 'published. Fix: oa-validate --library <packageDir> --as-version <X.Y.Z>');
185
+ }
186
+ if (!isTargetVersion(value)) {
187
+ throw new Error(`[oa-validate] Option --as-version is not a version - ${JSON.stringify(value)} is not `
188
+ + 'X.Y.Z or X.Y.Z-<prerelease>. Fix: pass the bare version, e.g. --as-version 1.2.4');
189
+ }
190
+ parsed.asVersion = value;
174
191
  } else if (token.startsWith('--')) {
175
192
  throw new Error(`[oa-validate] Unknown option - ${token}. Fix: run oa-validate --help for the options.`);
176
193
  } else if (parsed.serviceRoot === null) {
@@ -192,7 +209,8 @@ function parseArgs(argv) {
192
209
  parsed.json ? '--json' : null,
193
210
  parsed.library ? '--library' : null,
194
211
  parsed.all ? '--all' : null,
195
- parsed.workspace !== null ? '--workspace' : null
212
+ parsed.workspace !== null ? '--workspace' : null,
213
+ parsed.asVersion !== null ? '--as-version' : null
196
214
  ].filter((token) => token !== null);
197
215
 
198
216
  if (combined.length > 0) {
@@ -202,6 +220,17 @@ function parseArgs(argv) {
202
220
  }
203
221
  }
204
222
 
223
+ // A version belongs to ONE package, and only the library uniform judges one: an
224
+ // option no row of the run reads would declare nothing (`automation-gates.md` §1).
225
+ if (parsed.asVersion !== null && !parsed.library && !parsed.envReads) {
226
+ throw new Error('[oa-validate] --as-version needs --library - only the library uniform judges the version '
227
+ + 'being published. Fix: oa-validate --library <packageDir> --as-version <X.Y.Z>');
228
+ }
229
+ if (parsed.asVersion !== null && parsed.all) {
230
+ throw new Error('[oa-validate] --as-version judges ONE package - --all measures every package, and each has '
231
+ + 'its own version. Fix: oa-validate --library <packageDir> --as-version <X.Y.Z>');
232
+ }
233
+
205
234
  if (parsed.all && !parsed.library) {
206
235
  throw new Error('[oa-validate] Option --all needs --library - a service is checked one repository at a time. '
207
236
  + 'Fix: oa-validate --library --all');
@@ -247,11 +276,12 @@ function packageRelativeRowIds(manifest) {
247
276
  * against this package alone, and a `workspace` row's answer is filtered down to
248
277
  * what lies under this package's root. Nothing is filtered here.
249
278
  *
250
- * @param {{ manifest: object, packageRoot: string, workspaceRoot: string|null }} params
279
+ * @param {{ manifest: object, packageRoot: string, workspaceRoot: string|null,
280
+ * asVersion: string|null }} params — `asVersion` is `--as-version`, null for an audit
251
281
  * @returns {{ findings: Array<object>, notRun: Array<object> }}
252
282
  */
253
- function checkOnePackage({ manifest, packageRoot, workspaceRoot }) {
254
- const result = runManifest({ manifest, serviceRoot: packageRoot, workspaceRoot });
283
+ function checkOnePackage({ manifest, packageRoot, workspaceRoot, asVersion }) {
284
+ const result = runManifest({ manifest, serviceRoot: packageRoot, workspaceRoot, asVersion });
255
285
  const category = declaredCategory(readPackage(packageRoot).json) || NO_CATEGORY;
256
286
 
257
287
  return {
@@ -299,7 +329,7 @@ function checkEveryPackage({ manifest, workspaceRoot }) {
299
329
  };
300
330
 
301
331
  for (const bearer of bearers) {
302
- const one = checkOnePackage({ manifest, packageRoot: bearer.dir, workspaceRoot });
332
+ const one = checkOnePackage({ manifest, packageRoot: bearer.dir, workspaceRoot, asVersion: null });
303
333
  for (const finding of one.findings) {
304
334
  const where = packageRelative.has(finding.id)
305
335
  ? `${bearer.relativeDir}/${finding.where}`
@@ -424,7 +454,11 @@ function runLibrary(options) {
424
454
  const packageRoot = canonicalRoot(given);
425
455
 
426
456
  const workspaceRoot = resolveWorkspaceRoot({ explicit: options.workspace });
427
- const run = checkOnePackage({ manifest, packageRoot, workspaceRoot });
457
+ // A package outside the checkout this run speaks for is judged against the
458
+ // wrong tree; with no workspace there is no such checkout, and the run says
459
+ // NOT RESOLVED instead (d.940, `workspaceRoot.js` § requirePackageInApiCheckout).
460
+ if (workspaceRoot !== null) requirePackageInApiCheckout(workspaceRoot, packageRoot);
461
+ const run = checkOnePackage({ manifest, packageRoot, workspaceRoot, asVersion: options.asVersion });
428
462
  const report = {
429
463
  uniform: manifest.uniform,
430
464
  blockingSeverities: manifest.blocking_severities,
@@ -52,7 +52,7 @@ createServiceReadinessTests(__dirname, {
52
52
 
53
53
  `logger` defaults to `console` on purpose — in a bootstrap suite stdout is the
54
54
  report the developer reads. See
55
- [FALLBACKS_INVENTORY.md](/docs/standards/FALLBACKS_INVENTORY.md) §5.6.
55
+ `api/docs/standards/FALLBACKS_INVENTORY.md` §5.6.
56
56
 
57
57
  **Score Breakdown** (owned by `ServiceReadinessValidator`):
58
58
  - operations: 80 points (required)
@@ -104,7 +104,7 @@ If validation fails, clear error messages are shown:
104
104
  ✗ Service configuration missing: config/service/config.json
105
105
  Type: MISSING_CONFIG
106
106
  File: config/service/config.json
107
- Fix: Create config.json with service metadata. See: /docs/biz/60-templates/service-template.md
107
+ Fix: Create config.json with service metadata. See: api/docs/biz/60-templates/service-template.md
108
108
 
109
109
  ⚠️ WARNINGS (1):
110
110
 
@@ -193,7 +193,7 @@ module.exports = { createMyTests };
193
193
 
194
194
  ## Related Documentation
195
195
 
196
- - [/docs/standards/TESTING.md](/docs/standards/TESTING.md) - Testing standards
197
- - [/tests/TESTING.md](/tests/TESTING.md) - SPOT principles
198
- - [/shared/connector/conn-orch-validator/README.md](/shared/connector/conn-orch-validator/README.md) - Package documentation
199
- - [/shared/connector/conn-orch-validator/docs/DESIGN.md](/shared/connector/conn-orch-validator/docs/DESIGN.md) - Design principles
196
+ - `api/docs/standards/TESTING.md` - Testing standards
197
+ - `api/tests/TESTING.md` - SPOT principles
198
+ - [the package README](../../README.md) - Package documentation
199
+ - [DESIGN.md](../../docs/DESIGN.md) - Design principles
@@ -19,9 +19,9 @@
19
19
  * createServiceReadinessTests(__dirname); // Pass tests/bootstrap directory
20
20
  *
21
21
  * Related:
22
- * - /docs/biz/80-decisions/0005-no-http-in-biz-containers.md
23
- * - /docs/biz/00-model/service-shape.md (zero-HTTP canonical shape)
24
- * - /shared/connector/conn-orch-validator/README.md - Package documentation
22
+ * - api/docs/biz/80-decisions/0005-no-http-in-biz-containers.md
23
+ * - api/docs/biz/00-model/service-shape.md (zero-HTTP canonical shape)
24
+ * - api/shared/connector/conn-orch-validator/README.md - Package documentation
25
25
  */
26
26
 
27
27
  'use strict';
@@ -32,7 +32,9 @@ const ServiceReadinessValidator = require('../ServiceReadinessValidator');
32
32
  const MockRegistry = require('../mocks/MockRegistry');
33
33
  const { ServiceStructureValidator } = require('../validators/ServiceStructureValidator');
34
34
  const { MIN_COOKBOOK_FORMAT_VERSION } = require('../utils/cookbookFormat');
35
- const { HANDLER_REF_PATTERN, parseHandlerRef, resolveHandlerModule } = require('../utils/handlerRef');
35
+ const { parseHandlerRef, resolveHandlerModule } = require('../utils/handlerRef');
36
+ const { operationsRuleSentences } = require('../utils/operationsRules');
37
+ const { operationsDocumentFindings } = require('../utils/operationsDocumentRules');
36
38
 
37
39
  /**
38
40
  * Resolve every handler an `operations.json` declares, the way the runtime does.
@@ -75,13 +77,61 @@ function resolveDeclaredHandlers(serviceRoot, operationsFlat) {
75
77
  return resolved;
76
78
  }
77
79
 
80
+ /**
81
+ * The cookbook the readiness check validates: one `task` step per operation.
82
+ *
83
+ * It must be a cookbook `@onlineapps/cookbook-core` `validateCookbook` accepts,
84
+ * because that is what `CookbookTestUtils.validateCookbook` runs first (d.984);
85
+ * otherwise this helper rejects its own output. The format's `step_id` pattern
86
+ * is `^[a-zA-Z0-9_]+$` (api/docs/biz/40-cookbooks/format.md § Step definition),
87
+ * and operation names carry dashes, so every other character becomes `_`.
88
+ * Two operations that land on one `step_id` are refused by name — a step id
89
+ * addresses one step, and silently keeping either would test half the catalogue.
90
+ *
91
+ * @param {string} serviceName - the service the steps dispatch to
92
+ * @param {Object} operationsFlat - the flat `{ name: spec }` map of operations.json
93
+ * @returns {{version: string, steps: Object[]}} the synthesised cookbook
94
+ * @throws {Error} when two operation names map to the same step_id
95
+ */
96
+ function buildReadinessCookbook(serviceName, operationsFlat) {
97
+ const operationByStepId = new Map();
98
+ const steps = Object.entries(operationsFlat).map(([name, op]) => {
99
+ const stepId = `test_${name.replace(/[^a-zA-Z0-9_]/g, '_')}`;
100
+ if (operationByStepId.has(stepId)) {
101
+ throw new Error(
102
+ `[createServiceReadinessTests] Operations "${operationByStepId.get(stepId)}" and "${name}" map to `
103
+ + `the same step_id "${stepId}" - Expected: names that differ after replacing non-[a-zA-Z0-9_] `
104
+ + 'with "_". Fix: rename one operation in operations.json.'
105
+ );
106
+ }
107
+ operationByStepId.set(stepId, name);
108
+ return {
109
+ step_id: stepId,
110
+ type: 'task',
111
+ service: serviceName,
112
+ operation: name,
113
+ input: generateMockInput(op.input),
114
+ output: {}
115
+ };
116
+ });
117
+
118
+ return {
119
+ // Cookbook FORMAT version (api/docs/biz/40-cookbooks/format.md § Required fields).
120
+ version: MIN_COOKBOOK_FORMAT_VERSION,
121
+ steps
122
+ };
123
+ }
124
+
78
125
  /**
79
126
  * Create service readiness integration test suite
80
127
  *
81
128
  * @param {string} testsDir - Path to tests/bootstrap directory (use __dirname)
82
129
  * @param {Object} [options] - Optional configuration
83
130
  * @param {boolean} [options.includeOptionalChecks=true] - Include cookbook & registry checks
84
- * @param {number} [options.timeout=15000] - Test timeout in ms
131
+ * @param {number} [options.timeout=15000] - The budget, in milliseconds, that
132
+ * EVERY test this helper registers runs under. A positive whole number;
133
+ * anything else is refused when the option is read. Omit it to take 15000 —
134
+ * the helper's own default, never jest's.
85
135
  * @param {Object} [options.logger=console] - Logger handed to
86
136
  * ServiceReadinessValidator (info/warn/error/debug). Defaults to `console`,
87
137
  * which is the report channel of a jest bootstrap suite — see the comment at
@@ -102,6 +152,20 @@ function createServiceReadinessTests(testsDir, options = {}) {
102
152
  timeout = 15000
103
153
  } = options;
104
154
 
155
+ // Read where it is declared, refused where it is read (§4 Fail-Fast). A
156
+ // budget that is not a positive whole number of milliseconds cannot be one:
157
+ // jest reads `0` as "no budget at all", which is the opposite of what a
158
+ // caller asking for a budget means, and a string or a fraction would reach
159
+ // three `test()` calls and decide nothing there.
160
+ if (!Number.isInteger(timeout) || timeout <= 0) {
161
+ throw new Error(
162
+ '[createServiceReadinessTests] options.timeout is not a positive whole number of '
163
+ + `milliseconds - received ${String(timeout)}. Expected: the budget every test this helper `
164
+ + 'registers runs under, e.g. 15000. Fix: pass a positive integer as options.timeout, or '
165
+ + 'omit it to take the default 15000.'
166
+ );
167
+ }
168
+
105
169
  // Validate service structure FIRST
106
170
  console.log('\n🔍 Validating service structure...\n');
107
171
  const structureValidator = new ServiceStructureValidator(serviceRoot);
@@ -132,7 +196,7 @@ function createServiceReadinessTests(testsDir, options = {}) {
132
196
  // The version comes from package.json, never from config.json — the same
133
197
  // single source of truth the runtime uses:
134
198
  //
135
- // shared/connector/service-wrapper/src/ConfigLoader.js:239
199
+ // shared/connector/service-wrapper/src/ConfigLoader.js, `loadServiceConfig()`:
136
200
  // config.service.version = this.loadPackageVersion(basePath);
137
201
  //
138
202
  // That assignment is unconditional and happens BEFORE `${VAR}` placeholders
@@ -154,20 +218,7 @@ function createServiceReadinessTests(testsDir, options = {}) {
154
218
 
155
219
  if (includeOptionalChecks) {
156
220
  mockRegistry = new MockRegistry();
157
- testCookbook = {
158
- // Cookbook FORMAT version — the synthesised cookbook must satisfy the
159
- // same rule the Tier-1 gate enforces (api/docs/biz/40-cookbooks/format.md
160
- // § Required fields), otherwise this helper rejects its own output.
161
- version: MIN_COOKBOOK_FORMAT_VERSION,
162
- steps: Object.entries(operationsFlat).map(([name, op]) => ({
163
- step_id: `test-${name}`,
164
- type: 'task',
165
- service: serviceName,
166
- operation: name,
167
- input: generateMockInput(op.input),
168
- output: {}
169
- }))
170
- };
221
+ testCookbook = buildReadinessCookbook(serviceName, operationsFlat);
171
222
  }
172
223
 
173
224
  // `console` is the intended logger here, not a stand-in for one that is
@@ -228,19 +279,22 @@ function createServiceReadinessTests(testsDir, options = {}) {
228
279
 
229
280
  const operationsFlat = operations.operations || operations;
230
281
  expect(typeof operationsFlat).toBe('object');
231
- expect(Object.keys(operationsFlat).length).toBeGreaterThan(0);
232
282
 
233
- const validScopes = ['platform', 'tenant', 'workspace'];
283
+ // `schema_version`, and a dispatch table with nothing in it: the two
284
+ // document rules, asked of the one definition that owns them. The
285
+ // `toBeGreaterThan(0)` that used to stand here was the fifth answer to
286
+ // the second of them (d.465b).
287
+ expect(operationsDocumentFindings(operations)).toEqual([]);
234
288
 
235
- for (const [, operationSpec] of Object.entries(operationsFlat)) {
236
- expect(operationSpec).toHaveProperty('handler');
237
- expect(operationSpec).toHaveProperty('bundle_scope');
238
- expect(operationSpec.handler).toMatch(HANDLER_REF_PATTERN);
239
- expect(validScopes).toContain(operationSpec.bundle_scope);
240
- expect(operationSpec).not.toHaveProperty('endpoint');
241
- expect(operationSpec).not.toHaveProperty('method');
242
- }
243
- });
289
+ // The rules are the Registry's, asked of the one definition that owns
290
+ // them. Restating them here was the fourth copy, and — like the other
291
+ // three — it did not know `mutates`, `resource_type` or the kebab-case
292
+ // key rule, so this suite went green on declarations the Registry
293
+ // refuses on registration.
294
+ const verdict = operationsRuleSentences(operationsFlat);
295
+ expect(verdict.sentences).toEqual([]);
296
+ expect(verdict.valid).toBe(true);
297
+ }, timeout);
244
298
 
245
299
  test('every declared handler resolves to an exported function', () => {
246
300
  // The shape of the ref says nothing about whether the module is in the
@@ -254,7 +308,7 @@ function createServiceReadinessTests(testsDir, options = {}) {
254
308
  for (const entry of resolved) {
255
309
  expect(typeof entry.handler).toBe('function');
256
310
  }
257
- });
311
+ }, timeout);
258
312
  });
259
313
  }
260
314
 
@@ -287,4 +341,4 @@ function generateMockInput(inputSchema) {
287
341
  return mockData;
288
342
  }
289
343
 
290
- module.exports = { createServiceReadinessTests, resolveDeclaredHandlers };
344
+ module.exports = { createServiceReadinessTests, resolveDeclaredHandlers, buildReadinessCookbook };
package/src/index.js CHANGED
@@ -7,9 +7,9 @@
7
7
  * Production entry point: {@link ValidationOrchestrator} (7-step pre-validation
8
8
  * driven by operations.json — OpenAPI path iteration is NOT supported).
9
9
  *
10
- * @see /api/docs/biz/30-operations/registration-wire.md §3 (operations.json)
11
- * @see /api/docs/biz/40-cookbooks/test-runner-flow.md (validation probes)
12
- * @see /api/docs/architecture/validator.md (validator architecture)
10
+ * @see api/docs/biz/30-operations/registration-wire.md §3 (operations.json)
11
+ * @see api/docs/biz/40-cookbooks/test-runner-flow.md (validation probes)
12
+ * @see api/docs/architecture/validator.md (validator architecture)
13
13
  */
14
14
 
15
15
  const CookbookTestUtils = require('./CookbookTestUtils');
@@ -79,7 +79,6 @@ module.exports = {
79
79
  // the rule it implements today (containment, require, export is a function),
80
80
  // while the stricter declared form stays in `parseHandlerRef`, so adopting it
81
81
  // narrows nothing the wrapper accepts.
82
- get HANDLER_REF_PATTERN() { return require('./utils/handlerRef').HANDLER_REF_PATTERN; },
83
82
  get parseHandlerRef() { return require('./utils/handlerRef').parseHandlerRef; },
84
83
  get resolveHandlerModule() { return require('./utils/handlerRef').resolveHandlerModule; },
85
84
 
@@ -96,5 +95,15 @@ module.exports = {
96
95
 
97
96
  get createServiceReadinessTests() { return createServiceReadinessTests; },
98
97
  get BizCiGateContract() { return BizCiGateContract; },
99
- get IntegrationRun() { return require('./utils/integrationRun'); }
98
+ get IntegrationRun() { return require('./utils/integrationRun'); },
99
+
100
+ // The deploy contract (R1-R5, R7-R8) over one biz repository. It is exported
101
+ // for the same reason `resolveMigrationPlan` above is: the one consumer
102
+ // outside this package — `api/scripts/verify-biz-deploy-contract.sh` — reached
103
+ // it as `…/src/utils/deployContract.js`, and a path inside the package is not
104
+ // a contract. With the `exports` map (d.812) that path stops resolving at all,
105
+ // so the capability moves here, where it is named and kept.
106
+ get verifyDeployContract() { return require('./utils/deployContract').verifyDeployContract; },
107
+ get DEPLOY_CONTRACT_REQUIREMENTS() { return require('./utils/deployContract').DEPLOY_CONTRACT_REQUIREMENTS; },
108
+ get DEPLOY_CONTRACT_SCOPE() { return require('./utils/deployContract').DEPLOY_CONTRACT_SCOPE; }
100
109
  };