Skip to content

fix(spec,metadata-protocol): retire field's runtime-create door — an accepted PUT never reached the object (#7893) - #8496

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-7893-retire-field-runtime-create
Aug 13, 2026
Merged

fix(spec,metadata-protocol): retire field's runtime-create door — an accepted PUT never reached the object (#7893)#8496
os-zhuang merged 4 commits into
mainfrom
claude/issue-7893-retire-field-runtime-create

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#7893

Retires the runtime field create channel under ADR-0049 enforce-or-remove, per the maintainer ruling of 2026-08-12 (「接受你的全部建议。」) — option 2, retire the write channel.

What was wrong

field was declared with allowRuntimeCreate: true, so authoring a brand-new field at runtime was a sanctioned operation. The platform never built the read path for it. Measured end-to-end through the real HttpDispatcherObjectStackProtocolImplementationSysMetadataRepository:

  • PUT /api/v1/meta/field/showcase_task.zz_probe200, row persisted, state=active
  • GET /api/v1/meta/object/showcase_taskfields = [title, status], zz_probeabsent — forever
  • GET /api/v1/meta/field/showcase_task.zz_probe → 200, _diagnostics.valid: true

So the row is self-readable and well-formed, and reaches no object's fields — therefore no ObjectQL query, no physical column, no consumer that matters. Self-readable and universally inert. A declaration the platform cannot honour must be removed, not documented as inert (option 3 is what ADR-0049 forbids); building the read path (option 1) is a feature spanning at least three packages and needs its own card.

