Skip to content

Foundation Case Matrix ​

One row per clause of GitHub issue 13 "Implement Application Foundation" and of FOUNDATION_HTTP_AND_OBSERVABILITY.md, grouped by slice (A, B), by delivery (C1 to C6, D) and by the issue's "Acceptance evidence" list (AE1 to AE9). Each clause names the test that proves it, quoted verbatim as a spec file path, then the separator "›", then the exact test title, exactly like ORGANIZATION_CASE_MATRIX.md and OUTBOX_CASE_MATRIX.md; npm run lint:case-matrix verifies every citation and every count on this page.

This page is the executable acceptance evidence that AGENTS.md section 11 asks for before an issue checklist item is marked complete. It changes no behavior and adds no test. A clause with no test that proves it answers —; a clause that is only partly proven says "partial" and cites only the part that is proven, with the rest as its own — row. Operational evidence that code cannot provide (staging deployments, a telemetry destination, a k6 run against staging, branch rulesets, owner decisions) is a — row marked "operational" or "owner decision" and is never claimed.

Verification level. After the matrix was first written, every row of sections B1, C1, C3 and C4 (11, 24, 13 and 20 rows with a test), 16 sampled rows (C2: 3, C5: 5, C6: 3, AE1 to AE9: 5) and the 3 rows added for the OFFSET scan (C2) and the bounded span queue (C5 and AE6) were checked by opening each cited test and reading its assertions against the clause: 87 of the 160 rows with a test. Every B1 row was read again when its last gap closed, and each test added for a gap was shown to fail when the property it guards is broken. Where the cited tests proved only part of a clause the row became "partial" with its own — row, and where an existing test proved a clause but was not cited it was added. The other 73 rows (A1, the rest of C2, C5 and C6, D1 and the rest of AE1 to AE9) are matched by an exact existing test title and have NOT been read against the clause; in the sampled rows 7 of 16 needed a correction, so treat those 73 as unverified.

Two naming notes. The issue text names the cursor field nextCursor; the implemented and specified field is endCursor (with startCursor, hasNextPage and hasPreviousPage). The issue also cites sections 4.1, 7.8 and 8 of the owner specification; the current specification (FOUNDATION_HTTP_AND_OBSERVABILITY.md) no longer has those sections, so the clauses here come from the issue text and from the sections that exist today.


Verdict per issue item ​

"FULLY PROVEN" means every clause of the item is cited to a test. "PARTIAL" lists what is missing in the Gaps section. "OPEN" means the item needs operational evidence that does not exist yet. Counts are clauses with a cited test versus — rows, over the sections named in the second column.

ItemSectionsVerdictClauses with a testGap clauses
AA1PARTIAL112
BB1FULLY PROVEN110
C1C1PARTIAL242
C2C2PARTIAL161
C3C3FULLY PROVEN130
C4C4PARTIAL202
C5C5PARTIAL207
C6C6PARTIAL134
C (parent: C1 to C6 and the acceptance evidence list)C1 to C6, AE1 to AE9PARTIAL13534
DD1OPEN (operational)34

Reading the verdicts: tick an issue box only where the verdict is FULLY PROVEN. The staging deployments recorded in the issue comments are operational evidence and are listed in AE9; they are not repository-linked executable evidence and are not claimed here.


Totals ​

Use cases18
Per-case scenario rows200
Per-case scenario rows proven160
Cross-cutting invariants0 (0 proven)
Gaps40

A1 — Slice A: typed configuration, runtime pool, role safety and health probes ​

CaseExpected answerProving test
Configuration is typed and validated with Zod at startup; an invalid or missing key stops the boot naming the key and never its valueBoot fails fast; the error names the key and omits the valuesrc/foundation/config/config.module.spec.ts › fails fast without leaking values when NODE_ENV is invalid; src/foundation/config/config.module.spec.ts › fails fast when required database keys are missing; src/foundation/config/schema/environment-variables.schema.spec.ts › throws naming the invalid key without leaking its value
Discrete DATABASE_* settings with the documented defaults (pool max 10, connection 5 s, statement 5 s, lock 2 s, idle in transaction 10 s)A minimal valid environment parses with those defaultssrc/foundation/config/schema/environment-variables.schema.spec.ts › parses a minimal valid environment and applies defaults
The pg pool is built from the typed settings, including the four timeouts and optional TLSPool options carry the configured timeouts; SSL is enabled only when requestedsrc/foundation/database/pool/database-pool.service.spec.ts › builds a pg.Pool from the typed database configuration; src/foundation/database/pool/database-pool.service.spec.ts › enables ssl when the database configuration requests it
The API only runs as the restricted ninaku_runtime roleAny other DATABASE_USER is rejected for the API processsrc/foundation/config/schema/environment-variables.schema.spec.ts › rejects a DATABASE_USER other than ninaku_runtime: %s
Startup role-safety check refuses a superuser, a BYPASSRLS role or a table ownerThe application refuses to start; checked against real PostgreSQLsrc/foundation/database/role-guard/database-runtime-role-guard.service.integration-spec.ts › throws when connected as the postgres superuser; src/foundation/database/role-guard/database-runtime-role-guard.service.integration-spec.ts › rejects a real ninaku_migrator connection, which owns the schema, even when DATABASE_USER matches; src/foundation/database/role-guard/database-runtime-role-guard.service.spec.ts › throws when the connected role bypasses row level security
The role-safety failure never leaks a password or connection stringThe error message carries neithersrc/foundation/database/role-guard/database-runtime-role-guard.service.spec.ts › does not leak a password or connection string in the mismatch error message
GET /health/live answers without depending on the database200 with status up even when the database is not consultedtest/e2e/health.e2e-spec.ts › /health/live (GET) reports up without depending on the database
GET /health/ready checks PostgreSQL as ninaku_runtime200 when reachable, 503 when the pool is closedtest/e2e/health.e2e-spec.ts › /health/ready (GET) reports up when the database is reachable; test/e2e/health.e2e-spec.ts › /health/ready (GET) reports 503 when the database pool is closed
The readiness result never exposes the driver error textThe serialized health result omits the driver messagesrc/foundation/health/database-health.indicator.spec.ts › never exposes the driver error text in the serialized result
The pool closes on application shutdownThe pool is ended when the application shuts downsrc/foundation/database/pool/database-pool.service.spec.ts › closes the pool on application shutdown
PostgreSQL-backed application tests run against a real databasePool connects to PostgreSQL and runs a query as the runtime rolesrc/foundation/database/pool/database-pool.service.integration-spec.ts › connects to PostgreSQL and runs a query
Railway runtime database settings are declared as code and railway config plan reports no drift (operational)Declared in .railway/railway.ts; verified by the Railway CLI, not by a repository test—
Integration and e2e run against PostgreSQL 18 prepared with db:prepare (operational)Run locally by the integrator through npm run verify after npm run db:fresh; no CI workflow runs them since 12 October 2026; a verification run, not a test—

B1 — Slice B: request and trace context, transaction boundary, tenant context and RLS ​

CaseExpected answerProving test
Request context lives in AsyncLocalStorage and survives nested async workrequestId stays visible across awaits and is isolated between concurrent runssrc/foundation/correlation/request-context.service.spec.ts › keeps the requestId visible across nested async work; src/foundation/correlation/request-context.service.spec.ts › isolates concurrent runs from each other; src/foundation/correlation/request-context.service.spec.ts › does not leak the requestId outside of the run callback
Trace context lives in AsyncLocalStorage and survives nested async worktraceId stays visible across nested async work and is isolated between concurrent runs (the spanId is the next row)src/foundation/correlation/trace/trace-context.service.spec.ts › keeps the traceId visible across nested async work; src/foundation/correlation/trace/trace-context.service.spec.ts › isolates concurrent runs from each other
The spanId stays visible across nested async work and is isolated between concurrent runsspanId stays visible across awaits and each concurrent run reads its ownsrc/foundation/correlation/trace/trace-context.service.spec.ts › keeps the spanId visible across nested async work; src/foundation/correlation/trace/trace-context.service.spec.ts › isolates the spanId of concurrent runs from each other
A use case runs inside one transaction boundaryBEGIN, the unit of work and COMMIT on one session; rollback and release on failuresrc/foundation/transactions/transaction-runner.service.spec.ts › runs BEGIN, the unit of work and COMMIT, then releases the client exactly once with no argument; src/foundation/transactions/transaction-runner.service.spec.ts › rolls back and releases the client when the unit of work throws; src/foundation/transactions/transaction-runner.service.integration-spec.ts › keeps one open transaction for the whole unit of work
Transaction-local tenant and identity context is applied and then asserted by identity.assert_tenant_context()Every GUC is set transaction-locally, then the assertion runs before the work callbacksrc/foundation/tenancy/tenant-context-runner.service.spec.ts › establishes the whole tenant context with one statement; src/foundation/transactions/tenant-transaction-runner.service.integration-spec.ts › has the tenant context already asserted before the work callback runs its first query
A context that does not match an active membership is rejectedRejected before any work runs; nothing is committedsrc/foundation/tenancy/tenant-context-runner.service.integration-spec.ts › rejects a context with an organization that does not match the membership; src/foundation/tenancy/tenant-context-runner.service.integration-spec.ts › rejects a context built from identifiers with no active membership at all; src/foundation/transactions/tenant-transaction-runner.service.integration-spec.ts › rejects an unknown tenant context, never running the work, without committing anything
Tenant GUCs never leak to the next transaction on a pooled connectionCleared after commit and after rollback; an impersonation value does not survivesrc/foundation/tenancy/tenant-context-runner.service.integration-spec.ts › does not leak tenant GUCs to the next transaction on the same pooled connection after commit; src/foundation/tenancy/tenant-context-runner.service.integration-spec.ts › does not leak tenant GUCs to the next transaction after a rollback; src/foundation/transactions/tenant-transaction-runner.service.integration-spec.ts › does not let an impersonation value from one run survive into a later run on the same pooled connection
RLS-protected reads see only the caller organizationOnly the caller organization rows are visiblesrc/foundation/tenancy/tenant-context-runner.service.integration-spec.ts › lets an RLS-protected read see only the caller organization
Cross-tenant reads are deniedA direct read of another organization returns no rowssrc/foundation/tenancy/tenant-context-runner.service.integration-spec.ts › returns no rows for a direct read of another organization
A retried attempt re-applies the tenant contextAfter a real serialization failure the retried attempt runs under the same tenant contextsrc/foundation/transactions/tenant-transaction-runner.service.integration-spec.ts › reapplies the tenant context on a retried attempt after a real serialization failure
The tenant guard resolves the tenant from the authenticated caller and the organization headerNo caller means refusal before resolving; a repeated header is not guessedsrc/foundation/tenancy/tenant-context.guard.spec.ts › refuses a request that carries no authenticated caller before resolving any tenant; src/foundation/tenancy/tenant-context.guard.spec.ts › hands a repeated header to the resolver as undefined instead of guessing one value

C1 — HTTP success contract: envelope, statuses, types, correlation headers and protocol exceptions ​

CaseExpected answerProving test
Natural request bodies: the client sends the plain resource, not an envelopeA plain valid body reaches the handler unchangedtest/e2e/validation.e2e-spec.ts › accepts a valid body and passes the parsed value through the envelope
Success carries { data }200 with the handler value under data and a JSON content typetest/e2e/http-envelope.e2e-spec.ts › wraps a plain success response in { data }; src/foundation/http/envelope/http-envelope.interceptor.spec.ts › wraps a plain returned value into { data, meta }
meta is optional and carries what the route addsA response with explicit meta is { data, meta }test/e2e/http-envelope.e2e-spec.ts › wraps a response with explicit meta as { data, meta }; src/foundation/http/envelope/http-envelope.interceptor.spec.ts › wraps an HttpResponseWithMeta into { data, meta } carrying only the route meta
The handler value appears exactly once under data (no double nesting)partial: data equals the handler value; no test feeds an already wrapped valuetest/e2e/http-envelope.e2e-spec.ts › wraps a plain success response in { data }
No controller returns an envelope by itself: handlers return payloads and the interceptor owns the envelopeNot asserted. By design the interceptor wraps every plain object, because it cannot tell a payload that has a field named data from an envelope, so a handler returning { data: x } is answered as { data: { data: x }, meta }; what is unproven is that no controller does this by mistake (a scan or a test over every controller)—
No generic success flag (success: true, ok, status) is added to the bodyThe body holds exactly data and meta, and meta only what the route addssrc/foundation/http/envelope/http-envelope.interceptor.response-shape.spec.ts › answers a plain result with exactly data and meta, never a success flag; src/foundation/http/envelope/http-envelope.interceptor.response-shape.spec.ts › carries only the route meta, never a success flag, a request id or a time; test/e2e/http-envelope.e2e-spec.ts › adds no success flag: the body holds exactly data and meta
Status codes are preserved, including an explicit 201 with a Location header201 with Location and the envelopetest/e2e/http-envelope.e2e-spec.ts › composes the envelope with an explicit 201 status and Location header
Decimals and dates travel as stringsA decimal stays a string and a date stays an ISO stringtest/e2e/http-envelope.e2e-spec.ts › preserves decimal-as-string, ISO date-as-string and an explicit null field
Identifiers travel as strings in responsesPartial: every id and ...Id value of the real database-backed collection response of GET /organizations is a string (one route only)test/e2e/my-organizations.e2e-spec.ts › serializes every identifier of the response as a string
Null versus absence is preservedAn explicit null stays null; an absent field stays absent; a null result is { data: null }, not 204test/e2e/http-envelope.e2e-spec.ts › keeps an absent field distinct from an explicit null field; src/foundation/http/envelope/http-envelope.interceptor.spec.ts › wraps an explicit null result into { data: null, meta } instead of treating it as no representation; test/e2e/http-envelope.e2e-spec.ts › preserves decimal-as-string, ISO date-as-string and an explicit null field
Identifiers travel as strings in the responses of every other routeOnly the GET /organizations collection response is asserted; the identifiers of the other routes are not checked by any test—
The request id and the response time travel in headers, not in the bodyX-Request-Id and Date are present on an enveloped success and the body holds only stable meta, so a strong ETag names one exact bodytest/e2e/conditional-requests.e2e-spec.ts › answers byte-identical bodies under the same strong ETag, each with its own X-Request-Id
A server-generated X-Request-Id is set on every HTTP attemptA generated id on the response; concurrent requests get independent idstest/e2e/request-id.e2e-spec.ts › sets a generated x-request-id response header; test/e2e/request-id.e2e-spec.ts › assigns independent request-ids to concurrent requests
A client-supplied X-Request-Id is ignoredThe server generates its own valuetest/e2e/request-id.e2e-spec.ts › ignores an incoming X-Request-Id header and generates its own
The correlation header is also present on error and unmatched-route responsesX-Request-Id on a Problem Details response and on an unmatched routetest/e2e/problem-details.e2e-spec.ts › keeps X-Request-Id on an error response; test/e2e/request-id.e2e-spec.ts › still sets an x-request-id on an unmatched route error response
The trace id is not exposed as a response header (request id and trace id stay separate)No x-trace-id header, with or without an incoming traceparenttest/e2e/trace-context.e2e-spec.ts › does not expose the legacy x-trace-id header when no traceparent is sent; test/e2e/trace-context.e2e-spec.ts › does not expose the legacy x-trace-id header even with a valid incoming traceparent
Browsers can read the correlation and conditional headers across originsETag, X-Request-Id and Retry-After are listed in Access-Control-Expose-Headerstest/e2e/http-application-policy.e2e-spec.ts › exposes ETag, X-Request-Id and Retry-After so a browser can read them across origins
204 has no body or envelopeA declared 204 sends no body, even if the handler returns onetest/e2e/http-envelope.e2e-spec.ts › does not wrap a declared 204 response; test/e2e/http-envelope.e2e-spec.ts › sends no body on a 204 route even when the handler mistakenly returns one
304 has no bodyA matching If-None-Match answers 304 with an empty bodytest/e2e/conditional-requests.e2e-spec.ts › returns 304 with no body when If-None-Match exactly matches the current tag
HEAD sends no bodyNo body for HEAD against a route that returns a body on GETtest/e2e/http-envelope.e2e-spec.ts › sends no body for a HEAD request against a route that returns a body on GET
Downloads and streams are not wrappedA StreamableFile passes through unmodifiedtest/e2e/http-envelope.e2e-spec.ts › does not wrap a StreamableFile download response; src/foundation/http/envelope/http-envelope.interceptor.spec.ts › passes a StreamableFile through unmodified
Stream failures keep HTTP semanticsA failure before any byte is a raw 400; a failure mid-transfer truncates instead of completingtest/e2e/http-envelope.e2e-spec.ts › reports a stream error before any bytes were sent as a raw 400, never a { data } envelope or Problem Details; test/e2e/http-envelope.e2e-spec.ts › truncates the response instead of completing it when a stream fails mid-transfer
Protocol exceptions are declared with @SkipHttpEnvelope and the health probes stay rawAnnotated handlers and controllers are not wrapped; /health/live and /health/ready keep the Terminus bodysrc/foundation/http/envelope/http-envelope.interceptor.spec.ts › skips wrapping when the handler is annotated with @SkipHttpEnvelope; src/foundation/http/envelope/http-envelope.interceptor.spec.ts › skips wrapping when the controller class is annotated with @SkipHttpEnvelope; test/e2e/http-envelope.e2e-spec.ts › keeps the health check contract unwrapped; test/e2e/http-envelope.e2e-spec.ts › keeps the health readiness contract unwrapped
Conditional reads: ETag on opt-in routes, 200 on mismatch, 304 on strong, weak, wildcard or listed matchFresh ETag without If-None-Match; no ETag at all for routes that did not opt intest/e2e/conditional-requests.e2e-spec.ts › returns 200 with the body and a fresh ETag when no If-None-Match header is sent; test/e2e/conditional-requests.e2e-spec.ts › returns 200 with a fresh ETag when If-None-Match does not match the current tag; test/e2e/conditional-requests.e2e-spec.ts › returns 304 when If-None-Match carries a weak tag equal to the strong server tag; test/e2e/conditional-requests.e2e-spec.ts › returns 304 when If-None-Match is "*" and the representation exists; test/e2e/conditional-requests.e2e-spec.ts › returns 304 when the current tag appears anywhere in a comma-separated list; test/e2e/http-envelope.e2e-spec.ts › never sets an ETag or engages conditional behavior for a handler that has not opted in; test/e2e/conditional-requests.e2e-spec.ts › returns 304 with no body when If-None-Match exactly matches the current tag
Conditional writes: If-Match applies the write on a match and answers 412 Problem Details otherwise412 on mismatch or weak-only match; write applied on match or when no If-Match is senttest/e2e/conditional-requests.e2e-spec.ts › applies the write and returns the new ETag when If-Match matches the current tag; test/e2e/conditional-requests.e2e-spec.ts › rejects the write with 412 Problem Details when If-Match does not match the current tag; test/e2e/conditional-requests.e2e-spec.ts › rejects the write with 412 when If-Match carries only a weak tag equal to the current strong tag; test/e2e/conditional-requests.e2e-spec.ts › applies the write when no If-Match header is sent
Routes live under /api/v1 and the health probes stay unprefixedDefault version 1, explicit versions served side by side, undeclared versions refusedtest/e2e/api-versioning.e2e-spec.ts › serves a route with no explicit @Version() under /api/v1; test/e2e/api-versioning.e2e-spec.ts › does not serve a third version that was never declared; test/e2e/api-versioning.e2e-spec.ts › still answers /health/live exactly at that path, unprefixed and unversioned; test/e2e/api-versioning.e2e-spec.ts › serves version 1 at /api/v1/.../ping; test/e2e/api-versioning.e2e-spec.ts › serves version 2 at /api/v2/.../ping without touching version 1

