[Quality] Repo-wide audit fixes: error containment, drifted forks, silent failures (stacked on #294→#295)
From the PR description
Code quality follow-up: repo-wide audit fixes
A seven-area audit of the codebase (backend modules, backend lib/workers, remaining route files, frontend data layer, frontend components, Word add-in + shared UI, cross-cutting infra) surfaced ~100 findings. This PR lands the S/M-effort, high-confidence fixes - real bugs, silent-failure paths, drifted copy-paste forks, and missing safety infrastructure.
Diff size: 126 files, +2,857 / -2,604 (net +253). Ignoring whitespace it is +2,235 / -1,982 - the 620-line gap is almost entirely backend/src/routes/wordChat.ts, which reads +503/-486 raw but +55/-38 under git diff -w, because wrapping every handler in asyncRoute(...) re-indents its body. Per area: backend 74 files (+1,887/-1,712), frontend 30 (+706/-677), word-addin 10 (+79/-51), e2e 7 (+141/-149), .github 2 (+17/-3), docs 2 (+21/-12).
Stack mechanics (read this first)
This is the top PR of a three-PR architecture stack. Its base branch is olp-pr/service-layers (#295), not main, so the diff below contains only this PR's own 8 commits - the 14 commits of #294 and the 2 of #295 are not in it. (Earlier revisions of this description said "this PR's diff includes their commits until they land"; that is no longer true - the base was retargeted.)
| PR | Base branch | Commits in its own diff | Merge order |
|---|---|---|---|
| #294 - durable job queues | main |
14 | 1st |
| #295 - per-domain modules + service layers | olp-pr/durable-queues |
2 | 2nd |
| #356 (this one) - repo-wide quality fixes | olp-pr/service-layers |
8 | 3rd |
olp-pr/durable-queuesandolp-pr/service-layersexist in this repo only as mirrors of the identically-named branches on theamal66/mikefork, so GitHub can compute a clean stacked diff. They are force-pushed together on every rebase; origin and fork tips are kept byte-identical (currentlyf59aee93andb82bbbed). Nothing else should branch off them.- Please do not delete
olp-pr/service-layerson merging #295 until this PR has been retargeted, or this PR's base disappears from under it. - Why stacked rather than based on
main: most of these fixes live inside the module layout #295 creates. Basing onmainwould conflict wholesale with that motion. If #295 is not taken, I will re-port this ontomain- the fixes are independent of the layout, the file paths are not.
The 8 commits:
| Commit | Scope |
|---|---|
b18d1d4e |
contain async route errors in the un-modularized routes |
3807e51c |
collapse module copy-paste that had already diverged into bugs |
e8cabc78 |
backend/lib: citation merge, event-loop stalls, provider drift |
a295374e |
type-check the tests, share the mocks, measure what matters |
c9c88f5e |
frontend: one upload path, one SSE reader, guarded stream lifecycles |
d13d7126 |
frontend: component correctness, keyboard access, dead weight |
0fe18c8a |
word-addin: truthful edit cards, fail-closed privacy, real UI sharing |
b32e32d5 |
frontend test: a failing reader.cancel() must not eat frames already read |
What's fixed
Backend - error containment
Express 4 does not understand promises: an async handler that rejects is invisible to the router, so the request hangs until the client or proxy times out, and nothing is logged. Three route files had no containment at all - wordChat.ts (5 handlers), audit.ts (2), sourceDocuments.ts (1) - so 8 previously-unguarded handlers now go through a wrapper. Three other files (quickActions.ts 4, workflowAddons.ts 3, modules/workflows/workflows.routes.ts 21) each carried their own private copy of the same asyncRoute helper; all 28 now import the shared one. New backend/src/middleware/asyncRoute.ts provides asyncRoute + routerErrorHandler(tag), and app.ts gains a terminal error handler so anything a router re-raises stops at an opaque internal_error body instead of Express's default handler, which leaks stack traces outside production. Body-parser's own 400/413 keep their status and code, and a response that has already started streaming (SSE, file download) is handed on rather than rewritten.
Backend - silent failures and route correctness
- Document rename returned 200 on failure in two modules (
projects.documents.ts,library.service.ts): thedocument_versionsupdate's error was never destructured, and the response echoed the requested filename. Both now read back. - Quick-action DELETE returned 204 without matching a row; tabular review/chat delete + rename ditto;
sort_ordervalidation was unreachable for non-integers (silently coerced to 0); DB errors surfaced as 404 "not found" in quick-actions/add-ons; the audit view silently narrowed to own-events on a failed project lookup; CSV export could never show display names. sourceDocumentsreturned raw upstream error bodies as 502 (now redacted viasafeErrorMessage; credential failures map to 400).- Folder-move cycle guard was missing in both the library and projects ancestor walks (an interleaved move could persist a cycle and spin a request forever). Both now walk with a
visitedset. - History page learns the
wordsurface label - the audit rows themselves come from #294, see the note below.
Backend - deduplication that had already caused bugs
- The project upload pipeline was a 183-line fork of the shared one (
processProjectDocumentUpload, base lines 421-603) and had drifted: it omittedfolder_idfrom its response andlibrary_kind/library_folder_idfrom its INSERT. Deleted -projects.documents.tsdrops 603 → 415 lines and delegates tocreateDocumentFromUploadwith asurfaceparameter. countPdfPagesexisted twice (documents.shared.ts,projects.shared.ts) → one copy inbackend/src/lib/pdfjs.ts.- Suffix handling: 8 duplicated suffix-extraction implementations and 7 separate allowlist checks →
parseAllowedSuffix/documentSuffixinlib/documentTypes.ts;ALLOWED_DOCUMENT_TYPES.hasnow appears exactly once. devLogexisted 6 times → one copy inbackend/src/lib/log.ts.
Backend lib - correctness
- Citation fallback merged positionally. Fallback citations are POSTed to CourtListener as one newline-joined blob, and it returns one row per citation it detects, not one per input line - so an unparseable input (0 rows) or a parallel cite (2 rows) shifted every later citation's verdict onto the wrong citation, and surplus rows were appended as phantom results. Now keyed by a normalized citation string (
citationMatchKey(): lowercase, strip./,, collapse whitespace) with a used-row set. scryptSyncran on every encrypt/decrypt - ~40ms of blocked event loop each. Derived keys are now memoized per (secret, salt) in two places:lib/userApiKeys.tsandlib/mcp/client.ts(the latter is hit on every MCP tool call).- Chat-generated
.docxwas the only file type that never got a PDF rendition -generateDocx's 76-line drifted tail never wrotepdf_storage_pathand never calleddocxToPdf. It now delegates topersistGeneratedFile. Same change fixes abuf.buffer as ArrayBuffercast that ignoredbyteOffset. - Raw LLM stream logging is hard-disabled in production (
lib/llm/rawStreamLog.ts): these logs contain verbatim prompts, client documents and completions, and were previously gated only by an env flag.rawStreamLoggingAllowed()now returns false underNODE_ENV=productionregardless of the flag, and.env.examplelabels both vars "NEVER ENABLE IN PRODUCTION". CLAUDE_API_KEYdrift:userApiKeys.tsalready accepted it,providers.tsdid not - so Settings showed a green key and every request threw "not configured".ENVIRONMENT_KEY_ALIASESnow mapsANTHROPIC_API_KEY → ["CLAUDE_API_KEY"].- Shared-project email normalization drift:
tabularReviewsOverview.tsandworkflowsOverview.tspassedparams.userEmail ?? nullinto a case-sensitiveshared_withcontainment check, where projects already passed?.trim().toLowerCase() || null. Now all three match. - The stale-work sweep is bounded (
MAX_GENERATING_CELLS_PER_SWEEP = 500) with batched job lookups, so a large backlog can't produce an unbounded query. backend/.env.examplegains 72 lines documenting 31 previously-undocumented env vars - including 4 secrets (MCP_CONNECTORS_ENCRYPTION_SECRET,MCP_OAUTH_CLIENT_SECRET, ...),API_PUBLIC_URL,TRUST_PROXY_HOPS, the Ollama trio, 12 commentedRATE_LIMIT_*defaults, and the 2 raw-LLM-log vars above.- One dependency removed:
fast-diff. Its only consumer,lib/docxTrackedChanges.ts, no longer needs it (+3/-41, which also drops a deadindexAll(), unusedW_NS_ATTRS, and an unreferencedexport const _internaltest hatch).
Frontend
- Upload endpoints threw plain
Errors instead ofMikeApiError, so a 403mfa_verification_requiredon upload never opened the MFA popup - it rendered raw JSON. All 7 hand-rolledFormDatablocks inmikeApi.tsnow go through oneapiUploadRequest; zero hand-rolled multipart fetches remain. Regression test:"keeps the MFA code on upload failures so the popup can open". - Three hand-rolled SSE parsers, two of which dropped the final frame.
TRChatPanel.tsx:1205andTabularReviewView.tsx:391both didif (done) break;before flushing, andbuffer = lines.pop()discarded the trailing partial - so a stream that closed without a final newline lost its last event. Replaced by onereadSseFramesgenerator infrontend/src/app/lib/sse.ts, which flushes withdecoder.decode()on done. 11 transport tests. - Stream lifecycle:
useAssistantChatandTRChatPanelhad no unmount abort and no generation guard - two concurrent turns could interleave into one message, and the drip interval survived unmount. Both now carry a generation counter that the cleanup effect bumps, plus an identity-checkedabortRefreset. PdfViewrender race: rapid zoom wiped the container while aRenderTaskwas still writing into it. The task is now cancelled before the wipe, with two additional staleness gates.- Keyboard access: table rows were mouse-only. Fixed once in
TablePrimitive- 19<TableRow>sites inherit it, of which 8 are actually interactive (onClick) and therefore genuinely gain keyboard reachability; the other 11 are skeleton /interactive={false}rows. - Re-render correctness:
AuthContextandUserProfileContextrebuilt their context value on every render (and two more in(pages)/layout.tsx) - all four memoized. - Sidebar persistence bug:
localStorage.setItem("sidebarOpen", isSidebarOpen.toString())wrote the effective value, while the restore path reads it back as the desktop preference - so collapsing the sidebar on mobile corrupted the desktop setting. Now writesisSidebarOpenDesktop. AskInputPopupdouble-submit under StrictMode: submit moved out of a dependency-array-less effect into the resolving handler, with apendingSkipIdparameter so skipping the last outstanding item still submits.NewTRModalempty picker: project documents were snapshotted into state when the modal opened, but the parent passes[]until the project loads - opening early left the picker permanently empty. Now derived rather than copied.- Also: impure
setTabsupdater, double project fetch per chat-page mount, unhandledselectAllMatchingrejections (×3 hooks), and a dead-code sweep of roughly 280-300 lines including three deleted files (ProjectPickerModal.tsx,useGenerateChatTitle.ts, and the add-in'sbutton.tsx).
Word add-in
- Edits were reported as failed when they had actually applied:
mutationAppliedwas set after thecontext.sync()that executes the mutation, so a throw at or after the sync produced "Word couldn't apply this change" for an edit Word had already made - inviting a duplicate re-apply. The assignment moved above the await. - "View edit" permanently destroyed the bookmark anchor of an edit that had been accepted in Word's Review tab:
revealPersistedTrackedEditcalleddeleteBookmark+removeWordEditAnchorwhenever the range showed zero tracked changes. Reveal is now read-only. Pinned by a new e2e case appended toword-addin/e2e/tracked-edit-persistence.spec.ts: "View never deletes the bookmark of an edit resolved outside Mike", verified red-before / green-after. - Privacy mode failed open: the
.catch()on theOfficeRuntime.storageread had an empty body, so a failed settings read left the mode at its initial "cloud" value - silently reverting the user's "local, never persist" choice. Now fails closed tolocal. - 4 design-token value mismatches vs the frontend, in
src/shared/styles/tokens.css:--app-background#fafafa→#f9fafb,--app-surface-hover#f5f6f8→#f9fafb,--app-surface-active#eceef2→#eff0f3,--app-floatingrgba(253,253,254,.82)→#fefefe. - Dead byte-identical
src/shared/ui/button.tsxfork deleted; composer regains a height cap (max-h-48) and real accessible names ("Stop response" / "Send message"); the hardcodedREACT_APP_DEFAULT_MODEL: "gemini-3-flash-preview"inwebpack.config.jsbecomesprocess.env.REACT_APP_DEFAULT_MODEL || "";e2e-live/scripts are wired into npm scripts and intotsconfig.e2e.jsonso they type-check.
Tests & CI
- Backend tests were never type-checked - excluded from
tsconfig.json, and vitest strips types without checking them. Newbackend/tsconfig.test.json+npm run typecheck:test+ a CI step in the backend job. It surfaced ~69 latent type errors across 16 test files (measured: 71 → 2 under the new project, and the 2 survivors are aCannot find module 'bullmq'artifact of the measurement environment, present identically before and after). The bulk: 21 implicit-any, 17 spread-arity, 10 tuple-index, 7 node16.jsspecifier errors. - Test mocks deduped: three near-duplicate Supabase query mocks (69 / 69 / 80 lines - the tabular one a superset with
rpcCalls/operationsrecorders) → one 141-line__tests__/helpers/supabaseMock.ts; three of six identical auth mocks →__tests__/helpers/authMock.ts. Honest accounting: the suites shed ~190 lines and the helpers add ~180, so this is roughly line-neutral repo-wide - the win is one mock to fix, not fewer lines. Four inlinemakeQuerymocks remain (chat, projectChat, user, wordChat) and three auth copies remain; finishing them is a follow-up. - Coverage scope widened from
src/lib/**tosrc/**(with an explicitexcludefor tests, since settingexcludeat all replaces vitest's defaults). The old scope leftroutes/,modules/,workers/andmiddleware/out of the report entirely, so the ratchet could not see a regression there. Widening the denominator moves measured coverage from 52.72/46.31/53.24/54.11 to 47.56/40.86/49.78/49.20, so the floors move 52/46/53/54 → 45/38/47/47 - numerically down, over a much larger population. This also repairs the scope shrink #295 caused by movinglib/tabular/**(1,539 lines) out of the old measured set without adjusting the floors. - e2e: shared
e2e/helpers.ts, a defaultexpect.timeoutof 10s inplaywright.config.ts, and 5waitForLoadState("networkidle")waits replaced with element waits (one of them the.catch(() => {})-swallowed variant) - zero remain. - The Word add-in CI workflow's path filter now includes the frontend shared-UI files it actually bundles (
frontend/src/shared/**,frontend/public/icons/**), and its header comment is corrected from "fully self-contained" to "NOT self-contained". You can see this working on this PR: the "Typecheck and Playwright (chromium + webkit)" check runs on #356's head commit and does not run on #294 or #295 - those two touch the shared UI without triggering the add-in suite.
Base-case replication - see the problems on olp-pr/service-layers
- Async route errors hang the socket. On the base,
git grep -n "asyncRoute" backend/src/routes/wordChat.tsreturns nothing. Make anyawaitin a/word-chathandler throw before headers are sent (e.g. pointSUPABASE_URLat an unroutable host): the request never completes, the client waits for its own timeout, and the process logs anUnhandledPromiseRejectionwith no request attribution. On this branch the same throw returns aninternal_errorbody immediately and logs[word-chat] unhandled route error. - Citation verdicts shift onto the wrong citation. On the base, call the chat
verify_citationstool with a list where one entry is a parallel cite (CourtListener's/citation-lookup/returns two rows for it) or is unparseable (zero rows). Every citation after it gets the previous one's verdict, and a phantom extra result is appended.backend/src/lib/courtlistener.tson the base doesfallbackRows.shift()per slot - that is the bug in one line. - Rename appears to succeed when it fails. On the base, revoke UPDATE on
document_versions(or point the version row at a missing id) and rename a project or library document: the API returns 200 with the new filename, the UI shows it, and a refresh shows the old name. Neither module read the update's error. - MFA popup never opens on upload. With MFA enrolled and an expired AAL2 session, upload a document from the web app: the 403
mfa_verification_requiredarrives as a plainError, so the MFA popup never opens and raw JSON error text is rendered. - Word add-in "View" destroys the anchor. Apply an edit, accept it in Word's Review tab, then click View on the card: the bookmark is deleted and the card demotes to historical, permanently.
- A privacy setting that fails open. In the add-in, set storage to "local, never persist", then make the
OfficeRuntime.storageread fail (offline / cleared storage): on the base the mode silently reverts to cloud and the next turn is persisted server-side.
PR replication - see them fixed
Pick any of these three; each is observable without reading the diff.
A. SSE last-frame loss (frontend, no infrastructure needed).
cd frontend && npx vitest run src/app/lib/sse.test.ts
11 cases pass, including "swallows a failing reader.cancel() so the frames still surface" (the tip commit b32e32d5) - a stream whose reader.cancel() rejects during cleanup still yields every frame the consumer already received. To see the old behaviour, run the same file against the base: sse.ts does not exist there, and the two buggy inline parsers in TRChatPanel.tsx / TabularReviewView.tsx drop the final frame when a stream closes without a trailing newline.
B. MFA-on-upload (frontend).
cd frontend && npx vitest run src/app/lib/mikeApi.test.ts -t "MFA"
The regression test asserts the thrown value is a MikeApiError carrying mfa_verification_required, which is what the popup keys off. End-to-end: enroll MFA, let the AAL2 session expire, upload - the popup opens instead of raw JSON.
C. Word add-in "View" is read-only (real Word host).
cd word-addin && npm run test:e2e -- tracked-edit-persistence
The case "View never deletes the bookmark of an edit resolved outside Mike" fails on the base and passes here. By hand: apply an edit → accept it in Word's Review tab → click View → the bookmark survives and the card stays live.
D. Route-surface parity (proves this PR changes no endpoints).
for r in fork/olp-pr/service-layers fork/olp-pr/code-quality; do
git ls-tree -r --name-only "$r" backend/src | grep -E '\.ts$' | grep -vE '__tests__|\.test\.ts$' \
| while read -r f; do git show "$r:$f"; done \
| perl -0777 -ne 'print "\U$2\E $3\n" while /\b(\w*[Rr]outer)\s*\.\s*(get|post|put|patch|delete|all)\s*\(\s*"([^"]*)"/g' \
| sort > "/tmp/$(basename "$r").routes"
done
diff /tmp/service-layers.routes /tmp/code-quality.routes && echo IDENTICAL; wc -l /tmp/*.routes
→ 144 / 144, IDENTICAL. Resolved through app.ts's mount table (note userRouter mounts at both /user and /users) that is 174 mounted method+path pairs, also identical on both sides. (An earlier revision of this description claimed "181 method+path routes"; that figure is not reproducible from either tree - 144 registrations / 174 mounted are the real numbers.)
Tradeoffs & design decisions (flagged)
- Stacked on #295, not
main- the fixes live in the module layout. Basing onmainwould conflict wholesale with the motion; I'll re-port if #295 is not taken. - The terminal error handler honors body-parser's 4xx statuses rather than blanket-500ing, so malformed JSON stays 400 and an oversized body stays 413. A response that has already started streaming is handed to Express to destroy - its status line is gone, so there is no honest alternative.
user.shared.errorMessagekeeps PostgREST detail text (now redacted) rather than flattening to a generic message - its detail strings are load-bearing in tests and UI.- Coverage floors went numerically down (52/46/53/54 → 45/38/47/47) while the measured population roughly doubled. A reviewer who reads only the numbers will read it as a weakening. The alternative - keeping the old floors against the new scope - would have failed CI immediately and told us nothing. Known drift shipped by this PR:
docs/testing-coverage.md:121says "35 / 29 / 37 / 36" and the config says 45/38/47/47. The doc line is wrong and should be corrected; flagging it rather than quietly fixing it in a force-push. - Mock dedup is roughly line-neutral repo-wide (-190 in suites, +180 in helpers) and only covers 3 of 7 Supabase mocks and 3 of 6 auth mocks. The value is one place to fix, not a line count.
- The
scryptSyncmemo Map is unbounded and keyed on the raw secret. In practice it holds one entry; a runtime secret rotation would leak the old one, and the plaintext secret stays heap-resident as a Map key. Accepted for a ~40ms-per-call stall on the request path; an LRU would be ceremony for N=1. - The citation-merge fix is unpinned. No test file for
backend/src/lib/courtlistener.tsexists anywhere on the branch. It is verified by the base-case replication above, not by a test - call that out if it blocks the merge and I will add one. - The generated-
.docxrendition fix is also unpinned.documentOps.generatedRendition.test.tscoversgeneratePpt, notgenerateDocx. Related side effect to be aware of:lib/__tests__/documentGeneration.test.tsmocks../storageand../downloadTokensbut not../convert, sogenerateDocxnow enters the realdocxToPdfpath and will shell out tosofficeon a runner that has LibreOffice. It is inside a try/catch so it cannot fail the test, but it is real I/O in a unit test. word-addin/src/shared/styles/tokens.cssalso gains--color-azure: 0, 136, 255;, which is not one of the 4 mismatch fixes - the name exists nowhere else in the repo and has no consumer (the frontend spells the same colour--color-blue). Happy to drop it.backend/src/lib/supabase.tsnow exportstype Db, but 22 files still redeclare it locally. Half-finished on purpose (the migration is mechanical churn); say the word and I'll finish it or drop the export.- CI ordering nit: in
ci.yml,npm test --if-presentruns beforenpm run buildandnpm run typecheck:test, so a type-broken mock still gets to run its tests first. Harmless, but the new gate fires later in the job than its commit message implies. - Deferred L-effort items (each its own PR): DocTable / TRChatPanel / FileDirectory splits,
toolDispatcherhandler registry,packages/contractsshared types (the SSE event contract is authored 3× with real drift), finishingmodules/tabulardecomposition (tabular.routes.tsis still 1,806 lines),wordChatmodularization, ChatView unification,usePaginatedListextraction, zod env config at boot, backend/add-in ESLint, and the remaining mock/Db-type dedup above.
Testing evidence
All CI checks are green on b32e32d5: CodeQL, gitleaks (full history), backend build and tests, frontend build and tests, playwright, Supabase stack integration tests, eval harness, the "Fresh install vs upgraded deployment" schema-drift job, and - new to this stack - the Word add-in "Typecheck and Playwright (chromium + webkit)" job, which now triggers because this PR fixed its path filter.
Suite sizes at the tip (static counts of it(/test( on b32e32d5; a live run may differ slightly through .each expansion):
| Package | Test files | Cases |
|---|---|---|
backend (vitest) |
80 | ~760 |
backend Supabase-gated (test:stack) |
5 | 24 |
| frontend | 97 | ~525 |
| word-addin e2e | 21 spec files | 154 per project → 308 runs across chromium + webkit |
(Earlier revisions of this description cited 658 backend / "351+" frontend / 114 word-addin. Those were snapshots taken before several of the commits above; the word-addin figure in particular ignored that playwright.config.ts defines two projects. The table above is the current tree.)
Type gates: tsc clean, plus the new tsc -p tsconfig.test.json gate over the test sources. Word add-in: both tsconfigs clean, including e2e-live/**/*.mjs, which was previously unchecked.
New tests added by this PR: 11 SSE transport cases (frontend/src/app/lib/sse.test.ts, one of them the tip commit), the MFA-upload regression in mikeApi.test.ts, and the tracked-edit "View never deletes the bookmark" e2e case.
Corrections to earlier revisions of this description
Kept here rather than silently edited away, because several of them are claims a reviewer may have already read:
- "96 async route handlers had no error containment" → the real figure is 8 newly guarded handlers in 3 files, plus 28 migrated off three private copies of the same helper onto the shared one.
- "Word chat had no audit trail - now audits with surface
word" → that work is #294's (commita1eaab4b;surface: "word"is already present at this PR's base). This PR only adds thewordlabel to the History page's surface filter. ThewordChat.tsaudit lines in this diff are re-indentation underasyncRoute, not new code. - "Ollama abort threw the wrong error shape; OpenAI/Ollama stream readers leaked sockets" → both targets were deleted by the Vercel AI SDK migration (#368), which is already an ancestor of this base. Neither fix is in this diff.
- "
useSelectedModelallowlist now covers settings-tier model IDs" → already on the base (isAllowedModelId); not in this range. - "A failed profile fetch fabricated a fake profile (999999 credits)" → still present at
UserProfileContext.tsx:203and untouched here. What this PR actually changed in that file is memoizing the context value. - "Storage cleanup ×3 consolidated" → deliberately not done; all three blocks are byte-identical before and after, because merging them would reintroduce the leak #294 fixed. They already route through
enqueueStorageCleanup. - "Word add-in model catalog drift (3 models missing)", "anchor registry deleted", "byte-identical
mike-iconfork replaced with an@mike/*alias" → none of the three are in this range; those files are byte-identical at both refs (the alias already existed). - "CI: dead
landing/drift-check path removed" → nolandingreference exists in.github/at either ref. - "~600 lines of dead code incl. two whole files" → roughly 280-300 lines, across three deleted files.
- "Ratchet floors raised 23/17/23/23 → 35/29/37/36" → the real move is 52/46/53/54 → 45/38/47/47, downward in number and upward in scope; see the tradeoff note.
- "Endpoint parity: 181 method+path routes" → 144 registrations / 174 mounted; both identical between base and head.
- "Net -904 lines (+2,350/-3,254 across 123 files)" → +2,857/-2,604 across 126 files, net +253. It changed because the base was retargeted onto #295 (which took
b82bbbedand the wholesaleroutes/*deletions with it) and because the tip gainedb32e32d5.
🤖 Generated with Claude Code
Our analysis
Harden error handling and shared application infrastructure — read the full analysis →
Think the analysis missed something the PR description covers?
Capture this PR into my fork
Download a Markdown prompt that tells Claude how to port every
commit in this PR into your working tree. Run it via
claude -p < capture-pull-356.md from
inside the repo you want the changes in.