# ADR-0002: A grounded race analyst over published forecast artifacts

- **Status:** Accepted
- **Date:** 2026-08-21
- **Scope:** The reader-facing “Ask the Race” experience
- **Supersedes:** The deferred Phase 2 description in `ADR-0001_STATIC_PUBLICATION.md`

## Context

The complete forecast page is intentionally auditable, but most readers should not need to interpret every chart and methodology section before they can understand the race. The useful product is not a second, conversational forecast. It is a short answer to questions such as “Why is the candidate who trails the latest poll still favored?”, “What would have to be true for the other candidate to win?”, or “Would this new evidence actually change the model?”

A frontier model is useful both for understanding varied wording and for turning an audited briefing into a clearer explanation. It is not the authority for election facts, arithmetic, source acceptance, scenarios, or forecast probabilities. Those remain deterministic, versioned, and reviewable.

## Decision

We will add a multi-race **Ask the Race** analyst with two layers:

1. A deterministic TypeScript engine recognizes the core supported questions, reads only the currently published forecast artifact, performs bounded scenario arithmetic, and returns a typed briefing with facts, assumptions, limitations, and citations.
2. An optional Cloudflare Pages Function may use two explicitly configured models: a low-reasoning routing model maps varied reader wording into a closed intent, and a higher-reasoning analysis model may explain the resulting deterministic briefing. The second model receives a bounded qualitative projection: a static intent summary, candidate/component direction invariants, exact fact identifiers, and allowlisted qualitative hints. It does not receive the raw reader question, deterministic prose, exact values, units, assumptions, limitations, or source titles. It returns a short why explanation and what-to-watch note, each with the fact identifiers on which it relied. The full original packet remains local and is the authority used to validate every returned identifier and direction. The displayed thesis remains deterministic.

The routing attempt occurs before deterministic answer construction. Deterministic code then resolves the supported intent, parses any explicit scenario values, and constructs the complete reference briefing before any explanatory call. A routing failure falls back to deterministic intent handling; an explanation failure omits the optional prose. The feature therefore remains useful in local development, during an upstream outage, and before a model API key is configured.

## Reader contract

Every answer is visibly stamped as one of four things:

- **Official forecast** — explains the published probability or model state.
- **Conditional scenario** — answers an “if this, then that” question while keeping the official forecast unchanged.
- **Evidence analysis** — explains what a poll, event, or evidence limitation does or does not establish.
- **Not modeled** — says that the published system cannot support the requested conclusion.

The answer includes the forecast run and data cutoff, the assumptions required for any scenario, and links to the published sources that support it. Scenario arithmetic is never presented as a newly estimated election probability. Unsupported questions fail explicitly instead of inviting the language model to improvise.

The static page gives every race the same five-part reading order: summary, current polling, forecast and closeness, directional commentary, and conditional candidate paths. Ask the Race provides shorter question-specific views over that same artifact. Directional context remains reviewed commentary with exactly zero forecast effect; assessments share a `dependency_group` when they express the same underlying evidence family. Candidate paths are conditional descriptions, not separate or path-specific probabilities.

### Persistent Analyst's Take

Each registered race publishes a concise, always-visible Analyst's Take. A published free-prose release uses an `analyst-take.v3` sidecar; the current-run unavailable fallback remains the prose-free `analyst-take.v2` form. Both are snapshot-bound to the forecast run, artifact hash, and data cutoff, and repeat an exact zero forecast effect. For v3, the higher-reasoning analysis model writes one coherent four-part race briefing from a closed, deterministic claim plan with section-specific evidence identifiers, while the forecast thesis and candidate-specific paths remain deterministic and byte-matchable. Only a v3 model-authored, model-output-verbatim briefing with `owner_authorized_internal_use`, the `internal-use-owner-authorization.v1` policy, an owner role, and an authorization timestamp receives the interface labels **AI-authored race briefing** and **Evidence-bound · Owner authorized**. The calculated forecast frame remains separately labeled as not AI-generated, and fallback copy never receives an AI-authored label.

The briefing uses a dedicated, closed qualitative packet rather than the open reader question. Deterministic code selects the claim plan for race state and current polling, dependency-deduplicated reviewed context, conditional candidate needs, and what to watch. The model may author the prose but cannot choose the underlying facts. Its strict output carries evidence identifiers for every section and the complete briefing; postconditions require the canonical candidates and component directions, reviewed context direction and confidence, material caveats, an explicit outside-forecast boundary, and future-facing watch language. They reject new numbers, probabilities, citations, outside facts, campaign advice, advocacy, certainty, or forecast effects. These automated checks screen required facts and known failure modes but cannot mechanically prove generic semantic entailment. The owner may accept that remaining AI risk for internal use without a sentence-by-sentence human-review claim. Persistent publication permits no semantic repair: the authorized briefing remains byte-for-byte the captured model output.

The current sidecar is independently authorized and archived; it is not added to forecast inputs or the forecast artifact hash. Its exact bytes must match the content-addressed archive named by the race registry, and the canonical release digest stored in `approval_sha256` covers the snapshot bindings, content, evidence, sources, and provenance. A missing, stale, malformed, unauthorized, or archive-divergent sidecar blocks publication. A repository forecast release automatically publishes a current-run `status: unavailable` sidecar with no AI prose (unless the identical forecast already has an intact owner-authorized release), and the static resolver displays deterministic fallback copy with truthful provenance until the owner explicitly authorizes and promotes a passing draft.

The static build never calls OpenAI and never reads an operator credential. A trusted operator command reads `ANALYST_TAKE_OPENAI_API_KEY` from an explicit process value or a permission-restricted, Git-ignored local `.analyst-take.env`; that dedicated filename is outside Astro/Vite's automatic environment-file set. The command loads the current local registry and run-bound forecast, constructs the closed briefing packet deterministically, and calls the fixed OpenAI endpoint directly. Before semantic validation, every completed response is captured outside the repository in a nonpromotable operator envelope under a mode-`0700` directory and an exclusively created mode-`0600` file. The envelope contains only exact output through the 10,000-character safety ceiling, safe response metadata, hashes, and a closed capture status—never credentials, request headers, provider HTTP error bodies, or a public path. Over-limit output is omitted with a closed disposition while its SHA-256 digest remains available for diagnosis. A locally rejected envelope cannot be salvaged or promoted. A successful response separately emits an unreviewed v3 draft containing the actual response model and metadata plus hashes of the prompt, qualitative packet, raw structured output, and verbatim briefing. Neither artifact can publish or mutate the site. Automated screening, explicit owner acceptance of residual AI risk, and `scripts/promote_analyst_take.py --authorized-at <time> --authorize-internal-use` are required; the command recomputes those bindings, revalidates the candidate with the shared TypeScript guard before any release mutation, creates a full-digest immutable archive, updates the current pointer and registry, and reruns the full release gate. The deployed Ask Function rejects `summarize_race` unconditionally and has no summary token, quarantine, or diagnostic mode; its separate `OPENAI_API_KEY` remains a production-only Cloudflare secret.

This creates an intentional two-stage publication path: first publish a current-run `status: unavailable` v2 sidecar; then capture from a trusted operator environment, run the automated screens, record owner authorization for internal use, promote, and deploy the model-authored, model-output-verbatim v3 briefing as static JSON. The fallback must never display an AI-authored label, and a prior schema or prior-run narrative is never relabeled or carried forward.

## Model boundary

The reader-facing model may:

- recognize natural variations of a supported question; and
- select one implemented intent from the closed schema;
- explain what the supplied deterministic facts mean together; and
- identify which already-modeled evidence would most change the assessment.

It may not:

- search the web or admit new evidence during a reader request;
- invent or revise a number, coefficient, source, probability, or scenario result;
- add a factual claim that is not grounded in an identifier from the supplied briefing;
- restate quantitative values in generated prose, where context could be reassigned or distorted;
- alter an input file, model run, forecast artifact, or publication;
- profile the reader, remember a conversation, recommend a candidate, or produce personalized political persuasion; or
- present itself as an official election authority.

New polls and news continue through the separate research and human-review pipeline. Once accepted and published in a new artifact, the analyst can explain their documented effect.

## Runtime and data flow

The Astro page remains static. A small React island provides the question composer and briefing display. It posts to a same-origin Pages Function, which:

1. validates method, origin, content type, body size, race identifier, and request schema;
2. retrieves the race registry and current forecast through the Pages static-asset binding;
3. validates the fields used by the analyst;
4. optionally maps the reader's wording into a closed intent while deterministic code separately parses explicit scenario quantities;
5. computes the deterministic answer;
6. for a supported and safe answer, derives and sends an allowlisted qualitative projection to the configured analysis model while retaining the full packet locally;
7. accepts only strict explanatory output whose section-specific evidence identifiers exist in that packet and whose prose contains no new numbers, links, advocacy, certainty claims, or known candidate/component direction inversion; and
8. falls back to the deterministic briefing on any timeout, refusal, schema error, failed postcondition, or unsafe request.

HTTP responses are marked `no-store`, and Consequential Races stores no conversation history or question/response text. `X-Analyst-Generation` reports the final accepted response mode, not whether a routing call was attempted; `X-Analyst-Synthesis` reports the low-cardinality explanation state without including reader text or generated prose. When model assistance is enabled, OpenAI processes the question in the routing call. The explanation call instead receives the qualitative projection and the same pseudonymous safety identifier derived from the request IP address and user agent; the raw IP address is not included in either OpenAI request. Default abuse-monitoring logs may retain API inputs and outputs for up to 30 days, or longer when required by law or reasonably necessary to protect services or third parties. Approved Zero Data Retention or Modified Abuse Monitoring can exclude customer content from those logs, subject to OpenAI's documented exceptions. The interface discloses that processing before submission and tells readers not to include personal information. The browser may fetch the public forecast JSON and compute the same deterministic answer if the Function is unavailable.

Interactive reader routing and explanation remain foreground Responses requests in the Pages Function. Publication-summary generation exists only in the trusted operator command: it sends no raw reader question or exact published values, creates exactly one Response with `background: true` and `store: false`, and performs bounded polling only against the server-issued response identifier. It never automatically retries the creation POST and attempts best-effort cancellation on failure or deadline. The public endpoint rejects the shared schema's offline-only `summarize_race` value before loading assets or acquiring an upstream budget.

## Security and privacy gate

Model-assisted production responses require all of the following:

- `OPENAI_API_KEY` stored as a production-only Cloudflare secret;
- an explicit production feature flag and evaluated model identifier, using an exact snapshot when one is available;
- operator-verified spending and request/token controls at the account, organization, or project scope, recorded outside this repository;
- a canonical-origin gate and a verified Cloudflare zone rate limit on the ask endpoint;
- bounded request and response sizes and bounded foreground timeouts;
- `store: false` on the model request;
- no application logging of question text, response text, IP addresses, or model credentials; and
- evaluation coverage for grounding, numeric fidelity, prompt injection, non-persuasion, refusals, accessibility, and deterministic fallback.

The separate operator briefing gate requires `ANALYST_TAKE_OPENAI_API_KEY` to exist only in the trusted capture process or its permission-restricted, Git-ignored local `.analyst-take.env`, a fixed upstream destination and evaluated summary model, exactly one summary creation POST with no automatic create retry, bounded polling and best-effort cancellation, quarantine of every completed response before semantic validation, an exclusively created unreviewed draft only after acceptance, automated screening of required facts and known failure modes, and explicit owner authorization for internal use before any static content changes. Quarantine artifacts are operator-only, structurally unpromotable, and excluded from logs, edge responses, static content, and release archives.

The routing configuration is `gpt-5.6-luna` with low reasoning. The grounded explanation configuration is `gpt-5.6-sol` with high reasoning. These configured names are explicit aliases, not immutable dated pins; each alias/prompt/schema combination must pass evaluation, and a dated snapshot should replace it when OpenAI makes an appropriate snapshot available and that snapshot passes the same gate. The routing model is a bounded classifier; the analysis model can improve synthesis but receives no authority over the forecast or its quantitative claims. Because the encrypted key is production-only, activation uses a guarded canary: the paced live suite must pass twice, and any failure restores deterministic-only output.

Preview deployments receive neither production secret. If the production gate is incomplete, the endpoint returns the deterministic briefing and makes no metered upstream call.

`store: false` prevents longer-lived Responses API application-state retention; it does not disable abuse-monitoring logs, every form of prompt caching, or by itself establish Zero Data Retention or Modified Abuse Monitoring. OpenAI documents that background response data, including operator background requests from Zero Data Retention projects, is temporarily written to disk for roughly 10 minutes to support asynchronous execution and polling and then deleted when `store` is false. The publication workflow follows [OpenAI's published API data controls](https://developers.openai.com/api/docs/guides/your-data) and [OpenAI's background-mode guide](https://developers.openai.com/api/docs/guides/background), and must update this disclosure if the API project's retention status changes.

## Why no database

Questions are independent and operate on one immutable public artifact. The system stores no accounts, chat history, personalization, or mutable reader state. Static JSON plus the Pages asset binding therefore remains the right source of truth. Reconsider storage only if a documented requirement for durable abuse state, evaluation samples with an approved privacy policy, editorial review queues, or cross-session conversations appears.

## Consequences

- Readers get a concise, scenario-centered entry point while the detailed page remains the audit trail.
- The language model can recognize natural phrasings and write a more useful synthesis without becoming an untracked forecasting model.
- Supported scenarios must be implemented and tested deliberately; open-ended speculation is not a fallback.
- The published artifact contract becomes part of the analyst’s public API and should evolve compatibly across races. Each new race must register reviewed qualitative hints for every fact ID it can publish and pass a race-specific canary before synthesis is enabled.
- A model API outage reduces phrasing flexibility and explanatory polish, not access to the forecast, deterministic briefing, or scenario arithmetic.
- A model API outage does not make a stale persistent take look current; publication uses an explicit current-run unavailable record and deterministic fallback instead.
- Qualitative entailment cannot be proven by lexical postconditions alone. The deterministic thesis and facts are authoritative, adversarial checks reject known inversions, and each model/prompt combination requires representative canary evaluation; each persistent briefing additionally requires explicit owner acceptance of the remaining AI risk for internal use.

## Revisit

Review this decision after the analyst has operated through two materially new polling releases and after a second race is added. Expansion requires evidence from evaluations and real reader questions, not a broader prompt alone.
