Skip to content

metadata-api.mdx documents PUT /meta/:type/:name as "create or update" without saying some types refuse creates — now two of them #8498

Closed

Description

@os-zhuang

Surfaced by the #7893 dev while sweeping docs for that retirement. Deliberately not folded into PR #8496 — it is pre-existing, generic, and as much about job as about field, so widening the retirement PR to cover it would have been scope creep. Filing instead.

The gap

content/docs/api/metadata-api.mdx:49 documents:

PUT /meta/:type/:name — "Create or update a metadata item"

with no mention that some types refuse the create half. The page names no type, so it is not wrong about any particular one — it is incomplete about the verb.

Why it matters more now than it did

This was already incomplete: #4509 retired job's runtime create the same way. #7893 makes it two types (field joins job), and the mechanism is now an established pattern rather than a one-off — allowRuntimeCreate: false on a DEFAULT_METADATA_TYPE_REGISTRY entry means PUT answers 403 NOT_CREATABLE for a brand-new name while still accepting updates to an existing one.

So a reader following this page hits a 403 the page gives them no way to anticipate, and the refusal message is the first place they learn the rule. The refusal messages are good — #7893 gave field a remedy naming the route that actually works — but a reference page should not require you to fail first.

Suggested fix

One caveat sentence on that entry: creates are gated per type by allowRuntimeCreate, some types accept updates but refuse brand-new names, and the refusal names the working route. Point at wherever the per-type flags are readable rather than enumerating types inline, so the sentence does not go stale the next time one is retired.

⛔ Do not enumerate field and job by name unless the enumeration is generated — a hand-written list of retired types is the same shape that goes stale, and this card exists because the page did not track a change.

Explicitly NOT in scope

Verified while filing

The #7893 sweep found the surviving route is documented in two places, so this is a completeness gap rather than a dead end for readers: content/docs/api/metadata-api.mdx:49 itself carries an object-typed body example, and content/docs/concepts/metadata-lifecycle.mdx:114 now states explicitly that writing the object with the field in fields is how a field is added.

Related

#7893 · PR #8496 · #4509 (the job retirement that made this incomplete first) · ADR-0049

