Skip to content

fix(app-shell): a Studio switch inside the autosave debounce never writes the previous item's buffer into the one just opened (objectui#11232) - #11265

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11232-autosave-send-bound-to-target
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11232-autosave-send-bound-to-target

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11232
Clause-②: no

objectui#11203 (the package-switch member, closed by triage as folded into this card) rides here with its three pins.

Implemented on claude/issue-11232-autosave-send-bound-to-target, dispatched by the domain:ui seat 1 PM loop, session https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ. Triage 5915804318 is the ruling executed; claim 5918635427.

What changed

where before now
useDraftAutoSave the timer was keyed on the dirty flag, the blocked flag and the snapshot; the save it called was read through a ref that already addressed the newly opened item, while the buffer still held the previous item's document takes target, the item its save addresses (a primitive type:name, AGENTS.md #10). A dirty period is bound to the item it began on. A period that began on another item is never sent, by the timer or by flush, and it ends when the caller's dirty flag falls (each caller's load clears it as it installs the new buffer). A save's claim (objectui#11204) reads moved once the target has changed
the four callers none pass flow:NAME (Automations), the page leaf key (Interfaces page inspector), object:NAME (Data), app:NAME (Interfaces nav)
where StudioDesignSurface renders InterfacesPillar not keyed: a package switch kept the route mounted, and with it the nav editor's dirty flag, its open editing and the open page key={packageId}: one reset of the whole pillar on a package switch (triage's preferred shape for objectui#11203)

No other line of DataPillar changes: its hook call gains the required target, and its switch (the control) is untouched.

The pending edit is dropped, not sent to its own item

A switch inside the debounce drops the previous item's unsent edit. It is never written to the newly opened item, and it is not re-targeted to its own item either. Why this side of the ruling's choice:

  • It is what already happens on every pillar whenever the new item loads inside the debounce: the load installs the new buffer and clears the dirty flag, which cancels the timer. The defect was only the slow-load path. Dropping adds no new write; re-targeting would add a save nobody sends today.
  • It is what the Data pillar's switch has always done (its synchronous clear), the card's named control.
  • It matches the confirmed discard of a package switch (objectui#11203), where the author said "discard".
  • PR objectui#11230's sent-snapshot claim: a save that lands after a switch reads moved, so it never clears the flag of the item opened since. objectui#11189's nav-flush rule ("Done" sends a pending edit at once) is a close of the SAME item's editor and is unchanged; flush now also refuses a period that began on another item.

Which fix covers which member (the claim asked)

  • Automations flow switch, page-inspector page switch: the hook (ablation leg A).
  • A save in flight across a page switch: the hook's claim binding (leg C). The save itself always addressed its own item (the save call captured it).
  • Data object switch: the control, green at base and at HEAD.
  • The nav autosave on a package switch (objectui#11203): the key (leg B). The hook alone cannot reach it: the nav editor's dirty flag and open editing are the pillar's state, so after a confirmed discard a flag still stood over the next package's app, and no edit after the switch could start a period the hook would send. The page inspector's package-switch state, which triage asked measured in the same pass, is the key's too.

Premises re-measured at origin/main 02a22957c0 (after PR objectui#11259)

The objectui#11204 harness reading still holds on the tip. Pin file first, fix absent (the red run):

  • Automations: edit flow notify_owner's start label to 'Kick-off', open nightly_digest (load held) inside the debounce: 1 save, of nightly_digest, carrying notify_owner's document (label 'Notify owner', start 'Kick-off').
  • Page inspector: edit page home's label to 'Welcome', open landing (load held): 1 save, of landing, carrying home's document (label 'Welcome', the hello block).
  • Package switch, confirmed discard, fast load: 1 save of app beta_app to package com.beta.app, B's own navigation, unedited (objectui#11203's reading). With B's app load held: 1 save of acme_app to com.acme.app, the edit the author had just confirmed discarding.
  • Page inspector on a package switch: page home of package A stays open on package B, and an edit then saves page home with package com.beta.app.
  • Data (the control) and the cancelled discard: green.

PR objectui#11259 did not close any member: its edits (the load effect's first-leaf pick, the ?surface= restore, StudioNavItemInspector) do not touch the hook or the switch paths.

Pins

New file StudioDesignSurface.autosaveSwitch-11232.test.tsx: the real pillars and the real registered FlowPreview / FlowInspector / PagePreview / PageBlockInspector / PageDefaultInspector, a server double that records every save with its package and can hold an item's load or a save.

  • Automations: a flow switch inside the debounce sends nothing; the edit after the new flow's load saves to it (the control).
  • Page inspector: the same for a page switch.
  • Page inspector: a save in flight across a page switch lands on its own page, and an edit typed on the previous page's buffer before the next page's load lands is not sent there.
  • Data: an object switch inside the debounce sends nothing (the control, as it always has), and an edit after the load saves to it.
  • objectui#11203, through the real surface and its PackageSwitcher: a confirmed discard sends nothing to the new package and closes editing; an edit after the switch saves normally, to beta_app in com.beta.app; the same with B's app load held; a cancelled discard keeps package A and its edit, which then saves to acme_app in com.acme.app.
  • Page inspector on a package switch: the new package's page opens and an edit saves to it, in the new package.

The objectui#11189 / objectui#11204 / objectui#11167 / objectui#11196 autosave pins are unmodified and green (see the directory run below).

Reverse verification and ablations (each on a committed HEAD; mutation and restore proven on disk)

Every leg ran pnpm exec vitest run on the pin file (8 cases) at HEAD 3cd7a994d1. The pins import the pillar source directly (a relative import, no dist/), so no build leg applies. Legs A to C went through objectstack's scripts/ablation-replace.mjs in WRAP mode (anchor x1 to x0, blob moved, restore proven: blob == HEAD f8072729aaf6 and git diff HEAD empty). The base leg swapped in the base file under a trap, with the same two restore proofs.

leg mutation predicted observed
base StudioDesignSurface.tsx at 02a22957c0 (blob a2259cfc6561, verified on disk) red except the two controls 6 failed / 2 passed: the Data control and the cancelled discard green. The in-flight pin reads 2 saves, home 'Welcome' then landing 'Typed during load'; the package-switch page pin finds home's hello block still open on package B
A the timer's owned() check made a no-op red: Automations, page switch, in-flight 3 failed / 5 passed, exactly those
B key={packageId} deleted red: the confirmed-discard pin (editing stays open over a standing flag), the package-switch page pin 3 failed / 5 passed: those two AND the slow-load discard pin, which read 1 save of acme_app to com.acme.app (the discarded edit). Direction differs from the prediction: an instrumented re-run read target still app:acme_app when the debounce fired, because the new package's app name had not committed yet (see the harness note under Acceptance notes). So the key, not the hook, holds that pin
C the claim's target comparison removed red: the in-flight pin only 1 failed / 7 passed, exactly that

Tests and gates at 3cd7a994d1

Every run below is at 3cd7a994d1, from the worktree root, with root-relative paths. Pass counts are read from vitest's own summary lines.

Acceptance notes

  • The dropped edit, on every switch. An edit made less than 1.5 s before any switch (flow, page, object or package) is dropped, as above. On a package switch this now includes a page-inspector edit: before, it happened to be sent to its own page in its own package (the page save's callback still held the previous package); now the remount drops it. Page edits are not held by the surface's leave guard (only nav edits are), so no prompt announces it. Read at source, not a new class: the pillar-tab switch unmounts the pillar and drops such an edit the same way today.
  • An in-flight save's own UI effects are not bound to its item. A save of the previous item that lands after a switch still sets the "unpublished draft" badge, and a failure its error, on the item opened since. Nothing is written; read at source, not measured.
  • Harness boundary. pastDebounce waits inside one act() scope, which holds React renders from async continuations until the scope ends while real timers fire. So in leg B the switched-to app's name (set after the app list's await) was not yet committed when the debounce fired, and the hook still read the previous app as the target; that interleaving is also the real one when the new package's app list is slow, and the key is what holds it. Not measured in a browser.
  • Out of scope, measured on this branch and reported to the seat, not fixed here: the previous item's buffer stays on screen and editable under the newly opened item while its load is in flight, and a save from there writes it into the new item (page inspector autosave, the Automations enable toggle, the Data pillar autosave); and a package switch keeps the Automations pillar's open flow of the previous package, which an edit then saves into the new package.

Generated by Claude Code

…ites the previous item's buffer into the one just opened

useDraftAutoSave binds a dirty period to the item that was open when it
began (a new `target` identity from each caller). After a switch nothing
is sent, by the timer or by flush, until the caller's dirty flag has
fallen, and a save's claim reads moved once the target has changed. The
previous item's pending edit is dropped, as the Data pillar's switch has
always dropped it. InterfacesPillar is keyed by package where the surface
renders it, so a package switch resets the nav editor and the open leaf
(objectui#11203, folded in).

Claude-Session: https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ
Co-authored-by: Claude <noreply@anthropic.com>
…11232)

The save lands on its own page, and its claim, bound to the item it was
sent for, leaves the dirty period standing so an edit typed on the
previous page's buffer during the next page's load is not sent there.

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

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 1 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/6681-declared-lazy-marketplace-routes.md

  • names views/studio-design/StudioDesignSurface.tsx → packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx — edited by this change

    Both marketplace chunks were held by CHUNK CO-TENANCY, not by any import of the page: rolldown had parked components/SuggestedBindingsPanel.tsx (statically imported by the eager views/studio-design/StudioDesignSurface.tsx) in the first, and console/marketplace/InstalledListWidget.tsx (bare-imported by the package barrel for its SDUI registration) in the second. An app-shell-eager-leaves group in apps/console/vite.config.ts isolates those co-tenants so the three declared-lazy pages chunk by their own dynamic-only reachability. The console eager closure moves 3180.2 KB → 3171.5 KB gzipped (−8,888 bytes, 48 → 45 eager chunks) with the three per-chunk ceilings unmoved.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 54a78308a (merge-base with origin/main): 2 file(s) changed outside .changeset/, read against 1837 pending declaration(s) that publish a body (2449 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3582.7 KB 3607.4 KB
Main entry chunk (gzip) 149.9 KB 350 KB
Entry file index-DqJVK6Rr.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.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
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.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
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) 27.13KB 7.95KB
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) 570.05KB 136.37KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 228.91KB 63.54KB
fields (index.js) 261.24KB 66.29KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
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 (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.35KB 9.18KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 40.88KB 11.46KB
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.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
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) 52.17KB 15.06KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 138.73KB 37.05KB
plugin-designer (index.js) 216.32KB 44.56KB
plugin-detail (index.js) 242.19KB 63.62KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 173.69KB 44.62KB
plugin-gantt (index.js) 173.03KB 43.07KB
plugin-grid (index.js) 231.37KB 63.54KB
plugin-kanban (index.js) 48.46KB 15.12KB
plugin-list (index.js) 116.42KB 28.89KB
plugin-map (index.js) 23.50KB 7.82KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 33.05KB 9.67KB
plugin-tree (index.js) 11.20KB 3.89KB
plugin-view (index.js) 90.32KB 22.76KB
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.81KB 3.58KB
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) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.06KB 2.68KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 21.42KB 7.05KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
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.26KB
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) 2.52KB 1.31KB
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) 19.82KB 7.15KB
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: 3cd7a994d11f9f6dff67a5e10b130a87e51a5841
Local-runs: none

① Derived judgments

Inputs: card #11232 (body; triage 5915804318; serial note 5916014553; claim 5918635427; dev report 5919488482), PR #11265 (body, its 3 files, the net diff against main from the branch point 02a22957c0: +633/−7), and the head's check-runs, read twice. Read-only beside them: #11203, #11204 with PR #11230, #11189. Nothing built, run or re-run.

Accept sets and public surface: none move. useDraftAutoSave and DraftSend are module-private to StudioDesignSurface.tsx; its only readers are the four in-file callers. packages/app-shell/src/index.ts exports StudioDesignSurface and StudioDesignSurfaceProps from that file, not the pillars and not the hook. The new target: string option is required, so a caller that omits it is a type error, and Type Check is green on the head: all four pass it. StudioDesignSurfaceProps is unchanged. Named right.

(a) The binding, read at source. pendingRef now carries target and since; the layout effect keeps since while the dirty flag stays up (dirty && prev.dirty ? prev.since : target) and takes the current target otherwise. The timer and flush refuse a period whose since differs from target; a claim's unmoved() also requires target === sentFor. Per caller, for the card's letter (one switch inside the debounce, the new load slow):

⛔ "Never write the old buffer to the new target" holds on every path the card's letter names: the pending send is refused, and an in-flight send addressed its own item when it was taken (doSave captured current). It is a binding by item identity, not by buffer provenance, and the period ends only when the caller drops its flag; that leaves one blind spot outside the letter, under ③.

