docs← Back to article

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.

Download this articlePlain text ↗
# 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>