Tasks: 05 — Structured-Output Contract (trunk root)
Status legend:
[ ]pending •[~]in-progress •[x]done •[-]dropped
Phase 1 — Contract, harness & convention (inside packages/ai)
-
T01 — Define the
Artifactenvelope +defineArtifactfactory- Files:
packages/ai/src/structured/contract.ts,packages/ai/src/structured/contract.test.ts - Acceptance:
- Exports
type Artifact<TKind extends string, TSummary> = { kind: TKind; schemaVersion: number; summary: TSummary; prose?: string }. - Exports
defineArtifact(kind, version, schema)returning{ kind, version, schema, parse(input): Artifact<...>, generate(...) };parserunsschema.parseon the summary and wraps it in the envelope. summaryis whitelisted — only fields declared by the per-kind Zod schema survive; unknown keys are stripped (.strict()or equivalent).- Unit tests: a valid summary parses into a correct envelope; an unknown/extra field is rejected or stripped;
schemaVersionis carried through; type inference ofTSummarymatches the schema (compile-timeexpectTypeOf-style assertion or a typed fixture).
- Exports
- Estimate: 1 day
- Files:
-
T02 — Implement
assertArtifactwith env-aware throw-vs-fallback- Files:
packages/ai/src/structured/contract.ts(extend),packages/ai/src/structured/contract.test.ts(extend) - Acceptance:
- Exports
assertArtifact(artifact)— re-validates the envelope'ssummaryagainst its kind's schema right before any DB write. - In dev/test (
NODE_ENV !== "production") an invalid summary throws; in production it logs (console.error) and returns a deterministic safe-fallback artifact (never throws to the user). - Unit tests cover: valid artifact passes through unchanged; invalid artifact throws in test env; invalid artifact returns the safe-fallback (and logs) when
NODE_ENVis forced to"production".
- Exports
- Estimate: 0.5 day
- Files:
-
T03 —
generateObjectwrapper with bounded retry + Langfuse span- Files:
packages/ai/src/structured/generate-object.ts,packages/ai/src/structured/generate-object.test.ts - Acceptance:
- Wraps the Vercel AI SDK
generateObject({ model, schema, prompt/messages })(depai@^6already inpackages/ai/package.json). - Retries on schema-mismatch up to a bounded count (default 2 re-asks); on exhaustion surfaces a typed failure the caller maps to a deterministic fallback (consumed by
assertArtifact). - Emits an
experimental_telemetryspan (same Langfuse wiring style aspackages/ai/src/agents/orchestrator.ts) tagged with the artifactkind+schemaVersion. - Unit tests (model + Langfuse mocked): success on first try returns the parsed object; one mismatch then success returns the object; exhausted retries returns the typed failure. No live network.
- Wraps the Vercel AI SDK
- Estimate: 1 day
- Files:
-
T04 — First concrete per-kind schema:
evaluation(schemaVersion 1)- Files:
packages/ai/src/structured/schemas/evaluation.ts,packages/ai/src/structured/schemas/evaluation.test.ts - Acceptance:
- Exports
evaluationSummarySchema(Zod) matching spec #3 §4.1:jobId,score(0–100),score5(1–5),bandenum (apply_now|worth_it|specific_reason|not_recommended),jobFamily,archetype,dimensions[](key,weight,score5,rationale,sourceenumdeterministic|llm),blocks{roleSummary, cvMatch{evidence[],gaps[]}, levelStrategy, compDemand{summary,budgetFit enum}, customization, interviewPlan?[]},recommendation. - Exports
EVALUATION_SCHEMA_VERSION = 1and the inferredEvaluationSummarytype. - Whitelisted (
.strict()); no free-form blob fields. - Unit tests: a full valid evaluation parses; missing required field rejects; out-of-range
score/score5rejects; invalidbandrejects; optionalinterviewPlanabsent is valid.
- Exports
- Estimate: 1 day
- Files:
-
T05 — Public export surface (
structured/index.ts)- Files:
packages/ai/src/structured/index.ts,packages/ai/src/index.ts(re-export if the package uses a root barrel) - Acceptance:
- Barrel re-exports
Artifact,defineArtifact,assertArtifact, thegenerateObjectwrapper,evaluationSummarySchema,EVALUATION_SCHEMA_VERSION, andEvaluationSummary. - Export path matches the package's existing convention (mirrors how
tools/agentsare surfaced); importable from the package without reaching intosrc/structured/*file paths. pnpm check-typesclean; a smoke test imports each symbol from the public path.
- Barrel re-exports
- Estimate: 0.5 day
- Files:
-
T06 — Convention doc:
STRUCTURED_OUTPUT.md- Files:
packages/ai/STRUCTURED_OUTPUT.md - Acceptance:
- Documents the
Artifact<TKind, TSummary>envelope, thedefineArtifact/assertArtifact/generateObject-wrapper usage, the "new artifact tools emit a machine summary, not just prose" rule, the whitelist +schemaVersiondiscipline, and where each kind's schema lives (src/structured/schemas/<kind>.ts). - References constitution Article 5 (structured output) and Article 7 (grounded, no-invent).
- Lives under the package (not the repo root) per the no-stray-root-docs rule; contains zero competitor references.
- Documents the
- Estimate: 0.5 day
- Files:
-
T07 — Test guard: artifact tools must register a schema
- Files:
packages/ai/src/structured/guard.test.ts - Acceptance:
- A Jest test that enumerates artifact-producing tools (those importing
defineArtifact) and asserts each references a kind that has a registered schema undersrc/structured/schemas/. - Fails loudly with an actionable message when a new artifact tool is added without a schema.
- Runs under
pnpm test -- --selectProjects ai; green for the current set.
- A Jest test that enumerates artifact-producing tools (those importing
- Estimate: 0.5 day
- Files:
Phase 2 — First adoption (evaluateJob) + canvas + prompt + E2E
-
T08 — Wire
evaluateJobto emitArtifact<"evaluation", EvaluationSummary>- Files:
packages/ai/src/tools/evaluate-job.ts(owned by #3; #5 adds theArtifactplumbing),packages/ai/src/tools/__tests__/evaluate-job.test.ts - Acceptance:
- The tool builds its result via
defineArtifact("evaluation", EVALUATION_SCHEMA_VERSION, evaluationSummarySchema): deterministic dimensions (Comp/Remote/Level/CV-baseline) validated viaschema.parse; LLM-reasoned dims/blocks produced via thegenerate-object.tswrapper. userIdstays server-injected (never an LLM param), consistent withpackages/ai/src/agents/orchestrator.ts.- Unit test (LLM + DB mocked): a fixture (user profile + synced
jobsrow) yields a validArtifact<"evaluation">; a deliberately bad-fit fixture yieldsband: "not_recommended"with a concrete reason.
- The tool builds its result via
- Estimate: 1 day
- Files:
-
T09 — Persist with
assertArtifactimmediately before theevaluationswrite- Files:
packages/ai/src/tools/evaluate-job.ts(extend),packages/ai/src/tools/__tests__/evaluate-job.test.ts(extend) - Acceptance:
assertArtifact(artifact)is the last step before persisting to theevaluationsrow (table owned/created by #3 viapnpm db:push; #5 writes throughdbfrom@ever-hust/db).- The persisted
dimensions/blocksjsonbmatch the validated summary shape. - Unit test: a valid artifact persists (DB mocked, assert payload shape); a forced-invalid artifact in prod-mode logs + writes the safe-fallback rather than throwing.
- Estimate: 0.5 day
- Files:
-
T10 — Register
evaluateJobin the orchestrator + tools barrel (if not already by #3)- Files:
packages/ai/src/agents/orchestrator.ts,packages/ai/src/tools/index.ts,packages/ai/src/agents/orchestrator.test.ts - Acceptance:
export { evaluateJobTool } from "./evaluate-job"added totools/index.ts(if absent).evaluateJobadded to thetools: { ... }object increateOrchestratorStreamwithuserIdinjected server-side (same wrapper pattern asfavoriteJob/interviewPrep); still bounded bystopWhen: stepCountIs(5).- Guarded against a duplicate registration if #3 already added it (coordinate in one session/worktree).
orchestrator.test.tsassertsevaluateJobis present in the registered tool set.
- Estimate: 0.5 day
- Files:
-
T11 — Canvas-sync case for the evaluation artifact
- Files:
apps/web/hooks/use-canvas-sync.ts,apps/web/hooks/use-canvas-sync.test.ts(ortestsunderweb-libproject) - Acceptance:
- Adds
case "evaluateJob"tohandleToolResult, reading the validatedsummary(score, band, dimensions, blocks) and updating canvas state so the score/band surfaces on the right-hand canvas (consumed by #3's score-badge card). - Falls through gracefully (existing
defaultbranch) for malformed/absent summaries — never crashes the canvas. - Unit test: dispatching an
evaluateJobresult updates the expected canvas state; an empty/error result is ignored.
- Adds
- Estimate: 0.5 day
- Files:
-
T12 — System-prompt update: document structured output for
evaluateJob- Files:
packages/ai/src/prompts.ts,packages/ai/src/prompts.test.ts - Acceptance:
- The
DEFAULT_ORCHESTRATOR_PROMPTlistsevaluateJoband states it emits a structured machine summary (score, band, A–F blocks) alongside narration — the orchestrator narrates the structured result, does not re-format it. - A note flags that the same content must be mirrored into the Langfuse
orchestrator-systemprompt (labelproduction). prompts.test.tsasserts the default prompt mentionsevaluateJoband the structured-output convention.
- The
- Estimate: 0.5 day
- Files:
-
T13 — E2E: evaluation artifact round-trips to the canvas
- Files:
tests/e2e/evaluation.spec.ts - Acceptance:
- Playwright test (baseURL
http://localhost:8443) drives the chat to evaluate a synced job and asserts the structured result (score badge / band) renders on the jobs canvas. - Asserts an honest
not_recommendedverdict renders plainly when the fixture job is a bad fit (no hidden negative). - Runs under
pnpm test:e2e; green in CI.
- Playwright test (baseURL
- Estimate: 1 day
- Files:
-
T14 — Full-suite verification + competitor-clean check
- Files: (verification only —
packages/ai/**,apps/web/hooks/use-canvas-sync.ts,tests/e2e/evaluation.spec.ts) - Acceptance:
pnpm test -- --selectProjects ai,pnpm test(web-lib + ai),pnpm test:e2e,pnpm lint,pnpm check-typesall green.rgover the staged change returns zero competitor references (constitution Article 11); only Ever brands (Ever Jobs, Ever Gauzy, Hust, Ever Co.) appear.- CI (lint, type-check, unit, E2E) green on
developbefore opening the PR tomain. docs/specs/ROADMAP.mdprogress updated for epic 05.
- Estimate: 0.5 day
- Files: (verification only —
Notes
- Write tests alongside each implementation task; do not batch testing into a final task.
packages/dbgets no new table in this epic — theevaluationstable andpnpm db:pushare owned by #3. #5 only defines thejsonbshape (schemas/evaluation.ts) that #3 stores.- #5 must land before or alongside #3 so #3 can import
evaluationSummarySchema. - Verify zero competitor references before every commit (see constitution Article 11).
- Update
docs/specs/ROADMAP.mdprogress when an epic's tasks complete.