← Back to Archive
Project · 01 · System Design

Pathfinder

A weekly job-search agent for students. Fewer, better, you decide. Every Monday it reads the postings, picks twenty, explains each pick with a reason tag, and drafts materials that cite the student's own stories — then the student chooses a mode per job: Skip, Focus, Assist or Managed.

Year2026
MediumNext.js · SQLite · Design System
DisciplineSystem Design · Product · UI
StatusRunnable, pipeline mocked
Principle

The agent proposes. The student decides. Nothing is sent that wasn't chosen, and nothing is written that isn't cited.

pathfinder.app / report
The weekly report · twenty jobs, a reason on each, a mode switch on every row
01 · Architecture

Five layers, one swap line

The UI never learns whether a report was produced by rules or by a model. Everything that will change lives behind three functions in lib/report.ts; everything that won't — accounts, schema, components — is built for real from day one.

Sources

Postings

  • Greenhouse JSON
  • Lever · Ashby
  • School packs (Handshake)
  • Watchlist orgs
Now · lib/seed.ts 20 postings
→
Worker

Weekly pipeline

  • Fetch → dedupe
  • Embed + score
  • Reason tags DL MT BT EZ
  • Draft with citations
Now · generateReport() rule-based
→
Data

SQLite → Postgres

  • users · directions
  • resumes · stories
  • jobs · report_jobs
  • applications
Now · better-sqlite3 · WAL
→
Server

Next.js actions

  • bcrypt + signed cookie
  • middleware redirect
  • actions.ts — every write
  • setMode · saveParagraphs
Real · no mock
→
Client

Pathfinder DS

  • Broadsheet base
  • tokens.css light/dark
  • ModeSwitch · ReasonTag
  • MatchRing · StoryChip
Real · same classes as the DS
02 · Data Model

Seven tables; the student's stories are the centre

Directions rank what the student wants; résumés bind to directions; stories are the evidence. A report row is a scored (user, job, week); choosing a mode turns it into an application whose paragraphs carry storyIds — the citation that the UI enforces.

usersauth
  • idpk
  • emailunique
  • password_hashbcrypt
  • quota20
  • report_day · channelMon · email
  • school_packberkeley
directionsintent
  • idpk
  • user_id→ users
  • name · hint
  • rank1..n
  • resume_id→ resumes
storiesevidence
  • idpk
  • user_id→ users
  • title · body
  • tagscsv
jobsshared
  • idpk
  • org · title · type · url
  • deadlineiso
  • direction_hint
  • big_title · easy_getflags
  • base_score70
report_jobsweekly
  • idpk
  • user_id · job_id→
  • weekmonday iso
  • score · reasonsjson
  • rationale · story_idsjson
  • modenull | skip | focus…
  • unique(user, job, week)
applicationschosen
  • idpk
  • user_id · job_id→
  • modefocus | assist | managed
  • statusdrafting → offer
  • paragraphs[{text, storyIds}]
  • strategyjson
  • unique(user, job)
pathfinder.app / login
Sign inReal accounts — bcrypt, signed cookie, middleware redirect
pathfinder.app / tracker
Trackerapplications.status as a board — drafting → ready → submitted → interview → offer
03 · The Weekly Pipeline

From two hundred postings to twenty, each with a reason

01

Fetch

Pull ATS feeds and school packs; dedupe by org + title; drop past deadlines.

seedJobsIfEmpty()
02

Score

Base score plus a boost for matching the student's ranked directions; penalty for none.

base + max(0, 10 − 2·rank)
03

Tag

Four reasons, each with a one-line why: closes soon, strong match, big title, easy to get.

DL · MT · BT · EZ
04

Cite

Find up to two stories whose tags appear in the posting; they become the rationale's evidence.

storyIds: [3, 7]
05

Rank & cut

Deadlines within 7 days first, then score. Keep the quota.

.slice(0, quota)
06

Draft on demand

Materials are written only when a mode is chosen — one paragraph per cited story, one deliberately uncited.

ensureMaterials()
// lib/report.ts — the swap line. Replace the body, keep the signature; the UI does not change. export function generateReport(userId, week = currentWeek(), quota = 20) { /* rules → embeddings + Claude rerank */ } export function ensureMaterials(userId, jobId) { /* template → Claude drafting from the Story Bank, with storyIds */ } export function setMode(userId, reportId, mode) { /* skip deletes the draft; focus | assist | managed upserts an application */ }
04 · Modes & State

Four modes decide how much the agent does

A mode is set per job on the report. It is the only place where autonomy is granted, and it is granted one posting at a time.

Skip

Deletes any draft. The job stays in the report as declined so it isn't re-proposed.

Focus

The student writes; the agent supplies strategy, story chips and the posting's own words.

Assist

The agent drafts every paragraph with citations; the student edits and submits.

Managed · locked

Phase 3: Playwright submits on Greenhouse / Lever / Ashby. Shown with a lock so the boundary is visible now.

pathfinder.app / jobs / 1
// application.status drafting → ready → submitted → interview → rejected | offer // gate before "ready": every paragraph must cite a story const canSubmit = paragraphs.every(p => p.storyIds.length > 0); // uncited paragraphs get the amber underline and block submit
05 · Design System · Live

Seven signature components, one class contract

Pathfinder's semantic layer sits on the Broadsheet base: same class names and data-* attributes in the design system and in React, so bundle.css applies unchanged. These are real, rendered from the tokens — hover a reason tag.

ModeSwitch · per job
ReasonTag · with why
DLCloses soonDeadline in 3 daysMTStrong matchMatches your #1 direction, ProductBTBig titleStripe is on the watchlistEZEasy to getOn-campus; no cover letter
MatchRing · DeadlineBadge
88   3d 21d
StoryChip · the citation
Paragraph · cited vs uncited
I built the club's website in a week with two friends; it now handles sign-ups for 300 members.cites #3
I am a fast learner who thrives in collaborative environments and brings energy to every team.uncited · blocks submit
Job row · the report unit
Product Intern, Summer 2027
Stripe · Internship · closes Oct 3
88
DLCloses soonMTStrong match3d
Tokens
Source Serif 4 · 5px grid · radius 1–4px
06 · Roadmap · What is mocked, and where the real thing goes

Ship the shell first; swap the engine

Mocked nowReal versionPhase
seed.ts postingsWorker fetching Greenhouse / Lever / Ashby JSON + school packs1
generateReport()Embeddings + Claude rerank1
ensureMaterials()Claude drafting from the Story Bank, with storyIds citations1
Résumé summary boxPDF upload + parsing2
Autofill (disabled)Browser extension2
Managed (locked)Playwright submit on Greenhouse / Lever / Ashby3
SQLiteSupabase Postgres — swap lib/db.ts, keep the queries—
Reflection

Build the decisions first.
The intelligence
can arrive later.

Accounts, schema, components and the mode boundary are real today. Three functions stand where the model will go.

— 2026 · Zhang Yichi

Next Project

Sitescape

→