Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions packages/metadata/src/metadata-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2161,9 +2161,14 @@ export class MetadataManager implements IMetadataService {
* already resolve the `metadata` service for the environment they serve —
* adding one here would create a second scoping mechanism.
*
* Nothing reaches this method over HTTP in 17.x: the dispatcher seam is
* #5090's, and publish still rejects a non-empty `apis:` (#4936), so the
* whole path is structurally unreachable until the #5040 E7 flip.
* This method is reached over HTTP on a real boot. The dispatcher seam
* landed as #5090 (`packages/runtime/src/api-endpoint-step.ts`, called from
* the `setFallbackHandler` the dispatcher plugin installs), and #4936's
* wholesale publish refusal of a non-empty `apis:` was replaced by the
* #5040 E7 per-shape gates (`packages/spec/src/api/endpoint-publish-gate.ts`)
* — so declarations exist and requests arrive here. The showcase's two
* declared endpoints are matched and executed through this path in
* `packages/qa/dogfood/test/showcase-declarative-endpoints.dogfood.test.ts`.
*
* @throws when the metadata store cannot be read — an outage must never be
* reported as a miss, because a miss becomes a 404.
Expand Down
13 changes: 8 additions & 5 deletions packages/rest/src/openapi-endpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@
/**
* #5040 E6 — declared endpoints in the OpenAPI document.
*
* Two jobs, and the second one is the load-bearing one TODAY:
* Two jobs:
*
* 1. the positive shapes, driven straight through the pure enrichment with
* parsed declarations (publish still refuses to let any of them exist, so
* there is no boot that could exercise them end to end yet);
* parsed declarations. Since the #5040 E7 publish flip these are the LIVE
* path — a real showcase boot serves an `/openapi.json` carrying its two
* declared endpoints
* (`packages/qa/dogfood/test/showcase-declarative-endpoints.dogfood.test.ts`);
* the cases here pin the projection itself, apart from any boot;
* 2. the empty-set invariant — with no declarations the document must come
* back not merely equivalent but IDENTICAL, because that is the entire
* live-behaviour claim this change makes until the E7 flip.
* back not merely equivalent but IDENTICAL, which is what keeps a
* deployment that declares nothing byte-for-byte unchanged.
*/

import { describe, it, expect, vi } from 'vitest';
Expand Down
24 changes: 17 additions & 7 deletions packages/rest/src/openapi-endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,20 @@
* the mirror; it is not built here because Prime Directive #2 keeps logic out
* of `packages/spec` and the frozen vocabulary is not this unit's to widen.
*
* ## Today it emits nothing
* ## What it emits today
*
* Publish/validate still rejects a non-empty `apis:` until the E7 flip, so the
* enumeration yields an empty set and {@link enrichOpenApiWithEndpoints}
* returns its input document BY REFERENCE — the served bytes are identical to
* before this change. That invariant is pinned by a test rather than argued.
* Real documents. The #5040 E7 publish flip
* (`packages/spec/src/api/endpoint-publish-gate.ts`) ended the wholesale
* refusal of a non-empty `apis:`, so the enumeration is no longer empty on a
* deployment that declares endpoints: a real showcase boot serves an
* `/openapi.json` describing its two declared endpoints
* (`packages/qa/dogfood/test/showcase-declarative-endpoints.dogfood.test.ts`).
*
* The empty-set case is still exact rather than approximate — with nothing to
* add, {@link enrichOpenApiWithEndpoints} returns its input document BY
* REFERENCE — which is what keeps a deployment that declares no endpoint
* byte-identical to one built before this module existed. That invariant is
* pinned by a test rather than argued.
*/

