Uh oh!
There was an error while loading. Please reload this page.
refactor(reference): resolve a relationship target from reference alone (objectui#6837 half 2) - #7539
Conversation
objectui#6837 half 2, under the maintainer's 2026-08-31 ruling that protocol normalization belongs on the server and the front end just executes the protocol. objectstack#13847 landed the server half. Sixteen readers lose their `reference_to` arm. Ten are protocol readers that take the target straight off an object-schema field def; six are emitters whose READ side narrows while the emitted key stays byte-identical, because that key is what the emit's own target contract declares. Three readers were deliberately NOT narrowed. `fields/src/index.tsx`'s LookupCellRenderer, `widgets/LookupField.tsx` and `widgets/UserField.tsx` read `FieldMetadata`, ObjectUI's own contract, whose LookupFieldMetadata declares `reference_to` and never declares `reference` — and plugin-grid's relationalMetaCopySet.derivation.test.ts re-derives its read set from exactly those three sources. The choke point keeps stamping both spellings, unchanged, and now says so: a def arriving with only a legacy spelling gets one dev-mode warning naming the field and the ruling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
…vergent pins objectui#6837 half 2. Fixture triage, three outcomes rather than one sweep of spellings: - RETYPED (the fixture used an alias `FieldSchema` refuses): eight object-schema fixtures across app-shell and plugin-detail, plus plugin-gantt's demo producer — the one in-repo emitter measurably on the break surface, fixed AT the producer, which is what the ruling asks for. - REPLACED (the case pinned the arm that was deleted): deriveRelatedLists' "supports reference_to as well as reference", HeaderHighlight's "still honors the reference_to key", ObjectGantt.quickfilter's `reference` mirror, and the `reference_to` live arm in the three referenceArms-6837 pin files. Each is now a refusal beside a live-arm control and a choke-point control. - ADDED: referenceArms-6837.divergent.test.ts in app-shell and plugin-detail. A legacy-ONLY def cannot tell a legacy-first chain from a canonical-first one; a def whose two keys DISAGREE can, and that is the shape the ablation mutates against. ObjectGantt.quickfilter's `reference_to` fixture did not merely fail — its waitFor never settled, so the file hung and took the worker with it. Measured against the same package at the base commit, which is green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
Both errors came from `tsc -p tsconfig.test.json`, the second leg of each package's `type-check` script — the leg that exists because the build config excludes tests, so nothing else compiles them. That it caught these is the answer to whether the new pin files are inside the type-check program: they are, and the compiler named them. - core: the console.warn spy's `mock.calls` entries were implicitly `any`. - app-shell: `ActionParamDef` does not overlap `Record<string, unknown>`, so the cast was refused; the resolved param is read at its own type instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
✅ 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 on PR #7539 returned three FAIL findings, all of them about the PROSE describing the change rather than the change. Each is accepted, and each was re-verified here before editing. 1. The changeset said the choke-point warning made the BYO break "audible instead of silent". It does not. `normalizeSchemaReferenceKeys` has exactly two production call sites, both ingestion choke points, and both STAMP the def — so the warning fires only where the def still resolves. A hand-written schema served through any other DataSource, which IS the break surface, reaches the reader raw and warns nothing. Stated as the limitation it is, in the changeset, the source docblock and the PR body. The pin's PLACEMENT is as dispatched; only the claim about its reach was wrong. 2. The disclosed warn-once granularity was not the shipped one. The memo key also carried the target VALUE (so one field with two stale targets warned twice) and carried nothing identifying the object (so the same field name on N objects collided into one warning that could not say which). Neither was pinned: the existing pins held the value constant and used distinct field names, so both were invisible. Fixed by threading `schema.name` through — the warning now names the whole producer site — and the key is disclosed exactly, with a pin per segment in both directions. 3. The divergent-def docblocks had their justification backwards. A legacy-only refusal is ORDER-BLIND: it goes red under a canonical-first re-widening too, because either order gives a legacy-only def somewhere to resolve from. The divergent def is the order-SPECIFIC half, red only under legacy-first. The pin set was sound; the comment would have sent a future maintainer to delete the wrong half as redundant. Also corrected: the changeset undercounted the emitting sites (six of sixteen, measured eleven) and the zero-emitting DataSource implementations (six, measured eight). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
os-project-manager
commented
Sep 3, 2026
合约审查裁断:FAIL(三条)—— 逐字采信,补丁轮已派
转录档位核验:只提取 harness 生成的 Finding 1 — changeset 说 dev-mode 警告让 BYO 断裂「audible instead of silent」;它在断裂面上恰恰是静默的warn 位于 测量:scratch 测试把 ⇒ 读点断了(depth 0)而警告一次不响;警告只在「已被 stamp、因而仍能解析」(depth 1)的情形下响。对 changeset 所针对的人群,失败与从前一样静默。卡片评论 9(选项 A 的「Against」)记过这条限制,本 PR 没有带过来。 最小修法:纯散文,三处(changeset 段落、PR body 的「Defence in depth」、 Finding 2 — 披露的 warn-once 粒度不是发出的粒度,而维护者正被要求在两个都不描述代码的选项之间裁决PR body、changeset、测试 docblock、dev 报告的 open question 1,四处都写「per (field name, legacy spelling)」。代码是: **还带目标值,且不含任何标识 object 的东西。**测量: ⇒ 同字段同拼法但目标不同 → 两条(按披露的键应为一条);同名字段跨两个 object → 一条,且消息不点名 object,所以 PR 陈述的理由(「三个字段拼错就三条、点名每一个 offender」)在这个常见情形下未兑现。两种行为都没披露,也都没有 pin——那两个「双向」pin 保持 value 不变且用不同字段名,看不见这两件事。 ⛔ 不裁哪种粒度是想要的;裁的是:PR 对它所发出之物的陈述,在它请维护者裁决的那一点上不准确。 最小修法:(a) 把四处披露改成「once per (field name, spelling, target value);object-blind——同一字段在 N 个 object 上只响一次且无法说出是哪个」并为这两个事实各加一条 pin;或 (b) 把 object 纳入 memo 键( Finding 3 — divergent-def pin 的理由说反了两个 测量(Leg D:把两个站点按 canonical-first 重新加宽): 四条红全是 legacy-only 拒绝用例;每一条 最小修法(无代码改动):两个 docblock 与 PR body 改为——legacy-only 拒绝对两种重新加宽顺序都会红(order-blind);divergent def 只在 legacy-first 下红;两者合起来才指出回来的是哪一种顺序。 审查者核实通过的部分(摘要)分类(claim 1)正确,含
NARROWINGS(审查者自列,摘要)四组包套件与 派发席处置补丁轮,三条全修,⛔ 不放行。已恢复原实现者并发去三条 finding 全文,另附审查者记的三条非阻塞散文不准确(「six of the sixteen sites」实为 11; Generated by Claude Code |
✅ 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
|
os-project-manager
commented
Sep 3, 2026
补丁轮复审:PASS —— 逐字采信,放行
转录档位核验:转录 287 行、0 行不可解析、139 个 assistant 轮次全部
VERDICT: PASSScope: the patch-round delta only — commit Delta isolation — confirmed
① The three findings — each verified closed by contentF1 (warning's reach). Closed. F2 (disclosure vs code). Closed. Code: And the two behaviours flagged in round 1 as "true, undisclosed, invisible" are each seen by exactly one of the new pins — proven by mutating the key one segment at a time: ⛔ Not ruled: which granularity is wanted. Ruled: the statement matches the code. F3 (divergent-pin rationale). Closed. Both docblocks now carry the truth table with the correct rows. Re-measured Leg D on ② The code delta — nothing new introduced
③ The size statement — accurate in direction and conclusion⇒ shipped contribution is ≈ +0.7 KB gzipped (932 − 256 = 676 B); the un-minified +3.1 KB is mostly the doc comments rolldown strips. The PR's "roughly 1–2 KB gzipped" over-estimates the shipped cost and under-estimates the un-minified growth, but its two operative claims — additive not subtractive; nowhere near the 9.0 KB headroom — are correct and conservative. Acceptable as written. NARROWINGS
派发席处置放行:剥
⭐ 实现者在补丁轮里逐条重验后才同意,并明说没找到审查者错的地方;F3 它自己重跑了 Leg D 证实自己原来的说法是反的;一次探针返回 exit 99 被读成未测、保住 slot 重跑到真裁定。 warn-once 粒度已按代码实测重新上报维护者(#6837 评论 5530850542)——上一版描述的粒度是代码没有的,那也是本席的转述错误,已单独订正。```` Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Refs #6837 — half 2 only. Patch round 1 applied: contract review returned three FAIL findings, all of them about this description rather than about the change. All three are accepted, each was re-verified here, and the corrections are marked⚠️ CORRECTED below. See "What is still open" at the bottom for why this is
Refsand not a closing keyword.Executes the maintainer's 2026-08-31 ruling (第 6 场总监席决裁批 #14), 原文照录:
Protocol normalization belongs on the server; the front end just executes the protocol. Half 1 landed server-side (objectstack#13847). This is the front-end half: delete the
reference_tofallback arms the ingestion normalizer already made redundant.All measurements below were re-derived on this branch. Where the dispatch's own classification table turned out to be wrong, that is called out rather than smoothed over — the correction is the most load-bearing thing in this PR.
The census, and where the dispatch table was wrong
An anchored grep for the arm shape over non-test, non-dist source found 24 hits, 3 of them prose in comments — 21 code sites. They are not one licence:
Shape 1 — protocol readers (read an object-schema field def). Narrowed. 10 sites.
deriveRelatedLists.ts,RecordDetailView.tsx,RecordMetaFooter.tsx,RelatedList.tsx(the fetch),fieldEnrichment.ts,ObjectGantt.tsx,ObjectChart.tsx,ObjectGallery.tsx,UserFilters.tsx,ObjectTree.tsx.⭐
plugin-tree/src/ObjectTree.tsxwas missing from the dispatch's 12-row table. ItsdetectParentFieldreadsobjectSchema.fields[k].referenceoff the protocol exactly like the other nine — a 13th row, found by re-deriving rather than trusting the table.Shape 2 — emitters. Read side narrowed, emitted key byte-identical. 6 sites on one line, 11 in total.
RecordDetailDrawer.tsx,RelatedList.tsx(the cell meta),buildDefaultPageSchema.ts,ListView.tsx(theobjectDef.fieldsbranch),FilterConditionField.tsx,resolveActionParams.ts.RecordDetailView.tsx:1901,RecordMetaFooter.tsx:100,ObjectGallery.tsx:271,fieldEnrichment.ts:249(allreference_to) andUserFilters.tsx(referenceTo). 11 of the 16 sites emit, not 6. Harmless to the substance — "no emitted key changed" was verified mechanically over the whole diff and still holds — but the number was wrong.Target contract per site, established by reading the declaration:
RecordDetailDrawer.tsxreference_toDetailViewField/DetailViewFieldSchemareference_to, neverreferenceRelatedList.tsx(cell meta)reference_toFieldMetadata(LookupFieldMetadata)reference_to, neverreferencebuildDefaultPageSchema.tsreference_toDetailViewField)reference_to, neverreferenceListView.tsxreferenceToreferenceToFilterConditionField.tsxreferenceToFilterFieldDefreferenceToresolveActionParams.tsreferenceToActionParamDefreferenceToVerified mechanically rather than asserted — every changed line in non-test source keeps its left-hand key:
⛔ Shape 4 — the dispatch's table put these in shape 1. Narrowing them is a regression, and it is mechanically gated.
fields/src/index.tsx(LookupCellRenderer),fields/src/widgets/LookupField.tsx,fields/src/widgets/UserField.tsx— left unchanged.They do not read the protocol. All three read a
fieldprop typedFieldMetadata, whoseLookupFieldMetadataandMasterDetailFieldMetadatadeclarereference_toand never declarereference. Soreference_tois that contract's canonical key, and thereferencearm is the tolerance one — the mirror image of shape 2's rule.Three independent measurements say narrowing them would break things:
reference_toonly.RelatedList.tsxbuildsfieldMetawriting justreference_toand hands it straight toCellRenderer;RecordMetaFooter,ObjectGallery,RecordDetailView,fieldEnrichment,paramToFieldandbulkParamToFielddo the same. Narrowing the readers would strand every one of them.examples/schema-catalog/src/schemas/fields-lookup/basic-lookup.jsonandmulti-select-lookup.jsonare form schemas whose lookup field carriesreference_toand noreference, reachingLookupFieldthrough the form renderer.plugin-grid's__tests__/relationalMetaCopySet.derivation.test.tsre-derives its read set from exactly these three sources and requiresRELATIONAL_META_READ_SETto match in both directions. That table recordsreference_towith verdictadapter-stamped— "normalizeSchemaReferenceKeys stamps it fromreferenceat the getObjectSchema choke point". Deleting the reads turns that gate red as a missing entry.The prior slice had already recorded this tier boundary in
ObjectTree.referenceArms-6837.test.tsx's docblock; that paragraph is now scoped rather than retracted.Also not narrowed, and filed instead
ListView.tsx:2713, theschema.columnsfallback branch, reads a list-view column, not a field def. Measured onListColumnSchema:referenceandreference_toare both refused, and — unlike onFieldSchema— neither carries a rename hint, so neither is an alias there; they are undeclared exactly like a nonsense key. Different question, filed as #7531.The break surface — measured, so the changeset can be true
The declared behaviour change bites only where a def reaches a reader without passing an ingestion choke point (
MetadataProvider's object-type cache,ObjectStackAdapter.getObjectSchema). By construction that is anyDataSourceother thanObjectStackAdapter:getObjectSchemais a required member of the published interface and readers call it on the genericdataSource.Every in-repo implementation on that path was measured for what it emits:
reference_toreference:core/src/adapters/ApiDataSource.tscore/src/adapters/ValueDataSource.tstypes/examples/rest-data-source.tsexamples/byo-backend-console/src/mockDataSource.tsrunner/src/lib/mockDataSource.tsapps/site/app/components/galleryDataSource.tsapps/console/src/sdui-workbench-preview.tsxplugin-grid/demo/bulk-actions.tsxplugin-gantt/demo/main.tsx⭐ The ruling's named example,
examples/byo-backend-console, is not on the break surface at all — it declares no lookup or master_detail field anywhere; its onlyreferencehits are a vite triple-slash directive and a tsconfigreferencesarray. That finding is what stopped the changeset from making a blanket "BYO consumers break" claim.The single real in-repo hit,
plugin-gantt/demo/main.tsx, is fixed at the producer in this PR — which is what the ruling prescribes. Declared here because it is outside the strict arm-deletion scope: same defect class, mechanical, its correct form pinned by the spec, no competing claim on the file, same gate family.That measurement is what the changeset says, path by path, instead of a blanket warning.
Defence in depth — and⚠️ CORRECTED: what it does NOT cover
normalizeFieldReferenceKeysemits a dev-mode warning when a def arrives carrying only a legacy spelling and noreference. It names the object, the field and the key, and points at this ruling. Stamping is unchanged.The warning lives in
normalizeFieldReferenceKeys, reachable only throughnormalizeSchemaReferenceKeys, which has exactly two production call sites — re-derived here:Both are ingestion choke points, and both also stamp the def. ⇒ The warning fires precisely where the def still resolves and nothing is broken. A hand-written schema served through any other
DataSource— which is the break surface this PR defines — reaches the reader raw: it never enters this code and produces no warning at all. On that path the failure is exactly as silent as before.The pin's placement at the choke point is as dispatched and is not in question; only my description of its reach was. Reader-side or shared-resolver diagnostics (options B and C of the card's table §5) remain open. Card comment 9 had already recorded this limitation under "Against"; carrying it forward is the correction.
${named}:${key}:${String(f[key])}— it also carried the target value, and carried nothing identifying the object. So the disclosure was wrong twice over: one field with two stale targets warned twice (not once), and the same field name on N objects collapsed into one warning that could not say which object. Neither behaviour was pinned, because the original pins held the value constant and used distinct field names.Fixed by threading
schema.namethrough (option (b) of the two the review offered) rather than by documenting the gap.normalizeSchemaReferenceKeysalready holds the object name, so the warning now names the whole producer site instead of half of it. The key is (object name, field name, spelling, target value), and every segment is now pinned in both directions inreference-keys.legacyWarning-6837.test.ts— including the two behaviours that were previously invisible.Ablation — prediction first, then measurement
Recorded before running: restoring the legacy-first arm at all 16 sites should turn 8 of 8 pin files red, in 15 named cases, with every live-arm control, every choke-point control and every "still renders / still derives" control staying green.
Measured: 8 files failed of 8 run; 15 failed, 65 passed — the same 15 cases, no others. Restored tree, same 8 files: 80 passed of 80.
The script proved the mutation reached disk before reading any result (per-file
git hash-objectagainst theHEADblob, plus an injected-text census: 17 occurrences against a floor of 16), carriedtrap ... EXIT INT TERMwith absolute paths, and proved the restore by state —git diff HEADempty across all 15 files and every blob hash equal to itsHEADblob — not by an exit code. No rebuild leg: the rootvitest.config.mtsaliases every@object-ui/*specifier to that package'ssrc, and CI's test job runsvitest runwith no build step, so there is nodisthop to get wrong here.reference ?? reference_to, canonical-first):⇒ the legacy-only refusals are ORDER-BLIND — red under either re-widening order, because either one gives a legacy-only def somewhere to resolve from. The divergent def is order-SPECIFIC — red only under legacy-first, green under canonical-first, which still consults
referencefirst.The pin set was sound either way; the comment was the hazard. Read as written, a future maintainer would have deleted the legacy-only refusals as the redundant half and lost the only cases that fire under both orders. Both docblocks and this paragraph now state it as the complementary pair it is: the refusals say an arm came back, the divergent cases say which order.
Fixture triage — three outcomes, not one sweep
FieldSchemarefuses): 8 object-schema fixtures across app-shell and plugin-detail.deriveRelatedLists' "supports reference_to as well as reference",HeaderHighlight's "still honors thereference_tokey",ObjectGantt.quickfilter'sreferencemirror, and thereference_tolive arm in all threereferenceArms-6837files. Each is now a refusal beside a live-arm control and a choke-point control.schema-catalogform fixtures — they are shape-4 producers and correct as they stand.ObjectGantt.quickfilter.test.tsxdid not merely fail: itswaitFornever settled, so the file hung and took the vitest worker with it — a signature that reads like an environment flake. It was diagnosed against the same package at the base commit in a second worktree, which is green (62 files / 485 tests), so the crash was attributed to this change rather than to the box.Verification
Every number below is from
4cfed6d12, the branch head.packages/app-shell/packages/plugin-grid/+packages/fields/packages/plugin-list/+packages/plugin-charts/packages/plugin-detail/packages/plugin-gantt/packages/core/src/utils/packages/plugin-tree/turbo run type-checkandturbo run lintacross all 8 touched packages: all tasks successful, 0 errors.type-checkis the hyphenated spelling here, and its second leg (tsc -p tsconfig.test.json) is what compiles tests — it caught two real type errors in the new pin files, which is also the proof that those files are inside the program rather than silently excluded.Gates re-run on the final head after the last commit:
check:control-bytes,check:phantom-deps,check:self-import,check:vi-mock-specifiers,check:vi-mock-inherit,check:entry-guard,check:readme-exports,check:eager-closure,check:governed-queue-guard(NOT GOVERNED, 38 paths), changeset presence and no-major — all exit 0.check:eager-closure: 3182.4 KB gzipped against a 3191.4 KB budget — 9.0 KB headroom. Passing, and the headroom is thin enough to be worth a maintainer's attention.@object-ui/coresits in the console's eagerframeworkchunk. Review measureddist/utils/reference-keys.jsat 1188 → 3302 B gzipped and core's wholedistat 252,523 → 254,354 B (+1.8 KB); independently corroborated here from the source side — that module goes 2541 → 9959 bytes, +152/−3 lines, including a 939-character message template that survives minification as a string literal. So this change is net additive by roughly 1–2 KB gzipped, not subtractive. It does not threaten the budget (the 9.0 KB margin is structural and predates this PR), and review explicitly did not raise it as a finding — but the claim was false and is withdrawn.eslintwas run over the whole tree (4251 files) rather than narrowed. Two of the changed files carry errors; a control run of the same two files at the base commit in a second worktree returns the identical rule set and count, with line numbers shifted only by the comment lines this diff adds. Pre-existing, not introduced here.What is still open
Refsrather than a closing keyword, deliberately: two strands named on the card remain.MetadataProvider.tsx,plugin-form/sectionFields.ts,plugin-form/schemaDefaults.ts— still want their producers re-measured post-normalization.LookupField/LookupCellRenderer/UserField— the shape-4 trio above. The mislabel came in with the dispatch text and I carried it without checking it against the table; the substance (six readers, all untouched, all listed) is unaffected. Anchor note:schemaDefaults' arm is at line 267, not the 245 the dispatch named.RecordDetailDrawer.tsx(reference_field/referenceField) stays untouched under the spec seat's option-A ruling — two different concepts, not a synonym pair.Also out of scope by instruction and untouched: narrowing
DetailViewFieldSchema, and retiring the normalizer.Filed while working, unassigned: #7531 (list-column reads two keys
ListColumnSchemadeclares neither of) and #7532 (RuntimeField.reference_tois now declared but read by nothing).Card body corrected as instructed: #6837 claimed
target"is not in the spec's alias table either". Measured false —target: "reference"sits inFieldSchema's ownaliasesmap, andsafeParserefusestargetwith a "did you mean" rename that a nonsense key does not get. The operative conclusion survives and is stronger: the spec namestargetin order to refuse it.🤖 Generated with Claude Code
https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
Generated by Claude Code
Generated by Claude Code