Skip to content

finding(app-shell): buildExpressionUser's parameter is still unknown, so the input contract objectui#6551 declared is enforced at no call site #6559

Description

@claude

Found while implementing objectui#6551. Filed unassigned, recording only — not graded, no type.

Measured on claude/issue-6551-signedin-user-id-required (origin/main @ 0235ce7c1 plus that card's fix).

What was measured

objectui#6551 narrowed the cast buildExpressionUser reads its input through, so the module now DECLARES what a signed-in session is: id, name and email required, mirroring the spec's AuthUser. But the parameter it narrows behind is still unknown:

exportfunctionbuildExpressionUser(user: unknown): Record<string,unknown>{constu=userasExpressionUserSession|null|undefined;

So the declaration binds nothing at any call site. buildExpressionUser({ name: 'B', email: 'b@c.d' }) still compiles — that is pinned as a deliberate control case in expressionUser.sessionContract.types.test.ts ("the exported function still accepts an unchecked input — the cast is not a parameter check"), green on both legs of that file's discrimination run, precisely because it is the limit of what #6551 changed rather than a thing #6551 fixed.

Declared, not enforced. The gap is between the module's now-honest input type and the four production call sites, none of which the compiler checks against it.

The census, as measured

Every production caller passes useAuth().user, whose type is @object-ui/auth's AuthUser (extends the spec's AuthUser, so id/email/name are all required):

  • packages/app-shell/src/console/AppContent.tsx:659 and :909
  • packages/app-shell/src/views/RecordFormPage.tsx:187
  • apps/console/src/components/InternalFormRoute.tsx:78

So all four would type cleanly against a narrowed parameter today — this is a latent enforcement gap, not a live break, same class as objectui#6551 itself.

Why this is recorded and not fixed

objectui#6551's dispatch fenced it explicitly: narrowing the CAST was the settled shape, and "widening the parameter is a different question and a different card". It is also not mechanical — it is a signature change on a name published from the package entry (packages/app-shell/src/index.ts), and at least one existing consumer types the seam deliberately as unknown: apps/console/src/__tests__/internalFormShell.test.tsx:95 mocks it as buildExpressionUser: (user: unknown) => user.

There is also a real question of what the parameter should say, which is a decision and not an edit: ExpressionUserSession | null | undefined makes the four sites checked and the !u branch still reachable, but it also makes the normaliser refuse an unknown a caller has not yet narrowed — which is the shape AppContent and RecordFormPage would have to satisfy from useAuth() rather than from a cast.

What holds the line today

expressionUser.mountSites.ratchet.test.ts refuses a mount site that hand-rolls a descriptor, but it does so by SCANNING SOURCE, not by type-checking. It catches a new site that stops calling the normaliser; it cannot catch a site that calls it with the wrong thing.

Related

  • objectui#6551 — narrowed the cast; this is the residue its fix made visible, and its dispatch named it as a separate card.
  • objectui#6534 / objectui#5424 — the shape precedents on this same object.

Generated by Claude Code


Generated by Claude Code

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions