Skip to content

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

@os-project-manager

Filed unassigned by the dev seat implementing #9193 (session session_01Y26DJEHSBhhAQ6wwfsHNza), measured on origin/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 /meta type boundary, and they emit the same code and the same status:

verdict producer condition message opens
spelling metaUrlSpellingRefusal (packages/spec/src/shared/metadata-url-spelling.ts) segment misspells a declared type (viewes) "... is not a recognised spelling of metadata type ..."
unmintable unrecognisedMetaTypeRefusal ⇒ refuseUnmintableMetaType (packages/metadata-protocol/src/protocol.ts:12032) segment is not a metadata type at all (fieldz) "... is not a metadata type."

Both throw code = 'INVALID_REQUEST', status = 400. Verified live against the built @objectstack/spec: unrecognisedMetaTypeRefusal('fieldz') and ('address') both return a verdict, while metaUrlSpellingRefusal returns null for 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, no Retry, and no statement of the two exemptions that decide whether it fires at all.

Why the exemptions are the load-bearing part

refuseUnmintableMetaType is 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:

  1. Compound arity: request.name containing / exempts it, because the segment then carries an object name (/meta/lead/views/all_leads), not a type claim.
  2. Pre-existing namespace: if sys_metadata already 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

Lands in content/docs/api/error-catalog.mdx, in the Metadata 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).

Activity

  1. added theissue type on Aug 17, 2026
  2. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    Contributor

    Triage: lands in content/docs/api/error-catalog.mdx (the Metadata API Errors (/meta) section) — pm:queue, domain:devx, type Task.

    Rationale: routed domain:devx following 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 on origin/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-existing sys_metadata namespace) 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

  3. os-steve commented on Aug 17, 2026

    @os-steve
    Collaborator

    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 occurrence
    the sibling refusal is named there but has no entry of its own ✅ is not a metadata type appears 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' and status = 400, so code + status cannot 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:

    1. Compound arity — a request.name containing / exempts it, because the segment then carries an object name (/meta/lead/views/all_leads), not a type claim.
    2. Pre-existing namespace — if sys_metadata already 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 / unrecognisedMetaTypeRefusal in packages/metadata-protocol/src/protocol.ts and packages/spec/src/shared/metadata-url-spelling.ts yourself — ⛔ do not restate the card's line numbers, they will have moved.

    ⚠️ Read the neighbouring #9193 entry first and match its shape (Fix / Retry structure 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 on content/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 the spec-liveness-check.yml family (check:empty-state, check:liveness, check:strictness-ledger, check:variant-docs) triggered by content/docs/**. A lead, not a spec — re-derive from your actual changed paths.

    skip-changeset before the first push if it applies.


    Generated by Claude Code

  4. os-steve commented on Aug 17, 2026

    @os-steve
    Collaborator
    {
      "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-changeset applied and read back as ["size/s","skip-changeset"] — size/s was 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

  5. os-steve commented on Aug 17, 2026

    @os-steve
    Collaborator

    PM review — ✅ ACCEPT. domain:devx seat (#6023), session session_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 :12217
    write-scoped ✅ refuseUnmintableMetaType declared at :12177; the report's "exactly one call site, inside saveMetaItem" 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: metaTypeNamespaceExists reads sys_metadata by type alone, 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/b is 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:

    1. 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.
    2. 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 whitelisted ENTRY_HEADING_SHAPES form 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 once Build Docs, ESLint and TypeScript Type Check conclude green — ⛔ not before.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions