Skip to content

docs(api): catalog the unmintable /meta type refusal - #9287

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-9243-meta-unmintable-catalog-entry
Aug 17, 2026
Merged

docs(api): catalog the unmintable /meta type refusal#9287
os-steve merged 1 commit into
mainfrom
claude/issue-9243-meta-unmintable-catalog-entry

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes#9243

The /meta type boundary raises two refusals that share one envelope — INVALID_REQUEST / 400 — so code and status cannot tell them apart; only the message can. PR #9245 (for #9193) documented the spelling refusal and named this one in a single disambiguation line. This adds the sibling entry it deliberately did not absorb.

Docs-only. No behaviour change#8421 scoped this refusal deliberately and it is correct; only its documentation was absent.

The load-bearing part: the two exemptions

A bare envelope row would have been worse than no entry, because it would assert a determinism the code does not have. refuseUnmintableMetaType is not a pure function of the type segment — two shapes reach the door and are served:

  1. Compound arity — a name containing / exempts the request, because at that arity the type segment carries an object name rather than a type claim (PUT /meta/lead/views/all_leads is type='lead', name='views/all_leads'). Residue stated rather than hidden: PUT /meta/fieldz/a/b is therefore accepted. The exemption is the arity, not the name — PUT /meta/lead/all_leads is still refused.
  2. Pre-existing namespace — if sys_metadata already holds rows under that type key, the write proceeds. The store decides this, never the caller.

The same segment can be refused on one deployment and served on another, depending on stored state. The entry says this plainly, since it is the one thing a caller could otherwise only learn by collision.

Also documented: the refusal is write-scoped (one call site, in saveMetaItem; reads still answer and residue rows stay deletable), and the namespace probe raises 503 when the store is unreachable — while an unprovisioned store is the one read failure that does not escalate, so the 400 stands.

Verified against source, not restated from the card

All three properties were re-derived in the worktree (the card's line numbers had moved and are not repeated):

  • envelope code = 'INVALID_REQUEST', status = 400packages/metadata-protocol/src/protocol.ts
  • exemption 1 is the literal if (request.name.includes('/')) return;
  • exemption 2 is if (await this.metaTypeNamespaceExists(unrecognised.type)) return;, which reads sys_metadata by type alone
  • write-scoping: exactly one call site of refuseUnmintableMetaType, inside saveMetaItem
  • the static half is unrecognisedMetaTypeRefusal in packages/spec/src/shared/metadata-url-spelling.ts

Each is independently pinned by existing tests in packages/metadata-protocol/src/protocol.unrecognised-meta-type.test.ts (including the anti-vacuity control for arity and the positive control for the namespace exemption) and packages/runtime/src/meta-compound-arity-mint-door.test.ts. The quoted JSON message is verbatim from the producer.

Changes to the neighbouring entry, per the pair requirement

Two edits beyond the new entry, both because a second entry made the existing text inaccurate rather than merely redundant:

  • Section intro said the section "documents the type-spelling refusal" (singular). It now names both refusals and states up front that they share a code and status.
  • The spelling entry's disambiguation line half-restated this refusal's envelope inline. It now points at the entry below instead of duplicating it, and its read/write split is made explicit (on a read the request continues down the plugin path; on a write it meets this refusal). Keeping both the line and a full entry with duplicated envelope text would have been a drift pair.

Scope

Per the card: one envelope, this one. ⛔ Not a sweep — the wider class of wire-visible refusals with no catalog entry stays unmeasured, as #9193 already recorded. #8421 is not addressed here beyond documenting its behaviour.

Gates

Derived from the actual changed path with node scripts/pm/dispatch-gates.mjs content/docs/api/error-catalog.mdx (8 families, matching the dispatch list exactly), and re-run as a union after the final commit, at ae6c0624b:

gateresult
check:error-status-conformance✅ 54 codes reconciled; INVALID_REQUEST read as a ledger code the page publishes a status for
check:docs-audit-scope✅ 178 hand-written docs in sync
check:docs-redirects✅ 92 entries resolved
check:role-word✅ no new occurrences
check:empty-state✅ all classified
check:liveness
check:strictness-ledger
check:variant-docs

The new heading uses the whitelisted code + descriptive suffix shape from the conformance gate's ENTRY_HEADING_SHAPES, so it is read rather than landing in the unreadable-heading census. It claims 400, the same status the sibling entry already claims for this code, so the reconciled set is unchanged.

Docs-only, publishes nothing ⇒ skip-changeset.

Generated by Claude Code


Generated by Claude Code

The `/meta` type boundary raises two refusals that share one envelope —
`INVALID_REQUEST` / 400 — so code and status cannot tell them apart. Only
the spelling refusal had a catalog entry; its sibling was named in one
disambiguation line and otherwise undocumented.
Adds the entry for `refuseUnmintableMetaType`, matching the sibling's
Fix/Retry shape. The load-bearing part is the two exemptions, which a
caller can otherwise only discover by hitting them:
- compound arity — a `name` containing `/` puts an OBJECT name in the
`:type` segment, so the verdict does not apply (and `/meta/fieldz/a/b`
is therefore accepted); the exemption is the arity, not the name
- pre-existing namespace — the write proceeds if `sys_metadata` already
holds rows under that type key, decided by the store, never the caller
Together these mean the same segment can be refused on one deployment and
served on another. The entry says so plainly, records that the verdict is
write-scoped (reads answer, residue rows stay deletable), and notes the
503 the namespace probe raises when the store is unreachable.
Also updates the section intro, which described a single refusal, and
rewrites the spelling entry's disambiguation line to point at the new
entry instead of half-restating its envelope.
No behaviour change: #8421 scoped this refusal deliberately and it is
correct — only its documentation was absent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
@github-actionsgithub-actionsBot added size/s documentation Improvements or additions to documentation labels Aug 17, 2026
@os-steveos-steve added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed documentation Improvements or additions to documentation labels Aug 17, 2026 — with Claude
@os-steve
os-steve marked this pull request as ready for review August 17, 2026 11:45
@os-steve
os-steve added this pull request to the merge queueAug 17, 2026
Merged via the queue into main with commit 137c806Aug 17, 2026
28 checks passed
@os-steve
os-steve deleted the claude/issue-9243-meta-unmintable-catalog-entry branch August 17, 2026 13:15
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-steve@claude