Tasks: 10 — Cover-Letter Pipeline (HITL + PDF)
Status legend:
[ ]pending •[~]in-progress •[x]done •[-]dropped
Phase 1 — Render service + ATS-safe template (shared with #11)
-
T01 — Scaffold
packages/render-servicewithrenderHtmlToPdf()client- Files:
packages/render-service/package.json,packages/render-service/tsconfig.json,packages/render-service/src/index.ts,packages/render-service/src/client.ts,.env.example(addRENDER_SERVICE_URL), rootjest.config(addrender-serviceproject) - Acceptance:
renderHtmlToPdf(html, options)POSTs HTML to theRENDER_SERVICE_URLendpoint and returns a PDFBufferon success.- On endpoint failure it retries (bounded) and returns a typed
{ ok: false, retryable: true }result — it never throws past the boundary. - Package builds and type-checks;
pnpm installwires it into the workspace.
- Estimate: 1 day
- Files:
-
T02 — ATS-safe cover-letter HTML template + client unit tests (alongside)
- Files:
packages/render-service/src/templates/cover-letter.ts,packages/render-service/src/client.test.ts,packages/render-service/src/templates/cover-letter.test.ts - Acceptance:
coverLetterHtml(letter, meta)emits single-column, real-text, system-font HTML (no image-as-text, no multi-column) — asserted by snapshot.- Client test (mocked fetch) covers success →
Bufferand endpoint-down → retryable error. pnpm test -- --selectProjects render-servicegreen.
- Estimate: 0.5 day
- Files:
Phase 2 — documents table + Supabase Storage wiring
-
T03 — Add
documentsDrizzle schema + export- Files:
packages/db/src/schema/documents.ts,packages/db/src/schema/index.ts - Acceptance:
- Table follows house style:
integer("id").primaryKey().generatedAlwaysAsIdentity(),text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), nullableinteger("job_id").references(() => jobs.id, { onDelete: "cascade" }),text("kind", { enum: ["cover_letter", "resume"] }).notNull(),integer("version").notNull().default(1),text("storage_key"),text("pipeline_state", { enum: ["draft","user_review","approved","rendered"] }).notNull().default("draft"),jsonb("machine_summary").$type<CoverLetterSummary>(),timestamp("created_at").notNull().defaultNow(). - Indexes on
(user_id)and(user_id, job_id, kind); exported fromschema/index.ts.
- Table follows house style:
- Estimate: 0.5 day
- Files:
-
T04 — Apply schema with
pnpm db:push+ schema unit test- Files:
packages/db/src/schema/documents.test.ts(DB Jest project) - Acceptance:
pnpm db:pushapplies the table to the database with no diff left.- Test inserts a
documentsrow and selects it back; version defaults to 1; a second row for the same(userId, jobId, kind)is stored as version 2 by the insert helper. pnpm test -- --selectProjects dbgreen.
- Estimate: 0.5 day
- Files:
-
T05 — PDF storage helper (reuse Supabase storage)
- Files:
packages/render-service/src/storage.ts(thin wrapper over@ever-hust/supabaseuploadFile/getPublicUrl),packages/render-service/src/storage.test.ts - Acceptance:
- Writes the PDF to
documents/{userId}/{jobId}/{version}.pdfand returns the public URL. - Never upserts over an existing approved version (path is version-keyed).
- Unit test mocks
uploadFile/getPublicUrland asserts the path shape.
- Writes the PDF to
- Estimate: 0.5 day
- Files:
Phase 3 — State machine + un-overridable requireApproval gate (#6)
-
T06 — Pure pipeline transition function + guards
- Files:
packages/ai/src/cover-letter/pipeline.ts,packages/ai/src/cover-letter/pipeline.test.ts - Acceptance:
transition(state, event)only allowsdraft → user_review → approved → rendered; any other transition throws.renderedis unreachable unless the machine passed through an explicitapprovedevent (proven by an invariant test, including a simulated "model output says approved" input that must NOT advance state).pnpm test -- --selectProjects aigreen for this file.
- Estimate: 1 day
- Files:
-
T07 — Route approval through
requireApproval(#6) + approve API route- Files:
apps/web/app/api/documents/[id]/approve/route.ts,packages/ai/src/cover-letter/require-approval.ts(local shim re-exporting #6'srequireApprovalwhen present),apps/web/lib/api-schemas.ts(approve body schema) - Acceptance:
- The approve route is the ONLY code path that performs
user_review → approved; it callsrequireSessionUser(),applyRateLimit(userId, "authenticated"), validates with Zod, and usesapiBadRequest()/apiError()fromapps/web/lib/api-response.ts. - Approval is gated by
requireApproval(actionId); the shim delegates to #6 once merged. - A request without an authenticated session is rejected; a document not owned by the user 404s.
- The approve route is the ONLY code path that performs
- Estimate: 1 day
- Files:
Phase 4 — Keyword mirroring + no-invent + structured artifact (#5)
-
T08 — Deterministic JD+CV keyword mirroring
- Files:
packages/ai/src/cover-letter/keywords.ts,packages/ai/src/cover-letter/keywords.test.ts - Acceptance:
- Mirrors keywords computed from the JD (
jobs.description,jobs.skills) ∪ the user's CV (users.cvParsedData,users.skills), reusingextractAtsKeywords/skill-overlap frompackages/ai/src/tools/resume-helpers.ts. - Unit test asserts every mirrored keyword is a subset of (JD ∪ CV) tokens — zero invention.
pnpm test -- --selectProjects aigreen for this file.
- Mirrors keywords computed from the JD (
- Estimate: 1 day
- Files:
-
T09 —
cover_letterstructured artifact (#5) + export- Files:
packages/ai/src/structured/schemas/cover-letter.ts,packages/ai/src/structured/index.ts,packages/ai/src/structured/schemas/cover-letter.test.ts - Acceptance:
- Defines
coverLetterArtifactviadefineArtifact("cover_letter", 1, coverLetterSummarySchema)frompackages/ai/src/structured/contract.ts; summary whitelists mirrored keywords, keyword-coverage score, tone, word count, grounding gaps, pipeline state, version. - Exported from
structured/index.tsnext to the existingevaluationexports. - Test: valid summary
build()s; an out-of-whitelist / malformed summary failsassertArtifact.
- Defines
- Estimate: 0.5 day
- Files:
-
T10 — Apply
assertNoInventedto the draft (#6)- Files:
packages/ai/src/cover-letter/no-invent.ts(local shim re-exporting #6'sassertNoInvented),packages/ai/src/cover-letter/no-invent.test.ts - Acceptance:
- The draft text is validated so CV-evidence claims must reference real
users.cvParsedDatafields; a fabricated employer/number/quote is rejected. - Unit test: a draft with an invented employer fails; a grounded draft passes.
- The draft text is validated so CV-evidence claims must reference real
- Estimate: 0.5 day
- Files:
-
T11 —
coverLetterPipelinetool + orchestrator registration + prompt update- Files:
packages/ai/src/tools/cover-letter-pipeline.ts,packages/ai/src/tools/index.ts,packages/ai/src/agents/orchestrator.ts,packages/ai/src/prompts.ts,packages/ai/src/tools/cover-letter-pipeline.test.ts,packages/ai/src/prompts.test.ts - Acceptance:
- Tool defined with
tool({ description, inputSchema: z.object(...).max()-bounded, execute });userIdis injected server-side inorchestrator.ts(NEVER an LLM param), matching the existinggenerateCoverLetterwrapper. - Exported from
tools/index.ts; registered in thetools: { ... }object inorchestrator.ts; free-tier quota still enforced viacheckCoverLetterLimit(packages/ai/src/rate-limit.ts). - Tool returns an
assertArtifact-validatedcover_letterartifact (prose + summary) and the current pipeline state; it advancesdraft → user_reviewonly (never auto-approves/renders). prompts.tsdocuments the new tool and states the assistant must never claim a letter is approved/sent;prompts.test.tssnapshot updated.
- Tool defined with
- Estimate: 1 day
- Files:
Phase 5 — UI: draft → edit → approve → download PDF + version history
-
T12 — Render + download API routes
- Files:
apps/web/app/api/documents/[id]/render/route.ts,apps/web/app/api/documents/[id]/download/route.ts,apps/web/lib/api-schemas.ts - Acceptance:
renderroute runs ONLY when the document isapproved; it callsrenderHtmlToPdf+coverLetterHtml, stores the PDF (T05), advances state torendered, setsexport const maxDuration = 60, and on render-service failure returns a graceful retryable error (staysapproved).downloadroute streams the stored PDF for the owning user only;requireSessionUser()+applyRateLimit(userId, "export"); non-owner → 404.- Render is behind the cost gate / free-tier quota; never renders an unapproved document.
- Estimate: 1 day
- Files:
-
T13 — Cover-letter canvas card (draft → edit → approve → download + versions)
- Files:
apps/web/components/canvas/cover-letter-card.tsx - Acceptance:
- Overlay card modelled on
apps/web/components/canvas/salary-insights-card.tsx, using@ever-hust/ui/{card,button,badge,dialog}+cn(). - Shows editable draft; an explicit Approve button that calls the approve route; a
Download PDF action that appears ONLY when state is
rendered; a version-history list. - No "Download" before
rendered; no "approved" badge before the gate fires; nothing labelled "sent".
- Overlay card modelled on
- Estimate: 1 day
- Files:
-
T14 — Wire tool result into canvas sync
- Files:
apps/web/hooks/use-canvas-sync.ts - Acceptance:
- New
case "coverLetterPipeline"inhandleToolResultsurfaces the structured result (state, version, summary, job context) into canvas state and opens the cover-letter card. - Existing
case "generateCoverLetter"is left intact (backward compatible); state shape extended without breaking existing consumers.
- New
- Estimate: 0.5 day
- Files:
Phase 6 — Tests + CI green
-
T15 — E2E: generate → edit → approve → download PDF
- Files:
tests/e2e/cover-letter.spec.ts - Acceptance:
- Playwright spec drives the chat to draft a letter, edits it, clicks the explicit Approve, then
downloads a PDF; asserts the download is a non-empty
application/pdf. - Asserts the Download control is absent before approval (gate cannot be skipped from the UI).
pnpm test:e2egreen againsthttp://localhost:8443.
- Playwright spec drives the chat to draft a letter, edits it, clicks the explicit Approve, then
downloads a PDF; asserts the download is a non-empty
- Estimate: 1 day
- Files:
-
T16 — Full suite green + competitor-clean self-check
- Files: (CI only) — run
pnpm lint,pnpm check-types,pnpm test,pnpm test:e2e - Acceptance:
- All Jest projects and Playwright specs green; lint
--max-warnings 0and type-check clean. - Grep the diff for competitor names (Article 11) — empty result before commit/push.
- CI green on
develop;docs/specs/ROADMAP.mdprogress updated for epic 10.
- All Jest projects and Playwright specs green; lint
- Estimate: 0.5 day
- Files: (CI only) — run
Notes
- Write tests alongside each implementation task; do not batch testing into a final task (T15/T16 are the E2E + suite-wide gate, not the first time tests appear).
- Verify zero competitor references before every commit (see constitution Article 11). Our own Ever brands — Ever Jobs, Ever Gauzy, Hust, Ever Co. — are fine.
- Human-in-the-loop is structural: the
approvedtransition (T06/T07) is the only path to render; never auto-submit or auto-send. - Update
docs/specs/ROADMAP.mdprogress when an epic's tasks complete.