# Handle results: every shape gets a rendering The engine's answer has to reach the screen without gaining certainty on the way. This chapter turns raw answers into application results and renders each one, including the answers that settle nothing. ## At a glance - **Goal:** turn raw engine answers into application results with exactly four kinds — computed value, checked claim, not computed, unknown — and render each one without inventing certainty the answer does not carry. - **You need:** [Calculate and check](/build/calculate-and-verify/): this chapter consumes the answers collect and truth return. - **Run:** ```bash node --import tsx --test test/evaluate.test.ts ``` - **Files:** the complete handlers live in these two files in the archive; this chapter quotes the decisive branches. ```text src/read-result.ts AppResult kinds value|claim|not-computed|unknown src/to-view.ts toViewModel ``` ## What the user sees For the example case the view model reads: ```json { "headline": "The period ends on 2026-03-20", "detail": "Computed from the event date and the duration by the pinned canon." } ``` For the wrong candidate it reads: ```json { "headline": "Not established either way", "detail": "The engine neither established nor refuted the proposed date; it does not guess." } ``` Between the engine and those two lines, an answer passes four levels. Each answers its own question, and the application handles each in its own place: | Level | Question it answers | Examples | Handled by | |---|---|---|---| | Call error | Did the call return an answer at all? | `FactError`, `PackageNotFound` | the call site, as a failure of the application | | `evaluationStatus` | How did the computation finish? | `COMPUTED`, `MISSING_INPUT` | the readers: anything but `COMPUTED` is `not-computed` | | `truthStatus` or computed values | What does the answer say for this kind of query? | `TRUE_ONLY`, `NEITHER`; `["2026-03-20"]` | the readers: `claim` or `value` | | `AppResult` | How will the application present it? | `value`, `claim`, `not-computed`, `unknown` | the view: `toViewModel` | ## Check execution first A thrown call error — `FactError`, `PackageNotFound`, a type refusal — never reaches the readers: it is caught at the call site and shown as a failure of the application, not as an answer. Everything that does come back is mapped onto the application's four kinds. The levels stay apart: a thrown error, a non-computed evaluation, a truth status, and a value are four different things: ```ts // src/read-result.ts export type ClaimStatus = 'TRUE_ONLY' | 'FALSE_ONLY' | 'BOTH' | 'NEITHER'; export type AppResult = | { kind: 'value'; values: string[] } | { kind: 'claim'; status: ClaimStatus } | { kind: 'not-computed'; evaluationStatus: string } | { kind: 'unknown'; detail: string }; ``` Each reader checks `evaluationStatus` before anything else. It trusts the status field, not the presence of a value: a value beside a non-`COMPUTED` status is not an answer, so the result is `not-computed`. ## Read a claim or a value The collect reader keeps every element and reports anything unreadable as `unknown` instead of guessing: ```ts // src/read-result.ts // Calculate mode: the engine fills in every end date it establishes. export function readCollect(answer: Answer): AppResult { const raw = rawEvaluationStatus(answer); if (typeof raw !== 'string' || !KNOWN_EVALUATION.has(raw)) { return { kind: 'unknown', detail: `unrecognized evaluationStatus ${describeRaw(raw)}` }; } const status = raw; if (status !== 'COMPUTED') { return { kind: 'not-computed', evaluationStatus: status }; } const value = (answer as { value?: unknown }).value; if (!Array.isArray(value)) { return { kind: 'unknown', detail: 'COMPUTED collect without an array value' }; } const values: string[] = []; for (const element of value) { const text = elementText(element); if (text === null) { return { kind: 'unknown', detail: `unreadable collect element ${JSON.stringify(element)}` }; } values.push(text); } return { kind: 'value', values }; } ``` The truth reader passes the four-valued status through untouched — `NEITHER` is never mapped to false: ```ts // src/read-result.ts // Verify mode: the claim's four-valued status, passed through untouched. export function readTruth(answer: Answer): AppResult { const rawStatus = rawEvaluationStatus(answer); if (typeof rawStatus !== 'string' || !KNOWN_EVALUATION.has(rawStatus)) { return { kind: 'unknown', detail: `unrecognized evaluationStatus ${describeRaw(rawStatus)}` }; } if (rawStatus !== 'COMPUTED') { return { kind: 'not-computed', evaluationStatus: rawStatus }; } const raw = (answer as { truthStatus?: unknown }).truthStatus; if (typeof raw !== 'string' || !KNOWN_TRUTH.has(raw)) { return { kind: 'unknown', detail: `unrecognized truthStatus ${describeRaw(raw)}` }; } return { kind: 'claim', status: raw as ClaimStatus }; } ``` ## Show each kind on screen The view layer renders the four kinds, and only the four kinds: ```ts // src/to-view.ts export interface ViewModel { headline: string; detail: string; rulesApplied: string[]; sourcesNote: string; hashes: { program?: string; semantic?: string; result?: string }; limitations: string[]; } ``` Multiple collect values mean the question as asked has more than one answer; picking the first would silently choose for the user. So several established dates render every one of them, with a limitation note that the question had more than one answer: ```ts // src/to-view.ts return { headline: `The period ends on one of ${result.values.length} dates`, detail: `Established end dates: ${result.values.join(', ')}.`, rulesApplied, sourcesNote, hashes, limitations: ['Several dates were established; the app shows all of them.'], }; ``` `NEITHER` renders as not established either way, with the explicit warning that missing facts may still settle the question: ```ts // src/to-view.ts return { headline: 'Not established either way', detail: 'The engine neither established nor refuted the proposed date; it does not guess.', rulesApplied, sourcesNote, hashes, limitations: ['NEITHER is not a "no" — missing facts may still settle the question.'], }; ``` `BOTH` means the computation supports the claim and its negation at once — a genuine conflict the application must surface, never average away. The raw answer is kept apart from the view. `read-result` owns the mapping, `to-view` owns the words, and the document bytes stay untouched for chapter 7. The view never sees a status string; it sees an `AppResult`. ## Render unsupported answer shapes The `unknown` branch is deliberate: a status the application was not written for renders as not understood, never as a guess. New engine statuses must break the view loudly, not pass through as established. Both readers check the raw type first — `typeof raw === 'string'` before set membership — and never stringify an unknown value into a known status. `['TRUE_ONLY']` stringifies to `'TRUE_ONLY'`, but it is not the string `'TRUE_ONLY'`, so it reads as `unknown`: a malformed shape stays malformed even when its text spells a familiar word. The tests feed the readers every shape: values, claims, conflicts, empty and multiple collects, missing evaluations, unknown statuses, and malformed status shapes (arrays, nested arrays, objects, `null`, future strings) that must never coerce into a known status. Shapes the live canon does not produce (a conflict, an empty collect, a malformed status) are pinned with hand-built answers, labeled `SYNTHETIC FIXTURE` in the test file. ## Limits and errors - `not-computed` is not `unknown`: the first says the engine declined (missing reading, undecided court question), the second says the application cannot safely read what came back. They route to different follow-ups — supply the missing input, or review the app. - `NEITHER` renders as "Not established either way", never as "no". The words on the screen must not claim a negation the computation did not derive. - The reader trusts the `evaluationStatus` field, not the presence of a value. A value beside a non-`COMPUTED` status is not an answer. ## Check your understanding Run the reader tests, including the conflict and multi-value rows: ```bash node --import tsx --test test/evaluate.test.ts ``` ```text ok 7 - an unrecognized evaluationStatus reads as unknown ok 8 - a non-string truthStatus never coerces to a claim ok 9 - a non-string evaluationStatus never coerces to a status ok 10 - a multi-valued collect is never truncated ok 11 - every claim status renders its own headline ok 12 - an empty collect renders as no date, never as a blank ok 13 - not-computed and unknown render without derived claims # tests 13 # pass 13 # fail 0 ``` Before reading each answer, predict it.
A collect answer arrives with two dates, 2026-03-20 and 2026-03-23. What does the user see? Both dates. `readCollect` keeps every element, and the view shows the headline "The period ends on one of 2 dates" with both dates listed and a limitation note. It never picks the first one.
An answer has evaluationStatus MISSING_INPUT. Which AppResult kind does the reader return, and is it the same as unknown? `not-computed`, and it renders as "Not computed: MISSING_INPUT". It is not `unknown`: the engine declined to compute, which the application understands. `unknown` is reserved for answers the application cannot safely read, such as a status it was not written for.
A malformed answer carries truthStatus as the array ['TRUE_ONLY']. Does the user see "Yes"? No. The reader checks that the raw value is a string before checking the set of known statuses, so the array reads as `unknown` and renders as "Answer not understood", even though its text spells a known status.
## Next - [Collect missing facts](/build/collect-missing-facts/) - [How to read an answer](/guide/reading-an-answer/) - [Results and errors](/build/sdk/results-and-errors/) for every status, value shape, and error class