docs: clean up ADR-005 architecture docs after envelope implementation

Remove stale ADR-005 drift tables across all architecture docs since
ResponseEnvelope types, factories, detection, and integration points
are now fully implemented in source code. Key changes:

- api-surface.md: Remove ADR-005 drift table (all items implemented),
  retain ADR-006 drift table without execute() return type (now done)
- call-protocol.md: Remove ADR-005 drift table, update ADR-006 table,
  fix CallHandlerConfig to show callMap? (current source)
- adapters.md: Remove 'current source state' and 'implementation
  changes needed' tables for from_mcp and from_openapi, replace with
  current-accurate descriptions of envelope behavior
- response-envelopes.md: Remove 'current source state' blocks,
  update migration checklist to show all code changes completed
- 005-response-envelopes.md: Change status from Draft to Implemented
- 006-unified-invocation-path.md: Update Prerequisites section to note
  ADR-005 is now implemented
- build-distribution.md: Add response-envelope.ts to source layout
- architecture.md: Add response-envelopes.md link and ADR-005/006
  entries to design decisions table
- README.md: Add response-envelopes.md to documents table
- Update last_updated dates on all changed docs
This commit is contained in:
2026-05-11 02:55:13 +00:00
parent d74b750ecb
commit ddc0607b90
9 changed files with 40 additions and 107 deletions

View File

@@ -7,6 +7,7 @@
| [architecture/README.md](architecture/README.md) | Overview, why this exists, what it provides, consumer context, threat model |
| [architecture/api-surface.md](architecture/api-surface.md) | All public types, registry API, call protocol API, subscribe, env, adapters |
| [architecture/call-protocol.md](architecture/call-protocol.md) | PendingRequestMap, CallHandler, call≡subscribe semantics, events, error model, access control |
| [architecture/response-envelopes.md](architecture/response-envelopes.md) | Response envelope types, factory functions, detection, schemas, integration points |
| [architecture/adapters.md](architecture/adapters.md) | from_schema, from_openapi, from_mcp, scanner — how they work, how to add new adapters |
| [architecture/build-distribution.md](architecture/build-distribution.md) | Dependencies, project structure, sub-path exports, peer deps, build tooling |
@@ -17,4 +18,6 @@
| [001](architecture/decisions/001-logger-direct-import.md) | Direct @logtape/logtape import instead of wrapper module |
| [002](architecture/decisions/002-fs-injection.md) | Inject filesystem dependencies for runtime agnosticism |
| [003](architecture/decisions/003-peer-dep-adapters.md) | Peer dependencies for adapter isolation (MCP SDK, @std/path) |
| [004](architecture/decisions/004-schema-const-naming.md) | Schema const naming convention (AccessControlSchema + AccessControl type) |
| [004](architecture/decisions/004-schema-const-naming.md) | Schema const naming convention (AccessControlSchema + AccessControl type) |
| [005](architecture/decisions/005-response-envelopes.md) | Response Envelopes for transport-aware results |
| [006](architecture/decisions/006-unified-invocation-path.md) | Unified Invocation Path (execute as single entry point) |

View File

