Repository navigation
RS-13: A16 loss-repair fix (DEL repeat + repair share), VECTOR code guards, procedures and desk-check tool - #143
Merged
Merged
Conversation
…size With LIFETRAC_TX_BATCH=1 (the radio-monitor harness default) image_tx_daemon._batch_more priced the FIRST frame of a would-be batch with the 4 B container overhead, although a batch of one goes out unbatched. Every encode-to-fit frame of 240..243 B (BW500; 200..203 B at BW250) therefore looked like 2 fragments, and two full single-fragment frames were paired into a 492 B, 3-fragment train (2 fragments unbatched). Verified off air: two 243 B codec-6 deltas -> one 492 B batch -> 3 fragments. - codec-6 (VECTOR) frames are never batched, as the first frame or as a follower (carried, air order kept). The VS1 encoder sizes every frame to one fragment body, so batching can only lengthen the train, and it couples the loss of consecutive scene frames with their DELs (A16). - a lone frame is priced at its own payload, and a batch must never cost more fragments than sending the follower on its own (486 B + 243 B passed the per-frame guard at 4 fragments vs 3). - small frames still batch (RS-3.1) and 246 B pairs/triples keep the runt amortization the guard was written for. Tests: test_tx_batch_vector_guard.py (12; 8 fail on the old daemon), including a pair-size property sweep at both fragment bodies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nge-edge procedure RS13_VECTOR_LEG.md, amendments dated 2026-10-10 that apply from the next round (RS-13.1 stays judged by the text it flew under, quoted as "was"): - P8 is an absolute bound: on-air vector_stats p95 <= the encoder budget at the leg's camera rate (350 ms at 2 fps); the ratio to step 1 is recorded for information (A17). - P6's return clause counts the frame(s) already in the TX pipeline: codec-1 resumes within (1 + n) camera periods + one fragment airtime of the return command going on air, n <= tx_pipeline_depth + tx_prepare_ahead (A18). - The literal profile-2 leg command carries -ForceFrfHz <spot-checked carrier>; 2b (profile 1) explicitly does not (A19). RS13_RANGE_EDGE_LEG.md (new): the next radio session, not flown, radios only on an explicit GO per round. Conducted SMA-attenuator path (30 dB pads on both jacks, 20 dB fixed, 3/6/12/15/30 dB step set to 66 dB), safety rules (never key an open port, never join the jacks without the fixed pad, touch RF only with both radios parked), leakage check, same-day spot-check -> -ForceFrfHz, Round A 0 dB step (2c, 2a, 2c', 2a' with -ReactiveFire 1 -ProbeEcho 0: the VECTOR_SCENE.md 8.6 command-delivery row and the A16-fix confirmation at bw500), Rounds B/C walk-down per profile with per-step measures, RSSI tracking rule, stop rules and a RESULTS template. FHSS command and switch legs wait for the RS-12.15 reverse-delivery fix. RS13_RESULTS_TEMPLATE.md: P6/P8 rows follow the amendments. BENCH_RUNBOOK.md: index row for the range-edge procedure; -ForceFrfHz on the generic leg line (profile 2 only, prep 8). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…one epoch start)
Three findings of the 2026-10-04 reviews, each verified against the 2d_r4
leg (bench-evidence/radio_monitor_20261004_110423_4ab58b8d):
(b) Cold-start budget. In bridge mode the boot budget is the 12-fragment
LIFETRAC_FRAGMENT_BUDGET_BRIDGE_DEFAULT (byte_budget=2436 in the log)
until image_tx_daemon's retained tractor/link_budget arrives. VECTOR is
now held to one fragment of the active profile (LIFETRAC_REG_PROFILE,
which the harness passes to camera_service) or of the most conservative
image profile (203 B) until LinkBudget.update has run. Tile modes keep
the provisional budget; a known link_budget stays authoritative.
(c) Lazy encoder build. The first VECTOR frame imported OpenCV +
encode_vector and paid the process's first-call costs (l0 79.6 ms vs
~15 ms); the switch left a 1.038 s TX gap, past the strict < 1000 ms
FHSS authority streak. main() now builds the encoder before the first
capture and before MQTT, and warms the process with one frame on a
throwaway encoder (first-call costs are per process, not per encoder;
the kept encoder stays fresh, so its first frame is still "first").
Off air on the dev PC: first VECTOR frame 350-510 ms lazy -> 50-80 ms
prebuilt (steady ~45 ms). LIFETRAC_VECTOR_PREBUILD=0 restores the lazy
build.
(d) Double epoch start on every VECTOR entry (trigger first, then forced):
a real race, not cosmetic. The loop read the keyframe request BEFORE
cam.grab_rgb() (which blocks up to a frame period on the ffmpeg pipe)
and ENCODE_MODE AFTER it. The ENCODE_MODE command landed in that window
(cmd 08:05:22.520, TX gap 22.245 -> 23.283, a second frame 186 ms later
at 23.469), so the first VECTOR frame was built without its force (the
fresh encoder's own "first" start) and the next frame consumed the
force: a redundant LAYER_CLEAR 0 restart. On RE-entry the same race made
the first frame a delta against the previous session's stale mirror,
and a switch landing mid tile build could stamp codec 6 on tiles.
Fix: _MODE_LOCK makes "commit mode + raise its keyframe"
(_apply_encode_mode, dispatch_back_channel) atomic against "sample mode
+ consume request", which _build_frame now does after the capture
(force_evt=); the frame is built in that one sampled mode end to end
(tile encodes, cache key, codec byte). The M7 path's is_keyframe comes
from FrameAccum.last_forced. Contract unchanged: a mode change still
sets the event; quality-only changes still do not.
Tests: test_vector_entry_guards.py (20), including the 2d_r4 mechanism
reproduced through the legacy call order, single-start entry and re-entry,
lock-held keyframe raising, and main() building the encoder before the
first capture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tools/vector_replay_publish.py publishes a vector_dry_run capture to an MQTT broker at the capture's recorded inter-frame timing (absolute schedule, so a slow publish never drifts), with --speed, --loop/--passes, row windows (--start-row/--end-row/--start-at-key), seq-gap drop injection (--drop-rows by capture row, --drop-every by position in the pass), a per-frame JSONL --log and a --dry-run. Stdlib + paho only (runs inside lifetrac-v25:latest); QoS 0, never retained. Rows are numbered as the dry-run report and RS-13.1 RESULTS number them. tests/test_vector_replay_publish.py (23 tests, no broker): capture reader parity with vector_dry_run.iter_capture (incl. the real 2a_r4 capture), timing math (speed, max-gap, backwards steps), windows and start-at-key, drop injection keeping the time slot, publish order and absolute schedule with a fake clock and publisher, passes/loop/stop/Ctrl-C, QoS 0 + no retain, CLI dry run and usage errors. bench_tools/RS13_2_DESK_CHECK.md: rebuild the base image first (the deployed 4623980c2dac predates VECTOR web_ui), stage, bring up mosquitto + web_ui only through bench_tools/rs13_2_desk.compose.yml (lora_bridge behind an unused profile, bench radio endpoint off, own volumes, down -v), replay the four round-4 captures, the VECTOR_SCENE.md 7.5/4.3/3.5/6 checklist (C1-C20), what A16 looks like on screen with store-checked injected-drop expectations, the uplink tap, and the RESULTS skeleton under bench-evidence/RS_13_2_desk_check_*. Appendix A records a PC rehearsal (loopback broker + working-tree web_ui, no board): F1 banner under the console buttons, F2 RESYNC chip drawn past the 384 px canvas edge, F3/F4 fail-closed rules 1 and 5 not implemented, F5/F6 banner age/loss and SAFETY DETECTOR chip missing, F7 anchor age on NO_HORIZON epochs, F8-F10 minor. The stale-tile worker stayed silent through 125 s of VECTOR. BENCH_RUNBOOK lists the new files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VS1 now sends every DEL twice (its frame and the next, VECTOR_SCENE.md §4.3), so a base that applied the first copy sees the second name an id it no longer holds. Counting that as an orphan (§3.4 rule 4) would put an orphan on every DEL of a loss-free stream. The id is absent either way, so the store ignores it without an orphan and leaves a tombstone at its capture time (an existing tombstone is kept), which still refuses an older define of the id. A DEL of a held shape is unchanged (LWW against the define's geometry clock), so a repeat can never delete a newer shape. - vector_scene_store.py: the DEL branch of _apply_record. - VECTOR_SCENE.md §3.4 rule 4: DEL leaves the orphan list. - test_vector_scene_store.py: new test_a_del_naming_an_absent_id_is_no_orphan (duplicate DEL, never-held id, older define refused, newer accepted, the repeat inside the next epoch's start); the three orphan->resync tests use an INSERT for the unknown id instead of a DEL, counts unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…frame RS-13.1 A16: on a saturated VECTOR stream one lost frame that carried a DEL left the base a ghost for TTL_FRAMES applied frames; the DIGESTs mismatched on n_live, the resync refused the CONFIRMs that keep static shapes alive, the base TTL-dropped them while the mirror still counted them, and the carousel (slot 7, packed last) never got room to re-send them, nor was a repeat-pending shape ever offered to it, so the base stayed shapes short until the next epoch start (~30 s on air). Encoder-only fix (with the store's DEL rule of the previous commit), no CONFIRMs accepted in resync, so the 2-bit-tag guard of §4.3 stands: 1. DEL repeat. Every DEL goes out again in the next frame (slot 5 head, outside the CONFIRM/DIGEST reserve), so one lost frame never loses a DEL. A fresh define that re-uses the id in that frame replaces the repeat (it already states offset/colour for a base that missed the DEL; a DEL and a define of one id in one frame would share a capture time and the tombstone would refuse the define). The repeat stays owed until a repeat or a define of the id goes out; it is dropped for a layer an epoch start clears, and after TTL_FRAMES. 2. Repair share. Every fourth frame (V0's kappa duty at every level; at V0 the carousel frame itself) up to 25 % of the frame goes to re-sends BEFORE slots 5-6: carousel and repeat-once candidates alike, oldest (re)statement (define_frame) first. For an unchanged shape that is its TTL risk at a base in resync (the mirror's frames_since_verify is reset by CONFIRMs, so it is no guide); a shape with an UPD/UCOL since sorts by the same old frame, so a lost change is re-stated early. Not every frame (measured worse in the RS-13.1 sweep). Off-air acceptance (a16_sil.py, 400 frames, 100 single drops per case): case A16-like recovery p50/p90/max residual_p50 bw500 243 B real 0/100 3 / 7 / 10 1656.6 (shipped 33/100, 20/54/67, 1651.6) bw500 243 B norefresh 0/100 3 / 7 / 9 1656.8 (shipped 39/100) bw250 203 B real 0/100 7 / 9 / 11 1668.6 (shipped 60/100, 23/60/66, 1655.3) bw250 203 B norefresh 0/100 7 / 9 / 12 1670.5 (shipped 81/100, 1656.8) every disturbed episode recovered in the 70-frame window (0 unrecovered), lossfree_out_of_step_frames 0, base TTL drops == encoder TTL drops (0) in every window, no ghost survives the frame after the loss, carousel-due frames packing no re-send 0-3/99 (shipped 54/99 and 93/99). Residual +0.3 % (243 B) and +0.8 % (203 B) against the shipped encoder. Cost: ~2.6 DEL repeats per frame on the A16 scene (29 bits/frame at 243 B, 28 at 203 B, 1.5-1.8 % of the frame); the share moves up to 25 % of one frame in four from changes to re-sends. Encoder time unchanged (interleaved A/B on the A16 scene: p50 54.6 vs 55.8 ms at 243 B, 58.1 vs 57.7 ms at 203 B). Worst gap without a define/re-send/UPD for any live shape on the A16 scene: 7-8 frames (was 67 at 243 B, 114 at 203 B). Limits: at V1/V2 (100/40 B bodies) the fixed 1-in-4 share does not keep the 35-shape A16 scene inside the TTL (worst gap 27 frames at V1, 61 at V2; shipped 119/120). The floor/floorall columns of a16_sil.py patch enc._pack with the shipped-packer counterfactual, so for this branch they measure that patch, not the fix. Tests: new SaturatedLossTests in test_vector_sync.py (A16 scene at 243 and 203 B: loss-free in step with no orphans; a lost DEL frame back in step within TTL_FRAMES+3+one carousel period with no epoch start, base TTL drops == encoder's, never shapes short after the ghost expiry; every DEL repeated exactly once unless re-used; the hard-verify gap stays under the TTL), test_del_repeat_yields_to_an_immediate_reuse_and_never_deletes_it. Adjusted: test_upd_ucol_and_del (the repeat DEL), the lost-DEL-past-the- cooldown test (loses the repeat frame too, to keep a ghost), the V2 fill-up precondition of the tiny-budget refresh test (7 frames, not 6). VECTOR_SCENE.md §4.2 items 5 and 7, §4.3 DEL repeat and repair share. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… V2 repair-share open item - VECTOR_SCENE.md §4.3: an owed DEL repeat is also dropped when an epoch start clears the id's layer and after TTL_FRAMES; on a range-1/2 key frame it still goes out for the kept layers (relies on §3.4 rule 4, tombstones are per epoch). - §3.4 rule 4: DEL is no longer an orphan, so it no longer feeds the >20 % orphan resync input. - §4.5.4: open item - the fixed one-frame-in-four, 25 % repair share is not enough at V2 (SIL single drop 3/40, 99-frame missing runs under 5 % random loss, residual +3-4 % at V1/V2); scale it with the ladder. - TODO RS-13: A16 fix, code guards, procedure amendments, desk-check prep and the range-edge procedure marked as prepared on this branch, with what stays open (V2 share, carousel counters, grid-policy SIL, design-owner sign-off, on-air confirmation on a GO). Integrated tree: base_station 1598 passed / 2 skipped; a16_sil.py full run 0/100 at both budgets (recovery max 10 / 11), lossfree 0, residual +0.3 % / +0.8 %. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Procedure scoring can produce invalid or ambiguous pass results, and replay CLI validation is incomplete.
4 open findings
What changed in this PR
Implements RS-13 loss repair, VECTOR entry safeguards, replay tooling, and future bench procedures.
Changes:
- Adds DEL repetition and scheduled repair capacity.
- Hardens VECTOR startup, budgeting, and transmission.
- Adds replay tooling, tests, and range-edge procedures.
| File | Description |
|---|---|
VECTOR_SCENE.md |
Specifies loss-repair behavior. |
tools/vector_replay_publish.py |
Adds timed MQTT capture replay. |
TODO.md |
Updates RS-13 status. |
RS13_VECTOR_LEG.md |
Amends existing leg criteria. |
RS13_RESULTS_TEMPLATE.md |
Updates evidence fields. |
RS13_RANGE_EDGE_LEG.md |
Adds range-edge procedure. |
rs13_2_desk.compose.yml |
Isolates desk-check services. |
RS13_2_DESK_CHECK.md |
Documents browser replay checks. |
BENCH_RUNBOOK.md |
Links new procedures and tools. |
encode_vector.py |
Implements DEL repeats and repair share. |
image_tx_daemon.py |
Prevents VECTOR batching and fixes pricing. |
camera_service.py |
Adds budget, prebuild, and mode-switch guards. |
test_vector_sync.py |
Tests saturated-stream recovery. |
test_vector_scene_store.py |
Tests absent-ID DEL handling. |
test_vector_replay_publish.py |
Tests replay behavior. |
test_vector_entry_guards.py |
Tests VECTOR startup guards. |
test_vector_encoder.py |
Updates DEL-repeat expectations. |
test_tx_batch_vector_guard.py |
Tests batching safeguards. |
vector_scene_store.py |
Treats absent-ID DEL as idempotent. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| |---|---|---| | ||
| | P1–P5, P7, P8 | As in RS13_VECTOR_LEG.md, amended 2026-10-10: P8 is the absolute 350 ms bound at 2 fps. **P3 interleaved:** pooled seq-gap loss of 2a + 2a′ against 2c + 2c′, two-sided Fisher exact p ≥ 0.05, or a margin stated in RESULTS before the legs. These legs carry probes, so their image loss compares only with each other, not with RS-13.1's probe-free legs | RS13_VECTOR_LEG.md Step 2 | | ||
| | R0 | Per leg, the median of the base's 10-s `rx_rf:` RSSI medians, and the same for SNR. R0 is the median over the four legs and must sit in −65 to −75 dBm. If it does not, change the fixed middle pad (radios parked) before Round B and re-measure with one 120 s VECTOR leg. The 0 dB legs stand, with their level recorded | archive `rx_daemon.log` | | ||
| | command delivery (§8.6) | Per leg: the distinct PROBE seqs in the tractor's `LoRa cmd: PROBE seq=N … (rx#M)` lines that also appear in the base's `PROBE TX seq=N … (tx#K)` lines, over the base's PROBE TX count. Pool VECTOR (2a + 2a′) and `mono_g4` (2c + 2c′). **Not worse** = VECTOR ≥ `mono_g4`, or two-sided Fisher exact on delivered / undelivered p ≥ 0.05. Ignore the base's own `probe:` line: with `-ProbeEcho 0` it counts echoes and reads 0 %. Keep the score as `legs/<leg>_probe_score.txt` | archive `tx_daemon.log` (tractor), `rx_daemon.log` (base) | |
Comment on lines
+123
to
+125
| (1 + n) camera periods + 1 fragment airtime. n is the number of codec-6 | ||
| frames the base receives between that line and the first codec-1 payload: | ||
| the frames captured or already queued when the command landed, which the |
| "broker at the capture's timing, with optional drop injection.") | ||
| ap.add_argument("capture", help="vector_dry_run capture JSONL") | ||
| ap.add_argument("--host", default=os.environ.get("LIFETRAC_MQTT_HOST", "127.0.0.1")) | ||
| ap.add_argument("--port", type=int, default=int(os.environ.get("LIFETRAC_MQTT_PORT", "1883"))) |
| | P1–P5, P7, P8 | As in RS13_VECTOR_LEG.md, amended 2026-10-10: P8 is the absolute 350 ms bound at 2 fps. **P3 interleaved:** pooled seq-gap loss of 2a + 2a′ against 2c + 2c′, two-sided Fisher exact p ≥ 0.05, or a margin stated in RESULTS before the legs. These legs carry probes, so their image loss compares only with each other, not with RS-13.1's probe-free legs | RS13_VECTOR_LEG.md Step 2 | | ||
| | R0 | Per leg, the median of the base's 10-s `rx_rf:` RSSI medians, and the same for SNR. R0 is the median over the four legs and must sit in −65 to −75 dBm. If it does not, change the fixed middle pad (radios parked) before Round B and re-measure with one 120 s VECTOR leg. The 0 dB legs stand, with their level recorded | archive `rx_daemon.log` | | ||
| | command delivery (§8.6) | Per leg: the distinct PROBE seqs in the tractor's `LoRa cmd: PROBE seq=N … (rx#M)` lines that also appear in the base's `PROBE TX seq=N … (tx#K)` lines, over the base's PROBE TX count. Pool VECTOR (2a + 2a′) and `mono_g4` (2c + 2c′). **Not worse** = VECTOR ≥ `mono_g4`, or two-sided Fisher exact on delivered / undelivered p ≥ 0.05. Ignore the base's own `probe:` line: with `-ProbeEcho 0` it counts echoes and reads 0 %. Keep the score as `legs/<leg>_probe_score.txt` | archive `tx_daemon.log` (tractor), `rx_daemon.log` (base) | | ||
| | A16 confirmation (2a, 2a′, bw500) | Every resync episode opened by an **isolated** lost frame (no other loss within 25 frames after it) ends **by DIGEST** (`digest` in the dry-run `resync:` episodes line) within **≈ 25 frames of the lost frame**, counted in capture rows from the seq gap to the episode's end. That is the `a16_sil.py` acceptance figure; a lost DEL's ghost alone holds the base out of step for TTL_FRAMES = 20 applied frames. No such episode ends at an epoch start more than 25 frames after its loss. `scripts/digest_trace.py` over each episode shows the base's live count back at the encoder's DIGEST `n_live` within those frames, with no later row where the encoder counts more live shapes than the base. Record the base's `ttl_dropped`, and the encoder's TTL/carousel counters if `vector_stats` carries them. The final `digest_ok` is not `False` (`leg_replay.py`'s `last DIGEST checked` beside it) | capture summary, `leg_replay.py`, `digest_trace.py` | |
Conflicts in BENCH_RUNBOOK.md and RS13_VECTOR_LEG.md resolved by keeping both sides: the 2026-10-10 amendment notes and carrier-range detail from this branch, the bench.env / serial / HostIp placeholders and legs/ pointer from the bench kit. base_station on the merged tree: 1598 passed, 2 skipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.


These are the RS-13 follow-ups that need no radio, prepared after the RS-13.1 campaign (record:
bench-evidence/RS_13_vector_scene_2026-09-26/RESULTS.md). Nothing here has flown. On-air confirmation belongs to the range-edge session, on an explicit operator GO.A16 fix: loss repair on a saturated stream
The problem (RS-13.1 A16, reproduced off air):
The fix (
4b464cbastore,e0309c8fencoder):How it was chosen. A judge panel compared three candidates built in parallel:
Results:
a16_sil.py, 100 single drops, bw500 / bw250Both full-run numbers were reproduced independently by the judge, and again on this integrated tree.
Spec (
VECTOR_SCENE.md, needs the design owner's sign-off):Code guards (
rs13/guards, 32 unit tests)image_tx_daemonnever batches codec-6 frames. A frame that fits one fragment is priced as one; the old pricing turned near-full 243 B frames into 2-fragment trains under-TxBatch 1. This also changes tile-mode batching, which is unflown (every RS-13 leg ran-TxBatch 0).camera_servicelimits the VECTOR byte budget to one fragment untillink_budgetarrives. On air, 2a_r4 started atbyte_budget=2436.camera_servicebuilds the VectorEncoder ahead of time, so a mode switch no longer leaves a ≥ 1 s TX gap. On FHSS such a gap would break the strict authority streak.LIFETRAC_VECTOR_PREBUILD=0turns this off.firstfollowed byforced.Procedures and the desk check
RS13_VECTOR_LEG.md"Amendments 2026-10-10" apply from the next round; RS-13.1 stays judged by the old text.-ForceFrfHz <spot-checked carrier>(A19).New
RS13_RANGE_EDGE_LEG.mdfor the next radio session:-ReactiveFire 1, which gives the §8.6 command-delivery row and the A16 confirmation;RS-13.2 desk check:
tools/vector_replay_publish.pyreplays a capture to MQTT at its own timing, with drop injection (23 tests);RS13_2_DESK_CHECK.mdandrs13_2_desk.compose.yml.A rehearsal already found renderer gaps F1–F10, recorded for follow-up: the RESYNC chip is clipped, the console buttons cover the banner, and fail-closed rules 1 and 5 are missing.
Verification
a16_sil.pyfull run above was done on this integrated tree.Notes
BENCH_RUNBOOK.md. Whichever merges second needs a small conflict resolution.RS13_2_DESK_CHECK.mdlinksbench_tools/deploy_base.sh, which bench kit: reproduce the LifeTrac radio bench from GitHub (setup, deploy, legs, guards) #142 adds; a manual fallback is documented.vector_stats;check-rx-grid-policySIL case;a16_sil.py's "floor/floorall" counterfactual columns swap in the old packer, so they report spurious failures on this branch. Read the "real" and "norefresh" columns.🤖 Generated with Claude Code