feat(core): canonicalize the retired object-schema dialect at the ingestion choke point (objectui#7650) - #8873
Conversation
…estion choke point (objectui#7650) The object-schema serve path never parses. `ObjectStackAdapter.getObjectSchema` fetches the document, applies two mutations and returns it, with no `ObjectSchema.parse` anywhere. `FieldSchema` strictness therefore gates the metadata WRITE door only: a document stored before a key was tightened is served back verbatim, forever, and a host with its own `getObjectSchema` is served straight through. objectui#7155, #7166 and #7435 narrowed consumer reads to the camelCase spelling on the strength of "no spec-compliant producer can emit this key" — a claim about authoring, not about serving. This supplies the other half, the way objectui#6837 supplied it for `reference_to`: the legacy spelling is canonicalized once, here, and never at a consumer. The fold is DERIVED, not tabulated — the spec's own alias probe (lowercase, strip `_`, `-` and space) matched exactly against `FieldSchema`'s declared key set, read off `FieldSchema.shape`. `display_field`, `description_field`, `lookup_filters` and `lookup_columns` fold. `id_field`, `title_format` and a typo such as `sortible` probe onto no declared key and are left exactly as served — no special case, and no silent "correction". Nothing is dropped and nothing already set by the producer is overwritten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract review at
|
|
Handoff provenance — director seat, summon #18 segment 6 ( Generated by Claude Code |
|
Tier notice — the contract-review-tier requirement on this PR is lifted (skills seat, session Maintainer ruling, verbatim: 「现有的卡片如果写了要求fable的,也要让相关的项目经理知道,opus就够了。」 Under the same ruling set (quoted in full on objectstack-ai/objectstack#17285), the contract-review tier is reserved for the skills seat (protocol files + the published For this PR: its Generated by Claude Code |
Part of objectui#7650 — the second of the two halves the ruling on that card
(comment 5605081157) ordered, and the one the card is actually about. The first half is
objectui#8868. Neither completes the card alone, so nothing here uses a closing keyword.
Clause-②measuresyes(see below), so itcarries
needs:contract-reviewon both carriers and waits for the contract-review tier tobecome reachable. That is the designed cost of an honest declaration, not a stall to route
around.
The problem, restated from the measurement
The object-schema serve path never parses.
ObjectStackAdapter.getObjectSchemafetchesthe document, applies exactly two mutations and returns it — no
ObjectSchema.parseanywhere on the path (lit control: the same file does import and use
DroppedFieldsEventSchemaandisFilterAST, so the zero is a real absence).⇒
FieldSchemastrictness gates the metadata write door only. A document stored beforea key was tightened is served back verbatim, forever: it cannot be re-saved through the
strict door, but nothing ever asks it to be. And a host whose own
getObjectSchemareturnsa non-spec dialect is served straight through.
objectui#7155, #7166 and #7435 narrowed the consumer reads for
display_field,description_fieldandlookup_filtersto the camelCase spelling on the strength of "nospec-compliant producer can emit this key". That is a sound claim about authoring and a
different claim from "this consumer read can never fire". This restores the missing half,
the way objectui#6837 half 2 restored it for
reference_to— canonicalise once, at theingestion choke point, never at a consumer.
The shape, and the two shapes that were refused
Ruled A on objectui#7650: the serve path canonicalises.
would lose the value instead of arriving canonical, and silent data loss on a serve
path is the worst of the three shapes.
lintAuthoredRecordKeysand fold on itssuggestionwas refused on measurement, not preference: that function falls through to a Levenshtein
matcher when no
torow exists, so it answers "did you meansortable?" for the puretypo
sortible. A serve path that silently corrects a typo is worse than the defectit fixes.
How the fold is derived
By the spec's own alias-probe rule — lowercase, strip
_,-and space — matchedexactly against
FieldSchema's declared key set, read at runtime offFieldSchema.shape. Measured on the installed@objectstack/spec@17.3.0: 74 declaredkeys, zero probe collisions.
display_fielddisplayfielddisplayFielddescription_fielddescriptionfielddescriptionFieldlookup_filterslookupfilterslookupFilterslookup_columnslookupcolumnslookupColumnsid_fieldidfieldtitle_formattitleformatsortible(typo)sortiblereference_toreferencetoDerived rather than tabulated because a table has to be edited every time the spec grows a
camel key whose snake twin is still in stored documents, and the edit that does not
happen is the bug. The property that decided it:
id_fieldneeds no special case. Itprobes onto nothing declared and lands in the leave arm on its own — one rule giving both
the general behaviour and its exception.
lookup_columnsis in that table and is not in the card's list of four. It is not akey-by-key widening: it is a declared
FieldSchemakey whose snake twin belongs to the sameretired dialect, and the single derived rule covers it. It is pinned, so the generality is a
measured fact rather than an unnoticed side effect. Flagging it explicitly because a reader
comparing this diff against the card's four keys should not have to discover it.
What it deliberately does not do
arm", not "drop arm" in the destructive sense.
value.
probe rule, that probe folds nothing rather than picking one. Unreachable today by
construction; the zero-collision precondition is itself pinned, so the pin fires before
the guard has to.
main. Atest-only reset hook was written and then removed for exactly this reason.
The
id_fieldslice is blocked — and not on a cardThe dispatch that produced this work said
FIELD_KEY_GUIDANCE.id_field.whywas reachable,having verified it on objectstack's
origin/main. It is not reachable: measuredgrep -c id_fieldon the published@objectstack/spec17.3.0 (installed here) and17.4.0 (newest on npm) — 0 in both, with
startingNumber(2 hits) as the lit controlin the same read. The row landed after 17.4.0 was cut.
⇒ The blocker is a RELEASE of
@objectstack/speccarrying that row, not objectstack#16632(that card is not open, and it is not wrong). Hard-copying the sentence into this repo was
refused — a second copy of contract prose is the drift AGENTS.md #0.1 exists to stop — and so
was reading it optionally with a local fallback, which would silently degrade on an older
spec, this card's own defect class. Until the cut lands,
id_fieldis left alone, and theleave arm gets it right with no code owed.
title_formatis out of scope by ruling: it needs a separate eight-key maintainer decision,and widening this slice to reach it would re-block the slice.
Clause-② declaration
Clause-②: yes— measured on this diff.The mechanical floor is "a new exported symbol or a new key on a published payload is always
yes". No new exported symbol. But new keys on a published payload is precisely what this PR
does, by design:
displayField,descriptionField,lookupFiltersandlookupColumnsarestamped onto documents the published
@object-ui/data-objectstackadapter and the publisheduseMetadataItemhook serve. No shape that honours ruling A avoids this — everycanonicalisation adds the canonical key.
needs:contract-reviewis hung on this PR and onthe card.
Tests
New:
packages/core/src/utils/__tests__/reference-keys.retiredDialect-7650.test.ts, 25cases in four groups — the folds, the negative pins the ruling required by name
(
sortibleandid_fieldnot folded, plustitle_format, no-overwrite, idempotence), thedev-mode warning, and a contract-derivation group that asserts the rule against
FieldSchemaitself rather than against a copy of its key list: every folded pair must beone the spec REFUSES in the legacy spelling and ACCEPTS in the canonical one, with lit
controls in the same read (a bare def parses;
zzz_not_a_real_keydoes not).Runs, all in the dedicated worktree at commit
b3f26de35:packages/core/+packages/data-objectstack/:Test Files 195 passed (195),Tests 3704 passed (3704)packages/plugin-grid/(theRELATIONAL_META_READ_SETledger and its derivation test) +packages/app-shell/src/providers/+ the seven cross-package files that importnormalizeSchemaReferenceKeysdirectly (plugin-detail, plugin-gantt, plugin-tree,app-shell
deriveRelatedLists,ObjectView.objectBoundActions-7234):Test Files 145 passed (145),Tests 1274 passed (1274)turbo run type-check --filter=@object-ui/core --filter=@object-ui/data-objectstack --filter=@object-ui/plugin-grid:Tasks: 16 successful, 16 total; each package'stype-checkrunstsc --noEmit && tsc -p tsconfig.test.json, so the new test file is type-checked toocheck:designer-field-key-parity,check:handler-key-reads,check:spec-symbols,check:control-bytes,check:unreferenced-sources,check:vi-mock-specifiers,check:vi-mock-inherit,check:phantom-deps,check:unused-deps,check:changeset-presenceNOT MEASURED, with the reason:
check:spec-floors,check:readme-exportsandcheck:eager-closureeach refused to run for want of a built workspace ("Build theworkspace before running this gate", "its type entry
./dist/index.d.tsis not on disk","No eager-closure report at
apps/console/dist/eager-closure.json"). Those are brokengauges, not red gates, and a full workspace build plus a console build does not fit the
container's foreground budget under lock contention. Left to CI. The one of the three that
could plausibly move on this diff is
check:spec-floors, because the diff addsimport { FieldSchema } from '@objectstack/spec/data'to@object-ui/core, whose declaredfloor is
^17.2.0— checked by hand against the packed 17.2.0 tarball:grep -c FieldSchema dist/data/index.js= 40 anddist/data/index.d.ts= 5, lit controlstartingNumber= 2 in the same read. The floor is honest.Ablation
Three legs, from the committed tree, each proving a different pin family can fail.
Every leg proved the mutation reached disk before running (blob hash against
HEAD:packages/core/src/utils/reference-keys.ts=df88b4f58d84…) and restored bystate (blob back,
git diff HEADempty), under atrap … EXIT INT TERM.0269f66c2cc1…. Red: 8 failed, 17 passed. All fourfold cases, "runs on a def with NO relationship target", "reaches every field of a schema,
in both container shapes", and both warning cases.
5f01333994b1…. Red: exactly 1, "neverOVERWRITES a canonical key the producer already set".
4ba05aaeda0a…. This is the refusedoption C in miniature: a looser matcher. Red: exactly 1, "does NOT fold a TYPO —
sortiblenever becomessortable".id_fieldandtitle_formatstayed green under it,which is the correct direction — they have no near-miss to be dragged onto.
id_fieldpin. Its leave-armbehaviour is a property of the CONTRACT (no declared
idFieldonFieldSchema), so makingthat pin fail would mean fabricating a declared key, not mutating this code. The pin's job
is to lock the behaviour against a future implementation that replaces the derived rule with
a hand-written table. Stated rather than left as an implied fourth leg.
No build was involved: the mutated file is
@object-ui/core's own source and the testimports it by relative path, so nothing resolved through a
dist/that could have gonestale.
验收备注
Recorded, nothing filed, and none of it rides this PR:
referencewhile the pass now has two arms. Renaming themwould be a published-export change across roughly twenty in-repo citations and belongs to
its own card; the file header and both arms are marked instead, so a reader arriving from
a grep is not misled. Successor: whoever next touches those exports.
RELATIONAL_META_READ_SETinpackages/plugin-grid/src/relationalMetaKeys.tsisdeliberately unchanged. Its verdicts are about producibility and readership, and this
PR moves neither —
displayFieldand friends stayspecbecause a spec-compliantproducer still emits them. Its derivation test derives from consumer sources, and it
passes unchanged (in the 145-file run above). Mentioned because "the choke point now
stamps four more keys" reads like a ledger movement and is not one.
content/docs/fields/lookup.mdx,content/docs/fields/grid.mdxandcontent/docs/guide/record-edit-modes.mdauthorreference_toin example field defs, aspelling
FieldSchemarefuses at the metadata write door. Whether those examples describeauthored object metadata (in which case copying one fails) or a runtime field-metadata bag
(in which case they are fine) needs one reading I did not spend here, because it is a
different surface from this card's. Successor: the next docs pass over the lookup field.
Session, written as prose because a markdown-link footer does not reliably survive a body
PATCHin this repository: this pull request was produced by the Claude Code seathttps://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH.Generated by Claude Code