# TypeSafe sentiment integration

Internal Jev classifier, bundled into Canonry. It does not own project authorization, database writes, jobs, or retry scheduling.

- `src/client.ts` owns the fixed TypeSafe HTTP endpoint and safe typed errors. Runtime calls make exactly one attempt through the shared retry wrapper; the durable worker owns retries and total attempt limits.
- `src/classifier.ts` builds one pinned Jev request per answer-subject assessment. `createTypeSafeClassifier` uses direct `jev-1.13.0`; `createJevSentimentClassifier` shares the frozen rubric and evidence checks with the pi OpenRouter runtime in `packages/canonry/src/sentiment-openrouter.ts`. It validates immutable source hashes and full verbatim sentence spans, resolves evidence IDs locally, and never manufactures quotations or truncates input. Direct TypeSafe rejects routed model IDs before dispatch.
- Definition template versions and question wording are part of evaluator identity. Request-semantic changes require a version change and fresh independent evaluation. Moving model aliases are unsupported.
- The active schema-2 evaluator asks five identity/stance/evidence questions and no theme questions. Old schema-1 definitions remain readable history but must not dispatch under new semantics. Branded and non-brand scores are separate populations. A non-brand answer without the known intended subject is `subject-not-mentioned`, never unfavorable; missing frozen identity is `subject-not-applicable`. Invalid conclusion evidence withholds the headline judgment.
- `src/evaluation.ts` scores supplied independent labels offline. Never fabricate reviewer labels or infer release quality from synthetic fixtures. The corpus and frozen rubric under `evals/sentiment/` document unmet release gates. The gate accepts only the current definition hash, rows scored with it, tagged same-name and caveated-favorable challenge recall, and sufficient (not sparse) slices.
- The token estimate is calibrated (UTF-8 bytes / 2.5 plus 1024), not an upper bound; provider context rejection stays a permanent typed failure. Changing the subject identity inputs or sentence segmentation requires bumping `identityVersion` or `segmentationVersion` here and in the contracts definition together. `subject.qualifiedAliases` carries two populations under `qualified-subject-v3`: an Advanced Property's `identityAliases` (phrases with identifying context) and a Simple project's `qualifiedAliases` (bare aliases). Both are names an operator vouched for as this subject's own, never a shared name the identity question warns about, so the field keeps one meaning and filling it for Simple subjects was not an identity-input change: the request builder and subject field set are unchanged, and `subjectHash` isolates those assessments. Whether Jev weighs a bare qualified spelling like a contextual phrase is unverified until the `qualified-former-name` rows are labelled; if it does not, that is a request-semantic change and needs the bump. Adding, removing, or reinterpreting a subject field still requires the bump.
- Direct-client fetch injection is test-only and below the fixed URL. The shared classifier dispatch boundary is a production transport adapter, not a configurable endpoint. OpenRouter uses pi's native `typesafe-system-one` API, fixed `https://openrouter.ai/api/v1/systemone`, and zero internal retries. Literal returned-model and token metadata are observed with pi's injected fetch option because pi's normalized result carries the requested model ID. Routed definitions require `typesafe/jev-1.13` and served snapshot `typesafe/jev-1.13-20260917`; an alias or changed served model fails closed. Keep credentials out of returned failures, fixtures, evaluation artifacts, and logs.

When changes are finalized and ready for push or review, run `pnpm exec vitest run --project integration-typesafe`, the package typecheck, and `pnpm check`. Defer test runs during local iteration, following the root validation policy. Live calls require an explicit corpus and attempt/token budget.