C2 — Pagination: meta.pageInfo, limits, signed opaque cursors, keyset and authorization ​

CaseExpected answerProving test
meta.pageInfo reports hasNextPage, hasPreviousPage, startCursor and endCursor (the issue text says nextCursor; the implemented and specified name is endCursor)Every page carries the shape; the last page has no next page and null cursorstest/e2e/collections.e2e-spec.ts › walks the full collection across pages with a deterministic order; src/foundation/pagination/page-info.spec.ts › reports the end of the collection in both directions with null cursors; src/foundation/pagination/page-info.spec.ts › rejects an unknown property so the OpenAPI contract stays exact
Default limit 50Absent limit applies the documented defaultsrc/foundation/pagination/collection-query.schema.spec.ts › defaults to the documented default limit when absent; test/e2e/collections.e2e-spec.ts › applies the default limit and reaches the end in one page
Maximum limit 100 and a minimum of 1Out-of-range limits are validation failuressrc/foundation/pagination/collection-query.schema.spec.ts › accepts the documented maximum limit; test/e2e/collections.e2e-spec.ts › rejects a limit above the documented maximum of 100; test/e2e/collections.e2e-spec.ts › rejects a limit below the minimum bound
Cursors are opaque, versioned and signed, and never contain the keyRound trip works; an unsupported version is invalid; the key is absent from the tokensrc/foundation/pagination/cursor/cursor-codec.spec.ts › round-trips a valid cursor back to its original position; src/foundation/pagination/cursor/cursor-codec.spec.ts › rejects an unsupported token version as invalid; src/foundation/pagination/cursor/cursor-codec.spec.ts › never contains the signing key in the encoded token
A tampered cursor is refusedcollection.cursor_tampered for an altered payload or signaturesrc/foundation/pagination/cursor/cursor-codec.spec.ts › rejects a cursor whose payload was altered after signing as tampered; src/foundation/pagination/cursor/cursor-codec.spec.ts › rejects a cursor whose signature was altered as tampered; test/e2e/collections.e2e-spec.ts › rejects a cursor whose signature was altered as collection.cursor_tampered
An expired cursor is refusedcollection.cursor_expiredsrc/foundation/pagination/cursor/cursor-codec.spec.ts › rejects an expired cursor; test/e2e/collections.e2e-spec.ts › rejects an expired cursor as collection.cursor_expired
Key rotation invalidates cursors signed under an unknown keyRefused as invalidsrc/foundation/pagination/cursor/cursor-codec.spec.ts › rejects a cursor signed under a key the server no longer recognizes
A cursor is bound to its filters and sortReuse under a different filter is collection.cursor_incompatiblesrc/foundation/pagination/cursor/cursor-codec.spec.ts › rejects a cursor reused against a different binding as incompatible; src/foundation/pagination/cursor/cursor-binding.spec.ts › changes when a bound value changes; test/e2e/collections.e2e-spec.ts › rejects a cursor reused with a different filter as collection.cursor_incompatible
Authorization on every page: a cursor is bound to the tenant contextReuse under another tenant is collection.cursor_incompatibletest/e2e/collections.e2e-spec.ts › rejects a cursor reused under a different tenant context as collection.cursor_incompatible
Malformed cursors and the after-plus-before combination are explicit errorscollection.cursor_invalid for a malformed token; a validation failure for both after and beforetest/e2e/collections.e2e-spec.ts › rejects a structurally malformed after cursor as collection.cursor_invalid; test/e2e/collections.e2e-spec.ts › rejects a structurally malformed before cursor as collection.cursor_invalid; test/e2e/collections.e2e-spec.ts › rejects a request naming both after and before as a validation failure
Each decode failure maps to a distinct stable code without decoding detailDistinct codes; the public message carries no internal detailsrc/foundation/pagination/cursor/cursor.exceptions.spec.ts › maps every decode failure reason to a distinct stable code; src/foundation/pagination/cursor/cursor.exceptions.spec.ts › never includes internal decoding detail in the public message
Cursor size is boundedAn oversized token is a validation failure before decodingsrc/foundation/pagination/collection-query.schema.spec.ts › rejects a token longer than the maximum cursor size; src/foundation/pagination/cursor/cursor-codec.spec.ts › produces a token within the documented maximum size; test/e2e/my-organizations-query-hardening.e2e-spec.ts › refuses an oversized cursor as a validation failure before decoding it
Keyset order is deterministic and uses a parameterized tuple comparison (no OFFSET)partial: a parameterized tuple comparison and a stable, duplicate-free page walk across pagessrc/foundation/pagination/keyset/keyset-pagination.spec.ts › builds a parameterized ascending tuple comparison for a later forward page; src/foundation/pagination/keyset/keyset-pagination.integration-spec.ts › paginates the full collection across multiple pages in a deterministic order
No OFFSET paging in domain queriesnpm run lint:no-offset-paging fails on a SQL OFFSET inside a string or template literal of a non-test src/ file or in any database/**/*.sql file, and passes on the real tree; it does not see an OFFSET assembled from separate string pieces or a paging done in memorytooling/repository/check-no-offset-paging.spec.ts › reports an OFFSET with a bind parameter inside a query string; tooling/repository/check-no-offset-paging.spec.ts › reports an OFFSET in a SQL file and ignores one that only appears in a comment; tooling/repository/check-no-offset-paging.spec.ts › prints the stable sentinel and exits zero on the real tree
Filters and sorts are bounded to a declared allowlistAn undeclared sort field, SQL-shaped input or an unsafe column name is refusedsrc/foundation/pagination/collection-query.schema.spec.ts › rejects a token for a field that was not declared; src/foundation/pagination/collection-query.schema.spec.ts › rejects arbitrary SQL-shaped input; src/foundation/pagination/keyset/keyset-pagination.spec.ts › rejects a column name that is not a safe SQL identifier
Backward paging and an empty collectionWalking backward returns to the start; an empty filter result is a final empty pagesrc/foundation/pagination/keyset/keyset-pagination.integration-spec.ts › walks a page backward from a later position back to the start of the collection; test/e2e/collections.e2e-spec.ts › returns an empty collection with a final page for a filter matching no rows
Cursor protection, key rotation, size and expiry policy are documented and owner-confirmed (documentation and owner decision, not executable)Documented in RAILWAY_OPERATIONS.md and confirmed in a GitHub comment on issue 13; no test proves a document—

C3 — Validation at the HTTP boundary with Zod and Standard Schema ​

