Tasks: 01 — Harvest the Ever Jobs Backend
Status legend:
[ ]pending •[~]in-progress •[x]done •[-]dropped
Phase 1 — Widen the jobs-api client contract
-
T01 — Widen
SiteEnum+ add free-tier default subset- Files:
packages/jobs-api/src/types.ts,packages/jobs-api/src/index.ts(re-export) - Acceptance:
SiteEnumexpanded from the current 11 values to the full supported Ever Jobs set (keep the existing 11 as members; add the rest).- New exported
FREE_TIER_SITESconstant (a sensible subset ofSiteEnum) is added and re-exported fromsrc/index.ts. ScraperInputSchema.siteTypestill acceptsSiteEnum[]and remains optional; existing callers compile unchanged.pnpm check-typesgreen.
- Estimate: 0.5 day
- Files:
-
T02 — Relax the hard
countrydefault + describecompanySlug- Files:
packages/jobs-api/src/types.ts - Acceptance:
countryno longer silently defaults to"USA"inScraperInputSchema(callers pass it explicitly; absence means broad/no pin).companySluggains a.describe(...)documenting ATS/company-direct unlock; remains optional.- No existing call site relying on the old default breaks (audit
sync-jobs.ts+api/jobs/sync/route.ts— they passcountryexplicitly in T05/T06).
- Estimate: 0.5 day
- Files:
-
T03 — Unit tests for the widened client contract
- Files:
packages/jobs-api/src/types.test.ts,packages/jobs-api/src/client.test.ts - Acceptance:
- Test asserts
SiteEnumincludes the new members andFREE_TIER_SITES⊆SiteEnum. - Test asserts
ScraperInputSchemaparses an input with multi-siteType+companySlug+ a non-USAcountry. client.test.tsassertscompanySlug,siteType, andcountryserialise into the POST body ofsearchJobs(mockfetch).pnpm test -- --selectProjects jobs-apigreen.
- Test asserts
- Estimate: 0.5 day
- Files:
Phase 2 — Broaden the sync coverage
-
T04 — Add country + company-slug rotation lists
- Files:
packages/triggers/src/map-job.ts - Acceptance:
- New exported
SYNC_COUNTRIES(broad set) and optionalCOMPANY_SLUGSarrays added besideSEARCH_TERMS. - Lists are non-empty and documented; no removal of
SEARCH_TERMS.
- New exported
- Estimate: 0.5 day
- Files:
-
T05 — Thread
siteType/country/companySlugthrough the Trigger.dev sync- Files:
packages/triggers/src/sync-jobs.ts - Acceptance:
- Removes the hard
country: "USA"literal; rotates a{ searchTerm, country, siteType, companySlug }config per 15-min tick. - Passes the widened
siteTypeset (full orFREE_TIER_SITESper design) and an optional rotatedcompanySlug. - Per-tick result caps preserved (no fan-out); upsert-by-
externalIdlogic unchanged so Ever Jobs dedup collapses overlap. pnpm check-typesgreen.
- Removes the hard
- Estimate: 1 day
- Files:
-
T06 — Mirror the broadened params in the manual sync route
- Files:
apps/web/app/api/jobs/sync/route.ts - Acceptance:
- Accepts optional
country,siteType,companySlugin the request body (validated/clamped like the existingresultsWanted). - Drops the hard
country: "USA"; when body omitscountry, uses the broad default. - Returns
apiSuccess(...)with the resolved params; errors viaapiError(...).
- Accepts optional
- Estimate: 0.5 day
- Files:
-
T07 — Unit tests for sync rotation + param shaping
- Files:
packages/triggers/src/map-job.test.ts,packages/triggers/src/sync-jobs.test.ts(new) - Acceptance:
- Test asserts
SYNC_COUNTRIES/COMPANY_SLUGSare non-empty and rotate deterministically by tick index. - Test asserts the sync builds a
ScraperInputwith the widenedsiteType, a non-USAcountry, and an optionalcompanySlug(mockeverJobsClient.searchJobs). pnpm test -- --selectProjects triggersgreen.
- Test asserts
- Estimate: 0.5 day
- Files:
Phase 3 — Consume market analytics (cache + tool + route)
-
T08 — Create the
market_analyticscache schema- Files:
packages/db/src/schema/market-analytics.ts,packages/db/src/schema/index.ts - Acceptance:
- Table follows house style:
integer("id").primaryKey().generatedAlwaysAsIdentity();text("query").notNull();jsonb("snapshot").$type<MarketSnapshot>();timestamp("fetched_at").notNull().defaultNow(); index onquery. - Exported
marketAnalyticsfromschema/index.ts. - No changes to existing tables.
- Table follows house style:
- Estimate: 0.5 day
- Files:
-
T09 — Apply the schema with
pnpm db:push+ schema test- Files:
packages/db/src/schema/market-analytics.test.ts, (runtime)pnpm db:push - Acceptance:
pnpm db:pushapplies the new table with no diff on existing tables.- Test asserts column names/types and the presence of the
queryindex. pnpm test -- --selectProjects dbgreen.
- Estimate: 0.5 day
- Files:
-
T10 — Add the
refresh-market-analyticsTrigger.dev task- Files:
packages/triggers/src/refresh-market-analytics.ts - Acceptance:
- Task calls
everJobsClient.analyzeJobs(input)for the rotation terms and upserts{ query, snapshot, fetchedAt }intomarketAnalytics(conflict onquery). - Runs on its own (slower) cron — does not piggy-back the 15-min
sync-jobstick (OQ-3 default). - Failures are non-fatal per-term (logged, loop continues), mirroring
sync-jobs.ts. pnpm check-typesgreen.
- Task calls
- Estimate: 1 day
- Files:
-
T11 — Add the read-only
getMarketInsightsAI tool- Files:
packages/ai/src/tools/get-market-insights.ts,packages/ai/src/tools/index.ts - Acceptance:
tool({ description, inputSchema: z.object({...}).max()-bounded, execute })— inputjobTitle(max 200)/optionallocation(max 200); read-only (no writes, no apply/send).executereads the latestmarketAnalyticssnapshot for the query and returns a strict structured object (median/range, remote %, top companies, per-site comparison) alongside a graceful{ hasData: false }when no snapshot exists.userIdis NOT an input param (no user-scoped data here).- Exported from
tools/index.ts.
- Estimate: 1 day
- Files:
-
T12 — Register
getMarketInsightsin the orchestrator- Files:
packages/ai/src/agents/orchestrator.ts - Acceptance:
- Added to the
tools: { ... }object insidestreamText(read-only — no rate-limit wrapper required; free per OQ-2 default). - Imported from
../tools;stopWhen: stepCountIs(5)unchanged. pnpm check-typesgreen.
- Added to the
- Estimate: 0.5 day
- Files:
-
T13 — Document
getMarketInsightsin the system prompt- Files:
packages/ai/src/prompts.ts - Acceptance:
DEFAULT_ORCHESTRATOR_PROMPTgains agetMarketInsightsbullet under "Your Capabilities" + a short usage section (when to call, how to present).- Note added that the Langfuse
orchestrator-system(labelproduction) prompt must be updated to match (migration step). pnpm test -- --selectProjects ai(prompts.test.ts) green.
- Estimate: 0.5 day
- Files:
-
T14 — Add the
GET /api/marketread route- Files:
apps/web/app/api/market/route.ts,apps/web/lib/api-schemas.ts - Acceptance:
- Uses
requireSessionUser(),applyRateLimit(userId, "authenticated"), Zod query validation fromlib/api-schemas.ts, errors viaapiBadRequest()/apiError(). - Returns the cached
marketAnalyticssnapshot for ajobTitle/locationquery; empty payload when none. - Default
Cache-Controlheaders per the route convention.
- Uses
- Estimate: 0.5 day
- Files:
-
T15 — Unit tests for the tool + route schema
- Files:
packages/ai/src/tools/get-market-insights.test.ts,apps/web/lib/api-schemas.test.ts(extend) - Acceptance:
- Tool test: returns a structured snapshot when the cache has a row; returns
{ hasData: false }(no throw) when empty; input Zod.max()bounds enforced. - Route-schema test: the new market query schema parses valid input and rejects over-long strings.
pnpm test -- --selectProjects ai web-libgreen.
- Tool test: returns a structured snapshot when the cache has a row; returns
- Estimate: 0.5 day
- Files:
Phase 4 — Surface analytics in canvas + feed evaluation
-
T16 — Build the
MarketInsightsCardcomponent- Files:
apps/web/components/canvas/market-insights-card.tsx - Acceptance:
- Modelled on
salary-insights-card.tsx; ShadCN via@ever-hust/ui/card,@ever-hust/ui/badge,cnfrom@ever-hust/ui/lib/utils. - Renders median/range, remote %, top companies, per-site comparison; hides gracefully when
hasData: false. - Exposes a
MarketInsightsDatatype matching the tool's return shape.
- Modelled on
- Estimate: 1 day
- Files:
-
T17 — Wire the tool result through
use-canvas-sync- Files:
apps/web/hooks/use-canvas-sync.ts - Acceptance:
- New
marketInsights: MarketInsightsData | nullslice onCanvasState. - New
case "getMarketInsights"inhandleToolResultsets the slice only whenhasData(mirrors thesalaryInsightssampleSize>0guard). - New
clearMarketInsightscallback returned from the hook.
- New
- Estimate: 0.5 day
- Files:
-
T18 — Render the card on the chat canvas
- Files:
apps/web/app/(dashboard)/chat/page.tsx,apps/web/components/canvas/jobs-canvas.tsx - Acceptance:
MarketInsightsCardrenders as an overlay/panel (like the salary-insights card) whenmarketInsightsis set, with a dismiss wired toclearMarketInsights.- No layout regression to the existing jobs canvas / dashboard toggle.
pnpm lint+pnpm check-typesgreen.
- Estimate: 0.5 day
- Files:
-
T19 — Feed cached analytics into salary insights + expose Comp/Demand context
- Files:
packages/ai/src/tools/salary-insights.ts - Acceptance:
salaryInsightsToolreads the latestmarketAnalyticssnapshot (when present) and adds a market annotation field to its structured result (additive — existing fields unchanged).- Exported
getMarketContext(jobTitle, location?)helper returns the cached Comp/Demand block for epic #3 (evaluation engine) to consume. - Tests updated in
packages/ai/src/tools/salary-insights.test.ts(annotation present when cache hit; absent when miss). pnpm test -- --selectProjects aigreen.
- Estimate: 1 day
- Files:
-
T20 — E2E: multi-source results + market panel
- Files:
tests/e2e/jobs.spec.ts - Acceptance:
- E2E asserts the jobs canvas renders results spanning more than the original 11 sites (incl. at least one ATS/company-direct via
companySlug-seeded data). - E2E asserts a market panel/card renders after a market-insights interaction.
pnpm test:e2egreen againsthttp://localhost:8443.
- E2E asserts the jobs canvas renders results spanning more than the original 11 sites (incl. at least one ATS/company-direct via
- Estimate: 1 day
- Files:
Notes
- Write tests alongside each implementation task; do not batch testing into a final task.
- Every new AI tool is read-only — nothing here applies, submits, or sends (human-in-the-loop, Article 4 preserved).
- Every AI artifact emits a Zod-validated structured object alongside prose (Article 5); align
getMarketInsightsto epic #5's shared contract (OQ-1). - The only external dependency added is the existing Ever Jobs API (Article 2 standalone-first); no Gauzy dependency.
- DB change is additive (
market_analyticsonly) and applied withpnpm db:push; update the Langfuseorchestrator-systemprompt to matchprompts.ts. - Verify zero competitor references before every commit (constitution Article 11).
- Update
docs/specs/ROADMAP.mdprogress when this epic's tasks complete.