Skip to content

Deep cleanup: rate-limit message regression, correctness fixes, and dependency audit - #32

Merged
jakeh280 merged 4 commits into
mainfrom
chore/deep-cleanup
Sep 8, 2026
Merged

jakeh280 merged 4 commits into
mainfrom
chore/deep-cleanup

Conversation

@jakeh280

@jakeh280 jakeh280 commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Summary

Second pass after #31, per an explicit "fix everything you can find" mandate. Four independent fixes, each verified individually:

  1. Resolve npm audit high-severity advisory in browserslist — dependency bump to clear the flagged advisory (the one GitHub's push output just called out again on main).
  2. Fix a regression in the earlier 429-message fix — PR Fix AUDIT.md findings F1-F8 and all followups #31 hardcoded a generic rate-limit message for all 429s, based on lib/rateLimit.ts alone. It turns out proxy.ts is a separate rate limiter (its own in-memory store, runs earlier in the request path) that already produces a more useful message ("Rate limit reached. Try again in N minutes."). That real message was being discarded and replaced with the generic one. Now the actual parsed body message is surfaced, with the generic message only as a fallback when none is present. This bug was introduced by me earlier today, from reasoning about only one of the two rate limiters without knowing the second existed.
  3. Fix 4 correctness bugs found by an independent review, plus cleanup — from a full /code-review pass (2 parallel subagents, correctness + cleanup angles) run against app/ and lib/ beyond AUDIT.md's original scope:
    • outputParsing.ts: a strict/relaxed heading-detection tie-break dropped sections when sibling (non-nested) sections used different heading levels. Replaced the whole dual-pass approach with one unified /^#{2,3}\s+/m regex — a real simplification, not a patch.
    • historyStorage.ts: writeHistory([]) (deleting the last history item) fell through to a "leave storage untouched" fallback and read back the deleted item, silently undoing the deletion. Added an explicit empty-list case.
    • page.tsx: ClipsBentoCard's preamble paragraph rendered twice when the model didn't emit "Option N" headers.
    • page.tsx: handleFiles's loading-status guard discarded successfully-read file content entirely instead of just skipping the status transition.
  4. Fix AGENTS.md references to outputParsing.ts functions removed in fix chore: dependency updates, clip duration prompt fix, cleanup #3 — countCanonicalSections() / hasHeadedSection() no longer exist; docs now describe the unified regex approach.

Verification

  • All 100 unit tests pass (npm test), up from 74 at the start of this work.
  • Lint clean (npm run lint).
  • F2 (hour-preserving timestamps, from PR Fix AUDIT.md findings F1-F8 and all followups #31) got live-model proof this session: a real 80-minute synthetic transcript run against Gemini correctly returned 1:00:00 / 1:10:00 chapter timestamps past the hour mark, confirming the fix without a mock.
  • Findings 1/3/4 above reproduced as failing tests first, then fixed, then reverified green.

Deliberately left alone

  • The two independent rate limiters (proxy.ts and lib/rateLimit.ts) still keep separate state. Consolidating them is a real architectural fix but is a rearchitecture, not a bug fix — out of scope here.
  • Minor duplicated prompt text in systemPrompt.ts — editing production prompt wording needs a live-quality A/B comparison, not a mechanical edit.
  • Two flagged efficiency items (per-chunk reparsing) — measured sub-millisecond at real response sizes; making parsing incremental would risk the correctness fixes above for no measurable gain.

🤖 Generated with Claude Code

jakeh280 and others added 4 commits September 7, 2026 19:45
Pre-existing, unrelated to AUDIT.md's scope: a transitive build-tool
dependency (pulled in via Tailwind/PostCSS tooling), flagged by GitHub's
Dependabot alert on the repo. npm audit fix bumped it and its own
transitive deps (update-browserslist-db, node-releases,
electron-to-chromium, caniuse-lite, baseline-browser-mapping) - lockfile
only, no direct dependency version changed. npm audit now reports zero
vulnerabilities. Lint, build, and full test suite still pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… real rate-limit message

Found while checking whether AGENTS.md's parsing/rate-limit descriptions
still matched the code after this session's earlier fixes: there are two
independent rate limiters in front of app/api/chat/route.ts, not one.
proxy.ts (Next.js middleware, runs first) computes and returns a real
"Rate limit reached. Try again in N minutes." from its own window state.
lib/rateLimit.ts's in-route check (a second, separate limiter) only ever
returns the generic "Too many requests. Please try again later."

The earlier followup-2 fix only ever looked at the second one and
concluded the server's 429 body was never worth showing, so it hardcoded
AI_LIMIT_NOTICE unconditionally - discarding proxy.ts's actual reset-time
message for what is the more common rate-limit path in practice (it runs
first). aiLimitError() now takes the parsed body's message when there is
one, falling back to AI_LIMIT_NOTICE only when the body doesn't parse or
carries no message - not for res.statusText, which is just a generic HTTP
reason phrase and would be a worse fallback than the constant.

Verified live: confirmed proxy.ts's actual 429 body via 21 real requests
against the dev server ("Rate limit reached. Try again in 60 minutes."),
then confirmed via mocked fetch that the UI now shows a server message
verbatim when present and falls back to AI_LIMIT_NOTICE on a malformed body.

Also updates AGENTS.md's Output Parsing section, which had gone stale
against this session's F4/F7/F8 changes: the "## fallback only when no ###
at all" description, the outputHealth issue list missing incomplete-clips,
and the F6 "strictly between" duration wording.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A dedicated correctness-review pass (separate from and after this session's
AUDIT.md work) found 4 real, empirically-reproduced bugs in code this
session had already touched or was adjacent to:

1. writeHistory([]) - deleting the last remaining history item - fell into
   the "every write attempt failed, leave storage alone" fallback (a
   zero-length list makes the shrink loop run zero times) and read back the
   item that was just supposed to be deleted, silently undoing the
   deletion. Now special-cased: an empty target list clears storage.

2. parseBentoSections' heading-level split had a tie-breaking gap: it only
   switched from "###" to "##" when the "##" pass recovered *more* canonical
   sections. A model using "###" for two real sections and "##" for the
   other two (sibling sections at different levels, not one nested inside
   the other) tied 2-vs-2, and the tie-break kept only one pair - the
   other pair's content was swallowed into the section before it. Replaced
   the whole strict/relaxed dual-pass with a single split on "#{2,3}\s+",
   which recovers all four regardless of which level the model used per
   section - simpler than the two-pass comparison it replaces, not just a
   patch on top of it.

3. ClipsBentoCard rendered the Clips section's preamble text twice when the
   model didn't emit recognizable "Option N" headers: once as its own styled
   paragraph (unconditional whenever non-empty) and again inside the full
   raw-body markdown fallback that renders whenever there's nothing
   structured to show in the grid. Preamble now only renders in the
   structured (grid) branch, where the fallback isn't also showing it.

4. handleFiles' "don't let a slow file read override an active generation"
   guard (added while fixing followup 1) discarded the file's content
   entirely rather than just skipping the status transition - a file
   dropped just before switching to Direct Paste and clicking Generate was
   silently lost, with no error and no way to tell anything went wrong.
   Now captures (or clears, on a validation failure) the file data
   unconditionally and only guards the setStatus() calls specifically.

Also, from the same review's cleanup pass (bar: a nameable win, not a style
preference - skipped a few candidates that didn't clear it, including
extracting shared chapter-naming prompt text, which would mean editing
production prompt wording without a live model check):

- HISTORY_LIMIT was defined once in each of lib/history.ts (a bare literal
  `10`) and lib/historyStorage.ts (the actual constant) - moved to
  history.ts (avoids a circular import the other way) and re-exported.
- Extracted useCopyToClipboard(), shared by BentoCard and TitlesBentoCard,
  which had byte-for-byte identical copy-to-clipboard state and handlers.
- Extracted resetResultState() for the "clear per-generation UI state"
  cluster (errorMessage/limitNotice/outputIssues/historySaveFailed/isDemo)
  that runWithText, analyzeAnotherSermon, viewDemo, and loadFromHistory each
  set individually - the exact pattern that made adding outputIssues and
  historySaveFailed, earlier this session, three separate edits each.
- app/api/chat/route.ts's eight hand-copied `new Response(JSON.stringify(...))`
  error blocks became one `jsonError(status, message)` helper.

Verified: full test suite (100 tests, up from 97) plus live browser checks
for each of the four bugs (a real localStorage delete-to-empty, a mocked
Clips section with no Option headers, a controllable-delay FileReader
racing a mocked generation) and a full live smoke test of every reset path
(view demo, reset, load from history) after the resetResultState extraction.
Lint and build clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…on removed

countCanonicalSections() no longer exists after the strict/relaxed dual-pass
was replaced with a single unified heading split; updated the Output Parsing
section to describe the actual current mechanism (a single /^#{2,3}\s+/m
split) and why, rather than the two-pass comparison it replaced.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
sermon-intelligence Ready Ready Preview Sep 8, 2026 12:22am UTC

@jakeh280
jakeh280 merged commit b2c6d84 into main Sep 8, 2026
3 checks passed
@jakeh280
jakeh280 deleted the chore/deep-cleanup branch September 8, 2026 00:23
jakeh280 added a commit that referenced this pull request Sep 8, 2026
… docs and merged branches (#33)

Rate limiting: proxy.ts and lib/rateLimit.ts were two independent, unsynchronized
rate limiters guarding the same endpoint - proxy.ts (a fixed window, run as real
Next 16 middleware since the framework upgrade silently activated it) and
lib/rateLimit.ts (a sliding window, called directly from the API route). They
weren't equivalent: only the sliding window closes the gap a fixed window has,
where a burst of MAX_REQUESTS right before the hourly reset plus another right
after can let ~2x the limit through in a short span straddling the boundary.

Consolidating to one limiter means giving the survivor the *stricter*
algorithm, not just picking one arbitrarily - otherwise removing the "redundant"
check would have quietly doubled the worst-case burst instead of just deleting
dead weight. Extracted the sliding-window decision logic into a new pure
module, lib/proxyRateLimit.ts (checkAndRecord/getClientIp), kept separate from
proxy.ts's NextRequest/NextResponse glue so it's unit-testable without Next's
runtime - `next/server` isn't resolvable under plain Node, which is why this
logic used to run untested. Deleted lib/rateLimit.ts and its now-fully-redundant
call site in app/api/chat/route.ts. Verified via `next build` (both before and
after) that proxy.ts really does compile to real middleware
(`ƒ Proxy (Middleware)`, confirmed nodejs runtime in functions-config-manifest.json),
and added tests/proxyRateLimit.test.ts (8 tests, including one that specifically
pins the sliding-window boundary: aging out one hit opens exactly one slot, not
a fresh batch).

Docs: AGENTS.md's Rate Limiting section and file tree were stale on multiple
fronts - "not a Next.js middleware" (true when written, false since the Next 16
upgrade), a missing lib/proxyRateLimit.ts entry, and several pre-existing gaps
(boundedBody.ts, demoContent.ts, and three test files never added to the tree).
All corrected. Also dropped dangling "AUDIT.md F#" citations from test/lib
comments now that the doc is gone, rephrasing each to describe the bug directly.

Housekeeping: deleted AUDIT.md - everything in it is fixed, merged, and now
documented in AGENTS.md and the PR #31/#32 descriptions, so the standalone
doc no longer earns its keep. Deleted three remote branches
(agent/sermon-cross-promotion, claude/admiring-bassi, claude/lucid-cray) that
were fully merged into main (zero unique commits) and left over from other
sessions.

Verification: 108/108 tests pass (up from 100), lint clean, `next build`
succeeds with the middleware chunk intact, two independent review passes (one
of which caught the fixed-vs-sliding-window regression this commit fixes).

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — 1afd527e Deployed Sep 8, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant