Skip to content
docs
Arxo ↗

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

For LLMs16 sections
← Course mapChapter 02 / 25 · Beginner

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.

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.

This article continues nb-01: First permit: facts, a rule and a question: 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.

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.

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

Arxo 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.

Run the permits suite from the repository root:

Terminal
law test packs/examples/language-demo/permits

Observed result (tail of the output):

Output
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:

TestFacts givenExpected status
general rule: resident with a car (line 5)resident + vehicleTRUE_ONLY
denial wins by priority (line 27)resident + vehicle + finesFALSE_ONLY
without priority the conflict persists (line 38)badge + finesBOTH
power without grounds does not operate (line 287)notice, but no groundsNEITHER

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
yesnoTRUE_ONLY
noyesFALSE_ONLY
yesyesBOTH
nonoNEITHER

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.

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”.

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).

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

Section titled “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.

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 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”).

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 shows when a register may treat silence as trustworthy — a separate, explicit step, not something silence does on its own.

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.

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 and nb-09: From permit to duties and powers.

  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).
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.

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.

Documentation for Arxo. Writings — blog.arxo.io.

Anonymous visit counts on stats.arxo.io, no cookies.