Skip to content
docs
Arxo ↗

Questionnaire: fields to facts, then follow-ups

For LLMs6 sections

A questionnaire collects facts step by step instead of all at once. This page shows the pattern on the deadline example: a hand-written map from form fields to canon facts, follow-up questions derived from whyNot blockers through missingFacts, and labels taken from the canon manifest at author time.

Output
docs/build/examples/deadline-app

Two form fields become two canon facts; the other two fields feed the query and the case context, not the fact set:

Form fieldBecomesRole
eventDatefrist_ereignis(frist, 2026-03-06)the event that starts the period
durationDaysfrist_dauer_tage(frist, 14 calendar_day)the length of the period in days
candidateEndthe truth query argumentthe date to check, 2026-03-20
legalTimethe case contextthe date the law is projected on, 2026-09-17

The map lives in toFacts (facts) and toCaseInput (context), stamped with ADAPTER_VERSION 1.0.0. It is written by hand for this canon: the application does not derive it from the canon at run time, and it does not guess.

A half-filled form is a different contract from a complete one, so the follow-up procedure never evaluates it. It explains the draft with explainDraft (whyNot over toDraftCaseInput, which emits only the facts actually known), then turns that non-answer into questions:

  1. Validate what the user gave so far as a DraftInput: absent fields stay absent, present fields are checked strictly.
  2. Call explainDraft for the question: on the worked draft (event known, duration missing) it returns 2 blockers, naming the premises the candidate rules were waiting for.
  3. Pass the answer plus the facts actually sent to missingFacts in src/missing.ts: missingFacts(answer, sent, caseId). It compares full predicate URNs against the ASKABLE allowlist, checks the object identity, and reports one flat ask list — plus a verdict and an alternatives flag.
  4. If verdict is ask, render one question per missing fact, collect the answers, and explain again. If the routes disagree (alternatives), present the routes as a choice — never merge two branches into one combined requirement list.
Output
draft -> explainDraft returns 2 blockers (NEITHER, not a verdict)
-> missingFacts(answer, sent, caseId) against ASKABLE
-> verdict ask, no alternatives: one question per missing fact
-> verdict ask with alternatives: a choice between routes
-> verdict none-askable: no further question — stop asking

The procedure never invents a value and never reads NEITHER as “no”: a missing premise is a question for the user.

Each iteration has three possible outcomes:

  • Ask the next question. A next askable question exists (verdict is ask, no alternatives): render it, collect the answer, and explain again.
  • Offer a route choice. The routes disagree (alternatives): the interface presents them as a choice, then continues on the user’s pick.
  • Finish. No further askable question exists (none-askable): the questionnaire stops and the application shows whatever state resulted, including NEITHER on a complete form, a conflict, or a case for outside review.

Completeness of the form and decidability of the answer are different properties: all four fields filled in says nothing about whether the model establishes the claim. A none-askable verdict means the form cannot supply what is missing — not that no factual gap exists — and chasing a TRUE_ONLY past that point would mean demanding certainty the model does not establish.

The canon manifest (questions()) lists the questions a canon answers, the facts each question reads, and the labels and widgets the author wrote for them. The questionnaire copies that text into the form when the form is written — not on every request:

  • labels name the fact in the user’s words (the canon’s own label for frist_ereignis, the canon’s own label for frist_dauer_tage);
  • widgets say what shape the answer takes (a date, a day count);
  • relevantFacts says which facts a question needs, so the form knows what “complete” means before the first evaluation.

Because the copy happens at author time, the running form needs no manifest call: the page reads its four HTML controls (all strings at the DOM level), normalizes durationDays to a JSON number, and posts the four-field request validateFormInput checks. When the canon or its labels move, the author re-reads the manifest and updates the form and the hand map together.

On the worked case the questionnaire needs no follow-up: both facts are present, collect computes 2026-03-20 with COMPUTED, and truth answers TRUE_ONLY. The follow-up path shows when the duration is left out:

Output
given: eventDate 2026-03-06, no durationDays
truth: frist_ende(frist, 2026-03-20) -> NEITHER
whyNot: 2 blockers
ask: the missing duration premise
given: durationDays 14
collect: COMPUTED 2026-03-20
truth: TRUE_ONLY

The terminal case matters more than the happy path: full input with a wrong candidate stops the loop instead of asking forever:

Output
given: eventDate 2026-03-06, durationDays 14 (all fields filled)
truth: frist_ende(frist, 2026-03-21) -> NEITHER
whyNot: every need already satisfied by the sent facts
verdict: none-askable, no alternatives — stop, show NEITHER

Every step reports via as local; sources stays empty in this canon build; each evaluation carries its own program, semantic, and result hashes with canonical document bytes.

  • No automatic form from an arbitrary canon in Python. Python has no questions, so the Python path cannot read the manifest itself: it reuses the labels and widget choices the TypeScript side copied at author time.
  • No merging of alternative rule branches. When the blockers name premises from different candidate rules, the flat ask list stays empty and the report raises alternatives: the caller presents the routes as a choice instead of asking a merged list. The next explanation, after the user’s pick, decides which rule fires.
  • The hand map is per canon. A new canon means a new map, new labels, and new adapter cases — the procedure stays, the content changes.

The questionnaire runs on the missing-facts module; its tests pin the follow-up behavior:

Terminal
node --import tsx --test test/missing.test.ts
Output
ok 1 - a draft with only the event asks for durationDays
ok 4 - a complete draft asks nothing — but that is not "no factual gap"
ok 5 - a complete case with a wrong candidate ends the questionnaire on NEITHER
ok 10 - synthetic: alternative routes stay a choice, not a combined list
# tests 14
# pass 14
# fail 0

Predict before you open each answer.

The user gave the event date only. Which outcome does the next iteration have?

Ask the next question: the verdict is ask with no alternatives, and the only question is durationDays. The computed frist_ende_kalender is reported as not askable and never shown as a question.

All four fields are filled and the candidate is 2026-03-21. Which outcome?

Finish. Every askable need is already satisfied, so the verdict is none-askable with no alternatives. The application shows NEITHER instead of asking forever for a TRUE_ONLY the model does not establish.

Why does the Python path reuse labels from the TypeScript side?

Python has no questions, so it cannot read the canon manifest itself. It reuses the labels and widget choices the TypeScript side copied at author time.

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

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