Found while implementing objectui#6534. Filed unassigned, recording only — not graded, no type.
Measured on claude/issue-6534-anonymous-user-id, which is origin/main @ bf3a03c1d plus that card's fix. The observation is older than that branch and unaffected by it: it lives in the normaliser's SIGNED-IN branch, which objectui#6534 does not touch.
What was measured
objectui#6534 closed the anonymous branch's missing id by giving it id: null. The signed-in branch beside it forwards three keys raw, out of a cast that declares all three optional:
constu=useras|{id?: string;name?: string;email?: string;role?: string;[key: string]: unknown}|null|undefined;…return{id: u.id,// ← `undefined` when the session has no `id`name: u.name,// ← sameemail: u.email,// ← samerole: u.role??'user',isPlatformAdmin: u.isPlatformAdmin??false,positions: u.positions??[],};So buildExpressionUser({ name: 'B', email: 'b@c.d' }) returns an object where 'id' in built === true and built.id === undefined — present-and-always-undefined, which is precisely the shape objectui#5424 removed roles from this same object for: "the shape that teaches the wrong thing", where the context answers rather than being plainly absent and the answer is silently wrong.
The bottom three keys already defend against this with ??; the top three do not. That asymmetry is inside one object literal.
Why this is recorded and not fixed
It is not reachable today, as far as anything measured shows. The only production input is useAuth().user, a better-auth session principal that always carries id; packages/auth/src/types.ts derives it from the spec's AuthUser, whose id is required. So this is a latent shape hazard, not a live fail-open — unlike objectui#6534, which was reproduced on screen.
And the fix is a decision, not a mechanical edit. At least three defensible shapes, and they disagree about what a session with no id even IS:
id: u.id ?? null — consistent with the anonymous branch, but it is a FALLBACK in the consumer, which is the thing #0.1 and objectui#6534's own scope fence both forbid; it also silently equates "signed in, no id" with "signed out".- Treat a session with no
id as anonymous (fall to the !u branch) — arguably the honest reading, but it changes which branch runs for a real input and needs a census of what actually reaches the normaliser. - Narrow the cast so
id is required and let the producer be wrong loudly — contract-first, and the direction AGENTS.md #0.1 points; costs a type change on a shared normaliser.
objectui#6534 was fenced as "a tightening and only a tightening" with the shape settled in advance, so widening it into this would have been out of scope in two directions at once. Filed separately rather than folded in.
Related
- objectui#6534 — closed the same class on the ANONYMOUS branch; this is the residue its fix made visible by fixing the other half.
- objectui#5424 — set the "no present-and-undefined key" precedent on this exact object, and is the reason
undefined is not an acceptable answer here either.
Generated by Claude Code
Generated by Claude Code
Found while implementing objectui#6534. Filed unassigned, recording only — not graded, no type.
Measured on
claude/issue-6534-anonymous-user-id, which isorigin/main@bf3a03c1dplus that card's fix. The observation is older than that branch and unaffected by it: it lives in the normaliser's SIGNED-IN branch, which objectui#6534 does not touch.What was measured
objectui#6534 closed the anonymous branch's missing
idby giving itid: null. The signed-in branch beside it forwards three keys raw, out of a cast that declares all three optional:So
buildExpressionUser({ name: 'B', email: 'b@c.d' })returns an object where'id' in built === trueandbuilt.id === undefined— present-and-always-undefined, which is precisely the shape objectui#5424 removedrolesfrom this same object for: "the shape that teaches the wrong thing", where the context answers rather than being plainly absent and the answer is silently wrong.The bottom three keys already defend against this with
??; the top three do not. That asymmetry is inside one object literal.Why this is recorded and not fixed
It is not reachable today, as far as anything measured shows. The only production input is
useAuth().user, a better-auth session principal that always carriesid;packages/auth/src/types.tsderives it from the spec'sAuthUser, whoseidis required. So this is a latent shape hazard, not a live fail-open — unlike objectui#6534, which was reproduced on screen.And the fix is a decision, not a mechanical edit. At least three defensible shapes, and they disagree about what a session with no
ideven IS:id: u.id ?? null— consistent with the anonymous branch, but it is a FALLBACK in the consumer, which is the thing #0.1 and objectui#6534's own scope fence both forbid; it also silently equates "signed in, no id" with "signed out".idas anonymous (fall to the!ubranch) — arguably the honest reading, but it changes which branch runs for a real input and needs a census of what actually reaches the normaliser.idis required and let the producer be wrong loudly — contract-first, and the direction AGENTS.md #0.1 points; costs a type change on a shared normaliser.objectui#6534 was fenced as "a tightening and only a tightening" with the shape settled in advance, so widening it into this would have been out of scope in two directions at once. Filed separately rather than folded in.
Related
undefinedis not an acceptable answer here either.Generated by Claude Code
Generated by Claude Code