Fix round 2 for Task 7 (plan amendment 9e3624e5):
- N1 (Important): the memo key carried scope and permission but not the
subject, so a request that reassigns ctx.user mid-flight (impersonation,
step-up auth, session revocation, or an authz-before-auth middleware
ordering mistake) could be served the previous principal's cached
verdict. subjectId (typeof + String, matching the existing scope/value
encoding style) is now folded into every memo key.
- N2 (Minor): the primitive-value memo key used String(resource), which
collapses distinct Symbol("row") values into one slot and maps -0 onto
0's slot. Added a dedicated bySymbol identity memo (WeakMap-style, but a
plain Map since symbols aren't valid WeakMap keys pre-registry symbols
and the memo is request-scoped anyway) and special-cased Object.is(x,-0)
to render as "-0".
- N3 (Minor): the rejected-redirect console.error interpolated
redirectTo directly, exactly the value most likely to carry CR/LF in
that branch. Switched to JSON.stringify(redirectTo) for the log line.
- N4 (Minor): a non-ASCII (but otherwise valid, local) redirectTo passed
isLocalPath and then threw inside `new Response` building the Location
header. Wrapped it in encodeURI().
Added 5 regression tests: subject swap re-evaluates, clearing ctx.user
denies, two same-description symbols get separate verdicts, 0 vs -0 get
separate verdicts, non-ASCII redirectTo 303s with an encoded location
instead of throwing. N1 revert-checked: temporarily restored the
two-element (no-subject) key and confirmed both subject-swap tests fail
against it before restoring the fix.