Activity

  1. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    Routing report from the domain:metadata lane — ⛔ not claiming, ⛔ no label changed.

    PM session session_01XeQRiAa7vYRVX5Fog7Zby8.

    The file this card corrects is content/docs/api/metadata-api.mdx (confirmed present at that path on origin/main). The lane table routes content/docs/** to domain:devx, so by the anchoring rule this is a devx card whose subject happens to be the metadata API — the classic case the rule exists to disambiguate, since the label follows the package the fix lands in and never the issue's title vocabulary.

    ⇒ Suggested disposition (triage's call): relabel domain:devx (#6023's lane). No opinion offered on its content or priority.


    Generated by Claude Code

  2. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    Scope update: this card gains a third refusal class, from #8421 / PR #8770 (in review, not yet landed).

    This card's premise is that metadata-api.mdx documents PUT /meta/:type/:name as "Create or update a metadata item" without saying some types refuse creates — "now two of them".

    PR #8770 adds a third and differently-shaped class: a type name the platform does not have is now refused outright. PUT /api/v1/meta/fieldz/showcase_task.title changes from 200 {"success":true} to 400 INVALID_REQUEST, persisting nothing.

    It differs from the two already on this card in a way the doc line will need to reflect:

    • the existing two are per-type refusals — a recognised type that declines creates;
    • this one refuses because the type name is not recognised at all, so it is a statement about the whole :type path parameter rather than about any particular type.

    Line 26 of that page is the one most affected — it currently explains only the singular/plural folding ("object and objects address the same type") and says nothing about what happens to a name that folds to nothing.

    ⚠️ Verify against origin/main when this card is taken: PR #8770 is under review and deliberately not auto-merged pending a maintainer ratification of two narrowings, so the third class is not live yet. Do not document it until it lands.

    Also worth knowing for whoever takes this: PR #8770 refuses on the mint verb only. Reads and delete stay open, measured and argued in that PR — so the doc line belongs on PUT, not on the page's general description of /meta.

    Related: the docs-drift gate did not flag this page

    PR #8770 changes @objectstack/metadata-protocol and @objectstack/spec; the drift check listed 106 hand-written pages by package dependency and content/docs/api/metadata-api.mdx was not among them — the one page that documents the endpoint whose accept set the PR narrows. Filed separately so the gate gap has its own card rather than living in this one's comments.


    Generated by Claude Code

  3. os-zhuang commented on Aug 16, 2026

    @os-zhuang
    ContributorAuthor

    Suspected domain:* mislabel — reported, ⛔ not corrected. From the domain:metadata execution seat (#6367), PM session session_01NTKPDRoynY8i3HmdSFUxFj. An execution seat ⛔ does not change domain:* — that field has a single producer (triage) — so this is a report, and the card was skipped rather than dispatched this round.

    Why I think it is misrouted. The anchoring rule sets the domain by the package the fix lands in, and this card's fix lands entirely in documentation prose:

    • the named defect is content/docs/api/metadata-api.mdx:49;
    • the suggested fix is one caveat sentence on that entry;
    • the card's "explicitly NOT in scope" section rules out the only non-docs surfaces it touches (the refusal messages themselves, and releases/v17.mdx).

    content/docs/** routes to domain:devx (#6023) by the lane table. Nothing in the deliverable lands in packages/metadata* or packages/platform-objects, which is this seat's scope.

    Not a mechanical relabel — one judgement call for triage. The card's subject is a metadata contract (allowRuntimeCreate per-type gating, and which types refuse creates), so the "domain of the subject" and the "domain of the landing site" genuinely diverge here. The anchoring rule is explicit that the landing site wins and that title vocabulary must not decide it — which points at devx — but this is triage's call, not mine, and I would rather flag the divergence than quietly route around it.

    Prerequisite worth flagging either way, whoever takes it. The card says ⛔ do not enumerate field and job by name unless the enumeration is generated — because a hand-written list of retired types is the exact staleness shape that created this card. Whoever implements it should first check whether a generated readable surface for the per-type allowRuntimeCreate flags actually exists; if it does not, the "point at wherever the flags are readable" instruction has no target, and that is a scope question to settle before dispatch rather than during it.

    ⛔ Labels untouched (documentation, pm:queue, domain:metadata left exactly as they stand); ⛔ no assignee set. The card remains dispatchable for whichever lane owns it.


    Generated by Claude Code

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 17, 2026
  5. self-assigned this
    on Aug 17, 2026
  6. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    ContributorAuthor

    PM claim · seat pm-dispatch · session 17d48cbe-d3e0-562b-95a3-e2653a9bd9fb · branch claude/issue-8498-metadata-api-refusal-caveat

    Claiming for dispatch, at the maintainer's prompting.

    First, correcting my own earlier handling of this card

    On 2026-08-16 (5305713030) this seat reported a suspected domain:* mislabel and skipped the card. The report was right; the skip was wrong. ⛔ An execution seat does not change domain:* — but the label as it stands says domain:metadata, which is this seat's lane, and the devx seat cannot see a card labelled for mine. Skipping it on the strength of a suggestion triage has not acted on left it stranded in the one state where nobody picks it up. Dispatching it under the label it actually carries; if triage relabels mid-flight, the work moves with it.

    裁决 — not re-litigable

    1. ⛔ Do NOT hand-enumerate the refusing types. A hand-written list of retired types is the exact staleness shape that created this card. The caveat sentence points at wherever the per-type flags are readable; it does not restate them. This is the same defect class as findReferencesToMeta answers {references: []} for every target type absent from REFERENCE_PATHS — the "Used by" panel is silent by construction for 20+ declared types #9190 (REFERENCE_PATHS hand-curated) and 34 authored action visible/disabled predicates guard only the null half, so each silently hides its button on a list row that did not project the gated column #8990's census (a hand-written figure measured 12 where there were 57) — do not add a ninth instance to that tally.

    2. The scope prerequisite is a MEASUREMENT and it comes first. Determine whether a generated readable surface for the per-type allowRuntimeCreate / allowOrgOverride flags actually exists. ⚠️ If it does not, the "point at wherever the flags are readable" instruction has no target — stop and report. That is a scope question to settle before writing, not during. ⛔ Do not invent a surface to point at.

    3. Three refusal classes, and they are not the same shape. Two are per-type (a recognised type that declines creates). The third, from meta-unknown-type-namespace: a /meta type name that is not a plural of anything still mints a namespace — PUT /meta/fieldz/x answers 200 #8421 / PR fix(metadata): refuse a /meta type name the platform does not have, instead of minting a namespace for it (#8421) #8770, refuses because the type name is not recognised at all — a statement about the whole :type path parameter, not about any type. ⚠️ Measure whether PR fix(metadata): refuse a /meta type name the platform does not have, instead of minting a namespace for it (#8421) #8770 has landed on origin/main before documenting the third; on 2026-08-14 it was explicitly "not live yet — do not document until it lands." If it is still unlanded, document two and say so.

    4. The third class belongs on PUT only. PR fix(metadata): refuse a /meta type name the platform does not have, instead of minting a namespace for it (#8421) #8770 refuses on the mint verb; reads and delete stay open, measured and argued in that PR. ⛔ Do not attach it to the page's general description of /meta.

    5. ⛔ Never edit content/docs/releases/. The card's own "explicitly NOT in scope" section already rules out releases/v17.mdx and the refusal messages themselves. Both stay out.

    PM 机制假设 — measure, don't inherit

    Model tier

    Clause ② judged not triggered: documentation prose only; the fix lands entirely in content/docs/**; no accept/reject behaviour changes, no packages/spec/**, no error-code ledger, no *.zod.ts. Dispatching at sonnet. ⚠️ Fence: if closing this requires touching a refusal's message or its status, stop and report — that is a different card and a different tier.

    Route

    Worktree-first. Changeset per repo rules. Draft PR. ⛔ Never merge your own PR.


    Generated by Claude Code

  7. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    ContributorAuthor

    <!-- os-dev-report -->

    {
      "issue": 8498,
      "status": "done",
      "branch": "claude/issue-8498-metadata-api-refusal-caveat",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9289",
      "premise_still_valid": true,
      "summary": "Added a two-paragraph caveat under PUT /meta/:type/:name in content/docs/api/metadata-api.mdx covering both refusal classes: the per-type allowRuntimeCreate/allowOrgOverride gate (403 NOT_CREATABLE, no types enumerated inline -- linked instead to the generated MetadataTypeRegistryEntry schema reference and the pre-existing metadata-lifecycle.mdx overlay-whitelist table), and the unrecognisedMetaTypeRefusal 'not a metadata type at all' refusal from #8421/PR #8770 (400 INVALID_REQUEST, scoped to PUT only -- confirmed by reading its sole call site inside saveMetaItem). Line 26's singular/plural folding prose and the already-landed #9245 misspelling caveat were left untouched.",
      "tests": "Docs-only diff; ran the 4 content/docs-triggered spec-liveness gates plus docs-audit/redirects/role-word/doc-anchors, all green at 6e51a7c3c (this branch's head): check:nul-bytes OK; check:docs-audit-scope OK (178 hand-written docs in scope); check:docs-redirects OK; check:role-word OK; check:doc-anchors OK -- 244 internal fragment links across 396 files all resolve, confirming both new links in this PR are valid; pnpm --filter @objectstack/spec build OK (no generated-artifact drift); check:empty-state OK; check:liveness OK; check:strictness-ledger OK; check:variant-docs OK. Gate list re-derived via node scripts/pm/dispatch-gates.mjs against the actual changed path plus check:doc-anchors added by hand for the two new anchor links.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Measurements that determined scope

    1. Generated readable surface for the per-type flags -- exists, partially. content/docs/references/kernel/metadata-plugin.mdx is auto-generated (build-docs.ts; header says "AUTO-GENERATED -- DO NOT EDIT") and documents MetadataTypeRegistryEntry.allowRuntimeCreate / allowOrgOverride (existence, type, default) sourced from packages/spec/src/kernel/metadata-plugin.zod.ts. It does not enumerate the registry's concrete per-type values -- build-docs.ts documents the Zod schema, not the DEFAULT_METADATA_TYPE_REGISTRY array's contents. A separate, pre-existing, hand-written table with concrete current values already lives at content/docs/concepts/metadata-lifecycle.mdx#overlay-whitelist-shared-db-tenancy-invariant (added under #7893, not created or touched by this PR). Since both targets already existed and neither had to be invented, I did not stop -- the caveat links both instead of hand-enumerating field/job inline in metadata-api.mdx itself.

    2. PR #8770 (#8421) landed status -- PM's caution was stale, corrected by measurement. The comment on this card said "as of 2026-08-14 ... not live yet -- do not document until it lands." Checked mcp__github__pull_request_read: merged: true, merged_at: 2026-08-15T11:10:35Z, merge commit reachable from origin/main. It has been live for two days. Documented accordingly.

    3. unrecognisedMetaTypeRefusal vs. the already-documented metaUrlSpellingRefusal -- two different predicates, confirmed by reading packages/spec/src/shared/metadata-url-spelling.ts. main already carries a caveat (lines 14-17 of the file, landed 2026-08-17 by PR #9245 closing #9193) for metaUrlSpellingRefusal -- a misspelling of a declared type (viewes for view), which fires on nine verbs including reads. That is not the refusal this card is about. #8421/PR #8770's unrecognisedMetaTypeRefusal refuses a segment that is not a metadata type at all (fieldz), and is consumed only by refuseUnmintableMetaType, whose sole call site I confirmed is inside saveMetaItem -- the PUT path. So it belongs on PUT only, matching the card's mandate, and is a genuinely separate gap from what #9245 already closed.

    4. #9180 (singular/plural ruling) -- read in full; its step (1) (#9157/PR #9191) has landed, its step (3) has not. Step (1) narrows three read verbs (historyMetaItem/auditMetaItem/findReferencesToMeta) to refuse plurals. Step (3), which would rewrite metadata-api.mdx's "singular or plural" folding prose, is still "to file." That prose (currently around line 30-37, moved from the August-comment line 26) remains an accurate description of the currently-landed behavior for GET /meta/:type and GET /meta/:type/:name, so it was left untouched -- writing "types are singular" now would document a tolerance ahead of the ruling that retires it.

    Final prose added (the artifact, quoted per dispatch instructions)

    Under ### PUT /meta/:type/:name:

    Creates are gated per type: MetadataTypeRegistryEntry.allowRuntimeCreate decides whether a brand-new name may be minted under that type at all, separately from whether an item that already exists may be overlaid (allowOrgOverride). A type that declines the create half still accepts updates to names that already exist, and its refusal -- 403 NOT_CREATABLE -- names the route that does work. The flags are documented on MetadataTypeRegistryEntry; which types currently decline which half is tracked in the overlay whitelist rather than enumerated on this page, so this caveat doesn't go stale the next time a type is retired.

    This verb also refuses a :type segment that is not a metadata type the platform declares at all -- not a recognised spelling, and not a misspelling of one either -- with 400 INVALID_REQUEST, before any namespace is minted for it. The refusal is scoped to this verb: GET and DELETE on an item that already exists are unaffected. See unrecognised type spelling in the error catalog for the envelope -- the same code and status cover both this refusal and the misspelling one above; the message text tells them apart.

    Scope respected

    • content/docs/releases/** untouched.
    • The refusal messages/codes/statuses themselves untouched (tier fence held -- no packages/spec or runtime source touched, docs only).
    • No new error-catalog.mdx entry added -- the existing "Not this error" callout under the misspelling entry already states the second refusal's envelope shape and is what the new caveat links to; a full dedicated entry would be a separate, larger change.
    • No changeset: content/docs/** maps to no npm package (same treatment as precedent PR docs(api): catalog the /meta unrecognised-type-spelling refusal (#9193) #9245). skip-changeset label applied and read back: PR docs(api): caveat PUT /meta/:type/:name's create-refusal shapes #9289 carries ["size/s", "skip-changeset"].

    One platform-behavior note, not an action item

    Reading the PR back after creation, its body's attribution footer came back as _Generated by [Claude Code](https://claude.ai/code/session_01NTKPDRoynY8i3HmdSFUxFj)_ -- a different session ID (the PM's) than the one I wrote. Per AGENTS.md this is documented platform rewrite behavior and evidence of nothing else; not corrected, just noting it for the record.


    Generated by Claude Code


    Generated by Claude Code

  8. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    ContributorAuthor

    os-dev-report (marker correction — the previous comment's <!-- os-dev-report --> opener was HTML-escaped at rest to &lt;!-- os-dev-report --&gt; and no comment-edit tool is available in this session's toolset, so this follow-up carries the literal fallback marker instead, per the dispatch contract's own fallback instruction. The full report is comment #8498 (comment), unchanged.)

    {
      "issue": 8498,
      "status": "done",
      "branch": "claude/issue-8498-metadata-api-refusal-caveat",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9289",
      "premise_still_valid": true,
      "summary": "Added a two-paragraph caveat under PUT /meta/:type/:name in content/docs/api/metadata-api.mdx covering both refusal classes: the per-type allowRuntimeCreate/allowOrgOverride gate (403 NOT_CREATABLE, no types enumerated inline -- linked instead to the generated MetadataTypeRegistryEntry schema reference and the pre-existing metadata-lifecycle.mdx overlay-whitelist table), and the unrecognisedMetaTypeRefusal 'not a metadata type at all' refusal from #8421/PR #8770 (400 INVALID_REQUEST, scoped to PUT only -- confirmed by reading its sole call site inside saveMetaItem). Line 26's singular/plural folding prose and the already-landed #9245 misspelling caveat were left untouched.",
      "tests": "Docs-only diff; ran the 4 content/docs-triggered spec-liveness gates plus docs-audit/redirects/role-word/doc-anchors, all green at 6e51a7c3c (this branch's head): check:nul-bytes OK; check:docs-audit-scope OK (178 hand-written docs in scope); check:docs-redirects OK; check:role-word OK; check:doc-anchors OK -- 244 internal fragment links across 396 files all resolve, confirming both new links in this PR are valid; pnpm --filter @objectstack/spec build OK (no generated-artifact drift); check:empty-state OK; check:liveness OK; check:strictness-ledger OK; check:variant-docs OK.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code


    Generated by Claude Code

  9. added a commit that references this issue on Aug 23, 2026
    ceb37ba
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentationpm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions