Skip to content

Add the answer layer: deterministic triage, bottleneck attribution, dual-stack and portal/DNS checks - #4

Merged
TechLuddite merged 1 commit into
mainfrom
claude/netready-answer-layer-mf92ts
Aug 2, 2026
Merged

Add the answer layer: deterministic triage, bottleneck attribution, dual-stack and portal/DNS checks#4
TechLuddite merged 1 commit into
mainfrom
claude/netready-answer-layer-mf92ts

Conversation

@TechLuddite

Copy link
Copy Markdown
Owner

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/genai stays removed.

1. src/analysis/ — 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, 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:

  • 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 — the engine's equivalent of the offline regression run.
  • 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.

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 one THRESHOLDS object 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 .local candidate) 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:

Grade A+ on the numbers — the binding constraint is bufferbloat, not bandwidth. Latency rose from 18 ms idle to 80 ms under load, an increase of 62 ms.

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 reads not 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 plaintext generate_204 probe is reported as mixed-content-blocked (a new FailureReason) and runs only from an http origin.

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:

  • The live traffic monitor rendered "0 ms" average latency when no request had completed in the window: 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. Added to THIRD_PARTY_DISCLOSURES and the README table alongside the new probe hosts.
  • The privacy modal still claimed AGPL-3.0 after the MIT relicense.

Reuse

calculateNetReadyScore and MeasurementFailure, the exploreEdgePath probes (used whole as the multi-CDN step, which also supplies the HTTP/3 evidence), <MetricValue> and <FailureNotice>, and the ResponsibleNetworkingModal consent gate. <MetricValue> still has no fallback prop.

Verification

npm run check clean — 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:

  • Offline — every metric reads with a reason, the grade reads --, all four categories read , and the bottleneck panel says "Nothing has been measured yet". Triage returns this-device with 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-monitor 0 ms.
  • Link present, egress blocked — triage attributes to the connection (all-cdns-unreachable, confirmed, blocking), then https-blocked (likely), skips the bandwidth step, and lists seven things it could not determine. The score still reads --.
  • Seeded measurements — bufferbloat, bandwidth-limited and partial-measurement scenarios all render the headline correctly, including the partial case where only download and latency exist.

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-cors mode to separate "the provider will not let this page read the response" from "the family does not work".


Generated by Claude Code

…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
@TechLuddite
TechLuddite merged commit c28ac63 into main Aug 2, 2026
2 checks passed
@TechLuddite
TechLuddite deleted the claude/netready-answer-layer-mf92ts branch August 2, 2026 06:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants