# Collect missing facts: ask for exactly what the rules need ## At a glance - **Goal:** when facts are missing, ask the user for precisely the missing fields — and nothing else. Whatever the rules need beyond the form is reported with a reason, never asked, never silently dropped. - **You need:** [Handle results](/build/handle-results/) — a partial run answers `NEITHER`, which this chapter treats as the start of a question, not as an outcome; [Bind input](/build/bind-input/) supplies the draft contract. - **Run:** ```bash node --import tsx --test test/missing.test.ts ``` - **Files:** ```text src/runtime.ts explainWhy, explainDraft src/missing.ts ASKABLE allowlist, missingFacts src/cli.ts the explain command (draft over flags) ``` ## Ask for what a draft is missing A user has typed the event date but not the duration yet. Explain that draft against the proposed end date: ```bash node --import tsx src/cli.ts explain --candidate 2026-03-20 --event 2026-03-06 ``` ```text ask: durationDays — the length of the period in days, e.g. 14 cannot ask (not-askable): urn:de:corpus:clir:bgb-fristen#frist_ende_kalender — no form field supplies this predicate; it may be model-internal ``` The application asks one question — the duration — and names one thing it will not ask, with the reason. With nothing known, both fields are asked: ```bash node --import tsx src/cli.ts explain --candidate 2026-03-20 ``` ```text ask: eventDate — the event that starts the period, e.g. "2026-03-06" ask: durationDays — the length of the period in days, e.g. 14 cannot ask (not-askable): urn:de:corpus:clir:bgb-fristen#frist_ende_kalender — no form field supplies this predicate; it may be model-internal ``` The four cases the tests pin: ```text draft with event only ..... ask durationDays empty draft ............... ask eventDate, durationDays complete draft ............ ask nothing (verdict none-askable) computed intermediate ..... reported not-askable, never asked ``` The user answers `14`, the adapter rebuilds the case, and the evaluation from chapter 3 answers `COMPUTED` / `TRUE_ONLY` with its own fresh hashes. ## Explain a draft instead of evaluating it A draft is explained, never evaluated. Two runtime functions do this; both call the engine's `whyNot` question on the claim "the period ends on the candidate date": ```ts // src/runtime.ts // Explain mode: when the answer is not established, name the blockers. export async function explainWhy(input: FormInput, candidate: string): Promise { const model = await openModel(); return model.whyNot('frist_ende', ['frist', candidate], toCaseInput(input)); } // Draft mode: a partial draft can be explained — which questions are // still open? — but never evaluated as a complete case. export async function explainDraft(draft: DraftInput, candidate: string): Promise { const model = await openModel(); return model.whyNot('frist_ende', ['frist', candidate], toDraftCaseInput(draft)); } ``` `explainWhy` takes a complete form; `explainDraft` takes a draft and sends only the facts the draft actually knows — an absent field stays absent, it is never defaulted. The `explain` command uses `explainDraft`. Both run locally, like the evaluation calls. ## Read the engine's report `whyNot` answers with a list of **blockers**. A blocker is one rule that could establish the claim, together with the state of each of its conditions. For the draft with only the event, the report holds two blockers. Trimmed to the fields this chapter uses, it reads: ```json [ { "rule": "urn:de:corpus:clir:bgb-fristen#TagesfristEnde", "trigger": "UNDETERMINED", "conjuncts": [ { "trigger": "UNDETERMINED", "literal": { "predicate": "urn:de:corpus:clir:bgb-fristen#frist_ereignis", "args": [{ "kind": "entity_ref", "id": "urn:de:corpus:clir:bgb-fristen:entity:frist" }, { "kind": "var", "var": "v1" }] } }, { "trigger": "UNDETERMINED", "literal": { "predicate": "urn:de:corpus:clir:bgb-fristen#frist_dauer_tage", "args": [{ "kind": "entity_ref", "id": "urn:de:corpus:clir:bgb-fristen:entity:frist" }, { "kind": "var", "var": "v2" }] } } ] }, { "rule": "urn:de:corpus:clir:bgb-fristen#Verlegung193", "trigger": "UNDETERMINED", "conjuncts": [ { "trigger": "UNDETERMINED", "literal": { "predicate": "urn:de:corpus:clir:bgb-fristen#frist_ende_kalender", "args": [{ "kind": "entity_ref", "id": "urn:de:corpus:clir:bgb-fristen:entity:frist" }, { "kind": "var", "var": "v1" }] } } ] } ] ``` A blocker is not an error and not a verdict. The report lists the routes to the claim even when the claim is already established: on the complete draft `Verlegung193` is still reported, waiting on `frist_ende_kalender`, while `TagesfristEnde` already establishes the date. A complete case asks nothing because every askable need is already satisfied, not because the blockers disappear. ## Route, need, askable Three words carry the rest of the chapter: - **Route** — one way to reach the conclusion. Each blocker is one route: here `TagesfristEnde` (count the days) and `Verlegung193` (move an end that falls on a weekend or holiday). Satisfying one route is enough. - **Need** — a condition that route requires: one conjunct whose trigger is `UNDETERMINED`. `TagesfristEnde` needs `frist_ereignis` and `frist_dauer_tage`; `Verlegung193` needs `frist_ende_kalender`. - **Askable** — a need the application has an input field for. This form has two: the event date and the duration. ## Walk one blocker to one question Follow the draft with only the event through the report above: 1. **Route `TagesfristEnde`, need `frist_ereignis`.** The engine marks it `UNDETERMINED` because the question does not fix its value, but the application sent `frist_ereignis(frist, 2026-03-06)`. The need is about the case object `frist` and the sent fact covers it, so it counts as **known**. 2. **Route `TagesfristEnde`, need `frist_dauer_tage`.** No sent fact covers it. The allowlist has an entry for this exact predicate, pointing to the field `durationDays`, and the value position is a free variable (`v2`). It becomes a question: `ask: durationDays — the length of the period in days, e.g. 14`. 3. **Route `Verlegung193`, need `frist_ende_kalender`.** No sent fact covers it, and no field supplies it: it is a computed intermediate the rules derive once the inputs exist. It is reported as `not-askable` and never asked. Only one route has askable needs, so the flat ask list is `[durationDays]` and the verdict is `ask`. ## The askable allowlist The adapter asks only what the form can supply. `ASKABLE` is keyed by the engine's full predicate URNs, so a same-named predicate from another namespace never matches: ```ts // src/missing.ts // The only questions this form is allowed to ask. Full URNs were read // off real whyNot output of de.bgb.fristen@0.1.0, not guessed. export const ASKABLE: AskableField[] = [ { predicate: 'urn:de:corpus:clir:bgb-fristen#frist_ereignis', field: 'eventDate', hint: 'the event that starts the period, e.g. "2026-03-06"', }, { predicate: 'urn:de:corpus:clir:bgb-fristen#frist_dauer_tage', field: 'durationDays', hint: 'the length of the period in days, e.g. 14', }, ]; ``` ## Four checks for each need `missingFacts(answer, sent, caseId)` takes each need through four checks in order. A need about another object is `other-object` — a fact about this case never satisfies it. A need the sent facts already cover is known. A need no field supplies is `not-askable`. A need whose value position is fixed is `constrained-value`: the form cannot faithfully ask for one specific value as if it were open input. Only what passes all four becomes a question. The result is a report: ```ts // src/missing.ts export interface MissingReport { // The flat ask list. Non-empty only when exactly one route has // askable needs; otherwise the caller reads report.rules instead. missing: MissingNeed[]; rules: RuleNeeds[]; // Several routes need different askable facts: present a choice. alternatives: boolean; // 'ask' means the form can still supply something; 'none-askable' // means it cannot — NOT that no factual gap exists. verdict: 'ask' | 'none-askable'; // Literals skipped on an unrecognized trigger. Never silent. ignored: IgnoredLiteral[]; } ``` ## Alternative routes, other objects, unknown triggers When several routes need different askable facts, the flat list stays empty, `alternatives` is `true`, and the caller presents the per-route asks as a choice — alternatives never merge into one combined requirement list. For this canon the second route needs only the computed `frist_ende_kalender`, so the choice never occurs on the real model; a hand-built test pins the shape. The same tests pin the rarer cases with hand-built blockers: a need about another object is reported as `other-object`; a predicate from a foreign namespace never matches the allowlist; a conjunct on an unrecognized trigger is recorded in `ignored`, never silently skipped, so a future engine report cannot shrink the ask list without leaving a trace. ## Limits and errors - The adapter must not ask for computed intermediates. Asking the user for `frist_ende_kalender` would request a value the rules derive — the form would be asking for the answer it is supposed to compute. - The decision to keep asking is read from `verdict`, never from the length of `missing`. An empty ask list beside `NEITHER` with `verdict: "none-askable"` means the form has no further question — not that no factual gap exists: the candidate may be wrong, or the gap may sit outside what this form can supply, and that routes to review. But an empty `missing` with `alternatives: true` and `verdict: "ask"` means the opposite — questions exist on several routes, and the interface must show them per route instead of stopping. ## Check your understanding Run the missing-facts tests and confirm the ask lists: ```bash node --import tsx --test test/missing.test.ts ``` ```text ok 1 - a draft with only the event asks for durationDays ok 2 - an empty draft asks for both fields ok 3 - a draft with only the duration asks for eventDate 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 6 - the computed intermediate is unaskable, never a question # tests 14 # pass 14 # fail 0 ``` Predict before you open each answer.
The draft has only the duration (`--days 14`). What does `explain` ask? It asks for `eventDate` only. The duration is covered by the sent fact, so the `TagesfristEnde` route still needs just the event. `frist_ende_kalender` is still reported as `not-askable`.
All fields are filled, but the candidate is 2026-03-21. Does the questionnaire keep asking? No. The truth answer is `NEITHER`, and the report has verdict `none-askable` with no alternatives: every askable need is already covered by the sent facts. The questionnaire stops and shows `NEITHER`; the problem is the candidate, not a missing field.
`frist_ende_kalender` is UNDETERMINED in the report. Why is it never asked? No entry in `ASKABLE` points to it, so it fails the `not-askable` check. It is a computed intermediate: the rules derive it from the event and the duration, and asking the user for it would mean asking for the answer.
## Next - [Show grounds: proof, sources, hashes](/build/show-grounds/) - [Connect an AI assistant](/guide/mcp/) for the same inventory over MCP - [Query types](/build/sdk/query-types/): the `why_not` row explains the blocker report