Tasks: 04 — Liveness DTO + Ghost-Job Freshness
Status legend:
[ ]pending •[~]in-progress •[x]done •[-]dropped
Phase 1 — Upstream contract (Ever Jobs) + forward-compatible client type
-
T01 — Add additive
livenessto the Ever Jobs publicJobPostDto(upstream repo)- Files: (separate
ever-jobsrepo) theJobPostDtoreturned by/api/jobs/searchand job-by-id — add optionalliveness: { verdict: 'active'|'expired'|'uncertain', code, checkedAt }sourced from the existingliveness-httpchecker. - Acceptance:
- Field is optional / additive — no required field added to the DTO.
- Existing Ever Jobs consumers verified unaffected (response shape unchanged when liveness is absent).
- Verdict only ever one of
active | expired | uncertain;checkedAtis an ISO string. - Change tracked in the
ever-jobsrepo per its ownAGENTS.md(not in this tree).
- Estimate: 1 day
- Files: (separate
-
T02 — Extend Hust client
JobPostDtowith optionalliveness(forward-compatible)- Files:
packages/jobs-api/src/types.ts(addliveness?: { verdict: "active" | "expired" | "uncertain"; code?: string; checkedAt?: string }to theJobPostDtointerface);packages/jobs-api/src/types.test.ts. - Acceptance:
- A
JobPostDtowithoutlivenessstill type-checks and is accepted (forward-compatible). - A
JobPostDtowithlivenessexposes the typedverdict | code | checkedAt. - Unit test covers both present and absent
liveness. pnpm test -- --selectProjects jobs-apigreen.
- A
- Estimate: 0.5 day
- Files:
Phase 2 — Persist liveness on the synced jobs row
-
T03 — Add nullable liveness columns to the
jobsschema- Files:
packages/db/src/schema/jobs.ts(addlivenessVerdict: text("liveness_verdict", { enum: ["active","expired","uncertain"] }),livenessCode: text("liveness_code"),livenessCheckedAt: timestamp("liveness_checked_at")— all nullable; optionalindex("jobs_liveness_verdict_idx").on(table.livenessVerdict)). No change needed inpackages/db/src/schema/index.ts(jobsalready exported). - Acceptance:
- All three columns are nullable (no
.notNull(), no default that breaks existing rows). - Column names follow house style (snake_case
liveness_*); verdict uses thetext(..., { enum })pattern. pnpm check-typesgreen forpackages/db.
- All three columns are nullable (no
- Estimate: 0.5 day
- Files:
-
T04 — Push the schema change to the database
- Files: apply via
pnpm db:push(drizzle.config.ts → schema./src/schema/index.ts). No file edit beyond T03. - Acceptance:
pnpm db:pushcompletes;jobstable shows the three nullableliveness_*columns.- Existing rows are unaffected (verdict
NULLfor pre-existing jobs). - No destructive/altering of existing columns reported in the push diff.
- Estimate: 0.5 day
- Files: apply via
-
T05 — Map liveness in
mapJobToDband carry it through both sync paths- Files:
packages/triggers/src/map-job.ts(mapJobToDb: addlivenessVerdict: dto.liveness?.verdict ?? null,livenessCode: dto.liveness?.code ?? null,livenessCheckedAt: safeDate(dto.liveness?.checkedAt)); verify pass-through inpackages/triggers/src/sync-jobs.tsandapps/web/app/api/jobs/sync/route.ts(both spread...mappedinto the upsert — no change expected);packages/triggers/src/map-job.test.ts. - Acceptance:
- A DTO with
livenessmaps all three columns; a DTO withoutlivenessyieldsnullfor all three (no throw). - Invalid / unparseable
checkedAtcoalesces tonullviasafeDate. - Existing
mapJobToDbfields unchanged (additive only). - Unit tests cover present, absent, and bad-
checkedAtcases;pnpm test -- --selectProjects triggersgreen.
- A DTO with
- Estimate: 0.5 day
- Files:
Phase 3 — Render the liveness badge (canvas card + both detail views)
-
T06 — Create the presentational
LivenessBadgecomponent- Files: new
apps/web/components/canvas/liveness-badge.tsx(usesBadgefrom@ever-hust/ui/badge+cnfrom@ever-hust/ui/lib/utils; props{ verdict: "active"|"expired"|"uncertain"|null; checkedAt?: string | Date | null }). - Acceptance:
- Renders active=green / expired=grey / uncertain=amber; renders nothing when
verdictis null/undefined. - Shows a tooltip/title "checked
ago" when checkedAtpresent (reusetimeAgofrom@/lib/format-date). - Pure/presentational (
memo), no data fetching; accessible label (e.g.aria-label="Listing is expired"). pnpm lint+pnpm check-typesgreen.
- Renders active=green / expired=grey / uncertain=amber; renders nothing when
- Estimate: 0.5 day
- Files: new
-
T07 — Surface liveness columns through the search API and
JobCardData- Files:
apps/web/app/api/jobs/search/route.ts(addlivenessVerdict,livenessCode,livenessCheckedAtto thedb.select({...})block);apps/web/components/canvas/job-card.tsx(add the three fields to theJobCardDatainterface; render<LivenessBadge>in the card's badge/meta row). - Acceptance:
- Search response includes the three liveness fields for each job.
JobCardDatacarries the fields (nullable);JobCardshows the badge, anduncertainjobs are still rendered in the list (never filtered out).- Cards for jobs with
NULLverdict render with no badge and no layout shift. pnpm check-typesgreen.
- Estimate: 0.5 day
- Files:
-
T08 — Render the badge on both detail surfaces (canvas overlay + jobs page)
- Files:
apps/web/components/canvas/job-detail-panel.tsx(render<LivenessBadge>in the detail header);apps/web/app/(dashboard)/jobs/[id]/page.tsx(render badge in the header badges row and/or sidebar "Job Details" card; data already comes from thegetJobselect onjobs). - Acceptance:
- Badge appears on the canvas detail overlay and the server-rendered job detail page.
- Detail page keeps existing
JobPostingJSON-LD; liveness is presentational only (no schema.org change required). uncertain/expiredjobs still fully viewable;pnpm lint+pnpm check-typesgreen.
- Estimate: 0.5 day
- Files:
Phase 4 — Non-blocking apply-flow warning (human-in-the-loop preserved)
-
T09 — Emit a non-blocking
livenessWarningfrom theapplyJobtool- Files:
packages/ai/src/tools/apply-job.ts(extend the jobselectto readlivenessVerdict/livenessCheckedAt; when verdict is notactive, addlivenessWarning: { verdict, checkedAt, message }to the success result — without blocking the apply). - Acceptance:
- When the job verdict is
expired/uncertain, the result includeslivenessWarning; whenactiveorNULL, no warning field. - The apply still proceeds (the tool already requires user approval upstream — that gate is unchanged; nothing is auto-submitted).
userIdstays server-injected (never an LLM param); existing duplicate/transaction logic untouched.- Unit test in
packages/ai/src/tools/apply-job.test.ts(new alongside the tool) covers warning-present and warning-absent paths;pnpm test -- --selectProjects aigreen.
- When the job verdict is
- Estimate: 1 day
- Files:
-
T10 — Document the liveness behaviour in the orchestrator system prompt
- Files:
packages/ai/src/prompts.ts(getOrchestratorPrompt; mirror in Langfuse promptorchestrator-system) — add a short "Listing freshness / liveness" note instructing the model to surfacelivenessWarningto the user before they approve an apply, and to never hideuncertainjobs. - Acceptance:
- Prompt explains the liveness verdict and that a non-
activejob must be flagged to the user pre-approval, non-blocking. - Prompt states
uncertainjobs are shown, never dropped. pnpm test -- --selectProjects ai(prompts.test.ts) green; Langfuseorchestrator-systemupdated to match local fallback.
- Prompt explains the liveness verdict and that a non-
- Estimate: 0.5 day
- Files:
-
T11 — Surface the caution line in the apply approval card
- Files:
apps/web/components/chat/tool-approval.tsx(ingetToolDisplayfor theapplyJobcase, render an amber caution line whenargs.livenessWarning/verdict is present — e.g. "This listing may be closed (checked X ago) — you can still apply"). - Acceptance:
- For a non-
activejob, the approval card shows an amber, dismissible caution; Approve is still enabled (non-blocking). - For an
active/unknown job, the card looks exactly as today (no caution). - No change to the approval/deny mechanics;
pnpm lint+pnpm check-typesgreen.
- For a non-
- Estimate: 0.5 day
- Files:
Phase 5 — E2E + roadmap
-
T12 — Playwright coverage for badge + apply warning
- Files:
tests/e2e/jobs.spec.ts(extend; baseURLhttp://localhost:8443). - Acceptance:
- A job with an
expiredfixture verdict shows the grey badge; anuncertainfixture shows the amber badge and is still present in the list (asserts it is not hidden). - Initiating apply on a non-
activejob surfaces the non-blocking warning, and the user can still proceed. pnpm test:e2egreen locally.
- A job with an
- Estimate: 1 day
- Files:
-
T13 — Update roadmap progress
- Files:
docs/specs/ROADMAP.md. - Acceptance:
- Epic 04 progress reflects completion of the shipped phases.
- Zero competitor references confirmed across all changed files (Article 11) before commit.
- Full CI (lint, type-check, unit, E2E) green on
developbefore the PR merges (Article 10.4).
- Estimate: 0.5 day
- Files:
Notes
- Write tests alongside each implementation task; do not batch testing into a final task (T02/T05/T09 carry their own unit tests; T12 is the dedicated E2E).
- Verify zero competitor references before every commit (see constitution Article 11).
- Ship forward-compatible: every Hust change tolerates a missing/null
livenessso Hust merges and runs green even before the Ever Jobs DTO field is deployed (Article 2 standalone-first). uncertainis shown, never hidden — do not add any filter that drops non-activejobs.- Human-in-the-loop (Article 4) is preserved: the apply warning is non-blocking and the existing approval gate is enriched, never bypassed or auto-confirmed.
- Update
docs/specs/ROADMAP.mdprogress when an epic's tasks complete.