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
Section titled “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: this chapter consumes the answers collect and truth return.
-
Run:
Terminal 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.
Output src/read-result.ts AppResult kinds value|claim|not-computed|unknownsrc/to-view.ts toViewModel
What the user sees
Section titled “What the user sees”For the example case the view model reads:
{ "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:
{ "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
Section titled “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:
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
Section titled “Read a claim or a value”The collect reader keeps every element and reports anything unreadable
as unknown instead of guessing:
// 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:
// 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
Section titled “Show each kind on screen”The view layer renders the four kinds, and only the four kinds:
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:
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:
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
Section titled “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
Section titled “Limits and errors”not-computedis notunknown: 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.NEITHERrenders 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
evaluationStatusfield, not the presence of a value. A value beside a non-COMPUTEDstatus is not an answer.
Check your understanding
Section titled “Check your understanding”Run the reader tests, including the conflict and multi-value rows:
node --import tsx --test test/evaluate.test.tsok 7 - an unrecognized evaluationStatus reads as unknownok 8 - a non-string truthStatus never coerces to a claimok 9 - a non-string evaluationStatus never coerces to a statusok 10 - a multi-valued collect is never truncatedok 11 - every claim status renders its own headlineok 12 - an empty collect renders as no date, never as a blankok 13 - not-computed and unknown render without derived claims# tests 13# pass 13# fail 0Before 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.
- Collect missing facts
- How to read an answer
- Results and errors for every status, value shape, and error class
Documentation for Arxo. Writings — blog.arxo.io.
Anonymous visit counts on stats.arxo.io, no cookies.