Repository navigation
feat(regions): direct-evidence placement aggregate + operator dry-run (#488 M2) - #612
Conversation
Red commit. Tests for the direct-evidence region placement aggregate behind the nodes-region cache, the operator dry-run report, the regionPlacement config block and the relayed hop count that M1's advertHearing dropped (review N2 on #609). Production files carry the types and signatures only, so every new test except the default-mode guard fails. The placement rule, aggregate and cache tests are ported from the research PR #601 under new names (rpm2*, TestRegionPlacement488_*), so the two never collide in package main. Relates to #488 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#488 M2) Green commit for #488 milestone M2. - region_placement.go: one global (node, region) evidence aggregate, classified per observation by M1's packetpath.ClassifyAdvert, behind the nodes-region cache machinery (watermark, delta scans merged only on success, full rebuild every 30 min, per-set singleflight, fresh/stale ages). Rule: verified direct evidence first, then the fewest-hops fallback capped at maxHops (a legacy '[]' without a frame is a 0-hop fallback only), else unknown. Membership is re-derived on every refresh, never extended. - regionPlacement config (mode/maxHops/lifetimeDays/persistSpanHours); every default keeps today's behaviour: mode "heard" never builds the aggregate. Noted for the customizer later. - corescope-server -region-placement-dryrun: read-only report (counts per evidence kind, basis and region, unknown share for several maxHops values), no node keys. - Review N2 on #609: StoreObs.advertEv keeps a relayed frame's hop count, so advertHearing no longer returns Hops 0 for relayed hearings. - Review N7 on #609: the computeNodeHomeRegions caller comment describes the M1 rule. Relates to #488 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rapport — CS-pve-agent1 PR#612 #488 — head e6222c8Status: #488 M2 is implemented on a draft PR. Every requirement has a test that fails on the red commit and passes on green, and all 27 mutants are killed. CI is green on every job without a rerun. The PR is not merged and not marked ready. Evidence tags: [T] = a test or command run in this session; [K] = read from code or firmware source; [A] = analysis or inference. Commits
Requirements
Mutants were applied one at a time with the file restored afterwards; the final run against the green tree killed 27/27 [T]. Two earlier survivors led to test changes. A1 led to the deep-equal stats check. C3 first targeted a redundant inner fresh-check, which was equivalent (a queued refresh finds no new rows and does not scan), so that check and a redundant inner singleflight were removed; the mutant now removes the coalescing mutex [T]. Benchmark (i7-1260P, 4 vCPU, go1.27.1; medians of 7, ranges over 3 runs) [T]The generated DB has 1,066,840 observations, 2,340 nodes, 4 regions and 5,400 pairs.
Invariants
Local runs
A full CI (head
|
| Job | Result |
|---|---|
| Go Build & Test | pass |
| Go Server Tests (1/3), (2/3), (3/3) | pass, pass, pass |
| Go Ingestor Tests | pass |
| Go Libraries & CLI | pass |
| JavaScript Unit Tests & Lint | pass |
| Script & Container Unit Checks | pass |
| Verify Go Test Assignments | pass |
| Playwright E2E Tests (1/3), (2/3), (3/3) | pass, pass, pass |
| Verify E2E Shards & Coverage | pass |
| Validate Docker Image / Build & Publish Docker Image | pass / pass |
| Publish Badges & Summary, Release Artifacts, Deploy Staging | skipping (fork guards) |
No reruns were needed; the known flaky test (#271) did not fail [T].
Operator: run the dry-run on a DB copy
# 1. consistent copy while the ingestor runs (online backup), in a writable dir
mkdir -p /tmp/488 && sqlite3 /path/to/meshcore.db ".backup '/tmp/488/meshcore-copy.db'"
# 2a. with the deployed image (serves nothing, exits when done)
docker run --rm --entrypoint /app/corescope-server -v /tmp/488:/data <corescope-image> \
-db /data/meshcore-copy.db -region-placement-dryrun > region-dryrun.md
# 2b. or from a checkout of this branch
cd cmd/server && go run . -db /tmp/488/meshcore-copy.db -region-placement-dryrun > region-dryrun.md
# optional: evaluate other values with a config.json that holds only
# {"regionPlacement": {"maxHops": 5, "lifetimeDays": 30}}
# and add: -config-dir <dir containing it>
# 3. review region-dryrun.md (counts only), paste it into #488, then delete /tmp/488The copy must stay in a writable directory: the server opens it mode=ro, but SQLite may create -wal/-shm files next to a WAL-mode copy. The report contains counts only: no public keys, names or observer ids.
Remaining
- The default
maxHopsis unvalidated [A]. The CI-prepared fixture has no per-observation frames, so it shows 0 direct, and 73 of 97 nodes unknown at maxHops 3 [T]. A real instance's dry-run is needed before M5. persistSpanHoursis implemented and tested but off by default, as the owner deferred transient regions. Transient regions are not exposed anywhere [K].- Lifetime overflow. A
lifetimeDaysvalue above about 106,000 overflowstime.Duration, and the window then behaves like "no window". The effect is harmless, but no explicit cap is enforced [A]. - Redundant fast path. The lock-free fresh check in
currentRegionPlacementis a fast path only. Removing it is an equivalent mutant (a queued refresh finds no new rows and only re-stamps the snapshot), so no test pins it [T][A]. - Out of scope. M3 (
region_placementAPI field,?regionBasis=), M4 (UI, customizer entries) and M5 (default flip) are not part of this PR [K]. - Single machine. All timings come from one machine. The advert-dense delta costs about 2× the heard scan; it runs only in placed mode, at most once per 30 s, in the background [T][A].
Review — CS-Macmini PR#612 — head e6222c8Dom: APPROVE med nits Independent, read-only review on the merged tree ( No correctness bug found. The default mode is provably byte-identical to master, the aggregate reuses M1's classifier with no second copy, the watermark/delta/rebuild/singleflight semantics hold, the dry-run is read-only and names no node, and the perf claims reproduce. One real test gap, the rest are documentation and config-polish nits. Findings
Nothing above blocks the draft; none changes default-mode behaviour. The review pointsM2 scope, default mode unchanged. Verified end to end, not just by unit test: master ( Aggregate reuses M1's classifier. Watermark / delta / rebuild / failed-delta. The delta scans into a separate aggregate and Singleflight and no full scan per request. Bounded memory. No SQL under Dry-run read-only and private. Ran the built binary on a copy of a CI-prepared fixture: exit 0, DB sha256 byte-identical before and after, no side files, and the report contains no public key of the 200 in the fixture and no hex run of ≥12 chars [T]. Config defaults off and documented. N2 and N7. N2: No new Colours / XSS / fork guards / authorship. The diff touches no Operator instructions — checked, and the warning is load-bearing. I reproduced the recipe on a WAL database with a writer attached: Acceptance criteria, red before / green after. I rebuilt the red tree ( Perf plausibility. Reproduced on my own generated DB (scale 20, 322,280 observations, 780 nodes, 1800 pairs), medians of 7 [T]:
Same shape as the body at a third of the scale: placed is ~2.9× faster cold, equal warm, faster on an ordinary delta, and slower only on the advert-dense delta (1.3× here vs the body's 2.3× at 4× the advert density — consistent with a cost that is linear in classified rows [A]). On the e2e fixture, cold 1.016 ms heard vs 382 µs placed [T]. The claim that the regression is bounded to one background scan per 30 s in placed mode only follows from Tests and mutantsAll on the merged tree, go1.27.0, macOS. CI run
Each mutant was applied alone to a throwaway copy and the file restored and diffed against the clean tree afterwards [T]. Not verified
|
Relates to #488
Milestone M2 of the plan on #488 (§9), built on M1 (#609): a global direct-evidence region-placement aggregate in
cmd/serverbehind the existing nodes-region cache machinery, plus a read-only dry-run report for operators. The default stays today'sheardrule, so nothing changes for users. M3 (API field,?regionBasis=), M4 (UI) and M5 (default flip) are not part of this PR.Plan
7989ded0): tests plus compiling stubs (types and signatures only). Every new test fails except the default-mode guard, which pins "no behaviour change" and is green by design.e6222c8d): the aggregate, the cache wiring, the config block, the dry-run flag, and the two M1 review nits.#601 is not merged. Its prototype aggregate, the RP488 cache tests and the generator are ported under new names (
rpm2*,TestRegionPlacement488_*,TestPerf_RegionPlacementM2*_488), so the two PRs never collide inpackage main.Changes
cmd/server/region_placement.goregionEvidenceAgg: one entry per (node, region), each a fixed 80-byteregionEvidenceStats(newest verified-direct hearing, newest hearing per hop count 1-7, oldest direct hearing, newest legacy'[]'). Every observation is classified by M1'spacketpath.ClassifyAdvert(its own frame first, then path_json). There is no second classifier.placeNodeapplies the rule.regionPlacementSnapshotis an immutable region → sorted keys indexcmd/server/nodes_region_cache.goplacedmode,refreshNodeRegioncuts the per-set entry from the snapshot. The entry keeps the existing fresh TTL, stale-while-revalidate, max-stale wait, LRU cap and per-set singleflight. Inheardmode the code path is unchangedcmd/server/config.go,main.go,config.example.jsonregionPlacement.mode/maxHops/lifetimeDays/persistSpanHours, documented inconfig.example.json. Noted for the customizer in a later milestone (AGENTS rule 8). New flag-region-placement-dryruncmd/server/region_placement_dryrun.gocmd/server/advert_evidence.go,store.goStoreObs.advertEvbecomes a 2-byteadvertEvidence{code, hops}, soadvertHearing()returns the real hop count for relayed frames instead of 0.StoreObsstays 168 bytes (TestStoreObsLayout*green). N7: the caller comment at thecomputeNodeHomeRegionscall site now describes the M1 ruleRule (placed mode)
maxHops. Ties form a set. Owner decision: a legacy row without a stored frame counts an explicit'[]'only as a 0-hop fallback, never as direct evidence.Evidence older than
lifetimeDaysbefore the node's own newest hearing is ignored. This keeps quiet nodes placed and lets a node that moved drop its old region. Owner decision: transient regions are deferred, sopersistSpanHoursdefaults to 0 (off). The rule is implemented and tested, but it only runs when an operator sets the value.Config (every default keeps today's behaviour)
modeheard(today's rule; the aggregate is never built)heardorplaced; anything else logs a warning and staysheardmaxHopslifetimeDaysretention.packetDays)persistSpanHoursAll four values are startup-only and are meant for the customizer later.
Cache semantics kept
observations.id > watermark, into a separate aggregate. The result is merged only on success, so a failed delta leaves the aggregate and the published snapshot untouched. The stats merge by min/max, so delta-then-merge equals a full build (deep-equal test).nodeRegionRebuildInterval(30 min). It picks up retention pruning, observer region edits and latefrom_pubkeybackfills, which all sit below the watermark. Full builds sharenodeRegionFullMuwith the heard cache.regionAggMu.SELECTs on themode=rohandle and nothing is persisted.PacketStore.muis never taken; this code does not touch the store.Performance
Measured on an i7-1260P with 4 vCPU and go1.27.1. Medians of 7 runs, with the range over 3 runs. The generated DB has 1,066,840 observations (66,840 ADVERT), 2,340 nodes, 4 regions and 5,400 (node, region) pairs.
On the e2e fixture: cold 1.1 ms vs 0.58-0.68 ms; delta 0.36 ms vs 0.16-0.28 ms.
TestRegionPlacement488_DefaultModeIsHeard).To reproduce:
Operator dry-run
The dry-run reads a copy of the database and never writes to it. It prints counts per evidence kind, basis and region, plus the unknown share for maxHops 0-5 and 7. It prints no public keys, names or observer ids.
mode=ro, but SQLite may need to create-wal/-shmfiles next to a WAL-mode copy.config.jsonholding only aregionPlacementblock into a directory and add-config-dir <dir>. The sweep table already compares maxHops 0-5 and 7.region-dryrun.md: it contains counts only. It can then be pasted into nodes: investigate direct-evidence regional placement without regressing historical cache #488 to choose themaxHopsdefault before M5. Delete the copy afterwards.On the CI-prepared e2e fixture, the report shows no direct evidence: the fixture has no per-observation frames, so every zero-hop hearing is a legacy
'[]'fallback. At maxHops 3, 73 of 97 nodes are unknown; at maxHops 5 the count is 59, and at maxHops 7 it is 28. Those figures match the #488 analysis. The fixture is a worst case, with about one hearing per advert, so the real numbers have to come from a production copy.Tests
TestRegionPlacement488_PlaceRule(22 cases)'[]'fallback-only (4 cases)…_StatsAreBounded,…_BoundedMemory…_DeltaEqualsFullBuild…_DefaultModeIsHeard…_PlacedModeDropsRelayedOnly,…_NewObservationCanRemoveRegion…_RebuildNeededForEditsPruneBackfill…_QuietNodePlacedFromSQLHistory…_StartupSingleflight…_FailedDeltaLeavesAggregateIntact…_V2Schemaobserver_id, ISO timestamps)…_SyntheticAccuracy…_DryRunReport,…_DryRunEmptyDB,…_FixtureDryRun…_DryRunFlag(process)main()with the flag prints the report for the configured settings and exits 0…_ServerUsesConfiguredMode(process)mode: placedserves/api/nodes?region=OAKtotal 5 (heard: 37)…_Config,…_ConfigExampleconfig.example.jsondocuments the block with values equal to the defaultsTestAdvertHearingKeepsRelayHops_488,TestAdvertEvidence_SetOnEveryObservationPath_488(extended)Constraints
PacketStore.mu.cmd/serverstays read-only: onlySELECTs, and themode=rohandle.map[string]interface{}in production files. The counts per touched file are unchanged (store.go197,config.go26,db.go67) and the new files have 0.scripts/check-xss-sinks.sh --diff origin/masterhas nothing to scan. No colours. Fork guards are unchanged: 9 indeploy.yml, 1 inrelease-fast-path.yml.node scripts/run-go-server-shard.js --validatepasses. The new tests run in catch-all shard 3.🤖 Generated with Claude Code