Add the answer layer: deterministic triage, bottleneck attribution, dual-stack and portal/DNS checks - #4
Merged
Conversation
…wo new checks
Measurements were only half the job. The question users actually arrive with is
"is it me or the internet?", and a letter grade does not answer it.
src/analysis/ is a data-driven rules engine. Each rule declares the metrics it
consumes, a predicate, an ordinal confidence, a severity and a concrete
remediation; every finding carries the measurements that produced it. It is
deterministic, instant and offline — no model, no API call, nothing to trust
beyond the evidence shown. @google/genai stays removed.
Two invariants govern it, both tested:
- A rule whose declared inputs were not measured returns null. It is skipped
and the gap is reported as a MeasurementFailure. The single most important
assertion in the suite is that an empty snapshot fires no rule at all.
- Silence is not a clean bill of health. Fewer than three conclusive checks
yields `indeterminate`, which declines in both directions, rather than
"no fault found". A run that measured nothing concludes nothing.
One-button triage walks the real decision tree — link, name resolution,
interception, address families, four unrelated content networks, throughput,
behaviour under load — and returns a ranked verdict. Steps a browser genuinely
cannot perform say so: the LAN gateway step explains that a page cannot read
its own gateway address and points at the Port Scanner instead. When no
provider answers, the 25 MB bandwidth step is skipped rather than run to
produce a guaranteed failure.
Bottleneck attribution replaces the four flat progress bars as the dashboard
headline — "Grade A+ on the numbers, the binding constraint is bufferbloat, not
bandwidth". It is sensitivity analysis over the existing scoring function: each
measured input is lifted in turn to a level past which it stops limiting the
result. Those comparison values are internal to the calculation and a test
asserts none can reach the output. Severe bufferbloat overrides the ranking
because the score has no bufferbloat term and the user's experience does; the
headline says "on the numbers" so the grade and the finding do not read as a
contradiction.
Two new checks, both genuinely browser-doable:
- Dual-stack. Hostnames publishing only an A record and only an AAAA record,
two independent providers per family, plus which family the browser chose
for a dual-stack host. A silent family is reported as "no response", never
as "IPv6 is disabled" — those are not the same claim — and a family never
probed reads "not checked", including in the CSV.
- Captive portal and DNS hijack. Over https a portal cannot rewrite a
response without breaking the certificate chain, so it blocks instead; the
check looks for that signature rather than for a redirect it cannot see.
The plaintext generate_204 probe is reported as mixed-content-blocked (a
new FailureReason) and runs only from an http origin. For DNS it compares
reaching one server by name against by literal IP — the only test of the
system resolver a web page can perform — and cross-checks two DoH providers
on anycast names whose answer is identical worldwide.
Also, found by the offline browser run rather than by review: the live traffic
monitor rendered "0 ms" average latency when no request had completed in the
window, because a `|| 1` denominator divided a sum of zeros by one. Now null,
rendered as an em-dash with a reason. httpbin.org, contacted by the monitor's
test-traffic button, was undisclosed; the privacy modal also still claimed
AGPL-3.0 after the MIT relicense. Both fixed.
193 tests, strict typecheck clean, zero lint errors. Verified in Chromium
against the production build: offline, every metric reads "—" with a reason and
no number appears anywhere; with a link but no egress, triage correctly
attributes to the connection and skips the bandwidth step.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WKmUtoTQvJUo1Ksj43rjTC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Measurements were only half the job. The question users actually arrive with is "is it me or the internet?", and a letter grade does not answer it.
Phase 4 of the rebuild. Everything here is deterministic and programmatic — a table of rules with predicates over named measurements. No model, no API call, no network dependency for the reasoning itself.
@google/genaistays removed.1.
src/analysis/— a data-driven rules engineEach rule declares the metrics it consumes, a predicate, an ordinal confidence, a severity and a concrete remediation. Every finding carries the measurements that produced it, so a user can disagree with the tool on the evidence rather than having to trust it.
Two invariants govern it, and both are tested:
null. It is skipped and the gap is reported as aMeasurementFailure. The single most important assertion in the suite is that an empty snapshot fires no rule at all — the engine's equivalent of the offline regression run.indeterminate, which declines in both directions, rather thanno-fault-found. A run that measured nothing concludes nothing.Confidence is ordinal (
confirmed/likely/possible), never a percentage — "83% confident" would be an invented number with no calculation behind it. Every threshold lives in oneTHRESHOLDSobject with a comment justifying it, because these are judgements about human experience and they change what the tool tells people.2. One-button triage
Walks the real decision tree — link → name resolution → interception → address families → four unrelated content networks → throughput → behaviour under load — and returns a verdict with ranked probable causes and fixes, not a grade.
Steps a browser genuinely cannot perform say so rather than failing quietly. The LAN gateway step explains that a page cannot read its own gateway address (modern browsers return an mDNS
.localcandidate) and that an HTTPS page cannot open a plaintext connection to a device with no certificate — then points at the Port Scanner, where the user can enter the address themselves.When no provider answers, the 25 MB bandwidth step is skipped and reported as skipped, rather than run to produce a guaranteed failure. Likewise offline: the remaining checks do not run, because five failures with one cause is one finding, not five.
3. Bottleneck attribution on the dashboard
Replaces the four flat progress bars as the headline:
Computed by sensitivity analysis over the existing
calculateNetReadyScore: each measured input is lifted, one at a time, to a level past which it stops limiting the result, and whichever lift moves the score furthest is the constraint. Those comparison values live inside the calculation and never leave it — a test asserts none can reach the output. Substituting a value to answer "what if" is analysis; substituting one to fill a gap in a report is fabrication, and the two are kept apart.Severe bufferbloat overrides the ranking because the score has no bufferbloat term while the user's experience does. The headline says "on the numbers" so the grade and the finding do not read as a contradiction.
The bars remain below, demoted to supporting detail.
4. Two new checks
Dual-stack (
src/utils/dualStack.ts). Hostnames publishing only an A record and only an AAAA record, two independent providers per family, plus which family the browser actually chose for a dual-stack host. A silent family is reported as "no response", never as "IPv6 is disabled" — a browser cannot tell an absent IPv6 path from two probe hosts being unreachable. A family never probed readsnot checked, including in the CSV export.Captive portal and DNS hijack (
src/utils/captivePortal.ts). Over HTTPS a portal cannot rewrite a response without breaking the certificate chain, so it blocks instead — the check looks for that signature rather than for a redirect it cannot see. The plaintextgenerate_204probe is reported asmixed-content-blocked(a newFailureReason) and runs only from anhttporigin.For DNS it does the one test of the system resolver a web page can perform: reach one server by name, which uses the resolver, and by literal IP, which does not. Literal answering while the name does not is a broken or redirected resolver. Two DoH providers are cross-checked as well, but only on anycast names whose correct answer is identical worldwide — ordinary CDN hostnames disagree by design and would manufacture findings out of geography.
Also fixed
Found by the required offline browser run rather than by static review:
|| 1denominator divided a sum of zeros by one. Nownull, rendered as an em-dash with a reason.httpbin.org, contacted by the monitor's test-traffic button, was undisclosed. Added toTHIRD_PARTY_DISCLOSURESand the README table alongside the new probe hosts.Reuse
calculateNetReadyScoreandMeasurementFailure, theexploreEdgePathprobes (used whole as the multi-CDN step, which also supplies the HTTP/3 evidence),<MetricValue>and<FailureNotice>, and theResponsibleNetworkingModalconsent gate.<MetricValue>still has nofallbackprop.Verification
npm run checkclean — 193 tests (up from 84), strict typecheck, zero lint errors. Both CLAUDE.md pre-commit greps return nothing new.Driven in Chromium against the production build via
npm run preview:—with a reason, the grade reads--, all four categories read—, and the bottleneck panel says "Nothing has been measured yet". Triage returnsthis-devicewith one confirmed finding and marks the other seven steps not run, each with a reason. No number appears anywhere. This is what caught the traffic-monitor0 ms.all-cdns-unreachable, confirmed, blocking), thenhttps-blocked(likely), skips the bandwidth step, and lists seven things it could not determine. The score still reads--.The happy path against live endpoints could not be exercised here: this environment's network policy blocks all outbound hosts, including the ones the app already used. The probes are written so an unexpected CORS refusal degrades to an honest "could not determine" rather than a wrong verdict — the dual-stack probe retries in
no-corsmode to separate "the provider will not let this page read the response" from "the family does not work".Generated by Claude Code