# Regression handoff — bootstrap-firewall post-write verify (releases#304 W2)

hertz 2026-09-12. DRAFT, NOT SENT, NOT AGREED. No build grant, no field grant. Test ownership is to
be coordinated with todlando AFTER A7 END (doyle). Source read at **921aa68f** (.worktrees/asm-304-w2);
every line number below is that sha.

## 1. What the observed failure does and does not establish

`verify` → `decide` → `Ok(false)` comes from exactly one place, `pair_satisfied_by` at :569 — but
**`pair_satisfied_by == false` is SPEC-MATCH FAILURE, not representation mismatch** (doyle's
narrowing, taken). At this sha it is false when ANY of these holds, and the product's message does
not distinguish them:

1. **A named rule is missing** — `matching.next()` is `None` (:420-423). Zero rules of that name.
2. **Hygiene false** (:435-437) — the QUERY's own conjunction (enabled/inbound/allow/no platform,
   owner, dynamic keyword, RemotePort Any, LocalAddress Any, Package/Service Any, interface Any,
   security NotRequired, LocalUser/RemoteUser/RemoteMachine Any).
3. **Port** — not exactly one entry, or not equal to the bound port (:438-440).
4. **Program** — `None` spec requires `observed.program.is_empty()` (:446); a `Some` spec requires
   normalized-path equality (:447).
5. **Profile** — exact string inequality (:452), no normalization of order or spacing.
6. **Remotes** — case-folded, sorted, exact STRING set inequality (:455-459).

Only 4/5/6 are representation candidates. 1/2/3 are different findings with different fixes. **The
later census supports the representation candidates but cannot establish the failure-time inputs**
— it is not that dump. Nothing below may be built on the assumption that representation is the
cause; this handoff activates only if the failure-time snapshot implicates 4, 5 or 6.

The four arms that are NOT this (each its own distinct `Err`, each to stay that way): ambiguity
:564-568, non-Local `source_type` :579-586, `NoneConnected` :591-593, enforcement :594-601.

## 2. The evidence rule for every fixture added here

**Fixtures must encode INDEPENDENTLY CAPTURED NetSecurity spellings, never observations generated
from `RuleSpec`** (doyle). Concretely, a fixture string is admissible only if it was:

- read off a LIVE host from the raw NetSecurity objects (`Get-NetFirewallRule` plus
  `Get-NetFirewall{Port,Application,Address,Security,Interface,InterfaceType,Service}Filter`), and
- transcribed as a LITERAL into the test, and
- carries a provenance line beside it: host, date, OS build, rule name, and the exact command that
  produced it.

**Why this rule exists:** every fixture in the module today is built from the spec's own spelling —
`observed(...)` at :838 takes whatever profile/remote strings the author passes, and :993 asserts
`"Private,Domain"` against the very constant that produced it. The NetSecurity round trip is
therefore compared against itself, which is why causes 4/5/6 are invisible to the entire existing
suite by construction. A fixture generated from `RuleSpec` would reproduce that defect exactly.

## 3. The cells

Positive:

- **P1 — a captured pair satisfies its spec.** Field-captured observed strings for BOTH halves must
  satisfy `desired_specs` for the same binder/port/scope. This is the cell the current suite cannot
  express, and it is the one that would have caught this in CI.

Negative — each must STILL FAIL after any normalization is introduced:

- **N1 — a genuinely narrower scope still fails.** Observed remotes strictly narrower than the spec
  (a single host address where the spec wants the derived prefix) must NOT satisfy. Guards the real
  risk of a normalizing comparison: that it starts accepting scopes the policy did not choose.
- **N2 — a program-bearing rule does not satisfy a spec wanting none** (:441-448's existing stated
  policy: an unrequested narrowing is the difference between admitting the tailnet and admitting
  nothing). Unchanged, kept explicitly so a program-string normalization cannot erode it.
- **N3 — ineffective rules remain rejected.** `enforcement != ["Full"]` still `Err`s, and
  `source_type != "Local"` still `Err`s. These are `decide`-level, they run AFTER the pair matches,
  and no string normalization may reach them. Stated as its own cell so the separation is asserted
  rather than assumed.
- **N4 — a missing half fails.** One named rule absent ⇒ false. Guards against a normalization that
  makes absence match something.
- **N5 — a port mismatch fails.** Same guard on the axis normalization does not touch.

Control discipline on each: a red-on-purpose negative control AND the positive it must match, and a
normalization change must be shown to flip **only** the intended case — the other cells stay red
where they were red.

## 4. Open question, not to be decided without the dump

Whether canonicalization belongs in the **QUERY** (emit already-canonical strings, keeping Rust's
comparison a plain equality) or in **Rust** (compare canonically, leaving the query a faithful
reporter). Both are defensible and they differ in which side becomes the untested one. The captured
spellings decide it; picking first and capturing after would repeat the mistake this document exists
to fix.