CaseExpected answerProving test
Validation runs through Nest Standard Schema pipes for body, path, query and named headersAn invalid body is a BadRequestException with located field errors; unrelated headers pass throughsrc/foundation/validation/pipe/standard-schema-validation.pipe.spec.ts › throws a BadRequestException on an invalid body; src/foundation/validation/pipe/standard-schema-validation.pipe.spec.ts › reports the failing field with the query location; src/foundation/validation/pipe/standard-schema-validation.pipe.spec.ts › reports the failing field with the path location; src/foundation/validation/pipe/standard-schema-validation.pipe.spec.ts › lets unrelated headers pass through unvalidated
Body validationA missing required field is a body-located field errortest/e2e/validation.e2e-spec.ts › rejects a missing required field as a body-located field error
Query validation, intentional coercion and defaultsA numeric query string is coerced and a default applied; a value outside its range is a query-located errortest/e2e/validation.e2e-spec.ts › coerces a numeric query string into a number and applies a query default; test/e2e/validation.e2e-spec.ts › rejects a query value outside its declared range as a query-located field error
Path validationA path parameter that fails its format is a path-located errortest/e2e/validation.e2e-spec.ts › rejects a path parameter that fails its declared format as a path-located field error; test/e2e/validation.e2e-spec.ts › accepts a valid path parameter
Relevant header validationA named header is validated; a failing one is a header-located error; a missing Idempotency-Key names the fieldtest/e2e/validation.e2e-spec.ts › validates a named header while letting unrelated headers pass through; test/e2e/validation.e2e-spec.ts › rejects a header that fails its declared contract as a header-located field error; test/e2e/idempotency.e2e-spec.ts › answers 400 foundation.validation_failed naming the idempotency-key field when the header is missing
Cursor and limit parameters are validatedA cursor token over the size bound or a bad limit is refused by the query schemasrc/foundation/pagination/collection-query.schema.spec.ts › rejects a token longer than the maximum cursor size; src/foundation/pagination/collection-query.schema.spec.ts › rejects a limit above the documented maximum; test/e2e/my-organizations-query-hardening.e2e-spec.ts › refuses an oversized cursor as a validation failure before decoding it
Coercion is intentional and never silent for money, instants and enumsA decimal is never coerced to a number; a wrong scale, a non-UTC instant and an unknown enum value are refusedsrc/foundation/validation/schemas/decimal.schema.spec.ts › never coerces the value to a JavaScript number; test/e2e/validation.e2e-spec.ts › rejects a decimal amount with the wrong scale instead of coercing it; test/e2e/validation.e2e-spec.ts › rejects a non-UTC instant instead of silently converting it; test/e2e/validation.e2e-spec.ts › rejects an enum value outside the declared const object
Unknown-field policy is strict for body and queryAn unrecognized body field or query parameter is refusedtest/e2e/validation.e2e-spec.ts › rejects an unrecognized field under the strict unknown-field policy; test/e2e/validation.e2e-spec.ts › rejects an unrecognized query parameter under the strict unknown-field policy; src/foundation/validation/pipe/validation-issue.mapper.spec.ts › expands unrecognized keys into one field error per key
Size and range limitsOver-long text, oversized or empty arrays and oversized bodies are refused; NUL characters are refusedtest/e2e/validation.e2e-spec.ts › rejects a field that exceeds its declared size limit; test/e2e/validation.e2e-spec.ts › rejects an items array that exceeds its declared range limit; test/e2e/validation.e2e-spec.ts › rejects an empty items array as failing the minimum range limit; src/foundation/validation/bounded-text.spec.ts › refuses a NUL character with a custom issue; test/e2e/problem-details.e2e-spec.ts › rejects an oversized body as a real 413 without leaking its size or limit
Shared schema primitives reject malformed identifiersA malformed UUID is a validation failure before it reaches the databasetest/e2e/validation.e2e-spec.ts › rejects an identifier that is not a well-formed UUID; test/e2e/sign-in-methods.e2e-spec.ts › refuses a method id that is not a UUID as a validation failure, before it reaches the database; test/e2e/validation.e2e-spec.ts › accepts a body built entirely from shared schema primitives
Validation failures render as C4 Problem Details with one stable code per fieldfoundation.validation_failed with an errors list of located field errorssrc/foundation/validation/pipe/validation-failed.exception.spec.ts › builds a BadRequestException carrying the C4 validation code and the field errors unchanged; src/foundation/validation/pipe/validation-issue.mapper.spec.ts › maps a missing required field to a stable invalid_type code with its dotted field path; src/foundation/validation/pipe/validation-field-path.spec.ts › formats an array index without a leading dot; test/e2e/validation.e2e-spec.ts › rejects a missing required field as a body-located field error
A rejected value is never echoed backA value that looks sensitive does not appear in the response or in the field messagestest/e2e/validation.e2e-spec.ts › never leaks a rejected value that happens to look sensitive; src/foundation/validation/pipe/validation-issue.mapper.spec.ts › never includes the rejected value in the message; test/e2e/sign-up.e2e-spec.ts › never echoes the address or the password it refused
Explicit null stays distinct from an absent optional fieldThe parsed value keeps null and absence aparttest/e2e/validation.e2e-spec.ts › keeps an explicit null distinct from an absent optional field

C4 — Errors: RFC 9457 Problem Details, stable codes, traceId, retryable, errors list and PostgreSQL mapping ​

CaseExpected answerProving test
Every error is application/problem+json, unwrappedMedia type and body shape; no data envelopetest/e2e/problem-details.e2e-spec.ts › renders a controlled HttpException as application/problem+json, unwrapped; src/foundation/errors/problem-details/problem-details.filter.spec.ts › sets the problem+json content type and replies with the built problem
Unmatched routes and every documented status are rendered at their real status404 problem+json inside and outside the API prefix; each documented status keeps its own codetest/e2e/problem-details.e2e-spec.ts › renders an unmatched route as a problem+json 404; test/e2e/problem-details.e2e-spec.ts › renders an unmatched route outside the API prefix as a problem+json 404; src/foundation/errors/problem-details/problem-details.section6-conformance.spec.ts › renders every documented HttpException status at its exact real status code; src/foundation/errors/problem-details/problem-details.builder.spec.ts › derives exactly the documented status-to-code fallback map from the error code registry
A stable machine code is the contractA module-supplied code is forwarded; codes are unique and registered with a statustest/e2e/problem-details.e2e-spec.ts › forwards a module-supplied stable code and retryable flag; src/foundation/errors/registry/error-code.registry.spec.ts › has a unique stable string for every entry; src/foundation/errors/problem-details/problem-details.builder.spec.ts › maps a documented HttpException status to its registered error code
Each code has a registered problem type, title and default detailDistinct type per documented status; non-empty title and detailsrc/foundation/errors/problem-details/problem-details.section6-conformance.spec.ts › gives every documented error status a distinct, registered problem type; src/foundation/errors/problem-details/problem-type.registry.spec.ts › gives every problem type a non-empty title and default detail
instance is the request path onlyThe query string, which can carry cursors or search terms, is droppedsrc/foundation/errors/problem-details/problem-details.filter.spec.ts › keeps only the path in instance, never the query string that can carry cursors or search terms
traceId is present when correlation ran and absent when it did notPresent inside the API prefix; entirely omitted outside ittest/e2e/problem-details.e2e-spec.ts › carries traceId on an unmatched route inside the API prefix, where correlation ran; test/e2e/problem-details.e2e-spec.ts › omits traceId entirely outside the API prefix, where no correlation middleware ran; src/foundation/errors/problem-details/problem-details.builder.spec.ts › carries the current traceId when available
partial: traceId comes from the real active span or the incoming traceparentThe correlation middleware prefers the real active span context; the builder reads the current trace contextsrc/foundation/correlation/trace/trace-context.middleware.spec.ts › prefers the real active OpenTelemetry span context over the raw header; src/foundation/correlation/trace/trace-context.service.spec.ts › prefers a currently active child span over the snapshot captured at request entry
No end-to-end test asserts that the Problem Details traceId equals the active span trace id or the inbound traceparent trace idNot asserted: the e2e only checks that a non-empty string is present—
retryable is a registry decision, true only for transient-limit and in-progress-idempotency codesA caller-supplied retryable flag is ignored; a 429 is retryablesrc/foundation/errors/registry/error-code.registry.spec.ts › marks exactly the transient-limit and in-progress-idempotency codes as retryable; src/foundation/errors/problem-details/problem-details.builder.spec.ts › ignores a caller-supplied retryable:true and keeps the registry non-retryable decision; test/e2e/rate-limit.e2e-spec.ts › refuses the $routeName request that goes past its own limit, counted on this route alone
Retry-After accompanies retryable failuresSet for an in-progress idempotent request and a transient database failure; never for a reused keysrc/foundation/errors/problem-details/problem-details.filter.spec.ts › sets Retry-After for an in-progress idempotent request; src/foundation/errors/problem-details/problem-details.filter.spec.ts › sets Retry-After for a transient database failure answering 503; src/foundation/errors/problem-details/problem-details.filter.spec.ts › never sets Retry-After for a reused idempotency key
Field errors are consistently list-shapedpartial: a well-shaped list is rendered unchanged; a malformed or empty list is dropped; 413 and 422 keep theirstest/e2e/problem-details.e2e-spec.ts › renders a well-shaped field errors list unchanged; src/foundation/errors/registry/http-exception-payload.spec.ts › drops an errors list when any entry is malformed; src/foundation/errors/problem-details/problem-field-error.spec.ts › rejects an unknown property; test/e2e/problem-details.e2e-spec.ts › renders a 422 unprocessable-content HttpException with its own code and field errors preserved; src/foundation/errors/registry/http-exception-payload.spec.ts › drops an empty errors list; test/e2e/problem-details.e2e-spec.ts › renders a 413 payload-too-large HttpException with its own code and field errors preserved
A 415 keeps its field errors listAn HttpException 415 that carries field errors renders them unchanged under meta.errors; the body parser's own charset 415 has no field to report and carries nonetest/e2e/problem-details.e2e-spec.ts › renders a 415 unsupported-media-type HttpException with its field errors preserved
Transient PostgreSQL failures map explicitly to a retryable 503503 service.unavailable for serialization, deadlock, lock timeout and cancel; real serialization failure after retries is 503 with no driver messagesrc/foundation/errors/problem-details/problem-details.builder.spec.ts › answers 503 service.unavailable, retryable, for a transient %s DatabaseError; src/foundation/transactions/transaction-runner.service.integration-spec.ts › answers 503 service.unavailable, retryable, with no driver message once retries are exhausted on a real serialization failure; src/foundation/database/failures/is-transient-database-failure.spec.ts › is true only for serialization failure, deadlock, lock not available and query canceled
Untranslated PostgreSQL errors never become a business answerA unique violation or privilege rejection with no module translation is a sanitized 500; 42501 is not a tenant rejectionsrc/foundation/errors/problem-details/problem-details.builder.spec.ts › sanitizes a real DatabaseError unique violation into an unexpected 500, never a business conflict; test/e2e/problem-details.e2e-spec.ts › sanitizes a raw PostgreSQL 42501 rejection into an unexpected 500 instead of a tenant rejection; src/foundation/errors/problem-details/problem-details.builder.spec.ts › keeps a non-transient DatabaseError, such as insufficient privilege, on the unexpected 500 fallback
SQLSTATE classification is explicit and total over the mapped setEvery mapped SQLSTATE classifies; unmapped or look-alike values do notsrc/foundation/database/failures/classify-database-failure.spec.ts › classifies every mapped SQLSTATE with a constraint; src/foundation/database/failures/classify-database-failure.spec.ts › returns undefined for a DatabaseError carrying an unmapped SQLSTATE; src/foundation/database/failures/database-failures.integration-spec.ts › captures DEADLOCK when two sessions take advisory locks in opposite order; src/foundation/database/failures/classify-database-failure.spec.ts › returns undefined for an object literal carrying a code property that is not a real pg DatabaseError; src/foundation/database/failures/classify-database-failure.spec.ts › returns undefined for a DatabaseError whose code collides with an inherited object member name
Unexpected errors are sanitized: no SQL, constraint names, stack traces, connection details or driver textA safe generic 500; exception evidence never changes the public bodytest/e2e/problem-details.e2e-spec.ts › sanitizes an unexpected error into a safe 500 with no internal detail leaked; src/foundation/errors/problem-details/problem-details.filter.spec.ts › never lets the exception evidence change the public response body; src/foundation/errors/problem-details/problem-details.builder.spec.ts › sanitizes an explicit 500 HttpException and drops its message
Tenant data and stored requests do not leak through errorsA tenant-context rejection is a safe 403; a reused idempotency key does not echo the stored requesttest/e2e/problem-details.e2e-spec.ts › maps a tenant context rejection to a safe 403 with no cause leaked; test/e2e/idempotency.e2e-spec.ts › answers 409 idempotency.key_reused for the same key with a different body, without echoing the stored request
Parser and protocol errors are safe and exactMalformed JSON is a 400, an oversized body a 413 and an unsupported charset a 415, none leaking parser internalstest/e2e/problem-details.e2e-spec.ts › rejects a malformed JSON body as a safe 400 without leaking the parser internals; test/e2e/problem-details.e2e-spec.ts › rejects a request with an unsupported charset as a real 415 without leaking the rejected charset; test/e2e/problem-details.e2e-spec.ts › rejects an oversized body as a real 413 without leaking its size or limit
Modules can publish their own actionable failures but never a server-side oneA module code and type are honored; a 5xx cannot be publishedsrc/foundation/errors/public-failure/public-failure.spec.ts › answers with the code and type its module declared, not with a Foundation one; src/foundation/errors/public-failure/public-failure.spec.ts › refuses to publish a server-side failure, because public means actionable
Logging policy for errors: controlled 4xx are not logged as errors; an unexpected failure is logged onceNo error log for 4xx, validation or cursor rejections; exactly one error log for an unexpected failuresrc/foundation/errors/problem-details/problem-details.filter.spec.ts › does not log a controlled 4xx as an error; src/foundation/errors/problem-details/problem-details.filter.spec.ts › logs exactly once, at error level, with the name, message and a sanitized stack for an unexpected failure
partial: no automatic replay of non-idempotent writesThe transaction runner retries only transient SQLSTATEs, never an indeterminate COMMIT or a constraint error; the e-mail sender never retries by itselfsrc/foundation/transactions/transaction-runner.service.spec.ts › does not retry a real %s DatabaseError (%s); src/foundation/transactions/transaction-runner.service.spec.ts › does not retry when COMMIT rejects with a connection error carrying no SQLSTATE; src/foundation/messaging/messaging.module.spec.ts › never retries by itself, because sending an email cannot be undone
No test asserts that a failed non-idempotent write request is not replayed at the HTTP layer; the runner re-runs a rolled-back unit of work up to 3 times on serialization failureNot asserted: no HTTP-level test of a non-idempotent write that hits a retryable failure—

C5 — Observability: structured logging, OpenTelemetry tracing, metrics, secret protection and bounded export ​

CaseExpected answerProving test
JSON logging sits behind a small Ninaku boundaryOne JSON line per record, info to stdout and error to stderr, with Nest context kept as its own fieldsrc/foundation/logging/ninaku-logger.service.spec.ts › writes info-level records to stdout for log(); src/foundation/logging/ninaku-logger.service.spec.ts › writes error-level records to stderr without a raw top-level stack, while keeping sanitized error evidence; src/foundation/logging/process-logger.spec.ts › writes a valid JSON line carrying event, level and service on stdout for a warn event
Log fields are allowlisted and the line is size-boundedOnly the allowlisted fields are serialized; an oversized or unserializable record degrades to a minimal recordsrc/foundation/logging/log-record.spec.ts › serializes the common allowlisted fields; src/foundation/logging/log-record.spec.ts › includes conditional http, module, operation and error fields only when provided; src/foundation/logging/log-record.spec.ts › falls back to a minimal record when the serialized line would exceed the size budget
Secrets are redacted by field name, by known secret value and by credential patternSecret-named keys are blanket-redacted at any depth; known values and bearer tokens are removed from stringssrc/foundation/logging/redaction/redaction.spec.ts › blanket-redacts values under secret-named keys regardless of type; src/foundation/logging/redaction/redaction.spec.ts › removes a known secret value wherever it appears; src/foundation/logging/redaction/redaction.spec.ts › redacts a bearer token pattern; src/foundation/logging/ninaku-logger.service.spec.ts › redacts a secret nested deep inside an object passed to log()
Every configured secret is registered for redactionDatabase password, signing and encryption keys, messaging credentials and OTLP header valuessrc/foundation/logging/redaction/known-secrets.registry.service.spec.ts › carries every configured secret at once; src/foundation/logging/redaction/known-secrets.registry.service.spec.ts › includes configured OTLP exporter header values
Secret protection in error strings and provider outputIn production no exception message is ever written; only name, code, constraint and framessrc/foundation/logging/redaction/error-disclosure.spec.ts › hides messages for %j; src/foundation/logging/production-error-logging.spec.ts › keeps name, code, constraint, stack frames, request id and trace id for an Error logged through the Nest lifecycle; src/foundation/errors/describe-failure-cause.spec.ts › drops everything a provider error carries besides those three; src/foundation/errors/problem-details/problem-details.filter.production-logging.spec.ts › in production hides the detail of an HttpException that carries the hostile text; src/foundation/config/schema/environment-variables.schema.spec.ts › defaults NODE_ENV to production when absent, the value that hides error messages
Secret protection in automatic telemetry (spans, events, links, URLs, status message)The exporting layer redacts known secrets, sensitive query parameters and the status message before exportsrc/foundation/telemetry/pipelines/redacting-span-exporter.spec.ts › redacts a known secret found inside an ordinary span attribute value; src/foundation/telemetry/pipelines/redacting-span-exporter.spec.ts › redacts a sensitive query parameter inside a url.full attribute while keeping host, path and other parameters; src/foundation/telemetry/pipelines/redacting-span-exporter.spec.ts › redacts a known secret embedded in the span status message; src/foundation/telemetry/pipelines/node-instrumentation.spec.ts › redacts the application sensitive query parameter vocabulary on client and server spans; src/foundation/telemetry/pipelines/tracing-pipeline.spec.ts › exports no span status message when the configured environment is production
Severity policy for access logsinfo for 2xx, warn for a matched 404, error for 500; a healthy probe is not loggedsrc/foundation/telemetry/http/http-telemetry.middleware.spec.ts › logs a matched-route 404 response at warn; src/foundation/telemetry/http/http-telemetry.middleware.spec.ts › logs a 500 response at error; src/foundation/telemetry/http/http-telemetry.middleware.spec.ts › suppresses the log for a successful health check but still records the metric
Duplicate-log policy: one completion line per request and one error log per unexpected failureExactly one http.request.completed line per response id; a close after finish is not counted twicetest/e2e/logging-correlation.e2e-spec.ts › correlates a single response requestId with exactly one http.request.completed log line; src/foundation/telemetry/http/http-telemetry.middleware.spec.ts › does not double count a normal completion when close fires after finish
W3C Trace Context is honored and a valid context exists even with no exporterA valid traceparent is reused, an invalid one is ignored, and a valid span context is generatedsrc/foundation/correlation/trace/trace-id.spec.ts › reuses the trace-id from a valid traceparent header; src/foundation/correlation/trace/trace-id.spec.ts › generates a new trace-id when the header is invalid; src/foundation/telemetry/pipelines/tracing-pipeline.spec.ts › still generates a valid W3C trace/span context when no exporter is configured
Request, trace and span identities are separate and all reach the log lineThe request id is server-generated; trace id and span id come from the trace context; the logger carries all threetest/e2e/request-id.e2e-spec.ts › ignores an incoming X-Request-Id header and generates its own; src/foundation/correlation/trace/trace-context.service.spec.ts › exposes the spanId set for the current run; src/foundation/logging/ninaku-logger.service.spec.ts › carries the requestId, trace_id and span_id from context
A command identity is separate from the request, trace and span identitiesNot asserted: no foundation-level command identity exists in the tested code—
Real instrumented execution: HTTP and PostgreSQL spans share one traceAn HTTP server span and a nested database span share a trace id and parent linktest/e2e/telemetry-correlation.e2e-spec.ts › correlates the response requestId with an HTTP span and a nested database span sharing one trace
A response identifier locates its log lineThe response requestId matches exactly one completion line that carries a trace idtest/e2e/logging-correlation.e2e-spec.ts › correlates a single response requestId with exactly one http.request.completed log line
A response identifier is asserted to equal an identifier on the exported span or on the log line trace idNot asserted: the span test only checks that the header exists and the logging test only checks the trace id is a string—
HTTP metrics are bounded and low cardinalityCount and duration use only method, route template and status class; an unmatched request is labelled unmatched; duration is in secondssrc/foundation/telemetry/http/http-metrics.service.spec.ts › records request count and duration with only bounded attributes; src/foundation/telemetry/http/http-metrics.service.spec.ts › records the duration histogram in seconds, not milliseconds; src/foundation/telemetry/http/http-telemetry.middleware.spec.ts › falls back to an unmatched route label when no route was resolved
Metrics pipeline exports through the configured exporter and paces itselfInstruments record without export when no endpoint is configured; export uses its own intervalsrc/foundation/telemetry/pipelines/metrics-pipeline.spec.ts › records instruments without exporting anything when no endpoint is configured; src/foundation/telemetry/pipelines/metrics-pipeline.spec.ts › exports collected metrics through the configured exporter; src/foundation/telemetry/pipelines/metrics-pipeline.spec.ts › paces export using its own configured interval, not the trace batch schedule delay or export timeout
PostgreSQL client metrics named in the specification (db.client.connections.usage, db.client.queries.duration)Not asserted: only the two HTTP instruments exist in the code; no database metric is registered—
Durable audit is separate from logs and tracesNot asserted by a foundation test: audit records are written by their modules in the tenant transaction—
Tracing and metrics are disabled by default and need no destinationNo endpoint means no exporter; spans are still produced in processsrc/foundation/config/app-config.service.spec.ts › disables the telemetry exporter when no endpoint is configured; src/foundation/config/schema/environment-variables.schema.spec.ts › treats an empty OTEL_EXPORTER_OTLP_ENDPOINT as unset
Sampling is configurableEverything is sampled by default; a ratio of zero drops every span before exportsrc/foundation/telemetry/pipelines/tracing-pipeline.spec.ts › keeps sampling every span by default, matching the previous AlwaysOnSampler behavior; src/foundation/telemetry/pipelines/tracing-pipeline.spec.ts › drops every span before export when the sampler ratio is configured to zero
Export is bounded: queue, batch size and timeoutspartial: with the destination down the exporter never sees more than one batch and shutdown completes within a second; configuration cannot set a batch above the queuesrc/foundation/telemetry/pipelines/tracing-pipeline.spec.ts › keeps producing spans and stays within the bounded queue when the exporter destination is down; src/foundation/config/schema/environment-variables.schema.spec.ts › rejects OTEL_BSP_MAX_EXPORT_BATCH_SIZE greater than OTEL_BSP_MAX_QUEUE_SIZE naming both variables
The span queue never holds more than its configured maximum while the destination is downA burst of 200 spans against a queue of 20 delivers at most the queue plus one batch (25) to the exporter, whether it rejects every export or never answers; the rest are droppedsrc/foundation/telemetry/pipelines/tracing-pipeline.spec.ts › drops the spans beyond the configured queue size while the destination rejects every export; src/foundation/telemetry/pipelines/tracing-pipeline.spec.ts › drops the spans beyond the configured queue size while the destination never answers
Export retries are boundedNot asserted: no test counts or bounds export retry attempts—
Shutdown is bounded and reportedEach pipeline shuts down within OTEL_SHUTDOWN_TIMEOUT_MS even if the exporter never resolves; an incomplete shutdown is reportedsrc/foundation/telemetry/pipelines/bounded-shutdown.spec.ts › resolves with the timed-out outcome when the work never settles; src/foundation/telemetry/pipelines/tracing-pipeline.spec.ts › bounds shutdown even when the underlying exporter never resolves; src/foundation/telemetry/pipelines/metrics-pipeline.spec.ts › bounds shutdown even when the underlying exporter never resolves; src/foundation/telemetry/telemetry-bootstrap.shutdown-report.spec.ts › reports each pipeline that could not finish its shutdown, naming the pipeline and the outcome; src/foundation/config/schema/environment-variables.schema.spec.ts › rejects OTEL_SHUTDOWN_TIMEOUT_MS shorter than OTEL_EXPORT_TIMEOUT_MS naming both variables, so a full export attempt is never truncated by shutdown
Process shutdown drains telemetry after closing the application, even when close failsClose first, then drain; the original close failure is propagated and the drain still runssrc/bootstrap/graceful-shutdown.spec.ts › closes the application before draining telemetry; src/bootstrap/graceful-shutdown.spec.ts › still drains telemetry when app.close() rejects; src/bootstrap/graceful-shutdown.spec.ts › stays bounded even when the telemetry drain is slow, because telemetry.shutdown is itself time-boxed
Volume, access and retention budgets for logs and traces are approved (owner decision, operational)Recorded as pending in the issue; no test applies—
Compatible OpenTelemetry packages and the storage destination are evidenced and approved (operational)Packages are pinned; the destination is an open owner decision; nothing is exported today—

C6 — OpenAPI: generation, validation, examples, runtime agreement and incompatible-change detection ​

CaseExpected answerProving test
The document is generated from the real, decorated routes under the served prefix and versionOnly routes that exist are documented, at the path the app servestooling/openapi/openapi-document.spec.ts › documents the real, decorated route exactly as declared; tooling/openapi/openapi-document.spec.ts › documents a real route under the same global prefix and version the running app serves it at
Generation is deterministicSame bytes regardless of key order; one trailing newlinetooling/openapi/serialize-document.spec.ts › produces the same bytes regardless of key insertion order; tooling/openapi/serialize-document.spec.ts › ends with exactly one trailing newline
The committed artifact must match the code byte for byte (openapi:check)A missing or different committed file is a mismatchtooling/openapi/check-openapi-document.spec.ts › reports a mismatch when the committed file differs; tooling/openapi/check-openapi-document.spec.ts › reports a match when the committed file is byte-identical; tooling/openapi/check-openapi-document.spec.ts › reports a mismatch when the committed file does not exist
Shared components derive from the Zod schemas that validate at runtimePageInfo, ProblemDetails, the envelope, cursor and limit parameters and X-Request-Id are generated, not hand-writtentooling/openapi/shared-components.spec.ts › derives PageInfo from the Zod schema, not a hand-written duplicate; tooling/openapi/shared-components.spec.ts › derives ProblemDetails with an optional traceId and meta.errors list; tooling/openapi/shared-components.spec.ts › declares a generic Envelope schema whose meta carries no request id and no time, so a strong ETag names one exact body; tooling/openapi/shared-components.spec.ts › documents limit, after and before as reusable query parameters; tooling/openapi/shared-components.spec.ts › documents X-Request-Id as a reusable response header
Routes document the envelope under the status they really return, and error responses as Problem DetailsSingle resource, collection with required pageInfo, a skipped envelope, and one Problem Details body per documented statustest/fixtures/openapi-envelope/openapi-envelope-document.spec.ts › documents a collection as an array under data with meta.pageInfo required; test/fixtures/openapi-envelope/openapi-envelope-document.spec.ts › documents the envelope under the status the route actually returns; test/fixtures/openapi-envelope/openapi-envelope-document.spec.ts › leaves a route that skips the envelope documented without one; src/foundation/openapi/api-problem-response.decorator.spec.ts › documents each status as a Problem Details body with its own description
Header and tenant parameters are documented (Idempotency-Key, X-Organization-Id, bearer scheme)Required headers appear as parameters; the bearer scheme is declaredsrc/foundation/openapi/api-idempotency-key-header.decorator.spec.ts › documents a required, self-contained Idempotency-Key header parameter describing only strings; src/foundation/openapi/api-organization-id-header.decorator.spec.ts › documents a required X-Organization-Id header; tooling/openapi/openapi-document.spec.ts › declares the bearer scheme the authenticated routes point at
partial: the document is validated as a well-formed OpenAPI documentRequired fields and sections are parsed and tag usage is consistent; no check against the full OpenAPI schematooling/openapi/parse-openapi-document.spec.ts › refuses a document without paths; tooling/openapi/openapi-tag-consistency.spec.ts › flags an operation tag that the document never declares
Examples are generated and valid for the documented schemasNot asserted: the document contains examples but no test requires or validates them—
Runtime agreement for the health probesReal /health/live and /health/ready bodies validate with ajv against the schemas pulled from the generated document; the X-Request-Id header is documentedtest/e2e/openapi-contract.e2e-spec.ts › the real /health/live response matches its documented schema; test/e2e/openapi-contract.e2e-spec.ts › the real /health/ready response matches its documented schema; test/e2e/openapi-contract.e2e-spec.ts › the real /health/live response carries the documented X-Request-Id header
Runtime agreement for the success envelope, a paginated collection and a Problem Details errorNot asserted: no test validates a real enveloped, paginated or error response against the generated document—
Incompatible changes are detected across paths, operations, parameters, request bodies, responses, headers and schemasRemoved paths, operations, parameters, required properties, nullable, oneOf members and headers are breaking; additions are additivetooling/openapi/openapi-compatibility.spec.ts › classifies a removed path as breaking; tooling/openapi/openapi-compatibility.spec.ts › catches a removed required request-body property between two full documents; tooling/openapi/openapi-compatibility.spec.ts › catches a removed nullable modifier between two full documents; tooling/openapi/openapi-compatibility.spec.ts › catches a removed oneOf member between two full documents; tooling/openapi/openapi-compatibility.spec.ts › catches a removed documented response header between two full documents; tooling/openapi/response-compatibility.spec.ts › classifies a removed response code as breaking; tooling/openapi/parameter-compatibility.spec.ts › classifies a newly added required parameter as breaking
The classifier never silently reports no changeA pair with many breaking edits is never reported as unchangedtooling/openapi/openapi-compatibility.spec.ts › never silently returns no changes for a document pair with many breaking edits
Policy: a breaking change fails unless the major version is raisedBlocked without a major bump, including minor or patch only; passes with one; additive passestooling/openapi/openapi-compatibility-policy.spec.ts › fails when a breaking change is present and the major version was not bumped; tooling/openapi/openapi-compatibility-policy.spec.ts › passes when a breaking change is present and the major version was bumped; tooling/openapi/openapi-compatibility-policy.spec.ts › still blocks a breaking change when only the minor or patch version changed; tooling/openapi/openapi-compatibility-policy.spec.ts › passes when only additive changes are present, without a version bump
The baseline comes from a git ref with explicit handling of a missing baseline or an unresolvable refA missing document and an unresolvable ref are reported distinctly; the default ref is origin/stagingtooling/openapi/check-openapi-compatibility.spec.ts › reports a missing baseline when the ref exists but the document does not; tooling/openapi/check-openapi-compatibility.spec.ts › reports an evaluation error when the ref itself does not resolve; tooling/openapi/check-openapi-compatibility.spec.ts › defaults to origin/staging when nothing else is given
The openapi-compatibility job is a required check in the branch rulesets (operational, repository policy)The job runs on every pull request but is not a required check; a ruleset change is an owner decision—
Test fixtures for endpoint families that do not exist are not documentedThe C2 collections fixture never appears in the generated documenttest/e2e/openapi-contract.e2e-spec.ts › never documents the C2 test-only collections fixture
Test fixtures are not mounted in a production applicationNot asserted by a test: fixtures are only imported by test applications—

