Skip to content
docs
Arxo ↗

Handle results: every shape gets a rendering

For LLMs9 sections

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.

  • 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|unknown
    src/to-view.ts toViewModel

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:

LevelQuestion it answersExamplesHandled by
Call errorDid the call return an answer at all?FactError, PackageNotFoundthe call site, as a failure of the application
evaluationStatusHow did the computation finish?COMPUTED, MISSING_INPUTthe readers: anything but COMPUTED is not-computed
truthStatus or computed valuesWhat does the answer say for this kind of query?TRUE_ONLY, NEITHER; ["2026-03-20"]the readers: claim or value
AppResultHow will the application present it?value, claim, not-computed, unknownthe view: toViewModel

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:

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.

The collect reader keeps every element and reports anything unreadable as unknown instead of guessing:

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:

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 };
}

The view layer renders the four kinds, and only the four kinds:

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:

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:

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.

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.

  • 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.

Run the reader tests, including the conflict and multi-value rows:

Terminal
node --import tsx --test test/evaluate.test.ts
Output
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.

Documentation for Arxo. Writings — blog.arxo.io.

Anonymous visit counts on stats.arxo.io, no cookies.