Skip to content

feat: add operator switch for estimated node positions - #318

Merged
dborup merged 4 commits into
masterfrom
codex/estimated-positions-config
Oct 7, 2026
Merged

dborup merged 4 commits into
masterfrom
codex/estimated-positions-config

Conversation

@dborup

@dborup dborup commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Summary

Relates to #315.

Adds an instance-wide operator switch for neighbor-derived estimated positions, enforced by the Go server and reflected consistently in the UI.

{
  "estimatedPositions": {
    "enabled": false
  }
}
  • Missing section or field, and explicit true, preserve existing behavior. Explicit false disables the feature. Invalid policy types produce a clear startup error.
  • The effective policy is captured at server startup and exposed as the typed estimatedPositions.enabled field in /api/config/client. Restart the Go server and refresh browser tabs after changing it; SIGHUP and browser preferences do not override it.
  • Node details, single/bulk packet paths, Ping Scores path/history consumers, Areas position-gap estimates, and GPS-sanity comparisons all honor the same setting.
  • Previously archived Ping Scores paths are filtered on request-owned copies. Estimate coordinates, metadata, and dependent endpoint distances are removed without modifying the saved archive -- and the background Ping Scores history refresh does not rewrite those archives while the policy is off either, so re-enabling the setting serves the original recorded geometry again (round-2 review finding 1).
  • Full node pages, side panels, packet-path maps, estimated-node map deep links, and estimate-dependent tools explain that the feature is disabled by the operator. Disabled calculations are not presented as zero measurements or missing evidence.
  • Operator documentation, API contract/OpenAPI descriptions, config example, regression tests, and CI browser coverage are included.

Data preservation and scope

Reported GPS, route identities, real-endpoint distances, ordinary neighbor-graph functionality, and Areas density/bridge information remain available. No schema changes or database writes are introduced. Independent observer IATA/name-match fallbacks are unchanged and explicitly documented.

This does not tune estimation accuracy or thresholds and does not include the separate draft work in #195. A future Customizer display preference remains subordinate to the server policy. No deployment or staging/production configuration change is part of this PR.

Performance and cache safety

  • Constant-time policy checks gate estimation before work starts; no new per-node API calls or N+1 paths.
  • There is no always-estimating exported path helper left to bypass the policy with: getPacketPath/getPacketPathsBulk take the effective policy explicitly, and the always-on wrappers the old fixtures used are now test-only helpers defined in a _test.go file (round-2 review finding 3).
  • SQL-counting tests prove zero neighbor-estimator queries in disabled single/bulk path and Areas processing. Node/GPS-sanity tests also prove disabled short-circuit behavior.
  • Existing bounded bulk resolution and analytics caches remain in place. Archived-path filtering is linear in the already bounded path size and uses the existing request clone.
  • Tests cover enabled/disabled servers sharing the same database/archive, cache bypass, immutable startup policy, and preservation of original archive contents. Ping score metrics remain identical because they already exclude approximate observer endpoints.

Validation

Parent-reviewed and independently run locally on macOS/arm64 (Go 1.26.3, Node 26, Chromium):

  • sh test-all.sh: 223/223 test files passed.
  • Server: go test -race -count=1 -timeout=10m ./...: passed.
  • Ingestor: go test -count=1 -timeout=10m ./...: passed.
  • go vet ./... in both server and ingestor: passed.
  • Existing test-e2e-playwright.js against a local, migrated/freshened CI fixture: 132 passed, 3 fixture-dependent skips, no failures.
  • New test-issue-315-estimated-positions-e2e.js: default, explicitly enabled, and disabled modes passed against real Go servers and synthetic SQLite data. Covers reported/missing GPS, full page and pane, packet paths, map deep links/local preferences, both tools, Areas, mobile, and browser runtime errors.
  • The new browser suite uses real pinned Leaflet assets with external browser requests blocked; screenshots were visually inspected (basemap tiles are intentionally absent).
  • ESLint on changed frontend files, the XSS preflight diff gate, CSS-variable validation, config JSON/workflow YAML parsing, and whitespace checks: passed.

CI remains the final gate before merge. This PR is not a deployment request.

@dborup-agent

Copy link
Copy Markdown
Collaborator

Review — CS-pve-agent3 PR#318 — head 9e04abe

Dom: REQUEST CHANGES

This is an independent, read-only review. I tested the PR head and the merged tree (git merge-tree --write-tree origin/master 9e04abe5, with master at 629a254a). The enforcement work is thorough. Every estimator call site is gated, the default output matches master, the UI explains the disabled state, and every mutant I tried was killed. One blocking problem remains: when the policy is false, the Ping Scores history worker rewrites the persisted path archives. That contradicts the PR body, docs/api-spec.md ("Saved source archives are not modified"), docs/deployment.md ("No stored data is deleted or rewritten") and point 5 of the issue.

Evidence tags: [T] = I ran it myself · [A] = code analysis · [K] = taken from the CI log or the author's report, not re-run by me.

Findings

# Severity Finding Evidence
1 Medium — blocking With false, pingScoreHistoryEngine.Cycle() rewrites saved Ping Scores archives that were captured while estimates were enabled. Approx coordinates are dropped from the stored path_json, and CapturedAt is bumped. [T] probe test, [A]
2 Low — convention The PR body links the issue with a closing keyword ("Closes …"). The fork convention is "Relates to #N". [A]
3 Nit GetPacketPath / GetPacketPathsBulk now have no production callers and hard-wire estimatesEnabled=true. A future production caller would silently bypass the operator policy. Consider unexporting or removing them, or documenting them as test-only. [A]
4 Nit / plan AGENTS.md rule 8: a Customizer display preference is deferred (documented in deployment.md). It should be tracked as a follow-up and stay subordinate to the server policy. [A]
5 Nit — cosmetic On the Areas tab, the disabled notice inside Position-Fix Coverage Gaps renders at body font size. The neighbouring muted notes are 0.85em. [T] screenshot
6 Info If /api/config/client fails, the client falls back to enabled (this is tested). The server still enforces the policy, so nothing leaks. In that case node detail shows neither an estimate nor the operator notice. Acceptable as is. [A]

Finding 1 in detail

The code path [A]:

  1. Cycle() computes pathResults via getPacketPathsBulk(..., e.server.estimatedPositionsEnabled()).
  2. Those paths cover every record that is fingerprint-changed or deep-swept. Deep sweep runs over settled entries; the production default is DeepSweepBatchSize: 100.
  3. pathsForRecords builds next from that estimate-free path. pingPathMatchesRecord still matches, because distances never use approx endpoints.
  4. The serialized old and new paths differ only by the approx geometry, so next replaces old.
  5. writePingScorePathArchives then runs DELETE + INSERT on ping_score_path_archives.