D1 — Slice D: reproducible k6 and staging baseline ​

CaseExpected answerProving test
Percentile statistics are computed correctlyp50, p90, p95, p99 and max over sorted samples; empty input is an errortooling/performance/measurement/percentiles.spec.ts › computes percentiles over a larger unsorted sample set; tooling/performance/measurement/percentiles.spec.ts › throws when given an empty sample array
The measurement plan is valid and warmup is discardedPositive measured counts; warmup timings are dropped; multi-client samples are pooledtooling/performance/measurement/measurement-plan.spec.ts › rejects a zero measuredIterations, because computePercentileStatistics requires at least one sample; tooling/performance/measurement/scenario-runner.spec.ts › discards warmup timings and keeps exactly measuredIterations samples; tooling/performance/measurement/scenario-runner.spec.ts › pools every client’s measured samples into one percentile computation
Results are recorded as a reproducible documentMarkdown and JSON carry metadata and the measurementstooling/performance/reporting/result-formatting.spec.ts › assembles metadata and a copy of the measurements; tooling/performance/reporting/result-formatting.spec.ts › serializes the document as indented JSON that round-trips
A k6 baseline measures the available Foundation paths against staging (operational)Not done: the existing perf:http run is a single development machine, not k6 and not staging—
The record names exact commit, environment, dataset, load, thresholds, results and limitations for the staging run (operational)Not done for staging—
The baseline includes validation, logging, tracing and PostgreSQL overhead on the Foundation paths (operational)Not done: no staging measurement with tracing and logging enabled—
The baseline runs after C1 to C6 are deployed to staging in a low-traffic window (operational)Not done—

AE1 — Acceptance evidence: successes, errors, collections and headers match one contract, including the exceptions ​

CaseExpected answerProving test
Successes match the envelope contract{ data } or { data, meta } with requestId and timestamptest/e2e/http-envelope.e2e-spec.ts › wraps a plain success response in { data }
Errors match the Problem Details contractapplication/problem+json with type, title, status, detail, instance and codetest/e2e/problem-details.e2e-spec.ts › renders a controlled HttpException as application/problem+json, unwrapped
Collections match the pagination contractmeta.pageInfo on every pagetest/e2e/collections.e2e-spec.ts › walks the full collection across pages with a deterministic order
Headers match on success and on errorX-Request-Id is present on a success and on an error responsetest/e2e/request-id.e2e-spec.ts › sets a generated x-request-id response header; test/e2e/problem-details.e2e-spec.ts › keeps X-Request-Id on an error response
The declared exceptions match their documented contractThe health probes validate against their documented schemastest/e2e/openapi-contract.e2e-spec.ts › the real /health/ready response matches its documented schema
One test checks success, error and collection responses against the same generated documentNot asserted (see the runtime agreement gap in C6)—

AE2 — Acceptance evidence: altered, incompatible, expired or unauthorized cursors are rejected consistently ​

CaseExpected answerProving test
Altered cursorcollection.cursor_tamperedtest/e2e/collections.e2e-spec.ts › rejects a cursor whose signature was altered as collection.cursor_tampered; test/e2e/my-organizations-query-hardening.e2e-spec.ts › refuses a cursor whose payload was altered as collection.cursor_tampered
Incompatible cursorcollection.cursor_incompatibletest/e2e/collections.e2e-spec.ts › rejects a cursor reused with a different filter as collection.cursor_incompatible
Expired cursorcollection.cursor_expiredtest/e2e/collections.e2e-spec.ts › rejects an expired cursor as collection.cursor_expired
Unauthorized cursor (another tenant)collection.cursor_incompatibletest/e2e/collections.e2e-spec.ts › rejects a cursor reused under a different tenant context as collection.cursor_incompatible
Malformed or oversized cursorcollection.cursor_invalid or a validation failuretest/e2e/collections.e2e-spec.ts › rejects a structurally malformed after cursor as collection.cursor_invalid; test/e2e/my-organizations-query-hardening.e2e-spec.ts › refuses an oversized cursor as a validation failure before decoding it

AE3 — Acceptance evidence: concurrent requests cannot leak tenant, request or trace context ​

CaseExpected answerProving test
Request ids stay independent across concurrent requestsDistinct idstest/e2e/request-id.e2e-spec.ts › assigns independent request-ids to concurrent requests; src/foundation/correlation/request-context.service.spec.ts › isolates concurrent runs from each other
Request and trace ids stay with their own log lines across concurrent requestsOne line per request, distinct request and trace idstest/e2e/logging-correlation.e2e-spec.ts › does not mix requestId or trace_id between concurrent requests
Trace context is isolated between concurrent runsIsolated per runsrc/foundation/correlation/trace/trace-context.service.spec.ts › isolates concurrent runs from each other
Error evidence is not mixed between concurrent failuresEach failure logs its own request and trace idssrc/foundation/errors/problem-details/problem-details.filter.spec.ts › does not mix request and trace identifiers between two concurrent unexpected failures
Tenant context does not survive on a pooled connectionCleared after commit, rollback and a later contextsrc/foundation/tenancy/tenant-context-runner.service.integration-spec.ts › does not leak tenant GUCs to the next transaction on the same pooled connection after commit; src/foundation/transactions/tenant-transaction-runner.service.integration-spec.ts › does not let an impersonation value from one run survive into a later run on the same pooled connection; src/foundation/tenancy/tenant-context-runner.service.integration-spec.ts › does not leak tenant GUCs to the next transaction after a rollback; src/foundation/tenancy/tenant-context-runner.service.integration-spec.ts › does not let an impersonation value survive into a later application of a different context in the same transaction
Two tenants calling concurrently over HTTP never see each other contextNot asserted: no e2e test fires concurrent requests from two tenants and compares the answers—

AE4 — Acceptance evidence: deliberately inserted test secrets are absent from public responses, serialized logs and exported traces ​