(b) Drop, not re-target. Triage's letter is "cancels or re-targets", and it names DataPillar's synchronous clear (a drop) as the fallback shape, so a drop is inside it. It is stated in the PR (its own section, four reasons), in the changeset and in the hook's comment, and it is pinned: the three leaf pins assert server.saves empty after the switch and again after the load, so the old item receives nothing either. It is what main already does whenever the load lands inside the debounce (the page inspector's setIfDirty(false), Automations' finally, Data's synchronous clear); the fix makes the slow-load path agree with the fast one. Not a regression of #11204: with no switch the claim's target comparison is a no-op, and PR #11230's letter (an edit during an in-flight save of the same item lands in the next save) is untouched. The one edit this PR drops that #11230 would have sent is one typed on the previous item's buffer while it is shown under the next item, which could only have landed on the wrong page. Right.

(c) The package-switch member. StudioDesignSurface renders InterfacesPillar with key={packageId} at its mount site. Inside the claim's conditional surface: the claim names "InterfacesPillar's PackageSwitcher: only if the hook fix does not cover them", and triage's preferred shape for #11203 is verbatim "keying InterfacesPillar by package", which only its mount site can do. That the hook does not cover it is true at source: the app-load effect's !isSameApp branch installs the next package's app and clears neither navDirty nor editNav, so with the hook alone the flag never falls and every send on the next package would be refused for good. The console route (the :packageId/:tab element in StudioRoute.tsx) keeps the surface mounted across a package switch, as the harness does, so the key is live in production. #11203's three pins are present and read right: a confirmed discard sends 0 app saves after 3.5 s with editing closed and nothing held; an edit after the switch saves beta_app in com.beta.app; a cancelled discard keeps Acme and its item, which then saves to acme_app in com.acme.app. Beside them the slow-app-load variant and the page inspector on a package switch, which #11203's triage asked measured in the same pass. The pillar's unmount cleanups report false and null to the surface, so the leave guard and the dock label release with the remount. Right.

