Uh oh!
There was an error while loading. Please reload this page.
fix(metadata-protocol): refuse field reference-target queries instead of clearing them for deletion - #9603
Conversation
…ad of clearing them
`field` metadata items are addressed by the composite key `<object>.<field>`,
while every reference site that names a field holds the bare field name. The two
vocabularies are disjoint, so `findReferencesToMeta` answered `{ references: [] }`
for every field, always — rendered by the admin "Used by" panel as "Nothing in
the metadata graph points at this item. Safe to delete."
Adds `unanswerableTargetTypes` as the TARGET-side sibling of #9190's
`unwalkableSourceTypes` (which records a source shape that could not be READ —
a different fact) and refuses at the protocol seam with the 501 NOT_IMPLEMENTED
the route already uses for its sibling refusal. No response field and no new
error code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qYPmkKEsfbWY1yVg83p8F📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 7 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 37c6b1e25daae4ff65a04485cfd7a0bc0d4ea7b2 && git checkout 37c6b1e25daae4ff65a04485cfd7a0bc0d4ea7b2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ca2e020e47bbe1121aa7e12aaeeae0f6d402ce5f 37a38302ab5abdf53722affde032a5d6ec2a6064 && git checkout -B drift-repro ca2e020e47bbe1121aa7e12aaeeae0f6d402ce5f && git merge --no-ff 37a38302ab5abdf53722affde032a5d6ec2a6064
node scripts/docs-audit/affected-docs.mjs --json ca2e020e47bbe1121aa7e12aaeeae0f6d402ce5f
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32140662775 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#9327
The defect
A
fieldmetadata item is addressed by the composite keyOBJECT.FIELD(e.g.account.owner), but every metadata property that names a field holds only the bare field name (owner). The two sides are drawn from disjoint vocabularies, so the reference scan answered{ references: [] }for every field, on every deployment, regardless of real usage.objectui's metadata-admin renders that empty answer verbatim as "Nothing in the metadata graph points at this item. Safe to delete." — an unanswerable question shown as a positive clearance, on the screen where someone decides to delete. Same harm family as #9537, one layer in.The defect predates PR #9324 and survives it; derivation made it visible, it did not cause it.
Counted, not sampled
The card says "~30 more" reference sites and names three. Measured against the real schemas on this ref:
(sourceType, property)pairs targetingfieldPer source type (non-recursive):
view=82,object=34,page=23,translation=5,dashboard=4,action=2,dataset=2,permission=1,skill=1.The unbounded third row is precisely why #9324 indexes properties rather than paths. Every one of the 12 sites documents its value as a bare field name — "Field name (snake_case)", "Field name to filter on",
object.fields{}being az.recordkeyed by the bare name. None carries anOBJECT.FIELDmetadata key, so no site can ever match the addressed key.The answer to the dispatch's hard-stop question
⭐ #9324's discriminator does NOT extend to TARGET types — it needed a sibling.
unwalkableSourceTypesis populated only when a source type's schema cannot be read.fieldreads perfectly, is walked as a source, and contributes reference sites of its own — it could never be a member. The fault here is on the target side and has the opposite cause: the shape was read fine and still cannot match.So this adds
unanswerableTargetTypesas a sibling onReferenceSiteIndex, not a widening of the existing set. A pin asserts the two sets stay disjoint, because a type in both would mean one of them had become a general "something is wrong here" bucket.Clause ② was not reached. Honesty here did not require the response-shape discriminator that #9190's dispatch fenced to the spec seat. Nothing is added to the 200 body and no new error code is introduced: the refusal rides the existing ADR-0112 nested envelope with the same
501 NOT_IMPLEMENTEDthat this exact route already returns when the protocol cannot compute the graph at all (#9326). One route, one dialect for "the question was never asked".unwalkableSourceTypesliterally — a build-time record with a pinning test and no response change — would not have repaired the harm. #9190 could move its discriminator off the response because the gap it records is bounded (some answers get shorter). This gap is total: every answer for the type is empty, and a constant in a build never reaches the operator standing in front of "Safe to delete." The sibling therefore had to be consumed as a refusal rather than merely recorded — wire-visible behaviour, but not a wire shape change.What changed
packages/metadata-protocol/src/reference-sites.ts—unanswerableTargetTypesonReferenceSiteIndex, derived from a one-member seed (field) that is guarded by the schemas:nameVocabularyRejectsKeydrops a member whose ownnamevocabulary has widened to admit its key, soemail_templateandcapability(both legitimately dotted, where the name is the key) can never land in the set. The seed is typed to the declared universe, so naming an undeclared type is a compile error.packages/metadata-protocol/src/protocol.ts—findReferencesToMetarefuses an unanswerable target with501 NOT_IMPLEMENTED, prescriptively per ADR-0110 D3: the message names the answerable question,GET /api/v1/meta/object/account/references, recovered from the key the caller already typed.Scope held to the ruled shape (3). Shape (1) (qualifying bare names against the owning object) is a capability upgrade needing object context the walker lacks — a separate card. Shape (2) (matching bare names against the composite key) stays rejected: it trades false negatives for false positives on delete confirmations, the worse direction on this screen.
Verification
All commands run on
37a38302a, the final commit; gate union run after it, on the pushed tree.Reverse verification — reverted only the protocol seam, left the derived set in place:
Exactly the five refusal-asserting cases went red, and for the right reason — the unfixed protocol resolved with
{ references: [] }rather than throwing something else. Restored byte-identically (git diff HEADempty), restoration leg 31/31 green. Both legs readsrc/through relative imports inside one package, so nodist/is involved and no rebuild applies.@objectstack/metadata-protocol@objectstack/objectql@objectstack/rest@objectstack/metadata-protocolcarries notypecheckscript, sopnpm --filter … typecheckwould be a zero-match exit-0 false green. Type safety was established two other ways instead: a directtsc --noEmitin the package (my files contribute 0 errors against a 63-error pre-existing DEBT baseline), andcheck:type-check-debt --re-measureon a built closure, which reports "surplus: none — every entry sits exactly at its measurement, so any new error is red".Gate union, each captured as
cmd > log 2>&1; EXIT=$?(never piped intotail— #9552), allEXIT=0:check:changeset-gate-self-tests,check:cross-package-test-inputs,check:durability-log-level,check:filter-alias-parity,check:objectui-changeset,check-adr-0087-registration,check-changeset-no-major,check-cross-package-test-inputs,check-empty-changeset,check-affected-docs,check:query-options-erasure,check:engine-double-contract,check:where-matcher,check:type-check-coverage,check:type-check-debt --re-measure,check:nul-bytes.Gate set re-derived with
node scripts/pm/dispatch-gates.mjs(no path arguments) against the real 5-path changeset.Generated by Claude Code