CaseExpected answerProving test
Public responses omit a rejected sensitive valueValidation and refusal responses never echo ittest/e2e/validation.e2e-spec.ts › never leaks a rejected value that happens to look sensitive; test/e2e/sign-up.e2e-spec.ts › never echoes the address or the password it refused
Public error responses omit internalsA sanitized 500 carries no internal detailtest/e2e/problem-details.e2e-spec.ts › sanitizes an unexpected error into a safe 500 with no internal detail leaked
Serialized logs omit known secrets and secret-named fieldsRedacted in structured events, nested objects and error messagessrc/foundation/logging/ninaku-logger.service.spec.ts › redacts known secrets from structured event fields; src/foundation/logging/ninaku-logger.service.spec.ts › redacts a password field on a structured object passed to log(); src/bootstrap/graceful-shutdown.spec.ts › redacts a known secret carried in the error message when wired to a real process logger
Exported traces omit known secrets and sensitive query parametersRedacted in attributes, events, links, URLs and the status messagesrc/foundation/telemetry/pipelines/redacting-span-exporter.spec.ts › redacts a known secret found inside an ordinary span attribute value; src/foundation/telemetry/pipelines/redacting-span-exporter.spec.ts › redacts a known secret embedded inside a recorded event attribute; src/foundation/telemetry/pipelines/redacting-span-exporter.spec.ts › redacts sensitive attributes carried on a span link; src/foundation/telemetry/pipelines/redacting-span-exporter.spec.ts › redacts a sensitive query parameter inside a url.full attribute while keeping host, path and other parameters; src/foundation/telemetry/pipelines/redacting-span-exporter.spec.ts › redacts a known secret embedded in the span status message
One end-to-end test inserts a secret into a real request and inspects the response, the log lines and the exported spans togetherNot asserted: each layer is proven separately by unit and e2e tests—

AE5 — Acceptance evidence: a response identifier locates its logs and instrumented execution in staging ​

CaseExpected answerProving test
In process: the response identifier locates its log lineExactly one completion line per request idtest/e2e/logging-correlation.e2e-spec.ts › correlates a single response requestId with exactly one http.request.completed log line
In process: the request produces correlated HTTP and database spansOne trace, parent link from the database span to the HTTP spantest/e2e/telemetry-correlation.e2e-spec.ts › correlates the response requestId with an HTTP span and a nested database span sharing one trace
In staging: an identifier from a real response is looked up in staging logs and traces (operational)Not done: no telemetry destination is configured in any environment—

AE6 — Acceptance evidence: a failed telemetry destination cannot cause unbounded queues or memory, and shutdown is tested ​

CaseExpected answerProving test
Trace export outage keeps the queue and batch bounded and the API alivepartial: spans keep being produced and the exporter never sees more than one batchsrc/foundation/telemetry/pipelines/tracing-pipeline.spec.ts › keeps producing spans and stays within the bounded queue when the exporter destination is down
Trace export outage keeps the span queue boundedAt most the queue plus one batch reaches the exporter; the rest are dropped (queue length is not read directly and process memory is not measured)src/foundation/telemetry/pipelines/tracing-pipeline.spec.ts › drops the spans beyond the configured queue size while the destination rejects every export; src/foundation/telemetry/pipelines/tracing-pipeline.spec.ts › drops the spans beyond the configured queue size while the destination never answers
Shutdown with a hanging exporter is bounded and reportedTimed-out outcome and a shutdown warning naming the componentsrc/foundation/telemetry/pipelines/bounded-shutdown.spec.ts › emits a WARN telemetry.shutdown_incomplete event naming the component and outcome when shutdown timed out; src/foundation/telemetry/pipelines/tracing-pipeline.spec.ts › bounds shutdown even when the underlying exporter never resolves
Process shutdown stays bounded when the telemetry drain is slowResolves within the boundsrc/bootstrap/graceful-shutdown.spec.ts › stays bounded even when the telemetry drain is slow, because telemetry.shutdown is itself time-boxed
A metrics export outage keeps memory boundedNot asserted: only metrics shutdown is tested against a hanging exporter—
A real destination outage in staging (operational)Not done: no destination exists—

AE7 — Acceptance evidence: OpenAPI matches runtime and incompatible changes are detected ​

CaseExpected answerProving test
The committed document is checked against the codeA different committed file is a mismatchtooling/openapi/check-openapi-document.spec.ts › reports a mismatch when the committed file differs
Incompatible changes are detected and gated by the major versionA breaking change without a major bump failstooling/openapi/openapi-compatibility-policy.spec.ts › fails when a breaking change is present and the major version was not bumped
Runtime responses match the document for the health probesajv validation of the real bodytest/e2e/openapi-contract.e2e-spec.ts › the real /health/live response matches its documented schema
Runtime responses match the document for the envelope, collections and errorsNot asserted (see C6)—

AE8 — Acceptance evidence: cursor policy and telemetry destination, access, retention and cost are settled in C2 and C5 ​

CaseExpected answerProving test
Cursor expiry and size limits are enforced as configuredExpired and oversized cursors are refusedsrc/foundation/pagination/cursor/cursor-codec.spec.ts › rejects an expired cursor; src/foundation/pagination/cursor/cursor-codec.spec.ts › rejects a token longer than the maximum size as invalid
Cursor policy confirmed by the owner (decision, not executable)Confirmed in a GitHub comment on issue 13 and documented in RAILWAY_OPERATIONS.md—
Telemetry destination, access, retention and budget ceiling (owner decision, operational)Pending: three open owner questions, nothing exported today—
Sampling policy for production (owner decision)Pending: sampling is configurable and defaults to everything—

AE9 — Acceptance evidence: staging deployment and promotion (operational) ​

CaseExpected answerProving test
A deployed to staging with CI green and the api healthcheck passingRecorded in issue comments only; no repository-linked executable evidence—
C1 deployed to staging with CI green and the api healthcheck passingRecorded in issue comments only; no repository-linked executable evidence—
C2 deployed to staging with CI green and the api healthcheck passingRecorded in issue comments only; no repository-linked executable evidence—
C3 deployed to staging with CI green and the api healthcheck passingRecorded in issue comments only; no repository-linked executable evidence—
C4 deployed to staging with CI green and the api healthcheck passingRecorded in issue comments only; no repository-linked executable evidence—
C5 deployed to staging with CI green and the api healthcheck passingRecorded in issue comments only; no repository-linked executable evidence—
C6 deployed to staging with CI green and the api healthcheck passingRecorded in issue comments only; no repository-linked executable evidence—
Staging readiness is green as ninaku_runtime for the exit conditionOperational; not provable by a repository test—

Gaps ​

Every — row above, grouped by item. Each line is a clause no test proves today; none is claimed.

  • A1 (2) — Railway runtime database settings are declared as code and railway config plan reports no drift (operational); Integration and e2e run against PostgreSQL 18 prepared with db:prepare (operational).
  • C1 (2) — No controller returns an envelope by itself (by design the interceptor wraps every plain object, so a payload with a data field is wrapped; what is unproven is that no controller returns an envelope by mistake); Identifiers travel as strings in the responses of every other route (only GET /organizations is asserted). Partial rows: 2.
  • C2 (1) — Cursor protection, key rotation, size and expiry policy are documented and owner-confirmed (documentation and owner decision, not executable). Partial rows: 1.
  • C4 (2) — No end-to-end test asserts that the Problem Details traceId equals the active span trace id or the inbound traceparent trace id; No test asserts that a failed non-idempotent write request is not replayed at the HTTP layer; the runner re-runs a rolled-back unit of work up to 3 times on serialization failure. Partial rows: 3.
  • C5 (7) — A command identity is separate from the request, trace and span identities; A response identifier is asserted to equal an identifier on the exported span or on the log line trace id; PostgreSQL client metrics named in the specification (db.client.connections.usage, db.client.queries.duration); Durable audit is separate from logs and traces; Export retries are bounded; Volume, access and retention budgets for logs and traces are approved (owner decision, operational); Compatible OpenTelemetry packages and the storage destination are evidenced and approved (operational). Partial rows: 1.
  • C6 (4) — Examples are generated and valid for the documented schemas; Runtime agreement for the success envelope, a paginated collection and a Problem Details error; The openapi-compatibility job is a required check in the branch rulesets (operational, repository policy); Test fixtures are not mounted in a production application. Partial rows: 1.
  • D1 (4) — A k6 baseline measures the available Foundation paths against staging (operational); The record names exact commit, environment, dataset, load, thresholds, results and limitations for the staging run (operational); The baseline includes validation, logging, tracing and PostgreSQL overhead on the Foundation paths (operational); The baseline runs after C1 to C6 are deployed to staging in a low-traffic window (operational).
  • AE1 (1) — One test checks success, error and collection responses against the same generated document.
  • AE3 (1) — Two tenants calling concurrently over HTTP never see each other context.
  • AE4 (1) — One end-to-end test inserts a secret into a real request and inspects the response, the log lines and the exported spans together.
  • AE5 (1) — In staging: an identifier from a real response is looked up in staging logs and traces (operational).
  • AE6 (2) — A metrics export outage keeps memory bounded; A real destination outage in staging (operational). Partial rows: 1.
  • AE7 (1) — Runtime responses match the document for the envelope, collections and errors.
  • AE8 (3) — Cursor policy confirmed by the owner (decision, not executable); Telemetry destination, access, retention and budget ceiling (owner decision, operational); Sampling policy for production (owner decision).
  • AE9 (8) — A deployed to staging with CI green and the api healthcheck passing; C1 deployed to staging with CI green and the api healthcheck passing; C2 deployed to staging with CI green and the api healthcheck passing; C3 deployed to staging with CI green and the api healthcheck passing; C4 deployed to staging with CI green and the api healthcheck passing; C5 deployed to staging with CI green and the api healthcheck passing; C6 deployed to staging with CI green and the api healthcheck passing; Staging readiness is green as ninaku_runtime for the exit condition.

Application Foundation in progress. Tracked in issue #13.