Skip to content

Commit 7d711c9

Browse files
claude[bot]claude
andauthored
fix(metadata-protocol): the unanswerable-target refusal opens with prose, not a reader-less machine tag (#16244)
* fix(metadata-protocol): the unanswerable-target refusal opens with prose, not a reader-less machine tag (#16145) `findReferencesToMeta` refused an unanswerable TARGET type with a message opening `[unanswerable_target]`. Since #15685 the `/references` door relays that prose verbatim, so the tag became the first thing an operator reads on the screen where they decide whether to delete something. Measured across both producer files: of 31 bracketed-tag throw sites that declare a `code`, 30 spell the tag as the lowercase restatement of their OWN declared code (`[item_locked]`/`ITEM_LOCKED`, `[no_draft]`/`NO_DRAFT`, …). This one is the sole exception -- its code is `NOT_IMPLEMENTED`, so the tag restated nothing the envelope carries, and it has zero readers repo-wide. Per the 2026-08-29 ruling behind #12975, `error` is HUMAN LANGUAGE and `code` is the MACHINE TOKEN. No status, code or envelope position moves. The absence is now pinned, since nothing pinned the tag and therefore nothing pinned its removal either. Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ Co-authored-by: Claude <noreply@anthropic.com> * changeset: metadata-protocol patch for the unanswerable-target refusal prose (#16145) Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7778115 commit 7d711c9

3 files changed

Lines changed: 58 additions & 1 deletion

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@objectstack/metadata-protocol": patch
3+
---
4+
5+
`findReferencesToMeta`'s unanswerable-target refusal now opens with prose instead of a machine-shaped `[unanswerable_target]` tag that nothing read.
6+
7+
```
8+
before 501 {"error":{"code":"NOT_IMPLEMENTED","message":"[unanswerable_target] References to a 'field' item cannot be computed. … Ask the owning object instead: GET /api/v1/meta/object/account/references."}}
9+
after 501 {"error":{"code":"NOT_IMPLEMENTED","message":"References to a 'field' item cannot be computed. … Ask the owning object instead: GET /api/v1/meta/object/account/references."}}
10+
```
11+
12+
Nothing else moves: same `501`, same `NOT_IMPLEMENTED`, same envelope position, and the prescriptive sentence ADR-0110 D3 requires is untouched. Callers branch on `code`, which is unchanged; only the human-facing sentence is shorter.
13+
14+
Why the tag was wrong here specifically. This producer writes a bracketed tag on many refusals, and every other one is the lowercase restatement of that throw's own declared `code``[item_locked]` with `ITEM_LOCKED`, `[no_draft]` with `NO_DRAFT`, `[invalid_request]` with `INVALID_REQUEST`. Measured across the two producer files, 30 of the 31 tagged throw sites that declare a code restate it that way. This refusal declares `NOT_IMPLEMENTED`, so its tag was the sole exception: it named a token the envelope carries on no axis, and a repo-wide search finds no parser, no switch, no assertion and no doc that reads it. Per the ruling behind the `/data` door's `FORBIDDEN:` prefix removal, `error` is human language and `code` is the machine token.
15+
16+
It became worth fixing when the `/meta/:type/:name/references` door started relaying the producer's prose verbatim: before that the whole sentence was replaced by `Internal server error` and the tag reached nobody, and after it the tag was the first thing an operator read on the screen where they decide whether to delete something. The `@objectstack/rest` entry in this release quotes the pre-removal sentence in its example; this entry is the later word on that wire text.
17+
18+
The absence is now pinned in `protocol.reference-target-unanswerable.test.ts` — nothing pinned the tag, so without a pin nothing would have pinned its removal either.

packages/metadata-protocol/src/protocol.reference-target-unanswerable.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,34 @@ describe('[#9327] a `field` TARGET is refused, not cleared', () => {
101101
expect(err.message).toContain('account.owner');
102102
});
103103

104+
it('THE PIN: the message opens with PROSE — no machine-shaped bracketed tag', async () => {
105+
// [#16145] Since #15685 this sentence reaches the operator VERBATIM, so
106+
// its first characters are the first thing they read. Every OTHER bracketed
107+
// tag this producer writes (`[item_locked]`, `[no_draft]`, `[invalid_request]`,
108+
// …) is a lowercase restatement of that throw's OWN declared `code`, so the
109+
// token also rides the `code` axis. This refusal's code is `NOT_IMPLEMENTED`,
110+
// so `[unanswerable_target]` restated nothing the envelope carried and nothing
111+
// read it — while #12975 (2026-08-29) rules that `error` is HUMAN LANGUAGE and
112+
// `code` is the MACHINE TOKEN.
113+
//
114+
// Pinned as an ABSENCE because nothing else is: with the tag deleted, no other
115+
// assertion in this repo would notice it coming back.
116+
const protocol = protocolWith({});
117+
118+
const err = await expectUnanswerableRefusal(
119+
() => protocol.findReferencesToMeta({ type: 'field', name: 'account.owner' }),
120+
);
121+
122+
expect(err.message).not.toContain('[unanswerable_target]');
123+
expect(
124+
err.message.startsWith('['),
125+
`the message opens with a bracketed tag: ${err.message.slice(0, 48)}`,
126+
).toBe(false);
127+
// …and the prose it opens with INSTEAD is asserted here too, so this pin
128+
// cannot go green by the message becoming empty or generic.
129+
expect(err.message).toMatch(/^References to a 'field' item cannot be computed\./);
130+
});
131+
104132
it('the refusal is PRESCRIPTIVE — it names the answerable question (ADR-0110 D3)', async () => {
105133
// A refusal that only says "no" moves the operator from a false
106134
// clearance to a dead end. A field's dependents ARE reachable, through

packages/metadata-protocol/src/protocol.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21605,10 +21605,21 @@ export class ObjectStackProtocolImplementation implements
2160521605
// question, because a field's dependents ARE reachable — through the
2160621606
// object that owns it, which is where a field is authored and where the
2160721607
// reference graph has real edges.
21608+
//
21609+
// ⛔ And it opens with NO bracketed tag. The `[item_locked]`-style tags
21610+
// this file writes elsewhere are lowercase restatements of the throw's OWN
21611+
// declared `code`, so the wire carries the same token on the `code` axis;
21612+
// this refusal's code is `NOT_IMPLEMENTED`, so an `[unanswerable_target]`
21613+
// opener restated nothing the envelope carries and nothing ever read it.
21614+
// #12975 (2026-08-29) rules that `error` is HUMAN LANGUAGE while `code` is
21615+
// the MACHINE TOKEN, and since #15685 this prose reaches the operator
21616+
// VERBATIM — so the tag was the first thing they read. What separates this
21617+
// refusal from the route's other 501 is the sentence itself, not a tag.
21618+
// Its absence is pinned by `protocol.reference-target-unanswerable.test.ts`.
2160821619
if (REFERENCE_SITES.unanswerableTargetTypes.includes(singularTarget)) {
2160921620
const owner = targetName.includes('.') ? targetName.slice(0, targetName.indexOf('.')) : '<object>';
2161021621
const err = new Error(
21611-
`[unanswerable_target] References to a '${singularTarget}' item cannot be computed. `
21622+
`References to a '${singularTarget}' item cannot be computed. `
2161221623
+ `A '${singularTarget}' is addressed by the composite key '<object>.<field>' `
2161321624
+ `(here '${targetName}'), while every metadata property that names a field holds the `
2161421625
+ `BARE field name — so no reference site can ever match this key and an empty answer `

0 commit comments

Comments
 (0)