Skip to content

CodeRabbit full-repo review (do not merge)#2

Closed
LEANDERANTONY wants to merge 326 commits into
coderabbit-rootfrom
main
Closed

CodeRabbit full-repo review (do not merge)#2
LEANDERANTONY wants to merge 326 commits into
coderabbit-rootfrom
main

Conversation

@LEANDERANTONY

Copy link
Copy Markdown
Owner

@coderabbitai full review

Removed local setup instructions and testing section from README.
LEANDERANTONY and others added 6 commits May 15, 2026 00:46
… event routing

backend/webhooks/lemonsqueezy.py verifies HMAC-SHA256 of raw body against
AIJOBAGENT_LEMONSQUEEZY_WEBHOOK_SECRET, parses the LS event envelope, and routes
each of 10 subscription events to a state transition. Returns 401 on signature
failure, 200 with {status: duplicate} on idempotent replays, 200 on success.

backend/routers/billing.py mounts the webhook + a customer-portal helper endpoint
(POST /api/billing/portal). backend/app.py mounts the router under /api.

Idempotency: subscription_webhook_log table keyed by meta.webhook_id. The
upsert against subscriptions is also keyed on user_id, so even if the log
lookup fails-open, repeated events produce the same row.

Event-to-status mapping:
  created/resumed/payment_recovered/payment_success -> active
  updated                                            -> mirrors payload status
  cancelled                                          -> cancelled (kept paid until ends_at)
  payment_failed                                     -> past_due
  expired                                            -> expired (downgrades to Free)
  paused/unpaused                                    -> paused/active

Divergence from brief: status=cancelled (not active) on subscription_cancelled
so the resolver can render distinct UI ('renewing' vs 'ends X'). The tier
resolver still treats cancelled-with-active-period as paid, matching brief
intent.

+28 tests (signature verification, every event type, idempotency, end-to-end
route). Suite: 476 -> 504.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
frontend/src/lib/api.ts adds getCheckoutUrl(tier), isLemonSqueezyEnabled(),
and getCustomerPortalUrl(). When NEXT_PUBLIC_LEMONSQUEEZY_* env vars are
empty, isLemonSqueezyEnabled() returns false so the pricing CTAs render as
'Coming soon' fallback — lets this branch ship to main before LS KYC clears
or live variants exist.

frontend/src/components/landing-page.tsx PRICING_TIERS CTAs route Pro/Business
buttons to the LS hosted checkout URL with:
  ?checkout[custom][user_id]=<supabase-uid>
  &checkout[success_url]=<workspace>?ls_checkout=success
The custom_data binding lands in the webhook payload so the backend can
resolve which user just subscribed without trusting the LS-side email.

frontend/src/components/workspace/WorkspaceShell.tsx adds a 'Manage subscription'
button in the account popover (only visible when quotaSnapshot.tier !== 'free')
that POSTs to /api/billing/portal and window.location's to the one-time
LS customer portal URL. Also wires a post-checkout effect that detects
?ls_checkout=success in the URL on workspace mount and refreshes quota so
the tier badge updates immediately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
.env.example documents 7 new env vars (5 backend AIJOBAGENT_LEMONSQUEEZY_*
secrets + 2 NEXT_PUBLIC_LEMONSQUEEZY_* public values for the frontend bundle).
All default to empty; backend webhook returns 503 + frontend disables the
CTA with 'Coming soon' when unset, so a deploy without LS configured is safe.

docs/lemon-squeezy.md covers:
  - Architecture diagram (table + helper + webhook + frontend wiring)
  - Event-to-status mapping table (all 10 LS subscription events)
  - Setup walkthrough for sandbox + live mode
  - Webhook URL to register, signature secret rotation
  - Customer portal URL contract

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Day 42 covers the 8-step tier enforcement series end-to-end: tier shim
+ TIER_CAPS, atomic quota with refund-on-failure, gates at
/workspace/analyze + /workspace/assistant (sync+stream) + /workspace/parse-resume
+ resume builder + /jobs/search + saved_jobs + saved_workspaces,
tier-aware model selection (premium routes review/resume_gen/cover to
gpt-5.5), /workspace/quota endpoint + frontend Premium toggle, and
the tier-aware saved_workspaces retention sweeper. 99 new tier-
enforcement tests (451 -> 504 then 504 -> 511 with the lint fix).
Deployed to api.job-application-copilot.xyz.

Day 43 covers the Lemon Squeezy scaffold on feat/lemonsqueezy-integration:
subscriptions + webhook_log tables, 60s LRU cache on
get_active_subscription, HMAC-verified webhook with full event-to-status
mapping, frontend Pro CTA wiring with ?checkout[custom][user_id]
binding, env-gated 'Coming soon' fallback so the branch can merge
to main before LS KYC clears. 51 new tests (501 -> 504+ before the
docs commits).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four ADRs covering the load-bearing architectural decisions from the
recent tier enforcement + payments work:

