Collect missing facts: ask for exactly what the rules need
At a glance
Section titled “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 — a partial run answers
NEITHER, which this chapter treats as the start of a question, not as an outcome; Bind input supplies the draft contract. -
Run:
Terminal node --import tsx --test test/missing.test.ts -
Files:
Output src/runtime.ts explainWhy, explainDraftsrc/missing.ts ASKABLE allowlist, missingFactssrc/cli.ts the explain command (draft over flags)
Ask for what a draft is missing
Section titled “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:
node --import tsx src/cli.ts explain --candidate 2026-03-20 --event 2026-03-06ask: durationDays — the length of the period in days, e.g. 14cannot ask (not-askable): urn:de:corpus:clir:bgb-fristen#frist_ende_kalender — no form field supplies this predicate; it may be model-internalThe application asks one question — the duration — and names one thing it will not ask, with the reason. With nothing known, both fields are asked:
node --import tsx src/cli.ts explain --candidate 2026-03-20ask: eventDate — the event that starts the period, e.g. "2026-03-06"ask: durationDays — the length of the period in days, e.g. 14cannot ask (not-askable): urn:de:corpus:clir:bgb-fristen#frist_ende_kalender — no form field supplies this predicate; it may be model-internalThe four cases the tests pin:
draft with event only ..... ask durationDaysempty draft ............... ask eventDate, durationDayscomplete draft ............ ask nothing (verdict none-askable)computed intermediate ..... reported not-askable, never askedThe 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
Section titled “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”:
// Explain mode: when the answer is not established, name the blockers.export async function explainWhy(input: FormInput, candidate: string): Promise<Answer> { 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<Answer> { 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
Section titled “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:
[ { "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
Section titled “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) andVerlegung193(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.TagesfristEndeneedsfrist_ereignisandfrist_dauer_tage;Verlegung193needsfrist_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
Section titled “Walk one blocker to one question”Follow the draft with only the event through the report above:
- Route
TagesfristEnde, needfrist_ereignis. The engine marks itUNDETERMINEDbecause the question does not fix its value, but the application sentfrist_ereignis(frist, 2026-03-06). The need is about the case objectfristand the sent fact covers it, so it counts as known. - Route
TagesfristEnde, needfrist_dauer_tage. No sent fact covers it. The allowlist has an entry for this exact predicate, pointing to the fielddurationDays, 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. - Route
Verlegung193, needfrist_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 asnot-askableand never asked.
Only one route has askable needs, so the flat ask list is
[durationDays] and the verdict is ask.
The askable allowlist
Section titled “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:
// 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
Section titled “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:
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
Section titled “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
Section titled “Limits and errors”- The adapter must not ask for computed intermediates. Asking the user
for
frist_ende_kalenderwould 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 ofmissing. An empty ask list besideNEITHERwithverdict: "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 emptymissingwithalternatives: trueandverdict: "ask"means the opposite — questions exist on several routes, and the interface must show them per route instead of stopping.
Check your understanding
Section titled “Check your understanding”Run the missing-facts tests and confirm the ask lists:
node --import tsx --test test/missing.test.tsok 1 - a draft with only the event asks for durationDaysok 2 - an empty draft asks for both fieldsok 3 - a draft with only the duration asks for eventDateok 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 6 - the computed intermediate is unaskable, never a question# tests 14# pass 14# fail 0Predict 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.
- Show grounds: proof, sources, hashes
- Connect an AI assistant for the same inventory over MCP
- Query types: the
why_notrow explains the blocker report
Documentation for Arxo. Writings — blog.arxo.io.
Anonymous visit counts on stats.arxo.io, no cookies.