@onlineapps/conn-orch-validator 2.0.28 → 2.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -88,20 +88,52 @@ services/my-service/
88
88
  ```
89
89
 
90
90
  **Proof Lifecycle:**
91
- - **Valid for:** 30 days OR until service fingerprint changes
91
+ - **Valid for:** 7 days OR until service fingerprint changes
92
92
  - **Fingerprint:** SHA256 hash of (version + operations + dependencies + config)
93
93
  - **Revalidation:** Automatic if proof missing/invalid/expired
94
94
  - **Registry:** Accepts service with valid proof (skips Tier 2 validation)
95
95
 
96
96
  ---
97
97
 
98
+ ## Implementation Standard Levels
99
+
100
+ The validator evaluates each service against cumulative implementation standards. Levels are ordered — each requires all previous to pass:
101
+
102
+ | Level | Name | Checks | Since |
103
+ |-------|------|--------|-------|
104
+ | **v1.0** | Base Service Standard | `conn-config/`, `src/app.js`, `index.js`, valid `config.json` + `operations.json`, `@onlineapps/service-wrapper` dep | 2025-06 |
105
+ | **v1.1** | Multitenancy Standard | `wrapper.tenantContext` configured in `config.json` | 2026-03 |
106
+ | **v1.2** | Business Error Handling | `@onlineapps/service-common` dep + `businessErrorHandler` in `src/app.js` | 2026-03 |
107
+
108
+ **Key properties:**
109
+ - **Cumulative** — v1.2 requires v1.0 + v1.1 to also pass
110
+ - **Baked into validator** — older validator versions naturally know fewer levels (backward compatible)
111
+ - **Warnings** — next unsatisfied level generates `STANDARD_LEVEL_GAP` warnings with specific missing checks
112
+ - **Exposed in `/info`** — ServiceWrapper's `GET /info` endpoint returns the highest satisfied level
113
+ - **In validation results** — `validate()` returns `standardLevel` and `standardDetails`
114
+
115
+ ```javascript
116
+ // Programmatic access
117
+ const { ServiceStructureValidator } = require('@onlineapps/conn-orch-validator/src/validators/ServiceStructureValidator');
118
+ const validator = new ServiceStructureValidator('/path/to/service');
119
+ const { level, details } = validator.determineStandardLevel();
120
+ // level = 'v1.2', details = [{ level: 'v1.0', passed: true, checks: [...] }, ...]
121
+
122
+ // List all known levels
123
+ ServiceStructureValidator.getStandardLevels();
124
+ // [{ level: 'v1.0', name: 'Base Service Standard', since: '2025-06-01' }, ...]
125
+ ```
126
+
127
+ **Related:** [Service Endpoints — Standard Levels](/docs/standards/SERVICE_ENDPOINTS.md#implementation-standard-levels), [Error Handling Standard](/docs/standards/ERROR_HANDLING.md#business-service-error-standard)
128
+
98
129
  ## Related Documentation
99
130
 
100
131
  - [SERVICE_REGISTRATION_FLOW.md](/services/hello-service/docs/SERVICE_REGISTRATION_FLOW.md)
101
132
  - [/docs/architecture/validator.md](/docs/architecture/validator.md)
102
133
  - [/docs/standards/OPERATIONS.md](/docs/standards/OPERATIONS.md)
134
+ - [/docs/standards/ERROR_HANDLING.md](/docs/standards/ERROR_HANDLING.md)
103
135
  - [@onlineapps/service-validator-core](/shared/service-validator-core/README.md)
104
136
 
105
137
  ---
106
138
 
107
- *Last updated: 2025-10-22*
139
+ *Last updated: 2026-03-24*
package/jest.config.js CHANGED
@@ -1,5 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ process.env.OA_VALIDATION_TENANT_ID = process.env.OA_VALIDATION_TENANT_ID || '99';
4
+ process.env.OA_VALIDATION_WORKSPACE_ID = process.env.OA_VALIDATION_WORKSPACE_ID || '200';
5
+
3
6
  module.exports = {
4
7
  testEnvironment: 'node',
5
8
  coverageDirectory: 'coverage',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/conn-orch-validator",
3
- "version": "2.0.28",
3
+ "version": "2.0.30",
4
4
  "description": "Validation orchestrator for OA Drive microservices - coordinates validation across all layers (base, infra, orch, business)",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -157,13 +157,10 @@ class CookbookTestRunner {
157
157
  // Resolve operation endpoint from operations.json
158
158
  const endpoint = await this.resolveOperation(step.service, step.operation);
159
159
 
160
- // Build request
161
- // Validation context headers:
162
- // 1. x-validation-request: signals this is a validation probe (deterministic responses)
163
- // 2. account-id: required by multitenancy middleware for /api/** endpoints
164
- const validationAccountId = process.env.OA_VALIDATION_ACCOUNT_ID;
165
- if (!validationAccountId) {
166
- throw new Error('[CookbookTestRunner] Missing required environment variable OA_VALIDATION_ACCOUNT_ID');
160
+ const validationTenantId = process.env.OA_VALIDATION_TENANT_ID;
161
+ const validationWorkspaceId = process.env.OA_VALIDATION_WORKSPACE_ID;
162
+ if (!validationTenantId || !validationWorkspaceId) {
163
+ throw new Error('[CookbookTestRunner] Missing required environment variables OA_VALIDATION_TENANT_ID and/or OA_VALIDATION_WORKSPACE_ID');
167
164
  }
168
165
  const request = {
169
166
  method: endpoint.method,
@@ -172,7 +169,8 @@ class CookbookTestRunner {
172
169
  timeout: testConfig.timeout || this.timeout,
173
170
  headers: {
174
171
  'x-validation-request': 'true',
175
- 'account-id': validationAccountId,
172
+ 'x-tenant-id': validationTenantId,
173
+ 'x-workspace-id': validationWorkspaceId,
176
174
  ...resolveHeaders(endpoint.headers),
177
175
  ...resolveHeaders(step.headers)
178
176
  }
@@ -215,16 +215,15 @@ class ServiceReadinessValidator {
215
215
  // Generate test input based on schema
216
216
  const testInput = this.generateTestInput(operation.input);
217
217
 
218
- // Validation context headers:
219
- // 1. x-validation-request: signals this is a validation probe
220
- // 2. account-id: required by multitenancy middleware for /api/** endpoints
221
- const validationAccountId = process.env.OA_VALIDATION_ACCOUNT_ID;
222
- if (!validationAccountId) {
223
- throw new Error('[ServiceReadinessValidator] Missing required environment variable OA_VALIDATION_ACCOUNT_ID');
218
+ const validationTenantId = process.env.OA_VALIDATION_TENANT_ID;
219
+ const validationWorkspaceId = process.env.OA_VALIDATION_WORKSPACE_ID;
220
+ if (!validationTenantId || !validationWorkspaceId) {
221
+ throw new Error('[ServiceReadinessValidator] Missing required environment variables OA_VALIDATION_TENANT_ID and/or OA_VALIDATION_WORKSPACE_ID');
224
222
  }
225
223
  const headers = {
226
224
  'x-validation-request': 'true',
227
- 'account-id': validationAccountId,
225
+ 'x-tenant-id': validationTenantId,
226
+ 'x-workspace-id': validationWorkspaceId,
228
227
  ...resolveHeaders(operation.headers)
229
228
  };
230
229
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const { ValidationProofCodec } = require('@onlineapps/service-validator-core');
5
+ const { ValidationProofCodec, ValidationProofVerifier } = require('@onlineapps/service-validator-core');
6
6
  const FingerprintUtils = require('@onlineapps/service-validator-core').FingerprintUtils;
7
7
  const { ServiceStructureValidator } = require('./validators/ServiceStructureValidator');
8
8
  const ServiceReadinessValidator = require('./ServiceReadinessValidator');
@@ -97,10 +97,10 @@ class ValidationOrchestrator {
97
97
  return false;
98
98
  }
99
99
 
100
- // Decode and verify proof using ValidationProofCodec
101
- const verificationResult = ValidationProofCodec.decode(proof, {
102
- maxProofAge: 30 * 24 * 60 * 60 * 1000 // 30 days
103
- });
100
+ // Verify proof integrity + age using the shared core verifier defaults.
101
+ // This MUST match what Tier-2 validator enforces to avoid PROOF_EXPIRED drift.
102
+ const verifier = new ValidationProofVerifier();
103
+ const verificationResult = verifier.verifyProof(proof);
104
104
 
105
105
  if (!verificationResult.valid) {
106
106
  console.log(`[ValidationOrchestrator] Proof validation failed: ${verificationResult.reason}`);
@@ -136,7 +136,7 @@ class ValidationOrchestrator {
136
136
  const packageFile = path.join(this.serviceRoot, 'package.json');
137
137
  const dockerFile = path.join(this.serviceRoot, 'Dockerfile');
138
138
  const dockerComposeFile = path.join(this.serviceRoot, 'docker-compose.yml');
139
- const envExampleFile = path.join(this.serviceRoot, '.env.example');
139
+ const envTemplateFile = path.join(this.serviceRoot, '..', '..', 'config', 'env-templates', `${path.basename(this.serviceRoot)}.env`);
140
140
 
141
141
  if (!fs.existsSync(configFile)) {
142
142
  throw new Error('config.json not found');
@@ -154,8 +154,8 @@ class ValidationOrchestrator {
154
154
  if (fs.existsSync(dockerComposeFile)) {
155
155
  infra.dockerCompose = fs.readFileSync(dockerComposeFile, 'utf8');
156
156
  }
157
- if (fs.existsSync(envExampleFile)) {
158
- infra.envExample = fs.readFileSync(envExampleFile, 'utf8');
157
+ if (fs.existsSync(envTemplateFile)) {
158
+ infra.envTemplate = fs.readFileSync(envTemplateFile, 'utf8');
159
159
  }
160
160
 
161
161
  // Extract @onlineapps/* dependencies
@@ -4,7 +4,8 @@
4
4
  * Validates that a business service has correct directory structure,
5
5
  * configuration files, and follows OA Drive standards.
6
6
  *
7
- * Used by test helpers to provide clear error messages when something is wrong.
7
+ * Each standard level is cumulative v1.2 requires all checks from v1.0 + v1.1 + v1.2.
8
+ * Older validator versions naturally support fewer levels (they don't know about newer ones).
8
9
  *
9
10
  * @module validators/ServiceStructureValidator
10
11
  */
@@ -14,6 +15,122 @@
14
15
  const fs = require('fs');
15
16
  const path = require('path');
16
17
 
18
+ /**
19
+ * Try reading from new path first (config/service/), fall back to legacy (conn-config/).
20
+ * @param {string} root - Service root
21
+ * @param {string} filename - e.g. 'config.json'
22
+ * @returns {string|null} File contents or null
23
+ */
24
+ function readConfigFile(root, filename) {
25
+ const candidates = [
26
+ path.join(root, 'config', 'service', filename),
27
+ path.join(root, 'conn-config', filename)
28
+ ];
29
+ for (const p of candidates) {
30
+ try { return fs.readFileSync(p, 'utf-8'); } catch { /* try next */ }
31
+ }
32
+ return null;
33
+ }
34
+
35
+ function hasConfigDir(root) {
36
+ return fs.existsSync(path.join(root, 'config', 'service')) ||
37
+ fs.existsSync(path.join(root, 'conn-config'));
38
+ }
39
+
40
+ const STANDARD_LEVELS = [
41
+ {
42
+ level: 'v1.0',
43
+ name: 'Base Service Standard',
44
+ since: '2025-06-01',
45
+ checks: (root) => {
46
+ const results = [];
47
+ const has = (p) => fs.existsSync(path.join(root, p));
48
+ const read = (p) => { try { return fs.readFileSync(path.join(root, p), 'utf-8'); } catch { return null; } };
49
+
50
+ results.push({ passed: hasConfigDir(root), id: 'dir_conn_config', message: 'config/service/ or conn-config/ directory' });
51
+ results.push({ passed: has('src'), id: 'dir_src', message: 'src/ directory' });
52
+ results.push({ passed: has('tests'), id: 'dir_tests', message: 'tests/ directory' });
53
+ results.push({ passed: has('src/app.js'), id: 'file_app', message: 'src/app.js' });
54
+ results.push({ passed: has('index.js'), id: 'file_index', message: 'index.js entry point' });
55
+
56
+ const configRaw = readConfigFile(root, 'config.json');
57
+ let configValid = false;
58
+ if (configRaw) {
59
+ try {
60
+ const cfg = JSON.parse(configRaw);
61
+ configValid = !!(cfg.service?.name && cfg.service?.port);
62
+ } catch { /* invalid JSON */ }
63
+ }
64
+ results.push({ passed: configValid, id: 'config_valid', message: 'Valid config.json with service.name + service.port' });
65
+
66
+ const opsRaw = readConfigFile(root, 'operations.json');
67
+ let opsValid = false;
68
+ if (opsRaw) {
69
+ try {
70
+ const ops = JSON.parse(opsRaw);
71
+ opsValid = !!(ops.operations && typeof ops.operations === 'object');
72
+ } catch { /* invalid JSON */ }
73
+ }
74
+ results.push({ passed: opsValid, id: 'operations_valid', message: 'Valid operations.json' });
75
+
76
+ const pkgRaw = read('package.json');
77
+ let hasWrapper = false;
78
+ if (pkgRaw) {
79
+ try {
80
+ const pkg = JSON.parse(pkgRaw);
81
+ hasWrapper = !!pkg.dependencies?.['@onlineapps/service-wrapper'];
82
+ } catch { /* invalid JSON */ }
83
+ }
84
+ results.push({ passed: hasWrapper, id: 'dep_service_wrapper', message: '@onlineapps/service-wrapper dependency' });
85
+
86
+ return results;
87
+ }
88
+ },
89
+ {
90
+ level: 'v1.1',
91
+ name: 'Multitenancy Standard',
92
+ since: '2026-03-01',
93
+ checks: (root) => {
94
+ const configRaw = readConfigFile(root, 'config.json');
95
+ let hasTenantContext = false;
96
+ if (configRaw) {
97
+ try {
98
+ const cfg = JSON.parse(configRaw);
99
+ hasTenantContext = cfg.wrapper?.tenantContext !== undefined;
100
+ } catch { /* invalid JSON */ }
101
+ }
102
+ return [
103
+ { passed: hasTenantContext, id: 'tenant_context', message: 'wrapper.tenantContext configured in config.json' }
104
+ ];
105
+ }
106
+ },
107
+ {
108
+ level: 'v1.2',
109
+ name: 'Business Error Handling Standard',
110
+ since: '2026-03-24',
111
+ checks: (root) => {
112
+ const read = (p) => { try { return fs.readFileSync(path.join(root, p), 'utf-8'); } catch { return null; } };
113
+
114
+ const pkgRaw = read('package.json');
115
+ let hasServiceCommon = false;
116
+ if (pkgRaw) {
117
+ try {
118
+ const pkg = JSON.parse(pkgRaw);
119
+ hasServiceCommon = !!pkg.dependencies?.['@onlineapps/service-common'];
120
+ } catch { /* invalid JSON */ }
121
+ }
122
+
123
+ const appContent = read('src/app.js') || '';
124
+ const hasErrorMiddleware = appContent.includes('businessErrorHandler');
125
+
126
+ return [
127
+ { passed: hasServiceCommon, id: 'dep_service_common', message: '@onlineapps/service-common dependency' },
128
+ { passed: hasErrorMiddleware, id: 'error_middleware', message: 'businessErrorHandler middleware in src/app.js' }
129
+ ];
130
+ }
131
+ }
132
+ ];
133
+
17
134
  class ServiceStructureValidator {
18
135
  constructor(serviceRoot) {
19
136
  this.serviceRoot = serviceRoot;
@@ -49,26 +166,100 @@ class ServiceStructureValidator {
49
166
  // 5. Validate test structure
50
167
  this.validateTestStructure();
51
168
 
169
+ // 6. Determine standard level
170
+ const standardResult = this.determineStandardLevel();
171
+
52
172
  const valid = this.errors.length === 0;
53
173
 
54
174
  return {
55
175
  valid,
56
176
  errors: this.errors,
57
177
  warnings: this.warnings,
58
- info: this.info
178
+ info: this.info,
179
+ standardLevel: standardResult.level,
180
+ standardDetails: standardResult.details
59
181
  };
60
182
  }
61
183
 
184
+ /**
185
+ * Determine the highest satisfied implementation standard level.
186
+ * Levels are cumulative — each requires all previous levels to pass.
187
+ *
188
+ * @returns {{ level: string|null, details: Array<{ level: string, name: string, passed: boolean, checks: Array }> }}
189
+ */
190
+ determineStandardLevel() {
191
+ const details = [];
192
+ let highestPassed = null;
193
+
194
+ for (const standard of STANDARD_LEVELS) {
195
+ const checkResults = standard.checks(this.serviceRoot);
196
+ const allPassed = checkResults.every(c => c.passed);
197
+
198
+ details.push({
199
+ level: standard.level,
200
+ name: standard.name,
201
+ since: standard.since,
202
+ passed: allPassed,
203
+ checks: checkResults
204
+ });
205
+
206
+ if (allPassed) {
207
+ highestPassed = standard.level;
208
+ } else {
209
+ break;
210
+ }
211
+ }
212
+
213
+ if (highestPassed) {
214
+ this.info.push(`✓ Implementation standard level: ${highestPassed}`);
215
+ }
216
+
217
+ const nextLevel = details.find(d => !d.passed);
218
+ if (nextLevel) {
219
+ const failing = nextLevel.checks.filter(c => !c.passed);
220
+ for (const check of failing) {
221
+ this.warnings.push({
222
+ type: 'STANDARD_LEVEL_GAP',
223
+ level: nextLevel.level,
224
+ check: check.id,
225
+ message: `Standard ${nextLevel.level} (${nextLevel.name}): missing ${check.message}`,
226
+ fix: `Implement ${check.message} to reach standard ${nextLevel.level}. See docs/standards/SERVICE_TEMPLATE.md`
227
+ });
228
+ }
229
+ }
230
+
231
+ return { level: highestPassed, details };
232
+ }
233
+
234
+ /**
235
+ * Get all known standard levels (for /info endpoint and external tools).
236
+ * @returns {Array<{ level: string, name: string, since: string }>}
237
+ */
238
+ static getStandardLevels() {
239
+ return STANDARD_LEVELS.map(s => ({ level: s.level, name: s.name, since: s.since }));
240
+ }
241
+
62
242
  /**
63
243
  * Validate directory structure exists
64
244
  */
65
245
  validateDirectoryStructure() {
66
246
  const requiredDirs = [
67
- { path: 'conn-config', description: 'Configuration directory' },
68
247
  { path: 'src', description: 'Source code directory' },
69
248
  { path: 'tests', description: 'Tests directory' }
70
249
  ];
71
250
 
251
+ if (!hasConfigDir(this.serviceRoot)) {
252
+ this.errors.push({
253
+ type: 'MISSING_DIRECTORY',
254
+ path: 'config/service/ (or conn-config/)',
255
+ message: 'Required directory missing: config/service/ (or legacy conn-config/)',
256
+ description: 'Configuration directory',
257
+ fix: 'Create directory: mkdir -p config/service'
258
+ });
259
+ } else {
260
+ this.info.push('✓ Found Configuration directory');
261
+ }
262
+
72
263
  const recommendedDirs = [
73
264
  { path: 'tests/unit', description: 'Unit tests' },
74
265
  { path: 'tests/integration', description: 'Integration tests' },
@@ -108,48 +299,46 @@ class ServiceStructureValidator {
108
299
  * Validate configuration files
109
300
  */
110
301
  validateConfigurationFiles() {
111
- // 1. Validate conn-config/config.json
112
- const configPath = path.join(this.serviceRoot, 'conn-config/config.json');
113
- if (!fs.existsSync(configPath)) {
302
+ const configRaw = readConfigFile(this.serviceRoot, 'config.json');
303
+ if (!configRaw) {
114
304
  this.errors.push({
115
305
  type: 'MISSING_CONFIG',
116
- path: 'conn-config/config.json',
117
- message: 'Service configuration missing: conn-config/config.json',
306
+ path: 'config/service/config.json (or conn-config/config.json)',
307
+ message: 'Service configuration missing: config/service/config.json (or conn-config/config.json)',
118
308
  fix: 'Create config.json with service metadata. See: /docs/standards/SERVICE_TEMPLATE.md'
119
309
  });
120
310
  } else {
121
311
  try {
122
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
312
+ const config = JSON.parse(configRaw);
123
313
  this.validateConfigStructure(config);
124
314
  this.info.push('✓ Found valid config.json');
125
315
  } catch (error) {
126
316
  this.errors.push({
127
317
  type: 'INVALID_CONFIG',
128
- path: 'conn-config/config.json',
318
+ path: 'config.json',
129
319
  message: `Invalid JSON in config.json: ${error.message}`,
130
320
  fix: 'Fix JSON syntax errors in config.json'
131
321
  });
132
322
  }
133
323
  }
134
324
 
135
- // 2. Validate conn-config/operations.json
136
- const operationsPath = path.join(this.serviceRoot, 'conn-config/operations.json');
137
- if (!fs.existsSync(operationsPath)) {
325
+ const opsRaw = readConfigFile(this.serviceRoot, 'operations.json');
326
+ if (!opsRaw) {
138
327
  this.errors.push({
139
328
  type: 'MISSING_OPERATIONS',
140
- path: 'conn-config/operations.json',
141
- message: 'Operations specification missing: conn-config/operations.json',
329
+ path: 'config/service/operations.json (or conn-config/operations.json)',
330
+ message: 'Operations specification missing: config/service/operations.json (or conn-config/operations.json)',
142
331
  fix: 'Create operations.json. See: /docs/standards/OPERATIONS.md'
143
332
  });
144
333
  } else {
145
334
  try {
146
- const operations = JSON.parse(fs.readFileSync(operationsPath, 'utf-8'));
335
+ const operations = JSON.parse(opsRaw);
147
336
  this.validateOperationsStructure(operations);
148
337
  this.info.push('✓ Found valid operations.json');
149
338
  } catch (error) {
150
339
  this.errors.push({
151
340
  type: 'INVALID_OPERATIONS',
152
- path: 'conn-config/operations.json',
341
+ path: 'operations.json',
153
342
  message: `Invalid JSON in operations.json: ${error.message}`,
154
343
  fix: 'Fix JSON syntax errors in operations.json'
155
344
  });
@@ -178,7 +367,7 @@ class ServiceStructureValidator {
178
367
  if (!value) {
179
368
  this.errors.push({
180
369
  type: 'MISSING_CONFIG_FIELD',
181
- path: 'conn-config/config.json',
370
+ path: 'config/service/config.json',
182
371
  field: field,
183
372
  message: `Required field missing in config.json: ${field}`,
184
373
  fix: `Add "${field}" to config.json`
@@ -191,7 +380,7 @@ class ServiceStructureValidator {
191
380
  if (!/^[a-z][a-z0-9-]*$/.test(config.service.name)) {
192
381
  this.warnings.push({
193
382
  type: 'INVALID_SERVICE_NAME',
194
- path: 'conn-config/config.json',
383
+ path: 'config/service/config.json',
195
384
  field: 'service.name',
196
385
  value: config.service.name,
197
386
  message: `Service name should be lowercase kebab-case: ${config.service.name}`,
@@ -227,7 +416,7 @@ class ServiceStructureValidator {
227
416
  if (!operations.operations) {
228
417
  this.errors.push({
229
418
  type: 'INVALID_OPERATIONS_STRUCTURE',
230
- path: 'conn-config/operations.json',
419
+ path: 'config/service/operations.json',
231
420
  message: 'operations.json must have "operations" key',
232
421
  fix: 'Wrap operations in {"operations": {...}}. See: /docs/standards/OPERATIONS.md'
233
422
  });
@@ -238,7 +427,7 @@ class ServiceStructureValidator {
238
427
  if (typeof ops !== 'object' || Array.isArray(ops)) {
239
428
  this.errors.push({
240
429
  type: 'INVALID_OPERATIONS_TYPE',
241
- path: 'conn-config/operations.json',
430
+ path: 'config/service/operations.json',
242
431
  message: 'operations must be an object',
243
432
  fix: 'operations should be key-value pairs: {"operation-name": {...}}'
244
433
  });
@@ -248,7 +437,7 @@ class ServiceStructureValidator {
248
437
  if (Object.keys(ops).length === 0) {
249
438
  this.warnings.push({
250
439
  type: 'NO_OPERATIONS',
251
- path: 'conn-config/operations.json',
440
+ path: 'config/service/operations.json',
252
441
  message: 'No operations defined',
253
442
  fix: 'Add at least one operation to operations.json'
254
443
  });
@@ -271,7 +460,7 @@ class ServiceStructureValidator {
271
460
  if (!spec[field]) {
272
461
  this.errors.push({
273
462
  type: 'MISSING_OPERATION_FIELD',
274
- path: 'conn-config/operations.json',
463
+ path: 'config/service/operations.json',
275
464
  operation: name,
276
465
  field: field,
277
466
  message: `Operation "${name}" missing required field: ${field}`,
@@ -284,7 +473,7 @@ class ServiceStructureValidator {
284
473
  if (spec.endpoint && !spec.endpoint.startsWith('/')) {
285
474
  this.errors.push({
286
475
  type: 'INVALID_ENDPOINT',
287
- path: 'conn-config/operations.json',
476
+ path: 'config/service/operations.json',
288
477
  operation: name,
289
478
  field: 'endpoint',
290
479
  value: spec.endpoint,
@@ -298,7 +487,7 @@ class ServiceStructureValidator {
298
487
  if (spec.method && !validMethods.includes(spec.method)) {
299
488
  this.errors.push({
300
489
  type: 'INVALID_HTTP_METHOD',
301
- path: 'conn-config/operations.json',
490
+ path: 'config/service/operations.json',
302
491
  operation: name,
303
492
  field: 'method',
304
493
  value: spec.method,
@@ -350,7 +539,8 @@ class ServiceStructureValidator {
350
539
  // Check for @onlineapps dependencies
351
540
  const requiredDeps = [
352
541
  '@onlineapps/service-wrapper',
353
- '@onlineapps/conn-orch-validator'
542
+ '@onlineapps/conn-orch-validator',
543
+ '@onlineapps/service-common'
354
544
  ];
355
545
 
356
546
  for (const dep of requiredDeps) {
@@ -391,6 +581,26 @@ class ServiceStructureValidator {
391
581
  });
392
582
  } else {
393
583
  this.info.push('✓ Found src/app.js');
584
+
585
+ try {
586
+ const appContent = fs.readFileSync(appPath, 'utf-8');
587
+ if (appContent.includes('businessErrorHandler')) {
588
+ this.info.push('✓ businessErrorHandler middleware registered');
589
+ } else {
590
+ this.warnings.push({
591
+ type: 'MISSING_ERROR_MIDDLEWARE',
592
+ path: 'src/app.js',
593
+ message: 'businessErrorHandler middleware not found in src/app.js',
594
+ fix: 'Register businessErrorHandler from @onlineapps/service-common as Express error middleware. See: /docs/standards/ERROR_HANDLING.md'
595
+ });
596
+ }
597
+ } catch (readError) {
598
+ this.warnings.push({
599
+ type: 'UNREADABLE_APP',
600
+ path: 'src/app.js',
601
+ message: `Could not read src/app.js: ${readError.message}`
602
+ });
603
+ }
394
604
  }
395
605
 
396
606
  const indexPath = path.join(this.serviceRoot, 'index.js');