Found while implementing #14879 (the client SDK half of this same key). Not that PR's change and not addressed there; filed so it is not buried.
Measured, on a live server
Booting createRestApiPlugin with crud: { dataPrefix: '/objects' } and reading GET /api/v1/discovery, the one document reports the data routes in two places and they disagree:
Producer
packages/metadata-protocol/src/protocol.ts:5740 writes route: '/api/v1/data' as a literal into the services.data slot. The REST discovery handler's substitution pass only rewrites discovery.routes.* -- routes.data, routes.metadata, routes.ui, routes.mcp -- so the services block is never brought in line with the mounted paths and keeps the conventional literal whatever the deployment configured.
(Locate by symbol, not by line -- rest-server.ts line numbers have been moving.)
Why this is a defect and not an observation
The liveness ledger classifies crud_endpoints' dataPrefix live because it "has five independent consumers and moves the mounted paths and the advertised discovery document together", and #14691 retired patterns and objectParamStyle on the reasoning that "the mounted CRUD paths are the contract the client SDK, the discovery document and /openapi.json all describe -- a per-operation pattern knob could only make them lie." Here the discovery document lies to itself: a reader that trusts services.data.route is pointed at an unmounted path by the very document whose job is to say where the routes are.
Nearest neighbour is #4318 (discovery slots declaring routes that contradict their own handlerReady), same block, different subject -- that one is about routes that do not exist at all, this one is about a route that exists somewhere else.
Why it was not fixed in the #14879 PR
Scope: #14879's face is the client SDK, and this producer is in packages/metadata-protocol. The in-place exemption also fails on its own terms -- packages/rest/src/rest-server.ts, which owns the substitution pass this would extend, is held by open PR #16628.
What a fix would decide (not decided here)
Either the substitution pass is extended to the services block so both halves of the document follow the mounted paths, or services.*.route is declared as a conventional hint rather than an address and its consumers are checked against that reading. The first keeps one answer in the document; the second needs the consumers audited. Contract decision -- triage routes.
Generated by Claude Code
Found while implementing #14879 (the client SDK half of this same key). Not that PR's change and not addressed there; filed so it is not buried.
Measured, on a live server
Booting
createRestApiPluginwithcrud: { dataPrefix: '/objects' }and readingGET /api/v1/discovery, the one document reports the data routes in two places and they disagree:routes.datais/api/v1/objects-- correct, this is the mounted path.rest-server.tssubstitutes it in the discovery handler asdiscovery.routes.data = realBase + this.config.crud.dataPrefix.services.data.routeis/api/v1/data-- wrong, nothing is mounted there on this deployment. Measured across three clientconnect()calls in the [finding] The client SDK hard-codes/data/${object}…whilecrud.dataPrefixis live and discovery advertisesroutes.data = base + dataPrefix— a non-default prefix makes the SDK disagree with the mounts #14879 fixture: 3 occurrences of"route":"/api/v1/data", 0 of/api/v1/objects, two of the three against the custom-prefix server.Producer
packages/metadata-protocol/src/protocol.ts:5740writesroute: '/api/v1/data'as a literal into theservices.dataslot. The REST discovery handler's substitution pass only rewritesdiscovery.routes.*--routes.data,routes.metadata,routes.ui,routes.mcp-- so theservicesblock is never brought in line with the mounted paths and keeps the conventional literal whatever the deployment configured.(Locate by symbol, not by line --
rest-server.tsline numbers have been moving.)Why this is a defect and not an observation
The liveness ledger classifies
crud_endpoints'dataPrefixlive because it "has five independent consumers and moves the mounted paths and the advertised discovery document together", and #14691 retiredpatternsandobjectParamStyleon the reasoning that "the mounted CRUD paths are the contract the client SDK, the discovery document and /openapi.json all describe -- a per-operation pattern knob could only make them lie." Here the discovery document lies to itself: a reader that trustsservices.data.routeis pointed at an unmounted path by the very document whose job is to say where the routes are.Nearest neighbour is #4318 (discovery slots declaring routes that contradict their own
handlerReady), same block, different subject -- that one is about routes that do not exist at all, this one is about a route that exists somewhere else.Why it was not fixed in the #14879 PR
Scope: #14879's face is the client SDK, and this producer is in
packages/metadata-protocol. The in-place exemption also fails on its own terms --packages/rest/src/rest-server.ts, which owns the substitution pass this would extend, is held by open PR #16628.What a fix would decide (not decided here)
Either the substitution pass is extended to the
servicesblock so both halves of the document follow the mounted paths, orservices.*.routeis declared as a conventional hint rather than an address and its consumers are checked against that reading. The first keeps one answer in the document; the second needs the consumers audited. Contract decision -- triage routes.Generated by Claude Code