CI ran lint with continue-on-error because 23 errors predated it. All are
fixed, so the flag is gone and a typecheck step is added beside it — nothing
else in CI compiles the test files, so a type error in a fixture could only
ever be found on a developer's machine.
Real fixes, no blanket disables and no suppressions:
- react-hooks/set-state-in-effect (6): each was "reset or seed state when an
input changes", so each becomes a state adjustment during render behind a
previous-value guard — the documented React pattern for it. Two were also
latent bugs. transactions/page.tsx depended on `others`, rebuilt by .filter()
every render, so the effect already ran every render, one commit late.
shared/page.tsx depended on the `tagIds` array identity, so the row window
was reset more often than "changing any filter" claims — including after
"Show more", the one interaction it must not undo; the key now joins by
value. csv-import-modal reads localStorage once at mount, which is a lazy
useState initializer, not an effect (SSR-guarded, though the modal is
mounted from client state and never prerenders).
- react-hooks/static-components (3): SortIcon was declared inside SharedPage,
making it a new component type on every render. Hoisted to module scope with
the sort state passed as props.
- @typescript-eslint/no-explicit-any (9): the dynamically imported route
handlers in two integration tests. Typed as their real signatures with one
`as unknown as NextRequest` cast at the fixture boundary, so the call sites
and response bodies get checked instead of opted out.
- react/no-unescaped-entities (5): exact-equivalent entities (', "),
so no rendered text changes.
Also fixes the one pre-existing tsc error (unit/order-ingestion.test.ts:511):
`options` is optional, and `?? 0` keeps the assertion failing on an absent
list rather than silencing it with a non-null bang.
9 warnings remain (no-unused-vars, exhaustive-deps). eslint exits 0 on
warnings, so lint blocks on errors only — deliberately, since none of the
remaining warnings should stop a push.
The jump from "OK" to "Never again" is too big and most
disappointments live in the gap (user, 2026-07-28) — so a merely poor
meal either flattered itself as OK or got blacklisted.
Only 'never' raises the warning on a future order. A blacklist that fires
for every mediocre delivery is one nobody reads, so 'bad' records the
disappointment without triggering the alarm. Both set order_again = false
— you would not choose either again — and that split between "would I
order it" and "warn me about it" is the point of the extra level.
Migration widens the CHECK; nothing is removed, so no existing row needs
mapping.
Reverses a change made on request. The platform in the headline
fragmented the merchant: the same restaurant read as two, depending on
who carried the bag, and that is not a distinction anyone rating the food
cares about. It also already has a home — the expandable Order details
panel renders expense_metadata.platform next to its heading, which is
where the user asked for it.
The fragmentation was worse than cosmetic. merchantVerdict joined on an
exact merchant_normalized, and the platforms capitalise differently
("TEG Kebabs & Biryani" on Uber Eats, "TEG KEBABS & BIRYANI" on
DoorDash), so one restaurant kept two separate histories and a "never
again" recorded through one app never warned in the other — silently
defeating the point of the memory. Now case-folded; verified on real
data, where the DoorDash order sees 1 prior verdict against 0 before.
81 existing descriptions backfilled in one transaction, dry-run first and
dumped beforehand. The regex is anchored to the end so suburb parens
survive: "Order - Coles (Wyndham Vale) (Uber Eats)" becomes
"Order - Coles (Wyndham Vale)", not "Order - Coles".
The ledger already knew we had ordered from a place; it did not know the
food was bad. Orders got repeated from places we disliked because nobody
remembered by the time the next one went in. That is what the receipt
ingestion was for (ING-9) and the last piece was missing: order_reviews
existed as a table with no API, no UI and no writes.
Four levels, not three. "Loved" and "liked" are both "would order again"
but only one is worth a detour, and "ok" is not a recommendation.
A verdict belongs to a person, not to an order. A shared meal produces
two opinions and they routinely disagree — that disagreement is the
useful part, and the old UNIQUE on transaction_id alone could not hold
it. Now UNIQUE (transaction_id, participant_id), and the default is the
signed-in user rather than the owner: Sonu authenticates through the same
Traefik OAuth as participant 4, so an owner default would have filed her
verdict under his name.
Per-item opinions key on the item DESCRIPTION, not its index. An index is
meaningless across orders; "the Pad Thai here is good" is the signal that
has to survive into the next order from the same merchant. Only the two
poles are offered — a per-item "ok" answers neither of the questions you
ask at order time.
Sharing is recorded as a real 50/50 split, not a decorative flag. The
split already IS the record that an order was shared, and two records of
one fact drift apart.
An ABSENT item_verdicts means "leave them alone"; an empty array clears
them. Without that distinction a note-only save silently wipes every
per-item opinion — the same shape as the bug that reset `settled` on
split rewrites, and just as invisible on screen. Mutation-tested: making
keepItems a no-op fails exactly one test.
mockDbWithPool gained queryRow. Omitting an export from the mock makes it
undefined at the call site, which fails as "not a function" and reads
like a code bug rather than a test-harness gap.