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
Section titled “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
Section titled “Prerequisites”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.
Example
Section titled “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):
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
Section titled “Command and result”Run the permits suite from the repository root:
law test packs/examples/language-demo/permitsObserved result (tail of the 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 не исполнены; код 0Every 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
Section titled “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
Section titled “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
Section titled “Changed condition”One added fact flips the status. Start from Bob’s silent file (NEITHER
on permit_eligible(bob)):
- Add
vehicle_registered(bob):PermitEligibilityfires, and the status becomesTRUE_ONLY. The suite shows the same step for Ann ingeneral rule: resident with a car; applied to Bob, resident-onlyNEITHERbecomes resident-plus-carTRUE_ONLY. - Add
outstanding_fines(bob)instead:FinesRefusalfires unopposed —PermitEligibilitycannot fire without a car record — so the status becomesFALSE_ONLY, a genuine refusal with a derived reason. (TheRefusalOverEligibilitypriority matters only when both rules fire, as indenial wins by priority; here there is nothing to outrank.) - Add
fraud_flag(bob)to a badge holder’s file: the defeaterFraudBlocksBadgeremoves the badge’s support without deriving the negation, so the status becomesNEITHER— blocked support, not denial (defeater removes support without negation; full mechanics in nb-03: Exceptions and conflicting rules).
Alternatives
Section titled “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
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:
- 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. - Badge plus fraud flag →
NEITHER. Support was removed, but no rule derivednot 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
Section titled “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
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
Section titled “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 shows when a register may treat silence as trustworthy — a separate, explicit step, not something silence does on its own.
Limits
Section titled “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.
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.
Three levels
Section titled “Three levels”- Northbridge (this article, verified): the four statuses on
permit_eligible/permit_revoked, observed through the permits suite (see the table under “Command and result”). - 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>yieldsNEITHER— never a denial — provided nothing else on file supports either side. A denial needs the explicit negationnot <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. - Confirmed example elsewhere: state-task observance
(Constitution of Turkiye, art. 65) — package
tr.constitution,corpus/laws/tr/constitution/02-rights-general.law:123-131, constructnot_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
Section titled “Exercise”Bob’s file holds disability_badge(bob) and fraud_flag(bob), nothing
else.
- What is the status of
permit_eligible(bob)— and is Bob refused? - 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.
- Prerequisite: nb-01: First permit: facts, a rule and a question.
- Next: nb-03: Exceptions and conflicting rules —
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).
Documentation for Arxo. Writings — blog.arxo.io.
Anonymous visit counts on stats.arxo.io, no cookies.