Tasks: 03 — Evaluation Engine (evaluateJob) — KEYSTONE
Status legend:
[ ]pending •[~]in-progress •[x]done •[-]dropped
Tests are written alongside each implementation task, never batched. Verify zero competitor references before every commit (constitution Article 11). Depends on #5 structured-output (shared
Artifactcontract) and inherits #6 guardrails (no-invent, honestnot_recommended, cost caps). Standalone-first: only the Ever Jobs API (already synced intojobs) is required.
Phase 1 — Data spine (table + taxonomy/weights config)
-
T01 — Create the
evaluationstable- Files:
packages/db/src/schema/evaluations.ts(new); export inpackages/db/src/schema/index.ts - Acceptance:
integer("id").primaryKey().generatedAlwaysAsIdentity();userIdtext FK →users.idonDelete: "cascade";jobIdinteger FK →jobs.idonDelete: "cascade".scoreinteger (0–100);bandtext enum["apply_now","worth_it","specific_reason","not_recommended"];jobFamily/archetypetext;dimensionsjsonb$type<…>();blocksjsonb$type<…>();modelUsedtext;weightsUsedjsonb;createdAt/updatedAttimestamp().notNull().defaultNow().- Unique
(userId, jobId); indexesevaluations_user_band_idxon(userId, band)andevaluations_user_score_idxon(userId, score). - Exported from
schema/index.ts;pnpm check-typespasses.
- Estimate: 0.5 day
- Files:
-
T02 — Create the
job_family_configtaxonomy table + org override key- Files:
packages/db/src/schema/job-family-config.ts(new); export inpackages/db/src/schema/index.ts;packages/db/src/schema/org-ai-config.ts(add optionalevaluationConfigjsonb key) - Acceptance:
job_family_configholds family → archetype keyword packs as jsonb ($type<{ family: string; archetypes: { name: string; keywords: string[] }[] }>()), with anisActiveboolean and timestamps, matching house style.organizationAiConfigsgains an additiveevaluationConfigjsonb key ($type<{ weights?: Record<string,number>; taxonomy?: ... }>()); no existing column removed.pnpm check-typespasses; both exported fromschema/index.ts.
- Estimate: 0.5 day
- Files:
-
T03 — Push schema + seed the taxonomy
- Files: run
pnpm db:push;packages/db/src/seed/evaluation-taxonomy.ts(new) wired intopnpm db:seed - Acceptance:
pnpm db:pushappliesevaluations+job_family_configcleanly (no destructive diff).- Seed populates
job_family_configwith the spec §3.1 families (Software Eng, Data/ML, Design, Product, Sales, Marketing, Ops/Other) and their illustrative keyword packs. - Re-running the seed is idempotent (upsert on family).
- Estimate: 0.5 day
- Files: run
-
T04 — Schema unit tests
- Files:
packages/db/src/schema/__tests__/evaluations.test.ts(new),packages/db/src/schema/__tests__/job-family-config.test.ts(new) - Acceptance:
- Tests assert column presence, the
bandenum values, unique(userId, jobId), and the two indexes via the Drizzle table metadata. - Green under
pnpm test -- --selectProjects db; no network.
- Tests assert column presence, the
- Estimate: 0.5 day
- Files:
Phase 2 — Deterministic scoring core (no LLM)
-
T05 — Default weight matrix +
mergeWeights- Files:
packages/ai/src/evaluation/weights.ts(new);packages/ai/src/evaluation/weights.test.ts(new) - Acceptance:
- Default 10-dimension matrix matches spec §3.2 (North Star 25, CV match 15, Level 15, Comp 10, Growth 10, Remote 5, Reputation 5, Tech 5, Speed 5, Culture 5).
mergeWeights({ override, user, org })merges in order override → user → org → default, validates the result sums to 100, and falls back to default on an invalid set (never throws to the user).- Tests cover: default sums 100; user override; org override; one-off
weightOverride; invalid (sum≠100 / negative) → default fallback.
- Estimate: 1 day
- Files:
-
T06 — Job-family → archetype detection
- Files:
packages/ai/src/evaluation/taxonomy.ts(new);packages/ai/src/evaluation/taxonomy.test.ts(new) - Acceptance:
detectJobFamily(jobText, cv, prefs, packs)returns{ jobFamily, archetype }from keyword signals (JD + CV/preferences); deterministic, no LLM, no network.- Tests assert correct family/archetype for at least an SRE JD and a Field Marketing JD
(distinct families) plus an ambiguous JD falling back to
Ops / Other.
- Estimate: 1 day
- Files:
-
T07 — Deterministic dimensions (Comp / Remote / Level / CV-baseline)
- Files:
packages/ai/src/evaluation/deterministic.ts(new);packages/ai/src/evaluation/deterministic.test.ts(new); reusespackages/ai/src/tools/salary-helpers.ts(annualise) - Acceptance:
computeDeterministicDimensions(job, user)returns 4 dims each withkey,weight,score5(1–5),rationale,source: "deterministic".- Comp uses annualized
jobs.salaryMin/Max+salaryIntervalvs the user's target; Remote usesjobs.isRemotevs preference; Level usesjobs.jobLevelvs user level; CV-baseline is skills overlap fromcvParsedData. - Missing data degrades gracefully (e.g. comp
budgetFit: "unknown") — never invents. - Tests run without the LLM and cover present/missing salary, remote match/mismatch, and skills-overlap edges.
- Estimate: 1 day
- Files:
-
T08 — Weighted score + 1–5 mirror + band assignment
- Files:
packages/ai/src/evaluation/score.ts(new);packages/ai/src/evaluation/score.test.ts(new) - Acceptance:
computeScore(dimensions, weights)→{ score (0–100), score5 (1–5), band }.- Band cutoffs match spec §3.3:
apply_now≥4.5,worth_it4.0–4.4,specific_reason3.5–3.9,not_recommended<3.5. - Tests assert each band boundary exactly (4.5 / 4.0 / 3.5) and the 0–100 ↔ 1–5 mapping.
- Estimate: 0.5 day
- Files:
Phase 3 — LLM layer + structured output + tool
-
T09 — Evaluation machine-summary schema (#5 contract)
- Files:
packages/ai/src/structured/schemas/evaluation.ts(new, or consume from #5);packages/ai/src/structured/schemas/evaluation.test.ts(new) - Acceptance:
EvaluationSummaryZod schema mirrors spec §4.1 (jobId,score,score5,band,jobFamily,archetype,dimensions[],blocks{A–F},recommendation),schemaVersion = 1.- Returned as
Artifact<"evaluation", EvaluationSummary>per #5; if #5 has not landed, a thin localArtifact/assertArtifactshim is added with aTODOto switch to #5's module. - Tests: valid parse, invalid reject, version field present.
- Estimate: 0.5 day
- Files:
-
T10 —
evaluateJobtool (deterministic core +generateObject+ upsert)- Files:
packages/ai/src/tools/evaluate-job.ts(new);packages/ai/src/tools/__tests__/evaluate-job.test.ts(new) - Acceptance:
tool({ description, inputSchema, execute });inputSchema = z.object({ jobId, weightOverride?, includeInterviewPlan: z.boolean().default(false) }).max()-bounded;jobIdis the integerjobs.id(see plan Open Question 1);userIdis NOT in the input schema.executereads thejobsrow +users.cvParsedData+users.preferences, runs the deterministic core (T05–T08), passes deterministic dims as fixed facts intogenerateObject({ schema: EvaluationSummary })for the LLM dims + A–F blocks, assembles theArtifact, validates viaassertArtifact, and upserts theevaluationsrow (onConflictDoUpdateon(userId, jobId)).- Block F is produced only when
includeInterviewPlanis true. - Returns the validated
evaluationResult(structured object, per Article 5). - Tests (LLM mocked): deterministic dims marked
source:"deterministic"; upsert path called; a bad-fit fixture yieldsband: not_recommendedwith a non-empty reason; invalid LLM output is retried then safely handled (no throw to user).
- Estimate: 1 day
- Files:
-
T11 — Free-tier evaluation cap
- Files:
packages/ai/src/rate-limit.ts(addcheckEvaluateLimit);packages/ai/src/rate-limit.test.ts(extend) - Acceptance:
checkEvaluateLimit(userId)mirrorscheckSearchLimitshape ({ allowed, remaining }); pro tier (subscriptionStatusactive/past_due) is uncapped.- Tests cover under-limit, at-limit (
requiresUpgrade), and subscribed-bypass.
- Estimate: 0.5 day
- Files:
-
T12 — Export + register the tool in the orchestrator
- Files:
packages/ai/src/tools/index.ts(exportevaluateJobTool);packages/ai/src/agents/orchestrator.ts(register intools: { ... });packages/ai/src/agents/orchestrator.test.ts(extend) - Acceptance:
evaluateJobregistered in thestreamTexttoolsobject withuserIdinjected server-side and wrapped bycheckEvaluateLimitfor non-subscribers (same shape assearchJobs);stopWhen: stepCountIs(5)unchanged.- Orchestrator test asserts the tool is registered and
userIdis never sourced from the LLM.
- Estimate: 0.5 day
- Files:
-
T13 — Document the tool in the system prompt
- Files:
packages/ai/src/prompts.ts(getOrchestratorPrompt); Langfuse promptorchestrator-system;packages/ai/src/prompts.test.ts(extend) - Acceptance:
- Prompt documents
evaluateJob(when to call it; that it can/should returnnot_recommendedhonestly; no invented CV evidence) per guardrails (#6) + Article 7. prompts.test.tsasserts the prompt mentionsevaluateJob.
- Prompt documents
- Estimate: 0.5 day
- Files:
Phase 4 — UX surfaces (canvas + drawer + chat + sort)
-
T14 — Canvas-sync case for
evaluateJob- Files:
apps/web/hooks/use-canvas-sync.ts(addcase "evaluateJob"+ state field);apps/web/hooks/use-canvas-sync.test.ts(new or extend,--selectProjects web-lib) - Acceptance:
handleToolResult("evaluateJob", result)stores the structured evaluation on canvas state (keyed byjobId) and exposes a clear/reset callback likeclearSalaryInsights.- Unknown-tool default path remains intact.
- Test asserts state updates for an
evaluateJobresult and ignores malformed input.
- Estimate: 0.5 day
- Files:
-
T15 — Score badge + band pill on the job card
- Files:
apps/web/components/canvas/job-card.tsx; uses@ever-hust/ui/badge,cn()from@ever-hust/ui/lib/utils - Acceptance:
- Badge colors: green ≥80 / amber 60–79 / grey <60; band pill text "Apply now" / "Worth it" / "For a reason" / "Skip — here's why".
- Renders only when an evaluation exists for the job; absent evaluation = no badge (no layout break).
- Clicking the badge opens the detail breakdown (T16).
- Estimate: 0.5 day
- Files:
-
T16 — A–F breakdown in the detail drawer
- Files:
apps/web/components/canvas/evaluation-card.tsx(new, modeled onsalary-insights-card.tsx);apps/web/components/canvas/job-detail-panel.tsx - Acceptance:
- Renders the dimension table (key, weight, score5, rationale, source), CV-match evidence + gaps, comp fit, customization, optional interview plan, and the recommendation.
not_recommendedis shown plainly with its reason — never hidden.- Uses
@ever-hust/ui/*; matches the overlay-card pattern.
- Estimate: 1 day
- Files:
-
T17 — Read API route for persisted evaluations
- Files:
apps/web/app/api/evaluations/[jobId]/route.ts(new);apps/web/lib/api-schemas.ts(add schema); test undertests//--selectProjects web-lib - Acceptance:
GETusesrequireSessionUser(),applyRateLimit(userId, "authenticated"), Zod-validates thejobIdparam, returns the persistedevaluationsrow for(userId, jobId)orapiBadRequest()/404 viaapps/web/lib/api-response.ts.- Response carries the default private no-store cache headers.
- Test covers auth-required, valid fetch, and not-found.
- Estimate: 0.5 day
- Files:
-
T18 — "Best for me" sort (additive)
- Files:
apps/web/components/canvas/filter-bar.tsx;apps/web/components/canvas/jobs-canvas.tsx - Acceptance:
- Adds a "Best for me" sort option that orders jobs by persisted evaluation
scoredesc when evaluations exist; "Newest" remains the default and is not removed. - Jobs without an evaluation sort after scored jobs.
- Adds a "Best for me" sort option that orders jobs by persisted evaluation
- Estimate: 0.5 day
- Files:
Phase 5 — E2E + hardening + docs
-
T19 — Playwright E2E: evaluate a synced job end-to-end
- Files:
tests/e2e/evaluation.spec.ts(new) - Acceptance:
- Drives the chat to evaluate a synced job, asserts the score badge + band pill appear on the canvas card and the A–F breakdown opens in the drawer.
- Asserts a
not_recommendedfixture renders its reason plainly. - Green under
pnpm test:e2e(baseURLhttp://localhost:8443).
- Estimate: 1 day
- Files:
-
T20 — Consistency snapshot tests + model pin
- Files:
packages/ai/src/tools/__tests__/evaluate-job.fixtures.test.ts(new);packages/ai/src/tools/evaluate-job.ts(pin model + temperature for thegenerateObjectcall) - Acceptance:
- Deterministic core is snapshot-tested across a small fixture set of jobs (strong-fit, mid-fit, bad-fit) to guard scoring consistency.
- The LLM call pins model + temperature; documented in the test.
- Estimate: 0.5 day
- Files:
-
T21 — Docs + ROADMAP + competitor-clean check
- Files:
packages/ai/src/structured/STRUCTURED_OUTPUT.md(orpackages/ai/README);docs/specs/ROADMAP.md - Acceptance:
- Documents the evaluation artifact (schema v1), the weights-merge order, the determinism boundary, and the org override key.
- ROADMAP marks epic 03 progress.
pnpm lint,pnpm check-types,pnpm test,pnpm test:e2egreen; grep confirms zero competitor references across the diff (Article 11).
- Estimate: 0.5 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).
- Keep
evaluateJobstandalone-first: read already-syncedjobsdata; no live Ever Jobs call in the hot path; no Gauzy dependency. - Human-in-the-loop:
evaluateJobonly advises — it never applies, sends, or submits anything. - Update
docs/specs/ROADMAP.mdprogress when an epic's tasks complete.