Skip to content

feat(types): the sidebar node declares side on both faces; M3 re-run on the merged ref (objectui#11070 round 12) - #11964

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-11070-m3-rerun-sidebar-side
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-11070-m3-rerun-sidebar-side

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #11070 (round 12)
Clause-②: yes — widens: the strict authoring face (StrictAnyComponentSchema) now accepts sidebar.side ('left' / 'right'), which it refused with unrecognized_keys; narrows: the tolerant face (safeValidateSchema) now refuses a side outside those two values (invalid_value at side), which it kept unjudged.

Round 12 of objectui#11070, claim 6058868109, session https://claude.ai/code/session_01DBZ9bntPZ7VKyQNtJeNsgw. Measurement first: PR objectui#11069's M3 classification re-run on the merged ref, then sidebar.side declared on both faces. Every other class (ii) pair the re-run found is reported below with its read site and a recommended route, and is left unchanged here. The card's executable acceptance does not hold yet (three class (ii) pairs remain unruled), so #11070 remains open.

M3 re-run, on the merged ref

Base 70e3d772 (origin/main when the branch was cut; the seat read d92b2a15 at dispatch, and the one commit between them, objectui#11923, touches app-shell only). @object-ui/types built first. zod 4.6.5.

Instrument. The shipped face, as PR objectui#11069's M1 section prescribes, because scripts/measure-strict-authoring-face.mjs's own walker lacks the objectui#11022 registered-input admission. A scratch script (not committed) runs the measure script's three corpus loaders, copied unchanged, plus the JSON half of M2's pnpm check population (every other tracked .json). Each node document is judged by StrictAnyComponentSchema and AnyComponentSchema from the built packages/types/dist/zod. Refused keys are read with PR objectui#11069's own findUndeclaredKeys, copied verbatim from its head a11f73347d. Each key is attributed to its nearest enclosing node, found by the measure script's schema-guided node walk, so a pair is (component, key path from that node). The YAML half of M2's population holds no node document: 0 tracked YAML files carry a root type:, against 41 that carry a root name: as the lit control. The measure script was also run as-is at both ends: its per-component undeclared-key enumeration agrees with the scratch reading pair for pair, and no dashboard row is refused, so the M1 walker gap no longer moves any figure.

corpus node documents refused by the shipped strict face strict-only red today refused, naming no key
catalog 432 28 16 12 12
docs 183 27 13 14 8
authored 6 2 1 1 0
M2 JSON population (examples/hello-world/schema.json) 1 1 1 0 0
total 622 58 31 27 20

The 20 documents refused without naming a key are all refused by the tolerant face too, for value reasons (carousel items, resizable panel ids, child-slot unions, one onSubmit).

Classified (PR objectui#11069's classes; its M3 figures at f6ae5e22d in brackets):

class pairs occurrences strict-only
(i) undeclared, no read site found 32 (49) 48 (70) 35 (50)
(ii) a renderer reads it, no schema declares it 3 (32) 6 (149) 5 (141)
(iii) the TS type names it, the zod mirror omits it 0 (10) 0 (15) 0 (13)

Class (iii) is 0: the UnmirroredDeclared ledger in zod-mirror-parity.test.ts names none of the refused keys. options.description is gone from the refused population entirely (0 hits).

Class (ii), each with its read site

component · key path occurrences strict-only documents read site what the spec says recommended route
form · fields[].scale 3 3 catalog fields-number/with-decimal-places, fields-percent/required-percent, fields-percent/with-decimal-places form.tsx hands the entry to the field widget as field (field: field.field || field); PercentField reads percentField?.scale (through resolveFieldScale), NumberField reads numberField?.scale FieldSchema declares scale (17.7.0) declare on both faces by reference (stripImportedDefaults(SpecFieldSchema).shape.scale / SpecField['scale']), round 2's pattern. New since PR objectui#11069 measured: objectui#11255 moved these fixtures from precision to scale, the key the widgets read. Measure first: the spec refuses scale on a currency field, and a flat form-field face taking .shape.scale would not carry that cross-key rule.
form · fields[].currency 2 2 catalog fields-currency/euro-currency, fields-currency/usd-currency CurrencyField resolves its code through resolveFieldCurrency (@object-ui/i18n), whose first limb is field?.currency FieldSchema refuses currency as a field key; its home is currencyConfig (currencyMode: 'fixed' + defaultCurrency) runtime-only with document corrections: rewrite both fixtures to currencyConfig, which resolveFieldCurrency's second limb already honours. Declaring would contradict the spec (a stop condition). PR objectui#11069 graded this pair (i); the read sits across a package boundary. Whether limb 1 retires is a separate question: it also serves the metric widget's and dataset measure's currency and grid columns.
action:button · properties.endpoint 1 0 (red on both faces) docs guide/schema-rendering.md, the "Reference actions in schemas" fence action-button.tsx forwards schema.endpoint; ActionRunner resolves api || endpoint || target, its endpoint limb marked deprecated the spec's action:button row (17.6.0+) refuses it in favour of target; both faces refuse it with "Did you mean endpoint → target?" runtime-only (deprecated dialect) with the document corrected: the fence and its prose ("the endpoint key is endpoint") to target. No declaration: the spec already ruled.
sidebar · side 0 (probe-measured) — no corpus document writes it (5 sidebar nodes at base, none with side) shadcn's Sidebar, reached through the props SidebarNode forwards no sidebar row declared in this PR, below

Class (i), for the record (objectui#5250 slice B's input, not this card)

button · action (5), fullWidth, content; card · change (3), value (3), dataSource (2), content, icon; form · submitButton (3), title, fields[].columnSpan (the form reads colSpan), fields[].defaultChecked, fields[].defaultValue, fields[].schema; list · items[].type (3), itemTemplate; combobox · searchPlaceholder (2), emptyText (the node's form-control allow-list never forwards them to the custom Combobox); empty · message (2); navigation-menu · items[].items (2; the renderer reads children); app · pages; input · validations; label · required; grid · actions, pagination, rows, title; select · field, optionsFrom; data-table · dataSource, columns[].key, columns[].title (the renderer reads accessorKey / header). A source-read grade, as PR objectui#11069 said of its own: a key can still arrive through a prop spread.

sidebar.side

Measured first: @objectstack/spec 17.7.0 has no sidebar row. ComponentPropsMap holds page:sidebar (the page region, whose row declares children only), and no spec export names a sidebar side. So the route is the local one: declared flat, with the values the renderer honours, as round 7's columns and round 10's grid keys kept the faces flat.

  • SidebarSchema (navigation.ts): side?: 'left' | 'right', docblock stating what each value draws and that it shows on the collapsible form only.
  • The zod mirror (navigation.zod.ts): side: z.enum(['left', 'right']), optional, with a refusal message naming the two values.
  • The retired position key stays refused by name on every face. Its refusal now also says side: 'right' draws the right edge. objectui#11465 deliberately did not point there while side was undeclared.
  • content/docs/components/basic/sidebar.mdx gains a side section with a JSON example, and its "the renderer reads" sentence now names side and variant.

Reach, through the real SchemaRenderer and registry (the new components pin): on the collapsible form right draws data-side="right" with the panel at right-0, left draws the same markup as an absent key, and with collapsible: false every value draws the same column. Built-dist probe at the head: side: 'left' and 'right' ACCEPT on both faces, and 'top' is refused at side with invalid_value on both. A control key on the same node is still refused with unrecognized_keys.

M3 at the head f835af41: the 35-pair list is identical to the base. The one moved figure is the docs corpus, 183 → 184 documents, which is this PR's new side fence, accepted on both faces. The measure script's totals move by that document only (documents 621 → 622, nodes 2288 → 2290); every refusal figure is unchanged.

Hypotheses

  • H1 held, refined. Class (ii) is small: 3 corpus pairs, 6 occurrences, plus sidebar.side as a probe-measured member with no corpus occurrence. options.description is absent at both ends. Of the 3, one is new since PR objectui#11069 (scale), one is a regrade of its class (i) pair (currency), and one is red on both faces (properties.endpoint). The sidebar.side reading was re-taken: the base faces refuse it (the R0 rows below), and the renderer draws the right edge (the render pin).
  • H2: the first reading held, the second is falsified. At base, check:component-surface-parity --type ui:sidebar reports input-outside-keyset: side. After the declaration it does not go quiet: it keeps input-outside-keyset: side and adds interface-extra-key: side. That is the shape variant has carried since objectui#11465. The mechanism is in the gate's judgeRegistration: the accept set is spec ∪ named renderer reads ∪ props-channel reads ∪ framework reads, and the TS interface never enters it. SidebarNode hands side to shadcn through its rest spread, which the gate hedges (forwardsRestProps) but does not count as a read. Whole run: interface-extra-key 101 → 102 (rest-spread-hedged 97 → 98), input-outside-keyset 63 → 63. The gate is report-only and exits 0. Quieting it would take a named read of side in SidebarNode, which is a renderer change this round did not make.

Tests, reverse verification, ablations

Heavy runs through the verify lock, with NODE_OPTIONS=--max-old-space-size=4096 and --maxWorkers=2. Subjects resolve from source through the root vitest alias, so no leg needs a dist rebuild.

  • Pins. strict-face-read-keys-11070.test.ts (the card's pin) gains the round-12 rows. Both values parse on the strict face and keep their value on the tolerant face, and a misspelled sibling sidee is refused by name. top and a boolean are refused on both faces, at side, with invalid_value. The retired position is still refused by name, and its refusal names side. Type-level equality holds on both faces, with a @ts-expect-error for top. New components/.../sidebar-side-registration-11070.test.tsx, the sibling of objectui#11465's variant pin, holds the declaration against the live registration's side enum on both faces, plus the render half.
  • Predictions were written before any leg. All three legs ran on committed code, and each was restored to HEAD with the blob equal to HEAD's and git diff HEAD empty.
    • R0, both files at the base under this head's pins. Predicted 8 vitest red and 3 tsc errors, all in the pin. Observed Tests 8 failed | 145 passed (153) and 3 tsc errors in the pin (TS2339 twice, TS2353), none elsewhere.
    • A1, the zod side member deleted through objectstack's scripts/ablation-replace.mjs (anchor 1 → 0, blob 75cd14ca → a0108550). Predicted 7 red and 2 tsc errors. Observed Tests 7 failed | 146 passed (153). The two tsc errors: the pin's shape['side'], and zod-mirror-parity's assertionUnmirroredMatchesLedger naming navigation.zod.ts#SidebarSchema.
    • A2, the TS side member deleted (anchor 1 → 0, blob ea91bf5c → 52bfcff3). Predicted 0 red and at least 3 tsc errors. Observed Tests 153 passed (153) and 4 tsc errors: 2 in the pin, plus assertionMirroredUndeclaredMatchesLedger and assertionMirroredUndeclaredRecordsEveryKey.
  • Union at the final head f835af41 (packages/types/, the whole scripts/__tests__/, and the three sidebar component pins): Test Files 542 passed | 2 skipped (544), Tests 15100 passed | 2 skipped (15102).
  • Type-check: @object-ui/types (its script line @object-ui/types@17.7.0 type-check echoed; its tsconfig.test.json leg lists both pin files) and @object-ui/components (echoed; its test leg compiles the new pin). Both exit 0, with 0 error TS.

Gates at f835af41

objectui has no dispatch-gates, so this set is the brief's list plus what the diff touches, derived by hand from the root package.json. All exit 0:

  • The brief's list: check:component-surface-parity (report-only), check:spec-symbols, check:skill-examples (18 of 18 TS fences, 70 JSON fences, controls live), check:doc-types, check:doc-snippets (784 of 784 blocks, 0 failed, after building its 35-task --build-filter closure), check-changeset-presence (4 published source files, 1 changeset), check:changeset-claims (no pending changeset names a touched file), check:pending-changeset-literals, check:new-line-citations (0 new), check:control-bytes.
  • Derived from the diff: check:doc-fences, check:doc-examples, docs:check-links, check:doc-example-ids, check:prompt-keys, check:handler-key-reads, check:test-path-roots, changeset:check, and check-governed-queue-guard --test (NOT GOVERNED).
  • Narrowed lint, the 4 changed .ts/.tsx files with the root eslint.config.js and --no-inline-config:
    1. The population is its **/*.{ts,tsx} blocks.
    2. --format json lists 4 files, with 0 errors and 0 warnings. The three that exist at the base read the same 0/0 through --stdin.
    3. Invariance: no parserOptions.project or projectService, and no eslint-rules/ rule reads the filesystem, so the diff cannot move an untouched file's verdict.

NOT MEASURED: check:readme-exports, reason: it reads package READMEs and this diff touches none (its first run here also lacked built packages). NOT MEASURED: repository-wide pnpm lint and pnpm test, reason: CI-owned. NOT MEASURED: CI on this PR, reason: opened with this report; convergence is the seat's.

Acceptance notes


Generated by Claude Code

…#11070 round 12)

`SidebarSchema` declares `side?: 'left' | 'right'` on the TypeScript face and
its zod mirror. The `sidebar` registration offers both values, and the node
hands the key to shadcn's `Sidebar` through the props it forwards, so it
always drew; the shipped strict authoring face refused it with
`unrecognized_keys`. `@objectstack/spec` 17.7.0 has no `sidebar` row (its
`page:sidebar` row is the page region and declares `children` only), so the
key is declared locally and flat.

The retired `position` key stays refused by name; its refusal now says
`side: 'right'` draws the right edge. Pins: the card's strict-face pin (accept
rows with misspelled-sibling controls, wrong-value rows on both faces, the
`position` pointer, type-level equality on both faces) and a registration and
render pin through the real SchemaRenderer. The sidebar docs page gains a
`side` section.

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

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 337 chunks) 3308.1 KB 3312.0 KB
Main entry chunk (gzip) 71.4 KB 350 KB
Entry file index-DBl0sDHu.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) 17.82KB 6.58KB
app-shell (runtime-config.js) 22.59KB 7.89KB
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.15KB 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) 41.19KB 11.12KB
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.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 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) 587.49KB 141.58KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 266.92KB 67.47KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
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.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.50KB 11.82KB
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.04KB 3.92KB
plugin-calendar (index.js) 53.39KB 15.52KB
plugin-charts (index.js) 84.71KB 23.25KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 248.57KB 65.47KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 176.62KB 45.75KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 248.06KB 68.79KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 117.58KB 29.37KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.10KB 11.81KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.07KB 22.93KB
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) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
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.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
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) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
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) 1.13KB 0.65KB
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) 5.78KB 2.70KB
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 (node-slots.js) 7.18KB 2.34KB
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) 19.93KB 7.25KB
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: f835af41537e050cf5e71c68359697fe322a34c0
Local-runs: none

PR objectui#11964 (draft, 1 commit f835af41 whose one parent is the merge base with main, 70e3d772; no merge, no rebase, no force-push; 6 files, +235/−12; head repository = base repository, author objectstack-fleet[bot], assignee huangyiirene, no auto-merge, mergeable_state behind), round 12 of card objectui#11070: the M3 re-run on the merged ref that the release record 6058770188 asked the next claimant to run first, then sidebar.side (pointer 5956633122) declared on both faces — claim 6058868109, dev report 6059807928 (status: done, one open_questions entry, two out_of_scope_findings). Inputs: the card body and its 66 comments, the handover 5934870641 among them; the PR body, its file list and the net diff against the merge base (read in the existing checkout from the head fetched into a ref of its own; main has since taken six commits, 7241a81e through e44f9bc4 — objectui#11865, #11933, #11908, #11894, #11924 and #11943, app-shell, plugin-list, auth and console work — none touching any of the PR's six paths, so the queue's rebuild carries no textual conflict); @objectstack/spec at its @objectstack/spec@17.7.0 tag (59026cea) in the sibling checkout, which is the version packages/types/package.json depends on at the head (^17.7.0); and the 43 check-runs on the head, polled once a minute until none was in progress (final reading 2026-10-08T12:38Z, every run on f835af41): 40 success, 3 skipped by design (dependabot, Test (coverage) and the coverage shard matrix), 0 failure — Type Check, Lint, the eight Test shards and the Test roll-up, Test (dist pins), Doc Snippet Type Check, Doc Component Type Check, Doc Fence Language Check, Doc Example Id Check, Internal Docs Link Check, Docs Route Eager Closure Check, Build Docs, Build & E2E, Live E2E (informational), the five Changeset gates (Bump Policy, Fixed Group Check, Declaration, Claim Re-read, Overwrite Report), Line Citation Gate, Governed Surface Queue Guard, Spec Main Shape Gate, Pre-Install Import Graph Check, Bundle Analysis, README Export Check, Control Byte Scan, Shell Escape Residue Scan, Inert vi.mock Specifier Check, Skill Example Check, Skill Guide Path Check, Skill Eval Token Check, Action Ref Convention and label among them. The gate verdicts are those conclusions; nothing was built, run or re-run here, and the dev's M3 figures, ablation rows and probe readings are quoted as the dev's, not re-derived. No earlier ## Contract review names this head: the newest on the card (5934528537) names PR #11387's cbc637c2, and the PR thread carries one bot comment (the console performance budget), no record and 0 reviews.

① Derived judgments

Every accept-set and public-surface move the diff implies, each judged:

  • The strict face WIDENS on the sidebar arm by one key: side. SidebarSchema (zod, BaseSchema.extend) gains side: z.enum(['left', 'right']), .optional(), with a refusal message naming the two values. RIGHT — the declare route of the triage direction (5888862869: declare on both faces when authors write it and a renderer reads it), and exactly the key the pointer measured as refused (5956633122: unrecognized_keys on { type: 'sidebar', side: 'right' } while the renderer draws the right edge). The read is real and was verified on the head tree: the sidebar registration's inputs carry { name: 'side', type: 'enum', enum: ['left', 'right'] } and its defaultProps carry side: 'left'; SidebarNode destructures { schema, collapsible, ...props } and spreads {...props} onto shadcn's Sidebar, whose own prop is typed side?: "left" | "right" with the default "left" (packages/components/src/ui/sidebar.tsx, an upstream no-touch file, absent from the diff). The strict face is DERIVED — strict-authoring-face.ts's walker closes each object through catchall: z.never() and walks the discriminated union's options — so the key enters it with no exemption list and no .passthrough() flip; strict-authoring-face.ts is absent from the diff. Pinned where it can fail: it.each(['left', 'right']) rows parse on the strict face and keep the value on the tolerant face while a misspelled sibling sidee is still refused by name; the new components pin reads the LIVE registration's side enum (ComponentRegistry.getMeta('sidebar'), the accessor objectui#11465's variant pin uses) and asks both faces about every value in it, so a registration that grows a value the schema does not take turns red. The dev's R0 (both files at the base under this head's pins: 8 vitest red, 3 tsc errors all in the pin) and A1 (the zod member deleted: 7 red, 2 tsc) are quoted as the dev's; the pins' shape shows they cannot pass without the declaration. True width: every sidebar node — the arm has no sub-type.
  • The tolerant face NARROWS on the same key's value: a side outside the two edges is refused where BaseSchema's passthrough kept it unjudged. top and a boolean are now invalid_value at side on AnyComponentSchema (what safeValidateSchema and objectui validate run) and on the strict face alike. RIGHT — the arithmetic consequence of typing a formerly passthrough key, the shape rounds 1, 3, 7, 10 and 11 priced on other faces; pinned by two WRONG rows on both faces and by the exact path-and-code row [['side', 'invalid_value']]; stated in the changeset under a BREAKING banner with the one-line fix ("Write left or right"). Corpus: no catalog sidebar document writes side at the head (the only catalog "side" writers are the two components-overlay-sheet documents, a different node) — the dev's M3 reads the same (five sidebar nodes at the base, none with side), taken as reported.
  • The TypeScript face: SidebarSchema.side?: 'left' | 'right', flat and local. RIGHT. BaseSchema declares no index signature (objectui#8347), so at the base a typed literal carrying side was refused as an excess property; now it compiles, top is a compile error (the @ts-expect-error row), and the type-level rows assertionSidebarSideIsTheTwoEdges hold BOTH faces to exactly 'left' | 'right' | undefined through z.input of the zod member's shape, so a value widened or narrowed on either face fails to compile. The dev's A2 (the TS member deleted: 0 vitest red, 4 tsc errors — two in the pin, two in zod-mirror-parity's assertionMirroredUndeclared* rows) is quoted as the dev's and is the expected shape of a one-face deletion under that ledger.
  • A LOCAL declaration is right here, because the spec has no row to reference — measured at the tag, not inherited from the dev. At @objectstack/spec@17.7.0, ComponentPropsMap in packages/spec/src/ui/component.zod.ts carries 'page:sidebar': PageContainerProps — the page region, a strictObject whose surface reads "this container component (page:section / page:footer / page:sidebar)" and which declares children — and carries no 'sidebar' row; no export under packages/spec/src/ui declares a sidebar side (the only side: keys there are chart alias tables). objectui's own page:sidebar mirror (public-blocks.zod.ts#PageSidebarBlockSchema, spec-owned by reference) is that other node and is untouched. So "declare by reference" has nothing to reference, and the flat local route is the one rounds 7 (columns) and 10 (the grid keys) took when the spec declared nothing; the TS docblock points at the instrument that re-derives the spec entry (check:component-surface-parity --type ui:sidebar, a flag the script's usage line carries) rather than restating it. Consistent with the host-composition ruling (objectui#5097): side is an author-written key — a registration input the docs teach — not a host-supplied React prop.
  • The two-value enum matches what the renderer honours. RIGHT: shadcn's prop type is exactly "left" | "right", the registration's enum is exactly those two, and there is no third value to leave out, so the claim's spec-change stop condition had nothing to fire on. The render half is pinned through the real SchemaRenderer and registry: on the collapsible form right draws data-side="right" with the panel at right-0, left is byte-equal to the absent key, every offered value draws differently; with collapsible: false (shadcn's collapsible="none" branch, the in-flow column) every value is byte-equal to absent — which is what both docblocks and the docs say ("shows on the collapsible form only, as variant does"). The dev's built-dist probe (left / right ACCEPT on both faces, top refused at side on both) agrees with the pins; quoted as the dev's.
  • The retired position key stays retired, and its refusal now points at side. RIGHT on both faces: position?: never and retiredSidebarKey('position', …) are unchanged in kind; only the "Instead:" text grows "To draw the sidebar against the right edge, write side: 'right'". Pinned: { type: 'sidebar', position: 'right' } is still invalid_type at position on both faces, and the mirror's own message is pinned to contain "side: 'right'". objectui#11465 deliberately did not point there while side was undeclared (5956633122 records that); with side declared the pointer is true.
  • Docs. content/docs/components/basic/sidebar.mdx gains "The side key" (what each edge draws; the narrow-viewport sheet side; the collapsible-only clause) with a JSON fence the dev's check:doc-snippets judged on both faces, and the "renderer reads" sentence now names side and variant beside className — true of SidebarNode's spread. RIGHT. The TS "Schema" block does not show side; not owed, named under the reviewer's flags.
  • Parity ledgers — nothing was owed, and nothing moved. The claim's file surface named "the registry or parity rows that record ui:sidebar.side": no such row exists — a grep at the head over scripts, apps and packages outside the touched files finds no ledger recording ui:sidebar.side or sidebar.side, and the registration already offered side. zod-mirror-parity.test.ts registers navigation.zod.ts#SidebarSchema against Ts_SidebarSchema; both faces now carry side, so no UnmirroredDeclared / MirroredUndeclared row moves (the dev's A1 and A2 show the ledger reds on a one-face deletion, as it should). check:component-surface-parity is REPORT-ONLY by ruling — its header: "Exit: 0 -- ALWAYS, disagreements included", quoting 「report-only 起步,零分歧后翻阻断」 — so the H2 reading (below, ③) is a count on a report, not a gate.
  • Changeset — one new entry, no dated notes. RIGHT; priced in ②. Changeset Claim Re-read and Changeset Overwrite Report are green on the head, and the dev's check:changeset-claims read no pending changeset naming a touched file.
  • Exclusions held. No strict-authoring-face.ts; no objectql.ts / zod/objectql.zod.ts (objectui#6152 round 8's surface); no packages/components/src/ui/** (the no-touch zone); no renderer source (renderers/navigation/sidebar.tsx is unchanged — the node never named side and still does not); no package.json; no governed path among the six (Governed Surface Queue Guard green; the dev's check-governed-queue-guard --test read NOT GOVERNED); 247 changed lines.
  • Head = the reviewed head. f835af41's one parent is the merge base 70e3d772; the commit is the dev's and carries the model-free trailer pair (Claude-Session / Co-authored-by: Claude); every check-run ran on it; the net diff's file set and line counts (6 files, +235/−12) equal the PR's; the PR body opens "Part of spec(types): 32 keys the renderers read are refused by the strict authoring face — declare each or rule it runtime-only, the ruled precondition before objectui validate goes strict (objectui#5250) #11070 (round 12)" (no closing keyword anywhere near a card number; 0 angle brackets), ends with the session-URL footer the PR-body channel keeps, and carries only labeler.yml's four labels (documentation, package: types, package: components, tests).

② Semver level

  • The PR body's second line reads Clause-②: yes followed by em-dash prose naming both effects (widens: the strict face accepts sidebar.side; narrows: the tolerant face refuses a side outside the two values) and matches the claim 6058868109 (Clause-②: yes — "declaring side widens what the strict authoring face accepts"). The changeset's own bolded line reads Clause-②: yes (widening) and names the narrowing in the same sentence. Judged against what the diff publishes: the strict accept set of a published export (StrictAnyComponentSchema's sidebar arm, and SidebarSchema from @object-ui/types/zod) widens by one key; the tolerant face narrows on that key's value; the TypeScript face narrows for a top literal. The (widening) arm is the reading rounds 10 and 11 recorded for this mixed shape on this card (5932079845, 5934528537: the widening IS the published strict-face move, and the break rides the entry under its BREAKING heading) — RIGHT by that precedent, and yes takes at least minor. Named without changing the verdict: the fleet's one reader of the line, clause2-line.mjs, spells a diff that widens one surface and narrows another as yes (narrowing) ("both facts are true and both are read") and reads yes (widening) as "the first, said out loud" — true here, but the lesser of the two spellings; no objectui gate reads the arm (check-adr-0087-registration is objectstack's), and the BREAKING banner is typed, so nothing is unpriced. Whether the dev flips the arm before landing is the seat's call.
  • .changeset/11070-sidebar-side.md: @object-ui/types minor. MATCHES what the diff publishes. types is the one released package whose source moves (navigation.ts, zod/navigation.zod.ts); @object-ui/components carries a new test file only, which rides the fixed group with no bump of its own, as rounds 4, 6, 7, 10 and 11 read test-only edits; content/docs is not a released package. A widened accept set is a feature and the narrowing half is breaking, priced minor with the break stated — the fixed group's ceiling (never major: check-changeset-no-major and the Changeset Bump Policy gate), as round 1 and rounds 3 through 11 priced this class. Unlike round 11's entry, which the previous record named as carrying no BREAKING banner, this one carries the banner, the FROM → TO fix and the pricing sentence. Every sentence of the entry was read against the diff — the two values and their origin, "the node hands the key to shadcn through the props it forwards, so it always drew; only the declaration was missing", the TypeScript row, the zod WIDENS / NARROWS row, the position row, the BREAKING paragraph — and each holds. Changeset Bump Policy, Changeset Fixed Group Check, Changeset Declaration, Changeset Claim Re-read and Changeset Overwrite Report are green on the head.

③ Boundary flags

Dev flags (report 6059807928: status: done, one open_questions entry with three routes, two out_of_scope_findings, three deviations, the NOT MEASURED list; the PR body's "Acceptance notes"), each answered or escalated:

  • Open question — routes for the three class (ii) pairs the re-run leaves (A / B / C, the dev recommends all three in one round). Each premise was checked at the spec tag, and each route is ANSWERED here for the seat to book; none moves this head.
    • A, form · fields[].scale, declare by reference — concur, with the caveat made a required measurement. At 17.7.0 FieldSchema (surface: 'this field', a strictObject) declares scale as z.number().int().min(0).max(MAX_RENDERABLE_SCALE), and its .superRefine refuses scale on a currency field (its describe: "REFUSED on a currency field — delete it there", ruling 5791803339 in the member's own comment). So the dev's caveat is real, not hedging: a flat form-field face taking .shape.scale would carry the type and bounds but not the cross-key refusal, and a type: 'currency' form field writing scale would parse on objectui's faces while the spec refuses it. The next round either carries that rule on FormFieldSchema or states the width deliberately, measured first.
    • B, form · fields[].currency, runtime-only with document corrections — concur. FieldSchema declares no currency key at 17.7.0 (the currency: z.string().length(3) in that file belongs to CurrencyValueSchema, the runtime value shape, and currency() is the builder factory); CurrencyConfigSchema is a strictObject with currencyMode: 'dynamic' | 'fixed' and defaultCurrency, and it aliases currency to defaultCurrency INSIDE currencyConfig. Declaring fields[].currency would contradict the spec — the claim's stop condition — and currencyConfig: { currencyMode: 'fixed', defaultCurrency } is the spec's home, which resolveFieldCurrency's second limb already honours per the dev. Whether that helper's first limb retires is a liveness question with other feeders (the metric widget, dataset measures, grid columns) and is not this card's.
    • C, action:button · properties.endpoint, runtime-only with the document corrected — concur. The spec's own pin component-action-row-endpoint-21005.test.ts holds that action:button / action:icon refuse endpoint with the rename to target read from action-target-aliases.ts; the key is red on both objectui faces already. Correcting guide/schema-rendering.md's fence and sentence to target is the whole fix; retiring ActionRunner's deprecated endpoint limb is a separate card, as the dev says.
    • The card's executable acceptance ("0 class (ii) pairs, or each ruled with its documents corrected") is therefore correctly reported as NOT met, and the PR says "Part of", not a closing keyword. Not a flag against this PR.
  • Out-of-scope (1): check:component-surface-parity cannot see a key a renderer honours through a forwarded rest spread, so after the declaration ui:sidebar reads interface-extra-key: side beside the unchanged input-outside-keyset: side (the dev's H2 second half, falsified) — the shape variant has carried since objectui#11465. The gate is report-only by ruling and its forwardsRestProps hedge is by design, so no card is owed from this round and the reading is not a regression anything blocks on. ESCALATED as the seat's call, not a flag against this PR: counting a rest-spread forward as a read is a gate design change whose evidence belongs with the flip-to-blocking card, and the alternative — a named read of side in SidebarNode — is a renderer change outside this claim.
  • Out-of-scope (2): the 32 class (i) pairs (48 occurrences, 35 strict-only, a source-read grade) are routed to objectui#5250 slice B. Concur: the card body itself leaves the wiring and its class (i) census with objectui#5250.
  • Hypotheses — H1 held and was refined (three corpus pairs plus the probe-measured sidebar.side; options.description absent at both ends); H2's first half held (input-outside-keyset: side at the base) and its second half is falsified as above. Each is a reading of the brief's hypothesis, not a correction to the work.
  • Deviations — (a) the commit carries the model-free trailer pair rather than the harness reminder's model-named trailer: that is both repositories' AGENTS.md rule, read on the commit, and the PR footer is the session-URL form the PR-body channel keeps; (b) fetching PR feat(cli): objectui validate and objectui check judge through the strict authoring face (objectui#5250, slice A) #11069's branch left a remote-tracking ref origin/claude/issue-5250-strict-validate-wiring in the shared .git: a container-state note with no artefact in this diff, named so the next reader does not take it for a claim; (c) the relay's actions file and PR body lived in the per-issue scratch directory with issue-prefixed names and were deleted. None verdict-affecting.
  • Beyond the claim's named files — the docs page (the docs-driven commandment) and the components pin (the declaration held against the live registration, the sibling of objectui#11465's variant pin): both inside the claim's purpose, both judged RIGHT in ①. The excluded objectql.ts / zod/objectql.zod.ts are untouched.
  • NOT MEASURED by the dev — CI on this PR (answered above by the head's check-runs); repository-wide pnpm lint and pnpm test (Lint and the eight Test shards); check:readme-exports (README Export Check green). The M3 scratch instrument and the probes are not committed, as the PR body says, and are not re-derived here.
  • The pointer 5956633122 — answered by this diff: the declaration it asked for, on both faces, with the registration's own two values, and the position pointer objectui#11465 withheld.
  • The release record 6058770188 — its "run first" item was run first (M3 at the base before the declaration, then at the head: the pair list identical, the docs corpus moved by the new fence alone — the dev's reading); its "not owed" note on .changeset/7293-dataset-metric-subcaption.md holds (nothing here touches it); its "no new blocker" reading holds for this diff.

Reviewer's flags, none verdict-changing:

  • The Clause-② arm reads (widening) where the fleet reader's table spells the mixed shape (narrowing) — ② above; consistent with rounds 10 and 11 on this card, unread by any objectui gate, and the break is banner-stated. Named so the next record does not read the precedent as the reader's rule.
  • side is ACCEPTED on every sidebar node and HONOURED on the collapsible form only. A document with collapsible: false and side: 'right' validates on both faces and draws the same in-flow column as left — exactly what the docblocks, the docs and the render pin say. Named so a later "side does nothing" reading is attributed to collapsible, as variant already is.
  • The zod value is restated, not referenced. z.enum(['left', 'right']), the registration's enum: ['left', 'right'] and shadcn's "left" | "right" are three spellings with no shared constant, held together by the components pin reading the live registration and the type-level Equal rows — the same standing as variant since objectui#11465, and the only standing available while the spec has no row.
  • The reach pin mounts the node in a bare host, so the drawn markup is the node's own provider's; the app-shell host path (a provider above the node) is not drawn by it. The forwarding is the same {...props} spread either way, so nothing on the record contradicts the reach claim; named so it is seen to rest where it rests.
  • The docs TS "Schema" block omits side while the new JSON fence carries it; the prose names it. Not owed; a later docs pass may add it to the typed example.
  • The M3 figures (622 node documents, 58 strict-refused, 31 strict-only, 27 red today; 35 pairs, 3 class (ii), 0 class (iii); PR objectui#11069's own findUndeclaredKeys copied from a11f73347d) are the dev's uncommitted instrument and are quoted here as the dev's. Nothing on the head contradicts them; they are not a live count.

Nothing else in the diff, the card or the check-runs contradicts the claim, the release record (6058770188), the pointer (5956633122), the card's rulings (the triage direction 5888862869), the two bound rulings the triage named (objectui#5097 host composition — side is an authored key, not a host prop; objectui#5091 grid non-author keys — no grid file is touched), objectui#11465's retirement of position (still refused by name on every face), objectui#8347 (no index signature, no as any, no open type: string arm), ADR-0049 or the startup no-gradualism rule.

Implemented-by: claude/issue-11070-m3-rerun-sidebar-side
Reviewed-by: session_01DBZ9bntPZ7VKyQNtJeNsgw

VERDICT: PASS

Read at 2026-10-08T12:39Z.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 8, 2026 12:41
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 8, 2026 12:41
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 172acc3 Oct 8, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11070-m3-rerun-sidebar-side branch October 8, 2026 13:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation package: components package: types tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants