Skip to content

fix(app-shell): switching a report to joined clears the container binding it hides (objectui#10746) - #10762

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-10746-joined-report-clears-binding
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-10746-joined-report-clears-binding

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #10746
Clause-②: no — a Studio inspector's patch on a type switch; no declared surface moves

What changed

packages/app-shell/src/views/metadata-admin/inspectors/ReportDefaultInspector.tsx: the Report type picker's commit is now commitType. When the picked type is joined, the patch that commits type: 'joined' also carries dataset, values, rows, columns, chart and order as undefined-valued keys — only those the draft actually holds (draft[key] !== undefined). Any other type commits { type } alone, as before. The list lives in JOINED_CONTAINER_CLEARED_KEYS with the reasoning beside it; blocks, runtimeFilter and drilldown are never named.

order is a sixth key, beyond the card's five, under the role file's bounded in-place exemption (same defect class, mechanical, same file, same gate family, no other claim on the file): the spec's own reportForm hides order in the same "Dataset binding" section (visibleWhen: "data.type != 'joined'"), and the INSTALLED spec already refuses a container order on a joined report ("a joined report orders per block — move order onto blocks[]"), so the same invisible-key save refusal exists today for order. Evidence in H3 below.

New pin file ReportDefaultInspector.joinedClearsBinding-10746.test.tsx (8 tests); changeset .changeset/10746-joined-report-clears-binding.md ('@object-ui/app-shell': patch).

H1 — reproduction on origin/main (1422a920ed)

The pin file run against the untouched component: Tests 4 failed | 4 passed (8). The defect pin quotes the only onPatch argument the type picker commits today: AssertionError: expected { type: 'joined' } to strictly equal { type: 'joined', …(6) } — the commit is { type: 'joined' } and nothing else. The 4 pins green on base are the boundary/control pins, which pin what must NOT change.

H2 — the clear mechanism

onPatch is a shallow patch in all three hosts: ResourceEditPage applies it as handleDraftChange((d) => ({ ...d, ...patch })), ReportConfigPanel.handlePatch as { ...draftRef.current, ...patch }, and StudioDesignSurface.onPatch as setDraft((d) => ({ ...d, ...patch })); each saves through client.save, which is JSON.stringify. None offers a delete sentinel, so the one way to drop a key is an undefined-valued key — the spelling this inspector's own commitChart (chart: next.type ? next : undefined) and the siblings (ActionDefaultInspector, ObjectDefaultInspector, DatasetDefaultInspector's "Clear all", pinned by its objectui#9372 suite) already use. Measured in the second pin: after the spread the key is an OWN property holding undefined (Object.hasOwn true, value undefined — not null, not an empty string), and JSON.parse(JSON.stringify(committed)) — the shape client.save puts on the wire and the spec parses — has no such property. The spec's refinement itself skips undefined (if (value === undefined || …) continue; in the joined arm), so even the in-memory draft would not be refused.

  • Reach: The clear holds for the patch and for the save that follows it. The metadata-admin editor rebuilds its draft as the served draft spread over layered.effective (on load, after each save and after publish), and effective is the published layer, so a report whose PUBLISHED version was bound gets those keys back in the draft after the first draft save until it is published, and a report already saved joined with stale keys is not repaired on load. Both are the host's draft-over-baseline merge, objectui#10765.

H3 — the spec check

Installed @objectstack/spec is 17.4.0 (packages/app-shell/node_modules/@objectstack/spec). It PREDATES objectstack-ai/objectstack#20160: grep 'selects per block' over its dist/ gives 0 hits; the control grep 'orders per block' gives 3. One-off probe with the installed ReportSchema.safeParse (not committed):

  • joined + one block: success
  • joined + stale dataset / values / rows / columns / chart: success — 17.4.0 accepts them
  • joined + stale order: refused, custom at ['order'], "a joined report orders per block — move order onto blocks[]."
  • joined + blocks: []: refused at ['blocks']
  • joined + runtimeFilter + drilldown: false: success
  • joined + dataset: undefined as an own key: success

So the pins' parse leg measures the order half with the installed spec (before the fix: refused at ['order']; after: parses), and for the four selection keys the pins assert ABSENCE and cite the rule read at objectstack origin/main packages/spec/src/ui/report.zod.ts: JOINED_CONTAINER_SELECTION_KEYS = ['dataset', 'rows', 'columns', 'values'], one custom issue per present key at path: [key], message "a joined report selects per block — move KEY onto blocks[], or delete it; on the container it selects nothing." (KEY stands for the key's own name.) No checkout on this box holds a built spec dist carrying that refusal, and building one was outside this card.

