Files
jobtrackingapp/.gsd/milestones/M001/slices/S01/S01-RESEARCH.md
T

11 KiB

S01 — Research

Date: 2026-03-24

Summary

S01 owns R001 and R002 directly, and it materially supports R010 because imported correspondence becomes part of the job timeline and later follow-up context. The codebase already has a complete Gmail OAuth path, a Gmail message listing endpoint, single-message and thread import endpoints, and a per-job correspondence UI. The gap is not missing Gmail plumbing; it is that matching is still mostly manual. The backend returns raw Gmail search results, while the frontend (job-tracker-ui/src/components/Correspondence.tsx) applies a lightweight client-side score based only on the freeform query, snippet text, and already-imported subjects. That does not use the actual job/company context strongly enough to satisfy the “less manual cleanup” bar in R002.

The best approach is to keep the existing OAuth/import flow and add a job-aware matching layer rather than replacing the Gmail integration. In practice that means: enrich backend candidate discovery around a specific JobApplication, return grouped thread/message suggestions with explicit match reasons/confidence inputs, and then update the correspondence dialog to present those suggestions first. This follows the current architecture cleanly and preserves the no-auto-send boundary from D002. The React side should keep async work consolidated instead of scattering additional fetches across effects; the loaded react-best-practices skill is relevant here, especially async-parallel and client-event-listeners.

Recommendation

Add a dedicated job-scoped Gmail matching surface on top of the existing endpoints instead of trying to make the current generic /api/gmail/messages search UI smarter only in the browser.

Recommended shape:

  • Backend: add a job-aware endpoint in JobTrackerApi/Controllers/GmailController.cs that accepts jobApplicationId and optional overrides, loads the job + company context, builds Gmail queries from JobTitle, Company.Name, Company.RecruiterEmail, recruiter name, and recent imported correspondence, then returns ranked message/thread candidates with match reasons and enough metadata for import decisions.
  • Persistence: if the planner wants durable thread-aware behavior, extend Models/Correspondence.cs beyond ExternalMessageId to also persist at least ExternalThreadId and raw sender/recipient metadata. This is the cleanest way to support downstream S03 reply/follow-up context without re-deriving it later.
  • Frontend: refactor job-tracker-ui/src/components/Correspondence.tsx so the Gmail tab consumes enriched API data instead of doing primary ranking locally. JobDetailsDialog.tsx already loads the full job; passing the job or a reduced job-context prop into Correspondence is cheaper than forcing the Gmail tab to rediscover job facts.

Why this approach:

  • It uses the existing Gmail OAuth/token flow unchanged.
  • It moves matching logic to the backend where job context, dedupe checks, and future heuristics are easier to test.
  • It avoids over-investing in fragile client-only heuristics.
  • It creates a natural seam for S03, where better thread metadata and message provenance will matter again.

Implementation Landscape

Key Files

  • JobTrackerApi/Controllers/GmailController.cs — current Gmail API surface. Has /status, /connect-url, /messages, /import, and /import-thread. Import endpoints already attach messages to JobApplication and dedupe by Correspondence.ExternalMessageId, but discovery is still generic and not job-aware.
  • JobTrackerApi/Services/GmailOAuthService.cs — Gmail OAuth/token refresh and Gmail API access. ListMessagesAsync currently calls the Gmail list endpoint and then does an N+1 sequence of GetMessageAsync calls to hydrate summaries. There is no thread-specific fetch API, no job-aware query builder, and no ranking/match-reason contract here yet.
  • job-tracker-ui/src/components/Correspondence.tsx — the main S01 frontend surface. It opens the Gmail dialog, loads /gmail/status, loads /gmail/messages, groups by threadId, and sorts using scoreMessage(...). Current suggestions come from existing correspondence subjects, not from job/company/recruiter context.
  • job-tracker-ui/src/components/JobDetailsDialog.tsx — already loads the full JobApplication record and hosts the Correspondence tab. This is the easiest place to pass job context into Correspondence instead of refetching it inside the Gmail tab.
  • job-tracker-ui/src/types.ts — frontend contracts for GmailStatus, GmailMessageSummary, and CorrespondenceMessage. Any enriched matching response or persisted metadata expansion needs updates here.
  • Models/Correspondence.cs — currently stores From, Subject, Channel, ExternalMessageId, Content, and Date. No ThreadId, no original Gmail sender/recipient fields, and no match/debug metadata.
  • Data/JobTrackerContext.cs — EF relationships and ownership filters. JobApplication, Company, and GmailConnection are user-scoped via query filters; new Gmail-matching endpoints should continue loading jobs through this context rather than bypassing ownership.
  • Models/Company.cs and Models/JobApplication.cs — hold the matching signals that the current Gmail UI ignores: Company.Name, RecruiterEmail, RecruiterName, JobTitle, JobUrl, ShortSummary, and existing correspondence/timeline relationships.
  • JobTrackerApi/Controllers/CorrespondenceController.cs — current create/list/delete API for job-linked messages. If S01 persists extra Gmail metadata, this contract may need to expose it to the UI and timeline.
  • JobTrackerApi/Controllers/JobApplicationsController.cs — downstream dependency surface. It already reads Correspondences for follow-up drafting and timeline assembly, so better message/thread metadata here directly helps S03.
  • JobTrackerApi/Program.cs — important migration/backfill guardrail. The app manually ensures legacy SQLite/MySQL columns such as Correspondences.Subject, Channel, and ExternalMessageId. If S01 adds new persistence columns, these compatibility blocks must be updated alongside the EF migration.
  • JobTrackerApi.Tests/GmailControllerTests.cs — only covers the empty-thread import validation case today. Good starting point, but far below the verification level needed for S01.
  • job-tracker-ui/src/job-details-generated-drafts.test.tsx — representative React test style: mock api, render JobDetailsDialog, assert visible tab behavior. Follow this pattern for new Gmail suggestion/import UI tests.

