Skip to main content

cloud/reconciler/
ingress_verify.rs

1//! Is the backend reachable at the port its service record advertises? — the
2//! half [`collate_workspace_ingress`](crate::validate::collate_workspace_ingress)
3//! deliberately cannot answer (R844-F16).
4//!
5//! **Read [§What this does not prove](#what-this-does-not-prove) before
6//! treating a green result as a deploy gate.** That question is narrower than
7//! "does the front door work", and on 2026-09-03 the difference took yah.dev
8//! down for four minutes.
9//!
10//! `yah cloud ingress collate` is pure: no network, no credentials. That purity
11//! is load-bearing — it is what lets `xtask/tests/mirror_ingress.rs` plan this
12//! camp's real `.yah/services/` tree as a unit test — but it means the
13//! `100.64.0.3:8080` it prints is an **echo of declarations**: the mirror's
14//! `upstream_host` pin, or since R844-F12 the placement machine's declared
15//! `[registration].mesh_ipv4`. Collate attests that the mirrors *cohere*. It
16//! has never attested that anything answers.
17//!
18//! That distinction is not academic in this repo. The apex's `upstream_host`
19//! was once left at `127.0.0.1` after a second front door existed, and collate
20//! rendered it exactly as confidently as it renders a correct one — for the
21//! nineteen days the site was frozen.
22//!
23//! ## Four claims, and this module measures only the third
24//!
25//! | claim | evidence | who says it |
26//! |---|---|---|
27//! | "the mirrors agree on what fronts what" | the declaration tree | `collate` |
28//! | "a ready record exists for it" | `GET /service-records?ready=true` | [`ServiceRecordFanout`] |
29//! | "that record's address answers" | a TCP connect | **this module** |
30//! | "the front door is configured to dial it" | an HTTPS GET of the hostname, compared against the backend | **this module**, [`apply_public_path`] (R844-F18) |
31//!
32//! The second is *yubaba's opinion*, and R844-B11 is the proof it can be wrong
33//! while looking right: us-west-001 advertised `100.64.0.3:4325` as `Ready`
34//! while the workload answered on `100.64.0.1:4325`. The record was healthy,
35//! the record's own address refused connections, and every layer above it
36//! reported success. A connect is the only step that can catch that, because it
37//! is the only step that asks the world instead of asking a declaration.
38//!
39//! ## What this does not prove
40//!
41//! **Updated by R844-F18 — the gap this section describes is now closed by
42//! [`apply_public_path`], and the history below is why that check exists and
43//! what it is still not.** [`verify_collation`] alone proves **the backend is
44//! reachable at the port the record advertises**; on its own it never proves
45//! **the front door is configured to dial that port**. Those two coincide only
46//! while a pin forces them to — so that pass is *weakest in exactly the
47//! portless configuration it was built to certify*.
48//!
49//! That is not a theoretical gap. R844-T10 deleted the apex's `port` pin on the
50//! strength of a green run from this verb, on 2026-09-03:
51//!
52//! * kamaji allocated a **new** port for the redeployed workload — 34759;
53//! * the service record correctly advertised 34759;
54//! * this verb dialed 34759, found it open, and printed
55//!   *"2 rule(s) … 2 proven to serve, 0 not"*;
56//! * the public got **HTTP 503** for four minutes, because the running passway
57//!   was still configured for 8080 and **nothing reconfigures it**.
58//!
59//! So the verb reported success during the live outage it was built to prevent.
60//! The prior measurement that authorised the edit was green for a reason that
61//! did not survive a real deploy: it stripped the pins from a *copy* of the
62//! config while the old workload was still bound to 8080, which is the one
63//! arrangement in which the record and the front door cannot disagree.
64//!
65//! This is [`collate`]'s own limitation one level up — collate attests
66//! coherence and not reachability; a dial attests reachability of a *record*,
67//! and not that the front door agrees with that record.
68//!
69//! [`apply_public_path`] closes it by traversing the **public path** and
70//! comparing: it fetches the publish beacon
71//! ([`publish_beacon`](crate::reconciler::publish_beacon)) from
72//! `https://<hostname>/` and from each discovered backend, and fails the rule
73//! when the two serve different publishes. A bare `GET /` would not have done —
74//! a door pointed at the wrong backend answers 200 with a plausible page, which
75//! is how the apex stayed frozen for nineteen days. The beacon is the only
76//! object on either side that says *which publish this is*.
77//!
78//! **Two things that check is still not.** It is a **detector of the current
79//! state**, not a simulation of a pending edit — run it before and after an
80//! apply and require both green. And the public fetch goes wherever **DNS**
81//! sends it, so a hostname on two front doors is measured at one of them; the
82//! verdict says so in a note rather than implying it covered both.
83//!
84//! **So: a green [`verify_collation`] alone is necessary, not sufficient. Do
85//! not use it without the public-path pass as the gate on removing a pin.**
86//!
87//! [`collate`]: crate::validate::collate_workspace_ingress
88//!
89//! ## Why this is a separate verb and not a flag on `collate`
90//!
91//! Because the purity above is the feature. A `--live` flag would put a network
92//! read inside the function eleven offline tests call, and the pressure to make
93//! those tests pass would then push the network read towards being optional in
94//! a way that silently degrades. A sibling verb costs nothing that flag would
95//! not cost more.
96//!
97//! ## Pure, like everything else on this seam
98//!
99//! Nothing here opens a socket. The caller does the fanout read and the dial,
100//! and hands both in as data — the fifth instance of the shape
101//! [`resolve_ingress_placements`](crate::reconciler::resolve_ingress_placements),
102//! [`IngressPlan::resolve_upstreams`], [`IngressPlan::resolve_ports`] and
103//! [`IngressPlan::resolve_upstreams_from_config`] already use. So the verdict
104//! logic — which is where the interesting mistakes live — is unit-testable
105//! against a fake fleet with no network at all.
106//!
107//! ## A subset renders like a success, so a subset is a failure
108//!
109//! The failure class this whole relay exists to remove is a partial answer that
110//! looks complete. A rule placed on two nodes that resolves one address is
111//! *half a front door*: it renders, it dials, it serves — and half the fleet's
112//! traffic capacity is silently absent. [`RuleVerdict`] therefore fails a rule
113//! whose resolved backends do not cover its whole declared placement, and names
114//! the node that went missing along with why.
115//!
116//! @yah:ticket(R844-F18, "Verify the PUBLIC path — an HTTPS GET of the hostname through the real front door, compared against what the record claims")
117//! @yah:status(review)
118//! @yah:assignee(agent:bundle-anthropic-ashguard)
119//! @yah:at(2026-09-04T01:55:44Z)
120//! @yah:parent(R844)
121//! @yah:next("KEEP `collate` PURE (R772, R844-F5, R844-F12, R844-F16 each fought for this) and prefer a third verb or a flag on `verify` over touching it. `cargo test -p xtask --test main mirror_ingress` planning the camp's REAL .yah/services tree with no network is the property being protected; it is currently 11 green.")
122//! @yah:verify("And it must still be green on the healthy fleet: https://yah.dev/ through both declared front doors, agreeing with what `yah cloud ingress verify` resolves.")
123//! @yah:gotcha("A LIVE-OUTAGE-DETECTOR IS NOT AUTOMATICALLY A PRE-FLIGHT GATE, and this ticket should be honest about which it is building. An HTTPS GET proves the CURRENT front door serves; it cannot tell you what a config change is ABOUT to do, because the front door has not been reconfigured yet. That may still be enough — run it before and after an apply and require both green — but say so explicitly rather than letting a future reader assume it gates the edit. The failure that started this was precisely someone (twice) treating a green from the wrong vantage point as authorisation.")
124//! @yah:next("A SHAPE FOR THIS, from @Ashguard:griffin (session:75f87e36, the session that caused the outage behind it) — offered as a starting point, not a settled design. The open question on this ticket is whether an HTTPS GET is a pre-flight GATE or only an outage DETECTOR. I think it is only ever a detector, and that the ticket is really TWO checks answering two different questions:\n\n  1. PRE-FLIGHT, and it is not a probe at all — it is a COMPARISON. Read what the front door is actually configured to dial, FROM THE DOOR, and compare it against what the service record says the backend is. That is the check that would have caught tonight's outage BEFORE it happened, because the two disagreed (passway held 8080, the record advertised 34759) at a moment when every probe of either side in isolation was green. Note what makes it different from R844-F16's verify: verify reads the record and dials the port the record names, so both of its inputs come from the same side of the disagreement. The door's own configured value is the input nobody currently reads, and it is the only one that makes the comparison possible.\n\n  2. POST-CONDITION, which is where the HTTPS GET belongs — an unauthenticated GET of the public hostname through the real front door, asserted AFTER an apply, once R844-B19 guarantees the door has actually been repointed. Today that assertion cannot be trusted to mean anything, because B19's ordering bug means the door may never have been updated at all; the GET would just be re-measuring the old configuration and calling it a pass.\n\nWHY THIS PAIRS F18 WITH B19 RATHER THAN DUPLICATING IT: B19 makes the front-door update reliably HAPPEN; (2) is the assertion that it DID; (1) is the only one of the three that can speak before a change is applied. Sequencing follows from that — do not land (2) before B19, or it encodes today's broken ordering as the expected one.\n\nTHE CAVEAT I CANNOT RESOLVE AND WHOEVER TAKES THIS SHOULD NOT ASSUME AWAY: even (1) compares two CURRENT states. It does not simulate what a config change is about to do, which is what we actually wanted to know tonight. It catches an existing divergence, and it would catch this specific class because the divergence appears the moment the workload is redeployed — but it is not a general \"is this edit safe\" oracle, and nothing in this design is. If someone needs that, it is a different and much larger ticket, and it should be filed as one rather than smuggled in here.")
125//! @yah:handoff("LANDED. `yah cloud ingress verify` now takes the fourth step, on by default. For every hostname in the collation it fetches `https://&lt;hostname&gt;/.well-known/yah-publish.json`, for every discovered backend it fetches the same object over the mesh, and it FAILS the rule when the two name different publishes. Verdict logic is `apply_public_path` in oss/yubaba/crates/cloud/src/reconciler/ingress_verify.rs, pure like the rest of that seam — the CLI does both fetches and hands them in as `PublicReadings`, so the interesting mistakes are unit-testable against a fake fleet. New surface: `BeaconFetch`, `PublicReadings`, `apply_public_path`, three `VerifyFinding` arms, `fetch_beacon` and `--skip-public` in app/yah/cli/src/cloud.rs.")
126//! @yah:handoff("THE COMPARISON IS THE CONTENT, NOT THE GET — this is the design decision, and the ticket title's \\\"HTTPS GET\\\" understates it. A bare `GET /` proves only that something answered: a door pointed at the wrong backend returns 200 with a plausible page, which is exactly how the apex stayed frozen for nineteen days with every signal green. The publish beacon (`publish_beacon.rs`, `BEACON_KEY = .well-known/yah-publish.json`, R703-B4) is the one object on either side of the door that says WHICH PUBLISH THIS IS, so fetching it from both and comparing digests is what turns \\\"something answered\\\" into \\\"the front door is serving the backend the records name\\\". Reused rather than invented — the object already exists, `mesofact serve` already answers it out of the bundle, and R2 static publishes already write it.")
127//! @yah:handoff("THE TICKET'S OWN OPEN QUESTION — GATE OR DETECTOR — ANSWERED, AND ANSWERED THE WAY ITS FILER EXPECTED: **detector**, and the code says so in its own output rather than leaving a reader to infer it. `apply_public_path`'s doc, the CLI `--help`, the summary line printed on every run, W267 and the service-toml guide all now carry the same sentence: it compares two CURRENT states, cannot simulate an edit you have not applied, and the protocol is run-before-and-after-and-require-both-green. That is enough for the failure it was built for — the divergence appears the moment the workload is redeployed onto a new port — and it is deliberately NOT sold as an \\\"is this edit safe\\\" oracle. @Ashguard:griffin's caveat on this ticket was right and is preserved as the design, not assumed away.")
128//! @yah:handoff("GRIFFIN'S PART (1), THE PRE-FLIGHT \\\"READ THE DOOR'S OWN CONFIG AND COMPARE\\\", IS NOT WHAT SHIPPED — say so plainly rather than letting the ticket read as fully covered. Their shape proposed reading what passway is CONFIGURED to dial, from the door, and comparing that against the record. This ships the equivalent comparison one layer out: what the door ACTUALLY SERVES versus what the backend serves. Why that substitution rather than the config read: the running passway holds its upstreams in container env (`PASSWAY_UPSTREAMS`, `PASSWAY_UPSTREAM_SOURCE=static` — oss/passway/crates/passway/src/main.rs), so reading it means an SSH or a docker inspect per door, i.e. credentials and a shell on a production box inside a read-only verb. The served comparison needs neither, catches the same divergence class (it is true exactly when the door is dialing something else), and additionally catches a stale edge cache, which a config read cannot see. What the config read would still buy is naming WHY they diverge; that is a genuinely separable ticket and is not smuggled in here.")
129//! @yah:handoff("A FAILURE IS ONLY A FAILURE WHEN SOMETHING WAS ACTUALLY COMPARED — the design care, and the thing a careless version of this gets wrong in the direction that matters. Three arms produce a NOTE and leave the rule clean rather than a finding: a hostname that answers 200 with no beacon (it fronts something that is not a mesofact publish — a limit on the check, not a fault of the host, and failing it would red every non-bundle hostname in the fleet); a public 200 with no comparable backend (the note says verbatim that this proves the hostname is up and NOT that it is fronting the discovered backend); and a hostname nobody measured. Two arms fail: a transport failure, and a non-2xx. One arm is the point: `PublicBackendDivergence`, which names both digests and the backend address it compared against. And a backend that answers without a beacon adds nothing — `verify_collation` already dialed it and said what it found, so a second opinion phrased as an error would double-count one fact.")
130//! @yah:gotcha("THE LIMIT I COULD NOT DESIGN AWAY, AND DID NOT HIDE: the public fetch goes wherever DNS sends it, so a hostname published through two front doors is measured at ONE of them and this pass cannot say which. A divergence affecting only the other door reads as clean. Every verdict for such a hostname carries a note saying so — but only once a comparison actually happened, since on a rule where nothing could be compared that note is noise stacked on the finding. Closing it needs a fetch pinned to each door's public address with a `Host` override, which needs a public IP per door that nothing in the `Collation` carries today. Pinned by `a_hostname_on_two_front_doors_is_noted_as_measured_at_only_one`.")
131//! @yah:verify("UNIT: `cargo test --manifest-path oss/yubaba/Cargo.toml -p yah-cloud --lib ingress_verify` = 19 passed / 0 failed (11 before, +8 new). THE ONE THAT MATTERS IS `a_503_at_the_apex_fails_a_rule_whose_mesh_side_is_entirely_green` — it reproduces the 2026-09-03 outage as a unit test, asserting FIRST that `verify_collation` alone reports the rule clean (that assertion is the precondition, because a green mesh side is what reported success during the outage) and THEN that the public leg fails it. The other seven cover the stale-serve shape (200 serving a different digest than the backend), a transport failure, a hostname with no beacon, a public 200 with nothing to compare, an unmeasured hostname, a hostname on two doors, and the fully-clean case where the two digests match and NOTHING is caveated. Wider: `-p yah-cloud --lib ingress` = 94 passed / 0 failed; `-p yah-cloud --lib` = 1019 passed / 0 failed / 4 ignored (1011 before, so +8 and nothing lost).")
132//! @yah:verify("THE PURITY CANARY, which this ticket's own `next` named as the property to protect: `cargo test -p xtask --test main mirror_ingress` = 11 passed / 0 failed. `collate` was not touched — the public leg is a fourth step on `verify`, per the same reasoning R844-F16 used to make `verify` a sibling verb rather than a flag. `cargo check --workspace --all-targets` cargo-exit=0, zero `^error` lines. Installed and re-installed with `cargo xtask install` (sha256 b751f470e329253765075e5c050256f4e848ae1e9265f55fd6965f024bafbf24, `PATH resolves here`), per this relay's standing gotcha that `cargo build` does not update the binary an operator runs.")
133//! @yah:verify("THIS TICKET'S STATED ACCEPTANCE TEST — \\\"green on the healthy fleet, https://yah.dev/ through both declared front doors, agreeing with what verify resolves\\\" — WAS **NOT** MET, AND NOT BECAUSE OF THIS CHANGE. The fleet is not healthy right now: the mesh coordination server is down (`cloud.mesh.yah.dev` -&gt; 15.204.89.240 REFUSES :443 and :80 while :22 answers, and `tailscale status` reports this machine logged out with \\\"fetch control key ... connection refused\\\"), so NO 100.64.0.0/10 address is reachable from here and the mesh half of the check cannot run at all. Filed as R858 with the full measurement chain. I am recording this as unmet rather than reporting a partial green.")
134//! @yah:verify("WHAT THE LIVE RUN DID PROVE, and it is more than nothing: `yah cloud ingress verify --path .` from the freshly installed binary fetched `https://yah.dev/.well-known/yah-publish.json` over the real public internet, parsed it, and — because no backend beacon could be read across the dead mesh — printed exactly the right sentence instead of a pass: \\\"https://yah.dev answers with a publish beacon, but no discovered backend served one to compare it against, so this proves the hostname is up and NOT that it is fronting the discovered backend\\\". Independently confirmed by hand: `curl https://yah.dev/` = HTTP 200 in 0.81s and the beacon is `{\\\"prefix\\\":\\\"bundle/yah-marketing\\\",\\\"digest\\\":\\\"bfb47cd42468b080c474193fa6091ec273b6c99ab5a8a3b91d9c847cd7278551\\\",\\\"files\\\":39}`. So the public leg ran end to end against production and, on a real unplanned failure it was never designed for, refused to overclaim — which is the behaviour this ticket exists to install. `--skip-public` also exercised live: every verdict then reads \\\"the public path was not checked for yah.dev — this verdict speaks only for the mesh side, which is necessary and not sufficient\\\", and the summary names the dropped claim.")
135//! @yah:next("RE-RUN THE ACCEPTANCE TEST ONCE R858 CLEARS — it is one command and it is the only thing outstanding on this ticket: `yah cloud ingress verify --path .` must exit 0 with both front doors' rules reporting the mesh dial open AND the public beacon matching the backend's. Until the mesh is reachable that run measures nothing about the fourth claim.")
136//! @yah:notify_on(R858, "The mesh is reachable again — run this ticket's outstanding acceptance test: `yah cloud ingress verify --path .` must exit 0 with BOTH front doors reporting the mesh dial open AND the public beacon digest matching the backend's. It could not be run at landing time because no 100.64.0.0/10 address answered. If it passes, that closes the last open item here; if it fails, read which of the two legs failed before touching the code — the mesh leg is R844-F16's and predates this change.")
137
138use std::collections::BTreeMap;
139use std::fmt;
140
141use crate::reconciler::ingress::{Collation, IngressPlan, IngressRule};
142use crate::reconciler::service_discovery::ServiceRecordFanout;
143
144/// What a TCP connect to one resolved `host:port` actually did.
145///
146/// Two arms, no `Unknown`: unlike a discovery read — which can fail to *ask* a
147/// node, and whose whole vocabulary
148/// ([`RecordVisibility`](crate::reconciler::RecordVisibility)) exists to keep
149/// that apart from an empty answer — a dial either completed or it did not.
150/// The attempt is the evidence.
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub enum DialOutcome {
153    /// The connect completed. This is the only positive evidence in the whole
154    /// ingress stack that anything is listening.
155    Open {
156        /// How long the connect took, for an operator eyeballing a slow path.
157        millis: u128,
158    },
159    /// The connect did not complete, in the transport's own words — refused,
160    /// timed out, no route.
161    Closed(String),
162}
163
164impl DialOutcome {
165    /// One short label for a summary line.
166    pub fn as_str(&self) -> &'static str {
167        match self {
168            Self::Open { .. } => "open",
169            Self::Closed(_) => "CLOSED",
170        }
171    }
172
173    /// Did anything answer?
174    pub fn is_open(&self) -> bool {
175        matches!(self, Self::Open { .. })
176    }
177}
178
179impl fmt::Display for DialOutcome {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        match self {
182            Self::Open { millis } => write!(f, "open ({millis}ms)"),
183            Self::Closed(why) => write!(f, "CLOSED — {why}"),
184        }
185    }
186}
187
188/// One dialed address and what came back.
189#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct EndpointCheck {
191    /// The `host:port` dialed — exactly what the front door would dial.
192    pub address: String,
193    pub outcome: DialOutcome,
194}
195
196/// How one rule's placement resolved against a live discovery read.
197///
198/// Recorded *during* resolution rather than reconstructed after it, because two
199/// of these three facts are unrecoverable from the resolved plan: whether the
200/// address came from a pin or from the fleet, and which placement node supplied
201/// it. Both are exactly what an operator needs to act on a failure.
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct RuleResolution {
204    pub hostname: String,
205    pub slot: String,
206    /// The rule already carried an address before the read — the slot pins
207    /// `upstream_host`, and the pin wins everywhere (R844-F5/F12).
208    ///
209    /// Reported because it changes what the dial *means*: a pinned rule's
210    /// connect measures whether a **declaration** answers, and the fleet has no
211    /// say in what was dialed. That is the 127.0.0.1 case above.
212    pub pinned: bool,
213    /// Placement nodes that answered with a ready record on this rule's port.
214    pub covered: Vec<String>,
215    /// Placement nodes that did not, each with the reason — an `Unknown` node's
216    /// own words, or "answered, and has no such record".
217    pub missing: Vec<(String, String)>,
218}
219
220impl RuleResolution {
221    /// Key under which a resolution is looked up once collation has grouped the
222    /// rules by node.
223    ///
224    /// `(hostname, slot)` rather than hostname alone: one hostname is fronted
225    /// through exactly one provider (`collate_front_doors` rejects otherwise),
226    /// but the slot is what an operator opens to fix a finding, so carrying it
227    /// costs nothing and a message without it points at no file.
228    pub fn key(&self) -> (String, String) {
229        (self.hostname.clone(), self.slot.clone())
230    }
231}
232
233/// Everything the resolution pass learned, keyed for the verify pass.
234pub type RuleResolutions = BTreeMap<(String, String), RuleResolution>;
235
236/// Why one rule is not proven to serve.
237#[derive(Debug, Clone, PartialEq, Eq)]
238pub enum VerifyFinding {
239    /// No port resolved — the slot declares `fronted = true`, pins no number,
240    /// and no ready record supplied one.
241    PortUnresolved,
242    /// No address resolved. The detail carries whether the read was complete,
243    /// because "the workload is not up" and "the node that has it could not be
244    /// seen" are different facts (R844-F4).
245    NoBackend { detail: String },
246    /// Fewer backends than the rule's declared placement. Fatal on purpose —
247    /// see the module doc.
248    PartialPlacement {
249        covered: Vec<String>,
250        missing: Vec<(String, String)>,
251    },
252    /// A resolved address did not answer. The one finding no offline pass could
253    /// ever produce.
254    Unreachable {
255        address: String,
256        why: String,
257        /// The address came from the slot's `upstream_host` rather than from a
258        /// service record. Carried because it changes *which* claim just got
259        /// falsified — a discovered address that refuses means the record and
260        /// the world disagree, a pinned one means the TOML is wrong — and a
261        /// message that names the wrong one sends the operator to the wrong
262        /// file. Caught by running this verb against a mirror with a bogus pin.
263        from_pin: bool,
264    },
265    /// No resolution was recorded for this rule at all — a bug in the caller's
266    /// wiring rather than a fact about the fleet, reported instead of silently
267    /// rendering the rule as fine.
268    Unresolved,
269    /// The public hostname did not answer at all — DNS, TLS, connect, timeout
270    /// (R844-F18). The one finding that speaks for the public rather than for
271    /// the mesh.
272    PublicPathFailed { hostname: String, why: String },
273    /// The public hostname answered with a status the public would read as
274    /// broken. `503` here is the 2026-09-03 outage exactly: every mesh dial
275    /// open, every record correct, and this the only signal that disagreed.
276    PublicPathStatus { hostname: String, status: u16 },
277    /// The public path and the backend the service record names are serving
278    /// **different publishes** (R844-F18). The finding this ticket exists for:
279    /// it is true precisely when the front door is dialing something other than
280    /// the backend the rest of this report just proved reachable.
281    PublicBackendDivergence {
282        hostname: String,
283        public_digest: String,
284        address: String,
285        backend_digest: String,
286    },
287}
288
289impl VerifyFinding {
290    /// Human-readable finding, in the imperative where there is something to do.
291    pub fn message(&self) -> String {
292        match self {
293            Self::PortUnresolved => "no port resolved — the slot declares `fronted = true` \
294                 without `port`, and no in-scope node reported a ready record naming one. \
295                 Either the workload is not up, or its yubaba predates named ports and the \
296                 record is ambiguous; pin `port = <n>` on the slot to publish it anyway."
297                .to_string(),
298            Self::NoBackend { detail } => {
299                format!("no backend resolved — {detail}")
300            }
301            Self::PartialPlacement { covered, missing } => format!(
302                "resolves {} of {} declared placement node(s) — a SUBSET that renders like a \
303                 whole front door. Serving: {}. Missing: {}.",
304                covered.len(),
305                covered.len() + missing.len(),
306                covered.join(", "),
307                missing
308                    .iter()
309                    .map(|(m, why)| format!("{m} ({why})"))
310                    .collect::<Vec<_>>()
311                    .join("; ")
312            ),
313            Self::Unreachable {
314                address,
315                why,
316                from_pin,
317            } => {
318                let origin = if *from_pin {
319                    "The slot PINS this address in `upstream_host`, so nothing discovered it and \
320                     nothing but this connect could have contradicted it — fix the pin, or drop \
321                     it and let the fleet answer."
322                } else {
323                    "A ready service record is yubaba's OPINION; this connect is the measurement, \
324                     and they disagree (R844-B11)."
325                };
326                format!("{address} did not answer — {why}. {origin}")
327            }
328            Self::Unresolved => "no discovery resolution was recorded for this rule — the \
329                 verify pass planned it but never resolved it, which is a wiring bug in the \
330                 caller, not a fact about the fleet."
331                .to_string(),
332            Self::PublicPathFailed { hostname, why } => format!(
333                "https://{hostname}/ did not answer — {why}. Every line above measures the \
334                 MESH side; this is the only one that measures what the public gets, so a \
335                 report that is otherwise clean means the front door is not reaching the \
336                 backend the records name."
337            ),
338            Self::PublicPathStatus { hostname, status } => format!(
339                "https://{hostname}/ answered HTTP {status}. The backend above is reachable at \
340                 the port its service record advertises, so the front door is configured to \
341                 dial something else — that pair of facts is exactly the 2026-09-03 outage \
342                 (R844-T10), where a redeploy moved the port and nothing reconfigured the door."
343            ),
344            Self::PublicBackendDivergence {
345                hostname,
346                public_digest,
347                address,
348                backend_digest,
349            } => format!(
350                "https://{hostname}/ and the backend its service record names are serving \
351                 DIFFERENT publishes: the public path returns digest {public_digest}, {address} \
352                 returns {backend_digest}. The hostname answers, so nothing else in this report \
353                 can see it — the front door is dialing a backend other than the discovered one, \
354                 or an edge cache is still holding the previous publish."
355            ),
356        }
357    }
358}
359
360/// One rule's verdict on one front door.
361#[derive(Debug, Clone, PartialEq, Eq)]
362pub struct RuleVerdict {
363    /// Node whose front door publishes this rule.
364    pub machine: String,
365    /// Front-door provider, as `IngressProvider::as_str`.
366    pub provider: String,
367    pub hostname: String,
368    pub slot: String,
369    pub port: Option<u16>,
370    /// The address came from the slot's `upstream_host` pin, so the dial below
371    /// measured a declaration rather than a discovered fact.
372    pub pinned: bool,
373    /// Every resolved backend, dialed.
374    pub endpoints: Vec<EndpointCheck>,
375    /// Empty means proven: every declared placement node resolved and every
376    /// resolved address answered.
377    pub findings: Vec<VerifyFinding>,
378    /// Non-fatal context — today, the placement gaps of a *pinned* rule, where
379    /// the pin overrides placement so a gap is worth saying and not worth
380    /// failing.
381    pub notes: Vec<String>,
382}
383
384impl RuleVerdict {
385    /// Proven to serve.
386    pub fn is_ok(&self) -> bool {
387        self.findings.is_empty()
388    }
389
390    /// Every address this verdict dialed, in resolution order.
391    pub fn addresses(&self) -> Vec<String> {
392        self.endpoints
393            .iter()
394            .map(|e| e.address.clone())
395            .collect()
396    }
397
398    /// This rule's port for a message, or `<unresolved>` — the same spelling
399    /// [`IngressRule::port_label`] uses, so a verify line and a collate line
400    /// describing one unresolved rule read identically.
401    pub fn port_label(&self) -> String {
402        self.port
403            .map(|p| p.to_string())
404            .unwrap_or_else(|| "<unresolved>".to_string())
405    }
406}
407
408/// Every rule on every collated front door, verified.
409#[derive(Debug, Clone, Default, PartialEq, Eq)]
410pub struct VerifyReport {
411    pub verdicts: Vec<RuleVerdict>,
412}
413
414impl VerifyReport {
415    /// Rules that are not proven to serve.
416    pub fn failures(&self) -> impl Iterator<Item = &RuleVerdict> {
417        self.verdicts.iter().filter(|v| !v.is_ok())
418    }
419
420    /// Nothing to fix.
421    pub fn is_clean(&self) -> bool {
422        self.failures().next().is_none()
423    }
424}
425
426// ── The public path (R844-F18) ───────────────────────────────────────────────
427
428/// What one HTTP GET of a publish beacon returned — the caller's measurement,
429/// handed in as data like every other input on this seam.
430///
431/// The URL is always
432/// `<base>/.well-known/yah-publish.json` ([`publish_beacon::BEACON_KEY`]),
433/// because that object is the only thing on either side of the comparison that
434/// *identifies which publish is being served*. A bare `GET /` cannot do this
435/// job: a front door pointed at the wrong backend still answers 200 with a
436/// plausible page, which is how `yah.dev` stayed frozen for nineteen days with
437/// every signal green.
438///
439/// [`publish_beacon::BEACON_KEY`]: crate::reconciler::publish_beacon::BEACON_KEY
440#[derive(Debug, Clone, PartialEq, Eq)]
441pub enum BeaconFetch {
442    /// The request completed.
443    Answered {
444        status: u16,
445        /// The `digest` field of the beacon, when the body parsed as one.
446        /// `None` when it did not — a 404, or a hostname fronting something
447        /// that is not a mesofact publish. That is a limit on the comparison,
448        /// not a failure of the host, and is reported as a note.
449        digest: Option<String>,
450    },
451    /// The request did not complete — DNS, TLS, connect, timeout.
452    Failed(String),
453}
454
455/// Everything the caller measured on the public path, keyed for the pure pass.
456///
457/// Two maps rather than one because the two sides are addressed differently and
458/// deduplicated differently: a hostname is fetched once however many front doors
459/// publish it, and a backend address is fetched once however many hostnames
460/// resolve to it.
461#[derive(Debug, Clone, Default, PartialEq, Eq)]
462pub struct PublicReadings {
463    /// `hostname` → what `https://<hostname>/.well-known/yah-publish.json`
464    /// returned. A hostname absent from this map was not measured, and
465    /// [`apply_public_path`] says so rather than passing it.
466    pub public: BTreeMap<String, BeaconFetch>,
467    /// `host:port` → what `http://<host:port>/.well-known/yah-publish.json`
468    /// returned, read over the mesh. This is "what the record claims", made
469    /// comparable.
470    pub backends: BTreeMap<String, BeaconFetch>,
471}
472
473/// Add the public-path verdict to a report that so far only knows the mesh
474/// side (R844-F18).
475///
476/// ## The claim this closes
477///
478/// [`verify_collation`] answers *"is the backend reachable at the port its
479/// record advertises"*. This answers *"and does the front door actually reach
480/// it"* — the fourth row of the table in the module docs, which read `nothing
481/// yet` until this landed. It closes it by comparing two publish beacons: the
482/// one the **public** gets through the real front door, and the one the
483/// **backend the record names** serves. They agree only if the door is dialing
484/// that backend.
485///
486/// ## Detector, not oracle — read this before using it as a gate
487///
488/// It compares two *current* states. It cannot tell you what a config change is
489/// about to do, because the front door has not been reconfigured yet: run it
490/// before and after an apply and require both green. That is enough for the
491/// failure it was built for — the divergence appears the moment the workload is
492/// redeployed onto a new port — and it is not a general "is this edit safe"
493/// oracle. Nothing in this module is.
494///
495/// ## The limit that cannot be designed away here
496///
497/// The public fetch goes wherever **DNS** sends it. A hostname published
498/// through two front doors is measured at one of them, and this pass cannot say
499/// which — so a divergence that affects only the other door reads as clean. The
500/// verdicts for such a hostname carry a note saying so; closing it needs a fetch
501/// pinned to each door's public address with a `Host` override, which needs a
502/// public IP per door that nothing in the collation carries today.
503pub fn apply_public_path(report: &mut VerifyReport, readings: &PublicReadings) {
504    let doors_per_hostname = report.verdicts.iter().fold(
505        BTreeMap::<String, usize>::new(),
506        |mut acc, v| {
507            *acc.entry(v.hostname.clone()).or_default() += 1;
508            acc
509        },
510    );
511
512    for verdict in &mut report.verdicts {
513        let Some(fetched) = readings.public.get(&verdict.hostname) else {
514            verdict.notes.push(format!(
515                "the public path was not checked for {} — this verdict speaks only for the mesh \
516                 side, which is necessary and not sufficient",
517                verdict.hostname
518            ));
519            continue;
520        };
521
522        let public_digest = match fetched {
523            BeaconFetch::Failed(why) => {
524                verdict.findings.push(VerifyFinding::PublicPathFailed {
525                    hostname: verdict.hostname.clone(),
526                    why: why.clone(),
527                });
528                continue;
529            }
530            BeaconFetch::Answered { status, .. } if !(200..300).contains(status) => {
531                verdict.findings.push(VerifyFinding::PublicPathStatus {
532                    hostname: verdict.hostname.clone(),
533                    status: *status,
534                });
535                continue;
536            }
537            BeaconFetch::Answered { digest, .. } => digest,
538        };
539
540        let Some(public_digest) = public_digest else {
541            verdict.notes.push(format!(
542                "https://{} answers, but serves no publish beacon, so the public path could not \
543                 be COMPARED against the backend — only that something is there",
544                verdict.hostname
545            ));
546            continue;
547        };
548
549        let mut compared = false;
550        for endpoint in &verdict.endpoints {
551            match readings.backends.get(&endpoint.address) {
552                Some(BeaconFetch::Answered {
553                    digest: Some(backend_digest),
554                    ..
555                }) => {
556                    compared = true;
557                    if backend_digest != public_digest {
558                        verdict.findings.push(VerifyFinding::PublicBackendDivergence {
559                            hostname: verdict.hostname.clone(),
560                            public_digest: public_digest.clone(),
561                            address: endpoint.address.clone(),
562                            backend_digest: backend_digest.clone(),
563                        });
564                    }
565                }
566                // A backend that answers without a beacon, or does not answer
567                // at all, leaves nothing to compare against. Not a finding of
568                // its own: `verify_collation` already dialed it and said what
569                // it found, and a second opinion phrased as an error would
570                // double-count one fact.
571                _ => {}
572            }
573        }
574
575        if !compared {
576            verdict.notes.push(format!(
577                "https://{} answers with a publish beacon, but no discovered backend served one \
578                 to compare it against, so this proves the hostname is up and NOT that it is \
579                 fronting the discovered backend",
580                verdict.hostname
581            ));
582        } else if doors_per_hostname
583            .get(&verdict.hostname)
584            .copied()
585            .unwrap_or(0)
586            > 1
587        {
588            // Only worth saying once a comparison actually happened: on a rule
589            // where nothing could be compared, the note above is the finding
590            // and this one is noise stacked on top of it.
591            verdict.notes.push(format!(
592                "{} is published through more than one front door and the public fetch went \
593                 wherever DNS sent it, so this compares ONE of them",
594                verdict.hostname
595            ));
596        }
597    }
598}
599
600/// Fill one plan's rules from a live fanout **without stopping at the first
601/// rule that cannot be dialed**, recording how each one resolved.
602///
603/// Deliberately not [`IngressPlan::resolve_upstreams_from`], though it applies
604/// the same precedence (a rule that already has an address keeps it — the pin
605/// always wins) and reads the same `upstreams_for`. That method `bail!`s on the
606/// first undialable rule, which is right for an *apply*: publishing a front door
607/// that is 80% correct is worse than publishing none. It is wrong for a
608/// *verifier*, whose entire job is the complete picture — an operator who fixes
609/// one rule and re-runs only to be told about the next one has been handed a
610/// linked list instead of a report.
611///
612/// Ports must already be resolved ([`IngressPlan::resolve_ports_from`]):
613/// discovery matches records by port, so a portless rule resolves no address
614/// either and is reported as both.
615pub fn resolve_upstreams_reporting(
616    plan: &mut IngressPlan,
617    fanout: &ServiceRecordFanout,
618) -> Vec<RuleResolution> {
619    let mut out = Vec::new();
620    for rule in &mut plan.rules {
621        let pinned = !rule.upstream_hosts.is_empty();
622        if !pinned {
623            rule.upstream_hosts = fanout.upstreams_for(rule);
624        }
625        let (covered, missing) = placement_coverage(rule, fanout);
626        out.push(RuleResolution {
627            hostname: rule.hostname.clone(),
628            slot: rule.slot.clone(),
629            pinned,
630            covered,
631            missing,
632        });
633    }
634    out
635}
636
637/// Which of a rule's declared placement nodes actually supplied a backend, and
638/// why each of the others did not.
639///
640/// Mirrors [`ServiceRecordFanout::upstreams_for`]'s matching (by port, scoped to
641/// the rule's placement) so the two cannot disagree about what "covered" means —
642/// it answers *which nodes* produced that method's addresses, one level of
643/// detail below what it returns.
644fn placement_coverage(
645    rule: &IngressRule,
646    fanout: &ServiceRecordFanout,
647) -> (Vec<String>, Vec<(String, String)>) {
648    let mut covered = Vec::new();
649    let mut missing = Vec::new();
650    for machine in &rule.machines {
651        match fanout.nodes.get(machine.as_str()) {
652            None => missing.push((
653                machine.clone(),
654                "was never asked — it is not in the set this read fanned out over".to_string(),
655            )),
656            Some(visibility) => match visibility.unknown_reason() {
657                Some(reason) => missing.push((machine.clone(), reason.to_string())),
658                None => {
659                    let serving = rule.port.is_some_and(|port| {
660                        visibility
661                            .records()
662                            .iter()
663                            .any(|r| r.ports.contains(&port))
664                    });
665                    if serving {
666                        covered.push(machine.clone());
667                    } else {
668                        missing.push((
669                            machine.clone(),
670                            match rule.port {
671                                Some(port) => format!(
672                                    "answered, and reports no ready record on port {port}"
673                                ),
674                                None => "the rule has no resolved port to match a record on"
675                                    .to_string(),
676                            },
677                        ));
678                    }
679                }
680            },
681        }
682    }
683    (covered, missing)
684}
685
686/// Verify every rule on every collated front door: check the resolution, then
687/// dial what it produced.
688///
689/// `dial` is the caller's TCP connect. Each distinct address is dialed **once**
690/// — a rule published through two front doors is the same backend twice, and
691/// two connects would be two chances to disagree about one fact.
692///
693/// `read_note` is [`ServiceRecordFanout::unknown_note`]: when the fanout could
694/// not see part of the fleet, an empty resolution is `UNKNOWN`, not `absent`,
695/// and every [`VerifyFinding::NoBackend`] says so rather than asserting the
696/// workload is down.
697///
698/// Verdicts come back in front-door order, then rule order — the same walk the
699/// collation itself renders in, so a caller may stream the two side by side
700/// rather than looking each verdict up.
701pub fn verify_collation<D>(
702    collation: &Collation,
703    resolutions: &RuleResolutions,
704    read_note: Option<&str>,
705    mut dial: D,
706) -> VerifyReport
707where
708    D: FnMut(&str) -> DialOutcome,
709{
710    let mut dialed: BTreeMap<String, DialOutcome> = BTreeMap::new();
711    let mut verdicts = Vec::new();
712
713    for door in &collation.front_doors {
714        for rule in &door.rules {
715            let mut findings = Vec::new();
716            let mut notes = Vec::new();
717            let key = (rule.hostname.clone(), rule.slot.clone());
718            let resolution = resolutions.get(&key);
719
720            let pinned = resolution.is_some_and(|r| r.pinned);
721            if resolution.is_none() {
722                findings.push(VerifyFinding::Unresolved);
723            }
724
725            if rule.port.is_none() {
726                findings.push(VerifyFinding::PortUnresolved);
727            }
728
729            if rule.upstream_hosts.is_empty() {
730                findings.push(VerifyFinding::NoBackend {
731                    detail: match read_note {
732                        Some(note) => format!(
733                            "and the discovery read was PARTIAL, so this is UNKNOWN rather than \
734                             empty: {note}"
735                        ),
736                        None => "every node in scope answered and none reports a ready record \
737                                 for it, so the workload is not serving"
738                            .to_string(),
739                    },
740                });
741            } else if let Some(res) = resolution {
742                if !res.missing.is_empty() {
743                    if pinned {
744                        // The pin overrides placement, so a gap is not a
745                        // shortfall in what gets published — but it IS the
746                        // shape that hid the 127.0.0.1 drift, so it is said
747                        // out loud rather than dropped.
748                        notes.push(format!(
749                            "slot pins `upstream_host`, so this dialed a DECLARATION, not a \
750                             discovered address; {} placement node(s) report no ready record \
751                             for it: {}",
752                            res.missing.len(),
753                            res.missing
754                                .iter()
755                                .map(|(m, why)| format!("{m} ({why})"))
756                                .collect::<Vec<_>>()
757                                .join("; ")
758                        ));
759                    } else {
760                        findings.push(VerifyFinding::PartialPlacement {
761                            covered: res.covered.clone(),
762                            missing: res.missing.clone(),
763                        });
764                    }
765                }
766            }
767
768            let mut endpoints = Vec::new();
769            if let Ok(addrs) = rule.upstreams() {
770                for address in addrs {
771                    let outcome = match dialed.get(&address) {
772                        Some(prior) => prior.clone(),
773                        None => {
774                            let outcome = dial(&address);
775                            dialed.insert(address.clone(), outcome.clone());
776                            outcome
777                        }
778                    };
779                    if let DialOutcome::Closed(why) = &outcome {
780                        findings.push(VerifyFinding::Unreachable {
781                            address: address.clone(),
782                            why: why.clone(),
783                            from_pin: pinned,
784                        });
785                    }
786                    endpoints.push(EndpointCheck { address, outcome });
787                }
788            }
789
790            verdicts.push(RuleVerdict {
791                machine: door.machine.clone(),
792                provider: door.provider.as_str().to_string(),
793                hostname: rule.hostname.clone(),
794                slot: rule.slot.clone(),
795                port: rule.port,
796                pinned,
797                endpoints,
798                findings,
799                notes,
800            });
801        }
802    }
803
804    VerifyReport { verdicts }
805}
806
807#[cfg(test)]
808mod tests {
809    use super::*;
810    use crate::config::IngressProvider;
811    use crate::reconciler::ingress::{collate_front_doors, PlannedEdge};
812    use crate::reconciler::service_discovery::{DiscoveredRecord, UnknownReason};
813
814    fn rule(hostname: &str, port: Option<u16>, machines: &[&str]) -> IngressRule {
815        IngressRule {
816            hostname: hostname.to_string(),
817            port,
818            slot: "bundle".to_string(),
819            provider_id: None,
820            machines: machines.iter().map(|m| m.to_string()).collect(),
821            upstream_hosts: vec![],
822        }
823    }
824
825    fn plan(rules: Vec<IngressRule>, front_doors: &[&str]) -> IngressPlan {
826        IngressPlan {
827            provider: IngressProvider::Passway,
828            rules,
829            front_doors: front_doors.iter().map(|m| m.to_string()).collect(),
830            tunnel_id: None,
831            edge_provider_id: None,
832            image: None,
833        }
834    }
835
836    fn record(ident: &str, mesh_ip: &str, ports: &[u16]) -> DiscoveredRecord {
837        DiscoveredRecord {
838            ident: ident.to_string(),
839            mesh_ip: mesh_ip.to_string(),
840            ports: ports.to_vec(),
841            named_ports: Default::default(),
842        }
843    }
844
845    /// Plan → resolve → collate → verify, the whole pipeline the CLI runs.
846    fn run(
847        mut plans: Vec<(&str, &str, IngressPlan)>,
848        fanout: &ServiceRecordFanout,
849        dial: impl FnMut(&str) -> DialOutcome,
850    ) -> VerifyReport {
851        let mut resolutions = RuleResolutions::new();
852        let mut planned = Vec::new();
853        for (service, env, plan) in &mut plans {
854            for res in resolve_upstreams_reporting(plan, fanout) {
855                resolutions.insert(res.key(), res);
856            }
857            planned.push(PlannedEdge {
858                service: service.to_string(),
859                env: env.to_string(),
860                plan: plan.clone(),
861            });
862        }
863        let collation = collate_front_doors(&planned).unwrap();
864        let note = fanout.unknown_note();
865        verify_collation(&collation, &resolutions, note.as_deref(), dial)
866    }
867
868    fn always_open(_: &str) -> DialOutcome {
869        DialOutcome::Open { millis: 1 }
870    }
871
872    #[test]
873    fn a_resolved_rule_whose_endpoint_answers_is_clean() {
874        let mut fanout = ServiceRecordFanout::default();
875        fanout.push_answer(
876            "us-east-001",
877            vec![record("yah-marketing", "100.64.0.3", &[8080])],
878        );
879
880        let report = run(
881            vec![(
882                "yah-marketing",
883                "cloud",
884                plan(
885                    vec![rule("yah.dev", Some(8080), &["us-east-001"])],
886                    &["us-east-001"],
887                ),
888            )],
889            &fanout,
890            always_open,
891        );
892
893        assert!(report.is_clean(), "{:#?}", report.verdicts);
894        assert_eq!(report.verdicts.len(), 1);
895        assert_eq!(report.verdicts[0].addresses(), vec!["100.64.0.3:8080"]);
896        assert!(!report.verdicts[0].pinned);
897    }
898
899    #[test]
900    fn a_ready_record_whose_address_refuses_is_a_failure() {
901        // R844-B11 in miniature: the record is Ready and every offline check
902        // passes; only the connect knows.
903        let mut fanout = ServiceRecordFanout::default();
904        fanout.push_answer(
905            "us-west-001",
906            vec![record("yah-marketing", "100.64.0.3", &[4325])],
907        );
908
909        let report = run(
910            vec![(
911                "yah-marketing",
912                "cloud",
913                plan(
914                    vec![rule("yah.dev", Some(4325), &["us-west-001"])],
915                    &["us-west-001"],
916                ),
917            )],
918            &fanout,
919            |_| DialOutcome::Closed("connection refused".to_string()),
920        );
921
922        assert!(!report.is_clean());
923        let msg = report.verdicts[0].findings[0].message();
924        assert!(msg.contains("100.64.0.3:4325"), "{msg}");
925        assert!(msg.contains("connection refused"), "{msg}");
926        assert!(
927            msg.contains("OPINION"),
928            "a DISCOVERED address that refuses means the record and the world \
929             disagree, and the message has to say which: {msg}"
930        );
931    }
932
933    #[test]
934    fn an_unreachable_pin_blames_the_toml_not_a_service_record() {
935        // Found by running the verb against a mirror pinning a dead address:
936        // the message told the operator a service record disagreed with the
937        // world, when nothing had discovered anything — the address came
938        // straight off the slot, and that is the file to open.
939        let mut fanout = ServiceRecordFanout::default();
940        fanout.push_answer(
941            "us-east-001",
942            vec![record("yah-marketing", "100.64.0.3", &[8080])],
943        );
944
945        let mut pinned_rule = rule("yah.dev", Some(8080), &["us-east-001"]);
946        pinned_rule.upstream_hosts = vec!["100.64.0.99".to_string()];
947
948        let report = run(
949            vec![(
950                "yah-marketing",
951                "cloud",
952                plan(vec![pinned_rule], &["us-east-001"]),
953            )],
954            &fanout,
955            |_| DialOutcome::Closed("connection timed out".to_string()),
956        );
957
958        let msg = report.verdicts[0].findings[0].message();
959        assert!(msg.contains("upstream_host"), "{msg}");
960        assert!(
961            !msg.contains("OPINION"),
962            "no record was consulted, so none can be blamed: {msg}"
963        );
964    }
965
966    #[test]
967    fn resolving_a_subset_of_the_declared_placement_fails() {
968        // The failure class this verb exists to remove: one of two nodes
969        // answers, the rule renders and dials fine, and half the front door is
970        // silently absent.
971        let mut fanout = ServiceRecordFanout::default();
972        fanout.push_answer(
973            "us-east-001",
974            vec![record("yah-marketing", "100.64.0.3", &[8080])],
975        );
976        fanout.push_answer("us-west-001", vec![]);
977
978        let report = run(
979            vec![(
980                "yah-marketing",
981                "cloud",
982                plan(
983                    vec![rule("yah.dev", Some(8080), &["us-east-001", "us-west-001"])],
984                    &["us-east-001"],
985                ),
986            )],
987            &fanout,
988            always_open,
989        );
990
991        assert!(!report.is_clean(), "a subset must not pass");
992        let msg = report.verdicts[0].findings[0].message();
993        assert!(msg.contains("1 of 2"), "{msg}");
994        assert!(msg.contains("us-west-001"), "{msg}");
995        // The address it DID resolve is still reported — a failure that hides
996        // the working half is a worse report, not a stricter one.
997        assert_eq!(report.verdicts[0].addresses(), vec!["100.64.0.3:8080"]);
998    }
999
1000    #[test]
1001    fn an_unseen_placement_node_fails_as_unknown_not_as_down() {
1002        let mut fanout = ServiceRecordFanout::default();
1003        fanout.push_answer(
1004            "us-east-001",
1005            vec![record("yah-marketing", "100.64.0.3", &[8080])],
1006        );
1007        fanout.push_unknown(
1008            "us-west-001",
1009            UnknownReason::Unreachable("no route to host".to_string()),
1010        );
1011
1012        let report = run(
1013            vec![(
1014                "yah-marketing",
1015                "cloud",
1016                plan(
1017                    vec![rule("yah.dev", Some(8080), &["us-east-001", "us-west-001"])],
1018                    &["us-east-001"],
1019                ),
1020            )],
1021            &fanout,
1022            always_open,
1023        );
1024
1025        assert!(!report.is_clean());
1026        let msg = report.verdicts[0].findings[0].message();
1027        assert!(msg.contains("us-west-001"), "{msg}");
1028        assert!(msg.contains("no route to host"), "{msg}");
1029    }
1030
1031    #[test]
1032    fn a_rule_with_no_record_anywhere_reports_no_backend_on_a_complete_read() {
1033        let mut fanout = ServiceRecordFanout::default();
1034        fanout.push_answer("us-east-001", vec![]);
1035
1036        let report = run(
1037            vec![(
1038                "yah-marketing",
1039                "cloud",
1040                plan(
1041                    vec![rule("yah.dev", Some(8080), &["us-east-001"])],
1042                    &["us-east-001"],
1043                ),
1044            )],
1045            &fanout,
1046            always_open,
1047        );
1048
1049        assert!(!report.is_clean());
1050        let msg = report.verdicts[0].findings[0].message();
1051        assert!(msg.contains("not serving"), "{msg}");
1052        assert!(
1053            !msg.contains("PARTIAL"),
1054            "a complete read must not hedge: {msg}"
1055        );
1056    }
1057
1058    #[test]
1059    fn a_rule_with_no_record_on_a_partial_read_says_unknown_rather_than_down() {
1060        let mut fanout = ServiceRecordFanout::default();
1061        fanout.push_unknown(
1062            "us-east-001",
1063            UnknownReason::EndpointAbsent("GET … returned 404".to_string()),
1064        );
1065
1066        let report = run(
1067            vec![(
1068                "yah-marketing",
1069                "cloud",
1070                plan(
1071                    vec![rule("yah.dev", Some(8080), &["us-east-001"])],
1072                    &["us-east-001"],
1073                ),
1074            )],
1075            &fanout,
1076            always_open,
1077        );
1078
1079        assert!(!report.is_clean());
1080        let msg = report.verdicts[0].findings[0].message();
1081        assert!(msg.contains("UNKNOWN"), "{msg}");
1082        assert!(msg.contains("404"), "{msg}");
1083    }
1084
1085    #[test]
1086    fn a_portless_rule_reports_both_halves_rather_than_only_the_address() {
1087        // The `fronted = true` shape with nothing to match on: it must not read
1088        // as "the address is missing" alone.
1089        let mut fanout = ServiceRecordFanout::default();
1090        fanout.push_answer("us-east-001", vec![]);
1091
1092        let report = run(
1093            vec![(
1094                "yah-marketing",
1095                "cloud",
1096                plan(vec![rule("yah.dev", None, &["us-east-001"])], &["us-east-001"]),
1097            )],
1098            &fanout,
1099            always_open,
1100        );
1101
1102        let findings = &report.verdicts[0].findings;
1103        assert!(
1104            findings
1105                .iter()
1106                .any(|f| matches!(f, VerifyFinding::PortUnresolved)),
1107            "{findings:#?}"
1108        );
1109        assert!(
1110            findings
1111                .iter()
1112                .any(|f| matches!(f, VerifyFinding::NoBackend { .. })),
1113            "{findings:#?}"
1114        );
1115    }
1116
1117    #[test]
1118    fn a_pinned_upstream_is_dialed_and_flagged_as_a_declaration() {
1119        // The 127.0.0.1 case. The pin wins, so the dial is still performed —
1120        // but the verdict has to say the address came from a TOML, and the
1121        // placement gap it papers over has to be visible.
1122        let mut fanout = ServiceRecordFanout::default();
1123        fanout.push_answer("us-east-001", vec![]);
1124
1125        let mut pinned_rule = rule("yah.dev", Some(8080), &["us-east-001"]);
1126        pinned_rule.upstream_hosts = vec!["127.0.0.1".to_string()];
1127
1128        let mut dialed: Vec<String> = Vec::new();
1129        let report = run(
1130            vec![(
1131                "yah-marketing",
1132                "cloud",
1133                plan(vec![pinned_rule], &["us-east-001"]),
1134            )],
1135            &fanout,
1136            |addr| {
1137                dialed.push(addr.to_string());
1138                DialOutcome::Open { millis: 1 }
1139            },
1140        );
1141
1142        assert_eq!(dialed, vec!["127.0.0.1:8080"]);
1143        let v = &report.verdicts[0];
1144        assert!(v.pinned);
1145        assert!(v.is_ok(), "a pin that answers is not a failure: {v:#?}");
1146        assert_eq!(v.notes.len(), 1, "{:#?}", v.notes);
1147        assert!(v.notes[0].contains("DECLARATION"), "{}", v.notes[0]);
1148        assert!(v.notes[0].contains("us-east-001"), "{}", v.notes[0]);
1149    }
1150
1151    #[test]
1152    fn one_backend_published_through_two_front_doors_is_dialed_once() {
1153        let mut fanout = ServiceRecordFanout::default();
1154        fanout.push_answer(
1155            "us-east-001",
1156            vec![record("yah-marketing", "100.64.0.3", &[8080])],
1157        );
1158
1159        let mut dialed: Vec<String> = Vec::new();
1160        let report = run(
1161            vec![(
1162                "yah-marketing",
1163                "cloud",
1164                plan(
1165                    vec![rule("yah.dev", Some(8080), &["us-east-001"])],
1166                    &["us-east-001", "us-west-001"],
1167                ),
1168            )],
1169            &fanout,
1170            |addr| {
1171                dialed.push(addr.to_string());
1172                DialOutcome::Open { millis: 1 }
1173            },
1174        );
1175
1176        assert_eq!(
1177            report.verdicts.len(),
1178            2,
1179            "both front doors publish the rule, so both are verified"
1180        );
1181        assert_eq!(dialed, vec!["100.64.0.3:8080"], "dialed twice");
1182        assert!(report.is_clean());
1183    }
1184
1185    #[test]
1186    fn every_rule_is_reported_even_after_one_of_them_fails() {
1187        // The reason this does not call `resolve_upstreams_from`: an operator
1188        // fixing one rule at a time is being handed a linked list.
1189        let mut fanout = ServiceRecordFanout::default();
1190        fanout.push_answer(
1191            "us-east-001",
1192            vec![record("yah-marketing", "100.64.0.3", &[8080])],
1193        );
1194
1195        let report = run(
1196            vec![(
1197                "yah-marketing",
1198                "cloud",
1199                plan(
1200                    vec![
1201                        rule("a.yah.dev", Some(9999), &["us-east-001"]),
1202                        rule("b.yah.dev", Some(8080), &["us-east-001"]),
1203                    ],
1204                    &["us-east-001"],
1205                ),
1206            )],
1207            &fanout,
1208            always_open,
1209        );
1210
1211        assert_eq!(report.verdicts.len(), 2);
1212        let a = report
1213            .verdicts
1214            .iter()
1215            .find(|v| v.hostname == "a.yah.dev")
1216            .unwrap();
1217        let b = report
1218            .verdicts
1219            .iter()
1220            .find(|v| v.hostname == "b.yah.dev")
1221            .unwrap();
1222        assert!(!a.is_ok(), "the unresolvable rule fails");
1223        assert!(
1224            b.is_ok(),
1225            "and the rule after it is still resolved and dialed: {b:#?}"
1226        );
1227    }
1228
1229    // ── the public path (R844-F18) ───────────────────────────────────────────
1230
1231    /// The healthy apex: one hostname, one door, one backend, both sides
1232    /// serving the same publish.
1233    fn healthy_report() -> VerifyReport {
1234        let mut fanout = ServiceRecordFanout::default();
1235        fanout.push_answer(
1236            "us-east-001",
1237            vec![record("yah-marketing", "100.64.0.3", &[8080])],
1238        );
1239        run(
1240            vec![(
1241                "yah-marketing",
1242                "cloud",
1243                plan(
1244                    vec![rule("yah.dev", Some(8080), &["us-east-001"])],
1245                    &["us-east-001"],
1246                ),
1247            )],
1248            &fanout,
1249            always_open,
1250        )
1251    }
1252
1253    fn served(digest: &str) -> BeaconFetch {
1254        BeaconFetch::Answered {
1255            status: 200,
1256            digest: Some(digest.to_string()),
1257        }
1258    }
1259
1260    #[test]
1261    fn matching_beacons_on_both_sides_leave_the_verdict_clean() {
1262        let mut report = healthy_report();
1263        let readings = PublicReadings {
1264            public: [("yah.dev".to_string(), served("abc123"))]
1265                .into_iter()
1266                .collect(),
1267            backends: [("100.64.0.3:8080".to_string(), served("abc123"))]
1268                .into_iter()
1269                .collect(),
1270        };
1271        apply_public_path(&mut report, &readings);
1272        assert!(report.is_clean(), "{:#?}", report.verdicts);
1273        assert!(
1274            report.verdicts[0].notes.is_empty(),
1275            "a fully compared rule has nothing to caveat: {:?}",
1276            report.verdicts[0].notes
1277        );
1278    }
1279
1280    /// THE 2026-09-03 OUTAGE, reproduced as a unit test. Every mesh signal is
1281    /// green — the record is correct, the address answers, `verify_collation`
1282    /// alone reports the rule clean — and the public gets a 503 because the
1283    /// running front door is still dialing the port the workload left.
1284    #[test]
1285    fn a_503_at_the_apex_fails_a_rule_whose_mesh_side_is_entirely_green() {
1286        let mut report = healthy_report();
1287        assert!(
1288            report.is_clean(),
1289            "precondition: the mesh side is what reported success during the outage"
1290        );
1291
1292        let readings = PublicReadings {
1293            public: [(
1294                "yah.dev".to_string(),
1295                BeaconFetch::Answered {
1296                    status: 503,
1297                    digest: None,
1298                },
1299            )]
1300            .into_iter()
1301            .collect(),
1302            backends: [("100.64.0.3:8080".to_string(), served("abc123"))]
1303                .into_iter()
1304                .collect(),
1305        };
1306        apply_public_path(&mut report, &readings);
1307
1308        assert!(!report.is_clean());
1309        let msg = report.verdicts[0].findings[0].message();
1310        assert!(msg.contains("503"), "{msg}");
1311        assert!(
1312            msg.contains("yah.dev"),
1313            "the finding names the hostname the public dials: {msg}"
1314        );
1315    }
1316
1317    /// The stale-serve shape: the hostname answers 200 with a real page, so
1318    /// nothing short of comparing publishes can see it. This is the failure
1319    /// that froze the apex for nineteen days.
1320    #[test]
1321    fn a_200_serving_a_different_publish_than_the_backend_is_a_failure() {
1322        let mut report = healthy_report();
1323        let readings = PublicReadings {
1324            public: [("yah.dev".to_string(), served("old-digest"))]
1325                .into_iter()
1326                .collect(),
1327            backends: [("100.64.0.3:8080".to_string(), served("new-digest"))]
1328                .into_iter()
1329                .collect(),
1330        };
1331        apply_public_path(&mut report, &readings);
1332
1333        assert!(!report.is_clean());
1334        let msg = report.verdicts[0].findings[0].message();
1335        assert!(msg.contains("old-digest"), "{msg}");
1336        assert!(msg.contains("new-digest"), "{msg}");
1337        assert!(
1338            msg.contains("100.64.0.3:8080"),
1339            "and it names the backend it compared against: {msg}"
1340        );
1341    }
1342
1343    #[test]
1344    fn a_public_path_that_does_not_answer_at_all_is_a_failure() {
1345        let mut report = healthy_report();
1346        let readings = PublicReadings {
1347            public: [(
1348                "yah.dev".to_string(),
1349                BeaconFetch::Failed("dns error: no record".to_string()),
1350            )]
1351            .into_iter()
1352            .collect(),
1353            backends: [("100.64.0.3:8080".to_string(), served("abc123"))]
1354                .into_iter()
1355                .collect(),
1356        };
1357        apply_public_path(&mut report, &readings);
1358
1359        assert!(!report.is_clean());
1360        assert!(report.verdicts[0].findings[0]
1361            .message()
1362            .contains("dns error"));
1363    }
1364
1365    /// A hostname fronting something that is not a mesofact publish has no
1366    /// beacon to compare. That is a limit on the CHECK, not a fault of the
1367    /// host — so it is a note, and the rule stays clean rather than failing
1368    /// every non-bundle hostname in the fleet.
1369    #[test]
1370    fn a_hostname_with_no_beacon_is_noted_and_not_failed() {
1371        let mut report = healthy_report();
1372        let readings = PublicReadings {
1373            public: [(
1374                "yah.dev".to_string(),
1375                BeaconFetch::Answered {
1376                    status: 200,
1377                    digest: None,
1378                },
1379            )]
1380            .into_iter()
1381            .collect(),
1382            backends: [("100.64.0.3:8080".to_string(), served("abc123"))]
1383                .into_iter()
1384                .collect(),
1385        };
1386        apply_public_path(&mut report, &readings);
1387
1388        assert!(report.is_clean(), "{:#?}", report.verdicts);
1389        assert!(
1390            report.verdicts[0]
1391                .notes
1392                .iter()
1393                .any(|n| n.contains("no publish beacon")),
1394            "{:?}",
1395            report.verdicts[0].notes
1396        );
1397    }
1398
1399    /// The distinction this whole relay is about: "answers" is not "answers
1400    /// with the backend we just proved". A public 200 with no comparable
1401    /// backend must not read as a full pass.
1402    #[test]
1403    fn a_public_200_with_nothing_to_compare_says_so_rather_than_implying_a_match() {
1404        let mut report = healthy_report();
1405        let readings = PublicReadings {
1406            public: [("yah.dev".to_string(), served("abc123"))]
1407                .into_iter()
1408                .collect(),
1409            backends: BTreeMap::new(),
1410        };
1411        apply_public_path(&mut report, &readings);
1412
1413        assert!(report.is_clean());
1414        assert!(
1415            report.verdicts[0]
1416                .notes
1417                .iter()
1418                .any(|n| n.contains("NOT that it is fronting the discovered backend")),
1419            "{:?}",
1420            report.verdicts[0].notes
1421        );
1422    }
1423
1424    #[test]
1425    fn an_unmeasured_hostname_is_marked_as_unmeasured_not_as_passing() {
1426        let mut report = healthy_report();
1427        apply_public_path(&mut report, &PublicReadings::default());
1428
1429        assert!(report.is_clean(), "not checking is not failing");
1430        assert!(
1431            report.verdicts[0]
1432                .notes
1433                .iter()
1434                .any(|n| n.contains("was not checked")),
1435            "{:?}",
1436            report.verdicts[0].notes
1437        );
1438    }
1439
1440    /// DNS picks one door, so the comparison covers one door. Saying that is
1441    /// the difference between a caveat and a false claim of coverage.
1442    #[test]
1443    fn a_hostname_on_two_front_doors_is_noted_as_measured_at_only_one() {
1444        let mut fanout = ServiceRecordFanout::default();
1445        fanout.push_answer(
1446            "us-east-001",
1447            vec![record("yah-marketing", "100.64.0.3", &[8080])],
1448        );
1449        fanout.push_answer(
1450            "us-south-001",
1451            vec![record("yah-marketing", "100.64.0.2", &[8080])],
1452        );
1453        let mut report = run(
1454            vec![(
1455                "yah-marketing",
1456                "cloud",
1457                plan(
1458                    vec![rule(
1459                        "yah.dev",
1460                        Some(8080),
1461                        &["us-east-001", "us-south-001"],
1462                    )],
1463                    &["us-east-001", "us-south-001"],
1464                ),
1465            )],
1466            &fanout,
1467            always_open,
1468        );
1469        assert_eq!(report.verdicts.len(), 2, "one verdict per front door");
1470
1471        let readings = PublicReadings {
1472            public: [("yah.dev".to_string(), served("abc123"))]
1473                .into_iter()
1474                .collect(),
1475            backends: [
1476                ("100.64.0.3:8080".to_string(), served("abc123")),
1477                ("100.64.0.2:8080".to_string(), served("abc123")),
1478            ]
1479            .into_iter()
1480            .collect(),
1481        };
1482        apply_public_path(&mut report, &readings);
1483
1484        assert!(report.is_clean(), "{:#?}", report.verdicts);
1485        assert!(
1486            report
1487                .verdicts
1488                .iter()
1489                .all(|v| v.notes.iter().any(|n| n.contains("wherever DNS sent it"))),
1490            "{:#?}",
1491            report.verdicts
1492        );
1493    }
1494}