H4 — edges

  • joined → non-joined: nothing is restored; the patch is { type } alone and the author re-binds (pinned).
  • non-joined → non-joined: the binding is kept; the patch is { type } alone (control, pinned).
  • joined with blocks[] present: untouched, same array reference, in both directions (pinned).
  • Undo: there is no undo stack behind the metadata draft. ResourceEditPage's Undo2 button is doDiscardDraft (ADR-0034: discard the whole pending draft), not a per-edit undo, and handleDraftChange keeps no history. One undo cannot restore the keys; switching the type back does not either.
  • runtimeFilter and drilldown survive the switch (pinned): the joined branch reads both.
  • Partially bound report: the patch names only the keys present (pinned), so ReportConfigPanel.onFieldChange sees no phantom clears, and an unbound report's switch stays { type: 'joined' } (pinned).

Pins and ablations

Head 3f53c15776, pin file plus the two existing ReportDefaultInspector suites: Test Files 3 passed (3) · Tests 37 passed (37).

Red on base (1422a920ed, component untouched, pin file present): 4 failed | 4 passed — the defect pin, the own-key/serialised pin, the parse leg, the partially-bound pin. The other 4 are controls and boundaries, green on base by construction.

Per-hunk ablations on the committed head, each through objectstack's scripts/ablation-replace.mjs (anchor must hit exactly once; the blob change is verified on disk; restore proven blob == HEAD a9cb7e9c and git diff HEAD empty), the prediction written before each run, observed direction = predicted:

  • M1, drop the joined guard (if (nextType === 'joined') becomes if (typeof nextType === 'string')): predicted CONTROL F red; observed 1 failed | 7 passed (CONTROL: non-joined → non-joined keeps the binding).
  • M2, drop the presence filter (if (draft[key] !== undefined) patch[key] = undefined; becomes unconditional): predicted D and E red; observed 2 failed | 6 passed (names only present keys; unbound one-key patch). The first attempt was a NO-OP the tool refused — the replacement text was a substring of the anchor, so its on-disk count could not rise (1 to 1, a rise of 0) — and it restored; the leg was re-run with a distinct replacement.
  • M3, add blocks to the cleared list: predicted C and G red; observed 2 failed | 6 passed (parse leg: the joined report loses its blocks; CONTROL: blocks untouched).
  • M4, drop order from the list: predicted A, B and C red; observed 3 failed | 5 passed.

Assertion spelling: every patch is read with toStrictEqual, because toHaveBeenCalledWith / toEqual treat an undefined-valued key as absent and would read the defect and the fix alike.

Round 2 (d4945789f6, the contract-review wording round: the changeset's reach sentence and the pin file's three-host comment; no source line changed — the diff's only non-changeset lines are the comment lines shown by git diff -U0, and ReportDefaultInspector.tsx is untouched): pin file re-run Tests 8 passed (8).

Gates (local, derived by hand from package.json and the workflows — objectui has no dispatch-gates.mjs)

  • ① closure build turbo run build --filter='@object-ui/app-shell^...' --concurrency=2: Tasks: 28 successful, 28 total, 2m12s, under the verify lock.
  • ② pnpm --filter @object-ui/app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json): exit 0; tsc -p tsconfig.test.json --listFiles lists the pin file (1 hit among 4673 files).
  • ② vitest, the three files above: 37 passed; round 2 pin file: 8 passed.
  • node scripts/check-changeset-presence.mjs: ✅ (round 2: 2 source files of 1 released package changed, 1 changeset declared). node scripts/check-changeset-no-major.mjs: ✅. pnpm check:changeset-claims: ✅. pnpm check:pending-changeset-literals: ✅.
  • pnpm check:control-bytes: ✅ OK (round 2: 8942 tracked text files). The role file's control-byte grep over the changed files: no match.
  • pnpm check:new-line-citations: 0 new citation(s) (both rounds).
  • pnpm check:test-path-roots: ✅ OK. pnpm check:vi-mock-specifiers: ✅ OK.
  • eslint with the package config on the two touched .tsx files: exit 0.
  • NOT MEASURED locally, CI-owned: pnpm lint (repo-wide), the 8 test shards, Build & E2E, check:i18n-* (no t() key was added or changed). Round 1 CI on 3f53c157: 40 success, 3 skipped, 0 failed per the contract review.