@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-05-09
last_updated: 2026-05-11
---
# @alkdev/operations Architecture
@@ -74,6 +74,7 @@ Spokes will import `@alkdev/operation` for operation definitions and `@alkdev/pu
|----------|---------|
| [api-surface.md](api-surface.md) | All public types, registry, call protocol, subscribe, env, adapters |
| [call-protocol.md](call-protocol.md) | PendingRequestMap, CallHandler, call≡subscribe, events, error model, access control |
| [response-envelopes.md](response-envelopes.md) | Response envelope types, factory functions, detection, schemas, integration points |
| [adapters.md](adapters.md) | from_schema, from_openapi, from_mcp, scanner — how they work, how to add new adapters |
| [build-distribution.md](build-distribution.md) | Dependencies, project structure, sub-path exports, peer deps, build tooling |

View File

@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-05-09
last_updated: 2026-05-11
---
# Adapters
@@ -79,13 +79,7 @@ Processes all paths in the spec. For each path and method combination:
- Applies auth headers from config
- Returns JSON, text, or `ArrayBuffer` based on response content type
**Current source state** (`src/from_openapi.ts`): The handler currently returns raw response data — `response.json()`, `response.text()`, or `response.arrayBuffer()` (lines 273-279). It does NOT wrap the result in `httpEnvelope()`. Error handling throws a plain `Error` (line 268) instead of `CallError`. The response-envelopes spec requires wrapping in `httpEnvelope()` and throwing `CallError` on HTTP errors. The `Value.Cast()` normalization step against `outputSchema` is also not yet implemented. See [response-envelopes.md](response-envelopes.md) for the full specification.
| What | Current source (`src/from_openapi.ts`) | Target (per response-envelopes spec) |
|------|------------------------------------------|---------------------------------------|
| Handler return value | Returns raw `response.json()` / `.text()` / `.arrayBuffer()` (lines 273-279) | Returns `httpEnvelope(data, { statusCode, headers, contentType })` |
| Error handling | Throws `Error(\`HTTP ${status}\`)` (line 268) | Throws `CallError("EXECUTION_ERROR", ...)` |
| `Value.Cast()` | Not used | If `outputSchema !== Unknown`, cast `Value.Cast(outputSchema, data)` |
The handler wraps results in `httpEnvelope()` with HTTP metadata (status code, headers, content type). On HTTP error status, it throws `CallError("EXECUTION_ERROR", ...)`. `Value.Cast()` normalization against `outputSchema` is applied by `registry.execute()` and `CallHandler` as part of the shared result pipeline — see [response-envelopes.md](response-envelopes.md#shared-result-pipeline).
### `FromOpenAPIFile(path, config, fs?)`
@@ -179,11 +173,11 @@ async function createMCPClient(
- `namespace`: the `name` parameter (used as grouping)
- `type`: `MUTATION` (all MCP tools are mutations)
### `FromSchema(tool.inputSchema)` (converts JSON Schema to TypeBox)
- `outputSchema`: `tool.outputSchema ? FromSchema(tool.outputSchema) : Type.Unknown()` (MCP spec 2025-06-18+ provides `outputSchema`; older tools lack it)
- `handler`: calls `client.callTool({ name, arguments })`, wraps result in `mcpEnvelope()`
- `accessControl`: `{ requiredScopes: [] }` (no auth by default)
- `outputSchema`: `tool.outputSchema ? FromSchema(tool.outputSchema) : Type.Unknown()` (MCP spec 2025-06-18+ provides `outputSchema`; older tools lack it)
- `handler`: calls `client.callTool({ name, arguments })`, wraps result in `mcpEnvelope()`
- `accessControl`: `{ requiredScopes: [] }` (no auth by default)
**Current source state** (`src/from_mcp.ts` line 66): `outputSchema: Type.Unknown()` for all tools. The `tool.outputSchema` property is not used. The handler currently throws on `isError` (line 76) and returns `result.content` directly (line 79) instead of wrapping in `mcpEnvelope()`. The `structuredContent` field on `CallToolResult` is not used. See [response-envelopes.md](response-envelopes.md) for the full specification of what needs to change.
The handler returns pre-built `ResponseEnvelope` instances via `mcpEnvelope()`. `isError: true` results are wrapped in the envelope (not thrown), so consumers check `envelope.meta.isError`. `structuredContent` is preferred as `envelope.data` when available; otherwise `mapMCPContentBlocks(result.content)` is used. `Value.Cast()` normalization against `outputSchema` is applied by `registry.execute()` and `CallHandler` as part of the shared result pipeline.
### outputSchema and structuredContent
@@ -203,17 +197,7 @@ When a tool does NOT declare `outputSchema`:
See [response-envelopes.md](response-envelopes.md) for the full envelope specification and envelope stripping with `Value.Cast()`.
**Implementation changes needed** (tracking spec vs. current source):
| What | Current source (`src/from_mcp.ts`) | Target (per response-envelopes spec) |
|------|--------------------------------------|---------------------------------------|
| `outputSchema` at discovery | `Type.Unknown()` for all tools (line 66) | `tool.outputSchema ? FromSchema(tool.outputSchema) : Type.Unknown()` |
| Handler return value | Returns `result.content` (line 79) | Returns `mcpEnvelope(data, meta)` |
| `structuredContent` | Not used | Prefer as `data` when present; fall back to `mapMCPContentBlocks(result.content)` |
| `isError` handling | Throws `Error` (line 76) | Wraps in envelope with `meta.isError: true`, does NOT throw |
| `Value.Cast()` | Not used | If `structuredContent && outputSchema !== Unknown`, cast `Value.Cast(outputSchema, structuredContent)` |
The `CallToolResult` type in the installed SDK (`@modelcontextprotocol/sdk` DRAFT-2026-v1) already includes `structuredContent?: { [key: string]: unknown }` and the `Tool` type already includes `outputSchema?`. No SDK upgrade needed — only the adapter code needs updating.
The `CallToolResult` type in the installed SDK (`@modelcontextprotocol/sdk` DRAFT-2026-v1) includes `structuredContent?: { [key: string]: unknown }` and the `Tool` type includes `outputSchema?`. The adapter code extracts `outputSchema` at discovery time and uses `structuredContent` at call time.
### `MCPClientConfig`

View File

@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-05-10
last_updated: 2026-05-11
---
# API Surface
@@ -373,23 +373,7 @@ See [adapters.md](adapters.md) for detailed adapter documentation.
## Source vs. Spec Drift
This section documents differences between the architecture spec (this document) and the current source code. Items marked **ADR-005** or **ADR-006** are planned changes not yet implemented.
### ADR-005 (Response Envelopes) — not yet implemented
| What | Spec says | Source currently does |
|------|----------|----------------------|
| `ResponseEnvelope`, `ResponseMeta`, factory functions, `isResponseEnvelope()`, `unwrap()` | Exported from `src/response-envelope.ts` | None of these types or functions exist in source |
| `execute()` return type | `Promise<ResponseEnvelope<TOutput>>` | `Promise<TOutput>` |
| `execute()` result pipeline | Detect → wrap → normalize → validate | Returns raw `result`, validates raw output with `collectErrors` |
| `OperationEnv` inner function return type | `Promise<ResponseEnvelope>` | `Promise<unknown>` |
| `PendingRequestMap.call()` return type | `Promise<ResponseEnvelope>` | `Promise<unknown>` |
| `PendingRequestMap.respond()` validation | Enforces `isResponseEnvelope()`, throws on raw values | Accepts `unknown`, no validation |
| `subscribe()` yield type | `AsyncGenerator<ResponseEnvelope, void, unknown>` | `AsyncGenerator<unknown, void, unknown>` |
| `CallRespondedEvent.output` | `ResponseEnvelope` | `unknown` |
| `CallHandler` description | Wraps handler result, applies pipeline, publishes `call.responded` | Discards handler return value; handler publishes `call.responded` itself |
| `from_mcp` handler | Returns `mcpEnvelope()`, uses `structuredContent`, extracts `outputSchema` | Returns `result.content`, types `outputSchema` as `Type.Unknown()`, throws on `isError` |
| `from_openapi` handler | Returns `httpEnvelope()` with HTTP metadata | Returns raw response data, throws on HTTP error status |
This section documents differences between the architecture spec and the current source code. ADR-005 (Response Envelopes) has been fully implemented — all envelope types, factories, detection, and integration points are in source and match the spec. ADR-006 (Unified Invocation Path) is not yet implemented.
### ADR-006 (Unified Invocation Path) — not yet implemented

View File

@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-05-09
last_updated: 2026-05-11
---
# Build & Distribution
@@ -43,6 +43,7 @@ Dependencies, project structure, sub-path exports, peer deps, and build tooling.
types.ts # Core types: IOperationDefinition, OperationSpec, OperationType, etc.
registry.ts # OperationRegistry: registerSpec, registerHandler, execute, get, list
validation.ts # assertIsSchema, validateOrThrow, collectErrors, formatValueErrors
response-envelope.ts # ResponseEnvelope types, factories, detection, schemas, unwrap
call.ts # PendingRequestMap, buildCallHandler, CallEventMap, event types
subscribe.ts # subscribe(): direct AsyncGenerator execution
env.ts # buildEnv(): namespace-keyed env with direct/call-protocol modes

View File

@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-05-10
last_updated: 2026-05-11
---
# Call Protocol
@@ -159,7 +159,7 @@ function buildCallHandler(config: CallHandlerConfig): CallHandler
interface CallHandlerConfig {
registry: OperationRegistry
eventTarget?: EventTarget
callMap?: PendingRequestMap
}
type CallHandler = (event: CallRequestedEvent) => Promise<void>
@@ -309,20 +309,7 @@ This allows spec-only registration for scenarios where handlers are provided sep
## Source vs. Spec Drift
This section documents differences between the architecture spec (this document) and the current source code. Items are planned changes not yet implemented.
### ADR-005 (Response Envelopes) — not yet implemented
| What | Spec says | Source currently does |
|------|----------|----------------------|
| `CallEventSchema["call.responded"].output` | `ResponseEnvelopeSchema` | `Type.Unknown()` |
| `CallHandler` behavior | Wraps handler return value, publishes `call.responded` | Discards handler return value; handler must publish itself |
| `CallHandler` error handling | Publishes `call.error` via pubsub | Re-throws `CallError` (does not publish) |
| `call()` return type | `Promise<ResponseEnvelope>` | `Promise<unknown>` |
| `call()` resolution | Resolves with `ResponseEnvelope` from `output` field | Resolves with raw `unknown` from `output` |
| `respond()` validation | Enforces `isResponseEnvelope()` guard, throws on raw values | Accepts `unknown`, no validation |
| `subscribe()` yield type | `AsyncGenerator<ResponseEnvelope, void, unknown>`, wraps yields | `AsyncGenerator<unknown, void, unknown>`, yields raw values |
| `buildEnv()` return types | `Promise<ResponseEnvelope>` per function | `Promise<unknown>` per function |
This section documents differences between the architecture spec (this document) and the current source code. ADR-005 (Response Envelopes) has been fully implemented — `CallEventSchema["call.responded"].output` uses `ResponseEnvelopeSchema`, `CallHandler` wraps handler return values and publishes `call.responded`, `call()` returns `Promise<ResponseEnvelope>`, `respond()` enforces `isResponseEnvelope()`, `subscribe()` yields `ResponseEnvelope`, and `buildEnv()` returns `Promise<ResponseEnvelope>` per function. ADR-006 (Unified Invocation Path) is not yet implemented.
### ADR-006 (Unified Invocation Path) — not yet implemented
@@ -333,7 +320,6 @@ This section documents differences between the architecture spec (this document)
| `CallHandler` calls `execute()` | Thin adapter that calls `registry.execute()` internally | Reimplements lookup, validation, and access control independently |
| `buildEnv()` | Always uses `execute()`, no `callMap` option | Toggles between `execute()` and `callMap.call()` via `if (callMap)` |
| `OperationContext.trusted` | New field for nested call bypass | Does not exist |
| `execute()` return type | `Promise<ResponseEnvelope<TOutput>>` | `Promise<TOutput>` |
| `execute()` error type | Throws `CallError` | Throws plain `Error` |
## References

View File

@@ -1,6 +1,6 @@
# ADR-005: Response Envelopes for Transport-Aware Results
**Status**: Draft
**Status**: Implemented
**Date**: 2026-05-10
## Context

View File

@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-05-10
last_updated: 2026-05-11
---
# ADR-006: Unified Invocation Path
@@ -31,7 +31,7 @@ Meanwhile `execute()` is a **domain call** — same-process, same-trust, rich ty
### Prerequisites
This ADR depends on ADR-005 (response envelopes) being implemented in source first. The unified `execute()` requires `ResponseEnvelope` types, `isResponseEnvelope()`, and factory functions that don't exist in source yet.
This ADR depends on ADR-005 (response envelopes) being implemented in source first. **ADR-005 is now implemented** `ResponseEnvelope` types, `isResponseEnvelope()`, and factory functions exist in `src/response-envelope.ts`. The prerequisite is met.
## Decision

View File

@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-05-10
last_updated: 2026-05-11
---
# Response Envelopes
@@ -221,15 +221,6 @@ Flow:
**Note**: `isResponseEnvelope()` does not validate that the envelope's `source` matches the operation's origin. An MCP handler that explicitly returns a `localEnvelope(...)` passes through as-is. Handlers that explicitly construct envelopes take responsibility for their metadata.
**Current source state** (`src/registry.ts` lines 80-105): `execute()` returns `Promise<TOutput>` directly. It does not wrap results in envelopes, does not call `isResponseEnvelope()`, and validates raw `result` against `outputSchema`. The `Value.Cast()` normalization step is not implemented. Changes needed:
| What | Current source | Target |
|------|---------------|--------|
| Return type | `Promise<TOutput>` | `Promise<ResponseEnvelope<TOutput>>` |
| Wrapping | None — returns raw `result` | `isResponseEnvelope(result) ? result : localEnvelope(result, operationId)` |
| Validation | `collectErrors(spec.outputSchema, result)` — on raw value | `collectErrors(spec.outputSchema, envelope.data)` — on envelope data |
| `Value.Cast()` | Not used | If `outputSchema !== Unknown`, `envelope.data = Value.Cast(spec.outputSchema, envelope.data)` |
### `CallHandler`
Takes full ownership of publishing `call.responded`. Handlers return values; they do NOT publish events.
@@ -246,29 +237,14 @@ Flow:
9. Publish `call.responded` via `callMap.respond(requestId, envelope)`
10. On handler exception → publish `call.error` (existing). Note: an envelope with `meta.isError: true` does **not** trigger `call.error`. Only thrown exceptions do.
**Current source state** (`src/call.ts` lines 182-233): `buildCallHandler` calls `handler(input, context)` (line 226) but does not use the return value. The handler is expected to publish `call.responded` itself. Changes needed:
| What | Current source | Target |
|------|---------------|--------|
| Handler model | Handler publishes `call.responded` itself; return value ignored | Handler returns value; `CallHandler` wraps and publishes |
| Return value | `await handler(input, context)` called but result discarded | Result captured, wrapped in envelope if needed, then published |
| Envelope detection | Not applicable | `isResponseEnvelope(result)` check before wrapping |
| Result pipeline | None | Detect → wrap → normalize → validate → publish |
| `call.responded.output` | `Type.Unknown()` | `ResponseEnvelopeSchema` |
| `PendingRequestMap.respond()` | Accepts any `unknown` value | Must enforce `isResponseEnvelope()` guard |
### `PendingRequestMap.respond()`
Enforces that `output` is a `ResponseEnvelope` via the `isResponseEnvelope()` type guard (not full schema validation). If called with a non-envelope value, throws. This prevents any code bypassing `CallHandler`'s envelope wrapping. A future iteration may make `respond()` internal (not exported on the public API surface) to further enforce this invariant.
**Current source state** (`src/call.ts` lines 151-156): `respond()` publishes `call.responded` with `output: unknown`. No envelope validation.
### `PendingRequestMap.call()`
Resolves with the `ResponseEnvelope` from `call.responded.output` instead of raw `unknown`.
**Current source state** (`src/call.ts` lines 120-149): `call()` returns `Promise<unknown>`. The type should become `Promise<ResponseEnvelope>`.
### `subscribe()`
Each yielded value is wrapped in `ResponseEnvelope`. `SubscriptionHandler` implementations still yield raw values — `subscribe()` wraps each yield, consistent with how local handlers return raw values and `execute()` wraps them. If a yielded value `isResponseEnvelope()`, it passes through. Otherwise, `localEnvelope(value, operationId)` with a fresh `timestamp` per yield.
@@ -455,26 +431,24 @@ The following documentation changes have been completed:
| `api-surface.md` | `CallHandler` | Wraps handler result, publishes `call.responded`. No longer "handler publishes" model. | ✅ |
| `call-protocol.md` | `PendingRequestMap.respond()` | Now enforces `isResponseEnvelope()` check — throws on raw values. | ✅ |
| `api-surface.md` | `PendingRequestMap.respond()` | `respond()` now requires `ResponseEnvelope` argument. | ✅ |
| `adapters.md` | `from_mcp` | Handler returns `mcpEnvelope()`. MCP `isError: true` no longer throws. | ✅ (previous) |
| `adapters.md` | `from_mcp` | `outputSchema` extracted when available, via `FromSchema`. Falls back to `Type.Unknown()`. | ✅ (previous) |
| `adapters.md` | `from_openapi` | Handler returns `httpEnvelope()`. Error on HTTP error status still throws `CallError`. | ✅ (previous) |
| `adapters.md` | `from_mcp` | Handler returns `mcpEnvelope()`. MCP `isError: true` no longer throws. | ✅ |
| `adapters.md` | `from_mcp` | `outputSchema` extracted when available, via `FromSchema`. Falls back to `Type.Unknown()`. | ✅ |
| `adapters.md` | `from_openapi` | Handler returns `httpEnvelope()`. Error on HTTP error status still throws `CallError`. | ✅ |
The following **code** changes are still needed:
The following **code** changes have been completed:
| Code | Change |
|------|--------|
| `src/registry.ts` | `execute()` returns `Promise<ResponseEnvelope<TOutput>>` |
| `src/call.ts` | `CallHandler` captures return value, wraps in envelope, publishes `call.responded` |
| `src/call.ts` | `CallEventSchema` `output` field changes to `ResponseEnvelopeSchema` |
| `src/call.ts` | `PendingRequestMap.respond()` adds `isResponseEnvelope()` guard |
| `src/call.ts` | `PendingRequestMap.call()` resolves with `ResponseEnvelope` |
| `src/subscribe.ts` | `subscribe()` wraps yields in `ResponseEnvelope` |
| `src/env.ts` | `buildEnv()` functions return `Promise<ResponseEnvelope>` |
| `src/response-envelope.ts` | New file: types, factory functions, detection, schemas |
| `src/from_mcp.ts` | Handler returns `mcpEnvelope()`, extracts `outputSchema`, uses `structuredContent` |
| `src/from_openapi.ts` | Handler returns `httpEnvelope()` |
Additionally, any code subscribing to `"call.responded"` events via the pubsub system (not just `PendingRequestMap`, but any direct pubsub consumer) must expect `ResponseEnvelope` instead of `unknown` in the event payload.
| Code | Change | Status |
|------|--------|--------|
| `src/response-envelope.ts` | New file: types, factory functions, detection, schemas | ✅ |
| `src/registry.ts` | `execute()` returns `Promise<ResponseEnvelope<TOutput>>` | ✅ |
| `src/call.ts` | `CallHandler` captures return value, wraps in envelope, publishes `call.responded` | ✅ |
| `src/call.ts` | `CallEventSchema` `output` field changes to `ResponseEnvelopeSchema` | ✅ |
| `src/call.ts` | `PendingRequestMap.respond()` adds `isResponseEnvelope()` guard | ✅ |
| `src/call.ts` | `PendingRequestMap.call()` resolves with `ResponseEnvelope` | ✅ |
| `src/subscribe.ts` | `subscribe()` wraps yields in `ResponseEnvelope` | ✅ |
| `src/env.ts` | `buildEnv()` functions return `Promise<ResponseEnvelope>` | ✅ |
| `src/from_mcp.ts` | Handler returns `mcpEnvelope()`, extracts `outputSchema`, uses `structuredContent` | ✅ |
| `src/from_openapi.ts` | Handler returns `httpEnvelope()` | ✅ |
## References