Skip to content

fix(data-objectstack): classify a view overlay by its _isOverride marker only (objectui#10210, ruling B) - #10427

Merged
os-litant merged 1 commit into
mainfrom
claude/issue-10210-retire-overlay-shape-guess
Sep 25, 2026
Merged

os-litant merged 1 commit into
mainfrom
claude/issue-10210-retire-overlay-shape-guess

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #10210
Clause-②: no

This carries out the maintainer's ruling B on objectui#10210 (ruling comment 5824008636, 「10210 同意」). data-objectstack retires isLegacyOverlayRow, so a view row counts as a personalization overlay only when it carries the _isOverride marker. The ruling puts the B work on this card and asks for no separate execution card. The write half landed earlier as objectui#10332 (Part of). Implemented under the domain:ui seat 4 claim 5824191795 on branch claude/issue-10210-retire-overlay-shape-guess, session https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C.

What changed

  • packages/data-objectstack/src/index.ts: isLegacyOverlayRow is deleted. That was the shape guess: a flat row (no nested config) with viewKind: 'list'. isPersonalizationOverlayRow now reads only the marker, on the item or on its {list: …} body. Four comments the change made false are corrected: the marker docblock, VIEW_OVERLAY_OWNED_KEYS, narrowPersonalizationOverlay, and the comment in the listViews filter. No export, key, schema or signature moves.
  • Five existing pins are rewritten with the reason, and none is deleted (counts below). Three are the ones the ruling names; the other two are the app-shell pins described under "Surface breach".
  • A new pin, packages/data-objectstack/src/viewOverlayMarkerOnly-10210.test.ts, covers the heal, the accepted exposure and a same-shape marker control.
  • .changeset/10210-overlay-marker-only.md declares a patch for @object-ui/data-objectstack: a fix to how the view list reads rows, with no API change. It says broken views recover on read, and it names the one exposed class exactly as the ruling words it.

Mechanism assumptions, measured

  • A1 holds. On origin/main (378a4f6), isPersonalizationOverlayRow returned isLegacyOverlayRow(item, spec) when the marker was absent. git grep finds two callers of the classifier, both in index.ts. Here is what each did with a row the guess classified:
    • listViews() filter: dropped it from the saved-view list, so the app-shell tab was stamped readonly (isSystem = !saved). Now the row is returned as a saved view: a flat row as stored, with _draft kept in draft preview.
    • narrowPersonalizationOverlay(), which app-shell's sanitizeViewOverride calls for both loadViewOverrides read branches (the display merge): narrowed it to identity plus the five owned keys. Now it returns the row by reference, whole.
    • The write side of objectui#10332, as merged: updateViewConfig still stamps _isOverride last on a system-view target (pinned unchanged in viewOverlayMarker.test.ts), and the config save now writes a nested-config envelope. So no current writer produces an unmarked overlay.
  • A2 holds. The broken row is built the way the card measured it: the pre-objectui#10332 flat panel draft, landed through a store that inherits viewKind/object/label from the registry entry it shadows, as the platform's viewIdentityPatch does. Its keys are {label,type,columns,name,isDefault,id,viewKind,object}, plus filter because the draft edited one. listViews() returns it with the edited label, columns and filter, in both the published read and the ?preview=draft read, and narrowPersonalizationOverlay returns it whole. Through the real app-shell pipeline (listViews into buildViewTabs into isSavedViewId), the rewritten ObjectView.overrideMasquerade.test.ts case asserts that the tab is not read-only, that the mutating-handler guard admits it, and that the tab shows the edited label and columns.
  • A3 holds and is pinned as the accepted trade-off. A toolbar overlay written before the marker and never touched since has the same shape. It now reads back from listViews() with its frozen label, columns and filter. Merged over a newer code definition, that frozen copy wins. This is asserted in the new pin, in viewOverlayPatchOnly.test.ts, and end to end in app-shell's ObjectView.overlayPatchOnly.test.ts, where the admin's filter, columns and label edit is covered by the frozen copy.
  • A4 is partly falsified: the pins that asserted the guess are five, not the three named, and one of the three named asserted nothing about it. I measured this by running every suite with only the source hunk applied. Four assertions went red: one in listViews.test.ts, one in viewOverlayPatchOnly.test.ts, one in app-shell ObjectView.overlayPatchOnly.test.ts and one in app-shell ObjectView.overrideMasquerade.test.ts. viewOverlayMarker.test.ts stayed 8/8 green. Only its header prose described the guess, so the header is the part rewritten there. Each rewritten file carries a comment naming this ruling.
  • A5 holds. The source hunk was reverted to the base blob while the tests stayed at HEAD. The trap restored from HEAD and verified the restore by blob hash and an empty git diff HEAD. Result: 9 red, 46 green over the six files. All 4 rewritten assertions and 5 of the 6 new cases went red. The sixth new case is the same-shape marker control, which is green on both trees by design. The 8 cases in viewOverlayMarker.test.ts stayed green because nothing there was rewritten. No dist is involved: the data-objectstack tests import ./index, and app-shell resolves @object-ui/data-objectstack to src through the root vitest alias.

it( counts before and after (text count, then executed-test count from the vitest JSON report):

file before after
data-objectstack/src/listViews.test.ts 12 / 12 12 / 12
data-objectstack/src/viewOverlayMarker.test.ts 8 / 8 8 / 8
data-objectstack/src/viewOverlayPatchOnly.test.ts 8 / 8 8 / 8
app-shell/src/views/ObjectView.overlayPatchOnly.test.ts 17 / 17 17 / 17
app-shell/src/views/ObjectView.overrideMasquerade.test.ts 2 it( + 1 it.each( with 2 rows / 4 3 it( + 1 it.each( with 1 row / 4
data-objectstack/src/viewOverlayMarkerOnly-10210.test.ts (new) none 6 / 6

In ObjectView.overrideMasquerade.test.ts, the unmarked row moved out of the it.each table into its own it(, because the table's shared body asserts exclusion. The executed count is unchanged.

Surface breach: a bounded in-place fix, declared

The claim's surface is data-objectstack/src/index.ts, the three named pins, a new pin and one changeset. The measurement above found two more pins that asserted the guess through the real adapter, both in app-shell: ObjectView.overrideMasquerade.test.ts (its legacy unmarked row case) and ObjectView.overlayPatchOnly.test.ts (a PRE-MARKER legacy row is narrowed …). Leaving them would leave this PR red in CI. I rewrote them in place, the same way as the named pins. The dispatch said "stop on breach"; the role file allows a bounded in-place fix when all four conditions hold, and it wins when the two conflict. That conflict is raised in the report, not settled silently here. The four conditions:

  1. Same defect class: both pin the retired shape guess itself.
  2. Mechanical, with the shape fixed by the ruling: marker-only, the same rewrite as the named pins.
  3. No other holder: the file lists of all 16 open PRs were read; none touches either file or packages/data-objectstack/. The release PR's 1732 files were read in full, matching its changed_files.
  4. Same gate family: app-shell vitest was already a named gate, and nothing new needs verifying.

The seat needs to add these two paths to the claim surface. The same edit also repairs a stale cross-file line citation in ObjectView.overrideMasquerade.test.ts (it cited the savedViews normalization by an old line range; it now cites it by content), as AGENTS.md #11 asks for any file a change touches anyway.

Pending changesets corrected in place (check-changeset-overwrite, case 2: prose correction; front matter unchanged)

  • .changeset/10210-view-config-save-envelope.md (objectui#10332, still pending). Its ⚠️ paragraph said that already-broken views are not repaired by this release and keep reading back as read-only. That becomes false once this change ships with it, so the paragraph now says such views are repaired on read, edits kept, by this data-objectstack change. One present-tense clause ("is the shape the list reader treats as …") is put in the past tense.
  • .changeset/view-overlay-write-patch-only-5233.md said "rows written before this land are still tolerated on read". That is now true only for rows carrying the marker, so the sentence is scoped to them and points to this change for rows older than the marker.
  • .changeset/hollow-view-overlay-hydration-pin-5773.md names packages/data-objectstack/src/index.ts. I re-read it, and no sentence becomes false: its fixtures are real updateViewConfig writes, which carry the marker, and InterfaceListPage's hydration does not narrow.

Verification, all at HEAD 9736e8d20

  • Tests. Under the shared verify lock, pnpm exec vitest run --maxWorkers=2 ran the whole packages/data-objectstack/ package plus the 12 app-shell and console suites that name listViews(, _isOverride, narrowPersonalizationOverlay, sanitizeViewOverride, loadViewOverrides or listViewOverrides: 1072 passed, 0 failed (the base had 1066 of 1066; the difference is the 6 new cases). A second run covered 5 more suites that import the adapter and name listViews (metadataReadWarningToast, core element-data-source, and the kanban, list and timeline elementDataSource suites): 66 passed. Every suite that uses the real adapter and names an overlay reader was run: 19 of 19.
  • Type-check. pnpm --workspace-concurrency=2 --filter '@object-ui/data-objectstack^...' run build (exit 0), then pnpm --filter @object-ui/data-objectstack type-check (exit 0). Its program lists all 4 touched data-objectstack test files (--listFilesOnly). Next, turbo run build --filter='@object-ui/app-shell^...' --concurrency=2: 28 of 28 tasks succeeded. Then pnpm --filter @object-ui/app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json) exited 0, and tsconfig.test.json lists both edited app-shell test files.
  • Gates, all exit 0: check-changeset-presence ("7 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)"), check-changeset-no-major, check-changeset-fixed, check-changeset-overwrite (report-only; it names the two corrections above), check-changeset-claims ("No pending changeset names a file this change touches"; the self-contradiction reading is clean over 3 bodies), check-pending-changeset-literals, check-new-cross-file-line-citations ("VERDICT new-cross-file-line-citations: 0 new citation(s)"), check-control-bytes, check-shell-escape-residue, check-test-path-roots, check-vi-mock-specifiers, check-vi-mock-inherit, check-vi-mock-override-shape, check-unreferenced-sources and check-object-metadata-write-doors. check-governed-queue-guard --test over the 10 paths says NOT GOVERNED.
  • Lint, narrowed and stated as a measurement.
    • Scope: the root eslint.config.js **/*.{ts,tsx} and **/*.test.{ts,tsx} blocks, the same config each package's eslint . resolves to.
    • Run: eslint --format json over the 7 touched TypeScript files gives 7 files, 0 errors, 183 warnings, all no-explicit-any.
    • Same measurement on the base blobs through --stdin: index.ts 124 → 122, ObjectView.overrideMasquerade.test.ts 7 → 8, the other four unchanged, and 10 in the new pin, in line with its sibling pins.
    • Invariance: the config enables no type-aware linting (no parserOptions.project or projectService), so this diff cannot move the verdict for any untouched file.
  • NOT MEASURED: the full app-shell package run (roughly 760 files), which is beyond the foreground cap. CI's sharded pnpm test runs it.

Acceptance notes (not filed)

  • Comments outside this diff that still describe the retired net in the present tense. Carrier: none.
    • The buildViewConfigSaveBody docblock in packages/app-shell/src/views/ObjectView.tsx: "a flat row carrying viewKind is exactly the shape the adapter's legacy-overlay net reads as a personalization overlay".
    • The same sentence in the header of ObjectView.viewConfigSaveEnvelope-10210.test.ts.
    • The buildPersistedViewBody docblock: "already harmless on read since PR fix(data-objectstack,app-shell): a view overlay contributes only the keys it owns #5272 narrowed the merge" now holds only for marked rows.
      Each describes history accurately and is stale only in tense or scope. This is drift, not a defect.
  • Reasoned, not measured: a pre-marker overlay row (the exposed class) now reads as a saved view. The next toolbar toggle on that view therefore takes persistViewPatch's saved-view branch (isSavedViewId is true), writing the row whole and without the marker. Unlike before the ruling, the row does not re-mark itself on the next touch; it stays a plain row. This follows the ruling's "reads as a plain row", and I record it only so it is not rediscovered later.

Generated by Claude Code

…arker only (objectui#10210, ruling B)

Retire `isLegacyOverlayRow`, the shape guess that also treated a flat
view row carrying `viewKind: 'list'` as a personalization overlay. An
"Edit view config -> Save" on a code-defined view stored that same shape
before PR #10332, so `listViews()` dropped the user's own view, the tab
was stamped read-only, and publishing made it permanent. Under the
maintainer's ruling B (objectui#10210, comment 5824008636) the marker is
the only discriminant: such rows heal on read with their edits, and the
one exposed class (overlay rows written before the marker and never
touched since) is named in the changeset and pinned.

The pins that asserted the guess are rewritten with the reason, not
deleted: `listViews`, `viewOverlayPatchOnly` (and the header of
`viewOverlayMarker`), plus the two app-shell pins that ran the real
adapter through the same guess. The two pending changesets this makes
false are corrected in place.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3043.7 KB 3104.5 KB
Main entry chunk (gzip) 148.3 KB 350 KB
Entry file index-n0pohHg-.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.68KB 6.20KB
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) 27.95KB 10.04KB
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.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
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) 542.47KB 129.64KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.86KB 62.25KB
fields (index.js) 255.56KB 64.73KB
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) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
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) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
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) 16.01KB 3.93KB
plugin-calendar (index.js) 51.44KB 14.62KB
plugin-charts (index.js) 72.54KB 20.26KB
plugin-chatbot (index.js) 198.27KB 47.18KB
plugin-dashboard (index.js) 133.45KB 35.33KB
plugin-designer (index.js) 216.12KB 44.37KB
plugin-detail (index.js) 260.94KB 67.93KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 146.34KB 37.47KB
plugin-gantt (index.js) 168.47KB 41.53KB
plugin-grid (index.js) 215.22KB 58.85KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 114.36KB 28.24KB
plugin-map (index.js) 22.05KB 7.14KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.72KB 8.96KB
plugin-tree (index.js) 10.74KB 3.76KB
plugin-view (index.js) 85.86KB 21.38KB
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) 114.58KB 37.60KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
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) 5.00KB 2.39KB
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) 17.15KB 6.32KB
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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9736e8d20297fa0c9b9beed25bd564bf0e9923e3

Rendered by an isolated review subagent spawned by the domain:ui#4 seat; its served tier was checked against its transcript stamps (76 of 76 model stamps at the review tier). Adopted by this seat.

① Derived judgments

  • (a) Ruling fidelity: holds. git grep isLegacyOverlayRow over the whole tree at head returns nothing. isPersonalizationOverlayRow is now one boolean expression: the marker on the item or on its {list: …} body, and nothing else; no ?? chain, no shape branch, no tolerant fallback. The shape predicates the retired guess used (a flat row with no nested config, plus viewKind === 'list') survive in packages/data-objectstack/src only as (1) the listViews map step that flattens a nested config up to the switcher shape, which is display shaping of rows already admitted, and (2) the FORM_FAMILY exclusion of form/detail by declared viewKind, which never classified overlays. No app-shell overlay reader classifies at all: a whole-tree non-test grep for _isOverride hits only index.ts; listViewOverrides builds a name-keyed map without classifying; getView returns the row; InterfaceListPage's hydration merges what it fetched whole. Callers of the classifier at head, enumerated: listViews()'s filter (unmarked flat row: kept, returned as stored, with _draft: true added on the preview read) and narrowPersonalizationOverlay (unmarked flat row: returned by reference, whole). The only non-test caller of narrowPersonalizationOverlay is app-shell sanitizeViewOverride in ObjectView.tsx, reached from both branches of loadViewOverrides; for an unmarked flat row it now runs its filter-recovery pass over the whole row and hands it to viewOverrides. No other caller exists.
  • (b) Healing: pinned, with the real adapter. Every pin constructs new ObjectStackAdapter(...) and stubs only client.meta (a store) or the fetch. The new pin's first describe block lands the flat unmarked {label,type,columns,filter,name,isDefault,id,viewKind,object} row through a store that inherits identity the way the platform's write door does, then asserts: the published read (client.meta.getItems) returns it with the edited label, columns and filter; the ?preview=draft read (route stub) returns it with _draft: true; narrowPersonalizationOverlay returns it by reference. The tab gate is !isSavedViewId(savedViews, id) and savedViews is whatever listViews returned in either read mode; the rewritten app-shell overrideMasquerade case runs real listViews into buildViewTabs into isSavedViewId on the published read and asserts isReadonlyTab false, isMutable true, and that the tab shows the edited label and columns. So the tab is no longer read-only on both reads: the draft read is pinned at the listViews layer, the gate at the pipeline layer on the published read; the gate function does not depend on which read filled savedViews.
  • (c) The exposed class and its consequence: the dev's reasoning is true at head, and it is within the ruling. persistViewPatch computes targetIsSavedView = isSavedViewId(savedViewsRef.current, viewIdLocal), savedViewsRef.current mirrors the listViews result, buildPersistedViewBody(..., { isSavedView: true }) returns { ...baseViewDef, ...patch }, and updateViewConfig(..., { isSavedView: true }) withholds the marker (...(opts?.isSavedView ? {} : { [VIEW_OVERLAY_MARKER]: true })). A pre-marker overlay row, now in savedViews, therefore has its next toolbar toggle written whole and unmarked; baseViewDef is the tab buildViewTabs built, the definition with the saved row's body spread over it, so what lands is the frozen copy plus the toggled key plus id. This does not turn a read-time exposure into a new write-time one: the row at rest was already that frozen copy, flat and unmarked; after the toggle it is the same row with one more key, in the same class, and the display outcome is identical before and after. What is lost, relative to the pre-ruling code, is the old classifier's incidental self-heal (exclusion routed the toggle into the overlay branch, which wrote patch-only plus the marker). The ruling's "reads as a plain row" entails this: a plain row's toggle writes a plain row. Going forward the class neither grows (no writer produces an unmarked overlay; updateViewConfig stamps the marker on every system-view target) nor shrinks on its own; the operator remedy the analysis names (delete the row through the metadata API) is unchanged. No path lets a MARKED overlay row lose its marker: a marked row is excluded by listViews, so isSavedViewId is false, so the overlay branch re-stamps it; the classifier itself never writes. Conclusion: nothing beyond the acceptance note the dev already recorded; no card, no FAIL. The seat's closing comment should carry that note so the ruling's exposure statement is not later read as self-shrinking.
  • (d) Pins rewritten, not deleted: verified. My own it( counts, base then head: listViews.test.ts 12 then 12; viewOverlayMarker.test.ts 8 then 8 (only the header changed, as the diff shows); viewOverlayPatchOnly.test.ts 8 then 8; app-shell ObjectView.overlayPatchOnly.test.ts 17 then 17; app-shell ObjectView.overrideMasquerade.test.ts 2 it( plus one it.each of 2 rows, then 3 it( plus one it.each of 1 row (the unmarked row moved out of a table whose shared body asserts exclusion); the new pin has 6. No skip, only or todo anywhere in the six files. This matches the dev's table exactly. Protective meaning for marked rows is retained in every rewritten file: listViews.test.ts keeps "excludes a row carrying the explicit write-side marker" and "a marked row is excluded even without the legacy viewKind signal"; viewOverlayPatchOnly.test.ts keeps the marked-row narrowing cases ("keeps the five keys the toolbar owns…", "omits an owned key…", "a real updateViewConfig write comes back narrowed to its patch"); app-shell overlayPatchOnly keeps "a row stored in the old shape stops shadowing the source on the NEXT READ" with _isOverride: true in its fixture and "a PRE-FIX fat overlay no longer shadows…" written through the real updateViewConfig; overrideMasquerade keeps the marked row in its it.each. viewOverlayMarkerOnly-10210.test.ts is real: the real adapter class, a store that inherits viewKind/object/label from the registry entry, six cases covering heal (three), exposure (two) and a same-shape marked control. Red-first arithmetic is consistent: 12+8+8+17+4+6 = 55 executed over six files; 9 red = the four rewritten assertions plus five of the six new cases; walking each new case against the base classifier confirms it (the healed row is excluded so find is undefined; the draft list is []; toBe(row) fails on a narrowed copy; the exposure cases see an excluded or narrowed row); the marked control is green on both trees by design.
  • (e) Changeset truth: every sentence holds at head. .changeset/10210-overlay-marker-only.md: recovery on read with edits kept, marker as the sole discriminant, the retired guess described as a flat row carrying viewKind: 'list' kept for pre-marker toolbar rows, nothing rewritten at rest (neither listViews nor loadViewOverrides writes), and the exposed class stated in the ruling's words: overlay rows written before the marker (objectui#4227, closed 2026-08-15; verified closed_at 2026-08-15T16:16:59Z) and never touched since, whose frozen label, columns and filter copy covers the code definition again, no deployment named. 10210-view-config-save-envelope.md: frontmatter unchanged ('@object-ui/app-shell': patch, the diff hunks are body-only); the tense change ("was the shape the list reader treated") and the replacement paragraph ("repaired on read, edits kept, by the data-objectstack change… unless the row carries the _isOverride marker") are true; the untouched sentences remain true. view-overlay-write-patch-only-5233.md: frontmatter unchanged; the scoped sentence is true (a marked fat row is still narrowed on read, and its next toggle takes the overlay branch, so the PUT strips it at rest; a row older than the marker is not narrowed, as pinned). No line-address citations in any of the three. hollow-view-overlay-hydration-pin-5773.md remains true: its fixtures are real updateViewConfig writes, which carry the marker, and InterfaceListPage's hydration merges without classifying, so the classifier change does not reach it. The other pending changesets that name listViews (8900-saved-views-unread-not-absent, listviews-refused-read-discrimination, 8411-flat-view-overlay-columns) describe failure degrade and config unwrapping; none states the shape guess and none becomes false. Both packages sit in the 40-member fixed group, so the two 10210 changesets ship together.
  • (f) Stale prose outside the diff: dated, not a published face. buildViewConfigSaveBody's docblock says a flat row carrying viewKind "is exactly the shape the adapter's legacy-overlay net reads as a personalization overlay" inside a sentence narrating the pre-fix(app-shell): saving a view's config no longer turns the view read-only (objectui#10210) #10332 defect; the net no longer exists, so the tense is stale but the history it tells is accurate. buildPersistedViewBody's docblock says fat rows are "already harmless on read since PR fix(data-objectstack,app-shell): a view overlay contributes only the keys it owns #5272 narrowed the merge"; that now holds only for marked rows. Both functions are exported from ObjectView.tsx, but the package entry (src/index.ts via views/index.ts) exports only ObjectView, so neither symbol is part of the published API surface and neither docblock is a published face. The ObjectView.viewConfigSaveEnvelope-10210.test.ts header is a test comment, not a face. Drift to carry as a follow-up; not a defect in this PR.

② Semver level

patch on @object-ui/data-objectstack is right; no other package is bumped and none needs to be (the app-shell edits are two test files and two prose corrections to pending changesets, and a changeset is declared for the change). objectui AGENTS.md's version rule, quoted: 「minor/patch 独立演进——objectstack 没动时不必跟发;objectui 自己的改动照常用 changeset 推进」 and 「changeset 里不要声明 major —— … objectui 自身的破坏性变更也标 minor(在正文里写清 breaking 语义即可)」. The rule's only level distinction is breaking-goes-to-minor and never major. This change moves no export, key, schema or signature; listViews keeps its return contract; it retires a client-side heuristic over stored rows under a P1 bug ruling that frames it as a fix with one accepted data exposure and no named holder. That is a fix, so patch fits. minor would fit only if the exposed class were declared a breaking semantic, which the ruling does not do; the changeset's ⚠️ paragraph already states the exposure in the body, so the rule's "write the breaking semantics in the body" is satisfied either way. No major anywhere.

③ Boundary flags

  • Fixes #10210 is the only closing keyword, in the body; none in the title or the commit message. The Part of mention of objectui#10332 is prose, not a keyword. Right for the last half.
  • No model identifier in the PR title, the body, the one commit message or its trailers; scanned for model family names and versioned model ids; the co-author line carries the assistant's product name only and the session trailer is a URL.
  • File surface: exactly 10 files, equal to the claim's six (index.ts, the three named pins, the new pin, the new changeset) plus the amendment's four (ObjectView.overrideMasquerade.test.ts, ObjectView.overlayPatchOnly.test.ts, 10210-view-config-save-envelope.md, view-overlay-write-patch-only-5233.md). One commit; merge-base with main is the commit's parent.
  • mergeable_state: read behind at the start of this review, unknown while GitHub recomputed after main advanced, and clean on the settled read (00:38:58Z), with mergeable true. Main's commits since the base touch none of the ten files, nor ObjectView.tsx or viewIdentity.ts.
  • The PR is still marked draft.
  • Check-runs on the head, final at 00:37:18Z after foreground polling at 60s: 43 total, 40 success, 3 skipped, 0 failed, 0 in progress. The non-success runs by name: Test (coverage) (skipped), dependabot (skipped), Test (coverage shard ${{ matrix.shard }}/4) (skipped).
  • The dev's declared NOT MEASURED item (the full app-shell package run) is covered by CI's eight test shards, all green.

Implemented-by: claude/issue-10210-retire-overlay-shape-guess
Reviewed-by: session_01BP8CMtACxTdLjqR6rhd33C

VERDICT: PASS


Generated by Claude Code

@os-litant
os-litant marked this pull request as ready for review September 25, 2026 00:42
@os-litant
os-litant added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit 9ec8ceb Sep 25, 2026
45 checks passed
@os-litant
os-litant deleted the claude/issue-10210-retire-overlay-shape-guess branch September 25, 2026 00:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants