Skip to content

fix(react,components): make toRenderableSchema behaviour-preserving for the falsy primitives (objectui#8908) - #9029

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-8908-bridge-falsy-primitives
Sep 10, 2026
Merged

os-steve merged 1 commit into
mainfrom
claude/issue-8908-bridge-falsy-primitives

Conversation

@claude

@claude claude Bot commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #8908

toRenderableSchema's docblock promised "the value a caller forwards renders identically whether or not it passes through here". That is now true. It was false for exactly two of the six SchemaNode members, and the fix is in the bridge — the behaviour, not the docblock, per triage.

What changed

packages/react/src/schema-input.ts — the mapping has two legs and mirrors SchemaRenderer's own narrowing order instead of half of it:

return typeof node === 'number' || typeof node === 'boolean'
  ? (node ? String(node) : undefined)
  : node;

A truthy number / boolean still becomes its text form, which is what the renderer's defensive branch produces. A falsy one — 0, -0, NaN, false — becomes nothing, which is what the renderer's !evaluatedSchema leg produces. !node is exactly that set for the two members this function converts, so the ternary IS the renderer's order.

⚠️ A fork the dispatch asked to be flagged: route 2 as literally worded is not available

The brief assumed route 2 — "guard the bridge so 0 / false pass through unchanged" — and asked me to stop and report if any call site would break under the resulting type widening. It would, at every one of them, so the delivered repair reaches the same behaviour without widening anything. Measured, not assumed:

  • The return type is pinned exactly equal to SchemaRendererProps['schema'] by packages/react/src/__tests__/SchemaRenderer.propsResolution.test.ts (assertion 7, Equal< ReturnType< typeof toRenderableSchema >, SchemaRendererProps['schema'] >). Exact equality, so any widening fails it.
  • The same file pins number extends CallSiteProps['schema'] and the boolean twin as false (objectui#4548 ruling Q2). So widening the prop instead is blocked by the same suite from the other side.
  • 32 call sites, and every single one is a direct schema={toRenderableSchema(…)} forward into SchemaRenderer — 19 in shipped source (apps/site 5, packages/components 5, plugin-detail 6, plugin-report 2, plugin-view 1) and 13 in tests/examples. Not one calls a method on the result, spreads it, or narrows it. A widened return would therefore be a TS2322 at all 19 shipped ones — the exact failure the docblock records from PR fix(types): one SchemaNode and one label vocabulary — the union wins, labels resolve where the locale lives (#4580) #4608, which kept Build Docs red on main for ~5 hours.

⇒ Returning nothing for the falsy pair keeps the guarantee and the type. The published type face is byte-identical; only runtime behaviour moves. If you want the literal pass-through instead, that is a published-surface type break and it is yours to rule, not mine — say so and I will re-cut it.

Acceptance — the card's own two tables, re-measured

Both are executable pins now, and both were measured on the pre-fix bridge during the ablation (the "before" columns are that run's actual output, not a restatement of the card).

Six-row raw-vs-bridged, packages/react/src/__tests__/schema-input.bridgeFidelity.test.tsx:

value raw bridged (before) bridged (after) same
0 "" "0" "" now true
false "" "false" "" now true
42 "42" "42" "42" control, true both legs
true "true" "true" "true" control, true both legs
"" "" "" "" control, true both legs
"txt" "txt" "txt" "txt" control, true both legs

Three-row empty renderer, packages/components/src/__tests__/empty-action-falsy-primitive-8908.test.tsx:

empty.action text (before) text (after)
0 "T0" "T"
false "Tfalse" "T"
"" "T" "T"

The four same=true rows are live controls: they are asserted on both legs of the ablation and stay green on both, so "renders nothing for everything" cannot pass as a fix. Every row asserts both facts the brief required — that the two paths agree, and the concrete text each one produced — because an agreement-only assertion passes on a harness where the two could never have disagreed.

All six SchemaNode members are pinned, not just the broken two

Enumerated from the declaration (packages/types/src/base.ts), not from the card: BaseSchema | string | number | boolean | null | undefined. Each has an arm rendered raw-vs-bridged, including the whole falsy-number boundary (0, -0, NaN) and both truthy directions (42, Infinity).

The "cannot regress silently" half is mechanical, not a table someone has to remember to extend: MEMBER_CASES is a Record< MemberName, Case[] >, so every name needs an arm, and _MembersAreExactlyCovered pins MemberName to the declared union itself. Add a seventh member to SchemaNode and the file stops compiling until someone decides what it renders as.

objectui#8331's truthiness leg: KEPT, and here is the explicit justification

Not removed, and not left to vanish as tidying. data-table's emptyAction leg (schema.emptyAction ? … : null) is now redundant — with the bridge repaired, 0 / false reach the same answer whichever side decides it. It stays because:

  1. It is what makes that slot's answer independent of the bridge, and that independence is precisely why emptyAction was never affected by this defect while empty.action was.
  2. Its pins assert the OUTCOME, so they would stay green through a removal right up until the bridge regressed again. Removing the leg would spend a working guard to buy nothing.

Its rationale comment said the bridge is not behaviour-preserving for 0 / false. That was true when written and my change makes it false, so it is corrected in place rather than left to mislead — same for the docblock and one inline comment in data-table-empty-action-primitive-node.test.tsx, and for the bridge paragraph in empty.tsx. No assertion in either existing suite was weakened, retargeted or deleted; only prose moved. SchemaRenderer.primitiveSchema.test.tsx (objectui#4548's pin, the invariant this card restores) is untouched and green.

Ablation — prediction written before the run, then compared

Predicted red set was recorded before running: 8 tests across 2 files, named individually. Result: 8 failed | 69 passed (77), 2 files — every predicted name matched, no extras, no misses, no miscount to report.

The mutation was proven to land on disk before the reading was taken (marker counts flipped 1→0 and 0→1; blob hash 5635797… → fe15dce…), and the restore leg was verified by hash equality against the HEAD blob rather than by an exit code, under a trap … EXIT INT TERM with absolute paths. @object-ui/react is vitest-aliased to packages/react/src, so no build stands between the edit and the run and there is no stale-dist failure mode here.

Green on both legs, as predicted: SchemaRenderer.primitiveSchema.test.tsx, SchemaRenderer.propsResolution.test.ts, data-table-empty-action-primitive-node.test.tsx, empty-action-declared-7105.test.tsx, overlay-node-slot-doc-types-7082.test.ts.

Verification

what command result
targeted suites (2 new + 5 existing pins) pnpm exec vitest run on those 7 paths Test Files 7 passed (7), Tests 77 passed (77)
affected packages, full pnpm exec vitest run packages/react/ packages/components/ Test Files 336 passed (336), Tests 3273 passed (3273)
type-check pnpm --filter @object-ui/react --filter @object-ui/components run type-check both Done (this is what checks the compile-time pins)
lint pnpm --filter @object-ui/react --filter @object-ui/components run lint exit 0, 0 errors (948 pre-existing warnings, none on the changed lines)
build pnpm --workspace-concurrency=2 --filter "@object-ui/react..." --filter "@object-ui/components..." run build exit 0
gates check:control-bytes, check:new-line-citations, check:unreferenced-sources, check:comment-mask-corpus, check:changeset-presence, check-changeset-no-major all exit 0

NOT MEASURED: check:sdui-registration-pins, reason: PREREQUISITE NOT MET — it reads the built console bundle and asks for pnpm --filter @object-ui/console buildfirst; declared to CI. Its subject did not move: the diff toempty.tsx is comment lines only (verified), and no registration bytes changed anywhere. Everything else repo-wide is CI's run, not mine.

Snapshots in the brief that did not survive re-derivation

  • app-shell has zero call sites. Control: 50 files there mention SchemaRenderer, so the scan reaches the package and finds things in it. The card's list also omits apps/site (5 — the very package the docblock records breaking Build Docs), examples/schema-catalog (12) and packages/plugin-map (1). "Roughly fourteen" is exactly right for the four packages the card listed that really do have sites; the set was wrong, not the arithmetic.
  • The card's six-row table is six VALUES, not six members — it samples string / number / boolean twice each and never touches BaseSchema, null or undefined. A "pin all six members" suite built from that table would have missed half the union, which is why the members were enumerated from the declaration instead.
  • Confirmed unchanged on today's tree: the renderer still narrows !evaluatedSchema above the String(…) defensive branch, and the evaluation memo returns every primitive untouched, so the card's mechanism holds exactly as written.

Acceptance notes

  • Filed as objectui#9033 while enumerating call sites: header-bar's rightContent slot still guards with an && chain, so rightContent: 0 leaks the raw number and paints a stray "0" — the numeric-falsy JSX trap objectui#8331 closed one slot over. This change does NOT close it: the && short-circuits before the bridge is ever called. Measured through the real renderer stack ("Toggle Sidebar0" against a "Toggle Sidebar" baseline), probe deleted, tree verified clean. It is the only remaining instance in the tree. Deliberately not fixed here — different mechanism, different file, and the guard's truthiness-vs-nullish question deserves its own ruling now that the bridge is honest.
  • The pending .changeset/8331-…md release note contains one sentence this change falsifies (the bridge's String mapping "would otherwise turn them into the text "0" and "false""). I did not edit another card's release note; my changeset explicitly supersedes that sentence instead, so a reader of the combined CHANGELOG is not left with the contradiction. Noted, not filed — the successor text ships in the same release.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB


Generated by Claude Code

…or the falsy primitives (objectui#8908)

`toRenderableSchema`'s docblock promised "the value a caller forwards renders
identically whether or not it passes through here". That was false for exactly
two of the six `SchemaNode` members: the bridge mapped every `number` /
`boolean` onto `String(node)`, and `String(0)` / `String(false)` are the
NON-EMPTY strings '0' and 'false', which are truthy — so they sailed past
`SchemaRenderer`'s own `!evaluatedSchema` leg and rendered as their own text,
while the raw values render nothing (pinned since objectui#4548).

The shipped, reachable instance: `empty`'s `action` slot gates on nullish
(objectui#7105, so a bare string node renders), so
`{ type: 'empty', title: 'T', action: 0 }` painted a stray "T0".

The mapping now has two legs and mirrors the renderer's own narrowing order: a
truthy primitive becomes its text form, a falsy one (0, -0, NaN, false) becomes
nothing. The return type is UNCHANGED — passing the values through unchanged
would have widened it to admit `number` / `boolean`, which
`SchemaRenderer.propsResolution.test.ts` pins exactly equal to a prop that
refuses both by ruling (objectui#4548 Q2), and would have been a TS2322 at all
nineteen call sites that forward the result straight into a `schema=` prop.

Pins: all six `SchemaNode` members are rendered raw-vs-bridged and compared,
asserting both that the two paths agree and the concrete text each produced;
`MemberName` is tied to the declared union so a seventh member cannot skip the
guarantee. objectui#8331's `data-table` truthiness leg is KEPT — it is now
redundant rather than load-bearing, and its rationale is corrected in place
rather than allowed to go stale.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3489.9 KB 3512.7 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-DfsZok7b.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.69KB 6.21KB
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) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
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.41KB 1.23KB
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.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
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) 26.08KB 7.56KB
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) 500.20KB 114.67KB
core (index.js) 7.95KB 3.19KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 207.56KB 57.44KB
fields (index.js) 247.01KB 62.29KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
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) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
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.39KB 3.10KB
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) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.64KB 19.98KB
plugin-chatbot (index.js) 195.32KB 46.51KB
plugin-dashboard (index.js) 130.98KB 34.54KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 251.44KB 65.17KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.65KB 40.91KB
plugin-grid (index.js) 211.36KB 57.45KB
plugin-kanban (index.js) 46.03KB 14.30KB
plugin-list (index.js) 112.52KB 27.64KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
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) 83.34KB 27.61KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
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) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
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) 14.27KB 5.47KB
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

Copy link
Copy Markdown
Collaborator

In-seat review of record — Clause-② (contract delta), domain:ui @ objectui seat

Verdict: land. Both open questions ruled below; neither needs a change to this PR.

Clause-② was declared yes on the card before dispatch (objectui#8908 comment 5624273917) — toRenderableSchema is published via packages/react/src/index.ts:10, and its output changes for two inputs it already accepts. Per objectstack#17285 this lane runs the default tier for both build and review and the lane seat's own review plus the gates is the review of record, so this comment is it.

⭐ Stopping at the fork was the right call, and the measurement holds

You were told to STOP and report if any call site would break rather than take the literal route 2. You did, and I re-derived the numbers independently rather than accept them:

reading mine control
non-test toRenderableSchema( call sites 19 —
of those, forwarding straight into schema={…} 19 same probe, so the ratio is not an artefact
in packages/app-shell 0 50 files there match SchemaRenderer, so the scan reaches the package

By package: plugin-detail 6, components 5, apps/site 5, plugin-report 2, plugin-view 1.

⇒ All nineteen forward the result directly into the prop, so widening the return type really would be a TS2322 at every one of them. The fork was real, not defensive.

Your falsification of the card's own call-site set also reproduces: the card named app-shell (which has none) and omitted apps/site (which has five). And its arithmetic was fine — the four listed packages that do have sites total exactly 14, so "roughly fourteen" was a right count of a wrong set. That is lane fact ⑰ landing on a card body again.

Ruling — question 1: option A, what shipped. Keep it.

(node ? String(node) : undefined) reaches the same rendered outcome as passing the value through, because undefined and a raw 0 both hit SchemaRenderer's !evaluatedSchema leg first. So the docblock's promise — "the value a caller forwards renders identically whether or not it passes through here" — is now true as written, which is what triage asked for.

Options B and C both spend a published-surface type break to buy a distinction no call site can observe, since all nineteen render the result rather than inspect it. Both also require reopening objectui#4548 ruling Q2, which deliberately keeps number / boolean off the prop — that is a published-contract question and manual floor, not mine to take and not yours. ⛔ Do not re-cut it as B.

⚠️ One honest consequence, stated so it is on the record rather than discovered later: the function is now behaviour-preserving, not value-preserving. toRenderableSchema(0) is undefined, not 0. Your docblock says exactly this and explains why, which is the right place for it.

Ruling — question 2: no, do not hang needs:contract-review. You were right not to invent governance.

Measured, control lit: no workflow in objectui reads that label — it is PM tooling state, not a CI gate. And per objectstack#17285 the contract-review tier does not serve this lane at all, so hanging it would bind the door to a reviewer who does not exist here. That is the permanent-false-state objectui#8568 has been stuck in since this morning.

The declaration already lives on the card, and this comment is the review it summons. Noting that check-clause2-carriers will fire C3 on the pair for exactly that reason — disclosed, not routed around, and going to the maintainer grouped with objectui#8568, objectui#8973 and objectui#8943.

On the pins

The completeness pin is better than what triage asked for. MEMBER_CASES keyed by Record over MemberName, plus _MembersAreExactlyCovered tying that hand-written union to the declared SchemaNode, means a seventh member stops the file compiling until someone decides what it renders as — a compile-time completeness pin rather than a runtime list that can silently go stale.

And the oracle is right: both paths rendered and compared, plus the concrete text each produced, with the file's own docblock explaining why agreement alone would be worthless. -0, NaN and Infinity are additions the card never named and are the three most likely to be got wrong by a later "simplification".

⭐ You kept objectui#8331's truthiness leg and said in the diff that it is now redundant-but-retained, with the reason — and you downgraded that suite's affected cases from discriminating evidence to controls in its own docblock rather than leaving them reading as proof of something they no longer prove. That is the standard; a silent retention would have left the next reader with a false account of why the leg exists.

objectui#4548's primitiveSchema pin is untouched, as fenced.

Gates

Landing on every-check-green, not on this comment. The out-of-scope finding you filed as objectui#9033 (header-bar's rightContent && chain leaking a raw 0) is correctly out of scope here — it short-circuits before the bridge is reached, so this PR genuinely does not close it.


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review September 10, 2026 19:50
@os-steve
os-steve added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit 152f0a7 Sep 10, 2026
37 checks passed
@os-steve
os-steve deleted the claude/issue-8908-bridge-falsy-primitives branch September 10, 2026 20:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants