Repository navigation
feat(types): the sidebar node declares side on both faces; M3 re-run on the merged ref (objectui#11070 round 12) - #11964
Conversation
…#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>
✅ Console Performance Budget
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
Size Limits
|
Contract reviewServed-tier: PR objectui#11964 (draft, 1 commit ① Derived judgmentsEvery accept-set and public-surface move the diff implies, each judged:
② Semver level
③ Boundary flagsDev flags (report
Reviewer's flags, none verdict-changing:
Nothing else in the diff, the card or the check-runs contradicts the claim, the release record ( Implemented-by: VERDICT: PASS Read at 2026-10-08T12:39Z. Generated by Claude Code |
Part of #11070 (round 12)
Clause-②: yes — widens: the strict authoring face (
StrictAnyComponentSchema) now acceptssidebar.side('left'/'right'), which it refused withunrecognized_keys; narrows: the tolerant face (safeValidateSchema) now refuses asideoutside those two values (invalid_valueatside), which it kept unjudged.Round 12 of objectui#11070, claim
6058868109, sessionhttps://claude.ai/code/session_01DBZ9bntPZ7VKyQNtJeNsgw. Measurement first: PR objectui#11069's M3 classification re-run on the merged ref, thensidebar.sidedeclared 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/mainwhen the branch was cut; the seat readd92b2a15at dispatch, and the one commit between them, objectui#11923, touches app-shell only).@object-ui/typesbuilt 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'spnpm checkpopulation (every other tracked.json). Each node document is judged byStrictAnyComponentSchemaandAnyComponentSchemafrom the builtpackages/types/dist/zod. Refused keys are read with PR objectui#11069's ownfindUndeclaredKeys, copied verbatim from its heada11f73347d. 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 roottype:, against 41 that carry a rootname: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.examples/hello-world/schema.json)The 20 documents refused without naming a key are all refused by the tolerant face too, for value reasons (
carouselitems,resizablepanel ids, child-slot unions, oneonSubmit).Classified (PR objectui#11069's classes; its M3 figures at
f6ae5e22din brackets):Class (iii) is 0: the
UnmirroredDeclaredledger inzod-mirror-parity.test.tsnames none of the refused keys.options.descriptionis gone from the refused population entirely (0 hits).Class (ii), each with its read site
form·fields[].scalefields-number/with-decimal-places,fields-percent/required-percent,fields-percent/with-decimal-placesform.tsxhands the entry to the field widget asfield(field: field.field || field);PercentFieldreadspercentField?.scale(throughresolveFieldScale),NumberFieldreadsnumberField?.scaleFieldSchemadeclaresscale(17.7.0)stripImportedDefaults(SpecFieldSchema).shape.scale/SpecField['scale']), round 2's pattern. New since PR objectui#11069 measured: objectui#11255 moved these fixtures fromprecisiontoscale, the key the widgets read. Measure first: the spec refusesscaleon acurrencyfield, and a flat form-field face taking.shape.scalewould not carry that cross-key rule.form·fields[].currencyfields-currency/euro-currency,fields-currency/usd-currencyCurrencyFieldresolves its code throughresolveFieldCurrency(@object-ui/i18n), whose first limb isfield?.currencyFieldSchemarefusescurrencyas a field key; its home iscurrencyConfig(currencyMode: 'fixed'+defaultCurrency)currencyConfig, whichresolveFieldCurrency'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'scurrencyand grid columns.action:button·properties.endpointguide/schema-rendering.md, the "Reference actions in schemas" fenceaction-button.tsxforwardsschema.endpoint;ActionRunnerresolvesapi || endpoint || target, itsendpointlimb marked deprecatedaction:buttonrow (17.6.0+) refuses it in favour oftarget; both faces refuse it with "Did you meanendpoint→target?"endpoint") totarget. No declaration: the spec already ruled.sidebar·sidesidebarnodes at base, none withside)Sidebar, reached through the propsSidebarNodeforwardssidebarrowClass (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 readscolSpan),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 customCombobox);empty·message(2);navigation-menu·items[].items(2; the renderer readschildren);app·pages;input·validations;label·required;grid·actions,pagination,rows,title;select·field,optionsFrom;data-table·dataSource,columns[].key,columns[].title(the renderer readsaccessorKey/header). A source-read grade, as PR objectui#11069 said of its own: a key can still arrive through a prop spread.sidebar.sideMeasured first:
@objectstack/spec17.7.0 has nosidebarrow.ComponentPropsMapholdspage:sidebar(the page region, whose row declareschildrenonly), and no spec export names a sidebarside. So the route is the local one: declared flat, with the values the renderer honours, as round 7'scolumnsand 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.navigation.zod.ts):side: z.enum(['left', 'right']), optional, with a refusal message naming the two values.positionkey stays refused by name on every face. Its refusal now also saysside: 'right'draws the right edge. objectui#11465 deliberately did not point there whilesidewas undeclared.content/docs/components/basic/sidebar.mdxgains asidesection with a JSON example, and its "the renderer reads" sentence now namessideandvariant.Reach, through the real
SchemaRendererand registry (the new components pin): on the collapsible formrightdrawsdata-side="right"with the panel atright-0,leftdraws the same markup as an absent key, and withcollapsible: falseevery value draws the same column. Built-dist probe at the head:side: 'left'and'right'ACCEPT on both faces, and'top'is refused atsidewithinvalid_valueon both. A control key on the same node is still refused withunrecognized_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 newsidefence, 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
sidebar.sideas a probe-measured member with no corpus occurrence.options.descriptionis 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). Thesidebar.sidereading was re-taken: the base faces refuse it (the R0 rows below), and the renderer draws the right edge (the render pin).check:component-surface-parity --type ui:sidebarreportsinput-outside-keyset: side. After the declaration it does not go quiet: it keepsinput-outside-keyset: sideand addsinterface-extra-key: side. That is the shapevarianthas carried since objectui#11465. The mechanism is in the gate'sjudgeRegistration: the accept set is spec ∪ named renderer reads ∪ props-channel reads ∪ framework reads, and the TS interface never enters it.SidebarNodehandssideto shadcn through its rest spread, which the gate hedges (forwardsRestProps) but does not count as a read. Whole run:interface-extra-key101 → 102 (rest-spread-hedged 97 → 98),input-outside-keyset63 → 63. The gate is report-only and exits 0. Quieting it would take a named read ofsideinSidebarNode, 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=4096and--maxWorkers=2. Subjects resolve from source through the root vitest alias, so no leg needs adistrebuild.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 siblingsideeis refused by name.topand a boolean are refused on both faces, atside, withinvalid_value. The retiredpositionis still refused by name, and its refusal namesside. Type-level equality holds on both faces, with a@ts-expect-errorfortop. Newcomponents/.../sidebar-side-registration-11070.test.tsx, the sibling of objectui#11465's variant pin, holds the declaration against the live registration'ssideenum on both faces, plus the render half.HEADwith the blob equal to HEAD's andgit diff HEADempty.tscerrors, all in the pin. ObservedTests 8 failed | 145 passed (153)and 3tscerrors in the pin (TS2339 twice, TS2353), none elsewhere.sidemember deleted through objectstack'sscripts/ablation-replace.mjs(anchor 1 → 0, blob75cd14ca→a0108550). Predicted 7 red and 2tscerrors. ObservedTests 7 failed | 146 passed (153). The twotscerrors: the pin'sshape['side'], andzod-mirror-parity'sassertionUnmirroredMatchesLedgernamingnavigation.zod.ts#SidebarSchema.sidemember deleted (anchor 1 → 0, blobea91bf5c→52bfcff3). Predicted 0 red and at least 3tscerrors. ObservedTests 153 passed (153)and 4tscerrors: 2 in the pin, plusassertionMirroredUndeclaredMatchesLedgerandassertionMirroredUndeclaredRecordsEveryKey.f835af41(packages/types/, the wholescripts/__tests__/, and the three sidebar component pins):Test Files 542 passed | 2 skipped (544),Tests 15100 passed | 2 skipped (15102).@object-ui/types(its script line@object-ui/types@17.7.0 type-checkechoed; itstsconfig.test.jsonleg lists both pin files) and@object-ui/components(echoed; its test leg compiles the new pin). Both exit 0, with 0error TS.Gates at
f835af41objectui has no
dispatch-gates, so this set is the brief's list plus what the diff touches, derived by hand from the rootpackage.json. All exit 0: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-filterclosure),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.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, andcheck-governed-queue-guard --test(NOT GOVERNED)..ts/.tsxfiles with the rooteslint.config.jsand--no-inline-config:**/*.{ts,tsx}blocks.--format jsonlists 4 files, with 0 errors and 0 warnings. The three that exist at the base read the same 0/0 through--stdin.parserOptions.projectorprojectService, and noeslint-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-widepnpm lintandpnpm test, reason: CI-owned. NOT MEASURED: CI on this PR, reason: opened with this report; convergence is the seat's.Acceptance notes
objectui validategoes strict (objectui#5250) #11070 remains open. Its executable acceptance needs 0 class (ii) pairs, or a runtime-only ruling plus corrected documents for each. Three remain (fields[].scale,fields[].currency,properties.endpoint), each with a recommended route above, for the seat to book.sideis accepted on everysidebarnode and honoured only on the collapsible form, asvariantis. The faces stay flat.@object-ui/typesminor. The strict accept set widens, and the tolerant face's narrowing on a value outside the two edges is stated as BREAKING.Generated by Claude Code