Tasks: 19 — Batch Evaluation
Status legend:
[ ]pending •[~]in-progress •[x]done •[-]dropped
Phase 1 — Extract the reusable scoring core
-
T01 — Extract
evaluateJobCorefrom the on-demand engine- Files:
packages/ai/src/agents/evaluate-job-core.ts(new); reads frompackages/ai/src/tools/evaluate-job.ts(#3) and usespackages/ai/src/structured/index.ts(evaluationArtifact,runValidatedGeneration,assertArtifact); upserts via@ever-hust/dbevaluationstable - Acceptance:
evaluateJobCore({ userId, jobId, weightOverride?, includeInterviewPlan? })returns anArtifact<"evaluation">and upserts theevaluationsrow (latest wins).userIdis a required function argument — never an LLM-supplied param.- Deterministic dimensions (comp/remote/level/CV-baseline) are computed without the LLM and
marked
source: "deterministic". - Reuses the existing
evaluations_user_job_unique (userId, jobId)upsert path.
- Estimate: 1 day
- Files:
-
T02 — Delegate the
evaluateJobtool to the core + export- Files:
packages/ai/src/tools/evaluate-job.ts(edit — owned by #3);packages/ai/src/index.ts(exportevaluateJobCore) - Acceptance:
- The tool's
executecallsevaluateJobCoreand returns the same structured shape as before (no behaviour change for chat callers). evaluateJobCoreis exported from@ever-hust/aifor import bypackages/triggers.- The orchestrator registration in
packages/ai/src/agents/orchestrator.tsis unchanged.
- The tool's
- Estimate: 0.5 day
- Files:
-
T03 — Unit-test
evaluateJobCore- Files:
packages/ai/src/agents/evaluate-job-core.test.ts(new) - Acceptance:
- Fixture job + fixture user → deterministic dims computed without invoking the LLM.
- Asserts the returned artifact validates against
evaluationArtifactand the upsert is called with(userId, jobId). - Runs green via
pnpm test -- --selectProjects ai.
- Estimate: 0.5 day
- Files:
Phase 2 — Cost gate + cheap pre-filter
-
T04 — Implement the LLM-free score-floor pre-filter
- Files:
packages/ai/src/agents/score-floor-prefilter.ts(new);packages/ai/src/index.ts(export) - Acceptance:
scoreFloorPrefilter(userId, job)returns{ estimate5: number, keep: boolean }using only deterministic signals (comp vs target, remote fit, level fit, CV-skill overlap) — zero LLM calls.keepisfalsewhenestimate5is below the effective floor (per-tier default, optionalusers.preferencesoverride).
- Estimate: 1 day
- Files:
-
T05 — Add the per-tier batch quota cost gate
- Files:
packages/ai/src/rate-limit.ts(edit — addcheckBatchEvaluateQuota(userId, isSubscribed)reusingcheckRateLimit);packages/utils/src/constants.ts(edit — free/pro batch caps);packages/utils/src/index.ts+packages/utils/src/withCostGate.ts(newwithCostGateonly if epic #6 has not landed — otherwise import from #6) - Acceptance:
- Free vs pro batch caps are read from
users.subscriptionStatusin (active,past_due). withCostGate(scoreFloor | quota)skips work over quota / below floor and proceeds otherwise.- If #6's
withCostGatealready exists, this task imports it and adds no duplicate.
- Free vs pro batch caps are read from
- Estimate: 1 day
- Files:
-
T06 — Unit-test pre-filter + cost gate
- Files:
packages/ai/src/agents/score-floor-prefilter.test.ts(new);packages/ai/src/rate-limit.test.ts(edit — add quota cases) - Acceptance:
- Below-floor job →
keep: false; above-floor →keep: true(no LLM in the test). - Over-quota → gate skips; under-quota → gate proceeds; free vs pro caps both covered.
- Green via
pnpm test -- --selectProjects ai.
- Below-floor job →
- Estimate: 0.5 day
- Files:
Phase 3 — batch-evaluate Trigger.dev task + run tracking
-
T07 — Add the
batch_evaluation_runstable + push- Files:
packages/db/src/schema/batch-evaluation-runs.ts(new);packages/db/src/schema/index.ts(edit — export); runpnpm db:push - Acceptance:
- Columns follow house style:
integer("id").primaryKey().generatedAlwaysAsIdentity();userId text → users.id (cascade);status text enum [queued,running,done,failed];jobIds jsonb $type<number[]>; counts (total,evaluated,skippedBelowFloor,skippedOverQuota,failed) integer;createdAt/updatedAttimestamps. - Index
index("batch_eval_runs_user_idx").on(table.userId). - Exported from
schema/index.ts;pnpm db:pushapplies cleanly.
- Columns follow house style:
- Estimate: 0.5 day
- Files:
-
T08 — Implement
batchEvaluateTask(pre-filter → gate → bounded fan-out → idempotent upsert)- Files:
packages/triggers/src/batch-evaluate.ts(new);packages/triggers/src/index.ts(edit — exportbatchEvaluateTask) - Acceptance:
- Imports
evaluateJobCore+scoreFloorPrefilterfrom@ever-hust/ai. - Runs
scoreFloorPrefilterthenwithCostGate; below-floor / over-quota jobs are recorded as skipped and never reachevaluateJobCore. - Fans out survivors with a hard concurrency cap (no unbounded
Promise.allover the whole set). - Per-job
try/catch(mirrorssync-jobs.ts); failures increment the run'sfailedcount and do not abort the run. - Re-running the same
jobIdsupdatesevaluationsin place via the existing(userId, jobId)unique — no duplicate rows. - Updates
batch_evaluation_runscounts and finalstatus. - The task performs no outward action (no apply/send/favorite) — only
evaluationswrites.
- Imports
- Estimate: 1 day
- Files:
-
T09 — Unit-test the batch task (concurrency, skip path, idempotency)
- Files:
packages/triggers/src/batch-evaluate.test.ts(new) - Acceptance:
- Concurrency: with N jobs and cap C, no more than C
evaluateJobCorecalls are in-flight. - Skip path: a below-floor / over-quota job increments the skipped count and is never evaluated.
- Idempotency: a second run over the same
jobIdsissues upserts (no insert of duplicates). - Asserts the task never calls any outward-action path.
- Green via
pnpm test -- --selectProjects triggers.
- Concurrency: with N jobs and cap C, no more than C
- Estimate: 1 day
- Files:
Phase 4 — Enqueue API, progress + results UI (realtime), no auto-action
-
T10 — Enqueue + progress API routes
- Files:
apps/web/app/api/jobs/batch-evaluate/route.ts(new — POST enqueue);apps/web/app/api/jobs/batch-evaluate/[runId]/route.ts(new — GET progress);apps/web/lib/api-schemas.ts(edit — batch request Zod schema) - Acceptance:
- POST uses
requireSessionUser(),applyRateLimit(userId, "authenticated"), validates body with the new Zod schema, triggersbatchEvaluateTask, returns therunIdviaapiSuccess. - GET returns the
batch_evaluation_runsrow (status + counts) for the owning user only;apiBadRequest/apiErroron bad/missing run. - Request schema bounds
jobIdslength (e.g..max(...)) per the AI/API input-bounds rule.
- POST uses
- Estimate: 1 day
- Files:
-
T11 — Canvas sync + progress/results overlay card
- Files:
apps/web/hooks/use-canvas-sync.ts(edit — addcase "batchEvaluate");apps/web/components/canvas/batch-evaluation-card.tsx(new — templatesalary-insights-card.tsx); reuseapps/web/hooks/use-realtime-jobs.ts/packages/supabase/src/realtime.tsfor landed results - Acceptance:
handleToolResult("batchEvaluate", result)updates canvas state with run id + progress.- The overlay card shows queued/running/done + counts and a results summary; uses
@ever-hust/ui/card/badgeandcn(). - Evaluated jobs light up the #3 score badge/band pill on
apps/web/components/canvas/job-card.tsx(no new badge component). - The only actions offered are view/favorite — no apply/send control is rendered.
- Estimate: 1 day
- Files:
-
T12 — Register batch capability in the orchestrator prompt + tool surface
- Files:
packages/ai/src/prompts.ts(edit — document the batch capability + no-auto-action invariant; mirror to Langfuse promptorchestrator-system); if a chat-callablebatchEvaluateJobstool is added, register it inpackages/ai/src/agents/orchestrator.tstools: { ... }withuserIdinjected server-side - Acceptance:
- The system prompt explains batch evaluation scores a set and surfaces results, and that it never applies/sends on the user's behalf.
- If a tool is exposed, its
userIdis orchestrator-injected (never an LLM param) and it is added topackages/ai/src/tools/index.ts+ the orchestratortoolsobject.
- Estimate: 0.5 day
- Files:
-
T13 — E2E: batch-evaluate a small set, assert badges + zero auto-action
- Files:
tests/e2e/batch-evaluation.spec.ts(new) - Acceptance:
- Seeds/uses a small synced set, enqueues a batch, waits for results.
- Score badges/band pills appear on evaluated cards; the "evaluated"/"Best for me" view filters to scored jobs.
- Asserts no application/outreach is created and no apply/send action fires (HITL preserved).
- Runs against
http://localhost:8443viapnpm test:e2e.
- Estimate: 1 day
- Files:
Notes
- Write tests alongside each implementation task; do not batch testing into a final task.
- Verify zero competitor references before every commit (see constitution Article 11).
- Update
docs/specs/ROADMAP.mdprogress when an epic's tasks complete. - Upstream order matters: #5 structured contract (shipped) and #3
evaluationstable (shipped) are in place; #3'sevaluateJobtool and #6'swithCostGateare assumed — Phase 1/2 extract or ship-then-absorb additively if they have not landed.