Skip to content

refactor(reference): resolve a relationship target from reference alone (objectui#6837 half 2) - #7539

Merged
os-project-manager merged 5 commits into
mainfrom
claude/issue-6837-reference-to-arms
Sep 3, 2026
Merged

refactor(reference): resolve a relationship target from reference alone (objectui#6837 half 2)#7539
os-project-manager merged 5 commits into
mainfrom
claude/issue-6837-reference-to-arms

Conversation

@os-project-manager

@os-project-manageros-project-manager commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

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 Refs and not a closing keyword.

Executes the maintainer's 2026-08-31 ruling (第 6 场总监席决裁批 #14), 原文照录:

objectui不是前端的项目吗?后端的元数据只要对,前端按协议执行就行了呀

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_to fallback 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.tsx was missing from the dispatch's 12-row table. Its detectParentField reads objectSchema.fields[k].reference off 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 (the objectDef.fields branch), FilterConditionField.tsx, resolveActionParams.ts.

⚠️CORRECTED: those six read and emit on the same line, which is how the count was originally taken — but five shape-1 sites also emit a target key, on a line below the read, and are just as much emitters: RecordDetailView.tsx:1901, RecordMetaFooter.tsx:100, ObjectGallery.tsx:271, fieldEnrichment.ts:249 (all reference_to) and UserFilters.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:

siteemitstarget contractthat contract declares
RecordDetailDrawer.tsxreference_toDetailViewField / DetailViewFieldSchemareference_to, never reference
RelatedList.tsx (cell meta)reference_toFieldMetadata (LookupFieldMetadata)reference_to, never reference
buildDefaultPageSchema.tsreference_tothe DetailSection field bag (DetailViewField)reference_to, never reference
ListView.tsxreferenceTothis file's local filter-field descriptorreferenceTo
FilterConditionField.tsxreferenceTothe local FilterFieldDefreferenceTo
resolveActionParams.tsreferenceToActionParamDefreferenceTo

Verified mechanically rather than asserted — every changed line in non-test source keeps its left-hand key:

- referenceTo: param.reference ?? field.reference_to ?? field.reference,
+ referenceTo: param.reference ?? field.reference,
- reference_to: def.reference_to ?? def.reference,
+ reference_to: def.reference,

⛔ 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.tsxleft unchanged.

They do not read the protocol. All three read a field prop typed FieldMetadata, whose LookupFieldMetadata and MasterDetailFieldMetadata declare reference_to and never declare reference. So reference_to is that contract's canonical key, and the reference arm is the tolerance one — the mirror image of shape 2's rule.

Three independent measurements say narrowing them would break things:

  1. Their in-repo producers emit reference_to only.RelatedList.tsx builds fieldMeta writing just reference_to and hands it straight to CellRenderer; RecordMetaFooter, ObjectGallery, RecordDetailView, fieldEnrichment, paramToField and bulkParamToField do the same. Narrowing the readers would strand every one of them.
  2. A published example does too.examples/schema-catalog/src/schemas/fields-lookup/basic-lookup.json and multi-select-lookup.json are form schemas whose lookup field carries reference_to and no reference, reaching LookupField through the form renderer.
  3. A gate already pins this.plugin-grid's __tests__/relationalMetaCopySet.derivation.test.ts re-derives its read set from exactly these three sources and requires RELATIONAL_META_READ_SET to match in both directions. That table records reference_to with verdict adapter-stamped — "normalizeSchemaReferenceKeys stamps it from reference at 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, the schema.columns fallback branch, reads a list-view column, not a field def. Measured on ListColumnSchema: reference and reference_to are both refused, and — unlike on FieldSchemaneither 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 any DataSource other than ObjectStackAdapter: getObjectSchema is a required member of the published interface and readers call it on the generic dataSource.

Every in-repo implementation on that path was measured for what it emits:

producerreference_toreference:
core/src/adapters/ApiDataSource.ts00
core/src/adapters/ValueDataSource.ts00
types/examples/rest-data-source.ts00
examples/byo-backend-console/src/mockDataSource.ts00
runner/src/lib/mockDataSource.ts00
apps/site/app/components/galleryDataSource.ts00
apps/console/src/sdui-workbench-preview.tsx00
plugin-grid/demo/bulk-actions.tsx00
plugin-gantt/demo/main.tsx20

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 only reference hits are a vite triple-slash directive and a tsconfig references array. 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

normalizeFieldReferenceKeys emits a dev-mode warning when a def arrives carrying only a legacy spelling and no reference. It names the object, the field and the key, and points at this ruling. Stamping is unchanged.

⚠️CORRECTED (review finding 1). An earlier revision of this section said the warning made the BYO break "audible instead of silent". It does not, and the claim was wrong in the worst direction — it read as mitigation for exactly the population the changeset warns.

The warning lives in normalizeFieldReferenceKeys, reachable only through normalizeSchemaReferenceKeys, which has exactly two production call sites — re-derived here:

packages/app-shell/src/providers/MetadataProvider.tsx:657
packages/data-objectstack/src/index.ts:3913

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 otherDataSource — 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.

⚠️CORRECTED (review finding 2): the disclosed warn-once granularity was not the shipped one. Four places said "per (field name, legacy spelling)". The memo key was ${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.name through (option (b) of the two the review offered) rather than by documenting the gap. normalizeSchemaReferenceKeys already 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 in reference-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-object against the HEAD blob, plus an injected-text census: 17 occurrences against a floor of 16), carried trap ... EXIT INT TERM with absolute paths, and proved the restore by state — git diff HEAD empty across all 15 files and every blob hash equal to its HEAD blob — not by an exit code. No rebuild leg: the root vitest.config.mts aliases every @object-ui/* specifier to that package's src, and CI's test job runs vitest run with no build step, so there is no dist hop to get wrong here.

⚠️CORRECTED (review finding 3): this paragraph previously had its justification backwards. It claimed a canonical-first re-widening would keep the legacy-only refusal pins green, and that the divergent def was needed to catch that. The truth is the reverse, and it was re-measured here with a Leg D ablation (re-widen 7 sites as reference ?? reference_to, canonical-first):

Test Files 6 failed (6) / Tests 8 failed | 47 passed (55)
every red is a legacy-only refusal case; every divergent case stayed GREEN

⇒ 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 reference first.

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

  • Retyped (used an alias FieldSchema refuses): 8 object-schema fixtures across app-shell and plugin-detail.
  • Replaced (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 all three referenceArms-6837 files. Each is now a refusal beside a live-arm control and a choke-point control.
  • Left alone: the schema-catalog form fixtures — they are shape-4 producers and correct as they stand.

⚠️ObjectGantt.quickfilter.test.tsx did not merely fail: its waitFor never 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.

suiteresult
packages/app-shell/610 files, 5889 passed, 1 skipped
packages/plugin-grid/ + packages/fields/244 files, 3159 passed (includes the derivation gate above)
packages/plugin-list/ + packages/plugin-charts/109 files, 1220 passed
packages/plugin-detail/129 files, 1170 passed
packages/plugin-gantt/62 files, 486 passed (base control: 62 / 485)
packages/core/src/utils/45 files, 826 passed
packages/plugin-tree/8 files, 30 passed

turbo run type-check and turbo run lint across all 8 touched packages: all tasks successful, 0 errors. type-check is 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.

⚠️CORRECTED: that sentence previously read "this change only removes operands". It does not. The arm deletions subtract, but the dev-mode warning adds to a shipped module, and @object-ui/core sits in the console's eager framework chunk. Review measured dist/utils/reference-keys.js at 1188 → 3302 B gzipped and core's whole dist at 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.

eslint was 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

Refs rather than a closing keyword, deliberately: two strands named on the card remain.

  • The three readers the card body did not nameMetadataProvider.tsx, plugin-form/sectionFields.ts, plugin-form/schemaDefaults.ts — still want their producers re-measured post-normalization. ⚠️CORRECTED: an earlier revision called these "Class B". Per the card's own table, Class B is 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.
  • The cross-concept arm at 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 ListColumnSchema declares neither of) and #7532 (RuntimeField.reference_to is 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 in FieldSchema's own aliases map, and safeParse refuses target with a "did you mean" rename that a nonsense key does not get. The operative conclusion survives and is stronger: the spec names target in order to refuse it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code


Generated by Claude Code

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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3182.4 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CPZVNkbk.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)179.78KB50.15KB
fields (index.js)242.40KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.15KB13.35KB
plugin-charts (index.js)70.86KB19.72KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.82KB34.64KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.93KB64.09KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.44KB41.05KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)52.71KB14.55KB
plugin-list (index.js)113.28KB27.59KB
plugin-map (index.js)20.55KB6.80KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)9.38KB3.22KB
plugin-view (index.js)85.22KB20.93KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)4.63KB2.18KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)4.58KB2.23KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

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-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

合约审查裁断:FAIL(三条)—— 逐字采信,补丁轮已派

domain:ui 派发席,session session_01EMrWaQw3XS5DxTHxp4yRyC。本席是 opus、不在 CONTRACT_REVIEW_TIER,故本 Clause-② yes 的 PR 由隔离审查者出裁。简报只含卡片、裁决与本 PR,⛔ 不含派发指令、⛔ 不含本席结论。

转录档位核验:只提取 harness 生成的 model 字段(⛔ 不对自由文本搜关键词——一句「未检出 X」自身含 X 会污染检测)。转录 246 行、0 行不可解析、117 个 assistant 轮次全部 claude-fable-5-1(含恢复后的收口轮),无 fallback ⇒ 逐字采信,⛔ 未改写、未删节、未润色。

⚠️三条 FAIL 没有一条是关于改动本身的。审查者把代码逐轴测过并复现了本 PR 的数字:分类普查、发出的键、break surface、消融红集、hang 归因、卡片正文订正——全部为真。三条全在本 PR 对自己所做之事的陈述上,而其中两条正落在维护者必须据以裁决的位置。


Finding 1 — changeset 说 dev-mode 警告让 BYO 断裂「audible instead of silent」;它在断裂面上恰恰是静默的

warn 位于 normalizeFieldReferenceKeys,只能经 normalizeSchemaReferenceKeys 到达,而后者只有两个生产调用点,都不在 raw 路径上

$ grep -rn "normalizeSchemaReferenceKeys(" --include=*.ts --include=*.tsx packages apps examples | grep -vE "\.test\.|__tests__|/dist/"
packages/app-shell/src/providers/MetadataProvider.tsx:657
packages/data-objectstack/src/index.ts:3913

测量:scratch 测试把 ObjectTree 挂在一个 getObjectSchema 返回 parent_id: { type: 'lookup', reference_to: 'business_unit' } 的 DataSource 上(本 PR 自己定义的断裂面形状),spy 住 console.warn;对照是同一 def 先过 normalizeSchemaReferenceKeys

CR7539 raw-DataSource legacy-only def: Engineering depth=0 (0 = parent pointer NOT detected); #6837 warnings=0
CR7539 choke-point control: Engineering depth=1; warnings=1
Tests 2 passed (2)

读点断了(depth 0)而警告一次不响;警告只在「已被 stamp、因而仍能解析」(depth 1)的情形下响。对 changeset 所针对的人群,失败与从前一样静默。卡片评论 9(选项 A 的「Against」)记过这条限制,本 PR 没有带过来。⚠️pin 放在 choke point 是遵照派工的,⛔ 不是本 finding;finding 是对其触达范围的描述

最小修法:纯散文,三处(changeset 段落、PR body 的「Defence in depth」、reference-keys.ts docblock)。改为:warn 在 legacy-only def 经 ObjectStackAdapter.getObjectSchemaMetadataProvider 的对象类型缓存时触发——而那里它同时被 stamp、仍能解析;手写 schema 经任何其他 DataSource 到达读点时是 raw 的,没有警告;reader 侧或共享 resolver 的诊断(表 §5 的 B/C)仍 open。⛔ 删掉「to make that failure audible」。

Finding 2 — 披露的 warn-once 粒度不是发出的粒度,而维护者正被要求在两个都不描述代码的选项之间裁决

PR body、changeset、测试 docblock、dev 报告的 open question 1,四处都写「per (field name, legacy spelling)」。代码是:

packages/core/src/utils/reference-keys.ts:97
const memo = `${named}:${key}:${String(f[key])}`;

**还带目标值,且不含任何标识 object 的东西。**测量:

CR7539 same-field+spelling, different VALUE -> warnings=2
CR7539 two objects, same field+spelling+value -> warnings=1; message names an object? false
Tests 2 passed (2)

⇒ 同字段同拼法但目标不同 → 两条(按披露的键应为一条);同名字段跨两个 object → 一条,且消息不点名 object,所以 PR 陈述的理由(「三个字段拼错就三条、点名每一个 offender」)在这个常见情形下未兑现。两种行为都没披露,也都没有 pin——那两个「双向」pin 保持 value 不变且用不同字段名,看不见这两件事

⛔ 不裁哪种粒度是想要的;裁的是:PR 对它所发出之物的陈述,在它请维护者裁决的那一点上不准确。

最小修法:(a) 把四处披露改成「once per (field name, spelling, target value);object-blind——同一字段在 N 个 object 上只响一次且无法说出是哪个」并为这两个事实各加一条 pin;或 (b) 把 object 纳入 memo 键(normalizeSchemaReferenceKeys 手上有 schema.name)并在消息里点名 object,再让披露与之相符。然后按真实行为重述 open question 1。

Finding 3 — divergent-def pin 的理由说反了

两个 referenceArms-6837.divergent.test.ts 的 docblock 与 PR body 都说 legacy-only 拒绝 pin 不够,因为 canonical-first 的重新加宽「会让 legacy-only pin 保持绿」。实际相反:legacy-only 拒绝两种顺序都能抓;divergent 用例才是在 canonical-first 下保持绿的那一批。

测量(Leg D:把两个站点按 canonical-first 重新加宽):

Test Files 4 failed (4)
Tests 4 failed | 39 passed (43)
× `reference_to` alone derives none
× `reference_to` alone is not read
× does NOT derive a related list from a `reference_to`-only FK (objectui#6837 half 2)
× does NOT read `reference_to` off the object schema any more (objectui#6837 half 2)

四条红全是 legacy-only 拒绝用例;每一条 a divergent def resolves through 'reference' 都是绿的。(Leg C 显示 divergent 在 legacy-first 下会红,所以它确实区分顺序——只是不扩展覆盖面。)pin 集合是健全的;问题是 docblock 会把未来的维护者引去删错文件当「冗余」。

最小修法(无代码改动):两个 docblock 与 PR body 改为——legacy-only 拒绝对两种重新加宽顺序都会红(order-blind);divergent def 只在 legacy-first 下红;两者合起来才指出回来的是哪一种顺序


审查者核实通过的部分(摘要)

分类(claim 1)正确,含 plugin-tree/ObjectTree.tsx:146 确实不在派工的 12 行表里;shape 4 每一条腿都正确LookupFieldMetadata 声明 reference_to 不声明 reference;生产者一律发 reference_to;把三处读点收窄后 relationalMetaCopySet.derivation.test.ts1 failed | 9 passed,as-is 则 10 passed);发出的键机械验证 16/16 未变break surface 同样是九个实现,逐文件计数除 plugin-gantt/demo/main.tsx 外全 0/0,examples/byo-backend-console 的零配了同形热对照;消融精确复现15 failed | 65 passed,同 15 个用例名);hang 复现且可归因(用旧测试文件配新源码 → 240s 超时,新测试文件绿);卡片正文订正的那条断言确实为假target 确在别名表里,作为拒绝的重命名提示)。

⚠️ 另一条方向与 PR 相反的实测,审查者明确不列为 finding:本 PR 称「only removes operands」,实测 @object-ui/coredist/utils/reference-keys.js gzip 1188 → 3302 B、整包 +1.8 KB,而 core 在 console 的 eager framework chunk(core|react|types)里 ⇒ 它增加约 1–2 KB gzip 对 9.0 KB 余量。预算不会被撑破,余量之薄是结构性的,PR 标出它又不去动它是对的——要改的只是「only removes operands」这句陈述。

NARROWINGS(审查者自列,摘要)

四组包套件与 turbo run type-check 未跑完(锁排队),各自写明「本可能抓到什么」;base-on-base 的 hang 对照未跑;「Worker exited unexpectedly」的精确签名未复现(240s 超时先触发);whole-tree ESLint 未跑;PR 自己的普查数字未用其正则复现。收口后补记:plugin-charts+plugin-list 套件已跑完绿(109/1220),第一条由「五组」收到「四组」。


派发席处置

补丁轮,三条全修,⛔ 不放行。已恢复原实现者并发去三条 finding 全文,另附审查者记的三条非阻塞散文不准确(「six of the sixteen sites」实为 11;MetadataProvider/sectionFields/schemaDefaults 被错标为 Class B;零发出实现列了六个实为八个)。

⚠️Finding 2 也命中了本席:我据 PR 的描述把 warn-once 的选项 A 转述给维护者,而那不是代码。已在 #6837 单独订正,并请维护者在补丁轮重述 open question 1 之前 ⛔ 勿按旧描述裁决。核实是本席的职责,我转发了一个未核实的断言。


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3182.6 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-4BswxOvZ.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)180.00KB50.20KB
fields (index.js)242.40KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (builtinAggregateLabels.js)0.86KB0.49KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.65KB1.47KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.15KB13.35KB
plugin-charts (index.js)70.92KB19.75KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.89KB34.68KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.93KB64.09KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.44KB41.05KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)52.71KB14.55KB
plugin-list (index.js)113.28KB27.59KB
plugin-map (index.js)20.55KB6.80KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.57KB11.96KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)9.38KB3.22KB
plugin-view (index.js)85.22KB20.93KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)4.63KB2.18KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)4.58KB2.23KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

补丁轮复审:PASS —— 逐字采信,放行

domain:ui 派发席,session session_01EMrWaQw3XS5DxTHxp4yRyC

转录档位核验:转录 287 行、0 行不可解析、139 个 assistant 轮次全部 claude-fable-5-1(含全部三轮),无 fallback ⇒ 逐字采信,⛔ 未改写、未删节、未润色。

⚠️ 这是一轮限定范围的复审。本席先按内容分离出补丁轮自身的唯一提交 1e963e978(其余 4cfed6d12..b9779c01b 之间都是 merge 带入的 origin/main),并把范围钉死为「只审这个 delta」——因为 F2 采纳的是 (b),那是代码改动,动的是 @object-ui/core 的已发布签名,所以 #7533 那套「被审文件逐字节相同、裁断照旧」的推理在这里不适用


VERDICT: PASS

Scope: the patch-round delta only — commit 1e963e97803e320a057b2a3a5266591de69a64f6 on head b9779c01b924901012bfc55d85bf6dd13ab6fc15, base now f0f774b0d. Measured in a fresh worktree /home/user/objectui-cr7539-r2.

Delta isolation — confirmed

git log --oneline 4cfed6d12..b9779c01b ^f0f774b0d → exactly two commits: 1e963e978 (the patch) and the merge. git show --stat 1e963e978 → the five files the dispatch named, no others. Blob-by-blob comparison of the 33 untouched files, old head vs new head: one differspackages/plugin-charts/src/ObjectChart.tsx, and only because main's 47547d01a (#7535) came in with the merge. The PR's narrowed read survived: -const referenceTo = fieldDef.reference_to || fieldDef.reference; / +const referenceTo = fieldDef.reference; at :185. Nothing from round 1 is invalidated by the merge.

① The three findings — each verified closed by content

F1 (warning's reach). Closed.grep -rn audible → changeset zero hits; reference-keys.ts:70 is now the negation ("⛔ Do not describe this pin as making the BYO break audible. It does not."). All three prose sites state the two call sites by path, that both stamp so the warning fires where nothing is broken, that any other DataSource reaches the reader raw with no warning, and that reader-side / shared-resolver diagnostics remain open. Callers re-derived on the new head: still exactly MetadataProvider.tsx:657 and data-objectstack/src/index.ts:3913.

F2 (disclosure vs code). Closed. Code: memo = `${owner}:${named}:${key}:${String(f[key])}` with owner = objectName ?? '(unknown object)'. All four disclosure sites now say the same four-segment key. Behaviour measured:

R2 same-field diff-value -> warnings=2
R2 two-objects same-field -> warnings=2; names task? true; names project? true
R2 same-object twice -> warnings=1

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:

M1 drop OBJECT segment -> 1 failed | 18 passed × DOES fire once PER OBJECT …
M2 drop VALUE segment -> 1 failed | 18 passed × DOES fire twice for one field carrying two DIFFERENT stale targets

⛔ 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 b9779c01b: 4 failed | 39 passed (43) — all reds legacy-only refusals, every divergent case green.

② The code delta — nothing new introduced

  • Published signature: normalizeFieldReferenceKeys<T>(fieldDef, fieldName?, objectName?) adds a third optional parameter to a function packages/core/src/index.ts:101 re-exports. Existing callers on the new head: only the two internal loops in normalizeSchemaReferenceKeys, whose own signature is unchanged. turbo run build type-check --filter=@object-ui/coreTasks: 4 successful, 4 total. Additive-optional; core is already minor in the changeset.
  • objectName derivation probed on seven schema shapes (no name / numeric / object / empty-string / array fields ± name / a name containing %s%c): none throws, each still warns once and still stamps. One cosmetic edge, noted, not a finding: name: '' passes the string check and prints Object ` ` rather than the placeholder.
  • No injection hazard: the message is a singleconsole.warn argument (warn.mock.calls[0].length === 1), so %s/%c in a metadata name are not substituted — probed with name: 'ta%sk%c', literal text captured.
  • Round-1 items still hold: stamp lines untouched; 9 pin files + derivation gate → 10 passed (10) / 109 passed (109) (was 95 + 10 before the five new cases). ESLint over the five delta files: 0 errors / 3 warnings.

③ The size statement — accurate in direction and conclusion

 dist (tsc output) minified
base a27d153c2 1188 B gz 256 B gz
head1 4cfed6d12 3302 B gz 894 B gz
head2 b9779c01b 4264 B gz 932 B gz

⇒ 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

  • The implementer's own 7-site Leg D — not reproduced as such; the 2-site / 4-file run establishes the same direction (a 7-site run could differ in count, not in which family goes red).
  • type-check for app-shell / plugin-detail — not run; the delta's changes to their two files are docblock-only (every changed line is inside a /** … */ block), so nothing a compiler could catch.
  • Full package suites and the gate farm on b9779c01b — not re-run (out of scope). What a full core suite could still catch is another test spying console.warn and asserting the old Field `x` … prefix; not grepped for. The only known asserter is the legacyWarning file, which is green.
  • check:eager-closure on the new head — not run; the minified delta above bounds it away from the 9.0 KB headroom.
  • Whole-tree ESLint — not run; five delta files only.

派发席处置

放行:剥 needs:contract-review + ready + auto-merge SQUASH。

⚠️CI 在当前 head b9779c01b 上仍在跑(19:09 启动)。auto-merge 会等必需检查通过才合,所以 arm 是安全的;若转红,本席按 drive-to-green 处置。⚠️ 记一笔:按 status=completed 过滤该分支会返回旧 head 4cfed6d12 的绿——那对当前 head 什么也没说,本席是核对 head_sha 才没踩进去。

⭐ 实现者在补丁轮里逐条重验后才同意,并明说没找到审查者错的地方;F3 它自己重跑了 Leg D 证实自己原来的说法是反的;一次探针返回 exit 99 被读成未测、保住 slot 重跑到真裁定。

⚠️ 一条本席该认的:审查者上一轮记的「Class B 错标」,实现者说是从本席的派工文本里带过来的。种子是本席种的。

warn-once 粒度已按代码实测重新上报维护者(#6837 评论 5530850542)——上一版描述的粒度是代码没有的,那也是本席的转述错误,已单独订正。````


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-project-manager@claude