@onlineapps/conn-orch-validator 12.1.1 → 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 (59) hide show
  1. package/CHANGELOG.md +607 -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/.gitlab-ci.yml +203 -37
  53. package/templates/business-service/README.md +7 -4
  54. package/templates/business-service/config/env-templates/shared.env +1 -0
  55. package/templates/business-service/docs/80-setup/INSTALL.md +31 -3
  56. package/templates/business-service/docs/80-setup/PLATFORM_MATRIX.md +5 -2
  57. package/templates/business-service/src/config/index.js +15 -0
  58. package/TESTING_STRATEGY.md +0 -92
  59. package/jest.config.js +0 -37
@@ -1,8 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ const {
4
+ validateCookbook: validateCookbookFormat,
5
+ CookbookValidationError
6
+ } = require('@onlineapps/cookbook-core');
3
7
  const {
4
8
  MIN_COOKBOOK_FORMAT_VERSION,
5
- checkCookbookFormatVersion,
6
9
  readCookbookSteps,
7
10
  stepIdentityOf
8
11
  } = require('./utils/cookbookFormat');
@@ -10,6 +13,9 @@ const {
10
13
  /**
11
14
  * CookbookTestUtils - Utilities for cookbook testing
12
15
  */
16
+ /** The control-flow step types the V2.1 schema knows. */
17
+ const CONTROL_FLOW_TYPES = ['foreach', 'switch', 'fork_join', 'steps'];
18
+
13
19
  class CookbookTestUtils {
14
20
  /**
15
21
  * Create a mock workflow message
@@ -65,59 +71,38 @@ class CookbookTestUtils {
65
71
  }
66
72
 
67
73
  /**
68
- * Validate cookbook structure (basic validation)
74
+ * Validate a cookbook for the readiness-wrapper path
75
+ * (`ServiceReadinessValidator.checkCookbookExecution`).
76
+ *
77
+ * FIRST, `@onlineapps/cookbook-core` `validateCookbook` — the same function
78
+ * the Tier-1 runner (`CookbookTestRunner.validateCookbook`, d.980) and the
79
+ * receiving side run. It owns every structural rule this entry point used to
80
+ * copy: the object itself, `version`, the `steps` array, a step's `step_id`,
81
+ * `type` and `service`, and a step carrying `id` (d.983). Until d.984 this
82
+ * method held its own copy of each, with its own messages, and the `id` ban
83
+ * stayed behind cookbook-core until d.985 — one concern on two rails
84
+ * (`.claude/rules/change-discipline.md` § One rail per concern).
85
+ *
86
+ * This entry point answers with a list, not an exception, so a refusal
87
+ * becomes the list's one error. From cookbook-core only a
88
+ * `CookbookValidationError` is a finding about the cookbook; anything else it
89
+ * throws is a defect and propagates (the caller,
90
+ * `checkCookbookExecution`, reports it as a failed check).
69
91
  *
70
- * The format-version rule is the shared one (`utils/cookbookFormat`), the
71
- * same rule the Tier-1 `CookbookTestRunner` enforces — one fact, one owner.
72
- * Presence alone used to be enough here, so a cookbook stuck on an outdated
73
- * format passed the readiness wrapper unnoticed.
92
+ * @param {*} cookbook - the parsed cookbook
93
+ * @returns {{valid: boolean, errors: string[]}}
74
94
  */
75
95
  static validateCookbook(cookbook) {
76
- const errors = [];
77
-
78
- if (!cookbook) {
79
- errors.push('Cookbook is required');
80
- return { valid: false, errors };
81
- }
82
-
83
- const versionProblem = checkCookbookFormatVersion(cookbook.version);
84
- if (versionProblem) {
85
- errors.push(versionProblem);
86
- }
87
-
88
- // The one shape the format allows, through the one owner of that rule
89
- // (`utils/cookbookFormat`), so this entry point and the Tier-1 runner cannot
90
- // disagree about what a cookbook's steps are.
91
- let steps;
92
96
  try {
93
- steps = readCookbookSteps(cookbook.steps);
97
+ validateCookbookFormat(cookbook);
94
98
  } catch (error) {
95
- // The refusal is the finding. This entry point answers with a list, not
96
- // an exception, so the message becomes one more error in that list.
97
- errors.push(error.message);
98
- return { valid: false, errors };
99
- }
100
-
101
- if (steps.length === 0) {
102
- errors.push('Steps array is required');
103
- } else {
104
- steps.forEach((step, index) => {
105
- if (!stepIdentityOf(step)) {
106
- errors.push(`Step ${index}: step_id is required`);
107
- }
108
- if (!step.type) {
109
- errors.push(`Step ${index}: type is required`);
110
- }
111
- if (step.type === 'task' && !step.service) {
112
- errors.push(`Step ${index}: service is required for task steps`);
113
- }
114
- });
99
+ if (!(error instanceof CookbookValidationError)) {
100
+ throw error;
101
+ }
102
+ return { valid: false, errors: [error.message] };
115
103
  }
116
104
 
117
- return {
118
- valid: errors.length === 0,
119
- errors
120
- };
105
+ return { valid: true, errors: [] };
121
106
  }
122
107
 
123
108
  /**
@@ -212,50 +197,76 @@ class CookbookTestUtils {
212
197
  }
213
198
 
214
199
  /**
215
- * Generate control flow step (foreach, switch, etc.)
200
+ * Generate a control-flow step (`foreach`, `switch`, `fork_join`, `steps`) in
201
+ * the shape the V2.1 schema of `@onlineapps/cookbook-core` accepts.
202
+ *
203
+ * The step names the service that runs it, and every task it nests by default
204
+ * runs in that same service
205
+ * (api/docs/governance/confirmations/cookbook-execution-owner.md 002,
206
+ * api/docs/biz/40-cookbooks/format.md). Nested steps passed in `options`
207
+ * (`body`, `cases`, `default`, `branches`, `steps`) are taken as given.
208
+ *
209
+ * @param {'foreach'|'switch'|'fork_join'|'steps'} type
210
+ * @param {object} options
211
+ * @param {string} options.service - the service that runs the step (required)
212
+ * @param {string} [options.step_id] - defaults to `<type>_step_<timestamp>`
213
+ * @returns {object} one control-flow step
214
+ * @throws {Error} on a missing `service`, a V1 `id` option or an unknown type
216
215
  */
217
216
  static createControlFlowStep(type, options = {}) {
217
+ if (!CONTROL_FLOW_TYPES.includes(type)) {
218
+ throw new Error(`[CookbookTestUtils] createControlFlowStep('${type}') - unknown control-flow type '${type}' - Expected: one of ${CONTROL_FLOW_TYPES.join(', ')}. Fix: pass one of them as the first argument`);
219
+ }
220
+ if (typeof options.service !== 'string' || options.service.length === 0) {
221
+ throw new Error(`[CookbookTestUtils] createControlFlowStep('${type}') - \`service\` is required - Expected: the name of the service that runs this control-flow step. Fix: pass { service: '<name>' }`);
222
+ }
223
+ if (Object.prototype.hasOwnProperty.call(options, 'id')) {
224
+ throw new Error(`[CookbookTestUtils] createControlFlowStep('${type}') - \`id\` is not a step identity - Expected: \`step_id\` (the schema refuses \`id\` on every step). Fix: pass { step_id: '<id>' }`);
225
+ }
226
+
227
+ const { service } = options;
228
+ const task = (stepId, operation) => ({ step_id: stepId, type: 'task', service, operation });
218
229
  const baseStep = {
219
- step_id: options.step_id || options.id || `${type}-step-${Date.now()}`,
220
- type
230
+ step_id: options.step_id || `${type}_step_${Date.now()}`,
231
+ type,
232
+ service
221
233
  };
222
234
 
223
235
  switch (type) {
224
236
  case 'foreach':
225
237
  return {
226
238
  ...baseStep,
227
- items: options.items || '$api_input.items',
228
- body: options.body || {
229
- step_id: 'foreach-body',
230
- type: 'task',
231
- service: 'test-service',
232
- operation: 'processItem'
233
- }
239
+ iterator: options.iterator || '{{api_input.items}}',
240
+ body: options.body || [task('foreach_body', 'processItem')]
234
241
  };
235
242
 
236
243
  case 'switch':
237
244
  return {
238
245
  ...baseStep,
239
- condition: options.condition || '$api_input.type',
240
- cases: options.cases || [
241
- { value: 'type1', step: { step_id: 'case1', type: 'task', service: 'service1' } },
242
- { value: 'type2', step: { step_id: 'case2', type: 'task', service: 'service2' } }
243
- ],
244
- default: options.default || { step_id: 'default', type: 'task', service: 'default-service' }
246
+ expression: options.expression || '{{api_input.type}}',
247
+ cases: options.cases || {
248
+ type1: task('case1', 'handleType1'),
249
+ type2: task('case2', 'handleType2')
250
+ },
251
+ default: options.default || task('default_case', 'handleDefault')
245
252
  };
246
253
 
247
254
  case 'fork_join':
248
255
  return {
249
256
  ...baseStep,
250
- branches: options.branches || [
251
- { step_id: 'branch1', type: 'task', service: 'service1' },
252
- { step_id: 'branch2', type: 'task', service: 'service2' }
253
- ],
257
+ branches: options.branches || {
258
+ branch1: task('branch1', 'runBranch1'),
259
+ branch2: task('branch2', 'runBranch2')
260
+ },
254
261
  join: options.join || { strategy: 'merge' }
255
262
  };
256
263
 
264
+ case 'steps':
257
265
  default:
258
- return baseStep;
266
+ return {
267
+ ...baseStep,
268
+ steps: options.steps || [task('steps_first', 'runFirst')]
269
+ };
259
270
  }
260
271
  }
261
272
  }
@@ -2,7 +2,15 @@
2
2
 
3
3
  const { assertLogger } = require('@onlineapps/logger-contract');
4
4
  const CookbookTestUtils = require('./CookbookTestUtils');
5
- const { HANDLER_REF_PATTERN } = require('./utils/handlerRef');
5
+ const { operationsRuleSentences } = require('./utils/operationsRules');
6
+ const {
7
+ operationsMapFindings,
8
+ operationsDeclarationFindings,
9
+ documentFindingSentence,
10
+ ofSeverity,
11
+ SEVERITY_ERROR,
12
+ SEVERITY_WARNING
13
+ } = require('./utils/operationsDocumentRules');
6
14
 
7
15
  /**
8
16
  * The semver 2.0.0 grammar, verbatim from semver.org's own published regular
@@ -41,8 +49,8 @@ const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]
41
49
  * reads no others.
42
50
  *
43
51
  * @see api/docs/biz/30-operations/schema-v3.md
44
- * @see /api/docs/biz/40-cookbooks/test-runner-flow.md (input probe contract)
45
- * @see /api/docs/biz/80-decisions/0005-no-http-in-biz-containers.md
52
+ * @see api/docs/biz/40-cookbooks/test-runner-flow.md (input probe contract)
53
+ * @see api/docs/biz/80-decisions/0005-no-http-in-biz-containers.md
46
54
  */
47
55
  class ServiceReadinessValidator {
48
56
  constructor(options = {}) {
@@ -51,7 +59,7 @@ class ServiceReadinessValidator {
51
59
  // it. Until then this class demanded a logger and never wrote a line, so a
52
60
  // caller that did not print the returned object learned nothing about why a
53
61
  // service was refused. Owner confirmation:
54
- // docs/governance/confirmations/connector-logger-contract.md 001.
62
+ // api/docs/governance/confirmations/connector-logger-contract.md 001.
55
63
  this.logger = assertLogger(
56
64
  'ServiceReadinessValidator',
57
65
  options.logger,
@@ -60,7 +68,7 @@ class ServiceReadinessValidator {
60
68
 
61
69
  // Readiness checks: core (80 points) + optional (20 points) = 100 points max
62
70
  // Core checks ALWAYS run, optional checks run if testCookbook/registry provided
63
- // See: /shared/connector/conn-orch-validator/README.md for usage pattern
71
+ // See: api/shared/connector/conn-orch-validator/README.md for usage pattern
64
72
  //
65
73
  // Weight allocation post-ADR-0005:
66
74
  // operations: 80 — was 60; absorbed the 20 pts from retired `health`.
@@ -205,59 +213,41 @@ class ServiceReadinessValidator {
205
213
 
206
214
  /**
207
215
  * Check operations.json compliance (v3 — handler registry).
208
- * Required per operation: description, handler, bundle_scope, input, output.
209
- * Forbidden (v2): endpoint, method, path.
216
+ *
217
+ * The per-operation rules are the Registry's own, read from the one
218
+ * definition that owns them (`utils/operationsRules.js` →
219
+ * `@onlineapps/service-validator-core`); `input` and `output` are this
220
+ * check's own requirement, because the readiness run synthesises a cookbook
221
+ * from them, and a missing `description` is a warning.
222
+ *
223
+ * @see api/docs/governance/confirmations/operations-schema-mirror.md
210
224
  */
211
225
  async checkOperationsCompliance(operations) {
212
226
  try {
213
227
  const errors = [];
214
228
  const warnings = [];
215
229
 
216
- if (!operations || typeof operations !== 'object') {
217
- return {
218
- passed: false,
219
- error: 'Operations must be an object'
220
- };
221
- }
222
-
223
- const operationCount = Object.keys(operations).length;
224
- if (operationCount === 0) {
225
- return {
226
- passed: false,
227
- error: 'No operations defined'
228
- };
229
- }
230
-
231
- const validScopes = ['platform', 'tenant', 'workspace'];
232
- const forbiddenV2Fields = ['endpoint', 'method', 'path'];
233
-
234
- for (const [name, operation] of Object.entries(operations)) {
235
- if (!operation.description) {
236
- warnings.push(`Operation '${name}' missing description`);
237
- }
238
- if (!operation.handler) {
239
- errors.push(`Operation '${name}' missing handler (v3 — 'handlers/<path>#<export>')`);
240
- } else if (!HANDLER_REF_PATTERN.test(operation.handler)) {
241
- errors.push(`Operation '${name}' has invalid handler ref: '${operation.handler}'`);
242
- }
243
- if (!operation.bundle_scope) {
244
- errors.push(`Operation '${name}' missing bundle_scope (platform|tenant|workspace)`);
245
- } else if (!validScopes.includes(operation.bundle_scope)) {
246
- errors.push(`Operation '${name}' has invalid bundle_scope: '${operation.bundle_scope}'`);
247
- }
248
- if (!operation.input) {
249
- errors.push(`Operation '${name}' missing input schema`);
250
- }
251
- if (!operation.output) {
252
- errors.push(`Operation '${name}' missing output schema`);
253
- }
254
-
255
- for (const forbidden of forbiddenV2Fields) {
256
- if (forbidden in operation) {
257
- errors.push(`Operation '${name}' has retired v2 field '${forbidden}' — remove per RFC §5.3`);
258
- }
259
- }
260
- }
230
+ // A map with nothing in it — asked of the one definition that owns that
231
+ // rule. This rail was the strict one of four before d.465b; it still
232
+ // refuses, but now it refuses in the same sentence as every other rail
233
+ // instead of one it wrote itself.
234
+ errors.push(...operationsMapFindings(operations).map(documentFindingSentence));
235
+
236
+ // The Registry's rules, asked of the one definition that owns them —
237
+ // including "the map must be a plain object", which this method used to
238
+ // answer on its own.
239
+ errors.push(...operationsRuleSentences(operations).sentences);
240
+
241
+ const isMap = operations !== null && typeof operations === 'object' && !Array.isArray(operations);
242
+ const operationCount = isMap ? Object.keys(operations).length : 0;
243
+
244
+ // A declaration complete enough for the cookbook this validator
245
+ // synthesises from it — the same question step 4 of the boot validation
246
+ // asks, asked of the same definition since d.465c, so the two can no
247
+ // longer answer it in two different sentences.
248
+ const declaration = operationsDeclarationFindings(operations);
249
+ errors.push(...ofSeverity(declaration, SEVERITY_ERROR).map(documentFindingSentence));
250
+ warnings.push(...ofSeverity(declaration, SEVERITY_WARNING).map(documentFindingSentence));
261
251
 
262
252
  return {
263
253
  passed: errors.length === 0,
@@ -17,9 +17,24 @@ const ValidationProofGenerator = require('./validators/ValidationProofGenerator'
17
17
  const { VALIDATOR_VERSION } = require('./validatorIdentity');
18
18
  const { ServiceStructureValidator } = require('./validators/ServiceStructureValidator');
19
19
  const { describeStepFailureWithContext } = require('./utils/stepFailure');
20
+ // Where the proof file lives is ONE fact of this package, and its owner is the
21
+ // module that writes it in the CI gate. Until d.758 this class built the same
22
+ // path from a literal of its own, so the boot path and the gate path each
23
+ // carried a copy — and two copies of one fact agree only while somebody is
24
+ // watching (`.claude/rules/doc-code-binding.md` §1). The wrapper's pair ended
25
+ // the same way in d.715 (`ConfigLoader.PROOF_RELATIVE_PATH`).
26
+ const { PROOF_RELATIVE_PATH } = require('./utils/preValidation');
20
27
  const CookbookTestRunner = require('./CookbookTestRunner');
21
28
  const { assertLogger } = require('@onlineapps/logger-contract');
22
- const { HANDLER_REF_PATTERN } = require('./utils/handlerRef');
29
+ const { operationsRuleSentences } = require('./utils/operationsRules');
30
+ const {
31
+ operationsDocumentFindings,
32
+ operationsDeclarationFindings,
33
+ documentFindingSentence,
34
+ ofSeverity,
35
+ SEVERITY_ERROR,
36
+ SEVERITY_WARNING
37
+ } = require('./utils/operationsDocumentRules');
23
38
  const { loadManifest, DEFAULT_MANIFEST_PATH } = require('./manifest/loadManifest');
24
39
  const { runManifest } = require('./manifest/runManifest');
25
40
  const { resolveWorkspaceRoot } = require('./manifest/workspaceRoot');
@@ -172,7 +187,7 @@ class ValidationOrchestrator {
172
187
  // hands the same object to CookbookTestRunner below, which demands all four.
173
188
  // An incomplete logger therefore reached the runner one line later and threw
174
189
  // in its name instead of this one. Owner confirmation:
175
- // docs/governance/confirmations/connector-logger-contract.md 001.
190
+ // api/docs/governance/confirmations/connector-logger-contract.md 001.
176
191
  this.logger = assertLogger(
177
192
  'ValidationOrchestrator',
178
193
  options.logger,
@@ -186,8 +201,8 @@ class ValidationOrchestrator {
186
201
  // failure one layer away from the cause.
187
202
  this.configPath = path.join(this.serviceRoot, 'config', 'service');
188
203
 
189
- this.runtimePath = path.join(this.serviceRoot, 'conn-runtime');
190
- this.proofPath = path.join(this.runtimePath, 'validation-proof.json');
204
+ this.proofPath = path.join(this.serviceRoot, PROOF_RELATIVE_PATH);
205
+ this.runtimePath = path.dirname(this.proofPath);
191
206
 
192
207
  // The manifest run of THIS validation, computed on first use (manifestRun()).
193
208
  // One orchestrator is one run: `ServiceWrapper._createValidationOrchestrator()`
@@ -455,6 +470,15 @@ class ValidationOrchestrator {
455
470
  const structureFailed = await this.runStep(results, 'structure', async () => {
456
471
  this.logger.info('[ValidationOrchestrator] Step 1/7: Service Structure');
457
472
  results.steps.structure = await this.validateStructure();
473
+ // Step 1 collects its warnings like step 4 does. Until d.791c it was the
474
+ // only step that did not: twelve `this.warnings.push(…)` in
475
+ // `ServiceStructureValidator` filled a field the summary never read, so
476
+ // findings like STANDARD_LEVEL_GAP reached no operator. Measured over all
477
+ // eight `api_biz/*` before the change: exactly ONE warning appears
478
+ // (ingest), so the symmetry arrived with compliance rather than with a
479
+ // flood (`automation-gates.md` §3). Before the push, because a failing
480
+ // structure returns below and its warnings are worth just as much.
481
+ results.warnings.push(...attributeToStep(results.steps.structure.warnings || [], 'structure'));
458
482
  if (results.steps.structure.valid) return false;
459
483
 
460
484
  results.success = false;
@@ -721,11 +745,20 @@ class ValidationOrchestrator {
721
745
 
722
746
  /**
723
747
  * Step 4: Validate operations compliance (v3 — handler registry dispatch).
724
- * Required per operation: handler ('handlers/<path>#<export>'), bundle_scope, input, output.
725
- * Forbidden (v2): endpoint, method, path.
726
- * Warned about: a missing `description` — inherited from the removed
727
- * readiness step, which was the only place that noticed it and then threw
728
- * the notice away.
748
+ *
749
+ * The per-operation rules are the Registry's, asked of the one definition
750
+ * that owns them (`utils/operationsRules.js` →
751
+ * `@onlineapps/service-validator-core`), so this step refuses exactly what a
752
+ * registration would refuse — including `mutates`, `resource_type` and the
753
+ * kebab-case key rule, which the copy that used to live here did not know.
754
+ *
755
+ * Two things this step checks beyond them, because a boot needs them and a
756
+ * registration does not: `input` and `output` must be declared (error), and a
757
+ * missing `description` is warned about — inherited from the removed
758
+ * readiness step, which was the only place that noticed it and then threw the
759
+ * notice away.
760
+ *
761
+ * @see api/docs/governance/confirmations/operations-schema-mirror.md
729
762
  */
730
763
  async validateOperations() {
731
764
  try {
@@ -734,39 +767,27 @@ class ValidationOrchestrator {
734
767
  const errors = [];
735
768
  const warnings = [];
736
769
 
737
- const validScopes = ['platform', 'tenant', 'workspace'];
738
- const forbiddenV2Fields = ['endpoint', 'method', 'path'];
739
-
740
- for (const [opName, opDef] of Object.entries(operations.operations || {})) {
741
- if (!opDef.description) {
742
- warnings.push(`Operation ${opName}: missing description`);
743
- }
744
-
745
- if (!opDef.handler) {
746
- errors.push(`Operation ${opName}: missing handler (v3 — 'handlers/<path>#<export>')`);
747
- } else if (!HANDLER_REF_PATTERN.test(opDef.handler)) {
748
- errors.push(`Operation ${opName}: invalid handler ref '${opDef.handler}' — expected 'handlers/<path>#<exportName>'`);
749
- }
750
-
751
- if (!opDef.bundle_scope) {
752
- errors.push(`Operation ${opName}: missing bundle_scope (platform|tenant|workspace)`);
753
- } else if (!validScopes.includes(opDef.bundle_scope)) {
754
- errors.push(`Operation ${opName}: invalid bundle_scope '${opDef.bundle_scope}' — expected one of ${validScopes.join(', ')}`);
755
- }
756
-
757
- if (!opDef.input) {
758
- errors.push(`Operation ${opName}: missing input schema`);
759
- }
760
- if (!opDef.output) {
761
- errors.push(`Operation ${opName}: missing output schema`);
762
- }
763
-
764
- for (const forbidden of forbiddenV2Fields) {
765
- if (forbidden in opDef) {
766
- errors.push(`Operation ${opName}: retired v2 field '${forbidden}' present — remove per RFC §5.3`);
767
- }
768
- }
769
- }
770
+ // The two DOCUMENT rules (`schema_version`, an operations map with
771
+ // nothing in it), asked of the one definition that owns them. Until
772
+ // d.465b this step was the rail that answered an empty map with silence
773
+ // — a clean PASS — while three other rails called the same document
774
+ // broken.
775
+ errors.push(...operationsDocumentFindings(operations).map(documentFindingSentence));
776
+
777
+ // Every rule the Registry refuses a registration on, asked once, of the
778
+ // one definition that owns them (`utils/operationsRules.js`). A missing
779
+ // `operations` key is one of them — it used to be read as `|| {}` here,
780
+ // which turned the Registry's refusal into a silent PASS.
781
+ const map = operations.operations;
782
+ errors.push(...operationsRuleSentences(map).sentences);
783
+
784
+ // What the Registry does NOT check, and this step does: a declaration
785
+ // complete enough for a cookbook to be generated from it. The readiness
786
+ // score asks the same question, and until d.465c the two worded every
787
+ // answer differently, which is two rails however identical the severity.
788
+ const declaration = operationsDeclarationFindings(map);
789
+ errors.push(...ofSeverity(declaration, SEVERITY_ERROR).map(documentFindingSentence));
790
+ warnings.push(...ofSeverity(declaration, SEVERITY_WARNING).map(documentFindingSentence));
770
791
 
771
792
  this.logger.info(`[ValidationOrchestrator] ✓ Operations compliance: ${errors.length === 0 ? 'PASS' : 'FAIL'}`
772
793
  + `${warnings.length > 0 ? ` (${warnings.length} warning(s))` : ''}`);
@@ -953,7 +974,7 @@ class ValidationOrchestrator {
953
974
  *
954
975
  * OUTSIDE A GIT CHECKOUT THE STEP DOES NOT MEASURE. Owner decision 2026-09-17
955
976
  * (confirmation `biz-service-manifest` 011): a production image is not a
956
- * checkout and carries neither `docker-compose.yml` nor `README.md`, so the
977
+ * checkout and carries neither a compose file nor a README, so the
957
978
  * uniform measured there reports absences that say nothing about the
958
979
  * repository — 12 findings for a healthy service. A verdict reached that way
959
980
  * is not a strict check, it is a wrong answer, and `NOT DEPLOYABLE` printed
@@ -983,7 +1004,7 @@ class ValidationOrchestrator {
983
1004
  if (!isGitCheckout(this.serviceRoot)) {
984
1005
  // One line, and no table: naming the findings of a tree this run cannot
985
1006
  // read would be the false guarantee of `automation-gates.md` §5 in
986
- // reverse — a reader acting on `README.md missing` inside a container
1007
+ // reverse — a reader acting on a missing-README finding inside a container
987
1008
  // would go looking for a file the image is not supposed to carry.
988
1009
  this.logger.info(`[ValidationOrchestrator] NOT RUN manifest conformance — ${NOT_A_CHECKOUT} `
989
1010
  + 'Deployability is proven per commit by the CI job `validate-uniform` over the checkout '
@@ -418,7 +418,7 @@ function runSetupDbAccount(options) {
418
418
 
419
419
  /**
420
420
  * Build the schema the contract declares. One implementation for every service:
421
- * the repo declares WHAT, this decides HOW (docs/biz/00-model/uniformity-principle.md).
421
+ * the repo declares WHAT, this decides HOW (api/docs/biz/00-model/uniformity-principle.md).
422
422
  */
423
423
  function runSetupDb(options) {
424
424
  const contractInfo = loadAndValidateIntegrationContract(options.serviceRoot, options.contractPath);
@@ -608,7 +608,7 @@ function exitWhenFlushed(code) {
608
608
  * and pass `service.url` to the runner, which never read it (`this.serviceUrl`
609
609
  * appears nowhere in CookbookTestRunner). `ConfigLoader.loadAll` no longer
610
610
  * returns that field either — the owner removed `port` and `url` from the
611
- * service shape (docs/governance/confirmations/biz-service-port-url.md 001), so
611
+ * service shape (api/docs/governance/confirmations/biz-service-port-url.md 001), so
612
612
  * the value had been `undefined` for every service on the platform. What the
613
613
  * load still did was make a readable config, an installed wrapper and a
614
614
  * resolvable `${ENV}` placeholder preconditions of a cookbook run that depends