Why a derived fact is missing
A derivation rule derives nothing, or derives rows but not the one you
expected. gnd why-not reports where each of the rule's clauses runs out of
bindings, how many bindings each earlier step had, and which values reached the
step that emptied them. An application or an agent can then add the missing
fact, or report which fact is missing.
gnd witness is the companion command for the other case: it proves a derived
row that is present.
Ask the question
gnd.api.why_not(model_path, population_path, fact_type, *, row_path=None,
reference_root=None) -> dict
Without --row, the question is about the rule as a whole: why does
fact-type have no rows? With --row, it is about one claimed row: why is
this row absent? The row file uses the witness request format
and holds exactly one target, whose fact_type must equal --fact-type.
The fact type must be derived or semiderived with a structured rule. Reference
populations bind as they do for check and witness.
Read the answer
GND's own evaluator decides first. The answer's status is one of:
status |
Exit | Meaning |
|---|---|---|
derived |
0 | The evaluator derives the row, or (rule mode) at least one row. |
blocked |
0 | The evaluator derives nothing that answers the question; clauses says where each clause stops. |
unsupported |
2 | The rule uses something why-not does not explain: aggregate, a prose rule (rule-opaque), or an opaque value in a fact type the rule depends on (opaque-value). |
refused |
3 | The input is invalid (reason: input-invalid). |
unavailable |
4 | A term could not be evaluated, for example a division by zero; reason names it. |
refused |
4 | A work limit was reached (reason: work-limit); work holds the counters. |
A blocked or derived answer exits 0 because both answer the question; read
status. Exit 4 means the answer is indeterminate, as it is for check.
Steps and the blocking step
This section is the normative definition.
For each clause of the rule, in the order the model declares them, why-not
walks the clause's parts in declared order, not in the order GND's evaluator
joins them. The answer says so with "order": "declared".
- Seed. In rule mode the walk starts from one empty binding. In row mode
each head variable is bound to the claimed row's value. If a head variable
appears at two roles with different claimed values, the step
head-bind:variablehas count 0. If a constant head term differs from the claimed value, the stephead-bind:constanthas count 0. Computed head terms are not bound here. - Positive atoms, in declared order. Each step joins the bindings so far
with the atom's rows, on the variables they share. An atom that shares no
variable makes a product. The step's
countis the number of distinct bindings after it. - Negated atoms, in order. Each removes the bindings whose ground row is present.
- Comparisons, the rule's root comparisons before the clause's own. Each keeps the bindings for which the comparison holds.
- Head, row mode only. Each binding's head terms are evaluated, and the bindings that produce the claimed row are kept.
The blocking step is the first step whose count is 0. Later steps are not
computed, and the clause says "steps_after_blocker": "not-evaluated".
Counts are of distinct bindings over the variables bound so far, under GND's
value identity: 1 and 1.0 are one value; true and 1 are two.
The blocker
{"kind": "atom", "index": 1, "fact_type": "wrote", "derivation": "base",
"ground": false, "asserted_not_read": false,
"roles": {"work": {"variable": "$w", "bound": true},
"person": {"variable": "$p", "bound": false}},
"sample_count": 2,
"samples": [{"work": "w1"}, {"work": "w2"}],
"near_miss_count": 0, "near_miss_truncated": false, "near_misses": []}
rolescovers every role of the atom. A role holds a variable, with whether an earlier step bound it, or a constant.groundis true when every role is a constant or a bound variable.sample_countis the number of bindings that reached the blocker.samplesholds up to five of them, keyed by role and projected onto the bound roles, in a fixed order.derivationisbasefor a fact type with asserted rows only, including one with a prose rule. Otherwise it isderivedorsemiderived.near_miss_count,near_miss_truncatedandnear_missesreport rows that differ from a binding only in spelling; see values spelled differently.
So the blocker above says: two listed works reached wrote, and neither has an
author. Adding wrote(w1, someone) would unblock the clause for w1.
For a negated, comparison or head blocker, reads lists the bound
variables the step reads, and samples are keyed by variable.
Values spelled differently
A join can be empty for two reasons: the fact you need is missing, or it is
there but spelled differently. FIELD NOTES and Field Notes are two values
to GND, so a blocker caused by either reads the same. To tell them apart, every
atom blocker reports near-misses: rows of its fact type that would have
joined if bound strings were compared after a fixed normalization.
Near-misses never change the answer. The status, the blocker, the counts, the
work counters and the exit are what exact matching gives. Whether two
spellings name one entity is your decision; why-not only points at the
candidates.
This is the blocker for the claim certified(q-17, Ada Byrne) over the
packaged quoted_claims example's spelling-gap.pop.json, with --raw:
{"kind": "atom", "index": 1, "fact_type": "authored", "derivation": "base",
"ground": true, "asserted_not_read": false,
"roles": {"work": {"variable": "$w", "bound": true},
"person": {"variable": "$p", "bound": true}},
"sample_count": 1,
"samples": [{"work": "Field Notes", "person": "Ada Byrne"}],
"near_miss_count": 1,
"near_miss_truncated": false,
"near_misses": [
{"roles": ["work"],
"sample": {"work": "Field Notes", "person": "Ada Byrne"},
"candidate": {"work": "FIELD NOTES", "person": "Ada Byrne"}}]}
The binding for Field Notes reached authored and found no row, but the
population holds a row for FIELD NOTES by the same person. Fix the spelling
on one side, or add the row with the spelling the rule expects. The population
also holds Field notes by Bo Lin; it is not reported, because the other bound
role, person, differs.
An empty list with near_miss_count: 0 means that why-not searched and found
nothing close: the fact is missing.
This section is the normative definition.
The normalization
Near-misses compare strings by N(v):
- Unicode NFKC normalization;
- case folding (
str.casefold); - deletion of every character whose Unicode general category is punctuation
(
P*); - each run of whitespace becomes one space; leading and trailing spaces are
removed. Whitespace is what Python's
str.split()splits on: Unicode whitespace plus the separators U+001C to U+001F.
N is defined only for strings. Numbers, booleans and other values never
near-match, and neither does a string whose N is empty ("..."). N uses
the Unicode database of the Python running GND.
So ACME CORP. near-matches Acme Corp., and Straße near-matches STRASSE.
Punctuation is deleted, not replaced: ACME-CORP near-matches ACMECORP, not
ACME CORP. Acme . Corp near-matches Acme Corp, and ℅ near-matches
co. There is no fuzzy, edit-distance or locale-dependent matching.
Which rows are candidates
Let the blocker's bound roles be the roles whose variable an earlier step bound. A blocker with no bound role has no near-misses.
A row of the blocker's fact type is a candidate when it has every role of the atom, every constant role matches exactly, and a free variable that appears at two roles has the same value at both. A bound variable that appears at two roles is compared with the binding at each occurrence, so a candidate row may spell it differently at different roles.
A binding and a candidate row form a near-miss when, at every bound role,
the two values are equal or have the same N, and at least one bound role
differs exactly. roles lists the bound roles that differ exactly.
Count, list and order
near_miss_countcounts distinct (binding, row) pairs, each projected onto the bound roles. Rows or bindings that differ only at other roles count once.near_misseslistsmin(5, near_miss_count)pairs, ordered by the canonical encoding ofsample, then ofcandidate.sampleandcandidateare projected onto the bound roles.- Values that are one value to GND, such as
1and1.0, are shown once. When separate bindings or rows differ only in such spellings, the pair shows the projection whose canonical encoding comes first. The whole projection is compared, not the number alone, so which spelling wins can depend on the other values and the role names; the choice is the same on every run. Within one binding, each value shows the spelling, among every row that met it, whose canonical encoding comes first (1before1.0), whatever the row order. This rule applies tonear_missesonly.sampleskeep the first value why-not met, so one blocker can show1insamplesand1.0innear_misses. - Near-misses appear on positive atom blockers at every descent level. The
three fields are absent from
negated,comparisonandheadblockers.
Search limits
The search looks at every row of the fact type and every binding once, and
builds at most 25 pairs for each normalized value. It has its own ceiling of
200,000 inspections, separate from the walk's budgets, so it can never cause a
work-limit exit. One ceiling serves the whole request: every clause and
every descent level draws on it, so a lowered ceiling can truncate a later
blocker after an earlier one used it up. If the ceiling is reached,
near_miss_truncated is true,
near_miss_count is a lower bound and the list holds what was found. With the
default limits the ceiling cannot be reached and the flag is always false,
because the walk's own probes already cover every row and binding the search
inspects.
Descent into a derived blocker
When the blocker is an atom over a derived or semiderived fact type with a
structured rule, and a sample binds every role, why-not explains that row in
turn: the first such sample, as a claimed row. The answer nests under the
clause's descent. Descent stops, and descent_stopped says why, when:
- the blocker's fact type is base (
not-derived); - no sample binds every role (
not-ground). Ask again with--rowto choose the row; - the blocker's fact type is computed by an aggregate, which why-not does not
walk (
aggregate); - the same fact type and row are already being explained (
cycle); - eight levels have been explained (
depth).
Semiderived fact types and covers
A semiderived fact type's asserted rows are never read by the rules that
depend on it. When a blocker's absent row is asserted in the population, the
blocker says "asserted_not_read": true. The fix is to make the rule derive the
row, not to assert it again.
Why-not ignores a population's covers list. Derivation never reads a derived
type's asserted rows, whatever covers says. So a target that check reports
as not covered is explained like any other.
Values in the answer
With no disclosure mode set, the Python API returns raw
values, like api.witness, and the CLI prints a redacted view: samples,
constant roles and an unavailable term's binding read "redacted", and the
answer carries "view": "redacted". An atom blocker keeps
near_miss_count and near_miss_truncated and shows near_misses as
"redacted". Pass --raw to print the values, with "view": "raw".
Under GND_DISCLOSURE=internal both print the raw view. Under customer
both return the redacted view and --raw is refused with exit 3. The
near-miss counters stay visible in every mode; who customer mode
protects says what they reveal.
Limits
Inputs have the witness lane's ceilings: 10 MiB per file, nesting depth 64,
4,096 bytes per string, and 128 digits per number. Work has two budgets.
GND's evaluator may perform 200,000 joins while deciding the status. The
walk may make 100,000 joins and 200,000 probes, and may store 400,000 binding
cells in total across all its steps, clauses and descent levels.
Reaching any of them returns refused with reason: work-limit and exit 4.
The near-miss search has its own ceiling of 200,000 inspections, which never
refuses a request (see search limits).
The output is described by why-not.schema.json, packaged with GND. Its
schema_version is gnd-why-not/2.