fix(routers): four small edges - error shape, lock key, typeahead feedback

↗ view on GitHub · Amal · 2026-08-18 · 57cf3b11

The low-severity residue of the adversarial review of this stack,
batched because each is a few lines and none changes a contract.

1. 42P01 ALONE DOES NOT MEAN "OUR TABLE IS MISSING"
   The deploy-before-migrate tolerance in getUserRouterModels swallowed
   any undefined_table error and answered "no saved models". But 42P01
   is raised for whatever relation was missing - a policy, view or
   trigger reaching for some other dropped table raises it from this
   query too. The PGRST205 arm already required the message to name
   user_router_models; the 42P01 arm now does the same, so a genuine
   schema fault surfaces instead of quietly reading as an empty
   selection.

2. ADVISORY LOCKS ARE KEYED WITH hashtextextended IN THIS REPO
   replace_user_router_models keyed its per-(user, router) advisory lock
   with hashtext(...) - an int4 hash. Every other advisory lock in this
   schema uses hashtextextended(..., 0), an int8. Two lock keys collide
   when their hashes collide, and collisions in a 32-bit space are not
   exotic: unrelated writers would then serialize on each other for no
   reason. Changed in BOTH of the places that now carry the hardened
   definition - backend/migrations/20260819_01_harden_replace_user_router_
   models.sql (the forward-shipping step for existing deployments) and
   backend/schema.sql (the fresh install) - kept byte-identical between
   them, so the function body diffs clean. The already-merged
   20260818_01 is not touched.

3. ENTER THAT DOES NOTHING MUST SAY WHY
   The typeahead's "Enter adds the typed id" path returned silently
   whenever the text was not id-shaped. From the user's side that is
   indistinguishable from a dead key. Enter on non-empty, non-id text
   now sets the same error line the save failure uses; an empty box
   stays silent, because nothing was asked for.

4. TWO SMALLER EDGES IN THE SAME COMPONENT
   - ARIA: the "Press Enter to add this model ID" hint and the "No
     matching models" placeholder lived INSIDE role="listbox", which
     admits only option/group children. Both move out to the surface
     around it; the listbox now holds options only, so the indices a
     screen reader announces line up with aria-activedescendant.
   - The 200-character model_id CHECK from the migration is mirrored
     into normalizeTypedModelId, so an over-long paste fails in the box
     with "Model IDs are at most 200 characters." instead of as an
     opaque 400 from the profile PATCH.

Also here: the stale-selection effect added earlier in this branch trips
react-hooks/set-state-in-effect, which is an ERROR in this config - the
frontend lint gate was red on this branch. Suppressed with the reason
(it reconciles state against asynchronously arriving data, and the
functional update is a no-op unless the selection is genuinely stale).

Tests: routerModels.test.ts (42P01 naming a different relation must
reject - pre-fix "promise resolved '[]' instead of rejecting");
RouterSettingsSection.test.tsx (the no-op-Enter case becomes a
feedback case, plus an over-long-id case, a hint-outside-the-listbox
case, and a normalizeTypedModelId length boundary at exactly 200). All
five fail pre-fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Repository open-legal-products/mike
Author Amal <mamalanand3@gmail.com>
Authored
Committed
Parents a63a615e
Stats 7 files changed , +150 , -26
Part of Harden model-router selection and failure handling

Capture this commit into my fork

Download a Markdown prompt that tells Claude how to port this exact commit into your working tree. Run it via claude -p < capture-commit-57cf3b11.md from inside the repo you want the change in.

⬇ Download capture-commit-57cf3b11.md