docs← Back to article

Markdown for LLMs

nb-02: Why a missing fact is not a refusal

The source Markdown for this article. Copy it into your assistant or download it as a text file.

Download this articlePlain text ↗
# nb-02: Why a missing fact is not a refusal

Engine: `law 0.1.0`, semantics `law.core/0.2`, std `0.2.0`.
Package: `demo.northbridge.permits` (`packs/examples/language-demo/permits/`).
All law is fictional; every act, document and calendar in this series is
synthetic and unofficial.

## Situation

Bob applies for a Northbridge parking permit. The clerk opens his file and
finds one record: Bob is a resident. There is no car record — no
`vehicle_registered` entry, and no statement that Bob has no car either.

The clerk asks: "Nothing supports his eligibility, so is he refused?" No.
A missing fact is not a refusal. The engine answers "is Bob eligible?"
with *no support either way*, which is a different outcome from "Bob is
not eligible".

This article shows the four truth statuses that keep those outcomes
apart, with each one happening in the permits suite.

## Prerequisites

This article continues
[nb-01: First permit: facts, a rule and a question](/tutorials/northbridge/nb-01-first-permit/):
asserting facts, the `PermitEligibility` rule, and asking a `truth(...)`
question in a `.lawtest` scenario. You should be able to read a
`given / evaluate truth(...) / expect truth_status == ...` test.

## Task

Given a file with a missing record, determine what the engine actually
concluded — support, denial, both, or neither — and act accordingly
instead of treating every "no" as a refusal.

## Example

Excerpt from `packs/examples/language-demo/permits/package.law` (the two
rules and the priority this article uses; line numbers are from the file):

```law
rule PermitEligibility defeasible {                          // line 23
    for a: Applicant;
    when demo.northbridge.vocabulary::resident(a) and demo.northbridge.vocabulary::vehicle_registered(a);
    then permit_eligible(a);
    unless permit_suspended(a);
}

rule FinesRefusal defeasible {                               // line 30
    for a: Applicant;
    when outstanding_fines(a);
    then not permit_eligible(a);
}

priority RefusalOverEligibility {                            // line 42
    prefer FinesRefusal over PermitEligibility;
    reason lex_specialis;
}
```

`PermitEligibility` concludes `permit_eligible(a)` only when both
`resident(a)` and `vehicle_registered(a)` are present. `FinesRefusal`
pulls the other way: it concludes the explicit negation, `not
permit_eligible(a)`, when `outstanding_fines(a)` is present. The
`priority` block names `FinesRefusal` the winner on the files where
both rules fire.

Nothing in the package concludes anything from the *absence* of a fact.
That silence is what this article is about.

<details>
<summary>Words in this excerpt that the next article explains</summary>

`defeasible` marks a rule that can lose to another rule; `unless`
names an exception that switches the rule off; `reason lex_specialis`
records *why* the refusal outranks eligibility. You do not need the
machinery behind these words yet — only the direction each rule pulls.

</details>

## Command and result

Run the permits suite from the repository root:

```sh
law test packs/examples/language-demo/permits
```

Observed result (tail of the output):

```text
  ok   [demo.northbridge.permits] tests/permits.lawtest / general rule: resident with a car
  ok   [demo.northbridge.permits] tests/permits.lawtest / unless removes the conclusion
  ok   [demo.northbridge.permits] tests/permits.lawtest / denial wins by priority
  ok   [demo.northbridge.permits] tests/permits.lawtest / without priority the conflict persists
  ok   [demo.northbridge.permits] tests/permits.lawtest / defeater removes support without negation
  ...
  ok   [demo.northbridge.permits] tests/permits.lawtest / power without grounds does not operate
  ...
итого: 28 проверено, 28 прошли, 0 не прошли, 0 не исполнены; код 0
```

Every `ok` line confirms the `expect truth_status == ...` assertion inside
the named test (see `tests/permits.lawtest`). A passing suite means each
answer matched its expectation; it says nothing about whether any
applicant deserves a permit.

The last line is in Russian: 28 tests checked, 28 passed, 0 failed,
0 skipped, exit code 0.

Four of those tests carry the four statuses of this article:

| Test | Facts given | Expected status |
|---|---|---|
| `general rule: resident with a car` (line 5) | resident + vehicle | `TRUE_ONLY` |
| `denial wins by priority` (line 27) | resident + vehicle + fines | `FALSE_ONLY` |
| `without priority the conflict persists` (line 38) | badge + fines | `BOTH` |
| `power without grounds does not operate` (line 287) | notice, but no grounds | `NEITHER` |

## Why: the four truth statuses

A `truth(...)` question does not return yes or no. It reports which of
the two sides — the statement and its explicit negation — found support.
Two sides, each supported or not, give four combinations:

| Statement supported? | Negation supported? | Status |
|---|---|---|
| yes | no | `TRUE_ONLY` |
| no | yes | `FALSE_ONLY` |
| yes | yes | `BOTH` |
| no | no | `NEITHER` |

Each row below is one cell of that grid, filled with a real test from
the suite.

`TRUE_ONLY`: the statement is supported, its negation is not. Ann, a
resident with a registered car, is eligible (`general rule`). The test
also records `applied(PermitEligibility)`, so the support is traced to
that rule.

`FALSE_ONLY`: the negation is supported, the statement is not. Ann with
unpaid fines is refused — not because support is missing, but because
`FinesRefusal` derives `not permit_eligible(a)` and the stated priority
prefers it over the eligibility rule (`denial wins by priority`).

`BOTH`: both sides are supported at once. Bob holds a disability badge
— a second, independent route to eligibility in the suite (rule
`BadgeEligibility`) — and has outstanding fines (support for the
negation via `FinesRefusal`). The suite's priority only ranks
`PermitEligibility` against `FinesRefusal`; it says nothing about the
badge route, so the conflict stays visible instead of one side
silently winning (`without priority the conflict persists`).

`NEITHER`: neither side is supported. This row uses a different
question from the suite — `permit_revoked`, answered from
`resells_permit` grounds: a revocation notice was issued but no grounds
are on file, so neither side is supported (`power without grounds does
not operate`).

Bob's missing car record has the same shape for the eligibility
question: with only `resident(bob)` on file, nothing supports either
side of `permit_eligible(bob)`, so the answer is `NEITHER`. Silence
produces `NEITHER` on this file; `FALSE_ONLY` needs the explicit
negation supported — here, derived by `FinesRefusal`.

## Why this construct

Two truth values force every gap into a falsehood: "not proven eligible"
collapses into "proven ineligible". Four statuses keep the gap open, so
the file can say "we have not established this" without claiming it
established the opposite.

The payoff is downstream: anything that consumes the answer — a clerk, a
notice, another rule — can react differently to "denied, here is the
reason" and to "undecided, here is the missing record".

## Changed condition

One added fact flips the status. Start from Bob's silent file (`NEITHER`
on `permit_eligible(bob)`):

- Add `vehicle_registered(bob)`: `PermitEligibility` fires, and the
  status becomes `TRUE_ONLY`. The suite shows the same step for Ann in
  `general rule: resident with a car`; applied to Bob, resident-only
  `NEITHER` becomes resident-plus-car `TRUE_ONLY`.
- Add `outstanding_fines(bob)` instead: `FinesRefusal` fires unopposed —
  `PermitEligibility` cannot fire without a car record — so the status
  becomes `FALSE_ONLY`, a genuine refusal with a derived reason. (The
  `RefusalOverEligibility` priority matters only when both rules fire, as
  in `denial wins by priority`; here there is nothing to outrank.)
- Add `fraud_flag(bob)` to a badge holder's file: the defeater
  `FraudBlocksBadge` removes the badge's support *without* deriving the
  negation, so the status becomes `NEITHER` — blocked support, not denial
  (`defeater removes support without negation`; full mechanics in
  [nb-03: Exceptions and conflicting rules](/tutorials/northbridge/nb-03-exceptions/)).

## Alternatives

The alternative is a two-valued reading where every question must come
out true or false. Under that reading, Bob's silent file would have to be
called a refusal, and the fraud case above would be indistinguishable
from the fines case — yet the package treats them differently on purpose:
a defeater blocks, while only a `not`-conclusion denies.

Another alternative is closing the gap with a default, such as "everyone
is refused unless proven eligible". That is expressible, but it must be
written as an explicit rule, so the refusal appears in the record as
derived rather than smuggled in through the absence of facts.

## Typical mistake: reading NEITHER as refusal

The mistake is treating "the engine did not grant it" as "the engine
denied it". Compare two of Bob's files, both ending without a permit:

1. Badge plus fines → `BOTH`, resolved against him only if a priority
   says so. No priority resolves that pair, so nothing is decided —
   sending Bob a refusal letter would state a conclusion the engine never
   drew.
2. Badge plus fraud flag → `NEITHER`. Support was removed, but no rule
   derived `not permit_eligible(bob)`. Recording this as "refused for
   fraud" invents a denial; the honest record is "badge support blocked,
   eligibility undecided".

The fix is to match the downstream rule to the status. A rule that
triggers on the *absence* of support (for example, "decide this file, a
record is missing") is correct on `NEITHER` and wrong on `FALSE_ONLY`. A
rule that triggers on a derived denial (for example, "send the refusal
with reasons") is correct on `FALSE_ONLY` and wrong on `NEITHER`.
Confusing the two sends the wrong letter.

## Proof behavior

Each status names the support behind it, and the suite pins it.
`TRUE_ONLY` for Ann's eligibility comes with
`applied(PermitEligibility)` (line 13 of the test file): the proof
names the rule that fired. `FALSE_ONLY` for the fines case comes from
`FinesRefusal` deriving the negation and winning through the stated
priority. `BOTH` names support on both sides at once — here, two
rules, though an asserted fact could supply either side.

`NEITHER` names nothing because neither side has support on that file
— not because "no rule fired" is itself the test. A directly asserted
fact supports its statement with no rule firing at all
([nb-01: First permit: facts, a rule and a question](/tutorials/northbridge/nb-01-first-permit/)
shows exactly that: asserting the conclusion gives `TRUE_ONLY` with no
`applied(...)`). Read every status from the two support questions — is
the statement supported, is its negation supported — never from the
absence of `applied(...)` alone. Adding the missing fact visibly
changes the answers (see "Changed condition").

## Not-proven answers: how to read NEITHER

`NEITHER` means "not proven either way on this file". It does not mean
"false in the world" — Bob may well own a car; the file just does not say
so. And it does not authorize acting as if the negation were proven.

The correct handling is to complete the file: request the missing
record, then re-ask. If the record cannot be obtained, the undecided
status itself is the finding to report.
[nb-06: When the register may stay silent](/tutorials/northbridge/nb-06-register-silence/)
shows when a register may treat silence as trustworthy — a separate,
explicit step, not something silence does on its own.

## Limits

Statuses describe *support in the record*, not the world: `NEITHER` does
not say Bob has no car, and `TRUE_ONLY` does not say the file is
complete. A `BOTH` conflict is reported, not resolved — resolution needs
a priority or a defeater, the subject of
[nb-03: Exceptions and conflicting rules](/tutorials/northbridge/nb-03-exceptions/).

Nothing in this article changes how deadlines, duties, or procedures read
these statuses; those consumers are covered in
[nb-08: Which edition applies and when the term expires](/tutorials/northbridge/nb-08-edition-and-terms/)
and [nb-09: From permit to duties and powers](/tutorials/northbridge/nb-09-duties-powers/).

## Three levels

1. **Northbridge** (this article, verified): the four statuses on
   `permit_eligible` / `permit_revoked`, observed through the permits
   suite (see the table under "Command and result").
2. **Domain template**: for any eligibility-style question with a
   positive rule (`when <record-1>(x) and <record-2>(x) then
   <eligible>(x)`), a file missing `<record-2>` yields `NEITHER` —
   never a denial — *provided nothing else on file supports either
   side*. A denial needs the explicit negation `not <eligible>(x)`
   supported, plus, where both sides can be supported at once, a
   stated way to win. Apply the template by naming, for the new
   domain, every route supporting each side and which priority (if
   any) resolves their conflict.
3. **Confirmed example elsewhere**: state-task observance
   (Constitution of Turkiye, art. 65) — package `tr.constitution`,
   `corpus/laws/tr/constitution/02-rights-general.law:123-131`,
   construct `not_known(...)` guard in a defeasible rule body
   (`StateTaskObserved`: a polity with no established task failure
   counts as performing). Absence of a failure finding is not a failure
   verdict — the same silence-vs-denial line as this article (`NEITHER`,
   never a refusal).

<details>
<summary>Evidence and scope of the external example</summary>

Documented in
`docs/research/constructs/07-negation-and-status/corpus-forms.en.md`,
section 1 (rated exemplary there). Verification confirms the named
construct at the cited lines only, by direct source read; it claims
nothing about deployment, runtime behavior, or legal correctness.

</details>

## Exercise

Bob's file holds `disability_badge(bob)` and `fraud_flag(bob)`, nothing
else.

1. What is the status of `permit_eligible(bob)` — and is Bob refused?
2. Name the single change to the file that makes him eligible, and the
   status that results.

Write the answer down before opening
[the nb-02 solution](/tutorials/northbridge/solutions/nb-02-solutions/).

## Links

- Prerequisite:
  [nb-01: First permit: facts, a rule and a question](/tutorials/northbridge/nb-01-first-permit/).
- Next: [nb-03: Exceptions and conflicting rules](/tutorials/northbridge/nb-03-exceptions/) —
  priorities, `unless`, and defeaters in full.
- Sources: `packs/examples/language-demo/permits/package.law` (rules,
  priority, defeater), `packs/examples/language-demo/permits/tests/permits.lawtest`
  (all scenarios quoted above).