fix(designer): keep a relationship target stored only as referenceTo (objectui#8896) - #8918
Merged
Merged
Conversation
…` (objectui#8896) Two field-IO sites dropped the retired `referenceTo` key without keeping its value, so a lookup or master-detail field whose target survived only under the pre-objectui#6041 spelling lost the target on the way through. `MetadataFieldsPage`'s carried-through half (objectui#8060) re-emits a stored field verbatim with no read door in front of it, so `carryOver`'s strip was the last thing to touch the field: the target left with the key and `assertRelationshipTargetPresent` then refused the WHOLE save — a save the author triggered by relabelling an unrelated field, on a page that renders the offending field read-only, so "Pick the target object" named a control that does not exist there. metadata-admin's `readFields`, the single read door for `draft.fields`, deleted the same value on load and committed the loss on the next save. Both sites now lift the value onto the spec spelling before dropping the retired key, which is the shape the designable half has always used. The strip itself is unchanged: `FieldSchema` refuses `referenceTo` by name and no site emits it. The recovery is written per site and keyed to this one key — NOT driven off the registry's `specEquivalent`, which objectui#6043 refused as a mechanism — and shares `assertRelationshipTargetPresent`'s own predicate, so it can never hand the guard a target the guard would refuse. The tombstone registry's unconditional "The strip loses nothing" and the read door's "Nothing is lost on the way out" are corrected to say where they hold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
…erenceto-carryover # Conflicts: # packages/plugin-designer/src/MetadataFieldsPage.tsx
…ating it unconditionally (objectui#8896) The third site's read path belongs to its caller, so `saveFields` has no in-repo caller to measure. Recording that as UNMEASURED rather than folding it into a fresh unconditional claim is the whole point of this card. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
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
|
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 free
to 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 #8896
Both sites the card names are closed, and the tombstone sentence it falsifies is
rewritten to be true per site rather than restated unconditionally. Every reading
below was measured on this branch — the post-merge ones on
378ef4ec5.3f4b45886) and touches the samefile. This branch is merged onto it and the reproduction was re-measured on the
merged head, so nothing here rests on a pre-merge reading.
The defect, reproduced before anything was changed
carryOverstripsreferenceTobecauseFieldSchemarefuses that spelling BYNAME. On the designable branch that now costs nothing: #8899 taught
toDesignerFieldto read the target under either spelling(
storedRelationshipTarget), andfromDesignerFieldre-emits it asreference.The two sites in this card have no read door in front of them.
Site 1 —
MetadataFieldsPage's carried-through half. Stored{ parent_id: { type: 'master_detail', label: 'Parent', referenceTo: 'invoice' } },author relabels an unrelated
namefield. On the unmodified tree, with the newpin in place:
putsis empty: no PUT at all. The author edited a different field, and apreserved field is rendered read-only on this page by design (objectui#8060), so
"Pick the target object" names a control that does not exist there. #8899's own
docblock names this path and says it is not addressed there.
Site 2 — metadata-admin's
readFields. The card filed this one as a shape tomeasure rather than a finding. Measured: the same loss, one door earlier. On the
unmodified tree,
Tests 4 failed | 20 passed (24)— the value is deleted on loadwith nothing left behind, and
writeFieldscommits the loss on the next save.Every reader downstream reads the canonical key alone, because objectui#6837's
ruling deleted the per-reader legacy arms (
resolveReferenceTois literallydef.reference), so the inspector's target editor rendered empty.Re-measured after the merge (ablation leg A below — the one line restored to
main's
carryOver(keep.raw)):Tests 4 failed | 3 passed (7). The defect islive on current main.
What the repair is — and the four things it is not
Both sites lift the value onto the spec spelling
referencebefore droppingthe retired key.
Site 1 does it through
storedRelationshipTarget— #8899's own reader, not asecond one. That is the point of the merge resolution:
toFieldsMapandcarryOverare already a sibling-writer family where a difference is a defectwaiting to be found twice, so the two branches of this one writer now state ONE
spelling rule, argued once, in the function #8899 added. This also sits with
#8868 (
objectui#7650), which canonicalizes reference keys at the metadataprovider's serve door — recover at the door, read canonically everywhere else.
assertRelationshipTargetPresentis byte-for-bytemain's.
carryPreservedFieldadopts what the document holds and judgesnothing; whether a value can be a target stays entirely the guard's question,
exactly as on the designable half. A stored
referenceTo: ' 'is thereforerefused on the guard's blank branch, by name, before the request.
referenceTowould be the hard 422 the strip exists to prevent.
specEquivalentmigration. The registry says of that field:"Documentation for the reader, NEVER an instruction to migrate a value
mechanically". objectui#6043 refused exactly that for
formula, whose value isa LANGUAGE;
isSystem's strip IS the whole write half of objectui#6044.normalizeFieldReferenceKeysfrom core at these sites. That helperalso stamps
reference_to, a second spellingFieldSchemarefuses and thiscarry-over does not strip: on a write path it trades one 422 for another.
The two sites differ in one recorded way, and the asymmetry is deliberate: site 1
adopts an unusable value and lets the guard refuse it by name; site 2 is a read
door with no guard behind it, feeding client validation and every reader, so it
never invents a
referencekey where the draft holds no usable target. Both arepinned, and both pins were made to fail.
Pins, each with a firing control that was made to fail
Three ablations on the merged head, run from the committed state. Every mutation
was proven on disk by a marker grep before its run; every restore by blob-hash
equality against the HEAD blob plus an empty
git diff HEAD, ending0 file(s) differ from HEAD.carryOver(keep.raw)Tests 4 failed | 3 passed (7)return;at the top ofassertRelationshipTargetPresentTests 2 failed | 5 passed (7)— both still-refused controls go red, the positive cases stay greenisUsableTargetconjunctsTests 2 failed | 7 passed (9)— canonical-first and no-invented-target go redLeg B is the control the card asked for by name: without it the pin would pass on
a build that simply deleted the gate. Restore proof:
F1 397759dc3b516a9bb74316198fefc59cf2eef0bc,F2 680d32659843bbf7da2e862904d5c7a442bb999d, both matching HEAD after restore.Fixtures re-judged, not batch-edited
Three cases in
object-fields-io.retiredKeys.test.tsasserted the old lossy shapewith
toEqual({ type: 'lookup', label: 'Owner' }). They were judged one at a timeand the asymmetry is now recorded as data (
RESIDUE):indexedandisSystemleave nothing behind for two different recorded reasons, and
referenceTois theone key whose VALUE the draft may hold nowhere else. Every
key in outassertion — the thing those cases are named for — is unchanged.
Gates, exit codes captured before any pipe — all on
378ef4ec5check:designer-field-key-paritydesigner-field-key-parity: OKcheck:control-bytescheck:vi-mock-inheritcheck:vi-mock-specifiersnode scripts/check-changeset-presence.mjscheck:governed-queue-guard --testtype-check— types, app-shell, plugin-designerDonelinesplugin-designer+types+app-shell/views/metadata-adminTest Files 430 passed (430)·Tests 5939 passed | 1 skippedpackages/app-shell+apps/console(required sweep)Test Files 768 passed (768)·Tests 7619 passed | 1 skippedPre-merge, on
5465cfffd, these also read 0:census:cross-file-line-citations,census:tsconfig-test-parity,check:doc-fences,check:comment-mask-corpus,check:unreferenced-sources.Every vitest run reports
RUN v4.1.10 /home/user/objectui-issue-8896— thisworktree, not a sibling's copy.
check:doc-examplesanswered 2 withPREREQUISITE NOT MET(
plugin-markdown/plugin-timelinedist/index.d.tsabsent in a freshworktree — packages this diff does not touch). And
pnpm run check:changeset-presenceanswered 1 because no script by that name exists in
package.json; the gateitself is the
node scripts/…row above, which answered 0.type-checkdoes not compile*.test.ts(x): measured withtsc --listFiles, 0 hits for both new pins, 1 hit each for the source files theycover (positive control). This repo's recorded shape, not a regression.
objectui#8614 ledger hazard — checked, with a control either way
scripts/check-doc-example-types.mjskeysUNGATED_EXAMPLESbyfile:lineandthis diff shifts lines in three source files. Zero of the 74 files that ledger
cites is a file this diff touches, and a tree-wide grep for any
FILENAME:LINEcitation to those four files returns 0 — re-run after the merge, same reading.
Both instruments fire: injecting a known-cited path into the intersection returns
a hit, and the control spelling returns 3. ⇒ no row needs re-keying.
Pre-flight and live branches — measured
Base
348725a7c→origin/main: the window was not empty, so the control canfire. It caught #8899 landing in this very file, which is why this branch carries
a merge.
git merge-tree --write-tree origin/main HEADagainst4fa0eb9c4nowexits 0 with no conflict.
claude/kanban-gantt-family-retirementtouches 49files, 15 under
packages/types/, and none ispackages/types/src/internal/retired-field-keys.ts;claude/issue-8071-member-pins-slice-5touches 3, none shared; #8868 touches 5,none shared. Positive control on the collision instrument: injecting one of this
diff's paths into the other side returns exactly 1 hit.
Clause-② stays
noNothing is declared and no accept set moves.
ServerFieldSchemagains no member —the retired spelling is read through the index signature, exactly as #8899
established and for the same reason (a declared property is what the
designer-field-key parity gate reads to prove no payload can emit a refused key).
No schema, no spec pin, no new exported symbol.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w