Skip to content

fix(react,app-shell,plugin-kanban,plugin-calendar): a standalone object-kanban / object-calendar honours the spec's page navigation, and a mode-less block, through the host's record navigator (objectui#11293) - #11323

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-11293-standalone-page-navigation
Oct 1, 2026

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11293
Clause-②: yes

What this changes

A standalone object-kanban / object-calendar now honours the spec's page navigation mode, and a navigation block written without mode (which takes the spec's page default), by opening the record page. drawer and the other members behave as before, and a parent view's click handler still takes the click first.

The rule lives in ONE place, the shared hook, per triage's direction (comment 5922527918): no per-renderer copy, and no narrowing of the spec's mode set.

  • @object-ui/react — useNavigationOverlay. In the page branch, when no onNavigate was supplied, the click goes to openRecord(objectName, recordId) on RelatedRecordActionsContext. That is the record navigator the HOST publishes, and the same seam the grid's link column and the lookup cells already read. The hook still builds no record URL for page. A supplied onNavigate still wins, so the grid keeps its explicit wiring. An onRowClick still takes the click before either.
  • @object-ui/app-shell — PageView publishes the console's record navigator to the blocks on a custom page. This is the producer-side half (see "Where the fallback lives" below). It lives in the new module views/pageRecordActions.ts. pageRecordActionsValue addresses /apps/APP/OBJECT/record/ID for the objects the console routes to. resolve returns no related-list handlers, on the model of the list page's listRecordActionsValue.
  • @object-ui/plugin-kanban / @object-ui/plugin-calendar. No renderer code changes: both already hand the hook their navigation and object name. The registrations' navigation input descriptions and docblocks retire the "page warning". They now say where page goes: through the host's record navigator, and nowhere under a host that publishes none. The READMEs, content/docs/plugins/plugin-kanban.mdx, plugin-calendar.mdx and content/docs/api/schema-reference.md follow.
  • Pins. kanbanNavigationMembers-8652 and calendarNavigationMembers-8652 re-point their two "page warning" rows. Under a host that publishes openRecord, mode: 'page' and a mode-less { size: 'lg' } each call it with (object, '1'), with no overlay and no window.open. Beside them: drawer under the same host is the lit control, a parent onRowClick takes the page click first (precedence control), and with no host navigator page opens nothing (the description's remaining claim). The parity ledger's two pins summaries follow.
  • New pins. useNavigationOverlay.hostRecordNavigator-11293 (react hooks) pins seven rows: page; mode-less; onNavigate wins; onRowClick wins; overlay lit control; absent navigation unchanged; no host or no object name. PageView.recordNavigator-11293 (app-shell) pins that a block on the page reads a navigator, openRecord routes through the router, an unroutable object opens nothing, and resolve yields no handlers.

Zone 2 measurements (dispatch order)

  1. Red reproduced first, at the mount (test edits only, fix not yet written). Run: pnpm exec vitest run on the two NavigationMembers-8652 files. Result: Tests 4 failed | 27 passed (31). The 4 red rows are exactly page and mode-less on each block: expected "vi.fn()" to be called 1 times, but got 0 times. The drawer lit control, the parent-onRowClick control and the no-host row were green.
  2. How the grid wires it — premise partly falsified. ObjectGrid has no record navigator of its own. Its hook call passes onNavigate: onNavigate ?? schema.onNavigate, which is host-supplied only (the console's ObjectView, ObjectDataPage and InterfaceListPage pass router closures). The only context navigator the grid reads is RelatedRecordActionsContext.recordHref / openRecord, for its link column. Measured with a throwaway probe, deleted before commit: a standalone object-grid with navigation: { mode: 'page' } and no onNavigate produced open: 0, openRecord: [], pushState: 0, dialog: false, with and without a provider. So "as the grid does" holds only when a host supplies the navigator, and the grid had the same dead click. The fix closes it for the grid too, and for the gallery, map, timeline and tree.
  3. Where the fallback lives: the shared hook. @object-ui/react owns both the hook and RelatedRecordActionsContext, so reading the context adds no dependency. The hook therefore owns the page fallback, and the grid keeps its explicit wiring. The order's "no" branch (both renderers pass onNavigate the way the grid does) would not reach the standalone case: a block on a custom page is a document, nothing passes it a function, and the calendar's onNavigate prop already means date navigation. One measured gap remained: PageView, the host of standalone blocks on custom pages, published no navigator. The record page (RelatedRecordActionsBridge) and the list page (listRecordActionsValue) did. So the producer-side half lands here too, as the order allows ("if the real producer is elsewhere, fix it at the producer").
  4. "Page warning" rows retired in both registrations' inputs and docblocks. Their pins are re-pointed as described above.
  5. A host that passes onRowClick keeps its behaviour. This is pinned at both mounts under a host navigator, and in the hook test.

Boundaries, stated

  • An absent navigation is unchanged. Only an AUTHORED block that resolves to page reads the published navigator, and that limit is load-bearing. A host withholds navigation by passing neither callback: RelatedList renders mobile rows as an object-gallery with no navigation, and with onRowClick undefined when the bridge withheld onView. Reading the navigator on the absent-key path would open those records anyway. The hook test pins this row. {} and no config still agree whenever an onNavigate is supplied (the existing modeDefault rows stay green).
  • Modifier clicks and new_window are untouched.
  • A side effect of publishing the navigator on custom pages: the grid's link column and the lookup cells on a custom page now render real anchors, as they already do on the record and list pages. The @object-ui/app-shell changeset declares it.

Tests (final runs on merged head 698710c094; main merged at cd5b19a7e7)

  • Build: pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-kanban^...' --filter '@object-ui/plugin-calendar^...' --filter '@object-ui/app-shell^...' build. Then type-check: pnpm --filter @object-ui/react --filter @object-ui/plugin-kanban --filter @object-ui/plugin-calendar --filter @object-ui/app-shell type-check. Each package ran tsc --noEmit && tsc -p tsconfig.test.json. Result: type-check: Done ×4, VERDICT command-exit 0. --listFiles on each tsconfig.test.json shows the touched test files are inside its scope.
  • pnpm exec vitest run --maxWorkers=2 packages/plugin-kanban/ packages/plugin-calendar/ packages/react/src/hooks/ apps/console/src/__tests__/registry-inputs-spec-parity.test.ts. Result: Test Files 160 passed (160), Tests 1436 passed | 95 skipped (1531).
  • One run covered two sets. (a) Every suite git grep -l finds for useNavigationOverlay or the 8652 pins, plus every suite that mounts RelatedRecordActionsProvider / RelatedRecordActionsBridge / listRecordActionsValue (58 files). (b) The app-shell views top-level suites, views/__tests__, and the suites that read PageView.tsx: the remount ratchet, page-key-read-census, vite-declared-lazy-views, page-dom-leak-whitelist-7933, residue-namespaces-3546 (183 files). Result: Test Files 231 passed (231), Tests 2292 passed | 9 skipped (2301).
  • Gates: pnpm check:new-line-citations (0 new citation(s)), pnpm check:control-bytes (OK), pnpm changeset:check (OK), node scripts/check-changeset-presence.mjs (OK, 4 changesets), check:vi-mock-override-shape / check:vi-mock-inherit / check:vi-mock-specifiers / check:test-path-roots (OK). node scripts/check-governed-queue-guard.mjs --test over the 19 paths: NOT GOVERNED.
  • ESLint, narrowed to the 10 changed .ts/.tsx files (--format json): 0 errors. The warnings are all pre-existing. PageView.tsx carries 5 no-explicit-any at base and 5 now, and the kanban/calendar index.tsx changes are string-only. The narrowing is a measurement, not a skip: eslint.config.js enables no type-aware linting (no parserOptions.project / projectService), and neither the config nor eslint-rules/ reads other files from disk. So this diff cannot move a verdict on an untouched file.
  • Reverse verification (ablation), on the committed fix (0919d4aa50; the merge touched neither ablated file), through the anchor-must-hit ablation-replace.mjs with a verified restore:
    • Hook: the openRecord(objectName, ...) call was replaced (anchor 1 to 0, blob changed). Result: Tests 6 failed | 32 passed (38), exactly the six page / mode-less rows (2 hook, 2 kanban, 2 calendar). Every control stayed green. Restored: blob equal to HEAD, git diff HEAD empty.
    • PageView: the provider value was replaced with null. Result: Tests 2 failed | 3 passed (5), the two mount rows. The pure builder rows stayed green. Restored: blob equal to HEAD.
    • The tests resolve @object-ui/react through the root vitest alias to src. The ablation therefore exercised source, and needed no rebuild.
  • NOT MEASURED: check:sdui-registration-pins, reason: it reads the console bundle (PREREQUISITE: build @object-ui/console first). check:readme-exports, reason: it needs every package's dist (the README edits are prose only, with no fenced imports). The console UI was not exercised in a browser.

Acceptance notes (observations, not filed)

  • The calendar's own onEventClick prop is not one of the hook's arms; only onRowClick is. So a React host that passes ONLY onEventClick, under a published navigator, now sees a page click navigate AND its callback fire. Measured producers of onEventClick on ObjectCalendar in this repo: zero (git grep), and the console passes none. Carrier: none.
  • The kanban/calendar drawer's "open full page" link uses deriveRecordPageHref, which derives from window.location. On a custom page (/apps/APP/page/NAME) it would derive /apps/APP/page/NAME/OBJECT/record/ID. This is a read-only inference, not measured in the console. Carrier: none.
  • The hook's new_window fallback with no onNavigate still opens an origin-rooted /OBJECT/record/ID, as the existing member pins assert. This PR leaves it untouched. Carrier: none.
  • The pending .changeset/8652-kanban-calendar-navigation.md still says page opens nothing. It ships in the same release as these changesets, which state the new behaviour. It is another card's release note, so it was left as written.

Session: https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ (domain:ui seat 1 dispatch, claim comment 5923202140).


Generated by Claude Code

claude added 5 commits October 1, 2026 02:33
… host's record navigator (objectui#11293)

`useNavigationOverlay`'s `page` branch called `onNavigate` and nothing
else, so a standalone object-kanban / object-calendar (and grid) with
`navigation: { mode: 'page' }`, or a block written without `mode`,
opened nothing. With no `onNavigate` supplied, the branch now calls the
`openRecord` the host publishes on `RelatedRecordActionsContext`, the
seam the grid's link column already reads. A supplied `onNavigate` and
an `onRowClick` keep precedence; an absent `navigation` is unchanged.

The kanban and calendar member pins re-point their `page` rows from the
retired warning to the navigation, under a host, with `drawer` as the
lit control and a parent `onRowClick` as the precedence control.

Claude-Session: https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ
Co-authored-by: Claude <noreply@anthropic.com>
…r to its blocks (objectui#11293)

A block on a custom page is rendered with no host callback, so an
object-kanban / object-calendar whose `navigation` resolves to `page`
had nowhere to go: the record page and the list page publish a record
navigator on `RelatedRecordActionsContext`, and `PageView` published
none. It now publishes one for the objects this console routes to,
`/apps/:app/:object/record/:id`, with no related-list handlers.

Claude-Session: https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ
Co-authored-by: Claude <noreply@anthropic.com>
…he `navigation` inputs now that `page` navigates (objectui#11293)

Both registrations' `navigation` descriptions, their docblocks, the two
READMEs, the plugin docs pages and the schema reference warned that
`page`, and a block without `mode`, open nothing on a board or calendar
no parent view navigates for. They now say where `page` goes: through
the record navigator the host publishes, and nowhere under a host that
publishes none. The parity ledger's pin summaries follow the re-pointed
rows. One changeset per released package changed.

Claude-Session: https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ
Co-authored-by: Claude <noreply@anthropic.com>
… own module (objectui#11293)

`pageRecordActionsValue` moves out of `PageView.tsx` into
`views/pageRecordActions.ts`, so the view module exports components
only, and the pin test drops its `any` reads.

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

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 20 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/11068-grid-declared-keys.md

  • names schema-reference.md → content/docs/api/schema-reference.md — edited by this change

    What was wrong. ObjectGridSchema declared eight keys the grid never read. An author who wrote description, emptyState, name, placeholder, rowSpecActions or bulkSpecActions on an object-grid got no type error, no validator refusal and no effect. The reference example in schema-reference.md taught description and showFilters as if they worked. It now authors only keys the grid reads.

.changeset/3917-retire-action-condition-branch.md

  • names content/docs/api/schema-reference.md → content/docs/api/schema-reference.md — edited by this change

    • ActionCondition is removed from @object-ui/types (and from the barrel export). - ActionSchema.condition is retyped to the predicate the runtime actually honours: boolean | string | { dialect?: string; source: string } — the same three arms ActionRunner's own ActionDef.condition carries, and the same vocabulary visible and disabled use. - ActionConditionSchema is removed from @object-ui/types/zod (and from the zod barrel); the condition key now validates against that predicate union. - The two teaching sites (content/docs/core/enhanced-actions.mdx Conditional Execution, content/docs/api/schema-reference.md ActionSchema table) are rewritten to the live vocabulary: condition is a gate; a branch is expressed as separate actions with mutually exclusive conditions.

.changeset/6170-retire-timeline-dead-keys.md

  • names content/docs/api/schema-reference.md → content/docs/api/schema-reference.md — edited by this change

    Also in this change: the in-repo example packages/types/examples/data-display-examples.json (its timeline node authored all three) is migrated to items / variant; the two content/docs/api/schema-reference.md snippets that authored events: [] now author items: []; and the plugin-timeline docs callout says retired rather than deprecated.

.changeset/6896-retire-chart-inline-data.md

  • names content/docs/api/schema-reference.md → content/docs/api/schema-reference.md — edited by this change

    ⚠️ One correction to the record the ruling rests on. The ruling states zero authorship of a populated series[].data outside tests across packages/ / apps/ / examples/. The re-measurement finds one such site inside those roots — packages/types/examples/data-display-examples.json (2 series) — plus four outside them, in documentation: content/docs/api/schema-reference.md (3) and content/docs/core/report-schema.mdx (1).

.changeset/6939-kanban-column-cards.md

  • names content/docs/api/schema-reference.md → content/docs/api/schema-reference.md — edited by this change

    Breaking, deliberately. KanbanColumn declared its card list as items in complex.ts and in the zod mirror complex.zod.ts. Every board reads cards. Measured on origin/main 78a3cc238: KanbanImpl.tsx reads .cards on 12 lines, KanbanEnhanced.tsx on 8, and bucketCardsIntoColumns twice more as col.cards || []; .items had zero read sites in either board (a same-shaped .title control on the same two files returns 8 and 3, so those zeros are readings and not a mis-shaped probe). Both catalog entries, the plugin docs and content/docs/api/schema-reference.md all author cards.

.changeset/6951-tree-view-data-retired.md

  • names content/docs/api/schema-reference.md → content/docs/api/schema-reference.md — edited by this change

    What was measured, on this branch's base. TreeViewSchema declared two spellings for its one inline-nodes slot — nodes (read second) and data (read third: boundData || schema.nodes || schema.data || [] at renderers/data-display/tree-view.tsx:105), both declared by objectui#6150. data had been REQUIRED until 777e5c6f4 (PR fix(types): tree-view mirror stops requiring the limb it reads third (group 2 of objectui#6939) #7533) made it optional, so this retirement starts from a declared-and-optional member on both faces. The in-repo corpus at the retirement: seven tree-view nodes under examples/schema-catalog and packages/types/examples plus one content/docs fence — six on nodes, two on data (packages/types/examples/data-display-examples.json and content/docs/api/schema-reference.md), both rewritten; no package source authored either spelling.

.changeset/7313-object-calendar-record-source.md

  • names content/docs/plugins/plugin-calendar.mdx → content/docs/plugins/plugin-calendar.mdx — edited by this change

    A widening. A node authoring staticData or data without objectName now validates (it always rendered — the read is resolveRecordSourceConfig(schema), keyed on the three). Every document that validated before still validates: objectName alone still parses, an empty one included, because presence is !== undefined. The one shape the refinement refuses (none of the three) was refused before too, at objectName. The two static-data examples in content/docs/plugins/plugin-calendar.mdx are now annotated ObjectCalendarSchema and compile under the doc-snippet gate.

.changeset/7664-kanban-arm-plugin-dialect.md

  • names content/docs/api/schema-reference.md → content/docs/api/schema-reference.md — edited by this change

    Migration. Author boards in the plugin dialect — objectName + groupBy for an object-bound board, or columns[].cards[] with badges for a static one. Replace DeclarativeKanbanSchema imports with KanbanSchema (from @object-ui/types, or the Zod KanbanSchema from @object-ui/types/zod; @object-ui/plugin-kanban re-exports the same KanbanSchema type). Delete draggable (drag-and-drop is always on) and column color (style a lane through className). content/docs/api/schema-reference.md's kanban section now documents this dialect.

.changeset/7997-detail-view-related-retired.md

  • names content/docs/api/schema-reference.md → content/docs/api/schema-reference.md — edited by this change

    Documentation. packages/plugin-detail/README.md and content/docs/api/schema-reference.md stop teaching the retired array and gain a migration block each.

.changeset/8067-component-input-member-kind.md

  • names apps/console/src/__tests__/registry-inputs-spec-parity.test.ts → apps/console/src/__tests__/registry-inputs-spec-parity.test.ts — edited by this change

    A registration's type: 'array' said a value was a list and stopped there, so a member that drifted from @objectstack/spec was invisible to every layer that reads a declaration. page:header.actions is the measured cost: the contract declares z.array(z.string()) ("Action IDs"), the renderer read the members as ActionDef objects, and the repo-wide parity gate in apps/console/src/__tests__/registry-inputs-spec-parity.test.ts stayed green for the whole life of the drift because both sides carried the key and neither could say what was inside it. What settled it was a maintainer ruling, not a test — and even after the fix, "these are ids" survived only as English in the registration's description.

.changeset/8268-testid-emitted-as-data-testid.md

  • names content/docs/api/schema-reference.md → content/docs/api/schema-reference.md — edited by this change

    The other direction — retiring the promise — was considered and declined. It is what c1fe272ad did for BaseSchema.hidden, but that key had a working behaviour to describe and zero named consumers, and the ruling's decline turned on exactly that. This promise already has carriers outside the type declaration: content/docs/api/schema-reference.md states it as a table row and authors testId in that page's own base-schema example, @object-ui/cli's OBJECTUI_STRUCTURAL_KEYS identifies a file as an ObjectUI schema node by this key, at this change ObjectGridSlotKey / ObjectFormSlotKey pin it, SchemaBuilder.testId() writes it, and ADR-0054 C4 — shipped — reads "the renderer emits data-testid … derived from metadata".

.changeset/8466-calendar-color-allday-fields.md

  • names packages/plugin-calendar/README.md → packages/plugin-calendar/README.md — edited by this change

    ObjectCalendar.tsx's getCalendarConfig reads FIVE flat keys off the node, and packages/plugin-calendar/README.md teaches all five in one sentence — "point titleField / startDateField / endDateField / allDayField / colorField at your own fields when they differ." Only three of the five were declared. The other two reached the renderer through BaseSchema's [key: string]: any on the TypeScript face and its .passthrough() on the zod mirror: admitted, never examined. A misspelling therefore left the calendar silently colourless while every published gate passed.

.changeset/8653-listview-title-retired-rowactiondefs-pinned.md

  • names content/docs/api/schema-reference.md → content/docs/api/schema-reference.md — edited by this change

    title — retired. ListView resolved its export filename through schema.label || (schema as any).title. @objectstack/spec/ui's ListViewSchema refuses title by name (unrecognized_keys: ['title']) while ObjectGridPropsSchema accepts it; packages/types mirrors the platform contract rather than ruling over it, so declaring title on ListViewSchema would have made this repo accept what the platform save gate rejects. That asymmetry is also why objectui#6639 could take the declare branch for ObjectGridSchema.title one package over and this site could not. A parse-based census of apps/ examples/ content/ and packages/ found zero list-view nodes authoring title, so the retirement costs no author a filename. Over that same corpus the instrument reports three object-grid nodes carrying the key: two authored ones, both in content/docs/api/schema-reference.md, plus one that is not authored at all — packages/plugin-view/src/ObjectView.tsx composes title: schema.table?.title onto a grid node it builds, so it is a producer writing the key rather than an author declaring it. ObjectGrid's own title reads are untouched — they remain declared, ruled and read.

.changeset/8801-object-kanban-allow-collapse-retired.md

  • names content/docs/api/schema-reference.md → content/docs/api/schema-reference.md — edited by this change

    • the declarations retired here — packages/types/src/objectql.ts and its mirror packages/types/src/zod/objectql.zod.ts; - the pins that assert the retirement — object-kanban-allow-collapse-retired-8801.test.ts and bare-kanban-node-key-retired-8802.test.ts; - a comment in packages/types/src/zod/complex.zod.ts, recording that the deleted retiredZeroReadKanbanKey helper once carried this spelling on the SIBLING arm; - one row of content/docs/api/schema-reference.md; - the .changeset/ release notes that discuss it — this one, the two historical entries covering the sibling arm's own spelling, and objectui#9629's note recording the correction to this paragraph.

.changeset/8802-8257-8008-kanban-gantt-family-retirement.md

  • names content/docs/plugins/plugin-kanban.mdx → content/docs/plugins/plugin-kanban.mdx — edited by this change

    The diagnostic's remedy text moves from a tag to a component. It used to end "render ANGLE-BRACKETS(kanban-ui) from a React host that passes onQuickAdd". That sentence is falsified by this change: kanban-ui is no longer a node type key, so a page written to the old advice draws unknown-component. It now names KanbanRenderer from @object-ui/plugin-kanban — still exported, still forwarding both halves by identity — which is the surviving way to get the pair. content/docs/plugins/plugin-kanban.mdx says the same thing the same way.

.changeset/9187-record-highlights-layout-two-values.md

  • names content/docs/api/schema-reference.md → content/docs/api/schema-reference.md — edited by this change

    ⚠️ The census behind this narrowing covers this repository only, and it found no in-repo authoring to migrate: every in-tree layout: 'grid' belongs to a different component (detail-view in content/docs/api/schema-reference.md and phase2-schemas.test.ts, ai-recommendations in packages/plugin-ai/README.md), and the one in-repo consumer of this interface that writes a layout (p1-spec-alignment.test.ts) writes 'horizontal'. So no document in this repository stops type-checking. A TypeScript consumer outside this repo that wrote grid is not observable from here and gets a compile error (TS2322) naming the key — which is why the FROM/TO is spelled out above.

.changeset/calendar-view-schema-converge.md

  • names content/docs/api/schema-reference.md → content/docs/api/schema-reference.md — edited by this change

    Runtime renderer behaviour is unchanged. @object-ui/plugin-calendar's README and content/docs/api/schema-reference.md are repaired to the converged surface in the same change, so no copy of the old contradiction survives.

.changeset/issue-5373-retire-crud-schema.md

  • names content/docs/api/schema-reference.md → content/docs/api/schema-reference.md — edited by this change

    crud had four declaration faces and no registered renderer, for the whole life of the key: the TS interface (packages/types/src/crud.ts), the zod mirror (packages/types/src/zod/crud.zod.ts), a dedicated branch in validateSchema that affirmatively PASSED it, and CRUDBuilder in @object-ui/core. A node spelling it painted the OBJUI-001 "Unknown component type" panel, and content/docs/api/schema-reference.md published it as reference material — so a reader (or an AI author) who copied the page got a red panel.

  • names api/schema-reference.md → content/docs/api/schema-reference.md — edited by this change

    Authoring crud is now REFUSED BY NAME rather than passed or silently ignored. validateSchema returns an error with code: 'RETIRED_TYPE' on schema.type — at any depth, since it is what validateChildren recurses with — so assertValidSchema throws and isValidSchema answers false. The message names the migration: object-grid for the record table with its toolbar, filters, pagination and row/batch actions, object-form for the create/edit form, and detail for the record view. api/schema-reference.md is rewritten around those shapes.

.changeset/object-view-unmirrored-keys-7779.md

  • names content/docs/api/schema-reference.md → content/docs/api/schema-reference.md — edited by this change

    listViews stays unmirrored, on the ruling's own fallback clause. The declaration's value is the local NamedListView — 64 declared top-level members (⚠️ re-taken at objectui#8980, which added the seventeen the protocol declares on this surface to the 47 this entry first measured), of which the renderer reads 21 off a named view. data is one of the 21 now: it used to reach the renderer through an as any cast on the named-view config in packages/plugin-view/src/ObjectView.tsx and be declared nowhere, and the objectui#8980 ruling declared it by name — objectui#7928's open half, answered. The spec's ViewSchema.listViews is a record of the STRICT ObjectListViewSchema, which requires columns and refuses options, ObjectQL tuple filters and default — that is, it refused the named views this package's own README and content/docs/api/schema-reference.md taught when this entry was written ({ label: 'All Users' } fails at columns; filter: [["owner", "=", "..."]] fails at filter.0), and objectui#8255 has since rewritten them in the spec shape. Mirroring the spec value would have lost documented behaviour; mirroring the local value would enforce 43 unread members (64 declared, minus the 21 that are both declared and read) into the contract — the very thing ruling B refused for the six local keys. The key therefore stays in the parity ledger with that measurement, pinned, until the maintainer decides its value type. It is not papered over with z.any().

.changeset/table-renderer-declared-column-contract-5350.md

  • names content/docs/api/schema-reference.md → content/docs/api/schema-reference.md — edited by this change

    What makes this site different from its three siblings is that the alias was not merely tolerated, it was published. content/docs/api/schema-reference.md §TableSchema shipped a copyable { "name": "id", "label": "#" } example and a property row reading "Column definitions with name, label, …", while packages/types declared the opposite pair. Docs and type disagreed about one type they both call TableColumn, each internally consistent. Retiring the alias without correcting the page would have turned a documented, working example into a silently broken one, so both halves land together: the page now authors accessorKey/header. The same row also advertised a render property that TableColumn has never declared — the renderer's hook is cell — and that claim is dropped rather than re-spelled.

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.

Angle-bracketed names in the quoted prose above are rewritten as ANGLE-BRACKETS(name): GitHub deletes tag-shaped fragments from a stored body, and a quote that silently loses the identifier it is about is worse than a visible repair.

Compared the checked-out tree with bef9f204a (merge-base with origin/main): 15 file(s) changed outside .changeset/, read against 1897 pending declaration(s) that publish a body (2510 pending in total). · run

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3585.8 KB 3607.4 KB
Main entry chunk (gzip) 150.4 KB 350 KB
Entry file index-DmJjtzDf.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) 571.60KB 136.88KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 229.96KB 63.80KB
fields (index.js) 261.05KB 66.23KB
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.49KB 9.23KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 40.95KB 11.48KB
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) 53.07KB 15.41KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 139.34KB 37.30KB
plugin-designer (index.js) 216.45KB 44.60KB
plugin-detail (index.js) 244.79KB 64.47KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 173.70KB 44.63KB
plugin-gantt (index.js) 173.03KB 43.07KB
plugin-grid (index.js) 231.61KB 63.61KB
plugin-kanban (index.js) 49.49KB 15.54KB
plugin-list (index.js) 116.63KB 28.97KB
plugin-map (index.js) 23.54KB 7.83KB
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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 21.59KB 7.71KB
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

…hing, now that it navigates (objectui#11293)

The pending `8652-kanban-calendar-navigation` changeset ships in the
same release as this PR's four changesets, and its last paragraph said
`page`, and a block without `mode`, open nothing on a board or calendar
no parent view navigates for. That went false with this PR. The passage
now states what is true: they open the record page through the record
navigator the host publishes, and still open nothing under a host that
publishes none. Prose only; the frontmatter and every other line are
unchanged.

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

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3585.8 KB 3607.4 KB
Main entry chunk (gzip) 150.4 KB 350 KB
Entry file index-DmJjtzDf.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) 571.60KB 136.88KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 229.96KB 63.80KB
fields (index.js) 261.05KB 66.23KB
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.49KB 9.23KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 40.95KB 11.48KB
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) 53.07KB 15.41KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 139.34KB 37.30KB
plugin-designer (index.js) 216.45KB 44.60KB
plugin-detail (index.js) 244.79KB 64.47KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 173.70KB 44.63KB
plugin-gantt (index.js) 173.03KB 43.07KB
plugin-grid (index.js) 231.61KB 63.61KB
plugin-kanban (index.js) 49.49KB 15.54KB
plugin-list (index.js) 116.63KB 28.97KB
plugin-map (index.js) 23.54KB 7.83KB
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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 21.59KB 7.71KB
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: 852d32f164fa0d81335df137c9ef7219391ecf5e
Local-runs: none

Inputs: card objectui#11293 (body, triage 5922527918, claim 5923202140, dev reports 5924179311 and 5924207947), PR objectui#11323 (body, 20-file list, the changeset-claim-re-read bot comment), the net diff cd5b19a7e7..852d32f164 (20 files, +672/−109, merge-base confirmed), the check-runs on the head (read twice), and PR objectui#11292's body for the 8652 pins. Read-only throughout: nothing built, run or ablated.

① Derived judgments

  1. @object-ui/react — useNavigationOverlay's page fallback. Right. The branch now reads useRelatedRecordActions()?.openRecord (same package, re-exported through context/index.ts) and, with no onNavigate, calls openRecord(objectName, recordId); it builds no URL. Every arm above it is byte-for-byte the old order: onRowClick first, modifier click, an ABSENT navigation (still onNavigate or nothing), none / preventNavigation, new_window / openNewTab, then page; a supplied onNavigate still wins. This is triage's "the shared hook owns the page fallback for a host with no onNavigate", with no per-renderer copy and no narrowing of the spec's mode set. The premise measurement holds on the source: the grid's call site passes onNavigate: onNavigate ?? schema.onNavigate (host-supplied only) and reads the context solely for its link column; kanban passes onRowClick: externalClick and calendar onRowClick: navIsOverlay ? undefined : onRowClick, neither an onNavigate. So the two blocks need no renderer edit, as the diff has it.

    • Accept-set the fallback reaches, named so the seat holds it: any hook consumer with an AUTHORED navigation resolving to page, no onNavigate, no onRowClick, under a published navigator — kanban, calendar, grid, gallery, map, timeline, tree, and ListView when schema.onNavigate is absent. Gantt supplies its own onNavigate and the console's three list surfaces (InterfaceListPage, ObjectDataPage, app-shell ObjectView) pass router closures: unchanged. The react changeset names kanban, calendar and grid and covers the rest generically ("a block a host rendered without wiring one"). Right.
    • The card's literal repro shape (a bare mount, no provider) still opens nothing, and is pinned as the description's remaining claim. Right: a bare mount has no record page to open without a hand-built URL, which the direction excludes; the console door for a standalone block is the custom page, answered by item 2.
  2. @object-ui/app-shell — PageView publishes a record navigator. Inside the direction, not a widening. pageRecordActionsValue(appName, objects, navigate) wraps the SchemaRenderer branch only (the InterfaceListPage branch keeps its own onNavigate); recordHref answers /apps/APP/OBJECT/record/ID for an object in the app's metadata and null otherwise, openRecord navigates only on a non-null href, resolve returns a frozen empty handler set. That is the route the console mounts (AppContent.tsx line 1110, :objectName/record/:recordId under the app root) and the record page's own builder shape minus the ?from= trail a page has no parent record to carry. The hook's fallback needs a host-published navigator and the custom page — the only console surface where a standalone block lives — published none; a page-only seam would have been a second copy of the rule. The declared side effect (the grid's link column and the lookup cells render real anchors on custom pages) is inherent to reusing the one seam and is disclosed in the app-shell changeset and the PR body. Acceptable. pageRecordActionsValue is not on app-shell's public index (views/index.ts does not export it); PageView is, and its behaviour changed, so the changeset is owed and present. Not memoised: resolve's returned identity is a module constant, so the context's stability contract on resolve holds; the value's own identity changes per render as the schema it wraps already does.

  3. "Page warning" retirement. Right, complete. OBJECT_KANBAN_INPUTS.navigation and OBJECT_CALENDAR_INPUTS.navigation (spread into both the object-calendar and calendar registrations) drop the warning and state the host-navigator route and the no-host case; both docblocks, both READMEs, plugin-kanban.mdx, plugin-calendar.mdx and the kanban row of schema-reference.md say the same. A grep of the head for "opens nothing on a board / calendar" and "no parent view navigates for" returns only the two 11293 changesets' past-tense sentences. The parity ledger's two pins summaries follow the re-pointed rows.

  4. Pins. Sound. Each 8652 file's page describe is now five rows: the lit drawer control under the host, page, mode-less { size: 'lg' }, a parent onRowClick taking the click first, and the no-host row. Row counts at head: kanban 15 + calendar 16 = 31, hook pin 7, PageView pin 5 — the dev's red-first "4 of 31", ablation (1) "6 of 38" and ablation (2) "2 of 5" are arithmetically those files. git diff 0919d4aa50 698710c094 on the two ablated paths is empty, so ablating on the fix commit read the same source as the merged head. Ablation (1) reddens exactly the six page / mode-less rows with every control green; ablation (2) reddens exactly the two mount rows. The hook pin's bare-mount row asserts only isOpen (no spy); the window.open spy sits on the mount-level no-host rows, where the claim is measurable.

  5. The 8652 changeset's corrected sentence is true. Kanban and calendar pass no onNavigate, so an authored page click goes to the published navigator or nowhere, and the descriptions now say so. Its lead sentence stays 8652's own statement about 8652; read in the same release it does not contradict the correction. Of the bot's 20 named changesets, I read each quoted claim against the only passages this PR edits in the named files (one navigation row, one navigation paragraph per doc, two pins strings): none is about those passages, so none goes false. A grep of all 2502 pending bodies at head for the falsifiable shapes (opens nothing, no parent view navigates, PageView, mode: 'page') finds only gantt notes (gantt supplies its own onNavigate), object-view-unmirrored-keys-7779's authoring advice (still true), and PageView notes on keying, context and page kind (untouched by the provider wrap). None goes false.

  6. CI on the head. Second read: 29 success, 3 skipped, 0 failure; 10 not concluded — Test shards 1 to 8, Type Check, Spec Main Shape Gate. They are not treated as pass or fail here; the seat reads them before merge, and a red among them re-opens this record. The two gates the dev could not run locally are answered by CI: check:sdui-registration-pins runs in the performance-budget workflow (Bundle Analysis, success) and check:readme-exports in README Export Check (success). The earlier Test failure on 698710c094 was the roll-up over shards the patch push cancelled and is not a verdict on this head.

② Semver level

Four minor changesets — @object-ui/react, @object-ui/app-shell, @object-ui/plugin-kanban, @object-ui/plugin-calendar — match what the diff publishes: new behaviour on a public hook, a public view that now publishes a navigator, and two blocks that honour a declared spec mode with their designer-facing inputs text rewritten. AGENTS.md's version policy forbids major and lets minor/patch evolve independently; the 8652 changeset in the same release carries the same plugin packages at minor. The fifth released package with a changed file, @object-ui/console, changed only the parity ledger (a test): no changeset owed. @object-ui/types is untouched, rightly — no type face moves. Changeset Declaration, Fixed Group Check, Bump Policy and Overwrite Report are green on the head. Clause-②: yes is right: public behaviour and published inputs descriptions change.

③ Boundary flags

open_questions: none in either report. Dev flags, each answered:

  • D1, surface widened to the producer (PageView, pageRecordActions.ts). Accepted in ① item 2.
  • D2, renderer files unchanged. Right: a renderer edit would have been the per-renderer copy triage excluded.
  • D3, ablations on the pre-merge fix commit. Verified: the merge touched neither ablated file.
  • D4, commit trailers per objectui AGENTS.md. A repo rule, not a contract item; noted.
  • NOT MEASURED gates (sdui-registration-pins, readme-exports). Answered by CI, ① item 6.
  • F1, a calendar host passing only onEventClick. Confirmed in ObjectCalendar.tsx: handleClick always runs, then onEventClick when the mode is not an overlay, so under a published navigator a page click now navigates and fires the callback. Reach: zero in-repo producers (my grep finds only the separate calendar-view renderer and type declarations; the console passes none). Nothing to file; the Acceptance note is the right carrier. If an outside host surfaces, the fix belongs in the calendar's own precedence (hand onEventClick to the hook as onRowClick), not the hook.
  • F2, the drawer's "open full page" href on a custom page. deriveRecordPageHref (RecordDetailDrawer.tsx lines 196 to 204) strips at the first /OBJECT marker of window.location.pathname; on /apps/APP/page/NAME the marker is absent, so it yields /apps/APP/page/NAME/OBJECT/record/ID, which no console route matches. Pre-existing, untouched by this PR, deterministic from the source but not measured at a mount. Acceptance note is right for this PR; fileable by the seat after one mount-level measurement (drawer mode, location set to a page path). This PR's seam gives that fix a host-published recordHref to prefer over the location-derived one.
  • F3, the new_window fallback's origin-rooted /OBJECT/record/ID. Pre-existing, asserted by the 8652 new_window rows, untouched here. Whether it routes in the console is a route-table question nobody has measured; Acceptance note, not blocking.
  • F4, the pending 8652 changeset. Resolved in patch round 1 and judged true in ① item 5.

Implemented-by: claude/issue-11293-standalone-page-navigation
Reviewed-by: session_0122Knsowci76D2rBWReCzzZ

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

CI red on the reviewed head: Test (shard 2/8) (new signature, not requeued)

Head: 852d32f164fa0d81335df137c9ef7219391ecf5e. The other 7 shards and every other check are green. The landing is held: the seat's ACCEPT was not posted and nothing is queued.

Signature (job 110206411237, read at 2026-10-01T03:48Z):

  • Failing step: Test (shard 2/8) → FAIL packages/app-shell/src/views/studio-design/StudioDesignSurface.canvasFrame.test.tsx > Interfaces canvas — one frame around a preview (objectui#8219) > a canvas state with no preview shell keeps the wrapper frame.
  • First error: TestingLibraryElementError: Unable to find an element with the text: /cannot be previewed or designed here/. This is the findByText(…, { timeout: 4000 }) timing out, ⛔ not a failed assertion.
  • DOM when the wait ran out: the breadcrumb and caption read Sales report · Report, so the click on the report leaf registered. The canvas body is still the Loading… spinner, i.e. the loading || !draftLoaded arm of StudioDesignSurface.tsx.
  • The shard was slow: setup 241 s, import 1402 s, run 724 s.
  • This file has no green run on any head of this PR. The previous head 698710c094's shards were cancelled by the push of 852d32f.

First read: most likely a load-timing race outside this diff, ⛔ not yet established.

  • The diff does not touch studio-design/.
  • Of the three facts that would allow a single requeue, only "first error is a timeout" holds so far.
  • Whether the failing file's import closure meets the diff is not yet measured. useNavigationOverlay.ts reaches it through the @object-ui/react barrel.
  • The same file passing at this shard on the base is not yet shown.

Next: the dev is measuring three things:

  • the import closure;
  • pass/fail counts for the file on this head and on origin/main;
  • the race itself, with ⛔ no timeout raise and ⛔ no heavyDomTests entry.

If the diff causes it, the fix lands in this PR. If it does not, a card is filed for the race and this PR waits for a fresh run. ⛔ No empty commit, and ⛔ no merge of main only to re-run. This seat has no means to re-run jobs.

domain:ui seat 1 · session_0122Knsowci76D2rBWReCzzZ · 2026-10-01T03:55Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Test (shard 2/8): not this PR's. The fix is objectui#11331, and this PR takes it from main

This follows up 5924419293. The dev's patch round 2 report on objectui#11293 has the measurements:

  • The race lies wholly in StudioDesignSurface.tsx, which this PR does not change.
    • The InterfacesPillar draft-load effect raises loading for the auto-selected first leaf.
    • Opening a leaf with no designer cancels that load, then takes the !current || !isEditable early branch, which never clears loading.
    • The canvas stays on Loading…, which is exactly the CI DOM.
  • A deterministic gated probe reproduces it on main be52115224 and on this head, with byte-identical output. Repeated runs of the file pass at rest (20/20 on each) and under 6-way load (24/24 on each).
  • Import closure: this diff's useNavigationOverlay.ts is in the file's module set, through the @object-ui/react barrel, but it is not on the failing path. PageView.tsx and pageRecordActions.ts are not in the closure.

Disposition:

  • ⛔ No requeue and no re-run: the import closure meets the diff, and this seat has no means to re-run jobs.
  • The race is filed as objectui#11331, a sub-issue of objectui#11293. It is claimed (5925206749) and dispatched now. Its fix covers the measured site and the two sibling pillars with the same shape.
  • When objectui#11331 lands, this branch merges main in one ordinary commit (a merged fix that the base lacks), CI re-runs on that commit, and the landing resumes. This PR's reviewed diff does not change: the contract review 5924325860 stays on record, and the seat compares the new head's diff against 852d32f.

domain:ui seat 1 · session_0122Knsowci76D2rBWReCzzZ · 2026-10-01T05:15Z

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3585.7 KB 3607.4 KB
Main entry chunk (gzip) 150.4 KB 350 KB
Entry file index-DneNWJPF.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) 571.28KB 136.83KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 229.96KB 63.80KB
fields (index.js) 261.05KB 66.23KB
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.49KB 9.23KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 40.55KB 11.41KB
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) 53.15KB 15.45KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 139.34KB 37.30KB
plugin-designer (index.js) 216.54KB 44.64KB
plugin-detail (index.js) 244.79KB 64.47KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 173.70KB 44.63KB
plugin-gantt (index.js) 173.11KB 43.10KB
plugin-grid (index.js) 231.53KB 63.58KB
plugin-kanban (index.js) 49.49KB 15.54KB
plugin-list (index.js) 116.63KB 28.97KB
plugin-map (index.js) 23.54KB 7.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 33.91KB 10.00KB
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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 21.59KB 7.71KB
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
objectstack-fleet Bot marked this pull request as ready for review October 1, 2026 07:35
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 1, 2026 07:35
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit 2124d04 Oct 1, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11293-standalone-page-navigation branch October 1, 2026 07:50
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
…es the canvas on "Loading…" (objectui#11331) (objectstack-ai#11338)

Fixes objectstack-ai#11331
Clause-②: no

A Studio pillar whose draft load is cancelled, with no load to replace
it, no longer leaves its canvas on "Loading…". The Interfaces race
removed here is the one that keeps `Test (shard 2/8)` red on PR
objectstack-ai#11323
(`StudioDesignSurface.canvasFrame.test.tsx`, "a canvas state with no
preview shell keeps the wrapper frame"). That file is unchanged here. It
goes green because the race is gone, not because it waits longer.

## The rule, one shape for the three draft loads

Each draft-load effect (`InterfacesPillar`, `DataPillar`,
`AutomationsPillar` in `StudioDesignSurface.tsx`) raised `loading` as
its load started. Only the load's own `finally` lowered it, behind `if
(!cancelled)`. **The rule now:** the cleanup of a run whose load has not
settled takes back what that run's start claimed. Each run keeps a local
`settled` flag, which its `finally` sets. Its cleanup runs `if
(!settled)` and does the following:

- **all three pillars:** `setLoading(false)`.
- **Data only:** it also releases the load-once claim
(`loadedNameRef.current = null`). This is the same rule applied to a
second claim, the only other claim a run's start makes. The Data run's
start claims two things: `loading` and `loadedNameRef`. The measurement
below shows that lowering `loading` alone leaves the Data case stuck.

**Why it cannot raise a second spinner or flash stale content:** when a
replacement load follows, the old run's cleanup `setLoading(false)` and
the new run's `setLoading(true)` happen in the same passive-effect
flush, so React batches them into one render. Even without that
batching, the canvas's `loading || !draftLoaded` arm (or `!objLoaded` in
Data) keeps the new item on the spinner until its own load is installed,
because the objectui#11272 buffer stamp names the item it was loaded
for. The stamping (`draftFor`, `ifDirty`, `hasDraft`, the `!draftLoaded`
arm and its `error && !loading ? null` rule) is unchanged, and the
canvas branch order is unchanged.

## What was measured (Zone 2)

1. **Interfaces, red first on `origin/main` `5262f7dd3f`.** The gated
probe is now a committed pin. It mounts the real `InterfacesPillar` with
`canvasFrame.test.tsx`'s mocks, holds the dashboard leaf's `layered`
read on a gate, and clicks the report leaf while that read is in flight.
On `5262f7dd3f` the race row fails with the DOM caption `Sales report`
over a canvas body that reads `Loading…`. A scratch probe asserted only
the issue's "after the gate opens" column, and on the ablated tree it
fails the same way after the gate opened. The control row, a click after
the dashboard grid rendered, shows "cannot be previewed or designed
here" on both trees.
2. **Data: reached through the component.** The pin re-renders the
pillar with a new client object between the start of the load and its
release. On `5262f7dd3f` the object stays on `Loading…` after the gate
opens. The cause is `!objLoaded`, not `loading`. The cancelled load
installed nothing, and the re-run bailed on `loadedNameRef.current ===
loadKey`. **Lowering `loading` alone does not fix this site** (ablation
leg D1 below: the race row stays red). Releasing the unsettled claim is
what fixes it. A claim whose load settled is kept, and a pin row says
so: a new client after the object loaded issues no second `layered`
read.
3. **Automations: not reachable through the component, so it has no
row.** Its effect returns early only on `!current`, and the pillar never
sets `current` back to null once a flow is open. Its three `setCurrent`
writes are the list effect's `c ?? deepLinked ?? items[0] ?? null`, a
create and a rail click, all of them non-null once a flow is open. So a
cancelled flow load always has a replacement. On `!current` the canvas's
`!current` branch comes before the loading arm, so no spinner is visible
with no leaf. That is a reading of the source; nothing in the tree
re-derives it. The cleanup takes the same shape here as at the other two
sites.
4. **One shape:** the `settled`-gated cleanup above, chosen over
clearing in every early-return branch. A branch-by-branch clear covers
only the early returns that exist today, and it does not cover the Data
bail at all. The cleanup is the one place every cancelled run passes
through.

## Pins: `StudioDesignSurface.cancelledLoad-11331.test.tsx`

| row | `5262f7dd3f` (no fix) | this head |
|:--|:--|:--|
| Interfaces race: report leaf clicked while the dashboard load is in
flight | **red** (`Loading…` under the `Sales report` caption) | green |
| Interfaces control: the same click after the dashboard rendered |
green | green |
| Data race: a new client while the object load is in flight | **red**
(`Loading…`, no `Add field`) | green |
| Data control: the same gated load with no new client | green | green |
| Data guard: a new client after the object loaded reads nothing again |
green | green |

## Ablation (fix committed first, each leg mutated through
`ablation-replace.mjs`, restore proven by blob hash and an empty `git
diff HEAD`)

| leg | mutation | red rows | green rows |
|:--|:--|:--|:--|
| I | Interfaces cleanup line removed | Interfaces race, and the scratch
probe's after-gate race row | the other 4 pin rows, and the probe's
control |
| D1 | Data keeps only `setLoading(false)`, so the claim is not released
| Data race | the other 4 pin rows, and both probe rows |
| D3 | Data releases the claim even when the load settled | Data guard
(`layered` read twice) | the other 4 pin rows, and both probe rows |

## Local verification (head `7b70a05236`)

- `pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...'
build`: exit 0. Then `pnpm --filter @object-ui/app-shell type-check`
(`tsc --noEmit && tsc -p tsconfig.test.json`): exit 0. `--listFiles`
shows the test project compiles the new pin.
- `pnpm exec vitest run packages/app-shell/src/views/studio-design/`:
`Test Files 85 passed (85)`, `Tests 496 passed (496)`.
- Repeated runs, one vitest process each: the new file passed 10 of 10
runs and `StudioDesignSurface.canvasFrame.test.tsx` passed 10 of 10.
- Gates, each at exit 0: `check:new-line-citations` (0 new),
`check-changeset-presence`, `changeset:check`, `check:control-bytes`,
`check:vi-mock-specifiers`, `check:vi-mock-inherit`,
`check:vi-mock-override-shape`, `check:test-path-roots`,
`check:changeset-claims` (report-only; the pending changeset it names is
about chunk co-tenancy, which this diff does not move),
`check:pending-changeset-literals`.
- ESLint on the two changed files only (`--no-inline-config`): the new
test has 0 findings. `StudioDesignSurface.tsx` has the same rule-by-rule
counts as `5262f7dd3f` read through `--stdin`, which is 1 error
(`react-hooks/static-components`) and 18 warnings, so nothing was added.
The config enables no type-aware linting, so this diff cannot move a
verdict in an untouched file. The repo-wide lint is CI's.
- ⛔ No timeout was raised and no `heavyDomTests` entry was added.

## Acceptance notes

- Changeset: `.changeset/11331-app-shell-cancelled-load-spinner.md`,
`@object-ui/app-shell` `patch`.
- CI was not awaited (the seat owns convergence). `Spec Main Shape Gate`
is expected red from objectui#11330, which this diff does not touch.
- Reach of the Data site: the pin reaches it through a
`useMetadataClient` re-mint, which the hook's `useMemo` can hand out
(AGENTS.md objectstack-ai#10). No console flow that re-mints the client mid-load was
exercised.

Session: `https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ`

---
_Generated by [Claude
Code](https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants