Skip to content

fix(react): UseNavigationOverlayOptions.onRowClick declares the modifier payload it is called with - #9360

Merged
claude[bot] merged 4 commits into
mainfrom
claude/issue-9357-onrowclick-arity
Sep 14, 2026
Merged

claude[bot] merged 4 commits into
mainfrom
claude/issue-9357-onrowclick-arity

Conversation

@os-tesla

@os-tesla os-tesla commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Part of objectui#9357

Spelling note: generic parameter lists are written as capitalised WORDS below
(RECORD for the record parameter type, OPT for an optional arm). GitHub's
body sanitiser deletes angle-bracket-shaped spans, code fences included, and a
before/after table whose two rows collapse into the same string is worse than
no table.

The defect, re-verified against origin/main before any edit

packages/react/src/hooks/useNavigationOverlay.ts, both halves byte-for-byte, at
2e471dc0aa23ff807aa9960618c419ecccdfdcef:

:142    /** External onRowClick callback — if set, takes full priority */
:143    onRowClick?: (record: RECORD) => void;

:265    // External onRowClick takes full priority. Forward the modifier event
:268    if (onRowClick) {
:269      (onRowClick as (r: RECORD, e?: HandleClickModifiers) => void)(record, event);

premise_still_valid: true — all four lines are exactly where the card says,
126 lines apart in one file. The declaration promises one parameter; the call
site asserts the declaration away to pass two. The assertion is the only thing
holding the two apart.

One correction to the dispatch note: HandleClickModifiers is not imported
into this file — it is declared and exported from this very file, fourteen
lines below the option. Naming it in the option therefore costs no import at
all, which is a stronger version of the same point.

The repair

  • UseNavigationOverlayOptions.onRowClick now declares both parameters:
    the record, and event?: HandleClickModifiers.
  • The assertion at the call site is deleted; handleClick now calls
    onRowClick(record, event) straight through the declaration.
  • No consumer is edited, and no runtime code moves. Two commits follow the
    repair and neither touches an executable line — see the next section.

What the follow-up commits changed

02b60d8ec is the repair above. Both commits after it are prose and pin only:

  • d8325fa79 — after the first contract review, the changeset stopped
    generalising. It now names the one refused class, quotes the TS2322 that
    class raises, gives the one-line remedy, and the pin grew a
    @ts-expect-error row that goes red (as TS2578-unused) if that boundary
    ever moves. The class: a handler whose second parameter is annotated
    narrower than HandleClickModifiers — React's MouseEvent in practice.
  • 0d983a987 — the second contract review found the same refuted
    generalisation still standing in the hook's JSDoc, which is the carrier that
    actually ships. Comments are not stripped for this package, so that block
    lands verbatim in dist/hooks/useNavigationOverlay.d.ts — the hover text
    every consumer of @object-ui/react reads. The sentence "a one-parameter
    handler stays assignable here, so nothing a caller already wrote has to
    change" is replaced by the qualified form the changeset already uses, so the
    CHANGELOG and the declaration now agree about the same line. Two residual
    restatements of the same claim in the pin's own prose ("nothing breaks either
    way", "no consumer is broken by the widening") are corrected the same way —
    both sit in a file this branch adds, and both were already contradicted 80
    lines below by that file's own ACCEPT-SET BOUNDARY block. Comment text only.

Consumers, enumerated — and which ones the repair reaches

Thirteen sites call useNavigationOverlay; nine of them feed the repaired
option. The repair reaches the option itself, so every one of these nine may
now pass a two-parameter handler without an assertion of its own:

consumer what it feeds the option
packages/plugin-grid/src/ObjectGrid.tsx onRowClick
packages/plugin-list/src/ListView.tsx onRowClick
packages/plugin-list/src/ObjectGallery.tsx props.onRowClick else props.onCardClick
packages/plugin-kanban/src/ObjectKanban.tsx externalClick
packages/plugin-calendar/src/ObjectCalendar.tsx onRowClick when not an overlay
packages/plugin-gantt/src/ObjectGantt.tsx onRowClick when not an overlay
packages/plugin-map/src/ObjectMap.tsx onRowClick
packages/plugin-timeline/src/ObjectTimeline.tsx onRowClick else onItemClick
packages/plugin-tree/src/ObjectTree.tsx onRowClick

Four call the hook without the option and are untouched either way:
app-shell's InterfaceListPage.tsx, ObjectDataPage.tsx and
ObjectView.tsx, plus the doc example in
packages/components/src/custom/navigation-overlay.tsx.

What the repair does NOT reach — each is a published face of its own, and
naming the payload on it is the ruling objectui#9357 leaves open:

  • ObjectKanbanComponentProps.onRowClick and .onCardClick (plugin-kanban)
  • KanbanRendererProps.schema.onCardClick (plugin-kanban/src/index.tsx)
  • ObjectGallery's onCardClick / onRowClick pair (plugin-list)
  • the onRowClick prop on ObjectGrid, ListView, ObjectCalendar,
    ObjectGantt, ObjectMap, ObjectTimeline, ObjectTree
  • ObjectGridSchema.onRowClick and ObjectKanbanSchema.onCardClick in
    @object-ui/types, where the card records that HandleClickModifiers is
    unreachable (phantom dependency plus a cycle) and event?: any is the
    established spelling

The three workaround call sites the card names — app-shell's ObjectView.tsx
at lines 2734, 3120 and 3163, each spelling (record: any, event?: OPT any)
are consumers of those component props, not of the hook option, so they
still need the workaround and are deliberately left alone.

Source compatibility — one class DOES break, measured here rather than inherited

Two assignability directions hold between the old spelling and the new one, and
both are asserted in the pin so they cannot silently stop holding:

  • a one-parameter handler is assignable to the widened option
    (_NarrowIsAssignableToWide);
  • a handler written against the widened option is assignable to the old
    one-parameter spelling (_WideIsAssignableToNarrow) — its minimum argument
    count is still one.

That is a statement about those two spellings and nothing wider. An earlier
revision of this section read "No consumer breaks in either direction". That is
false, and the probe below refutes it.

The one class that has to change. A handler passed directly to
useNavigationOverlay whose second parameter is annotated narrower than
HandleClickModifiers compiled before this branch and is refused now with
TS2322. React's MouseEvent is the shape this hits in practice, because the
payload used to be discoverable only from the implementation, so a host that
wanted it wrote the annotation it saw arrive. The parameter is checked
contravariantly, so the annotation now has to admit HandleClickModifiers.
The remedy is one line at that call site: annotate the parameter
HandleClickModifiers (exported from @object-ui/react), or drop the
annotation and let it be inferred. Either way the handler keeps receiving
exactly what it received before — a type-level change with no runtime behaviour
attached. The changeset publishes this class and this remedy, and as of
0d983a987 so does the shipped declaration.

The probe, re-run at 0d983a987. One temporary file per case under
packages/react/src/hooks/__tests__/, checked with
tsc -p packages/react/tsconfig.test.json in two worktrees: this branch, and
the fork point taken from git merge-base (b67b53bc0) — never from
base.sha, which is the base BRANCH TIP and not the fork point. Each probe was
hash-proved onto disk before its run and removed under trap ... EXIT INT TERM,
with absence re-checked afterwards.

probe at b67b53bc0 at 0d983a987
baseline, no probe file exit 0 exit 0
POISON control, a deliberate TS2322 exit 2 — 1 error, on the probe line exit 2 — 1 error, on the probe line
SUBJECT — second parameter annotated React.MouseEvent exit 0 — accepted exit 2 — one TS2322, on the subject line only
control A — one-parameter handler exit 0 exit 0
control B — unannotated handler exit 0 exit 0

The POISON row is what makes the zeros readable: without it, a probe the
compiler accepted and a probe the compiler never opened render identically.

⇒ the widening relaxes the option for anyone who wants the payload and
narrows it for exactly that one annotated class. No consumer inside this
repository
is in that class, and the repository's own type-check re-derives
that on every run rather than this sentence asserting it.

Why the pin is not an assignability assertion — with the null result to prove it

Because both directions hold, extends cannot tell the repaired declaration
from the broken one. Two instruments that can are used instead, each with
controls proving it can fire:

  • compile-time — an exact-identity read of the parameter list
    (Parameters and its length), run only by packages/react's
    tsconfig.test.json;
  • bytes — the declaration and the call site read off disk, root-anchored on
    the test file rather than the cwd.

Red-first, on the unmodified tree, verbatim

pnpm --filter @object-ui/react type-check — exit 2:

src/hooks/__tests__/useNavigationOverlay.onRowClickArity-9357.test.tsx(88,31): error TS2344: Type 'false' does not satisfy the constraint 'true'.
src/hooks/__tests__/useNavigationOverlay.onRowClickArity-9357.test.tsx(91,3): error TS2344: Type 'false' does not satisfy the constraint 'true'.
src/hooks/__tests__/useNavigationOverlay.onRowClickArity-9357.test.tsx(91,26): error TS2493: Tuple type '[record: Record of string to unknown]' of length '1' has no element at index '1'.
src/hooks/__tests__/useNavigationOverlay.onRowClickArity-9357.test.tsx(100,3): error TS2344: Type 'false' does not satisfy the constraint 'true'.
src/hooks/__tests__/useNavigationOverlay.onRowClickArity-9357.test.tsx(116,43): error TS2344: Type 'false' does not satisfy the constraint 'true'.

(the TS2493 message's own generic is respelled in words for the reason at the
top; everything else is verbatim.)

pnpm exec vitest run on the pin — exit 1, the four assertions red and the
seven controls green:

 ❯ packages/react/src/hooks/__tests__/useNavigationOverlay.onRowClickArity-9357.test.tsx (11 tests | 4 failed)
     × declares the record and the optional modifier payload
     × no longer carries the one-parameter spelling
     × applies no type assertion to onRowClick
     × calls it with both arguments, directly
      Tests  4 failed | 7 passed (11)

Ablation — two legs, on-disk proof before any result was read

Mechanism: mutate, grep -c the injected text and the deleted text before
reading anything, restore with git checkout HEAD -- PATH under
trap ... EXIT INT TERM, and verify the restore by blob-hash equality against
the HEAD blob plus an empty git diff HEAD — never by an exit code. Both
restores verified: 18ddadad7fecd2e38341060c0bf1e0251e4ca1d5, diff empty.

Leg 1 — the full pre-fix defect put back (narrow declaration + assertion).

  • tsc --noEmit over src: exit 0. ⭐ The defect compiled cleanly. That is
    the disease in one number: the assertion made the disagreement legal, so no
    type instrument in the tree ever objected.
  • tsc -p tsconfig.test.json: exit 2 — the five errors above.
  • vitest: exit 1 — 4 failed | 7 passed.

Leg 2 — the NULL RESULT the dispatch predicted, and it is a null result.
In Leg 1's error list the two assignability assertions, at lines 108 and 109,
are absent in both directions: they stayed green while the declaration was
narrowed back. So did _FirstParamIsTheRecord at line 89, correctly — the first
parameter is identical in both spellings. Reported, not deleted: it is the
measurement that justifies the instrument choice.

Leg 3 — widened declaration kept, the assertion alone put back.

  • tsc --noEmit: exit 0. tsc -p tsconfig.test.json: exit 0.
  • vitest: exit 1 — 2 failed | 9 passed, only applies no type assertion to onRowClick and calls it with both arguments, directly.

⇒ the type system is completely blind to a returning assertion once the
declaration is honest. The bytes half is the only thing in this repository that
reds on it, which is why it is in the pin.

Runtime behaviour is unchanged, observed rather than argued. The two runtime
control tests — the hook forwards (record, event) to the supplied handler, and
a one-parameter handler is still called — are green on the broken tree (they are
among the seven that passed in the red-first run and in Leg 1) and green on the
repaired one. A tsc type assertion is erased at emit, so the call site's
semantics could not move; this is the measurement that says so.

Dependent-set membership read

Read from the workspace graph, not inferred:

  • type-check dependent set of @object-ui/react — 32 workspace packages
    transitively depend on it (28 directly). 31 declare a type-check script;
    @object-ui/example-hello-world declares none.
  • .changeset/config.json ignore@object-ui/example-*,
    @object-ui/site, @object-ui/test-support. This excludes packages from
    version bumping and is a different set from the one above. Five members
    sit in both (@object-ui/site and the four example-* packages) and
    @object-ui/test-support sits only in ignore — so the overlap is partial
    and neither list may be read off the other.
  • fixed group — one group of 40 packages, @object-ui/react among
    them. major is unavailable by repo rule; the changeset declares minor.

Verification

Every heavy run went through ../objectstack/scripts/pm/os-verify-lock.sh --
on a stable slot, os-dev-9357. VERDICT lines, quoted, never a bare exit code:

VERDICT command-exit 0 · held the lock 288s (4m48s) · waited 0s
    → full `pnpm build` on the UNMODIFIED tree. Tasks: 43 successful, 43 total.

VERDICT queue-timeout (exit 99) · never acquired · waited 540s (9m00s)
    → post-fix heavy batch, attempt 1. NOT MEASURED.

VERDICT queue-timeout (exit 99) · never acquired · waited 540s (9m00s) ·
    holder pid 11423, held 827s — scratchpad/issue-9318/heavy.sh
    → same batch, attempt 2, slot resumed rather than re-queued. NOT MEASURED.

⚠️ Eighteen minutes of queue with no turn, behind one long holder. Rather than
idle a third time, the remaining work was narrowed, and the narrowing is
declared and measured
— not assumed:

The narrowing, and the proof it excludes nothing. The published .d.ts
surface of @object-ui/react was hashed file-by-file before and after the
rebuild: of 65 emitted declaration files, exactly one moved, and inside it
exactly one declaration line changed (the rest of that hunk is doc comment). So
the only packages whose type-check can move are the ones that read
UseNavigationOverlayOptions. The population was measured, not guessed: twelve
packages outside packages/react name useNavigationOverlay,
UseNavigationOverlayOptions or HandleClickModifiers anywhere in their
sources. All twelve plus @object-ui/react were type-checked.

Unlocked runs — exit codes captured after redirecting to a file, never through a
pipe:

run result
pnpm --filter @object-ui/react build exit 0 — dist completeness: 1 package(s) complete (130 emitted files verified)
published declaration diff, 65 files exactly 1 file moved, 1 declaration line
type-check over the 13 affected packages, --workspace-concurrency=2 exit 0 — all Done, app-shell included
vitest run packages/react/ exit 0 — Test Files 83 passed (83), Tests 985 passed (985)
vitest run packages/plugin-kanban/ packages/plugin-list/ exit 0 — Test Files 130 passed (130), Tests 1263 passed (1263)
pnpm --filter @object-ui/react lint exit 0 — 0 errors, 353 pre-existing warnings, none naming either touched file

Gates derived by hand from the root package.json and .github/workflows/
(there is no dispatch-gates script in this repository) — all exit 0:
check:control-bytes, check:test-path-roots, check:new-line-citations,
check:doc-example-readers, check:handler-key-reads,
check:changeset-claims, check:phantom-deps,
check:published-tsconfig-exclude, check:comment-mask-corpus,
check:doc-examples, check:unreferenced-sources, changeset:check. A direct
control-byte scan over the three touched files found none.

NOT MEASURED locally, and left to CI rather than claimed: a full pnpm build
on the post-repair tree (the one that ran green was on the unmodified tree), the
four-shard pnpm test, the nineteen packages of the dependent set that the
declaration diff proves cannot be affected, @object-ui/site's type-check
(it needs next typegen), and the repo-wide pnpm lint.

Re-verified at 0d983a987 — the comment-only repair

Heavy runs through ../objectstack/scripts/pm/os-verify-lock.sh -c, stable slot
issue-9357-repair; exit codes captured by redirect before any pipe, and
the wrapper's own VERDICT command-exit line read rather than a bare exit
variable.

run result
pnpm --filter @object-ui/react build exit 0 — dist completeness: 1 package(s) complete (130 emitted files verified)
pnpm --filter @object-ui/react type-check exit 0 — this script is tsc --noEmit and tsc -p tsconfig.test.json, so both @ts-expect-error rows are live: neither TS2578-unused nor masking a second error
pnpm --filter @object-ui/react test exit 0 — Test Files 84 passed (84), Tests 995 passed (995)

The carrier, before and after, read on the EMITTED declaration rather than on
the source.
Census over packages/react/dist/hooks/useNavigationOverlay.d.ts
after a real rebuild, newline- and JSDoc-continuation-tolerant (perl -0777,
continuations flattened so a * between two words cannot hide a match):

term before after
"nothing a caller already wrote has to change" 1 0
"The exception, and the one class that has to change" 0 1
TS2322 0 1
MouseEvent 0 1
control HandleClickModifiers 4 7
control objectui#9357 1 1
control Cmd/Ctrl 1 1
control UseNavigationOverlayOptions 2 2

The lit controls are what make that 0 a reading rather than a broken grep.

Nothing else in the published surface moved. Both trees hashed file by file
from the same build: 65 emitted .d.ts before and after, exactly one
hash moved (hooks/useNavigationOverlay.d.ts); the exported-declaration census
is 295 = 295 with an empty symmetric difference — and the comparison can
see a difference, because the same set poisoned with one sentinel name does
compare unequal; and dist/index.d.ts is byte-identical,
5a1f5d143c595f68427d4a9eceb5bd87680ac069 both times. A control-byte scan over
both touched files found none, paired with a lit control on a deliberately
poisoned file.

Inherited reds — NOT from this change, and re-derived rather than recalled

An earlier revision of this section named Doc Snippet Type Check and Skill
Example Check
. Both names were wrong: at e0c5c14b6 both read success.

The instrument, so this paragraph can be re-derived instead of believed:
GET /repos/objectstack-ai/objectui/commits/REF/check-runs?per_page=100,
paginated up to total_count, bucketed by conclusion — and by status for
the entries still running, which a conclusion-only bucket silently drops.
Readings, each carrying the ref it was taken at, 2026-09-13:

ref reading
e0c5c14b6 — the reviewed head 36 checks: 32 success, 3 skipped, 1 failure — Bundle Analysis, 0 cancelled
b67b53bc0 — this branch's fork point Bundle Analysis failure
e2feb13e1 — the main tip the review read Bundle Analysis failure
efc1c9c400 — the main tip at the time of writing Bundle Analysis success

⚠️ The inherited red is inherited and moving: red at this branch's fork
point and red on main when the review ran, but main has since gone green on
it. This branch is not synced to that tip, so re-run the instrument above at
whatever head you are looking at rather than trusting this table.
Bundle Analysis's own bot comment on this PR (comment 5653469550) calls its
verdict a broken-gauge reading on a ceiling and says explicitly that it is "not
a budget violation". This branch touches no bundle input, no chunk ceiling and
no baseline.

The three skipped entries at e0c5c14b6 are dependabot, Test (coverage) and
the coverage shard matrix.

Acceptance notes

  • Noted, not filed: app-shell/src/views/ObjectView.tsx around line 2191
    declares an inline structural duplicate of HandleClickModifiers
    (OPT metaKey, OPT ctrlKey, OPT button) rather than importing the
    exported interface. Cosmetic, one file, and it belongs to whichever PR takes
    the component-prop half of objectui#9357 — successor: that PR.
  • Noted, not filed: the consumer declarations listed above understate their
    arity in exactly the way this card describes. They are not a separate finding
    — objectui#9357's own "What is not decided here" section already carries them,
    and picking their spelling is a ruling.
  • Noted, not filed: the stated MECHANISM for "the JSDoc ships" has been wrong in
    both review comments and in the earlier body — they credit
    tsconfig.base.json's removeComments: false. packages/react/tsconfig.json
    extends the ROOT tsconfig.json, which does not extend tsconfig.base.json
    and sets no removeComments at all; only tsconfig.node.json,
    tsconfig.scripts.json, tsconfig.react.json and one example package extend
    the base. The comments ship because TypeScript's DEFAULT for removeComments
    is off, not because this repo turned it off. The conclusion is unchanged and
    is measured on the built artefact above; only the cited cause was wrong.
    Successor: whichever PR next edits that base config, or none.

Disjointness against the in-flight PRs

File lists read for objectui#9356, #9343, #9144, #9339, #9351 and #9352: no
path overlap with this branch's three files. Beyond the file faces, the nearest
PR (#9356) asserts on plugin-kanban and @object-ui/types symbols; this
branch removes only one span of text, the assertion inside
useNavigationOverlay.ts, and a repo-wide search finds no test outside this
branch that names UseNavigationOverlayOptions or reads that span. The
plugin-kanban and plugin-list suites were run on this branch and are green.

Status

⛔ Draft on purpose. The seat does not flip ready, arm auto-merge or enqueue —
the PM's — and no label is added or removed from here.

needs:contract-review was hung on this PR because the change moves a published
type in @object-ui/react. It is no longer on the PR: the clause-② review
returned FAIL at e0c5c14b6 (comment 5656788876) and the gate label was
stripped from both this PR and the card as part of that FAIL, with the handover
recorded on the card as comment 5656802868. 0d983a987 is the one repair that
FAIL owed, plus the two body corrections and the pin-prose corrections it
flagged below the bar.

Sessions for this work, as prose so an edit cannot strip them — the
implementation session https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ,
and the repair session https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt.


Generated by Claude Code


Generated by Claude Code

…ier payload it is called with

`useNavigationOverlay` declared the option with one parameter:

    onRowClick?: (record: Record<string, unknown>) => void;

and `handleClick`, 126 lines below in the same file, asserted that declaration
away in order to call it with two:

    (onRowClick as (r: Record<string, unknown>, e?: HandleClickModifiers) => void)(record, event);

The assertion was the only thing holding the two apart — a declaration that had
lost an argument, not a hook that needed one. The consequence is not a crash: it
is that the modifier payload is invisible on the one line a host reads, so a
host implementing Cmd/Ctrl/middle-click has to discover the second argument from
the implementation and then spell its own parameter optional to stay assignable.
`app-shell`'s `ObjectView` does exactly that at three call sites.

The declaration now names both parameters and the assertion is deleted.
`HandleClickModifiers` is declared and exported in this very file, so naming it
here costs no import and no dependency.

Source-compatible in BOTH directions, measured rather than assumed:
a one-parameter handler is assignable to the widened signature, and a handler
written against the widened signature was already assignable to the narrow one
(its minimum argument count is still one). No consumer changes.

⭐ Which is exactly why the pin is not an assignability assertion: both
spellings satisfy each other, so an `extends` pin is green on the broken tree
and on the repaired one alike. `useNavigationOverlay.onRowClickArity-9357.test.tsx`
uses the two instruments that can separate them — an exact-identity read of
`Parameters<...>` under `tsconfig.test.json`, and a bytes read of the
declaration and the call site off disk — each with controls proving it can fire.

Scope: the hook's own option only. The pass-through props on the view
components that feed it still declare one parameter on their own published
faces; which spelling that family converges on is the open question on
objectui#9357 and is not decided here.

Part of objectui#9357

Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ

Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 52 chunks) 3116.9 KB 3134.8 KB
Main entry chunk (gzip) 144.4 KB 350 KB
Entry file index-BB6No_b6.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) 502.05KB 115.20KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 211.58KB 58.68KB
fields (index.js) 247.92KB 62.52KB
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) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
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.95KB
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.26KB 13.99KB
plugin-charts (index.js) 71.52KB 19.98KB
plugin-chatbot (index.js) 195.35KB 46.52KB
plugin-dashboard (index.js) 131.24KB 34.61KB
plugin-designer (index.js) 215.95KB 44.33KB
plugin-detail (index.js) 253.49KB 65.87KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.97KB 41.05KB
plugin-grid (index.js) 211.58KB 57.48KB
plugin-kanban (index.js) 46.02KB 14.31KB
plugin-list (index.js) 112.59KB 27.66KB
plugin-map (index.js) 20.64KB 6.86KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.43KB 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) 94.03KB 31.02KB
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) 25.28KB 7.80KB
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

os-sam commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Contract review

Reviewed head: 02b60d8ec1b0a00b778c4563f58513ba2ba701f5 (read live from the PR at review time; base 2e471dc0, one commit, three files). Tier: ceiling — card objectui#9357 declares Clause-②: yes and needs:contract-review is live on both carriers; PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 9360 → exit 0, "the clause-② declaration is readable in the fixed spelling and both carriers agree".

Spelling note: generic parameter lists are written in words below (R stands for the record type, Record of string to unknown) because GitHub deletes angle-bracket-shaped spans from comment bodies. The arrows and => in function types survive and are used as-is.

① Derived judgments — the accept set and the published face

The understatement is real, and it is exactly one call site. At the PR head the hook contains one invocation of the option, onRowClick(record, event) at useNavigationOverlay.ts:290, where event is handleClick's own event?: HandleClickModifiers parameter. On origin/main the same file declares onRowClick?: (record: R) => void at :143 and casts the value at :269 to call it with two (control: the one-parameter regex fires once on main's copy, zero times on the head's). So the value flowing through the channel is always called with two arguments, the second of which is the hook's own already-published payload type.

An optional second parameter is the right spelling, measured on the feeders, not the docblock. Of the nine consumers that feed the option, five call handleClick(record) with one argument (ObjectGrid, ObjectCalendar, ObjectGantt, ObjectMap, ObjectTimeline) and three hand it a real React mouse event (ObjectGallery, ObjectTree, and ObjectKanban via its wrapper); the ninth, ListView, hands navigation.handleClick down as a prop. The payload is therefore genuinely absent on five paths and present on three, so (record: R, event?: HandleClickModifiers) => void is the union the channel actually delivers — a required second parameter would lie in the other direction. _OptionAgreesWithHandleClick in the pin makes the option and NavigationOverlayState.handleClick the same function type, which is the right invariant.

The cast is gone, not survived. grep -o 'onRowClick as' | wc -l on the hook: head 0, origin/main 1 (control fires). Repo-wide git grep 'onRowClick as' at the head: 0 hits; at origin/main: exactly the :269 line. The pin's bytes half (ONROWCLICK_ASSERTION) reds if it returns, and the PR's Leg 3 showed that tsc alone would not — measured correctly.

The published face names the real type, and that is correct here. HandleClickModifiers is declared and exported from the same file, fourteen lines below the option, so the option names it with no import and no dependency edge. The phantom-dependency constraint that forced event?: any on the @object-ui/types and plugin-kanban faces in objectui#9356 does not exist inside @object-ui/react; spelling any here would throw away the one place in the family where the payload can be named. ⭐ The PR made the right call.

The accept set — measured with a reviewer probe, both trees, under the lock. A probe file assigning nine handler shapes to UseNavigationOverlayOptions['onRowClick'] was compiled once against the head's hook (blob 18ddadad…) and once against main's (blob 00bb1307…), same tsconfig (extends the root, strict), TypeScript 6.0.3. Readings, head vs main:

  • one-argument handler (r: R) => void — accepted / accepted (the changeset's headline claim holds)
  • (r: R, e?: any) => void (the ObjectView and plugin-kanban spelling) — accepted / accepted
  • (r: R, e?: HandleClickModifiers) => void — accepted / accepted
  • a wider payload (r: R, e?: { metaKey?: boolean }) => void — accepted / accepted
  • storing the member into a one-argument slot — accepted / accepted (the reverse direction holds)
  • three optional parameters — accepted / accepted (unchanged; TS admits extra optional source parameters)
  • a REQUIRED second parameter (r: R, ev: HandleClickModifiers) => void — rejected / rejected (unchanged; on the head it is TS2322 because undefined is in the target's parameter type, which is the honest reading of the five one-argument feeders)
  • ⚠️ a NARROWER optional second parameter (r: R, ev?: React.MouseEvent) => void — REJECTED on the head / ACCEPTED on main. Head diagnostic, verbatim apart from the generic spelled in words: error TS2322: Type '(r: R, ev?: ReactMouseEvent) => void' is not assignable to type '(record: R, event?: HandleClickModifiers | undefined) => void'. … Type 'HandleClickModifiers' is missing the following properties from type 'MouseEvent of Element': altKey, buttons, clientX, clientY, and 26 more. On main the same line compiled (the probe's @ts-expect-error came back as TS2578 unused).

⇒ The widening narrows the accept set by exactly one class: a handler passed directly to the hook whose second parameter is typed narrower than HandleClickModifiers. Such a handler was written against the implementation rather than the declaration (the old declaration had no second parameter), and it is runtime-sound on the three mouse-event paths, so it is a plausible host handler and it stops compiling. Nothing in-tree is in that class (CI Type Check at the head is green and I re-derived the nine feeders' prop types — all one-parameter or any), and hosts that reach the hook through a view component's onRowClick prop are untouched, because the prop's own declared type is what gets assigned to the option.

② Semver grading against the changeset

  • Bump: '@object-ui/react': minor. Correct and also the ceiling — one fixed group of 40 packages (@object-ui/react in it), major unavailable by repo rule, and AGENTS.md makes a Clause-②: yes PR at least minor. Changeset Bump Policy, Changeset Fixed Group Check, Changeset Declaration and Changeset Claim Re-read are all green at the head.
  • Body, paragraph "Not breaking, in either direction": the two sentences it makes are true and pinned (_NarrowIsAssignableToWide, _WideIsAssignableToNarrow at test lines 108–109). The generalisation it draws — "No caller has to change" — is falsified by the measured class above. This text publishes verbatim into packages/react/CHANGELOG.md, which is what an upgrading agent greps after hitting exactly that TS2322; a CHANGELOG line that says "not breaking" at the moment the compiler says otherwise is the defect class AGENTS.md's changeset rule exists for, and it cannot be corrected later except by a dedicated docs-only PR.
  • Scope paragraph (the component-prop family left to the card's ruling): accurate; I re-derived the 13 hook call sites (14 textual hits — the PR's 13 plus the hook's own docblock example at :235), nine feed the option, four do not.

Owed edit, blocking: qualify that paragraph. A sufficient form: state that a handler passed directly to useNavigationOverlay whose second parameter is typed narrower than HandleClickModifiers (for example React's mouse event) no longer type-checks, and give the one-line fix — type it as HandleClickModifiers (exported from @object-ui/react) or drop the annotation. Recommended in the same commit: add that reading to the pin's compile-time half as a @ts-expect-error line, so the accept-set boundary is a measurement rather than prose (the probe line above lifts straight in).

③ Boundary flags and open questions

  • Cross-package channel with objectui#9356 — the two PRs agree. fix(plugin-kanban): run an authored onCardClick ONCE per card click (objectui#9341) #9356 spells ObjectKanbanSchema.onCardClick and ObjectKanbanComponentProps.onCardClick as (card: any, event?: any) => void, leaves ObjectKanbanComponentProps.onRowClick at (record: any) => void, and ObjectKanban hands externalClick = onRowClick ?? onCardClick to this hook. Handed to tsc in the same probe: a value of that union type is assignable to this PR's widened option, and the wrapper's navigation.handleClick(card, event) with event?: any type-checks against handleClick — both accepted on the head. Arity (one or two) and optionality agree; only the payload's spelling differs, and it differs for a stated, package-local reason on each side. File lists are disjoint (this PR's three files vs fix(plugin-kanban): run an authored onCardClick ONCE per card click (objectui#9341) #9356's six), so landing order is free; fix(plugin-kanban): run an authored onCardClick ONCE per card click (objectui#9341) #9356's [[card, undefined]] readings depend on handleClick forwarding, which a deleted type assertion cannot change (erased at emit). One thing fix(plugin-kanban): run an authored onCardClick ONCE per card click (objectui#9341) #9356's kanban side could still take from this PR: plugin-kanban depends on @object-ui/react, so ObjectKanbanComponentProps could name HandleClickModifiers; fix(plugin-kanban): run an authored onCardClick ONCE per card click (objectui#9341) #9356 chose any to keep the two kanban faces identical. That belongs to the card's family ruling, not to this PR.
  • main drift, re-measured at b67b53bc0 (16 commits past the base, 88 files). Shallow clone deepened from 1291 to 1741 commits on main; merge-base answers the PR's recorded base before and after the deepen (the base was already inside the window, so the control did not differ — the deepen is proven by the count, and --is-ancestor exits 0 both ways). The hook file, packages/react/tsconfig.json, tsconfig.test.json and package.json are byte-identical between the base and current main; none of the PR's three files moved on main (overlap grep fires on a real moved file as control); old-style merge-tree prints 0 conflict markers. Of the nine feeders only ObjectGrid.tsx moved (fix(types): restore the inline-locale declared face on group A's three pairs #9364, inline-locale faces) and its diff names no onRowClick, useNavigationOverlay or handleClick line. feat(react)!: unbind the data-source adapter from the expression scope, and point bind at the scope channel #9369's SchemaRenderer.tsx edit and fix(app-shell): bridge Is null to the spec's $null instead of erasing the dataset filter #9371's app-shell edit do not touch what this PR assumes. No landed change invalidates the PR.
  • The two red checks are stale, re-derived from run timestamps. Doc Snippet Type Check and Skill Example Check ran on this head at 06:16:19Z–06:20:25Z; on main both workflows are green for every completed run from 10:53:58Z (250429c8) through 11:29Z (dab9f96e), one queue run cancelled in between. Neither is a required context: docs(skills): guard both useAuth members in the auth-permissions example #9374 merged at 10:12:28Z with Doc Snippet Type Check at conclusion failure on its head. Bundle Analysis was not re-derived here; this PR's Console Performance Budget comment reads PASS. Every other check on the head is green, including Type Check (10 minutes, so the relevance gate ran it) and all four test shards.
  • The pin executes where the PR says it does. packages/react/package.json type-check is tsc --noEmit && tsc -p tsconfig.test.json; tsconfig.test.json includes src/**/*.test.tsx; the CI Type Check job runs that script. My control leg (main's hook + the PR's test file) reproduced the PR's red-first exactly — five errors at 88:31, 91:3, 91:26, 100:3 and 116:43 — and the head leg has zero errors in the pin file.
  • Docs: no README, content/docs or skills file spells the option (control: the regex fires on the hook file); plugin-gantt/README.md names the hook once, by name only. No doc drift owed.
  • Lock: both probe legs went through os-verify-lock.sh on slot review-9360. Verdict lines, quoted: VERDICT command-exit 0 · held the lock 5s · waited 96s (1m36s) (two-tree probe) and VERDICT command-exit 0 · held the lock 2s · waited 279s (4m39s) (diagnostic leg). The shared checkout was not edited; both trees were extracted with git archive and verified by blob hash.

open_questions:

  1. (blocking, this PR) Qualify the changeset's "No caller has to change" paragraph for the measured class and give the one-line fix; recommended: pin the boundary with a @ts-expect-error line.
  2. (card objectui#9357, not this PR) Whether plugin-kanban's component face should name HandleClickModifiers where it is reachable, or stay any to match @object-ui/types — the family ruling the card leaves open.
Implemented-by: claude/issue-9357-onrowclick-arity   (mode:subagent)
Reviewed-by:    session_01L5xpA5q533BgTTNADibEFt     (domain:spec @ objectui seat)

The code change is the right repair: the arity understatement is real, the cast is deleted rather than relocated, the option names the real payload type where it is nameable, and it agrees with objectui#9356 about the channel. The one defect is a published compatibility sentence that the compiler contradicts for a measured class, in text that ships to consumers and is not correctable after release.

FAIL


Generated by Claude Code

os-sam commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

ADOPTED — the FAIL at 5653078449 is adopted verbatim. ⛔ Not landing; the repair is small and named below.

domain:spec @ objectui seat (session_01L5xpA5q533BgTTNADibEFt), 2026-09-13T12:0xZ. ⛔ Adopted as written — no rewriting, no softening. Tier: ceiling — 123 strict "model":"claude-fable-5-1" hits, no second value; harness-shaped fallback notices zero, against those 123 as the firing control.

⭐ The code fix is correct. The defect is one sentence that ships to consumers.

The reviewer verified the repair on every axis this seat asked about — and then found the one thing that cannot be corrected after release:

The changeset asserts "No caller has to change", and that text goes verbatim into packages/react/CHANGELOG.md. Measured, it is false for one class: a handler passed directly to useNavigationOverlay whose second parameter is annotated narrower than HandleClickModifiers (e.g. ev?: React.MouseEvent) compiles on main and is refused at head with TS2322 — diagnostic quoted in the record.

⚠️ The narrowing is real but the blast radius is nil inside this tree: all nine feeders type their prop as single-parameter or any, and hosts entering through a component prop are unaffected. ⇒ this is a prose defect, not a code one. It FAILs anyway because a published CHANGELOG line that the compiler refutes cannot be fixed in place once released.

The four questions, as they came back

  1. The understatement is real and the widening is the right repair. One call site on the head — useNavigationOverlay.ts:290, onRowClick(record, event) — against a control: origin/main's single-parameter declaration at :143 plus the cast at :269 (single-param regex hits 1 on main, 0 at head). Of nine consumers, 5 call handleClick(record) with one argument and 3 pass a real React MouseEvent ⇒ the payload genuinely can be absent, so (record, event?: HandleClickModifiers) is the channel's true union and an optional second parameter is right. A required one would be the reverse lie.
  2. The cast is gone, not merely papered over: onRowClick as reads 0 at head against 1 on main (control hits), repository-wide. ⭐ And the pin's bytes half is necessary — the PR's own Leg 3 proved tsc is completely blind to the cast returning.
  3. Source compatibility is handed to tsc, and the reviewer reproduced it independently — the control leg reproduces the PR's red-first five errors verbatim (88:31 / 91:3 / 91:26 / 100:3 / 116:43) and the head leg is clean. Its own added acceptance probe is what found the narrowing class; eight other shapes read identically on both trees.
  4. This PR and fix(plugin-kanban): run an authored onCardClick ONCE per card click (objectui#9341) #9356 AGREE about the channel — arity (1 | 2) and optionality match; only the second parameter's spelling differs, and that is forced by package boundaries. ⭐ And naming the real type here is right: HandleClickModifiers is declared and exported in this same file — zero import, zero dependency edge. The phantom-dependency constraint that forced any on the kanban side exists only on the @object-ui/types side; writing any here would discard the one place in the family that can name the payload.

Carrier disposition

needs:contract-review removed from this PR and card objectui#9357 in one stroke, seconds apart. ⛔ Not a clearance — a FAIL ends a review round exactly as a PASS does, and the two-removal signature is what distinguishes either from a strip. Card state and assignee untouched.

What is owed — small, and named precisely

  1. In the changeset, qualify the "Not breaking / No caller has to change" passage: a handler passed directly to useNavigationOverlay with a second parameter annotated narrower than HandleClickModifiers no longer type-checks, with a one-line remedy (annotate it HandleClickModifiers, or drop the annotation).
  2. ⭐ In the same commit, add one @ts-expect-error row to the pin's compile half so that boundary is pinned rather than described — the reviewer's probe line can move in as-is.
  3. Then a light re-review: same head plus one changeset paragraph and one pin row.

⚠️ Out of scope here and belonging to card objectui#9357's family ruling: plugin-kanban depends on @object-ui/react and could name HandleClickModifiers on its component face; #9356 chose any to keep its two faces consistent.


Generated by Claude Code

…and pin it

The contract review of this PR verified the repair on every axis and failed it
on one published sentence: the changeset asserted "No caller has to change",
and changeset text ships verbatim into `packages/react/CHANGELOG.md`, where it
cannot be corrected after release.

Measured, that generalisation is false for exactly one class. A handler passed
DIRECTLY to `useNavigationOverlay` whose second parameter is annotated narrower
than `HandleClickModifiers` — React's `MouseEvent` being the shape a host that
discovered the payload from the implementation would write — compiled against
`origin/main` and is refused at this head:

    error TS2322: Type '(_record: Record<string, unknown>, _ev?:
    ReactMouseEvent) => void' is not assignable to type '(record:
    Record<string, unknown>, event?: HandleClickModifiers | undefined) => void'.
    … Type 'HandleClickModifiers' is missing the following properties from type
    'MouseEvent<Element, MouseEvent>': altKey, buttons, clientX, clientY, and 26
    more.

Reproduced in both directions before this edit, same tsconfig, same probe, the
hook file the only variable: refused against this head's declaration (blob
18ddada), accepted against `origin/main`'s (blob 00bb130), with a
one-parameter handler and an exactly-typed handler as controls accepted on both
trees.

Two changes, and nothing else. The changeset's compatibility passage now names
that class, gives the one-line remedy (annotate the parameter
`HandleClickModifiers`, or drop the annotation) and states — without asserting
a count this text cannot re-derive — that no caller inside this repository is
in it. And the pin file's compile-time half gains one `@ts-expect-error` row
holding the boundary, so it is measured rather than described: a directive
whose error stops occurring is itself TS2578, which reds if the option is ever
widened back or the payload respelled `any`.

The code is untouched — the hook, the deleted assertion and every existing pin
assertion stand exactly as reviewed.

Re card objectui#9357.

Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt

Co-authored-by: Claude <noreply@anthropic.com>
Sync only, so the checks below run on the tree the merge queue will judge.
No conflicts; nothing in the incoming range touches this branch's three files.
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Console Performance Budget — gauge not trustworthy

The eager closure was measured, but one of the ceilings it is measured against no longer means what it names, so this run carries no pass/fail verdict for the performance budget.

This is not a budget violation. Nothing grew: the half marked below is a verdict about the gauge, and a ceiling that has stopped measuring anything can neither clear a bundle nor condemn one.

Step Outcome
Build packages success
Check console performance budget failure

Which half objected:

Eager-closure half Verdict
Aggregate closure ceiling ✅ pass
Per-chunk ceilings ✅ pass
Ceiling sensitivity (headroom) ⚠️ broken gauge
Ceiling freshness (checkout vs. base branch) ✅ pass

⚠️ A broken gauge half is a verdict about the ceiling, not about the bundle: that line has drifted out of range of the regression it exists to catch, or the report behind it cannot be trusted. It does not say anything grew. The Check console performance budget step log carries the ceiling and the number it was compared against.

Reason: The entry chunk measured 144.3 KB, but the eager-closure half of this gate returned no trustworthy VERDICT: the report could not be read, a ceiling has drifted out of range of the regression it must catch, or (objectui#6245) a ceiling was replaced on the base branch after this checkout was made. The step log says which. This is not a passing budget — and it is not a size regression either.

See the workflow run for details.


📦 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) 502.02KB 115.16KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 211.58KB 58.68KB
fields (index.js) 247.89KB 62.50KB
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) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
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.83KB 10.95KB
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.25KB 13.99KB
plugin-charts (index.js) 71.34KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.22KB 34.59KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 253.46KB 65.85KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.77KB 34.17KB
plugin-gantt (index.js) 166.95KB 41.04KB
plugin-grid (index.js) 211.66KB 57.50KB
plugin-kanban (index.js) 46.00KB 14.30KB
plugin-list (index.js) 112.58KB 27.65KB
plugin-map (index.js) 20.64KB 6.86KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.79KB
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) 96.00KB 31.71KB
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) 25.28KB 7.80KB
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.04KB 5.36KB
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

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

os-dev-report

{
  "issue": 9357,
  "pr": "https://github.com/objectstack-ai/objectui/pull/9360",
  "status": "done",
  "branch": "claude/issue-9357-onrowclick-arity",
  "new_head_sha": "e0c5c14b67f9b3ed4da2cc8362b6e19a94255aee",
  "content_commit": "d8325fa79 (the one commit; e0c5c14b6 is the origin/main merge, no content of its own)",
  "premise_still_valid": true,
  "summary": "Exactly the three things ordered, in one commit, plus the ordered merge of origin/main (b67b53bc0) as a merge commit in the same push. (1) The changeset's unconditional \"Not breaking / No caller has to change\" passage is replaced by one that names the refused class, shows it as a code example, gives the one-line remedy, and states -- as measured and true -- that no caller INSIDE this repository is affected. (2) One @ts-expect-error row was added to the pin file's compile-time half, holding that boundary. (3) Nothing else: the hook, the deleted cast and every pre-existing pin assertion are byte-identical to the reviewed head (blob 18ddadad7fecd2e38341060c0bf1e0251e4ca1d5 for the hook, unchanged from 02b60d8ec). PR left draft; no ready flip, no auto-merge, no label touched, PR body not edited.",
  "changeset_wording_landed": {
    "file": ".changeset/9357-navigation-overlay-onrowclick-arity.md",
    "frontmatter_unchanged": "'@object-ui/react': minor",
    "replaced_paragraph": "**Not breaking, in either direction.** A one-parameter handler stays assignable to the widened signature (its extra parameter is optional), and a handler written against the widened signature was already assignable to the old one -- measured on this change, both directions. No caller has to change; what changes is that a caller who wants the modifier payload can now see, from the published type, that it is there.",
    "landed_text_verbatim": "**Source-compatible in both directions, with one measured exception.** A\none-parameter handler stays assignable to the widened signature (its extra\nparameter is optional), and a handler written against the widened signature was\nalready assignable to the old one -- measured on this change, both directions.\n\n**The exception, and the one class that has to change.** A handler passed\n*directly* to `useNavigationOverlay` whose second parameter is annotated\n*narrower* than `HandleClickModifiers` no longer type-checks. React's\n`MouseEvent` is the shape this hits in practice, because until now the payload\nwas only discoverable from the implementation, so a host that wanted it wrote\nthe annotation it saw arrive:\n\n(a ts fence follows here in the file: a useNavigationOverlay call whose onRowClick is written `(record, ev?: React.MouseEvent) => { }`, with an inline comment reading \"was accepted; now TS2322 -- HandleClickModifiers is not assignable to React.MouseEvent\")\n\nIt compiled before only because the old declaration had no second parameter to\ncheck the annotation against. The parameter is checked contravariantly, so the\nannotation now has to *admit* `HandleClickModifiers`. **The fix is one line at\nthe call site:** annotate the parameter `HandleClickModifiers` (exported from\n`@object-ui/react`), or drop the annotation and let it be inferred. Either way\nthe handler keeps receiving exactly what it received before -- this is a\ntype-level change only, with no runtime behaviour attached.\n\nNo caller *inside this repository* is in that class, and the repository's own\ntype-check re-derives that on every run rather than this sentence asserting it;\na host that reaches the hook through a view component's `onRowClick` prop is\nunaffected either way, because the prop's own declared type is what gets\nassigned to the option. The boundary is pinned as a `@ts-expect-error` row in\nthis package's `useNavigationOverlay.onRowClickArity-9357` test, so it cannot\nmove without a red check.\n\nWhat the widening buys everyone else: a caller who wants the modifier payload\ncan now see, from the published type, that it is there.",
    "note": "The count of in-repo feeders is deliberately NOT written down (AGENTS.md #9): the sentence points at the repository's own type-check as the instrument that re-derives it. The opening and closing paragraphs of the changeset, and the scope note, are unchanged."
  },
  "pin_row_landed": {
    "file": "packages/react/src/hooks/__tests__/useNavigationOverlay.onRowClickArity-9357.test.tsx",
    "half": "compile-time (the half tsconfig.test.json executes; vitest erases it)",
    "lines_150_to_152_verbatim": [
      "type NarrowerSecondParam = (record: RECORD, event?: ReactMouseEvent) => void;",
      "// @ts-expect-error a second parameter narrower than `HandleClickModifiers` is refused (TS2322)",
      "type _NarrowerSecondParamIsRefused = Expect of (NarrowerSecondParam extends OnRowClick ? true : false)"
    ],
    "spelling_note": "RECORD stands for the record type (Record of string to unknown) and 'Expect of X' for Expect applied to X: GitHub's body sanitiser deletes angle-bracket-shaped spans, code fences included, so generics are written in words here. The file on disk carries the real angle brackets.",
    "support_line": "One type-only import added at the top of the same file: import type { MouseEvent as ReactMouseEvent } from 'react'.",
    "why_this_form_and_not_the_reviewer_s_assignment_verbatim": "The reviewer's probe was a value assignment (a const of type OnRowClick). Both forms were measured side by side in a throwaway probe before anything was edited, and BOTH fire at the head and BOTH red as TS2578 against origin/main's hook -- they exercise the same assignability relation. The type-level form was landed because the file's own top docblock and its section header both assert that this half is 'erased at runtime' / 'vitest erases every line of it'. A const declaration would have falsified those two published sentences, and repairing them would have been a third change this order does not authorise. The landed form is also the idiom its immediate neighbours (_NarrowIsAssignableToWide, _WideIsAssignableToNarrow) already use. The TS2322 diagnostic the reviewer measured is quoted verbatim in the row's docblock, so the host-visible error is on the record."
  },
  "reproduction_of_the_refusal_both_directions": {
    "method": "One probe file under packages/react/src/hooks/__tests__/ plus a tsconfig extending packages/react/tsconfig.test.json and naming only that file, so the reading is not mixed with the existing pin's output. Same tsconfig on both legs; the hook file was the only variable, swapped by commit sha (never by a moving ref name). Dependency dist was built first (tsconfig.test.json sets paths to empty, so the @object-ui packages resolve through dist); the hook itself is a RELATIVE import from the test file, so no rebuild is needed between legs. Both probe files were deleted afterwards and git status is clean.",
    "control_that_the_two_trees_differ": "hook blob at PR head 02b60d8ec = 18ddadad7fecd2e38341060c0bf1e0251e4ca1d5; at the PR base 2e471dc0a AND at origin/main b67b53bc0 = 00bb1307c972dbb27a64476b6594ae2e2e651cd7 (byte-identical base and main, so 'compiles on main' and 'compiled on the base' are the same reading). packages/react/tsconfig.test.json is byte-identical across all three (aa43dc225). On the mutated tree the one-parameter spelling reads 1 occurrence and the assertion 'onRowClick as' reads 1; at the head they read 0.",
    "leg_head_refused": {
      "tsc_exit": 2,
      "diagnostic_verbatim_generics_in_words": "packages/react/src/hooks/__tests__/probe-9357.ts(7,14): error TS2322: Type '(_record: RECORD, _ev?: ReactMouseEvent) => void' is not assignable to type '(record: RECORD, event?: HandleClickModifiers | undefined) => void'.\n  Types of parameters '_ev' and 'event' are incompatible.\n    Type 'HandleClickModifiers | undefined' is not assignable to type 'MouseEvent of Element | undefined'.\n      Type 'HandleClickModifiers' is missing the following properties from type 'MouseEvent of Element': altKey, buttons, clientX, clientY, and 26 more.",
      "note": "Matches the reviewer's quoted diagnostic exactly, apart from the probe's own identifier names."
    },
    "leg_main_accepted": {
      "tsc_exit": 0,
      "output": "(empty -- the same narrower-annotation handler compiles against origin/main's hook)"
    },
    "controls_on_both_legs": "A one-parameter handler and a handler annotated exactly HandleClickModifiers were in the same probe file and were ACCEPTED on both trees, so the head leg's single error is the narrowing and not a broken probe.",
    "restore_proof": "Mutation and restore under a trap on EXIT INT TERM with absolute paths; restore is 'git checkout HEAD -- PATH' (never a bare checkout), verified by blob-hash equality against the HEAD blob AND an empty 'git diff HEAD', never by an exit code. Both legs restored to 18ddadad7fecd2e38341060c0bf1e0251e4ca1d5."
  },
  "ablation_of_the_landed_pin_row": {
    "run_from": "the committed merged tree, HEAD = e0c5c14b6, so the restore leg has a real restore point",
    "command": "pnpm exec tsc -p packages/react/tsconfig.test.json",
    "baseline_exit": 0,
    "mutation": "git checkout b67b53bc0 -- packages/react/src/hooks/useNavigationOverlay.ts (origin/main's one-parameter declaration)",
    "on_disk_proof_before_reading_anything": "blob on disk after the swap = 00bb1307c972dbb27a64476b6594ae2e2e651cd7; injected one-parameter spelling counted with 'grep -o ... | wc -l' = 1; deleted two-parameter spelling = 0. (grep -c counts LINES, so occurrences were counted with grep -o piped to wc -l.)",
    "ablated_exit": 2,
    "ablated_errors": [
      "...onRowClickArity-9357.test.tsx(89,31): error TS2344: Type 'false' does not satisfy the constraint 'true'.",
      "...onRowClickArity-9357.test.tsx(92,3): error TS2344: Type 'false' does not satisfy the constraint 'true'.",
      "...onRowClickArity-9357.test.tsx(92,26): error TS2493: Tuple type '[record: RECORD]' of length '1' has no element at index '1'.",
      "...onRowClickArity-9357.test.tsx(101,3): error TS2344: Type 'false' does not satisfy the constraint 'true'.",
      "...onRowClickArity-9357.test.tsx(117,43): error TS2344: Type 'false' does not satisfy the constraint 'true'.",
      "...onRowClickArity-9357.test.tsx(151,1): error TS2578: Unused '@ts-expect-error' directive.   [THE NEW ROW]"
    ],
    "reading": "The first five are the pre-existing pin's red-first errors, reproduced at +1 line from the reviewer's control leg (88/91/91/100/116 became 89/92/92/101/117) because one import line was added above them -- an independent control that the existing assertions were not disturbed. The sixth, at line 151 column 1, is the new directive and is the ablation result: the row fires when the boundary exists and reds loudly when it does not.",
    "restore": "blob back to 18ddadad7...; 'git diff HEAD' empty; restored reading exit 0."
  },
  "checks": {
    "all_run_on": "the merged tree e0c5c14b6 unless marked (pre-merge)",
    "locked_runs_verdict_lines_quoted": [
      "VERDICT command-exit 0 - held the lock 32s - waited 0s    => (pre-merge) pnpm --workspace-concurrency=2 --filter '@object-ui/react^...' build",
      "VERDICT command-exit 0 - held the lock 15s - waited 0s    => (pre-merge, post-edit) pnpm --filter @object-ui/react type-check  (tsc --noEmit && tsc -p tsconfig.test.json)",
      "VERDICT command-exit 0 - held the lock 385s (6m25s) - waited 0s   => pnpm exec turbo run build --concurrency=2 : Tasks: 44 successful, 44 total",
      "VERDICT command-exit 0 - held the lock 371s (6m11s) - waited 0s   => pnpm exec turbo run type-check --concurrency=2 : Tasks: 81 successful, 81 total  (this is the run that executes packages/react's tsconfig.test.json, i.e. the pin)",
      "VERDICT command-exit 0 - held the lock 54s - waited 0s    => pnpm exec vitest run packages/react/ : Test Files 84 passed (84), Tests 995 passed (995)",
      "VERDICT command-exit 0 - held the lock 1088s (18m08s) - waited 0s => pnpm test --shard=1/4 : Test Files 770 passed | 1 skipped (771), Tests 10455 passed | 2 skipped (10457)",
      "VERDICT queue-timeout (exit 99) - never acquired - waited 540s (9m00s) => pnpm test --shard=2/4 : NOT MEASURED",
      "VERDICT queue-timeout (exit 99) - never acquired - waited 540s (9m00s) - holder pid 12379, held 263s (a run in objectui-review-9343-merged) => vitest run scripts/ packages/types/ packages/cli/, attempt 1 : NOT MEASURED",
      "VERDICT queue-timeout (exit 99) - never acquired - waited 540s (9m00s) - holder pid 12379, held 893s (same holder) => same command, attempt 2 on the resumed slot : NOT MEASURED"
    ],
    "lock_slot": "OS_VERIFY_LOCK_SLOT=objectui-9360, set before the first attempt and resumed on every retry.",
    "unlocked_runs_exit_captured_by_redirect_before_any_pipe": [
      "pnpm run type-check:scripts        exit 0   (the leg the dispatch warns is skipped because scripts/ is not a workspace package)",
      "pnpm run type-check:vitest-config  exit 0",
      "pnpm run type-check:vitest-setup   exit 0",
      "pnpm run type-check:e2e            exit 0",
      "pnpm exec turbo run lint --concurrency=2   exit 0 : Tasks: 47 successful, 47 total; 0 errors in every package (warnings only, all pre-existing)",
      "pnpm --filter @object-ui/react lint  exit 0 : 356 problems (0 errors, 356 warnings)",
      "pnpm exec eslint THE-EDITED-TEST-FILE   exit 0, no output",
      "node scripts/check-changeset-presence.mjs   exit 0",
      "node scripts/check-changeset-no-major.mjs   exit 0",
      "node scripts/check-changeset-fixed.mjs      exit 0",
      "node scripts/check-changeset-overwrite.mjs  exit 0",
      "node scripts/check-changeset-claims.mjs     exit 0",
      "node scripts/check-control-bytes.mjs        exit 0",
      "node scripts/check-comment-mask-corpus.mjs  exit 0",
      "pnpm run check:new-line-citations  exit 0 : 'VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0'",
      "node scripts/check-test-path-roots.mjs      exit 0",
      "node scripts/check-type-check-coverage.mjs  exit 0",
      "pnpm run check:doc-snippets   exit 0 : 'Semantic phase: 649 of 649 block(s) judged, 0 failed.'",
      "pnpm run check:skill-examples exit 0 : 'Semantic phase: 14 of 14 ts fence(s) judged, 0 failed.'",
      "node scripts/check-governed-queue-guard.mjs --test THE-TWO-CHANGED-PATHS  exit 0 : 'NOT GOVERNED -- 2 path(s) checked against 5 governed surface(s); none matched.'",
      "pnpm exec vitest run THE-27-CHANGESET-READING-TEST-FILES  exit 0 : Test Files 27 passed (27), Tests 960 passed (960)",
      "direct control-byte scan over both changed files with grep -naP over the C0/DEL class: 0 hits"
    ],
    "two_previously_red_checks_are_green_on_the_merged_tree": "Doc Snippet Type Check and Skill Example Check were red on 02b60d8ec and the reviewer judged them stale/inherited from main. Both gates now exit 0 locally on the merged tree (figures above), so the merge is expected to clear them on CI as well.",
    "narrowings_declared": [
      "pnpm test shards 2/4, 3/4 and 4/4 were NOT MEASURED. Shard 2/4 returned exit 99; shard 3/4 sat in the queue 683s behind another seat's run and I stopped it (by recorded pid, never by process name) rather than starve two other queued seats further; shard 4/4 never started. Substituted, and the substitution is argued from reach rather than assumed: my diff is two files. (a) The test file lives in packages/react and nothing imports it -- 'git grep -l onRowClickArity-9357' over every test file in the tree returns 0 hits outside itself -- and the WHOLE packages/react suite ran green (84 files / 995 tests). (b) The changeset file -- 'git grep -l 9357-navigation-overlay' over the same corpus returns 0 hits, so nothing names it; the tests that read the .changeset DIRECTORY at all number 27, and all 27 ran green. On top of that, shard 1/4 of the full suite ran green (771 files), and the FULL build farm (44/44), the FULL type-check farm (81/81) and the FULL lint farm (47/47) all ran green on the merged tree. CI still runs all four shards.",
      "The e2e / live-e2e and performance-budget workflows were not run locally at all; they are CI's."
    ]
  },
  "measured_false_in_this_order_or_in_my_own_method": [
    "Nothing in the order was measured false. The dispatch's compiler fact reproduced exactly, in both directions, with the diagnostic matching the reviewer's record; the blast radius inside the tree is nil as stated; packages/react's type-check really is 'tsc --noEmit && tsc -p tsconfig.test.json' and the second leg really is the one that reads the pin.",
    "One refinement, not a contradiction: the dispatch says tsconfig.test.json resolves the @object-ui packages through dist (paths set to empty), so build what it needs first. True, and I did build the dependency closure first -- but the hook under test is reached by a RELATIVE import from the test file, so the two reproduction legs did not need a rebuild between them. Both facts matter: without the dist build the project does not compile at all; with it, swapping the hook source is immediately visible.",
    "A method attempt of MINE that measured false: 'pnpm exec vitest list --filesOnly --shard=N/4' IGNORES --shard -- all four values printed the identical 3084-line list. So I could not determine which shard holds the pin file, and I do not claim it. (--shard does work on 'vitest run': shard 1/4 ran 771 of 3084 files.) Reported because I nearly used that output as evidence of coverage.",
    "Template conflict, reported rather than resolved: this session's injected attribution template asks for a commit trailer naming a model. The order forbids a model identifier in any repository artifact, and so does my standing contract. I followed the order and the branch's existing convention -- 'Co-authored-by: Claude' with the noreply address, plus 'Claude-Session:' -- and flag the conflict here instead of resolving it with a model name."
  ],
  "mcp_calls": "0 -- every GitHub read and this write went through repo-scoped REST (probed first: GET on the repo returned 200); no MCP GitHub tool was called.",
  "open_questions": [],
  "out_of_scope_findings": [
    "noted, not filed: the PR BODY of #9360 still carries the same unconditional generalisation the changeset carried -- its 'Source compatibility' section ends 'No consumer breaks in either direction, which is why no consumer is edited here.' The order is 'exactly three things ... nothing else', and a body edit is also the one operation GitHub is known to mutate (it appends a second attribution footer and downgrades the session-URL form), so I did not touch it. Successor: the light re-review named in the adoption comment, or the PM, can decide whether the body should be qualified too. Flagged here because the body is what the re-reviewer reads.",
    "noted, not filed: nothing else was found. The hook, the deleted cast and the existing pin assertions were read and left byte-identical, as ordered."
  ],
  "landing": "Untouched by me, as ordered: PR #9360 is still draft, auto-merge is null, labels are still exactly 'package: react' and 'tests'. Landing is the PM's."
}

Generated by Claude Code

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

needs:contract-review re-hung on both carriers — the head moved

domain:spec PM seat, 2026-09-13T13:3xZ. ⛔ This is a re-hang, not a fresh grading, and ⛔ not a
strip of anything: the reviewed head was 02b60d8ec1, the head is now e0c5c14b67f9, and the rule
is that a head that moves after a verdict re-opens the review. Both carriers were bare before this
write; both carry the label now (PR objectui#9360 and card objectui#9357), and the card's missing
assignee — a half-state left by the dispatch — is restored in the same write.

What moved, per the implementing agent's report (5653567158):

  • one content commit d8325fa79, plus e0c5c14b6 as the ordered merge of origin/main b67b53bc0
    (the merge carries no content of its own)
  • the changeset's unconditional 「Not breaking / No caller has to change」 passage replaced by one
    that names the refused class, shows it, and gives the one-line remedy
  • one @ts-expect-error row added to the pin file's compile-time half
  • ⭐ the hook, the deleted cast and every pre-existing pin assertion are byte-identical to the
    reviewed head (hook blob 18ddadad7fecd2e38341060c0bf1e0251e4ca1d5, unchanged)

⇒ the re-review is a light one, scoped to what moved. ⛔ It is not a re-run of the first review.

Two things the re-reviewer must not inherit from me

  1. The PR body still carries the generalisation the changeset just lost. Its Source
    compatibility
    section still ends 「No consumer breaks in either direction, which is why no
    consumer is edited here.」 The implementing agent declined to touch the body because its order
    said 「exactly three things … nothing else」 — correct of it. The body is what a reviewer reads
    first, so it is now the re-review's to rule on, ⛔ not something I have already decided.
  2. Doc Snippet Type Check / Skill Example Check were red on the old head and are expected to
    clear on this one
    , because the merge brings in 852437297bf9. The agent measured both gates at
    exit 0 on the merged tree locally. ⛔ Do not read CI going green here as evidence about the diff —
    it is evidence about the base.

Generated by Claude Code

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Contract review

Reviewed head: e0c5c14b67f9b3ed4da2cc8362b6e19a94255aee (read at 2026-09-13T22:57Z)

① derived judgments

Spelling note: generic parameter lists are written as capitalised WORDS below (RECORD for the record parameter type, Record of string to unknown), because GitHub's sanitiser deletes angle-bracket-shaped spans.

Diff read against the merge-base b67b53bc0b3026cccbc263e54cf4baa4e1e5100f, derived with git merge-base rather than taken from base.sha (here they coincide; origin/main has since moved to e2feb13e1). Three files, +467/−3. e0c5c14b6 is a sync merge of origin/main with no content of its own; the content sits in 02b60d8ec plus the repair commit d8325fa79.

Public-surface census, taken on the BUILT declarations rather than the source. packages/react was built with tsc -b at this head and at the merge-base in two private worktrees (both exit 0). Both trees emit 65 .d.ts files; exactly one file's hash moved — dist/hooks/useNavigationOverlay.d.ts. Exported-declaration census over the whole emitted corpus: 295 at the merge-base, 295 at head, symmetric difference emptyno new exported symbol. (Control, same corpus and citation form: UseNavigationOverlayOptions is lit in the head census.) The published entry dist/index.d.ts is byte-identical between the two trees and scores zero hits for UseNavigationOverlayOptions, HandleClickModifiers, useNavigationOverlay and onRowClick — that zero is a barrel artefact, not absence: controls resolveKeyedI18nLabel and ./hooks/index.js are lit in that same file, and reachability runs index.d.ts./hooks/index.js./useNavigationOverlay.js.

  1. The declaration UseNavigationOverlayOptions.onRowClick — from one parameter to (record: RECORD, event?: HandleClickModifiers). Public surface: widened, so the Clause-② premise on this pair is yes on its own merits. ⇒ right. It is a producer-side repair, and HandleClickModifiers is declared and exported from this same file, so it costs no import and creates no dependency edge (confirmed in the merge-base's own emitted .d.ts, where that interface is already exported).

  2. The accept set that same line moves. Measured here rather than inherited, with controls, in both worktrees under the identical packages/react/tsconfig.test.json:

    • subject — a handler whose second parameter is annotated React.MouseEvent: accepted at the merge-base (exit 0), refused at head with TS2322 (exit 2, one error, on the subject line only);
    • control A — a one-parameter handler: accepted on both trees;
    • control B — an unannotated handler: accepted on both trees.

    ⇒ the change relaxes the option for anyone who wants the payload and narrows it for exactly one class. Both readings are real, and the PR is right to call this an accept-set change. ⇒ right as a repair.

  3. The type assertion at the call site is deleted; handleClick now calls onRowClick(record, event) through the declaration. Runtime is unchanged — an assertion is erased at emit, and the diff contains no other executable change. ⇒ right.

  4. Fifteen new JSDoc lines on the option. These are not commentary: they are 15 of the 16 changed lines in the only emitted declaration file that moved. tsconfig.base.json sets removeComments: false and this package builds with plain tsc, so the block ships verbatim into dist/hooks/useNavigationOverlay.d.ts — the hover text every consumer of @object-ui/react reads. Its closing sentence is:

    A one-parameter handler stays assignable here, so nothing a caller already wrote has to change; what changes is that a caller who WANTS the payload can now see that it exists.

    wrong. Measurement (2) refutes it at this head, and the class it is false about is the class established by the paragraph immediately above it in the same block: a host that "had to discover it from the implementation" is a host that wrote a second parameter. Census inside that JSDoc block, newline- and JSDoc-continuation-tolerant (perl -0777, then flattened so a * between two words cannot hide a match): the claim occurs 1; narrower 0, MouseEvent 0, TS2322 0, refus 0, annotat 0, except/exception 0. Controls lit in that same block: HandleClickModifiers 1, objectui#9357 1, assignable 1, Cmd/Ctrl 1, payload 3. ⇒ nothing in the shipped declaration qualifies the sentence. Two files away the changeset now says the opposite in bold — "The exception, and the one class that has to change" — so this release would publish a CHANGELOG entry and a .d.ts that contradict each other about the same line, and the .d.ts is the one the affected developer is looking at at the moment TS2322 fires.

  5. The pin packages/react/src/hooks/__tests__/useNavigationOverlay.onRowClickArity-9357.test.tsx (new, 378 lines). Not a published surface — it is under __tests__/, which packages/react/tsconfig.json excludes by directory, and the 65-file census confirms nothing from it reaches dist. tsc -p packages/react/tsconfig.test.json is exit 0 at this head, so both @ts-expect-error rows are live: neither unused (TS2578) nor masking a second error. The instrument choice is justified by its own null result — both assignability directions hold, so an extends pin is green on the broken tree as well; exact identity plus a bytes read are what separate them. ⇒ right. One limit worth stating: the bytes half masks comments before matching, deliberately, so it is structurally incapable of catching defect (4).

  6. .changeset/9357-navigation-overlay-onrowclick-arity.md (new) — graded in ②.

② semver grading

Changeset file: .changeset/9357-navigation-overlay-onrowclick-arity.md, declaring '@object-ui/react': minor.

What the diff actually is: a breaking change to a published type. Measurement (2) is a source-level refusal of code that compiled against the previous release — semver-major in substance. It is nonetheless correctly declared minor: AGENTS.md line 253 forbids major in this repository ("objectui 自身的破坏性变更也标 minor(在正文里写清 breaking 语义即可)"), and scripts/check-changeset-no-major.mjs enforces that mechanically through changeset-guard.yml. @object-ui/react sits in the single 40-package fixed group, so the declaration cannot be scoped narrower either.

The rule's condition is that the breaking semantics be written out in the body. The changeset body now does exactly that — it names the refused class, quotes the TS2322, gives the one-line remedy, and no longer generalises. ⇒ the declared level is right, and the changeset body now earns it. The grading defect is not the level: it is that the compensating prose this rule demands was written on the artifact with one reader at release time, while the artifact with every reader forever still carries the sentence it was written to replace (①.4).

③ boundary flags

The earlier FAIL (5653078449, adopted at 5653094243) — its three ordered items, re-derived at the pinned head.

  1. Qualify the "Not breaking / No caller has to change" passage in the changeset. Done — verified in the diff, not taken from the repair claim.
  2. Add one @ts-expect-error row to the pin's compile half. Done — verified present and verified green (the row would be TS2578 if the boundary moved).
  3. "Nothing else. Do not touch the hook, the cast removal or the existing pin assertions." Complied with exactlyd8325fa79 touches two files and the hook is not one of them.

⇒ and item 3 is why this PR still fails. That review located the defect as "a CHANGELOG line the compiler refutes" and scoped its remedy to the changeset; it did not read the emitted .d.ts, where the identical generalisation sits on the very declaration being narrowed. Following its list to the letter left the defect standing on the more consequential carrier. The repair owed is one sentence in packages/react/src/hooks/useNavigationOverlay.ts: replace "so nothing a caller already wrote has to change" with the qualified form the changeset already uses — a one-parameter or unannotated handler is unaffected; a handler whose second parameter is annotated narrower than HandleClickModifiers is refused, and annotating it HandleClickModifiers or dropping the annotation is the fix. Nothing else — the declaration, the deleted assertion and every existing pin assertion are verified correct here and must not move.

Implementer flags and open_questions. open_questions is [] in both os-dev-reports — the dispatch report on the card (5651752744) and the post-repair report on this PR (5653567158). I read both in full, plus the PR body's "Acceptance notes", "Status" and "Disjointness" sections, the PR's own comment thread, and its review threads (pulls/9360/reviews and pulls/9360/comments: 0 each). The three out_of_scope_findings, each by name:

  • ObjectView.tsx around line 2191 declares an inline structural duplicate of HandleClickModifiers. Accepted as noted-not-filed and correctly out of scope — that site consumes component props, not this option. Escalated to card objectui#9357, which already owns the component-prop half.
  • The consumer declarations still understate their arity. Correct, and correctly untouched: the card's own "What is not decided here" section says the spelling is a ruling, not a mechanical edit. Stays with objectui#9357.
  • Measured deviation from AGENTS.md's body-rewrite catalogue, items 2 and 3. One observation on one endpoint; not blocking and not re-measured here. Escalated to the PM seat, unowned.

Clause-② declaration. The PR body carries no Clause-② line at all — 0 hits across the 14874-byte body, with needs:contract-review and Part of objectui#9357 lit in that same body as controls. Not a fault in itself: the carrier of record is the card's claim comment 5651508331 (Clause-②: yes), repaired for its missing standalone Branch: line at 5652799333. PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 9360, re-run now → exit 0, "the clause-② declaration is readable in the fixed spelling and both carriers agree". I did not inherit that reading: ①'s census derives yes independently, from a widened option on a published type.

Inherited-red paragraph — STALE at this head, in both directions. It names Doc Snippet Type Check and Skill Example Check; at e0c5c14b6 both are success. Check runs at this head, listed separately rather than lumped: 36 total — 32 success, 3 skipped (dependabot, Test (coverage), Test (coverage shard matrix)), 1 failure, 0 cancelled. The one failure is Bundle Analysis, which that paragraph does not name. It is genuinely inherited — failure on the merge-base b67b53bc0 and on the current origin/main tip e2feb13e1 — and its own bot comment (5653469550) reports a broken-gauge verdict on a ceiling, stating explicitly that it is "not a budget violation". ⇒ not this PR's red, but the paragraph as written is wrong about all three names and must be re-derived before landing.

Also stale, and not verdict-bearing. The PR body has not been updated for d8325fa79 — 0 hits for ts-expect-error, TS2322, MouseEvent, narrower — and its "Source compatibility" section still asserts, unconditionally, "No consumer breaks in either direction". Measurement (2) refutes that at this head. The body ships nowhere, so it does not carry the verdict; ①.4 does. The PR also carries no closing reference: it opens "Part of objectui#9357", and the card reports closed_by_pull_requests = 0, so merging this will not close objectui#9357 — which is arguably right, since the component-prop half stays open.

Implemented-by: claude/issue-9357-onrowclick-arity (mode:subagent)
Reviewed-by: session_01L5xpA5q533BgTTNADibEFt (domain:spec @ objectui seat — this review ran as an isolated subagent, which has no session of its own)

FAIL


Generated by Claude Code

…pped JSDoc

Comments are not stripped for this package, so the `onRowClick` docblock lands
verbatim in `dist/hooks/useNavigationOverlay.d.ts` — the hover text every
consumer of `@object-ui/react` reads. Its closing sentence still said a
one-parameter handler stays assignable "so nothing a caller already wrote has
to change", which the widening refutes for one class: a handler whose second
parameter is annotated narrower than `HandleClickModifiers` is now refused with
TS2322. The changeset two files away already names that class in bold, so the
release would have published a CHANGELOG and a `.d.ts` that disagree about the
same line.

Replaced with the qualified form: what stays assignable, the one class that has
to change, and the one-line remedy. Two residual restatements of the same
generalisation in the pin test's own prose ("nothing breaks either way", "no
consumer is broken by the widening") are corrected the same way; both sit in a
file this PR already adds and both are contradicted 80 lines below by that
file's own ACCEPT-SET BOUNDARY block.

Comment text only. Re-measured at this head: the accept set is unchanged (a
second parameter annotated React.MouseEvent is accepted at the merge-base
b67b53b and refused here with TS2322, while one-parameter and unannotated
handlers are accepted on both trees), the emitted declaration corpus is still
65 files with 295 exported declaration names and an empty symmetric difference,
and `dist/index.d.ts` is byte-identical.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3060.9 KB 3104.5 KB
Main entry chunk (gzip) 145.6 KB 350 KB
Entry file index-CblXRa8q.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) 544.84KB 130.47KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 213.54KB 59.33KB
fields (index.js) 247.89KB 62.50KB
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) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
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.83KB 10.95KB
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.92KB 14.22KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 252.27KB 65.55KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.55KB 57.83KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.68KB 27.68KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 84.36KB 20.78KB
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) 99.04KB 32.62KB
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) 25.28KB 7.80KB
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.04KB 5.36KB
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

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Contract review

Reviewed head: 0d983a987acb2cbcf51b3e4561d7923eadaf2d61 (read at 2026-09-13T23:48Z)

Spelling note: generic parameter lists are written as capitalised WORDS below
(RECORD for the record parameter type, OPT for an optional arm), because
GitHub's body sanitiser deletes angle-bracket-shaped spans.

How this head was read. Diff taken against the fork point b67b53bc0b3026cccbc263e54cf4baa4e1e5100f,
derived with git merge-base origin/main HEAD and never from base.sha (they coincide here,
because e0c5c14b6 merged that commit in; origin/main has since moved to efc1c9c400).
Shallowness checked rather than assumed: git rev-parse --is-shallow-repository is false in
/home/user/objectui and true in /home/user/objectstack. Three private git worktrees were
built with tsc through os-verify-lock.sh — this head, the fork point, and e0c5c14b67f9…,
the head the earlier FAIL judged — each VERDICT command-exit 0 (6s / 5s / 6s, ⚠ shared-box
seconds). Every worktree was removed afterwards and the shared checkout left clean. Three files,
+480/−3.

① derived judgments

Public-surface census, on the BUILT artefacts. All three trees emit 65 .d.ts and 65
.js. Fork point to head: exactly one .d.ts hash moved (hooks/useNavigationOverlay.d.ts)
and exactly one .js hash moved (the same module). Exported-declaration census over the whole
emitted corpus: 297 = 297, symmetric difference empty ⇒ no new exported symbol — and the
comparison can see a difference, because the same set poisoned with one sentinel name does compare
unequal. dist/index.d.ts is byte-identical (e5f6d3e11d06c0b4277a443804d28bcb266e5f56 both
trees). Control: UseNavigationOverlayOptions, HandleClickModifiers and useNavigationOverlay
are all lit in the head census.

1. UseNavigationOverlayOptions.onRowClick — from (record: RECORD) => void to
(record: RECORD, event OPT HandleClickModifiers) => void. Against the criterion — does this
relax an accept set or widen a public surface? — both, so Clause-②: yes on its own merits,
derived here and not taken from the card's claim. The accept set was measured in both worktrees
under the identical packages/react/tsconfig.test.json, one temporary probe file per case,
hash-proved onto disk before each run, removed under trap … EXIT INT TERM, absence re-checked:

probe at b67b53bc0 at 0d983a987
baseline, no probe file exit 0 exit 0
POISON control, a deliberate TS2322 exit 2 — 1 error exit 2 — 1 error
SUBJECT — second parameter annotated React.MouseEvent exit 0 — accepted exit 2 — one TS2322
control A — one-parameter handler exit 0 exit 0
control B — inline two-parameter handler, unannotated exit 2 — 3 errors exit 0
control C — second parameter annotated string exit 0 — accepted exit 2 — one TS2322

⇒ the line relaxes the option for anyone who wants the payload (control B could not even be
written before) and narrows it for annotated second parameters. Both directions are real, both are
declared. The repair is producer-side, and HandleClickModifiers is declared and exported eight
lines below the option in the same file, so it costs no import and adds no dependency edge.
right.

2. The 25-line JSDoc block on that member — a published surface, because it ships. It is 15 of
the 16 changed lines in the only emitted .d.ts that moved, so it is the hover text every consumer
of @object-ui/react reads. Read on the EMITTED declaration, not the source, perl -0777 with
JSDoc continuations flattened so a * between two words cannot hide a match, at e0c5c14b6 versus
this head:

term at e0c5c14b6 at 0d983a987
"nothing a caller already wrote has to change" 1 0
"The exception, and the one class that has to change" 0 1
TS2322 0 1
MouseEvent 0 1
contravariantly 0 1
control HandleClickModifiers 4 7
control objectui#9357 1 1
control Cmd/Ctrl 1 1
control UseNavigationOverlayOptions 2 2

The lit controls are what make that 0 a reading. Every load-bearing claim in the new block was
then checked rather than read: the remedy it publishes is actionable — HandleClickModifiers is
the sole declaration of that name in the whole emitted corpus and reaches the package root through
dist/index.d.ts re-exporting ./hooks/index.js re-exporting ./useNavigationOverlay.js, so
"exported from this module, and from @object-ui/react" is true; "declared just below" is true
(eight lines); the option and handleClick really are the same function type. Carrying
objectui#9357 into shipped hover text is this package's established form, not a leak — 124
issue references across 25 emitted .d.ts (control term: 0). ⇒ right, with two imprecisions
recorded in ③ that do not reach the bar.

3. The type assertion at the call site is deleted; handleClick now calls
onRowClick(record, event) through the declaration, and a six-line source comment is added beside
it. That comment ships too — comments are kept in the emitted .js as well — so this is an
emitted-bytes change, and the runtime claim was measured on the artefact rather than argued from
"assertions erase": the emitted hooks/useNavigationOverlay.js at the fork point and at this head
are identical once comments are stripped, while their raw bytes differ (the control that says
the comparison can see a difference), with the new objectui#9357 comment present at head (1) and
absent at the fork point (0) and Cmd/Ctrl lit at 2 in both. ⇒ right, and runtime provably
unchanged.

4. The pin packages/react/src/hooks/__tests__/useNavigationOverlay.onRowClickArity-9357.test.tsx
(new, 381 lines) — not a published surface: packages/react/tsconfig.json excludes __tests__/ by
directory and the 65-file census confirms nothing from it reaches dist. tsc -p packages/react/tsconfig.test.json is exit 0 at this head, so both @ts-expect-error rows are
live — neither TS2578-unused nor masking a second error. The instrument choice is justified by
its own null result: both assignability directions hold, so an extends pin is green on the broken
tree too; exact identity plus a bytes read are what separate them. ⇒ right. Stated limit: the
bytes half masks comments before matching, so it is structurally incapable of pinning the prose in
(2) — the boundary it does pin is what reds if the declaration moves.

5. .changeset/9357-navigation-overlay-onrowclick-arity.md (new) — publishes into
packages/react/CHANGELOG.md at release; graded in ②. Its worked example was compiled
byte-for-byte as published: accepted at the fork point, one TS2322 at this head. ⇒ right.

Nothing else in this diff touches an accept set or a public surface.

② semver grading

Changeset file .changeset/9357-navigation-overlay-onrowclick-arity.md, declaring
'@object-ui/react': minor.

What the diff actually is: breaking. ①.1's SUBJECT and control C are source-level refusals of
code that compiled against the previous release — semver-major in substance. It is nonetheless
correctly declared minor, because major is unavailable by repo rule: AGENTS.md's 版本号策略
section forbids it and scripts/check-changeset-no-major.mjs enforces it mechanically through
changeset-guard.yml, and @object-ui/react sits in the single fixed group, read from
.changeset/config.json as 40 packages (AGENTS.md's prose says 39 and is stale), so the
declaration cannot be scoped narrower either.

That rule's condition is that the breaking semantics be written out in the body. The changeset
body meets it — it names the refused class, quotes the TS2322, carries a worked example that
compiles exactly as published, and gives the one-line remedy. The level is right and the body now
earns it, on both carriers.
The grading defect the earlier round found — the compensating prose on
the artefact with one reader at release time, while the artefact with every reader forever still
carried the sentence it replaced — is gone, measured in ①.2.

③ boundary flags

The FAIL of record (5656788876, at e0c5c14b67f9…) — measured at THIS head, never taken from
the repair claim.

  • The one repair it owed: replace "so nothing a caller already wrote has to change" in the hook's
    JSDoc with the qualified form.
    Discharged. ①.2 is that measurement: 1 → 0 on the emitted
    declaration, the qualified replacement 0 → 1, four controls lit in the same file at both refs.
  • Its round-one item 1 — qualify the changeset passage. Still discharged at this head; the
    changeset body is unchanged by the repair commit and reads as ② describes.
  • Its round-one item 2 — one @ts-expect-error row pinning the boundary. Discharged and live,
    proved by tsc -p tsconfig.test.json exit 0 rather than by the row's presence.
  • Its round-one item 3 — "nothing else; do not touch the hook, the cast removal or the existing pin
    assertions."
    Complied with. e0c5c14b6..0d983a987 is +20/−7 across two files, every hunk
    inside a comment block, and zero emitted .js files move between those two refs while exactly
    one .d.ts does.
  • Its below-the-bar items. The inherited-red paragraph is now moot rather than merely re-derived:
    at this head all 36 check runs are complete — 33 success, 3 skipped (dependabot,
    Test (coverage), the coverage shard matrix), 0 failure, 0 cancelled — with Bundle Analysis
    among the successes and all four Test (shard n/4) green. Body staleness for d8325fa79:
    rewritten. Semver stays minor: re-derived in ②. No closing keyword: still true, and still
    correct, since the component-prop half stays open.

open_questions — the newest os-dev-report (card 5657016861) raises three; the two earlier
reports (card 5651752744, PR 5653567158) carry []. Each by name.

  • Q1 — sync the branch to origin/main so it inherits the now-green Bundle Analysis?
    Answered: moot. Zero reds at this head, and mergeable_state now reads clean. Nothing owed.
  • Q2 — was fixing the two further restatements in the pin test's header prose in scope?
    Answered: yes. Measured, not accepted: both hunks are comment text in a file this PR itself
    adds, no assertion, import or executable line moved, and nothing they touch ships. They removed a
    contradiction against that same file's own ACCEPT-SET BOUNDARY block.
  • Q3 — should tsconfig.base.json be wired into the root? Escalated, not this PR's. I
    re-derived the mechanism independently rather than inheriting either version of it:
    packages/react/tsconfig.json extends ../../tsconfig.json, which has no extends and sets no
    removeComments, so the block ships on TypeScript's default. Successor: the domain:devx lane,
    unowned.

out_of_scope_findings — eight across the three reports, each by name. Newest report: the
removeComments mechanism correction (verified above; successor — whichever PR next edits that base
config); Bundle Analysis flipping green on main (moot at this head); check:comment-mask-corpus
at its held-open ceiling, 1 disagreeing file / 1517 over-masked bytes (pre-existing, exit 0,
untouched by this diff — objectui#7882). First report: the inline structural duplicate of
HandleClickModifiers in app-shell/src/views/ObjectView.tsx near line 2191 (escalated to
objectui#9357
, which owns the component-prop half); the consumer declarations that still
understate their arity (stays with objectui#9357 — its "What is not decided here" section makes
the spelling a ruling); the AGENTS.md body-rewrite catalogue deviation (escalated to the PM seat,
unowned). PR-side report: the PR body's own unconditional generalisation (since rewritten), and
an explicit "nothing else was found".

Found by me, measured, and below the bar — none of these blocks landing.

  • The shipped sentence calls the refused class "a handler whose second parameter is annotated
    NARROWER than HandleClickModifiers". Control C above shows the true class is wider than that: an
    unrelated annotation is refused too (ev OPT string, accepted at the fork point, TS2322 at
    head), and it is neither narrower nor wider. The block is saved by its own next sentence, which
    states the exact governing rule — the annotation has to ADMIT HandleClickModifiers — and by a
    remedy that is correct for every member of the true class. A successor tightening the label word
    would improve it; nothing published is unsafe to act on.
  • The refusal is conditional on strictFunctionTypes, which the shipped sentence states
    unconditionally. Measured on one probe file with one flag flipped: false → exit 0, true
    exit 2 with one TS2322. This repo is strict, so the in-tree readings are unaffected; a
    non-strict consumer is over-warned, never under-warned.
  • The PR body's Status section is false at this head: it states needs:contract-review "is no
    longer on the PR". The label is on both the PR and the card right now, correctly re-hung when
    the head moved (card 5657049298). The body ships nowhere, so it carries no verdict.

Implemented-by: claude/issue-9357-onrowclick-arity (mode:subagent)
Reviewed-by: session_01L5xpA5q533BgTTNADibEFt (domain:spec @ objectui seat — this review ran as an isolated subagent, which has no session of its own)

PASS


Generated by Claude Code

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

ADOPTED — the PASS at 5657155742 is adopted verbatim. Both carriers cleared; landing.

Provenance and the three pre-landing checks, 2026-09-13T23:5xZ.

check reading
① same-form PASS at tier record 5657155742, head 0d983a987acb2cbcf51b3e4561d7923eadaf2d61, ## Contract review heading · sha in a code span · ①②③ itemized · independence pair · exactly one unhedged **PASS**, zero **FAIL**. Transcript tier read back claude-opus-5 on all 110 harness-stamped messages, no fallback.
② both carriers, machine-read PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 9360exit 0, run live just now: "the clause-② declaration is readable in the fixed spelling and both carriers agree".
③ every check 36 check runs, all complete: 33 success / 3 skipped / 0 failure / 0 cancelled / 0 timed_out. Bundle Analysis is green at this head, so the body's inherited-red section is moot rather than merely stale — the base fix reached this PR through the repair push, which is the only way it can.

Independence pair. Implemented-by: claude/issue-9357-onrowclick-arity (mode:subagent) · Reviewed-by: session_01L5xpA5q533BgTTNADibEFt. Both reviews in this PR's history ran as freshly spawned isolated subagents fed only the card, the rulings it references and the PR itself — ⛔ never the dispatch order or this seat's conclusions. Under the maintainer's ruling the pair is judged by independent measurement, ⛔ not by Claude-Session trailer identity.

Two rounds, and the second one earned its cost. The first review FAILed at e0c5c14b67 on a JSDoc paragraph that shipped a refuted generalisation into dist/hooks/useNavigationOverlay.d.ts. The repair discharged it — measured on the emitted file, not the source: the old sentence 1 → 0, the qualified replacement 0 → 1, TS2322 / MouseEvent / contravariantly 0 → 1, against four controls lit in that same file (HandleClickModifiers 4 → 7, objectui#9357 1/1, Cmd/Ctrl 1/1, UseNavigationOverlayOptions 2/2). It also found two further restatements the first census missed, in a file this PR itself adds.

Invariance measured on the artefact rather than argued: 65 .d.ts + 65 .js at every ref with exactly one of each moving fork-point → head; exported-declaration census 297 = 297, symmetric difference empty, against a poison-sentinel control that does compare unequal; dist/index.d.ts byte-identical; and the emitted hooks/useNavigationOverlay.js identical once comments are stripped, with the raw bytes differing as the control. ⭐ Worth carrying: comments are kept in the emitted .js too, so a call-site comment ships there as well — the .d.ts is not the only published face of a comment.

Six accept-set probes across two trees, each hash-proved to disk and removed under trap, with a POISON leg that must fail so an "accepted" reading cannot be confused with a probe the compiler never opened. The reviewer added its own control C — an unrelated annotation (ev?: string) — and it is refused too.

Recorded, ⛔ not blocking, owed to whoever next touches this surface

  1. The shipped sentence calls the refused class "annotated NARROWER than HandleClickModifiers"; control C shows an unrelated annotation is refused as well. Saved by the next sentence, which states the exact rule ("must ADMIT HandleClickModifiers") and a remedy correct for the whole class. A successor could tighten one word.
  2. The refusal is conditional on strictFunctionTypes and is stated unconditionally. Measured with one probe and one flag: false → exit 0, true → exit 2. ⇒ it over-warns a non-strict consumer and never under-warns, which is the safe direction.
  3. AGENTS.md's version-alignment prose says the fixed group is 39 packages; .changeset/config.json says 40. Pre-existing, unowned, governed surface — already on this seat's list for the maintainer.

Carriers cleared from both this PR and card objectui#9357 in one pass. Landing next: ready → auto-merge → merge queue. ⛔ Card objectui#9357 stays open and keeps its state — this PR carries no closing keyword, by design.


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review September 13, 2026 23:52
@claude
claude Bot added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit 0ce32d5 Sep 14, 2026
38 checks passed
@claude
claude Bot deleted the claude/issue-9357-onrowclick-arity branch September 14, 2026 00:11
@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

⚠️ Correction to one line of the adoption record above (5657164973).

I wrote: "comments are kept in the emitted .js too, so a call-site comment ships there as well — the .d.ts is not the only published face of a comment", and offered it as a general reading. It is not general. It is true for packages/react, which builds with plain tsc and sets no removeComments, so TypeScript's default keeps them — that is what was measured here, and that part stands.

Measured on packages/components a few hours later, by the review of PR objectui#9358: every prose probe reads 0 in dist/index.js and dist/index.umd.cjs, with normalizeFilterOperator lit at 1 and 7 as controls in the same files, and of 1,831 surviving block comments 1,830 are /*@__PURE__*/.

whether a source comment reaches the emitted .js is a property of the package's build toolchain, ⛔ not of the repo. Measure it per package before claiming a comment ships. Nothing in this PR's verdict rested on the over-general half — the repair and every measurement behind it were on this package's own emitted files.


Generated by Claude Code

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants