# 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. ```text docs/build/examples/deadline-app ``` ## 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` 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. ```text 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. ## 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 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. ## 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: ```text 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: ```text 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. ## 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 The questionnaire runs on the missing-facts module; its tests pin the follow-up behavior: ```bash node --import tsx --test test/missing.test.ts ``` ```text 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.