Repository navigation
Description
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
- ⛔
content/docs/releases/v17.mdx— it carries ajobretirement row and nofieldrow. That is correct and untouched: release notes compile centrally from changesets, and meta-field-write-inert: an acceptedfieldPUT never reaches the object — a runtime-created field is storedvalid=trueand is absent fromfieldsforever #7893's changeset is the input. Do not edit it. - The refusal messages themselves. meta-field-write-inert: an accepted
fieldPUT never reaches the object — a runtime-created field is storedvalid=trueand is absent fromfieldsforever #7893 shippedNESTED_TYPE_REMEDYsofield's refusal namesPUT /api/v1/meta/object/:object; that is working as intended and is not what this card is about.
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
hotlong commentedon Aug 14, 2026
Routing report from the
domain:metadatalane — ⛔ 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 onorigin/main). The lane table routescontent/docs/**todomain: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
hotlong commentedon Aug 14, 2026
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.mdxdocumentsPUT /meta/:type/:nameas "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.titlechanges from200 {"success":true}to400 INVALID_REQUEST, persisting nothing.It differs from the two already on this card in a way the doc line will need to reflect:
:typepath 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 ("
objectandobjectsaddress the same type") and says nothing about what happens to a name that folds to nothing.origin/mainwhen 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-protocoland@objectstack/spec; the drift check listed 106 hand-written pages by package dependency andcontent/docs/api/metadata-api.mdxwas 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
os-zhuang commentedon Aug 16, 2026
Suspected
domain:*mislabel — reported, ⛔ not corrected. From thedomain:metadataexecution seat (#6367), PM sessionsession_01NTKPDRoynY8i3HmdSFUxFj. An execution seat ⛔ does not changedomain:*— 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:
content/docs/api/metadata-api.mdx:49;releases/v17.mdx).content/docs/**routes todomain:devx(#6023) by the lane table. Nothing in the deliverable lands inpackages/metadata*orpackages/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 (
allowRuntimeCreateper-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 atdevx— 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
fieldandjobby 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-typeallowRuntimeCreateflags 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:metadataleft exactly as they stand); ⛔ no assignee set. The card remains dispatchable for whichever lane owns it.Generated by Claude Code
os-zhuang commentedon Aug 17, 2026
PM claim · seat
pm-dispatch· session17d48cbe-d3e0-562b-95a3-e2653a9bd9fb· branchclaude/issue-8498-metadata-api-refusal-caveatClaiming 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 suspecteddomain:*mislabel and skipped the card. The report was right; the skip was wrong. ⛔ An execution seat does not changedomain:*— but the label as it stands saysdomain:metadata, which is this seat's lane, and thedevxseat 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
⛔ 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
findReferencesToMetaanswers{references: []}for every target type absent fromREFERENCE_PATHS— the "Used by" panel is silent by construction for 20+ declared types #9190 (REFERENCE_PATHShand-curated) and 34 authored actionvisible/disabledpredicates 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.The scope prerequisite is a MEASUREMENT and it comes first. Determine whether a generated readable surface for the per-type⚠️ 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.
allowRuntimeCreate/allowOrgOverrideflags actually exists.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⚠️ 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
/metatype name that is not a plural of anything still mints a namespace —PUT /meta/fieldz/xanswers 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:typepath parameter, not about any type.origin/mainbefore 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.The third class belongs on
PUTonly. 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.⛔ Never edit
content/docs/releases/. The card's own "explicitly NOT in scope" section already rules outreleases/v17.mdxand the refusal messages themselves. Both stay out.PM 机制假设 — measure, don't inherit
content/docs/api/metadata-api.mdx, the defect at line 49, and line 26 (which currently explains only singular/plural folding and says nothing about a name that folds to nothing) is the one most affected by the third class. All three line numbers are from August comments — re-derive them, the file has almost certainly moved./metasingular/plural story is changing under this card. [Ruling] The/metatype segment is ALWAYS singular — retire every plural spelling, no exception, in three steps #9180 is a live maintainer ruling that the type segment is ALWAYS singular, and its step ① landed (Three read-side/metaverbs (history, audit, references) never reachcanonicalizeMetaRequestType— they still derive their key from the MANIFEST map #9157 / PR fix(metadata): route the three read-side/metaverbs through the canonical type boundary (#9157) #9191). Line 26's folding prose may now be describing a tolerance that is being retired. Check [Ruling] The/metatype segment is ALWAYS singular — retire every plural spelling, no exception, in three steps #9180's state before writing anything about folding, and if the prose you'd write would contradict that ruling, ⛔ stop and report rather than documenting a tolerance on its way out.Model tier
Clause ② judged not triggered: documentation prose only; the fix lands entirely in⚠️ 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.
content/docs/**; no accept/reject behaviour changes, nopackages/spec/**, no error-code ledger, no*.zod.ts. Dispatching atsonnet.Route
Worktree-first. Changeset per repo rules. Draft PR. ⛔ Never merge your own PR.
Generated by Claude Code
os-zhuang commentedon Aug 17, 2026
<!-- 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.mdxis auto-generated (build-docs.ts; header says "AUTO-GENERATED -- DO NOT EDIT") and documentsMetadataTypeRegistryEntry.allowRuntimeCreate/allowOrgOverride(existence, type, default) sourced frompackages/spec/src/kernel/metadata-plugin.zod.ts. It does not enumerate the registry's concrete per-type values --build-docs.tsdocuments the Zod schema, not theDEFAULT_METADATA_TYPE_REGISTRYarray's contents. A separate, pre-existing, hand-written table with concrete current values already lives atcontent/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-enumeratingfield/jobinline inmetadata-api.mdxitself.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 fromorigin/main. It has been live for two days. Documented accordingly.3.
unrecognisedMetaTypeRefusalvs. the already-documentedmetaUrlSpellingRefusal-- two different predicates, confirmed by readingpackages/spec/src/shared/metadata-url-spelling.ts.mainalready carries a caveat (lines 14-17 of the file, landed 2026-08-17 by PR #9245 closing #9193) formetaUrlSpellingRefusal-- a misspelling of a declared type (viewesforview), which fires on nine verbs including reads. That is not the refusal this card is about. #8421/PR #8770'sunrecognisedMetaTypeRefusalrefuses a segment that is not a metadata type at all (fieldz), and is consumed only byrefuseUnmintableMetaType, whose sole call site I confirmed is insidesaveMetaItem-- thePUTpath. So it belongs onPUTonly, 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 rewritemetadata-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 forGET /meta/:typeandGET /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:Scope respected
content/docs/releases/**untouched.packages/specor runtime source touched, docs only).error-catalog.mdxentry 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.content/docs/**maps to no npm package (same treatment as precedent PR docs(api): catalog the /meta unrecognised-type-spelling refusal (#9193) #9245).skip-changesetlabel 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
os-zhuang commentedon Aug 17, 2026
os-dev-report (marker correction — the previous comment's
<!-- os-dev-report -->opener was HTML-escaped at rest to<!-- os-dev-report -->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
docs(api): caveat PUT /meta/:type/:name's create-refusal shapes (obje…