Uh oh!
There was an error while loading. Please reload this page.
fix(designer,app-shell): spell the designer's lookup target and system marker as the spec does (#6041, #6044) - #6225
Merged
Conversation
… the spec accepts (objectui#6041)
`referenceTo` is not in `FieldSchema`'s accept set. Measured against the
installed `@objectstack/spec` 17.2.0, through the whole object document that
`PUT /api/v1/meta/object/:name` validates:
ObjectSchema.safeParse({ …, fields: { rel: { type: 'lookup',
label: 'Owner',
referenceTo: 'user' } } })
=> success = false
=> unrecognized_keys at ["fields","rel"] keys=["referenceTo"]
so authoring a lookup field in the designer returned a hard 422
`INVALID_METADATA` and, because the key is then stored, blocked every later
save of that object.
Both directions move, because a write-only repair would leave every
already-saved lookup field unreadable:
WRITE `MetadataService.toFieldPayload` and
`MetadataFieldsPage.fromDesignerField` — the parity gate's two
`wire` shapes — now emit `reference`.
READ `toDesignerField` now reads `raw.reference`. A spec-parsed server
sends that key, so before this the reference box loaded EMPTY for
every existing lookup field.
`referenceTo` also joins `RETIRED_FIELD_KEYS`: `carryOver` spreads the previous
server def verbatim, so a stored misspelling would otherwise ride straight back
out to the same 422 and keep a blocked object blocked.
The designer's in-memory `DesignerFieldDefinition` keeps `referenceTo` — the
internal prop name every other UI surface in this repo already uses, out of
reach of any wire-bound shape, and classified `uiOnly` by the gate. Its ledger
entry is removed, which the gate's both-directions ratchet requires.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L…d-tripping `isSystem` (objectui#6044)
`isSystem` is not in `FieldSchema`'s accept set; the spec spells it `system`.
Measured against the installed `@objectstack/spec` 17.2.0:
FieldSchema.safeParse({ type: 'text', label: 'L', isSystem: true })
=> success = false
=> unrecognized_keys keys=["isSystem"]
Two defects, one misspelling, and they are two DIFFERENT sites.
READ — the quieter, worse half. `toDesignerField` read `raw.isSystem` while a
spec-parsed server sends `system`, so the flag was always `undefined`. Nothing
went red: the flag is optional, and `undefined` is a valid "not a system
field". But it is load-bearing — `FieldDesigner` refuses to delete a system
field and disables its name and type inputs — so `organization_id`,
`created_at` and friends presented as ordinary editable, DELETABLE business
fields.
WRITE — no emit site at all. `fromDesignerField` never names the key; its only
route out is the verbatim `carryOver` spread, so a stored misspelling
round-tripped back as a hard 422 that blocks every later save. The repair is a
`RETIRED_FIELD_KEYS` tombstone, deliberately paired with the read fix and never
a substitute for it: stripping alone would close the 422 and fossilize the dead
detection. `system` itself is NOT stripped — it is a real `FieldSchema` key, and
carrying it through is what feeds the repaired read.
`app-shell`'s `FieldMetadataPayload` never declared the key, so `toFieldPayload`
had nothing to fix. The ledger entry is removed, as the gate's ratchet requires.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7LContributor
✅ 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 02:42
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 25, 2026
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#6041
Fixes#6044
A two-card fold from the #5761 parity family: both members are spelling repairs toward the key the spec declares, each independently verifiable against the gate's ledger. One branch, one PR, one commit per card —
1eabaa2f6for the lookup target,8c09d055dfor the system-field marker.Issue #6043 (
formula→expression) is excluded from this fold and stays open — a bare rename there would ship non-CEL expressions under a valid key, a silent runtime failure worse than the loud 422. Nothing in this branch touches it, and its ledger entry is untouched.What was wrong
referenceToreferenceFieldSchemarefuses it by name → saving a lookup field hard-blocks the objectisSystemsystemMeasured against the installed
@objectstack/spec17.2.0 (ESM build, the one the app bundles), through the whole object document thatPUT /api/v1/meta/object/:namevalidates:Both spec spellings parse green, so this is a key-by-key result rather than a schema that refuses everything.
Both directions, per key
Fixing only the write side would have left every already-saved field unreadable, so each card moves both.
#6041 ·
referenceMetadataService.toFieldPayloadandMetadataFieldsPage.fromDesignerField(the gate's twowireshapes) now emitreference;FieldMetadataPayloadandServerFieldSchemadeclare it.toDesignerFieldnow readsraw.reference. A spec-parsed server sends that key, so before this the reference box loaded empty for every existing lookup field.referenceTojoinsRETIRED_FIELD_KEYS:carryOverspreads the previous server def verbatim, so a stored misspelling would otherwise ride straight back out to the same 422 and keep a blocked object blocked.#6044 ·
systemtoDesignerFieldnow readsraw.system. The flag is optional, so the dead read never went red —undefinedis a valid "not a system field" — whileorganization_id,created_atand friends presented as ordinary editable, deletable business fields.fromDesignerFieldnever names the key, so its only route out is the verbatimcarryOverspread. That answers the open question on the card: the round-trip and the detection read are separate sites, and neither is inMetadataService.ts—FieldMetadataPayloadnever declaredisSystem, sotoFieldPayloadhad nothing to change.systemitself is not stripped: it is a realFieldSchemakey, and carrying it through is what feeds the repaired read.The ledger moved, per key
pnpm check:designer-field-key-parity, exit 0 before and after — the oracle both cards are measured against.Before:
After:
Both entries had to be removed, not edited: the gate's ratchet reds a ledger entry that no longer applies. The two keys stay declared on
DesignerFieldDefinitionand move into the gate'suiOnlylist besideidandvalidationRules. That is deliberate —referenceTois the internal prop name every other UI surface in this repo already uses (LookupField,filter-builder,ObjectChart,ListView,UserFilters), it reaches no wire-bound shape, and the gate catches it the moment anyone adds it back to one. Keeping it also leaves the publishedDesignerFieldDefinitionsignature unmoved, so both changesets arepatch.The behavioural edge: what a half-filled draft does
The spec's prose calls
reference"Required for relationship types". That is not enforced by the parse at 17.2.0, measured rather than assumed:And
undefinedis dropped byJSON.stringifyunder either spelling, so a half-filled draft — typelookup, target box left empty — puts byte-identical bytes on the wire before and after this change, and saves in both. The rename blocks no draft it did not already block. (Worth stating precisely because the in-memory literal disagrees with the wire: zod's strict object counts a key whose value isundefined,JSON.stringifydrops it — which is why every assertion here is made on the captured PUT bytes rather than on the object handed to the client.)Verification
Gates by name, each exit code read from the gate's own verdict line, never a bare
$?behind a pipe. Run at8c09d055d, the final commit.pnpm check:designer-field-key-paritypnpm --filter @object-ui/plugin-designer --filter @object-ui/app-shell type-checktype-check: Doneechoed for both)pnpm exec vitest runover plugin-designer, app-shell services/utils/metadata-admin, and the gate's self-testpnpm --filter '...@object-ui/app-shell' --filter '...@object-ui/plugin-designer' build(+ each consumer's own closure)FieldMetadataPayloadno longer compilespnpm --filter @object-ui/plugin-designer --filter @object-ui/app-shell lint(each is a fulleslint .)pnpm check:phantom-deps@objectstack/specimport undeclaredpnpm check:vi-mock-specifiersvi.mock('./FieldDesigner')pnpm check:control-bytespnpm check:doc-fencescheck-changeset-presence/-no-major/-fixedmajorbumpcheck-type-check-coverage/check-lint-coverage/check-entry-guard/check-pre-install-import-graphDirection of the downstream sweep, demonstrated rather than assumed:
--filter '...@object-ui/plugin-designer'selects app-shell, console and both example consoles (consumers); the suffix form selects types, core, components … (dependencies). The prefix form is the consumer direction, which is where a contract tightening lands. The first attempt was red withCannot find module '@object-ui/plugin-map'— diagnosed as an unbuilt-closure red rather than a missing-node_modulesone (apps/console/node_modules/@object-ui/plugin-mapresolves topackages/plugin-map, which had nodist/), and green once each consumer's own^...closure was built.Reverse verification — the narrowed type must actually reject the old spelling, not silently widen. Pasting
referenceTo: field.referenceToback into theFieldMetadataPayloadliteral:Restored under
trap … EXIT INT TERM;git diff HEAD --statempty afterwards. Independently, the rebuiltpackages/app-shell/dist/services/MetadataService.d.ts:58now readsreference?: string;— so the assertion is against a fresh build, not a cached declaration.Per-key ablation — the fold's whole justification
Each card's fix was reverted alone, with the other left in place, mutations proven on disk by grepping the injected and the removed text separately, anchors asserted unique before writing, restore under
trap:referenceTocontrol for lookup fields — a key FieldSchema rejects by name, so saving a lookup field hard-blocks the object #6041 only → 6 failures, all inMetadataFieldsPage.specKeyReference.test.tsxandMetadataService.specKeyReference.test.ts;specKeySystem.test.tsxfully green (7/7).systemflag asisSystem— so system-field detection reads a key the server never sends, and the key round-trips back out as a 422 #6044 only → 3 failures, all inMetadataFieldsPage.specKeySystem.test.tsx; both reference files fully green (13/13).Each mutated tree was checked to still contain the other card's fix as a literal string present in both versions of the file, so neither run could pass by accident.
Which assertions would still pass on a revert
Stated because a suite that cannot distinguish the two states of the world is worth nothing, and these were measured in the ablation runs rather than guessed:
systemflag asisSystem— so system-field detection reads a key the server never sends, and the key round-trips back out as a 422 #6044) —undefinedis falsy in both worlds; it is the negative half of the read pair, there so the harness cannot pass with two degenerate principals.system" (Both designer field shapes spell the spec'ssystemflag asisSystem— so system-field detection reads a key the server never sends, and the key round-trips back out as a 422 #6044) —systemis a realFieldSchemakey andcarryOveralways spread it; it asserts the tombstone does not over-reach.referenceTocontrol for lookup fields — a key FieldSchema rejects by name, so saving a lookup field hard-blocks the object #6041, theowner_idcase) — measured green under ablation and left in place with this note: with the read dead,carryOverstill carries the server's ownreferencethrough, so the field's stored target survives even unfixed. The revert-sensitive form of that claim is the newly authored lookup field, which fails on revert.Everything else fails on a revert of its own card.
Out-of-scope findings — filed, not repaired here
ObjectSchemarefuses by name (group,sortOrder,relationships) — the #5761 parity gate reaches field shapes only #6223 — object-level payloads emitgroup,sortOrderandrelationships, three keysObjectSchemarefuses by name. Same class one level up, outside this gate's reach (it reads field shapes only).grouphas a live control.app-shell/utils/metadataConverters.tshas zero importers — a dead module still carrying a name-heuristicisSystemand areferenceTotolerance #6224 —app-shell/utils/metadataConverters.tshas zero importers and is not in the published entry: a dead second copy of the read converter, still deriving the system flag from a name heuristic.The object-shape
isSysteminMetadataObjectsPage.tsx/ObjectManager.tsxwas measured separately, as the card asked:ObjectSchemaacceptsisSystem(42-key accept set), so it is not the same defect and nothing was changed there.Generated by Claude Code