ADR-020 — resolve_user_tier(app_user) as the single shim every gate
routes through. Mirror of HelpmateAI's ADR-015 with AI Job Agent's
naming + TIER_CAPS shape. Payment swap is a one-function change.

ADR-021 — atomic SQL function (increment_aijobagent_counter) with
embedded cap check raising P0001 on overrun; Python wrapper translates
to QuotaExceededError -> global 429 handler. Diverges from HelpmateAI's
pre-check + post-increment pattern (atomic is safer under concurrent
workspace runs from the same user). Refund-on-failure for the LLM-cost
counters (tailored_applications, premium_applications, assistant_turns,
etc.); no refund for cheap counters like job_searches.

ADR-022 — tier-aware model selection via constructor injection
(model_overrides dict at ApplicationOrchestrator construction time)
rather than per-call model_override. Keeps the per-agent retry +
deterministic fallback paths tier-correct without extra plumbing.
Premium routes review/resume_gen/cover_letter to gpt-5.5; tailoring
stays mini (COGS pinning).

ADR-023 — Lemon Squeezy as Merchant of Record for v1. Same rationale
as HelpmateAI's ADR-017: solo dev unregistered in India, global reach,
tax compliance off-loading. processor-neutral subscriptions schema
reserves the Stripe + Razorpay slots for a post-incorporation migration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a 'Tier enforcement' row to the feature matrix covering the gates,
the premium GPT-5.5 routing, atomic quota + refund-on-failure, and the
retention sweeper. Bumps the ADR count from 19 to 23 with the four new
ADRs (020 tier shim, 021 atomic quota, 022 tier-aware model selection,
023 Lemon Squeezy MoR) called out by number.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented May 14, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
job-application-copilot Ready Ready Preview, Comment May 15, 2026 7:45am

@coderabbitai

coderabbitai Bot commented May 14, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 275 files, which is 125 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ae88cbb1-747e-4db4-9a63-20c642b158fa

📥 Commits

Reviewing files that changed from the base of the PR and between 17663f6 and 1032838.

⛔ Files ignored due to path filters (25)
  • docs/images/classic_resume_render.jpg is excluded by !**/*.jpg
  • docs/images/cover_letter_render.jpg is excluded by !**/*.jpg
  • docs/images/job-agent-architecture.svg is excluded by !**/*.svg
  • docs/images/job-copilot-logo.png is excluded by !**/*.png
  • docs/images/landing_jd.png is excluded by !**/*.png
  • docs/images/landing_jobsearch.png is excluded by !**/*.png
  • docs/images/landing_resumebuilder.png is excluded by !**/*.png
  • docs/images/modern_resume_render.jpg is excluded by !**/*.jpg
  • docs/static/demo_job_description/Sample_Job_Description_DataAnalyst.docx is excluded by !**/*.docx
  • docs/static/demo_job_description/Sample_Job_Description_MLEngineer.pdf is excluded by !**/*.pdf
  • docs/static/demo_resume/Black White Beige Simple Modern Tech Resume.pdf is excluded by !**/*.pdf
  • docs/static/demo_resume/Black and White Modern Professional Resume.pdf is excluded by !**/*.pdf
  • docs/static/demo_resume/Blue Geometric Lines Professional UX Design Tech Resume.pdf is excluded by !**/*.pdf
  • docs/static/demo_resume/Blue and White Professional Resume.pdf is excluded by !**/*.pdf
  • docs/static/demo_resume/Leander_Antony_Enhanced_Resume_updated.docx is excluded by !**/*.docx
  • docs/static/demo_resume/Leander_Antony_Resume_International.docx is excluded by !**/*.docx
  • docs/static/demo_resume/Purple and White Clean and Professional Resume.pdf is excluded by !**/*.pdf
  • docs/static/pdf_rendered/classic_resume_render.pdf is excluded by !**/*.pdf
  • docs/static/pdf_rendered/cover_letter_render.pdf is excluded by !**/*.pdf
  • frontend/package-lock.json is excluded by !**/package-lock.json
  • frontend/public/brand/job-copilot-logo.png is excluded by !**/*.png
  • frontend/public/landing/hero-workspace.png is excluded by !**/*.png
  • frontend/src/app/apple-icon.png is excluded by !**/*.png
  • frontend/src/app/favicon.ico is excluded by !**/*.ico
  • frontend/src/app/icon.png is excluded by !**/*.png
📒 Files selected for processing (275)
  • .coderabbit.yaml
  • .dockerignore
  • .env.example
  • .github/workflows/ci.yml
  • .github/workflows/deploy.yml
  • .gitignore
  • .python-version
  • DEVLOG.md
  • Dockerfile
  • README.md
  • backend/__init__.py
  • backend/app.py
  • backend/auth_models.py
  • backend/config.py
  • backend/maintenance.py
  • backend/model_routing.py
  • backend/models.py
  • backend/nightly_eval.py
  • backend/quota.py
  • backend/rate_limit.py
  • backend/request_auth.py
  • backend/routers/__init__.py
  • backend/routers/auth.py
  • backend/routers/billing.py
  • backend/routers/health.py
  • backend/routers/jobs.py
  • backend/routers/workspace.py
  • backend/run_traces.py
  • backend/services/__init__.py
  • backend/services/artifact_export_service.py
  • backend/services/auth_cookies.py
  • backend/services/auth_handoff_service.py
  • backend/services/auth_session_service.py
  • backend/services/job_cache_service.py
  • backend/services/job_search_service.py
  • backend/services/resume_builder_persistence_service.py
  • backend/services/resume_builder_service.py
  • backend/services/saved_jobs_service.py
  • backend/services/workspace_persistence_service.py
  • backend/services/workspace_quota_service.py
  • backend/services/workspace_run_jobs.py
  • backend/services/workspace_service.py
  • backend/subscriptions.py
  • backend/tiers.py
  • backend/vps/.env.example
  • backend/vps/Caddyfile
  • backend/vps/docker-compose.override.yml
  • backend/vps/docker-compose.yml
  • backend/webhooks/__init__.py
  • backend/webhooks/lemonsqueezy.py
  • backend/workspace_models.py
  • docs/.gitkeep
  • docs/adr/ADR-001-lightweight-document-parsing.md
  • docs/adr/ADR-002-demo-assets-for-reproducible-product-flows.md
  • docs/adr/ADR-003-streamlit-session-state-for-navigation-and-persistence.md
  • docs/adr/ADR-004-linkedin-data-export-ingestion-instead-of-direct-api-access.md
  • docs/adr/ADR-005-streamlit-first-backend-ready-delivery.md
  • docs/adr/ADR-006-playwright-first-pdf-export.md
  • docs/adr/ADR-007-remove-linkedin-import-from-active-product-scope.md
  • docs/adr/ADR-008-two-mode-grounded-assistant-panel.md
  • docs/adr/ADR-009-google-sign-in-via-supabase-for-persistent-identity.md
  • docs/adr/ADR-010-single-pass-review-corrections-and-task-tuned-model-budgets.md
  • docs/adr/ADR-011-unified-grounded-assistant-surface.md
  • docs/adr/ADR-012-nextjs-workspace-and-fastapi-runtime-baseline.md
  • docs/adr/ADR-013-cached-jobs-cache-layer-with-scheduled-refresh.md
  • docs/adr/ADR-014-postgres-rpc-for-ranked-search.md
  • docs/adr/ADR-015-docx-first-artifact-export-with-theme-palette.md
  • docs/adr/ADR-016-conversational-llm-resume-builder.md
  • docs/adr/ADR-017-workspace-assistant-state-aware-context.md
  • docs/adr/ADR-018-three-layer-llm-retry-and-per-agent-fallback-isolation.md
  • docs/adr/ADR-019-independent-step-navigation.md
  • docs/adr/ADR-020-tier-resolution-via-single-shim-function.md
  • docs/adr/ADR-021-atomic-quota-with-refund-on-failure.md
  • docs/adr/ADR-022-tier-aware-model-selection-via-constructor-injection.md
  • docs/adr/ADR-023-lemon-squeezy-merchant-of-record-for-v1.md
  • docs/adr/README.md
  • docs/architecture.md
  • docs/lemon-squeezy.md
  • docs/operations.md
  • docs/sql/job_cache_cron_setup.sql
  • docs/sql/supabase-bootstrap.sql
  • docs/sql/supabase-quota-counters.sql
  • docs/sql/supabase-resume-builder.sql
  • docs/sql/supabase-run-traces.sql
  • docs/sql/supabase-saved-jobs.sql
  • docs/sql/supabase-subscriptions.sql
  • docs/static/.gitkeep
  • docs/static/demo_job_description/Job_Title_Data_Analyst.txt
  • docs/static/demo_job_description/Sample_Job_Description_DataScientist.txt
  • docs/static/demo_resume/.gitkeep
  • frontend/.env.example
  • frontend/.eslintrc.json
  • frontend/.gitignore
  • frontend/README.md
  • frontend/next-env.d.ts
  • frontend/next.config.ts
  • frontend/package.json
  • frontend/src/app/globals.css
  • frontend/src/app/layout.tsx
  • frontend/src/app/page.tsx
  • frontend/src/app/privacy/page.tsx
  • frontend/src/app/workspace/page.tsx
  • frontend/src/components/BrandLogo.tsx
  • frontend/src/components/landing-page.tsx
  • frontend/src/components/workspace/AnalysisRunner.tsx
  • frontend/src/components/workspace/ArtifactViewer.tsx
  • frontend/src/components/workspace/AssistantPanel.tsx
  • frontend/src/components/workspace/CollapsibleSection.tsx
  • frontend/src/components/workspace/CommandPalette.tsx
  • frontend/src/components/workspace/JDReview.tsx
  • frontend/src/components/workspace/JobSearch.tsx
  • frontend/src/components/workspace/ResumeIntake.tsx
  • frontend/src/components/workspace/Sidebar.tsx
  • frontend/src/components/workspace/WorkspaceShell.tsx
  • frontend/src/components/workspace/icons.tsx
  • frontend/src/hooks/useAnalysisJob.ts
  • frontend/src/hooks/useArtifactExport.ts
  • frontend/src/hooks/useAssistantHistory.ts
  • frontend/src/hooks/useSavedJobs.ts
  • frontend/src/hooks/useWorkspaceQuota.ts
  • frontend/src/hooks/useWorkspaceSession.ts
  • frontend/src/lib/api-types.ts
  • frontend/src/lib/api.ts
  • frontend/src/lib/auth-session.ts
  • frontend/src/lib/humanizeApiError.ts
  • frontend/src/lib/job-workspace.ts
  • frontend/src/middleware.ts
  • frontend/tsconfig.json
  • frontend/vercel.json
  • pyproject.toml
  • src/__init__.py
  • src/agents/__init__.py
  • src/agents/common.py
  • src/agents/cover_letter_agent.py
  • src/agents/orchestrator.py
  • src/agents/resume_generation_agent.py
  • src/agents/review_agent.py
  • src/agents/tailoring_agent.py
  • src/assistant_service.py
  • src/auth_service.py
  • src/cached_jobs_store.py
  • src/config.py
  • src/cover_letter_builder.py
  • src/errors.py
  • src/exporters.py
  • src/job_sources/__init__.py
  • src/job_sources/ashby.py
  • src/job_sources/base.py
  • src/job_sources/demo.py
  • src/job_sources/greenhouse.py
  • src/job_sources/lever.py
  • src/job_sources/matching.py
  • src/job_sources/registry.py
  • src/job_sources/workday.py
  • src/logging_utils.py
  • src/openai_service.py
  • src/parsers/__init__.py
  • src/parsers/common.py
  • src/parsers/jd.py
  • src/parsers/resume.py
  • src/product_knowledge.py
  • src/prompts.py
  • src/quota_service.py
  • src/resume_builder.py
  • src/resume_builder_store.py
  • src/resume_diff.py
  • src/saved_jobs_store.py
  • src/saved_workspace_store.py
  • src/schemas.py
  • src/schemas_llm_outputs.py
  • src/services/__init__.py
  • src/services/fit_service.py
  • src/services/jd_llm_parser_service.py
  • src/services/jd_summary_service.py
  • src/services/job_service.py
  • src/services/profile_service.py
  • src/services/resume_llm_parser_service.py
  • src/services/tailoring_service.py
  • src/taxonomy.py
  • src/usage_store.py
  • src/user_store.py
  • src/utils.py
  • src/workflow_payloads.py
  • tests/backend/test_assistant_quota_enforcement.py
  • tests/backend/test_cost_tracking.py
  • tests/backend/test_lemonsqueezy_webhook.py
  • tests/backend/test_nightly_eval.py
  • tests/backend/test_quota.py
  • tests/backend/test_resume_quota_enforcement.py
  • tests/backend/test_search_and_saved_quota_enforcement.py
  • tests/backend/test_structured_outputs.py
  • tests/backend/test_subscriptions.py
  • tests/backend/test_tier_aware_workflow_model.py
  • tests/backend/test_tiers.py
  • tests/backend/test_workspace_quota_enforcement.py
  • tests/backend/test_workspace_quota_snapshot.py
  • tests/backend/test_workspace_retention.py
  • tests/conftest.py
  • tests/quality/.gitignore
  • tests/quality/LATENCY-RESULTS.md
  • tests/quality/_fetch_jds.py
  • tests/quality/assistant_quality_runner.py
  • tests/quality/cover_letter_quality_runner.py
  • tests/quality/eval_resume_parser.py
  • tests/quality/expected_jds/01-narvar-senior-ai-engineer.json
  • tests/quality/expected_jds/02-wayve-embedded-cpp.json
  • tests/quality/expected_jds/03-datadog-ai-research-paris.json
  • tests/quality/expected_jds/04-moloco-data-scientist.json
  • tests/quality/expected_jds/05-figma-enterprise-ae.json
  • tests/quality/expected_jds/06-glean-ai-outcomes-manager.json
  • tests/quality/expected_jds/07-placer-big-data-engineer.json
  • tests/quality/expected_jds/08-synthetic-clean.json
  • tests/quality/expected_jds/09-synthetic-preamble-title.json
  • tests/quality/expected_jds/10-synthetic-niche-skills.json
  • tests/quality/expected_jds/11-synthetic-allcaps-traditional.json
  • tests/quality/expected_jds/12-synthetic-bullet-heavy.json
  • tests/quality/expected_jds/13-synthetic-international.json
  • tests/quality/expected_jds/14-synthetic-narrative-prose.json
  • tests/quality/expected_jds/15-synthetic-minimal.json
  • tests/quality/expected_profiles/01-student-projects.json
  • tests/quality/expected_profiles/02-midcareer-tech.json
  • tests/quality/expected_profiles/03-academic-cv.json
  • tests/quality/expected_profiles/04-bootcamp-grad.json
  • tests/quality/expected_profiles/05-career-switcher.json
  • tests/quality/expected_profiles/06-minimal.json
  • tests/quality/expected_profiles/07-allcaps-traditional.json
  • tests/quality/expected_profiles/08-pdf-extracted-messy.json
  • tests/quality/expected_profiles/09-international-name.json
  • tests/quality/expected_profiles/10-early-career-internships.json
  • tests/quality/expected_profiles/11-senior-detailed.json
  • tests/quality/expected_profiles/12-government-military.json
  • tests/quality/expected_profiles/13-healthcare-rn.json
  • tests/quality/expected_profiles/14-finance-mba.json
  • tests/quality/expected_profiles/15-creative-designer.json
  • tests/quality/jd_parser_quality_runner.py
  • tests/quality/latency_runner.py
  • tests/quality/orchestrator_e2e_runner.py
  • tests/quality/parser_quality_runner.py
  • tests/quality/perf_assistant_stream.py
  • tests/quality/renderer_fidelity_runner.py
  • tests/quality/resume_builder_quality_runner.py
  • tests/quality/resume_builder_structuring_quality_runner.py
  • tests/quality/resume_generation_quality_runner.py
  • tests/quality/review_quality_runner.py
  • tests/quality/sample_jds/01-narvar-senior-ai-engineer.txt
  • tests/quality/sample_jds/02-wayve-embedded-cpp.txt
  • tests/quality/sample_jds/03-datadog-ai-research-paris.txt
  • tests/quality/sample_jds/04-moloco-data-scientist.txt
  • tests/quality/sample_jds/05-figma-enterprise-ae.txt
  • tests/quality/sample_jds/06-glean-ai-outcomes-manager.txt
  • tests/quality/sample_jds/07-placer-big-data-engineer.txt
  • tests/quality/sample_jds/08-synthetic-clean.txt
  • tests/quality/sample_jds/09-synthetic-preamble-title.txt
  • tests/quality/sample_jds/10-synthetic-niche-skills.txt
  • tests/quality/sample_jds/11-synthetic-allcaps-traditional.txt
  • tests/quality/sample_jds/12-synthetic-bullet-heavy.txt
  • tests/quality/sample_jds/13-synthetic-international.txt
  • tests/quality/sample_jds/14-synthetic-narrative-prose.txt
  • tests/quality/sample_jds/15-synthetic-minimal.txt
  • tests/quality/sample_resumes/01-student-projects.txt
  • tests/quality/sample_resumes/02-midcareer-tech.txt
  • tests/quality/sample_resumes/03-academic-cv.txt
  • tests/quality/sample_resumes/04-bootcamp-grad.txt
  • tests/quality/sample_resumes/05-career-switcher.txt
  • tests/quality/sample_resumes/06-minimal.txt
  • tests/quality/sample_resumes/07-allcaps-traditional.txt
  • tests/quality/sample_resumes/08-pdf-extracted-messy.txt
  • tests/quality/sample_resumes/09-international-name.txt
  • tests/quality/sample_resumes/10-early-career-internships.txt
  • tests/quality/sample_resumes/11-senior-detailed.txt
  • tests/quality/sample_resumes/12-government-military.txt
  • tests/quality/sample_resumes/13-healthcare-rn.txt
  • tests/quality/sample_resumes/14-finance-mba.txt
  • tests/quality/sample_resumes/15-creative-designer.txt
  • tests/quality/skill_canonicalization_runner.py

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch main

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8dacf9c0cc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread backend/services/auth_cookies.py Outdated
Comment on lines +28 to +29
_ACCESS_TOKEN_MAX_AGE_SECONDS = 60 * 60 * 24 * 7 # 7 days
_REFRESH_TOKEN_MAX_AGE_SECONDS = 60 * 60 * 24 * 30 # 30 days

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Align auth cookie lifetimes to avoid forced weekly logout

The access token cookie expires after 7 days while the refresh token cookie lasts 30 days, but session restore requires both tokens (resolve_authenticated_context rejects when either is missing). In practice, a user who returns after 7+ days of inactivity is logged out even though their refresh token is still valid, which breaks the intended "stay signed in" behavior and causes avoidable re-authentication churn.

Useful? React with 👍 / 👎.

Comment on lines +148 to +149
if existing_count >= cap:
raise QuotaExceededError(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Allow overwrite when saved workspace slot already exists

This cap check rejects saves when existing_count >= cap, which blocks Free users (cap=1) from updating their existing workspace even though storage is an upsert-by-user and comments describe overwrite as supported. The frontend autosave path calls /workspace/save directly (without pre-delete), so once a Free user has one saved workspace, every subsequent save fails with a quota error and the snapshot can no longer refresh.

Useful? React with 👍 / 👎.

Comment thread src/auth_service.py Outdated
Comment on lines +256 to +260
if not code_verifier and not auth_flow:
latest_flow = _consume_latest_pkce_flow()
if latest_flow:
auth_flow = latest_flow.get("auth_flow")
code_verifier = latest_flow.get("code_verifier")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove global latest-flow fallback in PKCE exchange

When auth_flow is missing, the exchange path consumes the most recently created PKCE flow from a global store. Because that store is shared across users, one malformed or malicious /auth/google/exchange request can steal another user's pending verifier, causing their legitimate login callback to fail. This cross-request coupling makes sign-in reliability depend on unrelated traffic and enables easy denial of sign-in attempts.

Useful? React with 👍 / 👎.

The saved_workspaces quota gate was rejecting every re-save from a
Free user (cap=1) because it checked `existing_count >= cap` without
distinguishing UPDATES to an existing slot from creation of NEW slots.
Today's store upserts on user_id (one row per user), so any "existing
row" means the save is an update — not a new slot — and the cap
shouldn't apply.

Real-world impact: the frontend autosave path calls /workspace/save
directly without pre-delete. Once a Free user has one saved workspace,
every subsequent autosave got rejected with a 429, breaking the
snapshot-refresh UX for every Free user past their first save.

Fix splits the gate into two questions:
  * is_existing_slot_update — does the user already own the slot?
  * is_creating_new_slot — would this save add to the slot count?

The cap fires only on the second case. Today, with one-row-per-user
storage, the gate is effectively dormant (existing_count == 1 always
means update, existing_count == 0 with cap >= 1 always passes). It
will start firing correctly the moment we migrate to per-slot rows
(future PR with slugs/labels), without revisiting this code path.

test_2nd_saved_workspace_on_free_returns_429 was inverted to
test_free_re_save_upserts_existing_workspace and now pins the
correct UX: re-saving is allowed because it's an update, not a new
slot. test_saved_workspace_delete_then_save_works was updated to
drop the intermediate 429 assertion (no longer fires under the
fix) and instead pin that DELETE + save round-trips cleanly.

Found by ChatGPT Codex review on PR #2 (May 2026).

Full backend suite: 501 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The auth_cookies module set ACCESS_TOKEN_MAX_AGE to 7 days while
REFRESH_TOKEN_MAX_AGE was 30 days. The docstring on those constants
claims "the access token mirrors [refresh] so a quiet tab doesn't
lose only half its credentials" — but the actual values didn't
mirror.

Real-world failure mode:
  * Session restore (resolve_authenticated_context) requires BOTH
    cookies present.
  * A user returning after 7+ days (but within 30) has a valid
    refresh cookie but a purged access cookie.
  * Restore rejects the half-credentialed pair and forces a fresh
    OAuth re-login, defeating the "stay signed in" UX the 30-day
    refresh window was meant to deliver.

Fix matches the docstring's stated intent: both cookies live for
30 days. The COOKIE's max_age governs browser purge time, NOT the
JWT's validity — Supabase rotates the access JWT on every restore
using the still-valid refresh JWT, so the inside-the-cookie token
being temporally stale is fine and expected.

Codex P1 finding on PR #2 (May 2026). Auth-test suite (14 tests)
still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /auth/google/exchange path had a defensive fallback that
consumed the most-recently-created PKCE flow from the shared
_pkce_flows SQLite store when both `auth_flow` and `code_verifier`
were missing on the request. The intent was to recover legitimate
users who lost their cookie mid-flow.

The unintended consequence (Codex P2 finding on PR #2, May 2026):
the store is shared across users. A malformed or malicious
exchange request could "steal" another user's pending verifier:

  * User A starts sign-in -> a PKCE flow lands in the store
  * User B (or an attacker) hits /auth/google/exchange with no
    auth_flow + no cookie
  * Fallback consumes User A's flow
  * User A's legitimate Google callback then fails ("session
    expired") because their flow was deleted from the store
  * In the worst case (attacker controls a matching auth_code),
    the exchange completes with User A's verifier on the
    attacker's behalf

Cross-request coupling like this makes sign-in reliability depend
on unrelated traffic. Removed in favor of failing fast with the
clean "Google sign-in session expired" error. Legitimate users who
hit this case re-run sign-in (rare, easy recovery); the attack
surface goes from "any user can hijack any other user's sign-in
flow" to zero.

Test inverted: the previous
test_exchange_code_for_session_uses_latest_local_flow_store_when_auth_flow_is_missing
pinned the buggy behavior. New test
test_exchange_code_for_session_does_not_consume_other_users_pkce_flow_when_auth_flow_is_missing
pins the secure behavior: when the caller arrives without
auth_flow + code_verifier, exchange raises "session expired" and
the victim's pending PKCE flow stays in the store untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror of HelpmateAI commit 8016457 (Codex P1 finding on HelpmateAI's
PR #4). The LS scaffold for both apps was built from the same
template, so the same URL bug exists here too — even though Codex
didn't flag it on AI Job Agent's PR #2 (Codex's review surfaces
sample issues rather than every instance).

Old: https://<store>.lemonsqueezy.com/buy/<variant>
New: https://<store>.lemonsqueezy.com/checkout/buy/<variant>

Per the LS docs at docs.lemonsqueezy.com/help/checkout/hosted-checkouts,
the `/buy/` path is a non-checkout endpoint. Every Pro upgrade CTA
would have resolved there and failed silently the moment LS goes
live with real variant IDs. This fix lands BEFORE KYC clears so
the broken state never reaches paid customers.

No test changes — the helper is exercised in real LS sandbox testing
which happens post-merge once variant IDs exist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LEANDERANTONY and others added 6 commits May 15, 2026 03:41
Programmatic wrapper around 5 quality runners (resume_parser, jd_parser,
tailoring, review, orchestrator_e2e). Captures pass/fail per runner +
key metrics in a JSON summary. Exits non-zero on regression beyond
5pt threshold (configurable via --threshold).

docs/operations.md adds the VPS crontab line and alerting paths.

Catches Prisha-style silent model-version drift before customers see
malformed outputs in production.

13 new tests in tests/backend/test_nightly_eval.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
src/schemas_llm_outputs.py defines strict Pydantic models for every
agent + service that consumes a JSON-shape LLM response:
  * TailoringOutput, ReviewOutput, ResumeGenerationOutput, CoverLetterOutput
  * JDParserOutput, ResumeParserOutput, JDSummaryOutput
  * ResumeBuilderTurnOutput, ResumeBuilderStructuringOutput

All models use extra='forbid' so unknown fields raise instead of
silently passing through — defends against Prisha-style silent
model-version updates that add/remove/rename fields.

Located at src/schemas_llm_outputs.py rather than src/schemas/ as
a package because converting schemas.py to a package would break
30+ existing imports.

No test changes in this commit — the schemas are exercised by the
agent migration in the next commit + its tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
src/openai_service.py adds run_structured_prompt(...) — wraps OpenAI's
response_format={'type':'json_schema',...} feature so the model is
CONSTRAINED to match the Pydantic schema at generation time, not just
validated post-hoc. Supporting helpers _inline_refs() and
_enforce_strict_object_constraints() rewrite Pydantic schemas into
OpenAI's strict-mode JSON Schema format (additionalProperties: false,
all required fields explicit, no $ref indirection).

Migrated 6 high-value call sites onto run_structured_prompt:
  * src/agents/tailoring_agent.py
  * src/agents/review_agent.py
  * src/agents/resume_generation_agent.py
  * src/agents/cover_letter_agent.py
  * src/services/jd_summary_service.py
  * backend/services/resume_builder_service.py (structuring pass)

Each agent uses a hasattr-shim that falls back to the old
run_json_prompt + model_validate(...) path so existing test fakes
that only mock run_json_prompt keep working. Test files for the
three migrated agents (cover_letter, resume_generation, orchestrator)
get small updates to add run_structured_prompt onto the existing
fakes alongside run_json_prompt.

Deferred call sites (still use run_json_prompt + key check):
  * resume_builder intake turn
  * ResumeLLMParserService
  * JobDescriptionLLMParserService
  * assistant_service
All have schemas defined; only the call-site swap is deferred.

18 new tests in tests/backend/test_structured_outputs.py + small
fixture updates in test_cover_letter_agent / test_resume_generation_agent
/ test_orchestrator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
docs/sql/supabase-run-traces.sql creates the aijobagent_run_traces
table for per-LLM-call cost + token tracking. Schema:
  * trace_id (uuid PK, default gen_random_uuid())
  * user_id (FK auth.users, on delete cascade)
  * task_name (text — which agent / pipeline stage)
  * model_name (text — gpt-5.4-mini, gpt-5.5, etc.)
  * prompt_tokens, completion_tokens (int)
  * cost_usd (numeric 10,6)
  * success (boolean)
  * created_at (timestamptz)

RLS: service-role-only writes (mirrors quota_counters), user-can-read-own
via the policy on user_id = auth.uid().

backend/run_traces.py exposes record_trace(...) with an in-memory
fallback for tests. Supabase backend used when SUPABASE_URL +
SERVICE_ROLE_KEY are configured.

Migration NOT yet applied to Supabase — leave as SQL file for the
user to apply via MCP or SQL editor post-merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
src/openai_service.py adds:
  * _MODEL_PRICING_USD_PER_MILLION map for all 4 models
        gpt-5.4-nano: $0.10 in / $0.40 out
        gpt-5.4-mini: $0.75 / $4.50
        gpt-5.4:      $2.00 / $10.00
        gpt-5.5:      $5.00 / $30.00
  * compute_call_cost_usd(prompt_tokens, completion_tokens, model_name)
  * _record_cost_trace() hook called from run_json_prompt /
    run_structured_prompt / run_text_stream after each successful call
  * user_id constructor arg so the trace row gets attributed properly

backend/services/auth_session_service.py threads user_id into the
OpenAIService instance so per-call traces are user-attributable.

17 new tests in tests/backend/test_cost_tracking.py covering:
  * Pricing-table correctness per model
  * Cost computation across token shapes
  * Trace row contract vs the SQL column shape (drift catch)
  * Tier-margin guard — Pro 500 questions on mini stays under $4.50 floor

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two deterministic runners (_run_resume_parser, _run_jd_parser) were
defined with positional underscore-prefixed  while
the rest used  (no underscore) and the test suite
called all of them with the openai_service= keyword. Aligned to
the keyword form across all 5 runners; the deterministic ones
explicitly  to mark intentional-ignore.

Renames a positional argument; no behavior change.

20/20 tests pass in tests/backend/test_nightly_eval.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Whitelists `coderabbit-root` as an allowed auto-review base so the
full-repo audit PRs against that branch get reviewed instead of
"Review skipped." Without this config, CodeRabbit refuses both
auto and manual `@coderabbitai review` triggers on any PR whose
base is not the repo's default branch.

profile: chill — surface issues without nit-blocking style noise.
request_changes_workflow: false — manual triggers always work.

Mirrors the HelpmateAI config (same audit-PR workflow, same trick).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@LEANDERANTONY

Copy link
Copy Markdown
Owner Author

@coderabbitai review --path "backend/**"

Scoping to backend to stay under the 150-file ceiling. Will follow with frontend/**, src/**, tests/** as separate runs.

@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown

@LEANDERANTONY Sure! Kicking off the review now, scoped to backend/**. I'll analyse those files and post findings shortly. Feel free to follow up with the other path scopes (frontend/**, src/**, tests/**) as separate runs once this one completes.

✅ Actions performed

Full review triggered.

LEANDERANTONY added a commit that referenced this pull request May 30, 2026
…(LLM-1, OBS-1)

Two spend-accounting holes left the per-user COGS the pricing rests on understated. (LLM-1) The resume-builder web_search tool called openai_service._client.responses.create directly, bypassing the accounting layer entirely — its tokens (the priciest part of an intake turn) never hit the weekly meter and never cost-traced. (OBS-1) _record_cost_trace checked only self._user_id while _record_token_meter had a meter_user_scope ContextVar fallback, so every JD/resume parse (bare OpenAIService inside a scope) was metered but wrote zero run-trace rows; create_embeddings never cost-traced at all and the embedding model had no pricing entry.

Per Rec #2, OpenAIService is now the single door. New OpenAIService.run_builtin_web_search fires the search AND records usage + cost-trace + enforces the session budget (and applies WEB_SEARCH_TIMEOUT_SECONDS, incidentally fixing the dead-30s-timeout LLM-2); _web_search routes through it. _record_cost_trace gained the same effective_user_id = self._user_id or _METER_USER_ID.get() fallback _record_token_meter uses, so metered and cost-traced are now the same set. create_embeddings cost-traces, and text-embedding-3-small was added to the pricing map.

tests/backend/test_cost_tracking.py adds: a bare service inside meter_user_scope now writes a cost-trace row (was zero); embeddings cost-trace with the embedding price; run_builtin_web_search meters + traces. The web_search dispatcher tests now run against a REAL OpenAIService (the raw-client bypass is gone) and a new test asserts the dispatcher cost-traces. The pricing-map test was updated for the 5th model.

Fixes: LLM-1, OBS-1

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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