What changed

  • DEFAULT_METADATA_TYPE_REGISTRY: field now carries allowRuntimeCreate: false, with the ruling, the measurement and the rejected options recorded at the entry.
  • codeOnlySourceHint gains a NESTED_TYPE_REMEDY map so field's refusal names the route that actually composes — PUT /api/v1/meta/object/:object with the new field in fields — instead of reading its own filePatterns back. The glob **/*.field.ts matches nothing in any app, so prescribing it would answer a refusal with a route that has never worked.
  • ADR-0087 D3 SemanticMigrationfield-runtime-create-withdrawn (major 17). No D2 conversion, deliberately: allowRuntimeCreate is a platform registry value, not an authorable one, so no authored source changes — an **/*.object.ts file valid before this change is valid after it, byte for byte. What changed is a runtime HTTP verdict.

Adding a field at runtime is not lost.object keeps allowRuntimeCreate: true, so PUT /api/v1/meta/object/{name} with the new field in fields both persists and composes. What is withdrawn is a second, broken spelling of that operation, not the operation.

Measurements — predicted vs actual

MeasurementPredictedActual
Brand-new field PUT, after403 NOT_CREATABLE, message names the object routeas predicted; nothing persisted
Field of a runtime-created object403 — the tier is the type, not the parentas predicted
Positive control: object route200, and the new field reads backas predicted, fields contains extra
Reverse verification: restore the flag, rebuild specthe two create arms go redexactly 2 red, both expected 200 to be 403 — the original defect reproduced
Reverse verification: object-route controlstays greenstayed green — the retirement is scoped
Reverse verification: the #7894 plural pinpredicted red — this was wrongstayed green, see below

The anti-vacuity arm is kept and asserts on body.data.item.fields, not body.item (which is undefined and made an empty read look like a pass during the earlier investigation). A declared field is asserted present in the same response, so a dead read cannot be what makes the check pass.

The one prediction that missed, and why

I predicted the existing #7894 plural pin would go red under reverse verification. It did not. The reason is not a retro-fit: that case targets showcase_task.title, a field the package ships, so it exercises #7743's overlay tier and answers NOT_OVERRIDABLE. It never touched the create tier at all.

That is a real coverage hole rather than a curiosity — two independent gates are reachable through the same URL fold and only one of them had a plural pin. This PR adds the missing half: plural spelling with a brand-new name, so the fold is pinned onto the create lock and answers 403 NOT_CREATABLE. Without it, a future change to PLURAL_TO_SINGULAR could reopen the plural spelling as a create door while every existing pin stayed green.

The plural door (#7894) — verified closed, not re-closed

#7894 landed as d56012f6 and is an ancestor of this branch. Both spellings now fold to the singular and earn the refusal: PUT /api/v1/meta/fields/{object}.{name} answers 403 NOT_CREATABLE, and neither the field nor the fields namespace mints a row. PLURAL_TO_SINGULAR is untouched by this PR.

Disposition of existing sys_metadata rows

Rows already written through the retired channel stay in sys_metadata and are inert. They were inert before this change too — no read path ever composed them into an object — so nothing that used to work stops working, and no data is silently reinterpreted. They remain self-readable by name and still report _diagnostics.valid: true, which asserts only that the isolated document is well-formed (see #8169 — the envelope has no "in effect" axis). They may be deleted at leisure: deleteMetaItem is deliberately not gated by this refusal, so repair stays possible. An operator who needs the write door back on one deployment sets OS_METADATA_WRITABLE=field; that unlocks the write only — the field still will not reach its object, which is why it is a diagnostic and not a workaround.

How #7743's pinned control was updated

packages/runtime/src/meta-field-overlay-lock.test.ts pinned the brand-new-field write under the banner "THE FEATURE — allowRuntimeCreate: true is real and must survive". #7743 wrote that case specifically so a later fix could not quietly retire runtime field authoring, so it is flipped deliberately and on the record, not quietly.

Those pins were doing exactly their job. They even predicted in their own comment that "a fix that refused every field PUT" would be how this file went wrong — correct about the mechanism, wrong about the premise they shared, that there was a create door here worth protecting. There was not: the door opened onto nothing. The file's header table and the block comment now carry that record, and the inversion is this file's own anti-vacuity proof — those cases demonstrably reached the write door, because they measured its 200.

#7743's overlay refusal is untouched and stays. An artifact-backed field is still refused 403 NOT_OVERRIDABLE via isNestedArtifactField; allowOrgOverride: false is unchanged. Making field overrides legal is a separate decision from making field creates work.

One real bug fell out of the harness: its registerItem double declared (type, name, item) while the producer calls (type, item, keyStrategy). Nothing failed — the call stored the item object as the key and the string 'name' as the value, so the seeded entry was never replaced and every read served the stale body. A write-through that silently no-ops is exactly what an object-route control must be able to see, so the arity is now pinned to the producer's.

Note on #5488

The api retirement (#5488) transfers its mechanism here — the NOT_CREATABLE inlet — but not its justification. That ruling rested on "zero business pull for Studio-authored runtime endpoints today", and "add a field" is the opposite: a core Studio/CRM operation. The justification here is this card's own ruling plus the object route staying open.

Verification

  • Reverse verification done from the committed state, direction predicted first.
  • packages/spec gates, all PASS: check:liveness, check:empty-state, check:authorable-surface, check:docs, check:api-surface, check:spec-changes, check:upgrade-guide, check:skill-refs, check:skill-docs, check:skill-examples.
  • Gates re-derived from the actual changed paths with scripts/pm/dispatch-gates.mjs; all PASS, including check-adr-0087-registration (which caught a genuinely missing adr-0087: disposition marker on the changeset — now registered field-runtime-create-withdrawn), check:adr-anchors, check:cross-package-test-inputs, check:i18n, check:engine-double-contract, check:query-options-erasure, check:type-check-coverage, check:nul-bytes.
  • Tests: @objectstack/spec 393 files / 10380 tests, @objectstack/metadata-protocol 81 / 1205, @objectstack/objectql 197 / 3538, @objectstack/runtime 151 / 2318 — all passing. The pin file itself is 16/16.
  • Typecheck clean for spec, metadata-protocol and runtime; the shrink-only test-typecheck debt ledger was not grown.

origin/main merged before pushing.


Generated by Claude Code

…n accepted PUT never reached the object (#7893)
The registry declared `field` with `allowRuntimeCreate: true`, so
`PUT /api/v1/meta/field/<object>.<name>` was a sanctioned write. It was
accepted, persisted and reported valid — and read by nothing. Measured
end-to-end through the real HttpDispatcher -> ObjectStackProtocolImplementation
-> SysMetadataRepository: the write answered 200 `state=active`, and
`GET /meta/object/showcase_task` then listed `fields = [title, status]` with the
new field absent, forever. The row is self-readable by name with
`_diagnostics.valid: true` — well-formed and universally inert.
`field` is the one declared type with no standalone existence: fields are
authored inside the object (`ObjectSchema.fields`), so a `field` write mints a
separate row keyed ('field','<object>.<name>') and nothing composes fragment
rows into their parent. ADR-0049 enforce-or-remove; maintainer ruled REMOVE on
2026-08-12.
Adding a field at runtime is NOT lost: `object` keeps `allowRuntimeCreate: true`,
so writing the object with the field in `fields` both persists and composes.
What is withdrawn is a second, broken spelling of that operation. The refusal
says so — `codeOnlySourceHint` gives fragment types their real remedy instead of
reading `field`'s `filePatterns` back (`**/*.field.ts` matches nothing in any
app).
- `field` -> `allowRuntimeCreate: false`, with the ruling, the measurement and
the rejected options recorded at the registry entry.
- ADR-0087 D3 SemanticMigration `field-runtime-create-withdrawn` (major 17).
No D2 conversion: `allowRuntimeCreate` is a platform registry value, not an
authorable one, so no authored source changes.
- #7743's overlay refusal (403 NOT_OVERRIDABLE) is untouched, and its pinned
control "THE FEATURE — allowRuntimeCreate: true is real and must survive" is
retired deliberately and on the record, with the inversion documented.
- Harness fix: the registry double's `registerItem` mirrored the wrong arity
(`(type, name, item)` vs the producer's `(type, item, keyStrategy)`), so
object write-through silently no-opped and the object-route control could not
see it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WMpuAfA2KSdDjGF6tm1bH
…rd the ADR-0087 disposition
The existing #7894 plural pin targets `showcase_task.title` — a field the
package ships — so it exercises #7743's OVERLAY tier and answers
NOT_OVERRIDABLE. Measured: reverting `allowRuntimeCreate` to true leaves that
case green while both create cases go red, so it said nothing about the tier
this card retires. Adds the plural + brand-new-name case so the fold is pinned
on the CREATE lock too.
Also adds the `adr-0087: registered field-runtime-create-withdrawn` disposition
marker the changeset was missing (check-adr-0087-registration, #6148).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WMpuAfA2KSdDjGF6tm1bH
@vercel

vercelBot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 13, 2026 4:08pm

Request Review

@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

PM correction — my dispatch premise was wrong, and I argued my way into it

CI is running; this is not the review. But the dispatch that produced this PR opened with a false statement of fact, and the record should carry it.

I wrote, in bold:

This is a FRESH START, not a recovery. … The container was reset, so there is no local worktree either. Nothing to recover — begin from origin/main.

All of that after the first clause was wrong./home/user/objectstack-issue-7893 existed, with a clean tree and the complete implementation already committed on the branch. The last claim comment on the issue carries the same session ID as the agent I was dispatching. This was a recovery of a killed run, and the dev correctly treated it as one — verified the existing work and finished it rather than redoing it. Nothing was lost.

The error, precisely

I checked remote branches, found no claude/issue-7893-*, and then reasoned that a container restart wipes the filesystem, so the remote listing must be the complete picture. A container restart kills the process, not the disk.

What makes this worse than an ordinary mistake: this lane has a standing rule that says exactly not to do that.

⛔ check for surviving local work first — ls-remote proves nothing was pushed, not that no work exists.

That rule exists because it has been paid for: three re-dispatches earlier today each found surviving local work after an agent died. I wrote that rule into three separate briefs. Then I constructed an argument for why this case was different, and the argument was wrong.

I did not skip the check out of haste — I skipped it because I had a reason. That is the more dangerous failure, because a reason feels like diligence. The rule is written as a check rather than a heuristic precisely so it survives a plausible-sounding argument against it. Next time the answer is to run ls and see, which costs one command.

Had the dev followed the instruction literally, it would have rebuilt work that already existed and could have clobbered the local commits. It didn't, because it looked before it started.

Also worth recording, from the same run

check-adr-0087-registration caught a genuinely missing adr-0087: disposition marker on the changeset — and the mechanism is worth knowing: that gate reads COMMITTED blobs via git cat-file, so an uncommitted fix reads as still-broken. Anyone debugging a stubborn failure there should commit before re-running rather than concluding the fix didn't work.

On the missed prediction

The dev predicted #7894's plural pin would go red under reverse verification and it stayed green — recorded rather than retro-fitted, which is right. The reason turned out to be substantive: that case targets showcase_task.title, a field the package ships, so it exercises #7743's overlay tier and answers NOT_OVERRIDABLE. It never touched the create tier. Two independent gates are reachable through one URL fold and only one had a plural pin; this PR adds the missing half.

That is the second time today a green pin covered a narrower surface than its name implied — the first was my own vacuous array-length gate on #8375. A pin's name tells you what someone intended to test, not what it reaches.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via @objectstack/spec)
  • content/docs/automation/connectors.mdx(via @objectstack/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/metadata-protocol, packages/spec)
  • content/docs/concepts/north-star.mdx(via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/cli.mdx(via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx(via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/metadata-protocol, @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/spec)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/permissions/system-context.mdx(via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/metadata-protocol, @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/apps.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v16.mdx(via @objectstack/spec)
  • content/docs/releases/v17.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/metadata-protocol, @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review August 13, 2026 16:25
@os-zhuang
os-zhuang added this pull request to the merge queueAug 13, 2026
Merged via the queue into main with commit 859cb83Aug 13, 2026
27 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-7893-retire-field-runtime-create branch August 13, 2026 16:43
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-zhuang@claude