Questionnaire: fields to facts, then follow-ups
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.
docs/build/examples/deadline-appThe hand map
Section titled “The hand map”Two form fields become two canon facts; the other two fields feed the query and the case context, not the fact set:
| Form field | Becomes | Role |
|---|---|---|
eventDate | frist_ereignis(frist, 2026-03-06) | the event that starts the period |
durationDays | frist_dauer_tage(frist, 14 calendar_day) | the length of the period in days |
candidateEnd | the truth query argument | the date to check, 2026-03-20 |
legalTime | the case context | the 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.
Follow-ups from whyNot
Section titled “Follow-ups from whyNot”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:
- Validate what the user gave so far as a
DraftInput: absent fields stay absent, present fields are checked strictly. - Call
explainDraftfor the question: on the worked draft (event known, duration missing) it returns 2 blockers, naming the premises the candidate rules were waiting for. - Pass the answer plus the facts actually sent to
missingFactsinsrc/missing.ts:missingFacts(answer, sent, caseId). It compares full predicate URNs against theASKABLEallowlist, checks the object identity, and reports one flat ask list — plus averdictand analternativesflag. - If
verdictisask, 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.
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 askingThe 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
(
verdictisask, 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, includingNEITHERon 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.
Labels from the manifest at author time
Section titled “Labels from the manifest at author time”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 forfrist_dauer_tage); - widgets say what shape the answer takes (a date, a day count);
relevantFactssays 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.
Worked transcript
Section titled “Worked transcript”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:
given: eventDate 2026-03-06, no durationDaystruth: frist_ende(frist, 2026-03-20) -> NEITHERwhyNot: 2 blockersask: the missing duration premisegiven: durationDays 14collect: COMPUTED 2026-03-20truth: TRUE_ONLYThe terminal case matters more than the happy path: full input with a wrong candidate stops the loop instead of asking forever:
given: eventDate 2026-03-06, durationDays 14 (all fields filled)truth: frist_ende(frist, 2026-03-21) -> NEITHERwhyNot: every need already satisfied by the sent factsverdict: none-askable, no alternatives — stop, show NEITHEREvery 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.
Limits
Section titled “Limits”- 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.
Check your understanding
Section titled “Check your understanding”The questionnaire runs on the missing-facts module; its tests pin the follow-up behavior:
node --import tsx --test test/missing.test.tsok 1 - a draft with only the event asks for durationDaysok 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 NEITHERok 10 - synthetic: alternative routes stay a choice, not a combined list# tests 14# pass 14# fail 0Predict 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.