Markdown for LLMs
Questionnaire: fields to facts, then follow-ups
The source Markdown for this article. Copy it into your assistant or download it as a text file.
# 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.
<details>
<summary>The user gave the event date only. Which outcome does the next iteration have?</summary>
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.
</details>
<details>
<summary>All four fields are filled and the candidate is 2026-03-21. Which outcome?</summary>
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.
</details>
<details>
<summary>Why does the Python path reuse labels from the TypeScript side?</summary>
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.
</details>