import { ApiEndpointSchema, type ApiEndpoint } from '@objectstack/spec/api';
Expand Down Expand Up @@ -328,8 +336,10 @@ export function selectDocumentableEndpoints(
* Fold declared endpoints into an OpenAPI document's `paths`.
*
* Returns `doc` ITSELF when there is nothing to add — that is what makes the
* empty-set case byte-identical rather than merely equivalent, and it is the
* state of the world until the E7 flip lets a non-empty `apis:` publish.
* empty-set case byte-identical rather than merely equivalent, which is the
* state a deployment declaring no endpoint stays in. Since the E7 flip let a
* non-empty `apis:` publish, the other branch is the live one wherever
* endpoints are declared.
*
* A declaration never displaces a built-in: if the document already describes
* the same path+method, the built-in keeps it and the declaration is reported.
Expand Down
8 changes: 5 additions & 3 deletions packages/rest/src/rest-openapi-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,11 @@ describe('GET /api/v1/openapi.json — endpoint enrichment', () => {
});

it('serves a document identical to the pre-#5093 one while no endpoint is declared', async () => {
// The load-bearing invariant: publish rejects a non-empty `apis:` until the
// E7 flip, so this is the ONLY state that exists in production today, and
// the change is required to be invisible in it. Compared against the same
// The load-bearing invariant: a deployment that declares no endpoint must
// not be able to tell the enrichment step exists. Since the #5040 E7
// publish flip this is no longer the only state in production — endpoints
// do publish — which is exactly why the no-declaration state needs pinning
// rather than assuming. Compared against the same
// handler fed a protocol with no `api` capability at all — i.e. the world
// exactly as it was before the enrichment step existed.
const withEmptyApis = await serveOpenApi(makeProtocol({ object: [], api: [] }).protocol);
Expand Down
10 changes: 6 additions & 4 deletions packages/rest/src/rest-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2862,10 +2862,12 @@ export class RestServer {
// ADR-0076): declared endpoints join it here rather than
// growing a second generator somewhere else.
//
// Until the E7 flip a non-empty `apis:` cannot publish, so
// the enumeration is empty and `enrichOpenApiWithEndpoints`
// hands `enriched` straight back — the served bytes today
// are exactly the ones served before this change.
// Since the E7 flip a non-empty `apis:` publishes, so this
// enumeration returns real declarations on a deployment that
// has them and the document grows a path entry per endpoint.
// Where nothing is declared the enumeration is empty and
// `enrichOpenApiWithEndpoints` hands `enriched` straight
// back, byte for byte.
try {
const apiResult = await protocol?.getMetaItems?.({ type: 'api' });
const apiItems: unknown[] = Array.isArray((apiResult as any)?.items)
Expand Down
6 changes: 4 additions & 2 deletions packages/runtime/src/api-endpoint-step.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
*
* Every case here is about ONE question: when does this step answer, and when
* does it write nothing so the transport's existing unmatched answer stands?
* Getting that wrong in either direction is a live behavior change on a surface
* that is supposed to be inert until the #5040 E7 flip.
* Getting that wrong in either direction is a live behavior change — and since
* the #5040 E7 publish flip it is live for real traffic, not just in principle:
* endpoints can be declared, so a step that answers when it should stay silent
* now shadows the transport's 404 on a deployment.
*
* `matchEndpoint` is driven by a stub implementing the contract in
* `@objectstack/spec/contracts` — deliberately, not by the real matcher: that
Expand Down
13 changes: 8 additions & 5 deletions packages/runtime/src/api-endpoint-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@
* ## What it does today, and what it does not
*
* On a match it runs the WHOLE chain: policies (#5040 E4) and then target
* execution (#5040 E5), wired together here by E5b. It is still structurally
* unreachable — a non-empty `apis:` is rejected at publish / validate until the
* E7 flip — so no deployment can observe it; the tests drive `matchEndpoint`
* through a stub, exactly as #5040 §5 prescribes for every E-series unit that
* lands before the flip.
* execution (#5040 E5), wired together here by E5b. Since the E7 publish flip
* (`packages/spec/src/api/endpoint-publish-gate.ts`) a non-empty `apis:` is no
* longer refused wholesale — only shape by shape — so a deployment CAN observe
* this step: the showcase declares two endpoints and a real boot serves them
* (`packages/qa/dogfood/test/showcase-declarative-endpoints.dogfood.test.ts`).
* The tests here still drive `matchEndpoint` through a stub, which is what
* keeps the decision (when to answer, when to write nothing) testable apart
* from the wiring.
*
* ## The chain, in the one order it can run in
*
Expand Down
7 changes: 4 additions & 3 deletions packages/runtime/src/api-mapping.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@
* applied. `transform` is the case #5137 was filed over, but an unusable
* path and colliding targets are the same category and answer identically.
*
* And the property that makes both safe to land before the E7 flip: with no
* declaration, the value that goes in is the value that comes out, by
* reference.
* And the identity property both rest on: with no declaration, the value that
* goes in is the value that comes out, by reference. That is what made these
* keys safe to land ahead of the #5040 E7 publish flip, and it is still what
* keeps an endpoint declaring no mapping free of any projection cost.
*/

import { describe, it, expect } from 'vitest';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@
* rides a success and never an error.
*
* The load-bearing assertion in most of these is a NEGATIVE one: that adding
* this seam changed nothing for anybody. Today's unmatched answers — the bare
* 404 and the 405 + `Allow` — must come back byte for byte, since a stack
* cannot declare an endpoint at all until the E7 flip.
* this seam changed nothing for anybody who did not ask for it. The unmatched
* answers — the bare 404 and the 405 + `Allow` — must come back byte for byte
* for every path no declaration owns. Since the #5040 E7 publish flip that is
* the assertion's whole weight: stacks CAN declare endpoints now, so "the
* fallback stays silent unless a declaration matches" is a promise to live
* deployments rather than a property of a surface nothing could reach.
*
* NOTE on the body guarantee: that the fallback receives a READABLE `req.body`
* (the difference from the `use()` middleware seam) is a transport promise, and
Expand Down
Loading
Loading