Uh oh!
There was an error while loading. Please reload this page.
fix(app-shell,designer): stop putting three object-level keys the spec refuses on the wire (#6223) - #6253
Merged
yinlianghui merged 2 commits intoAug 25, 2026
Conversation
…c refuses on the wire (objectui#6223)
`ObjectSchema` refuses `group`, `sortOrder` and `relationships` BY NAME.
Measured against the installed `@objectstack/spec` 17.2.0 (ESM build), whose
accept set is 42 keys:
const base = { name: 'account', label: 'Account',
fields: { n: { type: 'text', label: 'N' } } };
ObjectSchema.safeParse(base) => success = true (control)
ObjectSchema.safeParse({ ...base, isSystem: true }) => success = true (control)
ObjectSchema.safeParse({ ...base, pluralLabel: 'A' }) => success = true (control)
ObjectSchema.safeParse({ ...base, group: 'Sales' }) => unrecognized_keys ["group"]
ObjectSchema.safeParse({ ...base, sortOrder: 3 }) => unrecognized_keys ["sortOrder"]
ObjectSchema.safeParse({ ...base, relationships: [] }) => unrecognized_keys ["relationships"]
The two controls are what make that a key-by-key result rather than a schema
refusing everything.
Each key is resolved on its own, as the objectui#5761 family ruling requires:
group UI-only. The spec has no object-level grouping key
(`fieldGroups` groups the fields INSIDE one object), so the
Object Manager's grouping control and column stay and the
value is DERIVED from the accepted key `isSystem` instead of
round-tripped. `MetadataObjectsPage` also strips a `group`
already stored by an earlier build — its save-back spreads the
server document verbatim, so not writing the key is not the
same as removing it.
sortOrder Declaration removed. What populated it was the array index the
converter happened to be at. The field-level `sortOrder` is a
different key with its own card (objectui#6045) and is
untouched.
relationships Declaration removed from the payload. The spec models
relationships on the FIELD (`reference` / `master_detail` plus
object-level `indexes`); what the designer should author for a
relationship is a data-model question this does not settle.
The objectui#5761 parity gate gains a SECOND ORACLE: every entry in
`PAYLOAD_SHAPES` names the schema that judges it, and reach is resolved WITHIN
an oracle rather than across one — `group` is a legal `FieldSchema` key and a
refused `ObjectSchema` key at the same time, so a pooled accept set would have
stayed green on exactly these three.
That extension surfaced two more, filed and not fixed here: `enabled`
(objectui#6238, written by the soft-delete path) is ledgered, and `fields` sent
as an array where the spec wants a map (objectui#6240) is a value-level
rejection the key-name check cannot see — pinned as an assertion so it cannot
change silently.
Assertions are on captured PUT bytes, not the in-memory object: `undefined` is
a key zod's strict object counts and `JSON.stringify` drops.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L…es its key Found by objectui#6223's own per-key ablation, and it is a hole the second oracle opened. Re-declaring the OBJECT-level `sortOrder` left the gate GREEN: the ledger is keyed by key NAME, so objectui#6045's FIELD-level entry absorbed an object-level reappearance in silence — the ledger becoming the hiding place the gate's header says it must never be. Entries now name their oracle (defaulting to `FieldSchema`) and are matched on `(key, oracle)`. Staleness is scoped the same way in both directions: an entry whose oracle no shape declares the key under is stale even when a shape of the OTHER oracle still declares that spelling. Two executable controls carry it, both derived from the measurement above, plus an assertion that every real entry names an oracle rather than defaulting into one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
Contributor
✅ 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
|
yinlianghui
marked this pull request as ready for review
August 25, 2026 04:48
This was referenced Aug 25, 2026
Uh oh!
There was an error while loading. Please reload this page.
yinlianghui
deleted the
claude/issue-6223-object-payload-spec-key-parity
branch
August 25, 2026 05:01
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#6223
ObjectSchemarefusesgroup,sortOrderandrelationshipsby name, and both object-level designer writers were putting them on the wire. The #5761 parity gate could not see it: itsPAYLOAD_SHAPEShad three field shapes and no object shape, so the parent document the designers actually PUT was unchecked.The measurement — re-derived on this branch, not inherited
Measured against the installed
@objectstack/spec17.2.0, ESM build, onorigin/main@0409b766d(the card measured @7da7b8a1d, before #6225 landed; everything below was re-run):Accept set: 42 keys, containing none of the three. The two controls are what make this a key-by-key result rather than a schema that refuses everything, and they are asserted first in every test file here.
Unlike #6041's
referenceTo, none of the three has a near-spelling in the accept set — there is no "did you mean" suggestion for any of them, because the spec has no object-level counterpart at all.No live 422 is claimed. The card explicitly declined to reproduce one and so does this PR: whether the deployed route rejects these today depends on what that route parses with. The schema fact is the ground for the fix.
Three keys, three resolutions
Per the #5761 family ruling — the spec is the authority, each key resolved on its own.
group— UI-only; the control stays, the value is derived.The spec has no object-level grouping key.
fieldGroupsis onObjectSchema, but it groups the fields inside one object, so it is not a mapping target. The Object Manager's grouping is a display category:metadataConvertersderives it from thesys_prefix andMetadataObjectsPagewas readingraw.group— a key the schema refuses, so the server never stored one and that column rendered empty anyway. The grouping control and column are untouched; the value now derives fromisSystem, a key the spec does accept.Two halves, and the second is why a write-only fix would not do:
mergedis built by spreading the raw server document, so an object that already hadgroupstored from an earlier build would spread it straight back out and stay permanently unsaveable.delete merged.groupstrips it on the way out — the #4644 strip-on-load shape, applied where the spread is.sortOrder— the declaration is deleted.No spec equivalent at object level. What populated it was the array index the converter happened to be at (
sortOrder: index) — the order the list was already in, not a fact about the object. Removed fromObjectMetadataPayload/toObjectPayload.This is not#6045. That card is the field-level
sortOrder, a different key under a different oracle with its own resolution; it stays open and untouched, and there is an assertion here that field-levelsortOrderstill makes the trip so the two cards stay independently measurable.relationships— the wire declaration is deleted; the data-model question is named, not answered.The spec models relationships on the FIELD (
reference/master_detail) plus object-levelindexes. The payload stops declaring and sending an object-level relationship array. What the designer should author for a relationship is a data-model question this PR does not settle —ObjectDefinition.relationships(the UI model) and its read converter are deliberately left alone rather than guessed at.No spec-side addition is needed for any of the three, so the fork clause was not triggered. Nothing in
packages/specis touched.The gate extension — a second oracle
Every entry in
PAYLOAD_SHAPESnow names the schema that judges it, and three object-level shapes join the three field ones:FieldMetadataPayloadFieldSchemaServerFieldSchemaFieldSchemaDesignerFieldDefinitionFieldSchemaObjectMetadataPayloadObjectSchemaServerObjectSchemaObjectSchemaObjectDefinitionObjectSchemaReach is resolved within an oracle, never across one.
groupis a legalFieldSchemakey and a refusedObjectSchemakey at the same time, so a single pooled accept set would have stayed green on exactly the three keys this card is about. There is an executable control for that.The ledger hole this PR's own ablation found
Reverting the object-level
sortOrderalone left the gate green: the ledger is keyed by key name, so #6045's field-level entry absorbed the object-level reappearance in silence — the ledger becoming the hiding place its header says it must never be. Entries are now scoped to their oracle and matched on(key, oracle), with staleness scoped the same way in both directions. Second commit, two controls, plus an assertion that every real entry names an oracle rather than defaulting into one.Two more keys surfaced, filed and not fixed
enabled, whichObjectSchemarefuses by name — the soft-delete path is what puts it on the wire #6238 —enabled, refused by name. Ledgered.ObjectSchemadoes haveenable, but it isObjectCapabilities, a features-module object, not a boolean — so this is not a rename. The declaration is latent; the live path isdeleteObject/deleteMetadataItemwriting{ enabled: false, _deleted: true }directly.fieldsas an ARRAY whereObjectSchemarequires a map — a value-level rejection the key-name parity gate cannot see #6240 —fieldssent as an array where the spec requires a map (and requires it at all). A value-level rejection, the gate's coverage note 4, which a key-name check cannot see.Verification
Run at
fcca348f5, the branch head.packages/app-shell+packages/plugin-designerfull suites9a674307a; the second commit touches onlyscripts/)check:designer-field-key-paritydesigner-field-key-parity: OKcheck:phantom-deps/self-import/esm-specifiers/spec-symbols✅check-control-bytes/check-vi-mock-specifiers/check-changeset-no-major/check-changeset-fixed✅type-check(both packages, incl.tsconfig.test.json) +type-check:scriptsDone, exit 0Lint — a declared narrowing, not a full run.
pnpm lint(turbo, 47 tasks) was cut off by the container's foreground cap at 7 successful tasks, so it is not reported as a pass. Instead,eslint --no-inline-config --format jsonover the 7 changed files: 7 files linted, 0 errors, 2 warnings, both pre-existing onorigin/main(raw: anyat main:238 ofMetadataService.ts;void reload()at main:146 ofMetadataObjectsPage.tsx). The narrowing is sound because the config extendstseslint.configs.recommended— notrecommendedTypeChecked— and sets noparserOptions.project/projectService, so no rule reads another file's types and this diff cannot move the verdict on any file it does not touch. CI runs the full farm regardless.The gate is green before and after — so the evidence is elsewhere
pnpm check:designer-field-key-parityexits 0 on both trees. The evidence that this change did anything is the ledger/shape diff plus a per-key ablation, each leg mutating one key's resolution alone, proving the mutation on disk by grepping the injected text and separately the removed text, and restoring undertrap … EXIT INT TERM.git diff HEAD --statwas empty after every leg. No rebuild is involved: every mutated file is imported by relative specifier from its test, so noexports/distresolution is in play.groupgroupcases inMetadataService, all fourgroupcases inMetadataObjectsPage, gate redsortOrder,relationshipsfully greensortOrdergroup,relationshipsfully green;MetadataObjectsPagesuite fully greenrelationshipsgroup,sortOrderfully greenFieldSchema)STALE LEDGER ENTRIESand violationsA fourth leg is worth naming: the
oracleleg first aborted on its own anchor-uniqueness assertion (3 matches, not 1) and measured nothing rather than measuring the wrong thing.Which assertions would still pass on a revert
Stated plainly, because they are the ones that prove nothing about the fix:
ObjectSchema/FieldSchema— strictness, the 42-key accept set, the controls, that each key is refused by name. They are the non-vacuity floor and are true on any tree.a half-filled object … puts identical bytes, as it always did—undefinedis a key zod's strict object counts andJSON.stringifydrops, so an object that never had these keys populated produced byte-identical output before and after. It is here to prove the fix did not newly break the untouched half, which is a claim about what did not change.leaves the FIELD-level sortOrder alone — that key is #6045— unchanged by this PR in either direction, and deliberately so.the fixture really did carry the key(theLEGACY.groupnon-vacuity control) — an assertion about the fixture, not the code.ObjectSchema"no near-spelling" case — a property of the spec, not of this diff.groupandoraclelegs only, everysortOrderandrelationshipscase still passes — that separation is the per-key result.Everything else in the three
objectui#6223 ·describes reds on the revert of its own key, which is what the per-key ablation above measures.Notes
JSON.parseof the request body), never the in-memory object handed to the client — theundefinedasymmetry above is exactly why.minorfor both packages, naming the three retired spellings in words. Breaking for TypeScript consumers ofObjectMetadataPayload: the three properties are gone from the published type, so code that set them stops compiling — which is the point, since setting any of them produced a payload the route refuses. Per AGENTS.md 版本号策略, objectui's own breaking changes ship asminor.isSysteminMetadataObjectsPage/ObjectManageris accepted byObjectSchema(re-confirmed here) and was left alone. Field designer offers aformulacontrol for formula fields — a key FieldSchema rejects by name (spec spells itexpression), so saving a formula field hard-blocks the object #6043 and finding:FieldMetadataPayload.sortOrderis a key FieldSchema rejects, written bytoFieldPayload— latent only because nothing populates it #6045 are untouched.Draft on purpose — not marking ready, not enqueueing, not enabling auto-merge.
Generated by Claude Code
Generated by Claude Code