# 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