Uh oh!
There was an error while loading. Please reload this page.
fix(rest): refuse when findReferencesToMeta is absent instead of answering "nothing depends on this item" - #9425
Merged
Conversation
…ering "nothing depends on this item" (#9326) GET /api/v1/meta/:type/:name/references feature-detects findReferencesToMeta on the resolved protocol. When the method was absent the route answered 200 { references: [] }, so a capability gap reached the wire as the statement "nothing depends on this item" — ADR-0110 D3 collapsed, with the admin "Used by" panel rendering that empty case as "Nothing in the metadata graph points at this item. Safe to delete." for an operator about to delete something. The branch now answers 501 with the ADR-0112 nested envelope the sibling /meta 501 refusals converged on (#7035), so body.error.code is readable by the same consumer code that already reads the others. A protocol that has the method is untouched: empty and non-empty results both still pass through as 200. Refusing at the route rather than asserting at assembly is deliberate. findReferencesToMeta is not a member of RestProtocol and is not declared in @objectstack/spec at all — it is an ADR-0076 D9 server-only extension reached through a runtime cast — so a boot-time assertion would promote an undeclared optional extension into a required one, which is a spec contract decision. Co-Authored-By: Claude <noreply@anthropic.com>
Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
This was referenced Aug 18, 2026
os-zhuang
marked this pull request as ready for review
August 18, 2026 00:57
os-zhuang
enabled auto-merge
August 18, 2026 00:57
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#9326.
The measurement came first, and it chose the fix
The card's own honesty note said live-reachability was never measured, and the
dispatch fence made producing that measurement the first deliverable. Here it is.
Who can supply the protocol this route reads?
resolveProtocol()returnseither the
protocolservice of the request's per-environment kernel(
kernelManager.getOrCreate(envId)) or the one injected intoRestServerbyRestApiPlugin— whose service name is host-configurable (protocolServiceName).Is there an in-repo assembly that produces one lacking
findReferencesToMeta?No.
ObjectStackProtocolImplementation(
packages/metadata-protocol/src/protocol.ts) is the only implementationregistered under the
protocolservice in production code, it reaches the kernelthrough the one seam both mounts share (
assembleMetadataProtocol()), and itdefines the method unconditionally as a class member. The sibling
cloudreporegisters no
protocolservice of its own and never mentions the method(control-queried: 24
registerService(sites there, zero for'protocol').Is the branch therefore dead? No — and this is the part that decides the fix.
RestProtocolisDataProtocol & MetadataProtocol, andfindReferencesToMetais a member of neither. It is not declared anywhere in
packages/specat all(control query:
getMetaItem(matches in that very interface; the threebeyond-spec read verbs match zero times). It is an ADR-0076 D9 server-only
extension, which is exactly why the handler reaches it through a runtime cast.
So a host that implements the declared contract exactly and correctly is a
conforming deployment that lands on this branch, and
tsccannot say a word.In-process partial protocol doubles already do precisely this today (five in
packages/services/service-automation). The gap is not "latent because nobodycould reach it"; it is "unreachable from today's in-repo assemblies, and reachable
from any conforming embedder, because the contract does not require the method."
Why option 1 (refuse at the route), and why option 3 is not a complement
The fence put options 1 and 3 in scope and asked the measurement to choose. It
chose 1, and it rules 3 out rather than pairing with it:
extension into a required one. That is a contract widening in everything but
the file it is written in, and the contract lives in
packages/spec— theterritory this card explicitly does not carry authority over.
runtime-detected rather than widened into
RestProtocol. A boot assertioncontradicts it.
could not cover the embedder path anyway, since
RestServer's constructoraccepts anything typed
RestProtocol.And the house already answered this exact question, in this exact file:
GET /meta/:type/:name/layers— registered about fifty lines below/references,in the same block, for the same class of missing extension — refuses with
501NOT_IMPLEMENTED, and its comment reasons verbatim as this card does. Three moredo the same (
getMetaDiagnostics,listDrafts,migrateStoredMetadata)./referenceswas one of only two outliers.The change
The ADR-0112 nested envelope that #7035 converged the sibling
/meta501refusals onto — never the bare-string or sibling-
codedialects, which makebody.error.codereadundefined. Deliberately not the shape/layersuses:that one is the sibling-
codedialect, counted by both shrink-only dialectratchets
check:route-envelopeholds on this file (stringError: 44,siblingCode: 69), so copying it would have pushed a ratchet up. The nested shapeis counted by neither, and the ratchets are unmoved — verified, not assumed.
Message wording follows the three converted siblings
(
protocol.NAME() is not available in this kernel).Does any caller's observed response change?
Yes, on one deployment shape, and only there. A protocol that has the method is
untouched: empty and non-empty results both still pass through verbatim as
200.What changes is the answer when the protocol has no such method — previously
200with an empty list, now
501. No in-repo assembly produces that today (above).The pin
packages/rest/src/meta-references-capability-gap.test.tsasserts the thing thedefect actually was: an absent capability does not produce the same body as a
genuine zero-references answer. Both used to be
200 { references: [] }.Assertions are the ADR-0112 pair (
statusandbody.error.code) plus themessage at the declared position — never a bare
toThrow, which could notseparate "answered with the wrong body" from "did not refuse at all", and the
wrong body is the defect. A companion case keeps the healthy side honest, so the
inequality cannot be satisfied by breaking the success path instead of fixing the
broken one.
Reverse verification — direction predicted in writing before running
Predicted: restoring the empty-body branch turns exactly three of the four new
cases red — the 501 assertion, the equivalence pin, and the machine-readable case
— while "a protocol that CAN answer is untouched" stays green (the ablation
does not touch the success path), and nothing else in the package moves. Observed,
exactly that:
expected 200 not to be 200is the defect stating itself. The fix was committedbefore the ablation and restored with
git checkout, then proved byte-identicalto the committed state (
git diff --exit-codeempty).Zone 2 — the PM's assumptions, re-tested
2a — PR #9324 did not close this. Confirmed, not inherited. It is merged
(2026-08-17T14:38:10Z) and touched eight files, every one under
packages/metadata-protocol,packages/objectqlor.changeset— zero files inpackages/rest. Its own tier-fence note records "no new refusal, noresponse-shape change". It replaced a curated registry with a derived one inside
the protocol implementation, which cannot help a route that never calls it.
2b — falsified. The references route is NOT the only limb with this shape.
GET /meta/:type/:name/auditanswers a missingauditMetaItemwithres.json({ events: [] })— the same capability gap rendered as a well-formedempty answer, on a compliance surface, where it reads as "this item has no audit
trail". Filed separately, deliberately not a rider here. The sweep discriminates
rather than merely finding nothing: the same scan located four sibling limbs that
do refuse, so a zero was a possible outcome.
2c — options 1 and 3 are not complements; see above. The measurement chose,
and the reasoning is recorded here as the fence asked.
One more finding, filed separately: the fix is necessary but not sufficient
Refusing at the route makes the wire honest. It does not by itself remove the
user-visible harm, because the consumer swallows it:
objectuiResourceEditPage.tsxcatches every failure fromclient.references()into
setRefs([]), which renders the same "Nothing in the metadata graph points atthis item. Safe to delete." empty state, with only a
console.error. So a truthful501is currently collapsed back into the lie at the consumer. That is a seconddefect, in a second repo, and it is filed rather than ridden on this PR — but the
producer is still the right place to fix the producer's half, and this half is
what makes the consumer's half fixable at all.
Verification — all at
a412daaf8, the final commitGates re-derived from the actual changed paths with
node scripts/pm/dispatch-gates.mjs, which added several beyond the dispatchedlist (the changeset family, and the convention-triggered family a new test file
pulls in). All pass:
Ratchet family at the final head, after the full closure build
(
turbo run build --filter=./packages/* --filter=./packages/*/*, 70/70 successful):Tier fence
Not tripped. No
packages/specedit, no*.zod.ts, no error-code ledger row —NOT_IMPLEMENTEDis already a registered code with canonical status 501(
error-code-ledger.zod.ts), so nothing is minted. Option 2 (a wire-levelcoveragediscriminator) was ruled out of scope by the fence and the measurementdid not point there: the honest distinction this route needed is a refusal, not a
new response key.
Declared file surface honoured:
packages/rest/src/rest-server.ts(the referencesroute only), its test, and
.changeset/**. Nothing else touched.Generated by Claude Code