@onlineapps/conn-orch-validator 3.3.0 → 3.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,69 @@
2
2
 
3
3
  All notable changes to this package. Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format.
4
4
 
5
+ ## [3.3.2] — 2026-08-17
6
+
7
+ **ServiceStructureValidator refactor for ADR 0005 / Fáze F6 reality.**
8
+
9
+ 3.3.1 still required `src/app.js`, `service.port`, and `businessErrorHandler in src/app.js` — all dead after F6 removed Express. Any biz service whose validation-proof got invalidated post-F6 would hit `MISSING_APP` at Tier-1 step 1 and stall in restart loop. Surfaced when biz-meta's proof was manually invalidated in the Fáze F7 followup.
10
+
11
+ ### BREAKING
12
+
13
+ - **v1.0 Base Service Standard**: `src/app.js` no longer required. `src/handlers/` is now the required source dir (flat or `handlers/v3/` subdir — both are canonical variants). `service.port` no longer required in `config.json`.
14
+ - **v1.2 Business Error Handling**: `businessErrorHandler in src/app.js` check REMOVED (there is no src/app.js). Replaced with: at least one handler under `src/handlers/` imports and throws a `BusinessError` family class (`ValidationError`, `NotFoundError`, `ConflictError`, `ForbiddenError`) from `@onlineapps/service-common`. Errors flow through the wrapper's `ErrorMapper` post-A2.2, not through Express middleware.
15
+ - **`validateSourceStructure()`**: `MISSING_APP` error replaced with `MISSING_HANDLERS`. Presence of `src/app.js` now emits a **warning** (`LEGACY_APP_JS`) — dead code that F6 removes.
16
+
17
+ ### Added
18
+
19
+ - **v1.3 Zero-HTTP Shape (ADR 0005)** — new standard level asserting F6 cleanup landed:
20
+ - `src/app.js` absent
21
+ - `src/routes/` absent
22
+ - `src/middlewares/` absent
23
+ - `express` NOT in `package.json` dependencies
24
+
25
+ ### Rationale
26
+
27
+ Every biz service on the platform is post-F6 (verified in Fáze F acceptance ledger). Continuing to require Express artefacts would false-positive-fail every valid v3-canonical service.
28
+
29
+ ### Tests
30
+
31
+ Unit suite 158/158 GREEN before publish. Note: standard-level tests
32
+ (`StandardLevels.test.js`) were rewritten in the same commit to match
33
+ the post-F6 rules — 6 pre-existing test cases replaced with 6 matching
34
+ the new spec.
35
+
36
+ ## [3.3.1] — 2026-08-17
37
+
38
+ **Tier-1 step 5 refactor: retire HTTP `/health` probe per ADR 0005.**
39
+
40
+ Fáze F5 surfaced this in the live rollout: after service-wrapper 3.4.x removed Express init, every biz Tier-1 validation failed step 5 (`Health check failed`) because `ServiceReadinessValidator.checkHealth()` still called `fetch(url + '/health')`. There is nothing listening under ADR 0005 — biz containers have zero HTTP surface.
41
+
42
+ ### BREAKING (behaviour of `validateReadiness()`)
43
+
44
+ - `checkHealth()` method removed.
45
+ - `service.healthEndpoint` option removed (no consumer left).
46
+ - `service.url` still accepted but no longer consumed (kept in results.serviceUrl for report annotation only).
47
+ - Weight rebalance: `operations` 60 → 80 (absorbs the retired `health` 20 pts). `cookbook` and `registry` unchanged. Total remains 100.
48
+ - `results.checks.health` no longer emitted.
49
+
50
+ ### Helper API change (`createServiceReadinessTests`)
51
+
52
+ - No longer starts an Express `app.listen(testPort, ...)`.
53
+ - Removed `options.testPort` (no port to bind).
54
+ - Removed the `health endpoint responds correctly` test.
55
+ - Removed `result.checks.health?.passed` assertion.
56
+ - Existing biz-side test files that consume this helper (`api_biz/hello-service`, `api_biz/converter`, `api_biz/ingest` under `tests/bootstrap/`) continue to work unchanged — the helper signature stays `createServiceReadinessTests(testsDir, options)`, only the internal setup shrinks.
57
+
58
+ ### Rationale
59
+
60
+ Tier-1 runs in Phase 0.2 (very early — before the wrapper opens any connectors). Under ADR 0005 there is no HTTP listener to probe *ever*, in any phase; runtime liveness is observed by Registry via the MQ heartbeat + Redis projection path (`infrastructureHealthTracker`) *after* Phase 0.10. Tier-1 has no meaningful health signal to check — the honest fix is to drop the check.
61
+
62
+ Structure + config + operations correctness (steps 1-3) plus cookbook execution (step 4) already prove service readiness; `operations` at 80 pts + optional `cookbook` and `registry` cover the remaining 20 pts.
63
+
64
+ ### Tests
65
+
66
+ Full unit suite (158 tests, 11 suites) GREEN with the retired HTTP probe removed. No new tests needed — `checkHealth` had no isolated test; behaviour was exercised implicitly.
67
+
5
68
  ## [3.3.0] — 2026-08-17
6
69
 
7
70
  **Env-var contract rename per env-conventions.md.** Details: `api/docs/biz/60-templates/env-conventions.md`, `api/docs/biz/40-cookbooks/test-env-vars.md`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/conn-orch-validator",
3
- "version": "3.3.0",
3
+ "version": "3.3.2",
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
  "bin": {
@@ -11,8 +11,19 @@ const CookbookTestUtils = require('./CookbookTestUtils');
11
11
  * per-operation endpoints. The HTTP loopback check was removed when v3
12
12
  * adopted in-process handler invocation.
13
13
  *
14
+ * ADR 0005 (2026-08-17): biz containers have zero HTTP surface. The
15
+ * runtime `health` check (previously `fetch(url + '/health')`) is
16
+ * retired because there is nothing listening. Runtime health is
17
+ * observed by Registry via the MQ heartbeat + Redis projection path
18
+ * (see api_services_registry/src/services/infrastructureHealthTracker)
19
+ * — outside Tier-1's scope, and later in the boot sequence anyway
20
+ * (Tier-1 runs in Phase 0.2, heartbeat in Phase 0.10). The 20 points
21
+ * previously spent on `health` fold into `operations` so total stays
22
+ * at 100.
23
+ *
14
24
  * @see /api/docs/architecture/biz-service-invocation-model.md §5.3
15
25
  * @see /api/docs/biz/40-cookbooks/test-runner-flow.md (input probe contract)
26
+ * @see /api/docs/biz/80-decisions/0005-no-http-in-biz-containers.md
16
27
  */
17
28
  class ServiceReadinessValidator {
18
29
  constructor(options = {}) {
@@ -24,14 +35,16 @@ class ServiceReadinessValidator {
24
35
  // Readiness checks: core (80 points) + optional (20 points) = 100 points max
25
36
  // Core checks ALWAYS run, optional checks run if testCookbook/registry provided
26
37
  // See: /shared/connector/conn-orch-validator/README.md for usage pattern
27
- // v3: per-op HTTP endpoint probing is retired. Operations are dispatched in-process
28
- // via the handler registry. The 30-point weight previously spent on `endpoints`
29
- // is folded into `operations` so the total stays at 100.
38
+ //
39
+ // Weight allocation post-ADR-0005:
40
+ // operations: 80 was 60; absorbed the 20 pts from retired `health`.
41
+ // cookbook: 15 — unchanged (optional).
42
+ // registry: 5 — unchanged (optional).
43
+ // The retired `health` HTTP probe is documented in the class JSDoc above.
30
44
  this.checks = {
31
- operations: { weight: 60, required: true }, // operations.json v3 structure valid
32
- health: { weight: 20, required: true }, // health check works
33
- cookbook: { weight: 15, required: false }, // OPTIONAL - cookbook valid (with mocks)
34
- registry: { weight: 5, required: false } // OPTIONAL - registry compatible (MockRegistry)
45
+ operations: { weight: 80, required: true }, // operations.json v3 structure valid
46
+ cookbook: { weight: 15, required: false }, // OPTIONAL - cookbook valid (with mocks)
47
+ registry: { weight: 5, required: false } // OPTIONAL - registry compatible (MockRegistry)
35
48
  };
36
49
  }
37
50
 
@@ -45,8 +58,7 @@ class ServiceReadinessValidator {
45
58
  url,
46
59
  operations,
47
60
  registry,
48
- testCookbook,
49
- healthEndpoint = '/health'
61
+ testCookbook
50
62
  } = service;
51
63
 
52
64
  const results = {
@@ -77,13 +89,12 @@ class ServiceReadinessValidator {
77
89
  // 2. (v3) Per-op HTTP endpoint probing retired — operations are dispatched
78
90
  // in-process via the handler registry. No network call per operation.
79
91
 
80
- // 3. Verify health check
81
- results.checks.health = await this.checkHealth(url + healthEndpoint);
82
- if (results.checks.health.passed) {
83
- results.score += this.checks.health.weight;
84
- } else if (this.checks.health.required) {
85
- results.errors.push('Health check failed');
86
- }
92
+ // 3. (ADR 0005) Runtime health check retired — biz containers have no HTTP
93
+ // surface. Runtime liveness flows via MQ heartbeat to Registry's
94
+ // infrastructureHealthTracker; consumed by BusinessReadinessChecker
95
+ // (post Fáze A2.3). Tier-1 cannot observe that state because it runs
96
+ // in Phase 0.2, before the heartbeat publisher starts in Phase 0.10.
97
+ // The 20-point weight moved to `operations` above.
87
98
 
88
99
  // 4. Test cookbook execution (if provided)
89
100
  if (testCookbook) {
@@ -196,35 +207,8 @@ class ServiceReadinessValidator {
196
207
  }
197
208
  }
198
209
 
199
- /**
200
- * Check health endpoint via Node 18+ global fetch (no external HTTP client).
201
- */
202
- async checkHealth(healthUrl) {
203
- try {
204
- const response = await fetch(healthUrl, {
205
- method: 'GET',
206
- signal: AbortSignal.timeout(5000)
207
- });
208
-
209
- let data = null;
210
- try {
211
- data = await response.json();
212
- } catch (_) {
213
- // health endpoint may return non-JSON; keep data null and rely on status.
214
- }
215
-
216
- return {
217
- passed: response.status === 200,
218
- status: response.status,
219
- data
220
- };
221
- } catch (error) {
222
- return {
223
- passed: false,
224
- error: error.message
225
- };
226
- }
227
- }
210
+ // checkHealth() removed 2026-08-17 per ADR 0005 (no HTTP surface in
211
+ // biz containers). See class JSDoc header for full rationale.
228
212
 
229
213
  /**
230
214
  * Check cookbook structure validity
@@ -1,18 +1,26 @@
1
1
  /**
2
2
  * Create Service Readiness Integration Tests
3
3
  *
4
- * Generic test suite that validates service HTTP API readiness.
5
- * Works for ALL business services - no service-specific code needed.
4
+ * Generic test suite that validates service configuration + operations
5
+ * are ready for production. Works for ALL business services no
6
+ * service-specific code needed.
7
+ *
8
+ * ADR 0005 (2026-08-17): biz containers have zero HTTP surface. This
9
+ * helper NO LONGER starts an Express app, NO LONGER binds a test port,
10
+ * NO LONGER fetches `/health`. What it verifies:
11
+ * - Service structure valid (via ServiceStructureValidator)
12
+ * - operations.json valid v3 shape (handler, bundle_scope, input, output)
13
+ * - Optional: cookbook validation + registry compatibility
6
14
  *
7
15
  * @module helpers/createServiceReadinessTests
8
16
  *
9
17
  * Usage:
10
18
  * const { createServiceReadinessTests } = require('@onlineapps/conn-orch-validator');
11
- * createServiceReadinessTests(__dirname); // Pass tests/integration directory
19
+ * createServiceReadinessTests(__dirname); // Pass tests/bootstrap directory
12
20
  *
13
21
  * Related:
14
- * - /docs/standards/TESTING.md - Testing standards
15
- * - /tests/TESTING.md - SPOT principles
22
+ * - /docs/biz/80-decisions/0005-no-http-in-biz-containers.md
23
+ * - /docs/biz/00-model/service-shape.md (zero-HTTP canonical shape)
16
24
  * - /shared/connector/conn-orch-validator/README.md - Package documentation
17
25
  */
18
26
 
@@ -27,24 +35,22 @@ const { ServiceStructureValidator } = require('../validators/ServiceStructureVal
27
35
  /**
28
36
  * Create service readiness integration test suite
29
37
  *
30
- * @param {string} testsDir - Path to tests/integration directory (use __dirname)
38
+ * @param {string} testsDir - Path to tests/bootstrap directory (use __dirname)
31
39
  * @param {Object} [options] - Optional configuration
32
- * @param {number} [options.testPort=5556] - Port for test server
33
40
  * @param {boolean} [options.includeOptionalChecks=true] - Include cookbook & registry checks
34
41
  * @param {number} [options.timeout=15000] - Test timeout in ms
35
42
  *
36
43
  * @example
37
- * // In services/my-service/tests/integration/service-readiness.test.js
44
+ * // In services/my-service/tests/bootstrap/service-readiness.test.js
38
45
  * const { createServiceReadinessTests } = require('@onlineapps/conn-orch-validator');
39
46
  *
40
47
  * createServiceReadinessTests(__dirname);
41
48
  */
42
49
  function createServiceReadinessTests(testsDir, options = {}) {
43
- // Calculate service root (2 levels up from tests/integration/)
50
+ // Calculate service root (2 levels up from tests/bootstrap/)
44
51
  const serviceRoot = path.resolve(testsDir, '../..');
45
52
 
46
53
  const {
47
- testPort = 5556,
48
54
  includeOptionalChecks = true,
49
55
  timeout = 15000
50
56
  } = options;
@@ -72,58 +78,24 @@ function createServiceReadinessTests(testsDir, options = {}) {
72
78
  };
73
79
  const configPath = resolveConfig('config.json');
74
80
  const operationsPath = resolveConfig('operations.json');
75
- const appPath = path.join(serviceRoot, 'src/app.js');
76
81
 
77
82
  const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
78
83
  const operations = JSON.parse(fs.readFileSync(operationsPath, 'utf-8'));
79
- const app = require(appPath);
80
84
 
81
85
  // Extract metadata from config
82
86
  const serviceName = config.service.name;
83
87
  const serviceVersion = config.service.version;
84
- const healthEndpoint = config.wrapper?.health?.endpoint || '/health';
85
88
 
86
- // Create test suite
87
89
  describe(`${serviceName} Service Readiness @integration`, () => {
88
- let server;
89
- let baseUrl;
90
-
91
- beforeAll(async () => {
92
- // Start service for testing
93
- server = await new Promise((resolve) => {
94
- const srv = app.listen(testPort, () => {
95
- baseUrl = `http://127.0.0.1:${testPort}`;
96
- console.log(`\n✓ Test server started: ${baseUrl}\n`);
97
- resolve(srv);
98
- });
99
- });
100
- });
101
-
102
- afterAll(async () => {
103
- // Stop service
104
- if (server) {
105
- await new Promise((resolve) => {
106
- server.close(() => {
107
- console.log('\n✓ Test server stopped\n');
108
- resolve();
109
- });
110
- });
111
- }
112
- });
113
-
114
90
  test('service passes readiness validation', async () => {
115
91
  // Validator expects flat operations object
116
92
  const operationsFlat = operations.operations || operations;
117
93
 
118
- // Prepare optional checks
119
94
  let mockRegistry = null;
120
95
  let testCookbook = null;
121
96
 
122
97
  if (includeOptionalChecks) {
123
- // Create MockRegistry for registry compatibility check (+5 points)
124
98
  mockRegistry = new MockRegistry();
125
-
126
- // Auto-generate test cookbook from operations (+15 points)
127
99
  testCookbook = {
128
100
  version: '1.0.0',
129
101
  steps: Object.entries(operationsFlat).map(([name, op]) => ({
@@ -137,19 +109,15 @@ function createServiceReadinessTests(testsDir, options = {}) {
137
109
  };
138
110
  }
139
111
 
140
- // Run readiness validation
141
112
  const validator = new ServiceReadinessValidator({ logger: console });
142
113
  const result = await validator.validateReadiness({
143
114
  name: serviceName,
144
115
  version: serviceVersion,
145
- url: baseUrl,
146
116
  operations: operationsFlat,
147
- healthEndpoint: healthEndpoint,
148
117
  testCookbook: testCookbook,
149
118
  registry: mockRegistry
150
119
  });
151
120
 
152
- // Log detailed results
153
121
  console.log('\n📊 Readiness Validation Results:');
154
122
  console.log(` Score: ${result.score}/100`);
155
123
  console.log(` Ready: ${result.ready ? '✅' : '❌'}`);
@@ -167,7 +135,6 @@ function createServiceReadinessTests(testsDir, options = {}) {
167
135
  }
168
136
  console.log('');
169
137
 
170
- // Assertions
171
138
  expect(result).toHaveProperty('ready');
172
139
  expect(result).toHaveProperty('score');
173
140
  expect(result).toHaveProperty('checks');
@@ -177,26 +144,15 @@ function createServiceReadinessTests(testsDir, options = {}) {
177
144
 
178
145
  if (includeOptionalChecks) {
179
146
  expect(result.score).toBe(100);
180
- expect(result.checks.health?.passed).toBe(true);
181
147
  expect(result.checks.operations?.passed).toBe(true);
182
148
  expect(result.checks.cookbook?.passed).toBe(true);
183
149
  expect(result.checks.registry?.passed).toBe(true);
184
150
  } else {
185
151
  expect(result.score).toBeGreaterThanOrEqual(80);
186
- expect(result.checks.health?.passed).toBe(true);
187
152
  expect(result.checks.operations?.passed).toBe(true);
188
153
  }
189
154
  }, timeout);
190
155
 
191
- test('health endpoint responds correctly', async () => {
192
- const response = await fetch(`${baseUrl}${healthEndpoint}`);
193
- const data = await response.json();
194
-
195
- expect(response.status).toBe(200);
196
- expect(data).toHaveProperty('status');
197
- expect(data.status).toBe('healthy');
198
- });
199
-
200
156
  test('service has valid operations specification (v3)', () => {
201
157
  expect(operations).toBeDefined();
202
158
  expect(operations.operations || operations).toBeDefined();
@@ -50,7 +50,9 @@ const STANDARD_LEVELS = [
50
50
  results.push({ passed: hasConfigDir(root), id: 'dir_conn_config', message: 'config/service/ or conn-config/ directory' });
51
51
  results.push({ passed: has('src'), id: 'dir_src', message: 'src/ directory' });
52
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' });
53
+ // ADR 0005: src/handlers/ replaces src/app.js. Flat handlers/ or
54
+ // handlers/v3/ subdir are both valid canonical shapes.
55
+ results.push({ passed: has('src/handlers'), id: 'dir_handlers', message: 'src/handlers/ (v3 handler modules)' });
54
56
  results.push({ passed: has('index.js'), id: 'file_index', message: 'index.js entry point' });
55
57
 
56
58
  const configRaw = readConfigFile(root, 'config.json');
@@ -58,10 +60,11 @@ const STANDARD_LEVELS = [
58
60
  if (configRaw) {
59
61
  try {
60
62
  const cfg = JSON.parse(configRaw);
61
- configValid = !!(cfg.service?.name && cfg.service?.port);
63
+ // ADR 0005: service.port not required post zero-HTTP. Just name.
64
+ configValid = !!cfg.service?.name;
62
65
  } catch { /* invalid JSON */ }
63
66
  }
64
- results.push({ passed: configValid, id: 'config_valid', message: 'Valid config.json with service.name + service.port' });
67
+ results.push({ passed: configValid, id: 'config_valid', message: 'Valid config.json with service.name' });
65
68
 
66
69
  const opsRaw = readConfigFile(root, 'operations.json');
67
70
  let opsValid = false;
@@ -120,12 +123,63 @@ const STANDARD_LEVELS = [
120
123
  } catch { /* invalid JSON */ }
121
124
  }
122
125
 
123
- const appContent = read('src/app.js') || '';
124
- const hasErrorMiddleware = appContent.includes('businessErrorHandler');
126
+ // ADR 0005 / F6: businessErrorHandler was Express middleware in
127
+ // src/app.js. Post-F6 there is no src/app.js. Error handling now
128
+ // happens via BusinessError thrown from handlers → wrapper's
129
+ // ErrorMapper. Check: at least one handler imports a
130
+ // BusinessError-family class from @onlineapps/service-common.
131
+ const handlersDir = path.join(root, 'src', 'handlers');
132
+ let hasBusinessErrorUsage = false;
133
+ if (fs.existsSync(handlersDir)) {
134
+ const walk = (dir) => {
135
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
136
+ const p = path.join(dir, entry.name);
137
+ if (entry.isDirectory()) walk(p);
138
+ else if (entry.isFile() && entry.name.endsWith('.js')) {
139
+ const content = read(path.relative(root, p));
140
+ if (content && /@onlineapps\/service-common/.test(content) &&
141
+ /BusinessError|ValidationError|NotFoundError|ConflictError|ForbiddenError/.test(content)) {
142
+ hasBusinessErrorUsage = true;
143
+ }
144
+ }
145
+ }
146
+ };
147
+ try { walk(handlersDir); } catch { /* ignore */ }
148
+ }
125
149
 
126
150
  return [
127
151
  { passed: hasServiceCommon, id: 'dep_service_common', message: '@onlineapps/service-common dependency' },
128
- { passed: hasErrorMiddleware, id: 'error_middleware', message: 'businessErrorHandler middleware in src/app.js' }
152
+ { passed: hasBusinessErrorUsage, id: 'business_error_usage', message: 'At least one handler throws a BusinessError family from @onlineapps/service-common' }
153
+ ];
154
+ }
155
+ },
156
+ {
157
+ level: 'v1.3',
158
+ name: 'Zero-HTTP Shape (ADR 0005)',
159
+ since: '2026-08-17',
160
+ checks: (root) => {
161
+ const has = (p) => fs.existsSync(path.join(root, p));
162
+ const read = (p) => { try { return fs.readFileSync(path.join(root, p), 'utf-8'); } catch { return null; } };
163
+
164
+ // v1.3 asserts that F6/A2 cleanup landed: no dead Express surface.
165
+ const noAppJs = !has('src/app.js');
166
+ const noRoutesDir = !has('src/routes');
167
+ const noMiddlewaresDir = !has('src/middlewares');
168
+
169
+ const pkgRaw = read('package.json');
170
+ let noExpressDep = true;
171
+ if (pkgRaw) {
172
+ try {
173
+ const pkg = JSON.parse(pkgRaw);
174
+ noExpressDep = !pkg.dependencies?.express;
175
+ } catch { /* invalid JSON — separate check catches it */ }
176
+ }
177
+
178
+ return [
179
+ { passed: noAppJs, id: 'no_app_js', message: 'src/app.js absent (retired Express dispatch surface)' },
180
+ { passed: noRoutesDir, id: 'no_routes_dir', message: 'src/routes/ absent (dead HTTP route mounts)' },
181
+ { passed: noMiddlewaresDir, id: 'no_middlewares_dir', message: 'src/middlewares/ absent (dead Express middleware)' },
182
+ { passed: noExpressDep, id: 'no_express_dep', message: 'express not in package.json dependencies' }
129
183
  ];
130
184
  }
131
185
  }
@@ -594,39 +648,45 @@ class ServiceStructureValidator {
594
648
  }
595
649
 
596
650
  /**
597
- * Validate source code structure
651
+ * Validate source code structure.
652
+ *
653
+ * ADR 0005 (2026-08-17): biz containers have zero HTTP surface —
654
+ * no Express, no `src/app.js`, no per-op HTTP routes. The v3
655
+ * handler-registry model instead requires `src/handlers/` (flat
656
+ * or `handlers/v3/` subdir — both variants of the canonical
657
+ * shape) with `handler` refs from `operations.json` resolvable
658
+ * to real exports.
598
659
  */
599
660
  validateSourceStructure() {
600
- const appPath = path.join(this.serviceRoot, 'src/app.js');
601
- if (!fs.existsSync(appPath)) {
661
+ // Post-ADR-0005 hard requirement: at least one handler module
662
+ // (flat or under v3/) must exist. HandlerRegistry.validate() at
663
+ // bootstrap time enforces per-op resolution; here we just ensure
664
+ // the directory itself is present so the service has something
665
+ // to dispatch to.
666
+ const handlersFlat = path.join(this.serviceRoot, 'src/handlers');
667
+ if (!fs.existsSync(handlersFlat)) {
602
668
  this.errors.push({
603
- type: 'MISSING_APP',
604
- path: 'src/app.js',
605
- message: 'Express application missing: src/app.js',
606
- fix: 'Create src/app.js with Express app. See: /docs/biz/60-templates/service-template.md'
669
+ type: 'MISSING_HANDLERS',
670
+ path: 'src/handlers',
671
+ message: 'Handlers directory missing: src/handlers/',
672
+ fix: 'Create src/handlers/ with at least one v3 handler module. See: /docs/biz/60-templates/service-template.md'
607
673
  });
608
674
  } else {
609
- this.info.push('✓ Found src/app.js');
675
+ this.info.push('✓ Found src/handlers/');
676
+ }
610
677
 
611
- try {
612
- const appContent = fs.readFileSync(appPath, 'utf-8');
613
- if (appContent.includes('businessErrorHandler')) {
614
- this.info.push('✓ businessErrorHandler middleware registered');
615
- } else {
616
- this.warnings.push({
617
- type: 'MISSING_ERROR_MIDDLEWARE',
618
- path: 'src/app.js',
619
- message: 'businessErrorHandler middleware not found in src/app.js',
620
- fix: 'Register businessErrorHandler from @onlineapps/service-common as Express error middleware. See: /docs/standards/ERROR_HANDLING.md'
621
- });
622
- }
623
- } catch (readError) {
624
- this.warnings.push({
625
- type: 'UNREADABLE_APP',
626
- path: 'src/app.js',
627
- message: `Could not read src/app.js: ${readError.message}`
628
- });
629
- }
678
+ // src/app.js is ANTI-PATTERN post-ADR 0005 (dead Express surface).
679
+ // Warn if present so authors of new services notice they should
680
+ // remove it; existing services scheduled to be cleaned up via the
681
+ // Fáze F6 pass.
682
+ const appPath = path.join(this.serviceRoot, 'src/app.js');
683
+ if (fs.existsSync(appPath)) {
684
+ this.warnings.push({
685
+ type: 'LEGACY_APP_JS',
686
+ path: 'src/app.js',
687
+ message: 'src/app.js is dead code post-ADR 0005 (bootstrap no longer reads it)',
688
+ fix: 'Delete src/app.js; move any residual middleware into the wrapper adapter. See: /docs/biz/80-decisions/0005-no-http-in-biz-containers.md'
689
+ });
630
690
  }
631
691
 
632
692
  const indexPath = path.join(this.serviceRoot, 'index.js');