Repository navigation
The /meta "is not a metadata type" refusal (#8421) has no catalog entry either — it shares INVALID_REQUEST / 400 with the spelling refusal and is told apart only by message #9243
Description
Activity
Triage: lands in
content/docs/api/error-catalog.mdx(theMetadata API Errors (/meta)section) —pm:queue,domain:devx, type Task.Rationale: routed
domain:devxfollowing the sibling #9193's routing (docs catalog entry; acceptance behavior unchanged — the card itself claims no defect in the refusal). #9193 closed 06:35Z today via merged PR #9245, so the section this card writes into now exists onorigin/main— the card is dispatchable, not blocked. Same-day churn: the dev must sync current main first (PR #9245 touched the same section today) and use the sibling spelling-refusal entry as the shape precedent. Scope per the card: one entry for the unmintable-type refusal, including the two serve-not-refuse exemptions (compound arity; pre-existingsys_metadatanamespace) and its write-scoped nature.Size/model suggestion: S, sonnet (docs entry with a fully-specified shape precedent; gated by docs checks).
Generated by Claude Code
Claim: PM loop round 3
Session:session_01XqDQYVU5smx29ts9pAErja· Branch:claude/issue-9243-meta-unmintable-catalog-entry· Worktree:objectstack-issue-9243· Domain:domain:devx
File surface:content/docs/api/error-catalog.mdx— stop on breach; explain in the report
Container & model:M,mode:subagent,model: opus— no path-derived mandate. Documentation only, but the deliverable is a behavioural description of two exemptions a caller cannot infer, so it is judgement, not transcription.
Serial constraints cleared: not on any hot-file row of #6023; no in-flight claim names it. Batch siblings #9257 (packages/lint/src/), #9265 (content/docs/getting-started/), #9230 (scripts/docs-audit/) — disjoint.Premise re-check, from a fresh worktree asserted equal to
origin/main(fab693bee)premise measured #9193's PR landed and created the landing section ✅ ## Metadata API Errors (\/meta`)` present, 1 occurrencethe sibling refusal is named there but has no entry of its own ✅ is not a metadata typeappears once — the disambiguation line, not an entry⇒ The card's framing holds exactly: one envelope documented, its sibling named-but-undocumented.
The load-bearing part — ⛔ do not ship a bare envelope row
Both refusals emit
code = 'INVALID_REQUEST'andstatus = 400, socode+statuscannot tell them apart. What makes this entry worth writing is the two exemptions, which are the part a caller can otherwise only discover by hitting them:- Compound arity — a
request.namecontaining/exempts it, because the segment then carries an object name (/meta/lead/views/all_leads), not a type claim. - Pre-existing namespace — if
sys_metadataalready holds rows under that type key, the write proceeds.
⇒ The same segment can be refused on one deployment and served on another, depending on stored state. Document that plainly. It is also write-scoped — it runs on the mint door, not on reads.
⛔ An entry that lists the envelope and omits the exemptions would be worse than no entry: it would assert determinism the code does not have. Verify all three properties (both exemptions + write-scoping) against
refuseUnmintableMetaType/unrecognisedMetaTypeRefusalinpackages/metadata-protocol/src/protocol.tsandpackages/spec/src/shared/metadata-url-spelling.tsyourself — ⛔ do not restate the card's line numbers, they will have moved.⚠️ Read the neighbouring #9193 entry first and match its shape (Fix/Retrystructure and its disambiguation line). Two entries for two refusals sharing one envelope must read as a pair; if your entry makes #9193's disambiguation line redundant or wrong, say so rather than leaving both.⛔ Not in scope: any change to the refusal itself (#8421 argued its scoping carefully and it is correct behaviour — only its documentation is absent), and ⛔ not a sweep of the wider wire-visible refusals with no catalog entry class, which #9193 already recorded as unmeasured.
Gates (from the fresh tree, this moment) — note this page has its OWN gate
pnpm check:error-status-conformance⚠️ (keyed directly oncontent/docs/api/error-catalog.mdx— it will have opinions about the status/code you write) ·pnpm check:docs-audit-scope·pnpm check:docs-redirects·pnpm check:role-word· plus thespec-liveness-check.ymlfamily (check:empty-state,check:liveness,check:strictness-ledger,check:variant-docs) triggered bycontent/docs/**. A lead, not a spec — re-derive from your actual changed paths.skip-changesetbefore the first push if it applies.
Generated by Claude Code
- Compound arity — a
- added a commit that references this issue
on Aug 17, 2026 { "issue": 9243, "status": "done", "branch": "claude/issue-9243-meta-unmintable-catalog-entry", "pr": "https://github.com/objectstack-ai/objectstack/pull/9287", "premise_still_valid": true, "summary": "Premise re-derived from a worktree cut at origin/main (fab693bee, asserted equal): the `## Metadata API Errors (/meta)` section exists with 1 occurrence, and `is not a metadata type` appeared exactly once — the sibling disambiguation line, not an entry. Added the catalog entry for the unmintable-type refusal in content/docs/api/error-catalog.mdx, matching #9193's Fix/Retry shape. The load-bearing content is the two exemptions, all three properties re-verified against source rather than restated from the card: 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 so the store decides, never the caller; write-scoping is exact — `refuseUnmintableMetaType` has precisely ONE call site, inside `saveMetaItem`. The entry states plainly that the same segment can be refused on one deployment and served on another depending on stored state, records the residue that `PUT /meta/fieldz/a/b` is therefore accepted (the exemption is the arity, not the name), and adds the 503-vs-400 split on the namespace probe (an unprovisioned store is the one read failure that does not escalate). Per the Zone 3 pair requirement I also changed two things in the neighbouring text, because a second entry made them inaccurate rather than merely redundant: the section intro said the section documents 'the type-spelling refusal' (singular) and now names both and states up front that code+status cannot separate them; and the spelling entry's disambiguation line half-restated this refusal's envelope inline, which would have been a drift pair against a full entry — it now points at the entry below and makes its read/write split explicit. No change to the refusal itself; not a sweep.", "tests": "Gates derived from the ACTUAL changed path (`node scripts/pm/dispatch-gates.mjs content/docs/api/error-catalog.mdx`) returned exactly the 8 families the dispatch named — no additions, so nothing extra was owed. Union re-run AFTER the final commit, at `git rev-parse --short HEAD` = ae6c0624b, all 8 exit 0: check:error-status-conformance ('✓ every derivable runtime status is documented, and every documented status is reachable.' — scope line now reads '54 code(s) reconciled = 53 StandardErrorCode member(s) + 1 ledger code(s) a doc page publishes a status for (INVALID_REQUEST)', and my heading is READ rather than landing in the unreadable-heading census because it uses the whitelisted 'code + descriptive suffix' shape from ENTRY_HEADING_SHAPES); check:docs-audit-scope ('✓ docs-accuracy-audit scope is in sync with content/docs/: 178 hand-written doc(s).'); check:docs-redirects ('OK ... 92 entries'); check:role-word ('OK (43 baselined file(s), no new occurrences)' — this page is under content/docs so the ADR-0090 ratchet applies); and the spec-liveness family check:empty-state / check:liveness / check:strictness-ledger / check:variant-docs, all ✓. Self-scanned the file for raw control bytes with `grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'` — no match. No ablation was applicable: this is a docs-only diff with no built artifact to mutate, so the accuracy evidence is the source re-derivation above plus the existing pins that already cover each claim — protocol.unrecognised-meta-type.test.ts (which carries both the anti-vacuity control 'the exemption is the ARITY, not the name' and the positive control that a FRESH unrecognised type is still refused in a store that already holds residue rows) and runtime/meta-compound-arity-mint-door.test.ts. I did not run those suites: they are unchanged by this diff and gate the packages, not this page.", "open_questions": [], "out_of_scope_findings": [] }Notes outside the JSON, for PM triage:
skip-changesetapplied and read back as["size/s","skip-changeset"]—size/swas bot-applied first and was preserved by writing the union rather than a bare set.- The PR body carries a duplicated signature footer: mine in session-URL form plus one the platform appended at creation. Cosmetic, left alone rather than edited — reported per basic rule 2 rather than "corrected", since the second write is not mine.
- Report delivered at draft-PR time per the 2026-08-10 ruling; CI convergence not awaited.
Generated by Claude Code
Generated by Claude Code
PM review — ✅ ACCEPT.
domain:devxseat (#6023), sessionsession_01XqDQYVU5smx29ts9pAErja, 2026-08-17T11:2xZ (date -u). PR #9287.Verified against source myself, ⛔ not restated from the report
From a fresh worktree asserted equal to
origin/main:claim measured exemption 1 — compound arity ✅ protocol.ts:12182—if (request.name.includes('/')) return;exemption 2 — pre-existing namespace ✅ :12184—if (await this.metaTypeNamespaceExists(unrecognised.type)) return;, with the helper at:12217write-scoped ✅ refuseUnmintableMetaTypedeclared at:12177; the report's "exactly one call site, insidesaveMetaItem" is consistent with the reachability comment at:12200✅ PR form: draft · base
main·Fixes #9243· 1 file, +84 / −6 ·size/s/skip-changeset. Gates 27 total, none non-green (3 still converging).⭐ The entry does the thing the dispatch said it must, and then some
⛔ It is not a bare envelope row. It states plainly that the same segment can be refused on one deployment and served on another depending on stored state — and the dev sharpened why:
metaTypeNamespaceExistsreadssys_metadatabytypealone, so the store decides, never the caller. That is a better statement of the hazard than the card's.Two additions beyond what I asked for, both earned:
- The residue:
PUT /meta/fieldz/a/bis therefore accepted — because the exemption is the arity, not the name. That is the consequence a reader would otherwise hit in production, and it follows directly from exemption 1. - The 503-vs-400 split on the namespace probe: an unprovisioned store is the one read failure that does not escalate.
⭐ Zone 3 handled exactly right — and it found "inaccurate", not merely "redundant"
I said: if your entry makes #9193's disambiguation line redundant or wrong, say so rather than leaving both. It found two neighbouring statements that a second entry rendered inaccurate:
- the section intro described the section as documenting "the type-spelling refusal" — singular — which stops being true the moment a sibling entry exists. It now names both and states up front that code + status cannot separate them.
- the spelling entry's disambiguation line half-restated this refusal's envelope inline — which against a full entry becomes a drift pair: two places stating one envelope, free to diverge. It now points at the entry below and makes the read/write split explicit.
⇒ That is the difference between adding a row and keeping a page coherent. Fixing a drift pair at the moment you create it is much cheaper than the card that finds it six weeks later — and this lane has been filing exactly those cards all shift.
Verification discipline
✅ Gates derived from the actual changed path returned exactly the 8 families the dispatch named — no additions owed, and the dev said so rather than staying silent.
✅check:error-status-conformance— the gate keyed on this very page — passes, and the dev checked its scope line moved correctly (54 code(s) reconciled = 53 members + 1 ledger code) rather than just reading the exit code. It also confirmed the new heading uses a whitelistedENTRY_HEADING_SHAPESform instead of silently landing in the unreadable-heading census.
✅ Honest about what it did not do: no ablation, because a docs-only diff has no built artifact to mutate — and it named the existing pins that already cover each claim (protocol.unrecognised-meta-type.test.ts, carrying both the anti-vacuity control "the exemption is the ARITY, not the name" and a positive control that a fresh unrecognised type is still refused in a store holding residue rows) while stating it did not run them and why. ⛔ That is the correct call, and correctly disclosed.Landing
content/docs/**only ⇒ no maintainer-merge fork. Arming onceBuild Docs,ESLintandTypeScript Type Checkconclude green — ⛔ not before.
Generated by Claude Code
- The residue:
Filed unassigned by the dev seat implementing #9193 (session
session_01Y26DJEHSBhhAQ6wwfsHNza), measured onorigin/main@7ff5aa294. Duplicate-searched by envelope text, by symbol name and by file; nearest neighbours are #9193 (the sibling refusal, being fixed) and #8421 (where this refusal was introduced) — neither covers this.What was measured
There are two distinct refusals at the
/metatype boundary, and they emit the samecodeand the samestatus:metaUrlSpellingRefusal(packages/spec/src/shared/metadata-url-spelling.ts)viewes)unrecognisedMetaTypeRefusal⇒refuseUnmintableMetaType(packages/metadata-protocol/src/protocol.ts:12032)fieldz)Both throw
code = 'INVALID_REQUEST',status = 400. Verified live against the built@objectstack/spec:unrecognisedMetaTypeRefusal('fieldz')and('address')both return a verdict, whilemetaUrlSpellingRefusalreturnsnullfor both.#9193's PR documents the spelling refusal only. It names this one in a single disambiguation line — enough that the new entry is not false — but this refusal has no entry of its own: no
Fix, noRetry, and no statement of the two exemptions that decide whether it fires at all.Why the exemptions are the load-bearing part
refuseUnmintableMetaTypeis not a pure function of the type segment. It returns early — i.e. the request is served — in two cases a caller cannot infer from the message:request.namecontaining/exempts it, because the segment then carries an object name (/meta/lead/views/all_leads), not a type claim.sys_metadataalready carries rows under that type key, the write proceeds.So the same segment can be refused on one deployment and served on another, depending on stored state. That is exactly the kind of thing a caller can only discover by hitting it — the discovery mechanism #9193 exists to remove.
It is also write-scoped: it runs on the mint door, not on reads.
Not claimed
/metatype name that is not a plural of anything still mints a namespace —PUT /meta/fieldz/xanswers 200 #8421 argued its scoping carefully. Only its documentation is absent./metaspelling refusal is documented NOWHERE — nine verbs have thrown it since #7894 anderror-catalog.mdxhas no entry #9193 because it shares that envelope's code. The class — wire-visible refusals with no catalog entry — remains unmeasured, as The/metaspelling refusal is documented NOWHERE — nine verbs have thrown it since #7894 anderror-catalog.mdxhas no entry #9193 already noted./metaspelling refusal is documented NOWHERE — nine verbs have thrown it since #7894 anderror-catalog.mdxhas no entry #9193. That PR stands on its own; this is the sibling entry it deliberately did not absorb.Lands in
content/docs/api/error-catalog.mdx, in theMetadata API Errors (/meta)section #9193's PR adds.Backlinks: #9193 (sibling refusal, PR in flight) · #8421 (where this refusal was introduced) · #8586 (what made it statically answerable).