Serial

Round 2: origin/main at 25c7d584e4 (+5 commits over BASE 1422a920ed: #10752, #10753, #10708, #10751, #10761); none touches the three files; git merge-tree --write-tree exits 0 (clean, tree 738f2bf534). No merge commit was needed.

Acceptance notes

  • chart is cleared per the triage direction and [finding] a joined report's chart — container and blocks[].chart — parses and is never drawn, while the liveness ledger names the joined branch as its reader objectstack#20161 (inert on a joined container); the installed spec does not refuse it, so its clearing is pinned by absence only.
  • Deviation: the worktree-creating git fetch origin main ran against the shared checkout (the role-file recipe) rather than inside the worktree as the order asked; origin/main did not move (1422a920ed before and after), only the shared checkout's FETCH_HEAD was touched. Every later fetch used a private ref inside the worktree.
  • Observation, not filed: ReportConfigPanel hosts this inspector too, merges with the same spread and emits onFieldChange(key, undefined, next) once per cleared key — the documented "field changed" signal, correct for a live preview.
  • The draft-over-baseline merge that re-introduces a published binding after the first draft save is objectui#10765 (filed by the seat), not addressed here.

Generated by Claude Code

…ding it hides

Studio's report inspector committed `{ type: 'joined' }` alone and, in the same
render, hid its dataset / values / rows / columns / chart controls while the
spec's own `reportForm` hid the whole "Dataset binding" section (`order`
included) through `visibleWhen: "data.type != 'joined'"`. A report bound first
and switched second kept every one of those keys invisibly; `ReportSchema`'s
joined arm refuses a container `dataset` / `rows` / `columns` / `values`
(objectstack PR #20160) and has always refused a container `order`, so the
save was refused at a path no Properties-tab control could reach.

The type picker now drops `dataset`, `values`, `rows`, `columns`, `chart` and
`order` in the same patch that commits `type: 'joined'`, as `undefined`-valued
keys (the spelling `commitChart` and the sibling inspectors already clear
with); only keys the draft carries are named. `runtimeFilter` and `drilldown`
are kept (the joined branch reads both), switching between non-joined types
keeps the binding, switching away from `joined` restores nothing, and `blocks`
is never touched.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3066.1 KB 3104.5 KB
Main entry chunk (gzip) 148.3 KB 350 KB
Entry file index-CXnU148_.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.57KB 6.15KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.17KB 10.58KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 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) 554.92KB 132.66KB
core (index.js) 9.52KB 3.79KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.93KB 62.31KB
fields (index.js) 259.29KB 65.64KB
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.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.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) 51.65KB 14.69KB
plugin-charts (index.js) 82.37KB 22.57KB
plugin-chatbot (index.js) 198.40KB 47.22KB
plugin-dashboard (index.js) 134.11KB 35.62KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 233.23KB 61.74KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 157.08KB 40.13KB
plugin-gantt (index.js) 169.75KB 41.96KB
plugin-grid (index.js) 218.12KB 59.72KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 114.70KB 28.42KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.91KB 9.05KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 87.83KB 22.01KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 116.21KB 38.14KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 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) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3f53c1577655e278bfd1b72858b513250810625c

① Derived judgments

Hosts and the mechanism. ReportDefaultInspector has THREE hosts at head, not the two the PR body (H2) and the pin file ("as both hosts spell it") name:

  1. ResourceEditPage.tsx (registry, getMetadataDefaultInspector(type) :1955, mounted :2803-2812): handleDraftChange((d) => ({ ...d, ...patch })), a shallow spread. doSave (:1400-1500) sends config.fromDraft ? fromDraft(draft) : draft (report declares no fromDraft) to client.save, which is body: JSON.stringify(item) (metadata-client.ts:1080); the only pre-request check, assertObjectMetadataWritable, returns unless type is object. isDirty (:1736) and autosave (:1791-1812) compare JSON.stringify snapshots. No structuredClone, deep merge, IndexedDB or client-side zod parse on this path; localStorage holds only the autosave flag and inspector layout prefs. handleDraftChange's Object.is diff marks each cleared key as changed and drops its server issue, which is correct.
  2. ReportConfigPanel.tsx (:172): handlePatch spreads { ...draftRef.current, ...patch }, emits onFieldChange(key, undefined, next) per cleared key; ReportView.handleReportFieldChange spreads it into editSchema; handleReportConfigSave goes persistRuntimeMetadata to metadataClient.save to JSON.stringify. handleResumeDraft replaces the draft wholesale. Clean.
  3. StudioDesignSurface.tsx (report leaves via navSurface.ts:85, getMetadataDefaultInspector(current.type) :1875, mounted :2358): onPatch spread :1971-1976, client.save(current.type, current.name, draft, { mode: 'draft' }) :1982, useDraftAutoSave keys on JSON.stringify(snapshot) :194. Clean on the wire. UNLISTED in the published text.

The first PUT is clean on all three paths. The clear does not survive the metadata-admin refresh. ResourceEditPage rebuilds the draft as { ...lay.effective, ...draftReal } at load (:1054-1057), after every save (:1515-1520) and after publish (:1692-1696); StudioDesignSurface does the same at load (:1950-1954). On the server (objectstack metadata-protocol/src/protocol.ts), getMetaItemLayered composes effective from code plus the state: 'active' overlay only and never reads a draft; saveMetaItem stores body: request.item raw; the draft read returns that row raw. So for a report whose published or code version is bound (the card's "bound first, switched second" case whenever the binding was published), the first draft save (autosave defaults ON, :846-856, 1.5s) is clean, the refresh re-introduces dataset / values / rows / columns / chart / order from the published baseline under type: 'joined', invisibly again, and the NEXT save PUTs them: refused at dataset under the spec at objectstack main (at order under 17.4.0 when the published report carried one), reachable only from the JSON tab, until Publish. Not pinned, not stated in the changeset or PR body. The producer this card names (the type-switch commit) is now correct; the draft-over-effective merge is a host-level class (c) cause deserving its own card, but the published text must state the limit. This is the FAIL below.

Completeness of the cleared set. Spec at objectstack main 455dcc060d (report.zod.ts :252, :395-470): joined refuses dataset / rows / columns / values when present and non-empty (JOINED_CONTAINER_SELECTION_KEYS), order when non-empty, and empty or missing blocks; chart is NOT refused (objectstack#20161 still open, domain:spec). Cleared six = the five refused keys plus chart. Nothing refused is left uncleared; blocks is untouched (correct). chart is allowed but inert and invisible in THIS inspector for joined (hiddenFields plus the curated panel under datasetBound), so clearing it follows the triage; note the spec's own report.form.ts "Filter and chart" section has no visibleWhen and does show chart for joined. runtimeFilter, drilldown, name, label, description kept; filter and groupings are aliases the strict object already refuses. drilldown sits inside the spec form's hidden "Dataset binding" section for joined yet is read by the renderer and accepted, so keeping it is right (a spec-form inconsistency, not this card's).

The order extension. Same file, same class (a key the same visibility rule hides and the spec refuses on a joined container). 17.4.0 reading TRUE by probe with the installed dist: joined plus order: [{ by }] is REFUSED at ['order'] "a joined report orders per block"; joined plus order: [] parses; joined plus the four selection keys plus chart parses; joined plus blocks: [] refused at ['blocks']; dataset: undefined as an own key parses. grep 'selects per block' over the 17.4.0 dist: 0 files; 'orders per block': 18 files. order and its joined refusal landed together (#3916, spec CHANGELOG :46552-46582), so "has always refused a container order" holds.

Pins. Re-run in a detached worktree at head with pnpm install --offline --frozen-lockfile (no sibling lockfile matched: all three differ by react-map-gl), deps built via turbo cache, every run under os-verify-lock.sh. Head: pin file plus ReportDefaultInspector.test.tsx, .i18nTitle.test.tsx, ReportConfigPanel.panelTitle.test.tsx: 4 files, 43 passed. Base leg (component at 1422a920ed, pin at head): 4 failed | 4 passed, the defect pin, the own-key/serialised pin, the parse leg and the partially-bound pin, with the quoted expected { type: 'joined' } to strictly equal { type: 'joined', …(6) }. Ablations, each anchor hit once, file restored to a clean tree after each: M1 (guard dropped) 1 failed (CONTROL non-joined keeps binding); M2 (presence filter dropped) 2 failed (partially bound, unbound one-key); M3 (blocks added) 2 failed (parse leg, CONTROL blocks); M4 (order dropped) 3 failed (defect, own-key, parse leg). All as claimed. Tautology: pin 8's for (key of CLEARED) expect(committed).not.toHaveProperty(key) loop is vacuous, the draft never carried those keys; its load-bearing assertion is the strict { type: 'summary' } patch, so the pin stands. The parse leg is measured with 17.4.0 for order only and says so in its title, the file header and PR body H3.

Edges. joined to non-joined restores nothing: pinned and stated in changeset and PR body. Undo claim TRUE: Undo2 :2240 is onClick={doDiscardDraft} :2235 (DELETE ?state=draft); handleDraftChange keeps no history; ReportConfigPanel has no undo. A report already saved joined with stale keys, opened fresh: nothing clears it on load or save (load merges baseline and draft; save serialises what is there); not stated; not this card's producer, but it belongs in the same limit sentence.

Type-check pnpm --filter @object-ui/app-shell type-check at head: exit 0 (tsc --noEmit && tsc -p tsconfig.test.json).

② Semver level

'@object-ui/app-shell': patch is right. Diff touches one component (+46/-1), one new test, one changeset; JOINED_CONTAINER_CLEARED_KEYS and commitType are module-local, ReportDefaultInspectorProps unchanged, packages/app-shell/src/index.ts and package.json untouched, registration registerMetadataDefaultInspector('report', ...) unchanged. Clause-②: no, confirmed.

Published text against head: spec-version claims TRUE (installed and npm latest 17.4.0; os-main packages/spec/package.json 17.4.0 with #20160 merged 2bbebf5 at 04:03Z unreleased; no built spec dist on this box carries the refusal). "has always refused a container order" TRUE. Wire claims TRUE for the first PUT on all three hosts; the after-save refresh merge is the omission (① above). Host list INCOMPLETE: StudioDesignSurface missing from PR body H2 and the pin file's "as both hosts spell it". "only the JSON source tab could delete the key" holds for ResourceEditPage only; the runtime ReportConfigPanel has no JSON tab (a stronger defect statement, not a false one). Pending changesets on main naming ReportDefaultInspector: 8488-…, 8862-…, 9651-…, none touches the type picker; Changeset Claim Re-read check success; no changeset-claim-re-read bot comment on the PR (the only comment is the performance-budget report).

③ Boundary flags

Implemented-by: claude/issue-10746-joined-report-clears-binding
Reviewed-by: session_014mXUNuFomfj24w7s1pZzhN

VERDICT: FAIL

What stops it, both wording, no code change owed to this card:

  1. The changeset and PR body must state the fix's reach. Append to the changeset's last paragraph: "The clear holds for the patch and for the save that follows it. The metadata-admin editor rebuilds its draft as the served draft spread over layered.effective (on load, after each save and after publish), and effective is the published layer, so a report whose PUBLISHED version was bound gets those keys back in the draft after the first draft save until it is published, and a report already saved joined with stale keys is not repaired on load. Both are the host's draft-over-baseline merge, filed separately." Add the same to PR body H2 as a final bullet, and file the merge (ResourceEditPage.tsx :1054-1057, :1515-1520, :1692-1696; StudioDesignSurface.tsx :1950-1954) as a class (c) card of its own, area:reports sibling of Studio report inspector: switching a report's type to joined keeps its now-hidden dataset / values / rows / columns, which the spec refuses on a joined report once objectstack PR #20160 lands #10746.

  2. Host list. PR body H2, replace "onPatch is a shallow patch: ResourceEditPage applies it as … and ReportConfigPanel.handlePatch as …" with "onPatch is a shallow patch in all three hosts: ResourceEditPage applies it as handleDraftChange((d) => ({ ...d, ...patch })), ReportConfigPanel.handlePatch as { ...draftRef.current, ...patch }, and StudioDesignSurface.onPatch as setDraft((d) => ({ ...d, ...patch })); each saves through client.save, which is JSON.stringify." In the pin file, replace "The host's merge, as both hosts spell it" with "The host's merge, as all three hosts spell it (ResourceEditPage, ReportConfigPanel, StudioDesignSurface)".

Everything else in ①–③ passes: mechanism correct for the committed patch on every host, cleared set complete against the spec at main, order extension in class and true under 17.4.0, 8 pins and 4 ablations reproduce exactly, type-check and CI green, tree clean, not governed.

…e inspector hosts

Round-1 contract review on the PR: wording only. The changeset now states the
limit of the clear — it holds for the patch and the save that follows it,
while the metadata-admin editor's draft-over-`layered.effective` rebuild (on
load, after each save and after publish) brings a PUBLISHED binding's keys
back into the draft until publish and does not repair a report already saved
`joined` with stale keys (objectui#10765, the host merge, not this card's).
The pin file's merge helper comment names the third host, `StudioDesignSurface`,
beside `ResourceEditPage` and `ReportConfigPanel`. Prose and a comment only;
no source line changes.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3066.3 KB 3104.5 KB
Main entry chunk (gzip) 148.3 KB 350 KB
Entry file index-CuLhgiEQ.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.57KB 6.15KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.17KB 10.58KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 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) 554.94KB 132.67KB
core (index.js) 9.57KB 3.81KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.93KB 62.31KB
fields (index.js) 259.29KB 65.64KB
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.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.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) 51.65KB 14.69KB
plugin-charts (index.js) 82.37KB 22.57KB
plugin-chatbot (index.js) 198.40KB 47.22KB
plugin-dashboard (index.js) 134.11KB 35.62KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 233.23KB 61.74KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 157.08KB 40.13KB
plugin-gantt (index.js) 169.75KB 41.96KB
plugin-grid (index.js) 218.12KB 59.72KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 114.70KB 28.42KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.91KB 9.05KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 87.83KB 22.01KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 116.21KB 38.14KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 7.50KB 3.05KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.16KB 2.71KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 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) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d4945789f633b54a67d8a5b691936e368f05089e

① Derived judgments

The delta 3f53c157..d4945789. One commit, docs(app-shell): state the joined-clear fix's reach and name all three inspector hosts, author Claude. Name-status: M .changeset/10746-joined-report-clears-binding.md, M packages/app-shell/src/views/metadata-admin/inspectors/ReportDefaultInspector.joinedClearsBinding-10746.test.tsx. Numstat: 7 1 changeset, 2 1 test file, as claimed. ReportDefaultInspector.tsx untouched: blob a9cb7e9c2f at both commits, empty diff. Test-file changed lines with comment lines (*, /**, */) stripped: none; the three changed lines are :119-120 of a JSDoc block. No code token moved. Changeset frontmatter (--- / '@object-ui/app-shell': patch / ---) md5 4e8ce55021fe3b14329c8b4cb3f9f354 at both 3f53c157 and head, identical. Whole PR vs base 1422a920ed: still exactly three files (changeset A 31/0, pin A 228/0, component M 45/1).

Reach sentence. Changeset :25-31 and body H2 "Reach" bullet are, whitespace-normalised, identical to each other and to round 1's prescribed text up to the final clause, where "filed separately." became "objectui#10765." True at head: ResourceEditPage.tsx spreads the served draft over lay.effective ?? … at :1053-1056 (load, "Merge the draft over the effective baseline"), :1518-1521 in doSave (freshBaseline = lay.effective ?? itemToSave), :1696-1699 in doPublish (freshBaseline = lay.effective ?? draft); the two other lay.effective reads (:1649 doReset, :1721 doDiscardDraft) take the baseline alone and are not claimed. StudioDesignSurface.tsx :1950-1954 does the same at load. objectui#10765 exists, open, filed 06:23:42Z by the seat (objectstack-fleet[bot]), title names the layered.effective spread, body names the same three sites and the class (c) cause, cites #10746 / #10762; label finding only, unassigned, "Not graded here" (round 1 asked for an area:reports card; that is triage's label to add, not this PR's text). The effective = published-layer claim is the server-side reading verified in round 1 and restated in #10765.

Three-host sentence. Body H2 begins with round 1's prescribed sentence verbatim ("in all three hosts … StudioDesignSurface.onPatch as setDraft((d) => ({ ...d, ...patch })); each saves through client.save, which is JSON.stringify"; "Neither offers" became "None offers"). Spellings at head: ReportConfigPanel.tsx :105 const next = { ...draftRef.current, ...patch }; exact; StudioDesignSurface.tsx :1973 setDraft((d) => ({ ...d, ...patch })); exact, saves client.save(current.type, current.name, draft, { mode: 'draft', packageId }) :1982; ResourceEditPage.tsx :2808-2811 reads handleDraftChange((d) => ({ ...(d as Record OF string, unknown), ...patch })), so the published spelling drops a type-only cast (same paraphrase as the live body and round 1; not false), saves client.save OF any (type, savedName, itemToSave, …) :1500. The runtime host's "client.save" is ReportConfigPanel.onSave to ReportView.saveSchema to persistRuntimeMetadata (:182) to ctx.metadataClient.save(type, name, body, { mode: 'draft' }) (runtime-metadata-persistence.ts :211), the same MetadataClient.save, body: JSON.stringify(item) at metadata-client.ts :1080. Pin comment :119-122 is round 1's replacement verbatim: names all three hosts, then still spells only two (ResourceEditPage, ReportConfigPanel); StudioDesignSurface's spread is not spelled there. Compliant with the requirement as written; a nit, not a blocker.

Leftover two-host phrasing. None in the replacement body (no "both hosts", "two hosts", "Neither"), the changeset, the pin file or the component's comments (:93-96 "The host spreads the patch", :357-359 names ReportConfigPanel as one host that mirrors keys). Repo-wide "both hosts" hits are unrelated files.

Round 2 lines and figures. "no source line changed — the diff's only non-changeset lines are the comment lines … ReportDefaultInspector.tsx is untouched": TRUE (above). "pin file re-run Tests 8 passed (8)": the file declares 8 it( blocks; no re-run owed for a comment-only delta, and all 8 CI shards on head are green. Gates, re-derived in a scratch shared clone at head (source checkout untouched, 0 dirty paths before and after): check-changeset-presence.mjs prints "3 file(s) changed, 2 of them published source of a package the release covers … 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)" (the test file under src/ counts by design, "No carve-out for test files"), so round 2's "2 source files" is right and the live body's round-1 "1 source file" was the inaccurate figure, now replaced; check-control-bytes.mjs prints "scanned 8942 tracked text file(s); skipped 85 binary" at head AND at 3f53c157, so 8942 is right (the live "8940" was off). "Round 1 CI on 3f53c157: 40 success, 3 skipped, 0 failed per the contract review": matches the round-1 comment (id 5853351544, byte-identical to the saved record). Serial: main f308a655b8 was +3 over base (#10752, #10753, #10708) and merge-tree gave tree 72f420b431, as the body says; both figures are now stale (③).

Round-1 record read in full; its two requirements are met literally. Live body on GitHub is byte-identical to pr-body-10762-live.md; the r2 file differs from it only in the H2 sentence and bullet, the Round 2 paragraph, four gate lines, the Serial line and one Acceptance bullet. Title unchanged: fix(app-shell): switching a report to joined clears the container binding it hides (objectui#10746).

② Semver level

'@object-ui/app-shell': patch, frontmatter unchanged by this round (md5 above). The round-2 delta is prose and comments only; nothing in the earlier judgment moves: JOINED_CONTAINER_CLEARED_KEYS and commitType module-local, props and registration unchanged, index.ts and package.json untouched. Clause-② no, confirmed.

③ Boundary flags

Implemented-by: claude/issue-10746-joined-report-clears-binding
Reviewed-by: session_014mXUNuFomfj24w7s1pZzhN

VERDICT: PASS

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

Projects

None yet

1 participant