"""The `match` block — the ONE contract between the engine and the card. Defined once here (Pydantic) and mirrored as a `JobMatch` TS interface on the frontend `ScoutJob`. Every engine phase writes this exact shape; `scoutJobToMatchRole` reads it. Keeping the breakdown/growth shapes identical to the card's existing `MatchDim`/`growth` means the UI renders real numbers with **no shape change**. """ from __future__ import annotations from typing import Literal from pydantic import BaseModel, ConfigDict, Field Level = Literal["Strong", "Solid", "Light"] Fit = Literal["fit", "stretch"] class MatchDim(BaseModel): """One row of the "how you match" breakdown — mirrors the frontend `MatchDim`. `note` is a one-line, Gemini-toned observation for this dimension (the report-card voice).""" name: str score: int # 0–100 level: Level note: str | None = None class Growth(BaseModel): """The weakest *reachable* dim + the lift closing it buys — mirrors `MatchRole.growth`. `from` is a Python keyword, so it's aliased (JSON stays `{text, from, to}`).""" model_config = ConfigDict(populate_by_name=True) text: str from_: int = Field(alias="from") to: int class MatchResult(BaseModel): """Attached to each surviving ScoutJob as `job["match"]` — the engine's report card. Voice mirrors interview-service's Gemini video-analysis: a characterful archetype, an honest balanced one-liner, per-dimension notes, and a warm coach note. Templated now → Opus later.""" score: int # 0–100 — the real utility (NOT fetch order) fit: Fit archetype: str | None = None # characterful label, e.g. "The Stretch Worth Taking" one_line: str | None = None # balanced headline verdict (strength AND gap) reason: str # short "why picked" breakdown: list[MatchDim] = [] # per-dimension dims, each with a `note` growth: Growth | None = None coach_note: str | None = None # warm, actionable closing line proofReady: bool = False # user is Strong on the job's key skill factors: dict[str, float | None] = {} # debug/observability (per-factor φ; None = absent) def as_dict(self) -> dict: """Serialize for the ScoutJob payload — by alias so growth emits `from`/`to`.""" return self.model_dump(by_alias=True) def attach_match(job: dict, result: MatchResult) -> dict: """Set `job['match']` from a MatchResult (in place) and return the job.""" job["match"] = result.as_dict() return job