chore: initialize from backend template 0a6dd0e
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
# Conditional Inbound Transport Boundary Design
|
||||
|
||||
**Date:** 2026-08-02
|
||||
**Status:** approved by the user's instruction to apply the detailed P1/P2 review sequentially
|
||||
**Scope:** the opt-in GraphQL, gRPC, and WebSocket leaf modules and their release evidence
|
||||
|
||||
## Context
|
||||
|
||||
The three leaves are registered and tested independently, but neither `app-bootstrap` nor
|
||||
`sample-portfolio` has a production dependency on them. That omission is intentional: adding a
|
||||
classpath edge today would activate GraphQL, start a plaintext/reflection-enabled gRPC server by
|
||||
default, and unconditionally expose a wildcard-origin STOMP broker that serializes arbitrary domain
|
||||
events. The leaf documentation nevertheless describes sample contributions that do not exist, and
|
||||
the ordinary root `check` can become `NO-SOURCE` without a transport-specific positive-count and
|
||||
zero-skip qualification gate.
|
||||
|
||||
P1 therefore makes opt-in status executable and makes accidental activation fail closed. It does
|
||||
not add these leaves to the default runtime or claim the P2 production baselines.
|
||||
|
||||
## Runtime Membership SSOT
|
||||
|
||||
Every entry in `config/architecture/modules.json` gains an exact `runtime_memberships` array whose
|
||||
values are limited to the two composition roots: `app-bootstrap` and `sample-portfolio`.
|
||||
|
||||
- A composition root includes itself in its membership.
|
||||
- Direct production `api`/`implementation`/`compileOnly`/`runtimeOnly` project dependencies must
|
||||
equal the registry members for that root, excluding the root itself.
|
||||
- An empty array means the leaf is built and architecture-checked but absent from both shipped
|
||||
runtime graphs. GraphQL, gRPC, WebSocket, and Mongo remain in this state.
|
||||
- Test fixtures and custom qualification configurations do not change production membership.
|
||||
|
||||
Settings validation is fail-closed for missing, duplicate, or unknown membership names. A Gradle
|
||||
verification task compares the registry to both composition roots and is part of `check`.
|
||||
|
||||
## Explicit Qualification Composition
|
||||
|
||||
`app-bootstrap` owns a `conditionalTransportTest` source set whose classpath explicitly includes
|
||||
the three opt-in leaves. It proves that the opt-in artifacts resolve together while the registry
|
||||
still declares them absent from both default runtime graphs. It is evidence composition, not a new
|
||||
production dependency edge.
|
||||
|
||||
The root registers exact qualification `Test` tasks for GraphQL, gRPC, and WebSocket. Each task:
|
||||
|
||||
- names required test classes rather than broad discovery;
|
||||
- fails on no match or no discovery;
|
||||
- always reruns in UTC;
|
||||
- fails if the root suite reports any skipped test.
|
||||
|
||||
An aggregate `conditionalTransportQualification` task depends on the composition contract and all
|
||||
three exact lanes. CI invokes it explicitly from the existing release-blocking quality job, and the
|
||||
gate matrix records the task.
|
||||
|
||||
## gRPC P1 Boundary
|
||||
|
||||
gRPC activation becomes explicit and local-only until a later TLS/mTLS design exists:
|
||||
|
||||
- `enabled=false` and `reflectionEnabled=false` are defaults; missing properties create no runner,
|
||||
health manager, reflection service, or listener.
|
||||
- The current insecure credential mode requires an explicit local-development override and a
|
||||
loopback bind address. Non-loopback insecure bind fails startup.
|
||||
- Feature services require a caller-supplied authentication policy/interceptor. Missing or invalid
|
||||
metadata returns stable `UNAUTHENTICATED`; valid metadata reaches the service.
|
||||
- Health remains a local lifecycle probe; reflection is a separate explicit flag.
|
||||
- The error interceptor wraps `ServerCall.close`, so handler throws, listener throws, ordinary
|
||||
`responseObserver.onError`, and raw `StatusRuntimeException` all pass the same sanitizer.
|
||||
Recognized `ApiErrorCarrier` causes produce stable code/category trailers; unrecognized status
|
||||
descriptions become fixed `INTERNAL_ERROR` with no raw diagnostic.
|
||||
|
||||
A real ephemeral Netty unary service verifies authentication, reflection-off, all error paths, and
|
||||
sentinel redaction. TLS/mTLS, external bind, deadlines, streaming, and protobuf compatibility are
|
||||
P2 and remain unclaimed.
|
||||
|
||||
## GraphQL P1 Boundary
|
||||
|
||||
GraphQL remains classpath-selected: its absence from the default runtime is the disable mechanism,
|
||||
and the qualification classpath is the explicit opt-in mechanism. The wire lane starts a real
|
||||
random-port MVC server and crosses HTTP JSON, Spring Security, and CORS.
|
||||
|
||||
It verifies unauthenticated rejection, authenticated health success, allowed/disallowed origins,
|
||||
GraphiQL disabled, production-style introspection disabled, stable carrier errors, unknown errors,
|
||||
and absence of distinct secret sentinels from the complete response body. The existing resolver is
|
||||
changed only if a failing wire contract proves unsafe behavior.
|
||||
|
||||
Feature schema/resolvers, field authorization, depth/cost, persisted queries, DataLoader, schema
|
||||
compatibility, and subscriptions remain P2.
|
||||
|
||||
## WebSocket P1 Boundary
|
||||
|
||||
WebSocket gains `ca-skeleton.websocket.enabled=false`; both configuration and broadcaster are
|
||||
conditional. Enabled settings reject wildcard/blank origins and invalid endpoint/destination
|
||||
shapes.
|
||||
|
||||
The inbound channel requires an authenticated handshake principal, permits subscription only to
|
||||
the configured server topic, permits authenticated application sends under `/app/**`, and rejects
|
||||
client sends to `/topic/**`. A custom STOMP error handler emits only a fixed client-safe code.
|
||||
|
||||
The broadcaster no longer serializes arbitrary `@DomainEvent` objects. It consults an explicit
|
||||
projection allowlist; an event without exactly one projection is not sent. Projection output is a
|
||||
bounded primitive map, not the domain object graph.
|
||||
|
||||
A real random-port WebSocket/STOMP lane verifies disabled absence, origin/auth/connect/subscribe,
|
||||
server push, broker-send rejection, error redaction, and no projection/no broadcast. The simple
|
||||
broker remains local/R1 only; broker relay, cross-node durability, replay, backpressure, and a
|
||||
domain-specific versioned projection catalog remain P2.
|
||||
|
||||
## Documentation Truthfulness
|
||||
|
||||
Leaf READMEs and CLAUDE files describe only code that exists. Sample GraphQL schemas, gRPC services,
|
||||
and WebSocket publishers are future adoption examples, not current runtime features. Each document
|
||||
states the activation switch, exact P1 evidence, and unimplemented P2 limits.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- adding any of the three leaves to a shipped default runtime;
|
||||
- adding a production project dependency edge outside the registry;
|
||||
- claiming production readiness from local loopback/simple-broker tests;
|
||||
- implementing sample feature APIs or domain payloads;
|
||||
- staging, committing, amending, or pushing changes.
|
||||
Reference in New Issue
Block a user