(d) DataPillar. One required option and one comment on its hook call; its list effect, load effect, switch and both saves are the base's. Inside ⛔ "not DataPillar's switch". Right.

Pins against the ruling's list. Automations and the page inspector reproduce first (4 of 6 red at the branch point; 6 of 8 red on the base leg); Data is the control; a switch inside the debounce sends 0 saves to the newly opened item on each; an edit after the load saves normally on each. All present. Ablation A (the timer's owned() a no-op) 3 red, C (the claim's target comparison removed) 1 red, each as predicted; B under ③.

Check-runs on the head (conclusions are the gate verdicts): 35 success, 3 skipped (the two coverage rows and dependabot). On the second read Test (shard 2/8), Test (shard 4/8), Test (shard 6/8) and Test (shard 7/8) are still in_progress: not concluded, read as neither pass nor fail. Type Check, Lint, Spec Main Shape Gate, Build & E2E, Test (dist pins), shards 1, 3, 5 and 8, the five changeset gates and the rest: success. main moved three commits past the branch point; none touches studio-design/ (changesets only), and the PR reads mergeable.

② Semver level

.changeset/11232-autosave-send-bound-to-target.md: '@object-ui/app-shell': patch. Right: a behaviour fix inside the Studio surface; no export, prop, type, schema or accepted set moves. The hook and the pillars are not on the package index, and the remount on a package switch is behaviour of the exported StudioDesignSurface, not its API. The changeset's text matches the diff: the four callers named, the drop named, the package key named. Changeset Declaration, Changeset Bump Policy, Changeset Claim Re-read, Changeset Fixed Group Check, Changeset Overwrite Report: success. Clause-②: no on the PR and the claim: right.

③ Boundary flags

Dev deviations, each answered:

  1. DataPillar gains target: inside the ⛔ (d).
  2. The key at the mount site, not inside the pillar: inside the conditional surface, and triage's preferred shape (c).
  3. Pins beyond the list (the page inspector on a package switch; a save in flight across a page switch): the first finding(app-shell): switching package in Studio after discarding an unsaved nav edit makes the nav autosave write the NEW package's unedited navigation as a draft — a draft row on a package the author only opened #11203's triage asked for, the second is the ruling's "in-flight send" clause. Right to add.
  4. The base leg by a whole-file swap: a measurement note, no bearing on the diff.
  5. Leg B, predicted 2 red, observed 3 (f). Consistent with the diff: doNavSave's deps are [client, appName, appDraft, onDraftSaved], without packageId, so with the key deleted and the next package's app name not yet committed (the act() hold), the timer read since === target === app:acme_app and sent the discarded edit to acme_app in com.acme.app: the old buffer to its old target, not a cross-write, but against the author's confirmed discard. The same interleaving is real when the next package's app list is slow. So the key holds that pin for the reason the PR states, and the extra red sharpens rather than hides the PR's "which covers which": the hook never covers a package switch. No pin passes for a wrong reason.
  6. Build closure at e4594fdc66 with the source identical: accepted; Build & E2E is green on the head regardless.
  7. Probes never committed: the file list is the three files.
  8. No merge of main: checked, nothing on this surface to merge.

open_questions: none.

The PR's acceptance notes and the report's out_of_scope_findings, each answered:

  • A page-inspector edit dropped on a package switch (report entry 3; PR note 1). Accepted under the discard rule and stated. The leave guard mirrors navDirty only, so a page edit under 1.5 s old is dropped by the remount without a prompt; before, it reached its own page by a stale closure (doSave has no packageId in its deps), and every later edit on that still-open page went to the wrong package, which is the defect fixed. Not finding(app-shell): Studio's shared draft autosave drops an edit made while a save is in flight on its other callers (Automations measured; Data, page inspector) — the family closure, fix in useDraftAutoSave #11204's letter (no save in flight). The pillar-tab switch drops the same edit today. Fileable, low: the surface's leave guard holds nav edits only, and a page-inspector edit is not held on a package switch.
  • An in-flight save's UI effects (setHasDraft, setError) land on the item opened since: no write, read at source. A note for the family card below.
  • Finding 1, the load window (three pillars: the previous item's buffer shown and editable under the new item until its load lands; the autosave, the enable toggle and add-field write it into the new item). Outside this card's letter: the dirty period begins after the switch, so it is not a switch inside the debounce, and the enable toggle is an explicit save outside the hook. Same family: the buffer saved is not the target's. Pre-existing, read at source on the base, which has no binding at all. For the seat to file.
  • Finding 2, Automations on a package switch (package A's open flow stays open on B, and an edit saves it with B's packageId; Data read the same). Outside this card's letter: triage's family enumeration names the nav autosave as the package-switch member, and the claim keys nothing but InterfacesPillar. It is finding(app-shell): switching package in Studio after discarding an unsaved nav edit makes the nav autosave write the NEW package's unedited navigation as a draft — a draft row on a package the author only opened #11203's exact shape on the two unkeyed pillars, and a one-line key at the same mount site closes it, but the DataPillar slot is under this order's ⛔, so the stop is right. For the seat to file.
  • One more from this review, same family. The page inspector's load effect has a branch that replaces the buffer without ending the period: if (!current || !isEditable) { setDraft({}); setHasDraft(false); return; } clears no ifDirty. Edit a page, open an object leaf (a studio-canvas leaf: isEditable false, blocked true, so nothing is sent there), then reopen the same page: since and target are both that page again, the buffer is {}, and if the page's reload takes over 1.5 s the timer sends {} as that page's draft. Pre-existing on main (the base has no owned() and sends it the same way); outside the letter (the write goes to the item the edit began on, so the ⛔ is not breached); read at source, not measured, and whether the server refuses an empty page body decides its weight. The hook's comment says "each caller's load clears it as it installs the new buffer", which this branch does not, so the comment overstates the caller contract by one branch.
  • One card or two. One family closure card, "the buffer saved is not the target's, after a switch", with three members and their pins: the load window on three pillars; the package switch on Automations, Data read; the non-editable round trip on the page inspector. Two fixes, named per member: a key at the mount site for the two unkeyed pillars; a per-pillar binding of the buffer to the item its load installed it for, or hidden rail editors while a load is in flight. Same class, same door, same file; DataPillar is in scope there, which lifts this order's ⛔. Grade with this card's p1: the writes are silent and cross-item.

Nothing above blocks. Every path the card's letter and triage's ⛔ name is closed and pinned, the drop is inside "cancels", #11203 rides on its preferred shape with its three pins, and the semver line is right. The seat's ACCEPT waits on the four test shards.

Implemented-by: claude/issue-11232-autosave-send-bound-to-target
Reviewed-by: session_0122Knsowci76D2rBWReCzzZ

VERDICT: PASS

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

Projects

None yet

2 participants