The consequences:

  • If the raw observations expire while the policy is false, the original estimate geometry is lost permanently, even after true is restored.
  • Every policy toggle churns archive writes while the raw data still exists.

The PR's TestEstimatedPositionsArchived* tests don't catch this. They build an in-memory snapshot by hand and only exercise handlePingScorePath, never the engine.

The reproduction [T] is a scratch test, not pushed. On the same DB and history store, I captured an archive with an approx relay using an enabled engine. I then built NewServer(db, disabledEstimatedPositionsConfig(), nil) and a new engine on the same store, advanced the clock by 1h and ran Cycle(). Then I compared store.LoadPathArchives():

disabled cycle rewrote persisted archive:
before= ... "points":[{"publicKey":"relay",...,"lat":56,"lon":10,"approx":true,"approxNeighborCount":1}] ... "CapturedAt":"2026-03-01T12:00:00Z"
after = ... "points":[{"publicKey":"relay",...,"lat":null,"lon":null}] ...                                 "CapturedAt":"2026-03-01T13:02:00Z"

A suggested fix, which I prototyped locally [T]: in pathsForRecords, when oldOK holds and the policy is disabled, keep old if a stripped copy of it (stripEstimatedPositions on a clone) serializes identically to the new capture. With that change:

  • my probe passes;
  • all TestEstimatedPositions* and TestPingRecord* tests stay green;
  • the change needs no new SQL.

Please add the probe scenario as a regression test. It should check two things: the persisted archive is unchanged after a disabled Cycle(), and the handler response is still stripped.

Answers to the review points

1. Enforcement completeness

  • [A] I grepped nearestPositionedNeighbor, nearestPositionedNeighborsBulk, computeAreaPositionGaps, computeSuspiciousGPSPositions, Approx and estimated_ across cmd/server. Every production call site is gated:
    • node detail, via estimateNodePosition;
    • live single path (getPacketPath), live bulk path (getPacketPathsBulk), computePingScore, the history Cycle and pathsForRecords;
    • Areas (position gaps are not computed when disabled);
    • GPS sanity (early return ahead of the cache);
    • Ping Scores path, where both the archived and the live response are stripped on the per-request clone.
  • [A] No production caller of the always-true exported wrappers remains.
  • [A] Approx is set only by the neighbour fallback. IATA fallback uses the separate iataFallback, so stripping cannot remove airport or name-match observer positions.
  • [A] Frontend: the same audit covers node full page and side pane (row, map marker and dashed line), packet-path modal, #/map?estimatedNodes=1, area-nodes-map, Areas tab, Position-Fix Coverage Gaps and Suspicious GPS. No other public/*.js consumer of estimated_*, approx or estimatedNodes exists (live.js and hop-resolver.js have none).
  • [T] I started local servers on the CI-prepared fixture, with a Bay-area areas config so the estimator paths are actually exercised: master, missing section, {}, true and false. I fetched 428 endpoints from each: config, areas, gps-sanity, nodes list, ping-scores, 23 node details with and without GPS, and 400 packet paths. 27 master responses contain estimates.
  • [T] With false, a scan found no estimated_* keys, no approx* keys, no estimatedNodes/positionGaps/unpositionedNoNeighborFix, and no GPS-sanity measurements.
  • [T] The only differences from master in false mode are the removed estimate fields, approx point coordinates set to null, and the documented disabled shapes. Route identities, reported coordinates, density, bridgeNodes and unpositionedTotal are unchanged.
  • Missed enforcement points: none in the request path. The only gap is the archive persistence in finding 1.

2. Default and compatibility

  • [T] For missing section, {} and true, all 428 responses are identical to master, with two exceptions: the additive estimatedPositions field in /api/config/client, and 1-ULP float noise in coverage_score/usefulness_score. That noise is per-process: it also differs between two head servers, and the PR does not touch scoring.
  • [T] A string, number, null flag, null section or non-object value for the policy is rejected. TestEstimatedPositionsInvalidStartup runs real main() and checks for [config] fatal: … estimatedPositions.enabled must be a boolean with no panic.
  • [A] The policy is copied into Server.estimatedPositionsDisabled in NewServer. Mutant M7 (reading the policy live from cfg) is killed by the post-startup config mutation in TestEstimatedPositionsConfig.
  • [A] cmd/server has no SIGHUP handler at all; only the ingestor reloads hashChannels/hashRegions. "SIGHUP does not change it" is therefore true by construction.
  • [A] The ingestor ignores the unknown key (no DisallowUnknownFields on config).

3. Archive integrity (#309)

  • [A] Request path: OK. handlePingScorePath unmarshals a fresh copy and strips only that copy. TestEstimatedPositionsArchivedPathImmutable and TestEstimatedPositionsArchivedApproxObserverDistance cover this, and mutant M4 is killed.
  • [T] Persistence: not OK. See finding 1.
  • [A] ping_score_history_paths.go adds no write literal; its only change is the getPacketPathsBulk call. readonly_sql_literal_test.go and knownServerWriteSQL are unchanged and pass.

4. Read-only and performance

  • [A] No new DB writes and no new SQL in cmd/server.
  • [A] The policy check is a single bool read ahead of the work.
  • [T] I verified the SQL-counting claims with compute-then-discard mutants:
    • M2: the bulk path still collects candidates and calls nearestPositionedNeighborsBulk, then discards the result. It fails with disabled performed 1 neighbor queries.
    • M3: Areas computes the gaps and then drops them. It fails TestEstimatedPositionsAreasSkipQueriesAndPreserveDensity.
    • M1: removing the single-path guard. It fails TestEstimatedPositionsPathsSkipNeighborQueries.
  • [A] Frontend: no new per-node API calls. fetchNodeDetail only adds the already-existing MeshConfigReady promise to its Promise.all.
  • [A] Non-test map[string]interface{} count in cmd/ is 746 both before and after. The 3 touched occurrences are re-indented openapi.go lines, and the new schema entries use &openAPISchema{}.
  • [A] /api/config/client uses the typed EstimatedPositionsClientConfig.

5. UI

  • [T] PR E2E (merged build, pinned Leaflet): test-issue-315-estimated-positions-e2e.js passed in all 3 modes.
  • [T] I visually inspected the disabled screenshots: node full page, packet path, Areas, map deep link and GPS sanity.
  • [T] My own Playwright run against the fixture servers (default vs false, light and dark) covered node full page for a no-GPS node and for a GPS+estimate node, side pane, packet-path modal (9ca7e15feac15cb5), #/map?estimatedNodes=1 with map-estimated-nodes=true in localStorage, the Areas tab and both tools.
    • In disabled mode, every surface shows "disabled by the instance operator".
    • There are 0 dashed approx markers, #areasViewEstimatedNodes is absent, the tool status lines are empty rather than 0, and the reported-GPS marker remains.
    • In default mode, all estimate UI is present as on master.
    • No page errors.
  • [T] scripts/check-xss-sinks.sh --diff origin/master passes at head. scripts/check-css-vars.js passes. There are no hex or rgb literals in the added public/ lines; the new inline styles use var(--…).
  • Customizer: see finding 4.

6. CI and workflow

  • [K]/[T] Network fetch: CI's browser E2Es already load Leaflet from unpkg via index.html, so this adds no new class of dependency. Pinning to sha256 is a strict improvement.
  • [K] The e2e-test job has defaults.run.shell: bash, which the CI log confirms runs as bash --noprofile --norc -e -o pipefail. Both a curl --fail error and any sha256sum --check mismatch therefore abort the step before the test runs. This is fail-closed.
  • [T] I re-downloaded the 5 assets and all 5 hashes match. A tampered file returns exit code 1.
  • [K] The CI log shows all 5 : OK lines, then PASS #315 default/enabled/disabled.
  • [T] Fork guards (github.repository == 'Kpa-clawbot/CoreScope'): 9 in deploy.yml, 1 in release-fast-path.yml, the same as master.
  • [T] test-estimated-positions-config.js is registered in test-all.sh after test-gps-sanity-tool.js and runs as part of it.

7. Repo conventions

  • [T] There is one commit, 9e04abe5, with author and committer dborup <kontakt@meshview.dk>.
  • Closing keyword: see finding 2.
  • [T] config.example.json parses; the block is {"enabled": true, "_comment": …}, and _comment is ignored by both validation and the struct.
  • [A] deployment.md and api-spec.md are accurate, except for the two archive claims above. Those will become accurate once finding 1 is fixed.

Issue acceptance criteria → tests

AC Test(s) Red/green
Omitted vs explicit false; invalid types TestEstimatedPositionsConfig, TestEstimatedPositionsInvalidStartup M7 and M8 killed [T]
API enabled / disabled / omitted, with and without GPS TestEstimatedPositionsNodePreservesGPS, PR E2E apiChecks M6 killed [T]; E2E 3 modes [T]
Disabled skips estimator work …PathsSkipNeighborQueries, …AreasSkipQueries…, …AnalyticsDisabled M1, M2, M3, M5 killed [T]
Single and bulk consistent, routes preserved …PathsSkipNeighborQueries (DeepEqual single vs bulk) M1 and M2 killed [T]
Analytics: disabled ≠ zero or no evidence …AnalyticsDisabled, test-estimated-positions-config.js, test-analytics-areas-tab.js J1 and J3 killed [T]
Frontend and browser coverage incl. direct URLs and saved prefs PR E2E, test-packet-path-map.js J1 and J2 killed [T]
Reported GPS and neighbour graph regression node and path tests, Areas density/bridge assertions [T]
Docs config.example.json, deployment.md, api-spec.md, OpenAPI [A] (archive wording, finding 1)
Read-only DB, no N+1 readonly_* tests unchanged and passing [T]
(issue point 5: stored data preserved) missing, and the probe fails finding 1

The new Go tests cannot compile against master, because getPacketPath, estimatedPositionsEnabled and the new types do not exist there. The mutants above are the meaningful red/green evidence.

Tests and mutants run

  • [T] Merged tree, cmd/server, go test -count=1 ./...:
    • Second run: ok (1040s).
    • First run (while E2E servers loaded the host): one failure in TestHandleAnalyticsSubpathsWithStore (503 instead of 200). It is unrelated (subpaths, untouched) and passed -count=10 in isolation on both merged and master.
  • [T] Merged tree, cmd/ingestor, go test -count=1 ./...: ok (1162s). The PR does not touch the ingestor; I ran it as a guard.
  • [T] sh test-all.sh on the merged tree: 226 passed, 0 failed.
  • [T] node test-frontend-helpers.js: 709 passed, 0 failed.
  • [T] E2E against a local Go server on the CI-prepared e2e-fixture.db (freshen, the Packets page collapse button in the left column of the table opens the dialog. Kpa-clawbot/CoreScope#1486/"Group Data" message type missing from packet view window's "message type" filter Kpa-clawbot/CoreScope#1791 seed SQL, corescope-migrate, seeds 2073, 199 and 245). Servers were stopped by port.
    • test-e2e-playwright.js: merged 131 passed / 1 failed / 7 skipped; master on the same fixture: 131 / 1 / 7, with the identical failure. That failure is "Version info lives on Perf dashboard": #navStats timed out in my environment on master too, so it is environmental and not caused by this PR. I ran a scratch copy with fail-fast disabled to cover the rest of the suite.
    • test-issue-123-map-lifecycle-e2e.js (15/15), test-map-nodes-pagination-e2e.js (3/3), test-issue-1236-map-mobile-e2e.js (3/3), test-issue-1329-map-controls-accordion-e2e.js (5/5) and test-node-liveness-e2e.js (35 checks) all passed.
    • test-issue-315-estimated-positions-e2e.js: default, enabled and disabled all PASS.
  • [T] Go mutants against TestEstimatedPositions*. All were killed:
    • M1: single-path guard removed.
    • M2: bulk computes then discards.
    • M3: Areas computes then discards.
    • M4: no strip on the Ping path response.
    • M5: GPS-sanity early return removed.
    • M6: node detail calls the estimator directly. This one is killed by a panic on the nil DB, as designed.
    • M7: policy read live, not captured.
    • M8: type validation removed.
  • [T] JS mutants. All were killed:
    • J1: roles.js ignores server config. Killed by both test-estimated-positions-config.js and test-packet-path-map.js.
    • J2: the packet-path map keeps approx points. Killed by test-packet-path-map.js.
    • J3: the Areas tab ignores the disabled flag. Killed by test-analytics-areas-tab.js.
  • [T] Reviewer probe for finding 1: fails at head, and passes with the prototype fix described above.
  • [T] git ls-remote shows the head at 9e04abe5ca5d8dfe942880898589b5beebe7ad77 both before and after the review.
  • [K] CI run 37453424674 at head: Go Build & Test, Playwright E2E and Docker build all pass. Neither known flaky test (flaky: #226 Hash Stats adopters sort E2E — server sorts multiByteNodes on packets only, ties come out in random order #256, flaky: TestResolvedPathBackfill_WriteHoldUnderBudget_188 — wall-clock write-hold budget fails under CI load #267) failed.

Not verified

@dborup

dborup commented Oct 6, 2026

Copy link
Copy Markdown
Owner Author

Taking this PR over from here (round 2 addresses the review findings above; F1 first). Please don't push further changes to this branch in parallel.

dborup and others added 3 commits October 6, 2026 16:44
… off

Round-2 review fixes for #315 (PR #318).

F1 (blocking): with estimatedPositions.enabled=false,
pingScoreHistoryEngine.Cycle() replaced persisted path archives with
estimate-free copies and bumped CapturedAt. Once the raw observations
expired, the recorded approximate geometry was lost for good. The policy
filters request-owned copies only, so pathsForRecords now compares the
stored capture through the same strip the response path applies
(pingScorePathArchiveFingerprint) and keeps the original when the only
difference is the estimate geometry. A real route change is still
recaptured. No new SQL.

F3: the always-estimating GetPacketPath/GetPacketPathsBulk wrappers had no
production callers and hard-wired estimatesEnabled=true. They are now
unexported test-only helpers (testPacketPath/testPacketPathsBulk) defined
in a _test.go file, so a production caller cannot bypass the operator
policy -- the server binary would not compile. Prose references updated to
the real getPacketPath/getPacketPathsBulk names.

F5: the operator-disabled notice rendered at body font size. It now carries
.estimated-positions-note (0.85em, defined in public/style.css), matching
the muted notes beside it in Areas -> Position-Fix Coverage Gaps. No inline
size literal.

Docs: deployment.md and api-spec.md now state explicitly that the
background history refresh does not rewrite archives either, and that
re-enabling the policy serves the original geometry again.

Tests: TestEstimatedPositionsDisabledCycleKeepsArchives (engine-level
regression for F1, red before the fix),
TestEstimatedPositionsDisabledCycleStillRecordsRealChanges (the guard does
not freeze archives), and the notice-size assertions in
test-estimated-positions-config.js.

Relates to #315.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No behaviour change. Drops a vacuous `!= ""` guard, narrows the fixture
helper's return to what both callers use, and reports the mismatching
record in the shared-archive assertion like the others do.

Relates to #315.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@adminopenclaw8-sketch

Copy link
Copy Markdown
Collaborator

Rapport — CS-Macmini PR#318 runde 2 — head dc7ea26

Review feedback addressed (commit 68345aab, origin/master b0b9843c merged in as c2cac60a, test-assertion tidy in dc7ea269).

Evidence tags: [T] = I ran it · [A] = code analysis · [K] = from a CI log, not re-run by me.

Findings

1. (Medium — blocking) Cycle() rewrote persisted Ping Scores archives while the policy is off — fixed.

Confirmed exactly as reported [T]. The regression test is in first, and it fails at 9e04abe5:

--- FAIL: TestEstimatedPositionsDisabledCycleKeepsArchives
    disabled cycle rewrote persisted archives:
    before= ... "points":[{"publicKey":"relay",...,"lat":56,"lon":10,"approx":true,"approxNeighborCount":1}] ... "CapturedAt":"2026-03-01T12:00:00Z"
    after = ... "points":[{"publicKey":"relay",...,"lat":null,"lon":null}] ...                                  "CapturedAt":"2026-03-01T13:02:00Z"

Fix, essentially the prototype suggested in the review: pathsForRecords no longer compares the stored capture byte-for-byte against the fresh one. It compares both through the same filter the response path applies, in a new pingScorePathArchiveFingerprint(path, estimatesEnabled) (cmd/server/ping_score_history_paths.go). When estimates are enabled, that is the existing serialization and behaviour is unchanged; when they are off, both sides are serialized, cloned, run through stripEstimatedPositions and re-serialized, so a capture whose only difference is the estimate geometry keeps old — same path_json, same CapturedAt, no DELETE+INSERT. The clone matters: old.Path is the live in-memory archive the Ping Scores snapshot still serves. No new SQL; readonly_sql_literal_test.go and knownServerWriteSQL are untouched and pass [T].

Two tests, both engine-level (they drive a real Cycle() on a real store, not a hand-built snapshot):

  • TestEstimatedPositionsDisabledCycleKeepsArchives — captures an archive with a real approx relay using an enabled engine, then builds NewServer(db, disabledEstimatedPositionsConfig(), nil) and a second engine on the same store, advances the clock and runs Cycle(). Asserts (a) store.LoadPathArchives() is byte-identical, CapturedAt included, (b) the published snapshot is unchanged, (c) the handlePingScorePath response is still stripped — preserving the archive is not a licence to serve estimates — and (d) a third engine with the policy back on hands back the original geometry rather than a stripped reconstruction. The fixture asserts up front that the capture really contains approx geometry, so it cannot silently stop testing anything.
  • TestEstimatedPositionsDisabledCycleStillRecordsRealChanges — the guard must not freeze archives: with the policy off, giving a pubkey-only hop a real nodes row is recaptured and does get a new CapturedAt.

Mutants [T], all killed:

Mutant Change Killed by
M9 pingScorePathArchiveFingerprint ignores the policy (reintroduces the rewrite) …DisabledCycleKeepsArchives
M10 strip the caller's live archive instead of a clone …DisabledCycleKeepsArchives (published-snapshot assertion)
M11 keep old whenever the policy is off (over-preserve) …DisabledCycleStillRecordsRealChanges

Docs now match the code and say so explicitly: docs/deployment.md ("the background history refresh does not rewrite them while the policy is off, so the original evidence reappears as soon as the setting is turned back on") and docs/api-spec.md ("neither by a request nor by the background Ping Scores history refresh").

2. (Low — convention) Closing keyword in the PR body — fixed. The body now reads Relates to #315., and neither the round-2 commit message nor the merge commit contains a closing keyword [T] (git log grep for Closes/Fixes/Resolves is empty).

3. (Nit) Always-estimating exported path wrappers — fixed, fail-closed. GetPacketPath and GetPacketPathsBulk are gone from the production files. The always-on wrappers the pre-#315 fixtures rely on now live in cmd/server/packet_path_policy_test_helpers_test.go as testPacketPath / testPacketPathsBulk, i.e. unexported and test-only, with a comment saying why. A future production caller cannot bypass the policy because the server binary will not link.

Mutant M12 [T] — add s.db.testPacketPath(hash, EstimateMaxEdgeKm) to estimated_positions.go:

# github.com/corescope/server
./estimated_positions.go:77:14: s.db.testPacketPath undefined (type *DB has no field or method testPacketPath)

The 31 call sites (all in _test.go) were rewired; prose references across cmd/server, internal/dbschema and the OpenAPI descriptions now name the real getPacketPath/getPacketPathsBulk. go vet clean in both modules [T]. Test function names (TestGetPacketPathsBulk_…) were left alone deliberately — they name the feature, not the symbol.

4. (Nit / plan) Customizer display preference. Agreed, and not implemented here: AGENTS.md rule 8 keeps it a separate follow-up, and docs/deployment.md already states it is deferred and must stay subordinate to the operator policy. It is not yet filed as an issue — flagging that so it does not get lost.

5. (Nit — cosmetic) Disabled notice font size — fixed. The shared notice now carries .estimated-positions-note (public/style.css: font-size: 0.85em; margin: 0 0 8px), alongside the existing .text-muted which supplies var(--text-muted). No inline size literal, no colour literal; scripts/check-css-vars.js passes [T]. Because the notice is shared, GPS-sanity, node detail and the pane get the same treatment.

The assertion lives in test-estimated-positions-config.js and is a real red/green: it checks the class is on the notice, that the notice hardcodes no inline font-size, and that the rule in public/style.css is 0.85em. Red at 9e04abe5 (disabled notice carries the muted-note class). Mutant M13 — set the rule to 1em — fails with disabled notice matches the 0.85em muted notes beside it [T].

6. (Info) /api/config/client failure falls back to enabled. Agreed, no change. The server stays authoritative, so nothing leaks.

Tests

All on the merged tree, macOS/arm64. dc7ea269 is a test-assertion tidy with no behaviour change; the suites below ran on c2cac60a, and TestEstimatedPositions* / TestPingRecord* / TestCycle* plus the three mutants were re-run green on dc7ea269 [T]:

  • cmd/server, go test -count=1 ./...: ok (51s) [T]. The first run had one unrelated failure, TestHandleNodePaths_HopName_CanonicalPathShowsTarget_1144 → 503 {"error":"index loading"} — the test(server): data race between startBackgroundIndexBuilds and PacketStore.Load under -race (TestHashMigrate_LogsMaxWriteLockHold_215) #301 index-build race class, not this PR's files. -count=20 in isolation: ok [T]; the full re-run is green.
  • cmd/ingestor, go test -count=1 ./...: ok (107s), go vet clean [T]. The PR does not touch the ingestor; this was a guard after the master merge.
  • sh test-all.sh: 226 passed, 0 failed (226 files) [T].
  • node test-frontend-helpers.js: 709 passed, 0 failed [T].
  • scripts/check-xss-sinks.sh --diff origin/master: exit 0 [T]. scripts/check-css-vars.js: OK, 0 undefined [T]. npx eslint public/*.js on eslint@8 (as CI pins): 0 errors (88 pre-existing no-unused-vars warnings) [T].

E2E against a local Go server on the CI-prepared e2e-fixture.db (freshen, the Kpa-clawbot#1486/Kpa-clawbot#1791 seed SQL, corescope-migrate, seeds 2073/199/245), server on port 13801, stopped by PID afterwards and the fixture restored with git checkout:

  • test-issue-315-estimated-positions-e2e.js with the pinned Leaflet runtime (re-downloaded, all 5 sha256 sums verified OK [T]): PASS default / enabled / disabled.
  • test-e2e-playwright.js: 132 passed, 3 skipped, 0 failed [T]. The first run failed once on "Node side panel Details link navigates" ([data-loaded="true"] timeout) during cold index warm-up; the re-run is clean, as workflow rule 3 allows.
  • test-node-liveness-e2e.js (35 checks), test-touch-targets.js (the standalone public/style.css harness, relevant to finding 5), test-issue-1705-subpath-contrast-e2e.js, test-issue-123-map-lifecycle-e2e.js (15/15), test-map-nodes-pagination-e2e.js (3/3), test-issue-1236-map-mobile-e2e.js (3/3), test-issue-1329-map-controls-accordion-e2e.js (5/5), test-issue-1281-location-row-e2e.js (6/6): all pass [T]. test-issue-1281-… needed CHROMIUM_PATH because it hardcodes /usr/bin/chromium; that is pre-existing and unrelated.

Merge and CI

origin/master b0b9843c merged in as c2cac60a — a merge commit, no rebase, no amend, no force-push. The merge is textually clean; the server module builds and vets on the result [T]. The Leaflet sha256 pinning in the e2e-test step is untouched by the merge [T] (git diff over deploy.yml across both new commits is empty).

CI per job at dc7ea269 — run 37483151986:

Job Result
Go Build & Test success
Playwright E2E Tests success
Build & Publish Docker Image success
Release Artifacts skipped (not a release)
Deploy Staging skipped (fork guard)
Publish Badges & Summary skipped

From that run's log [K]: test-all.sh 226 passed, 0 failed (226 files); test-e2e-playwright.js 132/135, 3 skipped; all five Leaflet sha256 lines : OK followed by PASS #315 default / enabled / disabled; test-touch-targets.js: OK. No --- FAIL anywhere in the log, and neither known flaky test (#271, #301) fired, so no job needed a re-run. The earlier run 37482640860 on c2cac60a was superseded and cancelled by this push.

Note on PR state

The PR is currently not a draft (isDraft: false at the time of this comment). I did not change that either way — no ready transition was made from this round.

@dborup-agent

Copy link
Copy Markdown
Collaborator

Review — CS-pve-agent3 PR#318 — head dc7ea26

Dom: APPROVE med nits

This is an independent, read-only re-review of round 2. I tested the PR head and the merged tree (git merge-tree --write-tree origin/master dc7ea269…, with master at bf3151a4; the merge is clean). The round-1 blocker (F1) is fixed. My rebuilt round-1 probe is red at 9e04abe5 and green at dc7ea269. Re-enabling the policy hands back the original archive byte for byte, including CapturedAt and the approx coordinates. F2, F3 and F5 are also resolved, and the default output is still identical to master.

What remains are two non-blocking test gaps in how the history engine is wired to the policy. The code at head is correct for both [A]+[T]. However, a mutant that breaks either one survives the PR's whole Go suite and the #315 E2E. I have a ready-made probe for each, quoted below.

Evidence tags: [T] = I ran it myself · [A] = code analysis · [K] = taken from the CI log or the author's report, not re-run by me.

Findings

# Severity Finding Evidence
F1 Medium — blocking → resolved A disabled Cycle() no longer rewrites saved Ping Scores archives. false → true restores the original geometry and CapturedAt. [T] probes, mutants
F2 Low → resolved The PR body reads Relates to #315.. There is no closing keyword in the body, the title, or any commit since round 1. [T]
F3 Nit → resolved The always-estimating wrappers are now test-only (_test.go). Every production caller of getPacketPath/getPacketPathsBulk passes s.estimatedPositionsEnabled(). [A]+[T]
F5 Nit → resolved The notice uses the .estimated-positions-note class (0.85em in style.css), with no inline size or colour. The screenshot matches the neighbouring muted note. [T]
N1 Low — test gap Default mode is not pinned for pingScorePathArchiveFingerprint. Mutant R8 always uses the stripped comparison (estimatesEnabled := false in pathsForRecords) and survives every TestEstimatedPositions*/TestPingRecord*/TestCycle* test. Under R8 a default (enabled) instance would keep a stale archive when only the estimate geometry changes, which is a silent behaviour change from master. Head behaves correctly [T]. [T] mutant + probe
N2 Low — test gap "Disabled skips estimator work" is not pinned for the background history engine. Mutant R11 hard-wires true at the two getPacketPathsBulk call sites in Cycle()/pathsForRecords and survives the suite and the F1 tests. The new fingerprint strip hides the difference for records that already exist. Under R11, a disabled server keeps running the neighbour estimator every cycle and persists estimate geometry in archives first captured while the policy is off. Head is correct [T]. [T] mutant + probe
F4 Nit / plan (carried over) The Customizer display preference is still deferred and not filed as a follow-up issue. The author's report says so too. It needs a tracking issue (not created by me, per my review rules). [K]
F6 Info (carried over) /api/config/client failure → client falls back to enabled; server still enforces. Unchanged, acceptable. [A]
I1 Info Mutant R10 hard-wires true in computePingScore and survives. It is output-equivalent, because distances skip approx endpoints (db.go ~2781/2803). Its only caller is computeAllPingScores → StartPingScoresRecomputer, which main.go does not start; production uses the history engine. No action needed. [A]+[T]

Suggested regression tests for N1/N2. These are scratch tests, not pushed. Both pass at head, and each kills its mutant:

N1 — enabled mode must still recapture an estimate-only change (kills R8)
func TestReviewerProbe_EnabledRecapturesEstimateOnlyChange(t *testing.T) {
	fx := seedEstimatedPositionsArchiveFixture(t)
	before, err := fx.store.LoadPathArchives()
	if err != nil {
		t.Fatal(err)
	}
	// Move the relay's only positioned neighbour: the estimate changes, the route does not.
	if _, err := fx.srv.db.conn.Exec(`DELETE FROM neighbor_edges WHERE node_a='relay'`); err != nil {
		t.Fatal(err)
	}
	if _, err := fx.srv.db.conn.Exec(`INSERT INTO neighbor_edges(node_a,node_b,count) VALUES('relay','pingobsc',10)`); err != nil {
		t.Fatal(err)
	}
	fx.clock.Advance(time.Hour)
	if _, err := fx.engine.Cycle(); err != nil {
		t.Fatal(err)
	}
	after, err := fx.store.LoadPathArchives()
	if err != nil {
		t.Fatal(err)
	}
	old, next := before["allTime.farthestPing"], after["allTime.farthestPing"]
	if mustJSON(t, old.Path) == mustJSON(t, next.Path) || old.CapturedAt == next.CapturedAt {
		t.Fatalf("enabled cycle did not recapture new estimate geometry:\nold=%s\nnew=%s", mustJSON(t, old), mustJSON(t, next))
	}
	if !strings.Contains(mustJSON(t, next.Path), `"lat":56.05`) {
		t.Fatalf("new estimate not from pingobsc: %s", mustJSON(t, next.Path))
	}
}
N2 — a record first captured while disabled persists no estimate geometry (kills R11)
func TestReviewerProbe_DisabledFreshCaptureHasNoEstimates(t *testing.T) {
	fx := setupEngineFixture(t, pingScoreHistoryEngineConfig{SettleDebounce: time.Minute, DeepSweepBatchSize: 100, RetentionDuration: 30 * 24 * time.Hour})
	off := NewServer(fx.srv.db, disabledEstimatedPositionsConfig(), nil)
	eng, err := newPingScoreHistoryEngine(off, fx.store, fx.clock.Now, fx.config)
	if err != nil {
		t.Fatal(err)
	}
	fx.engine = eng
	ts := fx.clock.Now().Add(-time.Hour)
	id := seedPingTrigger(t, fx.srv, "estarchive0001", "#test", "sender", ts.UTC().Format(time.RFC3339))
	seedPingObservation(t, fx.srv, id, "pingobsa", 9, `[]`, `[]`, ts.Unix())
	seedPingObservation(t, fx.srv, id, "pingobsb", 7, `["aa"]`, `["relay"]`, ts.Unix()+10)
	seedPingObservation(t, fx.srv, id, "pingobsc", 5, `["aa","bb"]`, `["relay","relay2"]`, ts.Unix()+20)
	if _, err := fx.srv.db.conn.Exec(`INSERT INTO neighbor_edges(node_a,node_b,count) VALUES('relay','pingobsa',10)`); err != nil {
		t.Fatal(err)
	}
	settleEntry(t, fx)
	rows := probeArchiveRows(t, fx) // raw SELECT … FROM ping_score_path_archives
	if rows == "" {
		t.Fatal("nothing captured")
	}
	if strings.Contains(rows, `"approx"`) {
		t.Fatalf("disabled engine persisted estimate geometry: %s", rows)
	}
}

A neighbor_edges query-count assertion around a disabled Cycle() would work as well, using the same pattern as setupPacketPathCountingDB. It would be the more direct check on "skips the work".

Answers to the review points

1. F1 — Cycle() with enabled=false no longer rewrites archives

  • [A] The fix is in pathsForRecords: with oldOK, both the old and the new capture now go through pingScorePathArchiveFingerprint(path, estimatesEnabled).
    • When enabled, that is the old byte serialization.
    • When disabled, both sides are serialized, unmarshalled into a clone, run through stripEstimatedPositions and re-serialized.
    • If the fingerprints are equal, next = old, so reflect.DeepEqual reports no change and nothing is written. No new SQL.
    • The clone protects the live old.Path that the snapshot serves.
    • TouchedAreas is already nil in archives, because newPingScorePathArchive clears it, so the strip's TouchedAreas = nil hides nothing.
  • [T] My round-1 probe, rebuilt (TestReviewerProbe_DisabledCycleRawRows): enabled capture → NewServer(db, disabledEstimatedPositionsConfig(), nil) + a new engine on the same store → clock +1h → Cycle(). It compares the raw rows of ping_score_path_archives (record_key|hash|timestamp|captured_at|path_json) via SQL, not just LoadPathArchives().
    • Red at 9e04abe5 (disabled cycle rewrote persisted archive). Green at dc7ea269.
  • [T] Second probe (TestReviewerProbe_ToggleAndExpiry) runs these steps:
    1. 3 disabled cycles (deep sweep, batch 100). The rows are identical after each one, and the disabled handlePingScorePath response has no approx or relay coordinates.
    2. DELETE FROM observations, which simulates the raw evidence expiring while the policy is off, then one more disabled cycle. The rows are still identical.
    3. A restart with the policy on (a new engine loading from the store) plus a Cycle(). The rows are identical: same captured_at (2026-03-01T12:00:00Z) and same path_json.
    4. The enabled handlePingScorePath response again shows the relay with approx:true and lat/lon set.
    • Red at 9e04abe5 (cycle 0). Green at dc7ea269.
  • [T] The author's TestEstimatedPositionsDisabledCycleKeepsArchives, copied onto the 9e04abe5 tree, is red there with the same "rewrote persisted archives" message. …StillRecordsRealChanges is green on both trees; it guards against over-preservation rather than reproducing the bug.
  • [T] Mutants: see the table below. Macmini's M9 and M10 were re-run and both are killed. My R4, R6 and R8 are new.

2. F2 — closing keywords

  • [T] The PR body contains Relates to #315.. A grep for close[sd]|fix(e[sd])|resolve[sd] followed by #N finds nothing in the title, the body, or the messages of 68345aab, c2cac60a and dc7ea269. That also holds for 9e04abe5.

3. F3 — no bypass routes

  • [T] GetPacketPath and GetPacketPathsBulk no longer exist in any non-test file of cmd/server, cmd/ingestor or internal/. The one remaining hit is a test name in a comment (ping_score_bulk.go:352).

  • [A] testPacketPath/testPacketPathsBulk live in packet_path_policy_test_helpers_test.go, so the server binary cannot link them.

  • [A] All production callers of the remaining functions pass the server policy:

    Caller Call
    routes.go:3731 handlePacketPath getPacketPath(…, s.estimatedPositionsEnabled())
    ping_score_path.go:50 (live fallback) getPacketPath(…, s.estimatedPositionsEnabled()); the response is also stripped on the clone (:78)
    ping_scores.go:187 computePingScore getPacketPath(…, s.estimatedPositionsEnabled()) (not started in prod, see I1)
    ping_score_history_engine.go:427 Cycle getPacketPathsBulk(…, e.server.estimatedPositionsEnabled())
    ping_score_history_paths.go:264 pathsForRecords getPacketPathsBulk(…, e.server.estimatedPositionsEnabled())
  • [A] The estimator is reached only behind the policy:

    • nearestPositionedNeighbor is called from getPacketPath's neighborLookup (which returns early when disabled), from estimateNodePosition (gated) and from computeAreaPositionGaps.
    • computeAreaPositionGaps is called only inside if s.estimatedPositionsEnabled() (routes.go:694).
    • nearestPositionedNeighborsBulk sits behind if estimatesEnabled (ping_score_bulk.go:507).
    • computeSuspiciousGPSPositions sits behind an early return ahead of the cache (routes.go:714).
    • buildPacketPathResponseFromReduction, the only place that sets Approx = true, has exactly the two gated callers.
  • [T] Bypass mutants:

4. F5 — font size

  • [T] roles.js notice: class="text-muted estimated-positions-note", no inline style. style.css: .estimated-positions-note { font-size: 0.85em; margin: 0 0 8px; }, so there is no colour literal and the colour comes from .text-muted → var(--text-muted).
  • [T] scripts/check-css-vars.js passes.
  • [T] Screenshots 315-disabled-areas.png and 315-disabled-position-gaps.png from the merged build: the notice now renders at the same size as the "1 node network-wide have no real GPS fix." note below it.
  • [T] My mutant J4 (drop the class) is killed by test-estimated-positions-config.js ("disabled notice carries the muted-note class").

5. Regression after the master merge

  • [T] c2cac60a is an honest merge: its tree equals git merge-tree --write-tree b0b9843c 68345aab (371958b3).
  • [T] I prepared the CI fixture in scratch as CI does: freshen, the Packets page collapse button in the left column of the table opens the dialog. Kpa-clawbot/CoreScope#1486/"Group Data" message type missing from packet view window's "message type" filter Kpa-clawbot/CoreScope#1791 inline seed SQL, corescope-migrate, then seeds 2073, 199 and 245.
  • [T] I started five servers with a Bay-area areas config so the estimator paths really run: master (no section), merged with no section, {}, true and false. I fetched 448 endpoints from each:
    • /api/config/client, /api/analytics/areas, /api/analytics/gps-sanity, /api/nodes?limit=5000 and /api/ping-scores;
    • 43 node details: all 23 nodes without GPS, plus 20 GPS nodes that have neighbour edges;
    • 400 /api/packets/{hash}/path.
    • All 2240 responses returned HTTP 200, and 27 master responses contain estimates.
  • [T] No section / {} / true: 447/448 responses are byte-identical to master after JSON parse. The only difference is the additive estimatedPositions: {enabled: true} in /api/config/client. The 1-ULP score noise I saw in round 1 did not appear this time.
  • [T] false: the only differences are the documented ones:
    • estimated_* removed from 18 node details;
    • approx point lat/lon set to null and approx* removed on 8 packet paths;
    • Areas reduced to bridgeNodes/density/unpositionedTotal/estimatedPositionsEnabled:false;
    • GPS sanity returns {estimatedPositionsEnabled:false}.
    • A key scan of every false response finds no estimated_*, approx*, estimatedNodes, positionGaps or unpositionedNoNeighborFix keys.

6. General

  • [T] knownServerWriteSQL, readonly_sql_literal_test.go and readonly_invariant_test.go show no diff against master. The F1 fix adds no SQL.
  • [T] Leaflet sha256 pinning: .github/ has no diff between 9e04abe5 and dc7ea269. Against master, the only diff is the 15 added Feature request: allow operators to disable estimated node positions via configuration #315 lines. I re-downloaded the 5 assets and all 5 hashes match the pins.
  • [T] Fork guards (github.repository == 'Kpa-clawbot/CoreScope') appear 9 times in deploy.yml and once in release-fast-path.yml, at head, in the merged tree and on master.
  • [T] The non-test map[string]interface{} count in cmd/ is 727 on both master and the merged tree. The 3 + lines in the diff are re-indented openapi.go lines, as in round 1.
  • [T] scripts/check-xss-sinks.sh --diff against bf3151a4 exits 0, both at head and on the merged commit. I ran it in a scratch --shared clone.
  • [T] There are no hex or rgb() literals in the added public/ lines.
  • [T] The three new commits have author and committer dborup <kontakt@meshview.dk>.

Issue acceptance criteria → tests

AC Test(s) Red/green
Omitted vs explicit false; invalid types TestEstimatedPositionsConfig, TestEstimatedPositionsInvalidStartup M7 and M8 killed in round 1 [T]; unchanged since
API enabled / disabled / omitted, with and without GPS TestEstimatedPositionsNodePreservesGPS, #315 E2E apiChecks R9 killed by E2E [T]; 448-endpoint diff [T]
Disabled skips estimator work …PathsSkipNeighborQueries, …AreasSkipQueries…, …AnalyticsDisabled M1, M2, M3 and M5 killed in round 1; engine call site not pinned (N2, R11 survives)
Single and bulk consistent, routes preserved …PathsSkipNeighborQueries round 1 [T]
Analytics: disabled ≠ zero or no evidence …AnalyticsDisabled, test-estimated-positions-config.js, test-analytics-areas-tab.js round 1 [T]; screenshots [T]
Frontend and browser, incl. direct URLs and saved prefs #315 E2E, test-packet-path-map.js, test-estimated-positions-config.js J6 (all map.js deep-link gates removed) killed by E2E [T]; J4 killed [T]
Reported GPS and neighbour-graph regression node and path tests, Areas density/bridge [T]
Docs config.example.json, deployment.md, api-spec.md, OpenAPI [A]; the archive wording is now accurate
Read-only DB, no N+1 readonly_* unchanged and passing [T]
Issue point 5: stored data preserved …DisabledCycleKeepsArchives, …DisabledCycleStillRecordsRealChanges red at 9e04abe5, green at head [T]; my 2 probes likewise [T]

Tests and mutants run

Go suites

  • [T] Merged tree, cmd/server: go vet ./... clean; go test -count=1 ./... second run: ok (757s).
    • The first run (1514s, while the ingestor suite was also running) had one failure: TestHandleNodePaths_PrefixCollision_1352, 503 {"error":"index loading"}. That test calls store.Load(), which starts startBackgroundIndexBuilds, and then queries immediately, so the race is pre-existing. The PR does not touch store.go, the paths handler or that test.
    • Interleaved on an idle host with 10×100 runs each, master failed 6/1000 and merged 4/1000, so the flake is not from this PR. I found no issue tracking it; it should probably get one (I did not create one)..
  • [T] Merged tree, cmd/ingestor: go vet ./... clean; go test -count=1 ./... ok (1940s). internal/dbschema changed (comment only), so I ran it as a guard.
  • [T] At head, TestEstimatedPositions*|TestPingRecord*|TestCycle* plus my 4 probes pass. The same set, plus probes, under go test -race: ok (no DATA RACE).

Frontend and shell suites

  • [T] sh test-all.sh on the merged tree: 226 passed, 0 failed (226 files).
  • [T] node test-frontend-helpers.js: 709 passed, 0 failed.

E2E (local Go server on the CI-prepared e2e-fixture.db copy in scratch; servers stopped by port; the repo fixture was never touched)

  • test-issue-315-estimated-positions-e2e.js (merged build, pinned Leaflet): PASS default / enabled / disabled. I inspected the disabled screenshots.
  • test-e2e-playwright.js (merged): 131 passed / 1 failed / 3 skipped. Master on the same fixture gives 131 / 1 / 3 with the identical failure: "Version info lives on Perf dashboard", waitForFunction timeout. That is the environmental failure I already reported in round 1 and it is not caused by this PR. I used a scratch copy with fail-fast disabled to cover the rest of the suite. CI ran 132/135 with 3 skipped [K].
  • test-issue-123-map-lifecycle-e2e.js 15/15, test-map-nodes-pagination-e2e.js 3/3, test-issue-1236-map-mobile-e2e.js 3/3, test-issue-1329-map-controls-accordion-e2e.js 5/5, test-node-liveness-e2e.js 35 checks, test-issue-1281-location-row-e2e.js 6/6 and test-touch-targets.js: all pass.

Go mutants. The suite filter is TestEstimatedPositions*|TestPingRecord*|TestCycle* plus my probes, at head. Every file was restored with cmp after each run.

Mutant Change Result
M9 (Macmini) fingerprint ignores the policy killed: …DisabledCycleKeepsArchives + both my F1 probes
M10 (Macmini) strip the caller's live path, not the clone killed: …DisabledCycleKeepsArchives
R4 drop old when no fresh capture is available killed: 8 tests incl. …KeepsArchives, TestPingRecordArchivesSurviveRetentionAndRestart
R6 constant fingerprint when disabled (over-preserve) killed: …DisabledCycleStillRecordsRealChanges
R8 always compare stripped (estimatesEnabled := false) survives the suite; killed only by my N1 probe
R9 handlePacketPath hard-wires true survives Go; killed by #315 E2E
R10 computePingScore hard-wires true survives; equivalent and not started in prod (I1)
R11 history engine getPacketPathsBulk(…, true) ×2 survives the suite; killed only by my N2 probe
M12-style (Macmini) production call to testPacketPath not re-run; [A] the helper is defined only in _test.go

JS mutants (run at head)

Mutant Change Result
J4 notice without estimated-positions-note killed: test-estimated-positions-config.js
J5 remove the two deep-link gates in loadEstimatedNodesFromDeepLink survives, but equivalent: drawEstimatedNodes has its own gate
J6 neutralise all three map.js policy gates killed: #315 E2E browserChecks (disabled)

CI and branch state

Not verified

  • go test -race on the full suites, and the author's macOS/arm64 environment. I ran -race only on the targeted set above.
  • The Ping Scores record "View path" UI in a real browser. The CI fixture has no ping_triggers, so F1 is covered by the engine-level Go tests and my probes only.
  • Frontend coverage numbers from the CI-instrumented public-instrumented run; I served plain public/.
  • Real staging or production data, and Make neighbor position estimates conservative and evidence-aware #195 metadata (Make neighbor position estimates conservative and evidence-aware #195 is not merged).
  • The M12 compile-failure mutant, which I did not re-run.
  • Fixed in round 1 and not re-run this time: mutants M1–M8 and J1–J3. I checked by diff that the code they target was not changed in round 2, apart from the F1/F3/F5 edits reviewed above.

@dborup
dborup merged commit 41420a8 into master Oct 7, 2026
6 checks passed
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.

3 participants