Build Order

  1. Decide and lock the backend contract first.

    • Prove what a “smarter match” response looks like: message/thread grouping, rank/confidence, reasons, imported/already-linked flags, and import actions.
    • This is the riskiest part and unblocks everything else.
  2. Implement job-aware matching in GmailController + supporting service/helpers.

    • Load JobApplication with Company.
    • Build candidate Gmail queries from job/company/recruiter data.
    • Merge/dedupe results by message id or thread id.
    • Compute match reasons server-side.
    • Keep existing /import and /import-thread behavior unless the new contract proves they need richer return payloads.
  3. Only after the contract is stable, refactor Correspondence.tsx.

    • Replace scoreMessage(...) as the primary ranking engine with server-provided ranking/reasons.
    • Pass job context from JobDetailsDialog.tsx rather than introducing another job fetch in the correspondence tab.
    • Keep manual query override/search available as a fallback, not the primary UX.
  4. Then extend persistence if needed for thread continuity.

    • Add correspondence metadata only if the chosen backend contract needs it for dedupe, import clarity, or future reply context.
    • If added, update model, migration, and Program.cs compatibility shims together.
  5. Finish with tests.

    • Backend tests for matching/import behavior first.
    • Frontend tests for the new Gmail suggestion UI second.

Verification Approach

  • Backend unit/integration tests:
    • dotnet test JobTrackerApi.Tests
    • Add tests for: job-aware candidate endpoint contract, dedupe behavior for already-imported messages, ownership-scoped job lookup, and thread import summary results.
  • Frontend tests:
    • npm test -- --watch=false from job-tracker-ui
    • Add React tests covering: Gmail tab rendering ranked suggestions, showing match reasons, import button states, and fallback/manual search behavior.
  • Contract/manual verification:
    • Open a job in JobDetailsDialog → Correspondence tab.
    • Confirm Gmail connection state still works.
    • Confirm the Gmail tab now shows job-relevant suggestions before freeform searching.
    • Import a single message and a full thread; verify the job timeline/correspondence list updates and duplicates are skipped.
  • If persistence changes land:
    • Verify schema startup still succeeds on existing dev DBs because JobTrackerApi/Program.cs legacy EnsureColumn(...) blocks are easy to miss.

Constraints

  • Data/JobTrackerContext.cs applies ownership query filters to Company, JobApplication, and GmailConnection. Any new Gmail matching endpoint must keep loading through EF-scoped entities, not raw unfiltered ids.
  • JobTrackerApi/Services/GmailOAuthService.cs currently exposes only message list/detail methods. There is no reusable thread-fetch or search-aggregation abstraction yet.
  • JobTrackerApi/Program.cs contains manual schema repair code for SQLite/MySQL. Adding correspondence metadata requires updating both EF migration artifacts and these runtime compatibility paths.
  • Models/Correspondence.cs does not currently preserve Gmail thread identity or raw sender/recipient fields, which limits downstream thread-aware UX.

Common Pitfalls

  • Leaving ranking in the browserCorrespondence.tsx can polish server results, but if the primary intelligence stays in scoreMessage(...), S01 will remain query-driven and fragile.
  • Adding columns without updating Program.cs — this repo relies on startup-time EnsureColumn(...) logic for legacy/dev databases; migration-only changes are incomplete here.
  • Duplicating job fetches in the dialog treeJobDetailsDialog.tsx already owns the job record. Per the react-best-practices guidance (async-parallel, client-event-listeners), keep async fetching consolidated and avoid adding more dialog-level waterfalls or duplicated global listeners.
  • Treating thread import as enough without thread metadata — importing all messages in a thread helps today, but without persisting thread identity the app still cannot reason clearly about thread continuity later.

Open Risks

  • Gmail search quality may still be noisy even after better query construction; the planner should expect one iteration on ranking heuristics once real data is exercised.
  • ListMessagesAsync is sequential and could become noticeably slow if the new matching flow issues multiple Gmail searches per job. If that happens, batching/parallelization inside the Gmail service becomes part of S01, not a later optimization.

Skills Discovered

Technology Skill Status
React react-best-practices available
ASP.NET Core openai/skills@aspnet-core installed