Skip to content

Commit fff630b

Browse files
Copilothotlong
andauthored
fix: multi-adapter API route declaration & handler coverage systemic defects
- Discovery schema: add 'registered' status, handlerReady field, RouteHealthReportSchema - Dispatcher schema: add DispatcherErrorCode (404/405/501/503), DispatcherErrorResponseSchema, /health route - REST API plugin: add handlerStatus field to endpoints, RouteCoverageReportSchema - Runtime dispatcher: semantic 404/501/503 differentiation, /health handler, handlerReady in discovery - Dispatcher plugin: semantic error in sendResult, /health handler registration - Studio: filter service endpoints by handlerReady status (not just enabled) - CHANGELOG: document all changes Agent-Logs-Url: https://github.com/objectstack-ai/spec/sessions/808defd6-3fd9-4cb6-89a1-78d05f3e0b10 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 0a1afa7 commit fff630b

9 files changed

Lines changed: 585 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
- **Discovery Schema — `ServiceStatus` enum & `handlerReady` field** — Added `'registered'`
12+
status to `ServiceInfoSchema` to distinguish routes that are declared in the dispatcher
13+
table but whose HTTP handler has not been verified. Added `handlerReady` boolean field
14+
(default `false`) so clients can filter handler-ready services before displaying endpoints.
15+
- **Discovery Schema — `RouteHealthReportSchema`** — New schema for automated route/handler
16+
coverage reporting at startup. Includes per-route health entries (`pass`, `fail`, `missing`,
17+
`skip`) and summary counters.
18+
- **Dispatcher Schema — `DispatcherErrorCode` & `DispatcherErrorResponseSchema`** — Semantic
19+
error codes (`404`/`405`/`501`/`503`) with machine-readable `type` field
20+
(`ROUTE_NOT_FOUND`, `METHOD_NOT_ALLOWED`, `NOT_IMPLEMENTED`, `SERVICE_UNAVAILABLE`) and
21+
developer-facing `hint` strings.
22+
- **Dispatcher Schema — `/health` route** — Added health endpoint to `DEFAULT_DISPATCHER_ROUTES`.
23+
- **REST API Plugin — `handlerStatus` field** — Added `handlerStatus` (`implemented`, `stub`,
24+
`planned`) to `RestApiEndpointSchema` to track handler implementation readiness.
25+
- **REST API Plugin — `RouteCoverageReportSchema`** — Schema for adapter-generated coverage
26+
reports listing every declared endpoint and its implementation status.
27+
28+
### Fixed
29+
- **Runtime Dispatcher — semantic error differentiation**`HttpDispatcher.dispatch()` now
30+
returns typed 404 (`ROUTE_NOT_FOUND`) with diagnostic info instead of bare `{ handled: false }`.
31+
Added `notImplemented()` (501), `serviceUnavailable()` (503), and `routeNotFound()` (404)
32+
helper methods.
33+
- **Runtime Dispatcher — `/health` handler** — Added health endpoint returning `status`,
34+
`timestamp`, `version`, and `uptime`.
35+
- **Runtime Dispatcher — `handlerReady` in discovery**`getDiscoveryInfo()` now emits
36+
`handlerReady: true` for services with confirmed handlers and `handlerReady: false` for
37+
unavailable services.
38+
- **Dispatcher Plugin — semantic 404**`sendResult()` now returns `ROUTE_NOT_FOUND` error
39+
type with a hint pointing to the discovery endpoint. Added `/health` handler registration.
40+
- **Studio — handler-ready filtering**`useApiDiscovery()` now checks both `enabled` and
41+
`handlerReady` (or `status === 'available' | 'degraded'` for backward compatibility) before
42+
displaying service endpoints in the UI.
43+
1044
## [4.0.1] — 2026-03-31
1145

1246
### Fixed

apps/studio/src/hooks/use-api-discovery.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,15 +227,23 @@ export function useApiDiscovery() {
227227
// 2. Build service endpoints from discovery
228228
const serviceEndpoints: EndpointDef[] = [];
229229
for (const [serviceName, catalog] of Object.entries(SERVICE_ENDPOINT_CATALOG)) {
230-
const serviceInfo = discoveredServices[serviceName];
230+
const serviceInfo = discoveredServices[serviceName] as
231+
| { enabled: boolean; status?: string; handlerReady?: boolean; route?: string }
232+
| undefined;
233+
234+
// Only include services that are both enabled and have a handler ready.
235+
// Backwards-compatible: if handlerReady is not present (older backends),
236+
// fall back to status !== 'unavailable' && status !== 'stub' && status !== 'registered'.
231237
const isEnabled = serviceInfo?.enabled ?? false;
238+
const hasHandler = serviceInfo?.handlerReady
239+
?? (serviceInfo?.status === 'available' || serviceInfo?.status === 'degraded');
232240

233241
// Use route from discovery services, discovery routes map, or catalog default
234242
const routePrefix = serviceInfo?.route
235243
?? discoveredRoutes[serviceName]
236244
?? catalog.defaultRoute;
237245

238-
if (isEnabled) {
246+
if (isEnabled && hasHandler) {
239247
serviceEndpoints.push(...buildServiceEndpoints(serviceName, routePrefix));
240248
}
241249
}

packages/runtime/src/dispatcher-plugin.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ export interface DispatcherPluginConfig {
1212
}
1313

1414
/**
15-
* Send an HttpDispatcherResult through IHttpResponse
15+
* Send an HttpDispatcherResult through IHttpResponse.
16+
* Differentiates between handled, unhandled (404), and special results.
1617
*/
1718
function sendResult(result: HttpDispatcherResult, res: any): void {
1819
if (result.handled) {
@@ -32,7 +33,16 @@ function sendResult(result: HttpDispatcherResult, res: any): void {
3233
return;
3334
}
3435
}
35-
res.status(404).json({ success: false, error: { message: 'Not Found', code: 404 } });
36+
// Semantic 404: no route matched — include diagnostic info
37+
res.status(404).json({
38+
success: false,
39+
error: {
40+
message: 'Not Found',
41+
code: 404,
42+
type: 'ROUTE_NOT_FOUND',
43+
hint: 'No handler matched this request. Check GET /api/v1/discovery for available routes.',
44+
},
45+
});
3646
}
3747

3848
function errorResponse(err: any, res: any): void {
@@ -96,6 +106,16 @@ export function createDispatcherPlugin(config: DispatcherPluginConfig = {}): Plu
96106
res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
97107
});
98108

109+
// ── Health ──────────────────────────────────────────────────
110+
server.get(`${prefix}/health`, async (_req: any, res: any) => {
111+
try {
112+
const result = await dispatcher.dispatch('GET', '/health', undefined, {}, { request: _req });
113+
sendResult(result, res);
114+
} catch (err: any) {
115+
errorResponse(err, res);
116+
}
117+
});
118+
99119
// ── Auth ────────────────────────────────────────────────────
100120
server.post(`${prefix}/auth/login`, async (req: any, res: any) => {
101121
try {

packages/runtime/src/http-dispatcher.ts

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,67 @@ export class HttpDispatcher {
5959
};
6060
}
6161

62+
/**
63+
* 501 Not Implemented – route is declared but handler is a stub or missing.
64+
*/
65+
private notImplemented(route: string, service?: string) {
66+
return {
67+
status: 501,
68+
body: {
69+
success: false,
70+
error: {
71+
code: 501,
72+
message: `Not Implemented: ${route}`,
73+
type: 'NOT_IMPLEMENTED' as const,
74+
route,
75+
service,
76+
hint: service
77+
? `The "${service}" service route is declared but has no handler. Install or implement the handler.`
78+
: 'This endpoint is declared but not yet implemented.',
79+
},
80+
},
81+
};
82+
}
83+
84+
/**
85+
* 503 Service Unavailable – service exists in the route table but is currently down.
86+
*/
87+
private serviceUnavailable(route: string, service: string) {
88+
return {
89+
status: 503,
90+
body: {
91+
success: false,
92+
error: {
93+
code: 503,
94+
message: `Service Unavailable: ${service}`,
95+
type: 'SERVICE_UNAVAILABLE' as const,
96+
route,
97+
service,
98+
hint: `The "${service}" service is registered but currently unavailable. It may need to be started or a plugin installed.`,
99+
},
100+
},
101+
};
102+
}
103+
104+
/**
105+
* 404 Route Not Found — no route is registered for this path.
106+
*/
107+
private routeNotFound(route: string) {
108+
return {
109+
status: 404,
110+
body: {
111+
success: false,
112+
error: {
113+
code: 404,
114+
message: `Route Not Found: ${route}`,
115+
type: 'ROUTE_NOT_FOUND' as const,
116+
route,
117+
hint: 'No route is registered for this path. Check the API discovery endpoint for available routes.',
118+
},
119+
},
120+
};
121+
}
122+
62123
private ensureBroker() {
63124
if (!this.kernel.broker) {
64125
throw { statusCode: 500, message: 'Kernel Broker not available' };
@@ -133,11 +194,14 @@ export class HttpDispatcher {
133194
};
134195

135196
// Build per-service status map
197+
// handlerReady: true means the dispatcher has a real handler for this route.
198+
// handlerReady: false with status 'registered' means the route is in the table
199+
// but the handler may not exist or be a stub.
136200
const svcAvailable = (route?: string, provider?: string) => ({
137-
enabled: true, status: 'available' as const, route, provider,
201+
enabled: true, status: 'available' as const, handlerReady: true, route, provider,
138202
});
139203
const svcUnavailable = (name: string) => ({
140-
enabled: false, status: 'unavailable' as const,
204+
enabled: false, status: 'unavailable' as const, handlerReady: false,
141205
message: `Install a ${name} plugin to enable`,
142206
});
143207

@@ -174,7 +238,7 @@ export class HttpDispatcher {
174238
},
175239
services: {
176240
// Kernel-provided (always available via protocol implementation)
177-
metadata: { enabled: true, status: 'degraded' as const, route: routes.metadata, provider: 'kernel', message: 'In-memory registry; DB persistence pending' },
241+
metadata: { enabled: true, status: 'degraded' as const, handlerReady: true, route: routes.metadata, provider: 'kernel', message: 'In-memory registry; DB persistence pending' },
178242
data: svcAvailable(routes.data, 'kernel'),
179243
// Plugin-provided — only available when a plugin registers the service
180244
auth: hasAuth ? svcAvailable(routes.auth) : svcUnavailable('auth'),
@@ -1207,6 +1271,19 @@ export class HttpDispatcher {
12071271
};
12081272
}
12091273

1274+
// 0b. Health Endpoint (GET /health)
1275+
if (cleanPath === '/health' && method === 'GET') {
1276+
return {
1277+
handled: true,
1278+
response: this.success({
1279+
status: 'ok',
1280+
timestamp: new Date().toISOString(),
1281+
version: '1.0.0',
1282+
uptime: typeof process !== 'undefined' ? process.uptime() : undefined,
1283+
}),
1284+
};
1285+
}
1286+
12101287
// 1. System Protocols (Prefix-based)
12111288
if (cleanPath.startsWith('/auth')) {
12121289
return this.handleAuth(cleanPath.substring(5), method, body, context);
@@ -1265,8 +1342,11 @@ export class HttpDispatcher {
12651342
const result = await this.handleApiEndpoint(cleanPath, method, body, query, context);
12661343
if (result.handled) return result;
12671344

1268-
// 3. Fallback (404)
1269-
return { handled: false };
1345+
// 3. Fallback — return semantic 404 with diagnostic info
1346+
return {
1347+
handled: true,
1348+
response: this.routeNotFound(cleanPath),
1349+
};
12701350
}
12711351

12721352
/**

0 commit comments

Comments
 (0)