Skip to content

feat(core): canonicalize the retired object-schema dialect at the ingestion choke point (objectui#7650) - #8873

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-7650-serve-path-canonicalise-retired-keys
Sep 10, 2026
Merged

feat(core): canonicalize the retired object-schema dialect at the ingestion choke point (objectui#7650)#8873
os-zhuang merged 1 commit into
mainfrom
claude/issue-7650-serve-path-canonicalise-retired-keys

Conversation

@claude

@claude claude Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Part of objectui#7650 — the second of the two halves the ruling on that card
(comment 5605081157) ordered, and the one the card is actually about. The first half is
objectui#8868. Neither completes the card alone, so nothing here uses a closing keyword.

⚠️ This pull request is expected to PARK. Clause-② measures yes (see below), so it
carries needs:contract-review on both carriers and waits for the contract-review tier to
become reachable. That is the designed cost of an honest declaration, not a stall to route
around.

The problem, restated from the measurement

The object-schema serve path never parses. ObjectStackAdapter.getObjectSchema fetches
the document, applies exactly two mutations and returns it — no ObjectSchema.parse
anywhere on the path (lit control: the same file does import and use
DroppedFieldsEventSchema and isFilterAST, so the zero is a real absence).

FieldSchema strictness gates the metadata write door only. A document stored before
a key was tightened is served back verbatim, forever: it cannot be re-saved through the
strict door, but nothing ever asks it to be. And a host whose own getObjectSchema returns
a non-spec dialect is served straight through.

objectui#7155, #7166 and #7435 narrowed the consumer reads for display_field,
description_field and lookup_filters to the camelCase spelling on the strength of "no
spec-compliant producer can emit this key". That is a sound claim about authoring and a
different claim from "this consumer read can never fire". This restores the missing half,
the way objectui#6837 half 2 restored it for reference_to — canonicalise once, at the
ingestion choke point, never at a consumer.

The shape, and the two shapes that were refused

Ruled A on objectui#7650: the serve path canonicalises.

  • B — drop the legacy spelling, stamp nothing was refused. A stored legacy document
    would lose the value instead of arriving canonical, and silent data loss on a serve
    path is the worst of the three shapes.
  • C — call the spec's published lintAuthoredRecordKeys and fold on its suggestion
    was refused on measurement, not preference: that function falls through to a Levenshtein
    matcher when no to row exists, so it answers "did you mean sortable?" for the pure
    typo sortible. A serve path that silently corrects a typo is worse than the defect
    it fixes.

How the fold is derived

By the spec's own alias-probe rule — lowercase, strip _, - and space — matched
exactly against FieldSchema's declared key set, read at runtime off
FieldSchema.shape. Measured on the installed @objectstack/spec@17.3.0: 74 declared
keys, zero probe collisions.

served key probe declared match outcome
display_field displayfield displayField folded
description_field descriptionfield descriptionField folded
lookup_filters lookupfilters lookupFilters folded
lookup_columns lookupcolumns lookupColumns folded
id_field idfield left as served
title_format titleformat left as served
sortible (typo) sortible left as served
reference_to referenceto left as served, handled by the older arm

Derived rather than tabulated because a table has to be edited every time the spec grows a
camel key whose snake twin is still in stored documents, and the edit that does not
happen is the bug
. The property that decided it: id_field needs no special case. It
probes onto nothing declared and lands in the leave arm on its own — one rule giving both
the general behaviour and its exception.

⚠️ lookup_columns is in that table and is not in the card's list of four. It is not a
key-by-key widening: it is a declared FieldSchema key whose snake twin belongs to the same
retired dialect, and the single derived rule covers it. It is pinned, so the generality is a
measured fact rather than an unnoticed side effect. Flagging it explicitly because a reader
comparing this diff against the card's four keys should not have to discover it.

What it deliberately does not do

  • Removes nothing. The legacy key stays on the document exactly as served. "Leave
    arm", not "drop arm" in the destructive sense.
  • Overwrites nothing. A canonical key the producer already set keeps the producer's
    value.
  • Folds nothing onto an ambiguous probe. If two declared keys ever collide under the
    probe rule, that probe folds nothing rather than picking one. Unreachable today by
    construction; the zero-collision precondition is itself pinned, so the pin fires before
    the guard has to.
  • Adds no export. The file's three exported symbols are byte-identical to main. A
    test-only reset hook was written and then removed for exactly this reason.

The id_field slice is blocked — and not on a card

The dispatch that produced this work said FIELD_KEY_GUIDANCE.id_field.why was reachable,
having verified it on objectstack's origin/main. It is not reachable: measured
grep -c id_field on the published @objectstack/spec 17.3.0 (installed here) and
17.4.0 (newest on npm) — 0 in both, with startingNumber (2 hits) as the lit control
in the same read. The row landed after 17.4.0 was cut.

⇒ The blocker is a RELEASE of @objectstack/spec carrying that row, not objectstack#16632
(that card is not open, and it is not wrong). Hard-copying the sentence into this repo was
refused — a second copy of contract prose is the drift AGENTS.md #0.1 exists to stop — and so
was reading it optionally with a local fallback, which would silently degrade on an older
spec, this card's own defect class. Until the cut lands, id_field is left alone, and the
leave arm gets it right with no code owed.

title_format is out of scope by ruling: it needs a separate eight-key maintainer decision,
and widening this slice to reach it would re-block the slice.

Clause-② declaration

Clause-②: yes — measured on this diff.

The mechanical floor is "a new exported symbol or a new key on a published payload is always
yes". No new exported symbol. But new keys on a published payload is precisely what this PR
does, by design: displayField, descriptionField, lookupFilters and lookupColumns are
stamped onto documents the published @object-ui/data-objectstack adapter and the published
useMetadataItem hook serve. No shape that honours ruling A avoids this — every
canonicalisation adds the canonical key. needs:contract-review is hung on this PR and on
the card.

Tests

New: packages/core/src/utils/__tests__/reference-keys.retiredDialect-7650.test.ts, 25
cases in four groups — the folds, the negative pins the ruling required by name
(sortible and id_field not folded, plus title_format, no-overwrite, idempotence), the
dev-mode warning, and a contract-derivation group that asserts the rule against
FieldSchema itself rather than against a copy of its key list: every folded pair must be
one the spec REFUSES in the legacy spelling and ACCEPTS in the canonical one, with lit
controls in the same read (a bare def parses; zzz_not_a_real_key does not).

Runs, all in the dedicated worktree at commit b3f26de35:

  • packages/core/ + packages/data-objectstack/: Test Files 195 passed (195),
    Tests 3704 passed (3704)
  • packages/plugin-grid/ (the RELATIONAL_META_READ_SET ledger and its derivation test) +
    packages/app-shell/src/providers/ + the seven cross-package files that import
    normalizeSchemaReferenceKeys directly (plugin-detail, plugin-gantt, plugin-tree,
    app-shell deriveRelatedLists, ObjectView.objectBoundActions-7234):
    Test Files 145 passed (145), Tests 1274 passed (1274)
  • turbo run type-check --filter=@object-ui/core --filter=@object-ui/data-objectstack --filter=@object-ui/plugin-grid:
    Tasks: 16 successful, 16 total; each package's type-check runs
    tsc --noEmit && tsc -p tsconfig.test.json, so the new test file is type-checked too
  • gates green on their own printed verdict line: check:designer-field-key-parity,
    check:handler-key-reads, check:spec-symbols, check:control-bytes,
    check:unreferenced-sources, check:vi-mock-specifiers, check:vi-mock-inherit,
    check:phantom-deps, check:unused-deps, check:changeset-presence

NOT MEASURED, with the reason: check:spec-floors, check:readme-exports and
check:eager-closure each refused to run for want of a built workspace ("Build the
workspace before running this gate", "its type entry ./dist/index.d.ts is not on disk",
"No eager-closure report at apps/console/dist/eager-closure.json"). Those are broken
gauges, not red gates, and a full workspace build plus a console build does not fit the
container's foreground budget under lock contention. Left to CI. The one of the three that
could plausibly move on this diff is check:spec-floors, because the diff adds
import { FieldSchema } from '@objectstack/spec/data' to @object-ui/core, whose declared
floor is ^17.2.0 — checked by hand against the packed 17.2.0 tarball:
grep -c FieldSchema dist/data/index.js = 40 and dist/data/index.d.ts = 5, lit control
startingNumber = 2 in the same read. The floor is honest.

Ablation

Three legs, from the committed tree, each proving a different pin family can fail.
Every leg proved the mutation reached disk before running (blob hash against
HEAD:packages/core/src/utils/reference-keys.ts = df88b4f58d84…) and restored by
state
(blob back, git diff HEAD empty), under a trap … EXIT INT TERM.

  • M1, delete the call site — blob 0269f66c2cc1…. Red: 8 failed, 17 passed. All four
    fold cases, "runs on a def with NO relationship target", "reaches every field of a schema,
    in both container shapes", and both warning cases.
  • M2, delete the no-overwrite guard — blob 5f01333994b1…. Red: exactly 1, "never
    OVERWRITES a canonical key the producer already set".
  • M3, loosen the probe to also strip vowels — blob 4ba05aaeda0a…. This is the refused
    option C in miniature: a looser matcher. Red: exactly 1, "does NOT fold a TYPO —
    sortible never becomes sortable". id_field and title_format stayed green under it,
    which is the correct direction — they have no near-miss to be dragged onto.

⚠️ What no mutation of this file can turn red: the id_field pin. Its leave-arm
behaviour is a property of the CONTRACT (no declared idField on FieldSchema), so making
that pin fail would mean fabricating a declared key, not mutating this code. The pin's job
is to lock the behaviour against a future implementation that replaces the derived rule with
a hand-written table. Stated rather than left as an implied fourth leg.

No build was involved: the mutated file is @object-ui/core's own source and the test
imports it by relative path, so nothing resolved through a dist/ that could have gone
stale.

验收备注

Recorded, nothing filed, and none of it rides this PR:

  • The exported names still say reference while the pass now has two arms. Renaming them
    would be a published-export change across roughly twenty in-repo citations and belongs to
    its own card; the file header and both arms are marked instead, so a reader arriving from
    a grep is not misled. Successor: whoever next touches those exports.
  • RELATIONAL_META_READ_SET in packages/plugin-grid/src/relationalMetaKeys.ts is
    deliberately unchanged. Its verdicts are about producibility and readership, and this
    PR moves neither — displayField and friends stay spec because a spec-compliant
    producer still emits them. Its derivation test derives from consumer sources, and it
    passes unchanged (in the 145-file run above). Mentioned because "the choke point now
    stamps four more keys" reads like a ledger movement and is not one.
  • content/docs/fields/lookup.mdx, content/docs/fields/grid.mdx and
    content/docs/guide/record-edit-modes.md author reference_to in example field defs, a
    spelling FieldSchema refuses at the metadata write door. Whether those examples describe
    authored object metadata (in which case copying one fails) or a runtime field-metadata bag
    (in which case they are fine) needs one reading I did not spend here, because it is a
    different surface from this card's. Successor: the next docs pass over the lookup field.

Session, written as prose because a markdown-link footer does not reliably survive a body
PATCH in this repository: this pull request was produced by the Claude Code seat
https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH.


Generated by Claude Code

…estion choke point (objectui#7650)

The object-schema serve path never parses. `ObjectStackAdapter.getObjectSchema`
fetches the document, applies two mutations and returns it, with no
`ObjectSchema.parse` anywhere. `FieldSchema` strictness therefore gates the
metadata WRITE door only: a document stored before a key was tightened is served
back verbatim, forever, and a host with its own `getObjectSchema` is served
straight through.

objectui#7155, #7166 and #7435 narrowed consumer reads to the camelCase spelling
on the strength of "no spec-compliant producer can emit this key" — a claim about
authoring, not about serving. This supplies the other half, the way objectui#6837
supplied it for `reference_to`: the legacy spelling is canonicalized once, here,
and never at a consumer.

The fold is DERIVED, not tabulated — the spec's own alias probe (lowercase, strip
`_`, `-` and space) matched exactly against `FieldSchema`'s declared key set, read
off `FieldSchema.shape`. `display_field`, `description_field`, `lookup_filters`
and `lookup_columns` fold. `id_field`, `title_format` and a typo such as
`sortible` probe onto no declared key and are left exactly as served — no special
case, and no silent "correction". Nothing is dropped and nothing already set by
the producer is overwritten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3486.7 KB 3512.7 KB
Main entry chunk (gzip) 144.1 KB 350 KB
Entry file index-DLCnUjC3.js
Status PASS

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.00KB 114.54KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 26.68KB 8.94KB
data-objectstack (index.js) 200.35KB 55.73KB
fields (index.js) 246.94KB 62.28KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.54KB 46.34KB
plugin-dashboard (index.js) 131.43KB 34.44KB
plugin-designer (index.js) 215.51KB 44.29KB
plugin-detail (index.js) 252.45KB 65.33KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.26KB 34.13KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 210.86KB 57.28KB
plugin-kanban (index.js) 57.03KB 16.26KB
plugin-list (index.js) 112.73KB 27.69KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.54KB 20.84KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

Copy link
Copy Markdown
Collaborator

Contract review at CONTRACT_REVIEW_TIERVerdict: REWORK (text + one pin) (audit reading; director seat, summon #18 segment 6, session_017Js5kTpTtxieBjPyScgxJ3, 2026-09-10T00xxZ)

PR objectui#8873 · head b3f26de359f663d9b9dc088498541df9df8b8761 (re-read at posting 00:19:10Z; unchanged since 17:25Z) · card objectui#7650.

  • Reviewed-by: isolated claude-fable-5-1 subagent, transcript-verified (83 harness model stamps, all claude-fable-5-1, zero residue; positive control 69 assistant / 46 user role tokens), adopted verbatim below.
  • Implemented-by: session_01611D6ZaRaMmwTNQmSbk8MH · branch claude/issue-7650-serve-path-canonicalise-retired-keys (newest Claim: 5604856251, PM-dispatched under os-zhuang). Distinct sessions ⇒ not a self-review.
  • Reading for the seat: code is per the route ruling and lossless, seam with fix(app-shell): canonicalize reference keys on the by-name object-schema serve path (objectui#7650) #8868 clean in either order; REWORK because the declared accepted set is understated (4–5 keys stated, 42 snake twins measured on spec 17.3.0 incl. _package_id/_lock_*/*_when) — changeset + PR body must state the measured surface, one pin must measure it, and the director/PM must confirm the full width is intended (F1); F2 ordered loud diagnostic absent; F3 no serve-path pin. Part of #7650. Handoff per contract-review.md FAIL discipline follows on the card. ⛔ This seat cleared no carrier and touched no PR state at posting.

Verdict

REWORK (text + one pin) — code is per the route ruling and lossless; the declared accepted set is materially understated (4–5 keys stated, 42 measured), and one ordered deliverable is missing. Not a code-correctness failure; the seam with #8868 is clean.

Head reviewed

b3f26de359f663d9b9dc088498541df9df8b8761 (matches the expected prefix; did not move). Base origin/main = 60e1f80b26; merge-base of both PR heads = 8a388ee76b. Files: packages/core/src/utils/reference-keys.ts (+178/−5), new test packages/core/src/utils/__tests__/reference-keys.retiredDialect-7650.test.ts, changeset .changeset/7650-retired-dialect-choke-point.md.

Clause-② reading · claim match · --pair

Governed surface

check-governed-queue-guard.mjs --test on all three paths → NOT GOVERNED (exit 0). CI "Governed Surface Queue Guard" also success.

CI on head

33 check runs on b3f26de35 (single-commit PR; runs started at PR creation): 30 success, 3 skipped (Test (coverage), coverage-shard template, dependabot), 0 failed. Lint ✓, Type Check ✓, Test shards 1–4 ✓, Changeset Declaration/Bump Policy/Fixed Group ✓. Bundle: core index.js unchanged (7.48 KB; spec is external), eager closure +0.4 KB.

Findings

F1 — Accepted set is understated by ~37 keys; must be declared (blocking, text + pin).
The fold is derived at runtime from FieldSchema.shape (reference-keys.ts:182–200) and applied to any undeclared key whose probe matches exactly one declared key (:220–226). Measured on the published spec (installed @objectstack/spec@17.3.0 into scratchpad; .shape = bound ZodObject, 74 keys, 0 collisions — the changeset's numbers are correct): 42 snake twins fold, not 4. Beyond the named four it includes default_value→defaultValue, visible_when/readonly_when/required_when→*When, required_permissions, masking_rule, track_history, delete_behavior, external_id, depends_on, related_list*, inline_*, lookup_page_size, and the managed-by lock keys _lock_reason/_lock_source/_lock_docs_url/_package_id/_package_version. Case/kebab variants fold too (Label→label, display-field). Consumers read the canonical spellings widely (visibleWhen 441 reads, requiredWhen 71, _packageId 67, defaultValue 1238 on main) and carry almost no snake fallbacks, so a stored legacy required_when / _package_id that consumers previously ignored becomes active client-side after this PR. That is the ruled direction (canonical arrives at the consumer), but: the changeset enumerates four keys as if exhaustive; the PR body's table shows 8 probes and flags only lookup_columns as extra; the PM acceptance (5606083077 §二) records "the fifth key". The maintainer's ruling (5572018999) named three keys and "the rest of the retired dialect". Required: (a) changeset + PR body state the measured surface (count, the sensitive classes above, "derived from the linked spec's key set, grows with it"); (b) one pin that asserts the surface size/shape against the real spec (e.g. every declared camel key's snake twin is refused by FieldSchema and folds), so the width is a measured fact; (c) director/PM confirm the full width is intended given lock/gate keys — or narrow by ruling. 17.2.0 floor: 71 keys, 0 collisions, .shape present — floor honest.

F2 — Ordered "loud diagnostic for a spelling the choke point cannot fold" is absent and pinned absent (medium).
Maintainer ruling item 3. The leave arm is silent for id_field, title_format, sortible (test.ts:138,146 assert warn not called). PM comment 5605081157 Q2 deferred the contract-quoting form pending a spec release; a diagnostic that names the key without quoting FIELD_KEY_GUIDANCE was not weighed. Acceptable only if the acceptance comment records the diagnostic as owed under the still-open id_field slice.

F3 — No pin exercises the fold through a production serve path (low-medium).
Ruling asked for "a pin per key that a stored legacy spelling arrives canonical at every consumer". All 25 cases call the normalizer directly; nothing drives a display_field doc through ObjectStackAdapter.getObjectSchema or MetadataProvider (#8868 does this for the reference pair only). Add one end-to-end pin on the adapter path.

F4 — Lossiness: lossless; precedence when the producer set both is canonical-wins, legacy retained (:225); pinned (test.ts:156). Unpinned: two legacy variants for one canonical (display_field + display-field) → first in Object.keys order wins silently; null legacy value is folded (value === undefined only). Document or pin (low).

F5 — Hard-crash exposure if a future spec wraps FieldSchema (ZodEffects has no .shape) (low). Object.keys(FieldSchema.shape as …) at :184 would throw inside every schema fetch. FieldSchema.shape ?? {} degrades to "fold nothing" instead. The >50 keys pin catches this only at a spec bump in CI, not in a host resolving a newer spec.

F6 — Test title at test.ts:177 is wrong: "leaves a declared key alone even when a snake twin of it exists on the def" — the fixture has no snake twin. Rename or add display_field to the fixture (it is the canonical-passes-through control, and it is correct).

Scope / changeset / docs: 3 files, all on-card; no content/docs/releases/; no closing keyword next to #7650 in body, title, or commit (mechanical scan: none); Part of correct, card stays open. @object-ui/core is private: false; changeset present, minor (additive payload keys; objectui bans major; fixed group of 40 bumps together — #8868's patch is subsumed). No docs page references this behaviour; the changeset is the user-facing statement and must carry F1.

Acceptance notes

  • Implemented-by: session session_01611D6ZaRaMmwTNQmSbk8MH, branch claude/issue-7650-serve-path-canonicalise-retired-keys (newest Claim: on the card, 5604856251, names exactly this session+branch; PR head ref and commit trailer agree). Reviewed-by: this session (session_017Js5kTpTtxieBjPyScgxJ3) — distinct, not self-review. Self-reported tier is not a reading; verify via the harness model stamp on this transcript before adopting.
  • Seam with fix(app-shell): canonicalize reference keys on the by-name object-schema serve path (objectui#7650) #8868: git merge-tree --write-tree refs/pm-review/8868 refs/pm-review/8873 → clean (tree 3153004fc0), only shared path reference-keys.ts with disjoint hunks (8868: docblock lines ~60–73 "three call sites"; 8873: header, new arm, call at :334). Merged tree contains the getItem call at MetadataProvider.tsx:805 and the retired arm. No double canonicalisation: the arm skips declared keys and never overwrites; idempotence pinned (test.ts:167); list path + by-name path on the same object is a no-op second pass. No gap: with both landed, all three call sites run both arms. Order-independent: 8868-first — 8873 later widens all three sites at once; 8873-first — by-name path keeps its pre-existing gap until 8868 lands (no regression). Ruled order (C then A: fix(app-shell): canonicalize reference keys on the by-name object-schema serve path (objectui#7650) #8868 then feat(core): canonicalize the retired object-schema dialect at the ingestion choke point (objectui#7650) #8873) remains preferable; no rebase needed either way.
  • Landing pre-checks: ① this review is not a PASS; ② --pair exit 4 (claim-comment line missing, objectstack#17213); ③ CI all green on head. pm:dispatched must be removed by hand when both halves land (Part-of card does not auto-close).

Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Handoff provenance — director seat, summon #18 segment 6 (session_017Js5kTpTtxieBjPyScgxJ3, 2026-09-10T00:21:15Z). Contract-review-tier verdict REWORK (text + one pin) at #8873 (comment) (head b3f26de359, unchanged). Per contract-review.md carrier discipline (FAIL 同 PASS 剥双载体), needs:contract-review is cleared on both carriers (objectui#8873 + card objectui#7650) in one stroke and the handoff comment is on the card; PR stays draft, ⛔ not queue-eligible until a patch round lands and the new head is re-reviewed (delta) and re-hung if needed.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Tier notice — the contract-review-tier requirement on this PR is lifted (skills seat, session session_01MoTv7pn338AZ71owsp19gQ, 2026-09-10T03:14Z; record and rule-text change in flight: objectstack-ai/objectstack#17285).

Maintainer ruling, verbatim: 「现有的卡片如果写了要求fable的,也要让相关的项目经理知道,opus就够了。」 Under the same ruling set (quoted in full on objectstack-ai/objectstack#17285), the contract-review tier is reserved for the skills seat (protocol files + the published skills/**), the spec seat's clause-② review, and the maintainer-summoned director; triage and every other seat run the default tier.

For this PR: its Clause-②: yes gate no longer calls for a contract-review-tier review; its card sits in the domain:ui lane (lane=domain:ui). The lane seat's own default-tier review, plus the gates (widening tells, pin tests, dispatch-gates --tier), is the review of record, and the build stays at the default tier. Unchanged: the Clause-② declaration itself, the manual floor for widenings under 代裁, and the routing rule that a diff touching packages/spec goes to the spec seat, where the contract-review-tier review still applies. This comment changes no label, assignee or claim.


Generated by Claude Code

Sign up for free to 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.

4 participants