# 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.
Words in this excerpt that the next article explains `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.
## 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 (x) and (x) then (x)`), a file missing `` yields `NEITHER` — never a denial — *provided nothing else on file supports either side*. A denial needs the explicit negation `not (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).
Evidence and scope of the external example 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.
## 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).