Archive the completed v1 roadmap, open the V2 gate board - #22
Merged
Merged
Conversation
docs/ROADMAP.md becomes the forward-looking V2 board: five workstreams (12-16) with an explicit status/gate model, where every workstream passes design entry, engineering, device behavior, claim truth, and release in order. The completed v1 phase narrative, feasibility analysis, and exit criteria move unedited to docs/history/ROADMAP_V1.md at the stable v1.0.7 tag, so v1 questions still have an answer without the active board carrying 1,000 lines of finished history. CLAUDE.md, AGENTS.md, and the README indexes point at the V2 board and its design-entry documents first, and at the v1 archive only when a question is actually about phases 0-11. version.h's versioning comment and build.yml's one ROADMAP reference follow the same move; neither changes behavior. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Focus asks one narrow question -- at this selected frequency, what did the receiver observe for how long -- and answers it with one bounded, radio-owned request that restores Watch on every exit path. It is bench-image only: there is no operator control yet, `coverage` is deliberately blank in focus.csv, and no coverage or activity label can be displayed until its thresholds are earned. Firmware: a Core-1 one-bin request with a fixed 141-bucket RSSI histogram (median/P90/peak with no heap allocation), a Core-0 focus.csv writer, and a restore-before-publish terminal row. A request is bounded in wall-clock time as well as in samples -- every sample waits on the shared SPI bus, so a contended bus could otherwise stretch a pass past its dwell with the radio away from home and nothing to stop it. Mutual exclusion with Probe, Sweep, Cell, and Scope is enforced in both directions. Also fixes a latent silent-drop in Serial Control's STATUS: its argument buffer was hand-sized to 240 bytes against a frame budget of ~230, and an over-long frame is dropped rather than truncated, so a long session's wider counters would have lost the whole frame -- newest fields first, with no error. The budget is now derived from the frame size and tested at saturation. Evidence, all on real hardware: - Cancel, injected failure, timeout, and four-way arbitration each restore home before their durable row is written. - The controlled dwell matrix ran 900 trials with zero transport errors, zero queue or row drops, and home restored every time. 14 of 15 arms separated a controlled source from ambient 30/30 vs 0/30. - Budgets are measured off the built image rather than estimated: 188 B of working state against a 256 B target, a 189 B worst-case row against a 256 B buffer, 1,072 B radioTask frame in a 6,144 B stack. The matrix's most consequential result is a limit, not a capability: detection tracks the source's airtime against Focus's sample spacing, not dwell length. With a fixed 8 samples the spacing is dwell/7, so a 2,000 ms dwell observes eight instants rather than 2,000 ms, and worst-case detection degraded monotonically as source airtime approached that spacing until one arm stopped separating entirely. That is the design's own "observation time is not coverage" rule with numbers behind it, and it means a later slice must scale the sample budget with the dwell before any operator-facing dwell or observation figure can mean what it reads. Consequently no single fixed RSSI condition separates every arm, so the qualifying condition gets a documented candidate rather than an accepted constant, and the coverage thresholds stay unselected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The closing paragraph still described the radio request/recovery path and bench harness as upcoming work; both shipped. It also predated the matrix, so it implied thresholds could be selected next -- but §6.4 showed the sampling design has to change first, and any threshold or radio-away budget chosen before that change would be measured against sampling the product will not ship. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 2026-09-04 matrix showed detection tracks the source's airtime against dwell/(samples-1), so choosing a real sampling policy needs to sweep spacings down to ~10 ms across a 2,000 ms dwell -- 201 samples, past the old ceiling of 64. The new 208 is bounded by the histogram rather than by RAM: buckets are uint8_t, so a pass whose samples all land in one 1 dB bucket saturates at 255 and the histogram then refuses to report a percentile at all. The histogram is a fixed 141 bytes whatever the sample count, so this costs no static RAM. Also documents a bench hazard that cost this session a wrong flash. The Cardputer and the bench Heltec enumerate identically and their ttyACMn numbering swaps between resets; targeting a raw path put LoRaTrace on the Heltec, which then failed its IO-expander init and read exactly like a dead receiver. The framed harnesses are self-checking, but flashing is not, so HARDWARE_TESTING.md now specifies /dev/serial/by-id and records the failure signature. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bench receiver's channel lived only in config.txt on the bench SD card, so replacing a failed card silently moved home from 918.5/SF8/BW125 to the built-in 906.875/SF11/BW250 default. That is not a cosmetic difference: Focus receives at the *home* bandwidth, so measurements would have continued at a different bandwidth than the run they were being compared against, with nothing in the output to show it -- and at BW250 the +-125 kHz bin-offset analysis inverts, since that offset falls inside the passband instead of outside it. The bench image now pins the channel through build flags and says so at boot. Production is untouched: benchHomeChannelOverride() returns false and changes nothing without both LORATRACE_BENCH_FAULTS and the -D flags. Harness fixes, each from a failure this session rather than from review: - The matrix waits for SD to mount instead of failing on one early poll. SD readiness lags HELLO by seconds, so a healthy device read as a broken one. - It refuses to run unless the resolved home channel matches --expect-home-khz, which is what would have caught the card swap immediately. - --samples is repeatable, so a spacing sweep is one process with one pre-flight gate rather than several invocations that could each half-run. - HELLO gets a 45s window. Opening native USB-CDC resets the board, but when that reset lands varies, and the fixed 8.5s settle could expire before boot had even started -- which read as an unresponsive device. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first bench slice fixed the sample count at 8, which made a longer dwell strictly worse at catching bursts: spacing grows with the dwell, so a 2,000 ms pass observed eight instants rather than 2,000 ms. A 360-trial sweep held the dwell at 2,000 ms and varied only the spacing. A 94 ms source was missed at 286 ms and 100 ms spacing (8/15 and 2/15 trials) and caught in 15/15 at both 50 ms and 20 ms, with the worst-case reading improving from -97 dBm to -66 dBm between those two -- so 50 ms is the edge of working, not a safe choice. Detection collapses once spacing approaches the source's airtime. 20 ms keeps roughly a 2x margin against the ~40-50 ms airtime of the fastest realistic mesh traffic. That margin is a judgement about what is worth observing; the measurement itself says only that spacing at or above the airtime fails and half of it works. Finer sampling costs nothing measurable: radio-away was 2,073-2,075 ms across every arm whether the pass took 8 samples or 101, and the histogram is a fixed 141 bytes regardless of sample count. The cost of a longer dwell is the dwell. focusSamplesForDwell() rounds the interval count up, because truncating would let a 30 ms dwell take two samples 30 ms apart and quietly violate the constant it derives from. It is split into two constexpr functions because the device toolchain builds as C++11, where a constexpr body must be a single return -- the native test environment accepts locals and does not catch it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bench measurements need real path loss between the transmitter and the receiver, and USB does not reach that far. The transmitter now optionally joins a network and serves the identical @LTTX/1 framed protocol over TCP, so a host can drive it from across a house. The harness needed no change at all: bench_harness.py opens ports through pyserial's serial_for_url(), which already accepts socket://host:port. Replies follow whichever transport issued the command and stay there. That is not a detail: ARM's TX_STARTED/TX_DONE are emitted from loop() long after the command returns, so a bridge-driven pulse would otherwise send its completion evidence out a USB port nobody is reading, and the harness gates every source-on trial on exactly those frames. Credentials live in NVS, set once over USB by scripts/heltec_wifi_setup.py, which prompts locally so a password never reaches a transcript or a shell history. WIFI_SSID/WIFI_PASS are refused over the bridge itself: a credential change should not be possible through the transport it authorizes, and a wrong one would disconnect the sender mid-command. WiFi.setSleep(false) is applied after association rather than before. Setting it ahead of WiFi.begin() is silently discarded, leaving the station in modem sleep -- which keeps the association looking perfect to the AP while dropping broadcast frames, so the board answers no ARP and appears dead to everything except its own logs. The fixture is mains powered; there is nothing to save. NET_TEST dials outward and reports the result, which is what finally distinguished "our firmware is broken" from "the network is blocking these two clients". It showed the board reaching the gateway and the internet while unable to reach the host, in both directions -- wireless client isolation, not a defect. Documented so the next person spends minutes on it, not an evening. SECURITY: the bridge has no authentication, and its commands key a transmitter. It is off unless explicitly enabled, output stays capped at TX_POWER_DBM, and every transmit path is bounded -- but anything that can reach the port can fire a pulse. Trusted networks only; --disable when idle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 900-trial matrix proposed `p90 >= -90 dBm` while the bench source sat ~70 dB above ambient -- a regime where every metric separates and any threshold looks defensible. Repeating the question at a realistic level (transmitter outdoors, driven over the new WiFi control bridge; receiver antenna fitted, so the ambient floor is real; source peak ~-85 dBm against -99/-100 dBm) across 120 trials, the candidate fails outright. So does every floor-relative variant: peak-median, peak-p90, p90-median. One of ten metric/position combinations separates, by 2.0 dB, which is inside ordinary RSSI variance and would not survive a temperature change or a different antenna. Absolute and relative families fail together, so this is not a matter of having picked the wrong statistic. Two mechanisms explain it. At low SNR `p90` collapses even when the source is present for most of the window -- at one position the transmitter radiated 57-71% of the pass while `p90` still read -94 to -97 dBm against a -100 floor, because the instantaneous RSSI during a weak transmission mostly fails to stand above noise and only its best moments do. And real MeshOregon traffic reached -94 dBm during source-off control trials, which is not measurement error but an actual transmission that no RSSI condition can distinguish from a controlled fixture. Coverage reporting is untouched: valid passes, observation time, and the median/P90/peak summary remain honest. What the evidence refuses is the step from "RSSI was elevated" to "something transmitted" -- exactly the step §3's contract was written to prevent. The design was right and the constant was wrong, which is the outcome that measurement is for. `coverage` stays blank and the activity count stays unpopulated. Two routes remain untested: a per-pass count of samples above an adaptive floor, for which `FocusObservation` already reserves `qualifying_count` and never populates it; and CAD or packet evidence, which §3 already contemplates as the alternative basis for observed activity. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… time Two 240 s arms against one independently timed reference train, transmitter outdoors on the WiFi bridge so the link is real rather than a bench-adjacent one. Watch alone received 0.883 of the train; Watch with Focus interleaved at a 48.1% away fraction received 0.463. The 95% intervals do not overlap. The useful part is the arithmetic. If Focus merely stops the receiver hearing anything while away and costs nothing else, the predicted reception is the baseline scaled by the time left at home: 0.883 x (1 - 0.481) = 0.459. Measured 0.463, well inside the interval. So the loss is accounted for entirely by away time -- no extra penalty from retuning, from recovery, or from any lingering effect after home listening is restored. That makes the away duration Focus already records an honest proxy for what a request costs, which is what would make an operator-facing away-time display meaningful rather than decorative. It equally means there is no mitigation: half the listening time away is half the packets, and time between restored Watch windows reached 7.45 s. The budget decision itself is deliberately not made here. §6.3 requires it to be an operator product judgement taken after the measurement, not implied by it; this supplies the exchange rate and stops there. Limits are recorded with the evidence: a 0.883 baseline rather than a perfect link, one duty cycle at one dwell, and a fixed-interval fixture rather than real bursty traffic. Also hardens the harness's timeouts the way the matrix runner was hardened, after native USB-CDC truncation cost that run an abort. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every RSSI summary statistic was rejected at field levels, and the follow-up named one untested idea: `peak` is a single sample and therefore noise-prone, so count the samples above an adaptive floor instead of taking an extreme of them. A pass with one stray strong sample and a genuinely busy pass have the same peak; only a count tells them apart, and there is a host test for exactly that case. Bench images now retain the pass histogram and report a ladder of counts at median + 2/4/6/8/10/15/20 dB. That is deliberate: it lets one run evaluate any candidate rule offline rather than reflashing per guess, and it keeps the rule relative to each pass's own floor, since an absolute threshold is precisely what the previous measurement rejected. Across 120 trials at the same porch configuration, `C6 >= 2` -- two or more samples at the pass median plus 6 dB -- detected 57/60 source-on trials, 95% CI [0.863, 0.983], against 1/60 source-off. `C4 >= 5` performs identically, so the ladder has a plateau rather than one lucky point. The single flagged control trial read -63 dBm against a -101 dBm median with six elevated samples. Nothing 38 dB above its own noise floor is noise: that is a real transmission that was not ours, correctly caught by the detector and mislabelled by an experiment that cannot tell "our fixture is quiet" from "the band is quiet". The 1.7% false rate is an upper bound, not a measurement against silence. The three misses are all within a couple of dB of the floor, which is where a detector should miss. This is a candidate, not a constant, and `qualifying_count` stays unpopulated. The threshold is a count out of 101 samples, and the sampling policy now scales sample count with dwell, so two samples is 2% of a 2,000 ms pass and 33% of a 100 ms one. Expressing it as a fraction of accepted samples, or validating it per dwell, is the next measurement rather than a decision to take at a keyboard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two sweeps, 480 trials, no arm failures, same porch configuration. The dwell sweep answers whether the rule is a count or a fraction. Roughly 4-5% of accepted samples transfers between 500 ms and 2,000 ms passes, so it is a fraction. It does not rescue a 100 ms pass: with six samples no threshold both catches the source and rejects ambient, at any margin. That is a floor on the instrument rather than a tuning problem, and it means a short pass may report coverage honestly while reporting no activity at all. The occupancy sweep matters more, because it bounds what the rule can claim. Holding dwell fixed and varying only the source's duty within the pass, detection runs 90-93% at 43-57% occupancy and collapses to 37-43% at 28.6%. A single SF8 packet inside a 2,000 ms pass is 3-7% occupancy -- far below where this already misses more than half the time. So the rule detects a persistently occupied channel; it does not detect individual packets. That is not a defect to tune away, it is the honest scope of sampling energy rather than detecting a signal, and it makes CAD or packet reception the better-supported basis for §3's observed activity. §3 has contemplated that alternative from the start; it is now an evidenced preference rather than a hunch. Median-as-floor survived every occupancy tested -- the pass median read -101 dBm for source-on and source-off alike. The reason bounds the result: at these levels most samples read like noise even while the transmitter radiates, the same effect that made p90 useless. A strong source at the same occupancy would pull the median up and break the assumption, and this sweep did not reach that regime or license any claim about it. qualifying_count stays unpopulated. What to populate it with is now a design decision with evidence behind it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both files were left behind by a scripted edit that failed partway through after the design doc had already been updated, so a4bc335 recorded the finding in one place and not the other two. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow-up 5 concluded that the count rule "detects a persistently occupied channel, not individual packets", and argued from there that CAD or packet reception was the better-supported basis for an activity claim. Both came from a single link at roughly 12 dB SNR. Repeating the SNR-sensitive tests at roughly 25 dB overturns it. At 28.6% occupancy, detection went from 11/30 to 26/30. The cliff moved, so it was never the statistical limit it was presented as -- it was where that particular link stopped delivering samples above the margin. Detection depends on occupancy and SNR together, and single-packet detection is untested rather than excluded: a 150 ms packet at 20 ms sampling yields about seven elevated samples, above the four or five the best rules require. The link change is worth recording on its own. A matched whip replaced the stubby and both antennas were stood vertical; vertical alignment mattered more than the antenna, worth about 13 dB at 918.5 MHz where the horizontal pair had been sitting in each other's pattern null. Two conclusions survived both links. The 100 ms floor is real -- six samples per pass detected 23/60 at 12 dB and 28/60 at 25 dB, so thirteen decibels bought almost nothing and it is a sampling limit rather than a link limit. And the rule is a fraction of accepted samples, 3.8-5.0% at 12 dB and 4.0-7.7% at 25 dB, not a fixed count. Recorded as a method note as well: three conclusions were drawn from one link, two held and the most consequential inverted. Where a conclusion depends on signal level it needs two levels before it goes in the evidence file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same partial-failure as b74b4b5: a scripted multi-file edit asserted against STATUS text that had already been rewritten, so 9d5a286 carried the correction in the design doc and evidence file but left the withdrawn claim standing in the two places most likely to be read first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Workstream 12's measurements bear on Sweep whether or not that was the intent. Phase 9 already recorded that a short bin dwell can miss genuine traffic; what 12 adds is the quantitative form -- detection needs airtime above the sampling spacing, six samples per pass is below a usable floor and 13 dB did not rescue it, and a count beats the extreme statistics Pass A currently thresholds on. Parked as a candidate rather than acted on, for two reasons written into the entry. Sweep aims at persistent energy across 200+ bins, where short dwells are a reasonable trade and Pass B's CAD step already exists because a Pass-A peak is not packet evidence -- the architecture encodes the distinction 12 measured. And 12 itself drew three conclusions from one link, two of which held while the most confident inverted on a second, so any claim about Sweep's sensitivity needs the same two-baseline discipline before it is written down. Entry criteria are concrete rather than a call to go and look: a controlled per-bin sensitivity measurement at two or more signal levels, reusing 12's transmitter fixture and benchSweepFloorQuery's existing readback, plus a decision on whether Waterfall should distinguish "sampled and quiet" from "barely sampled" in what it draws. Also adds the matrix runner's single-pulse mode, which places one armed transmission inside a long window instead of a burst -- the only way to ask whether a single packet is detectable, since a burst answers a different question however its duty is tuned. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s stale One armed 148 ms transmission placed inside a 2,000 ms pass -- 7.4% occupancy -- was detected 29/30 with 0/30 false positives at the stronger link. That settles the question the occupancy withdrawal left open: an RSSI-sampling pass can support a packet-level activity signal. A 148 ms packet sampled every 20 ms yields about seven elevated samples against the four the rule needs, so nothing about the instrument prevented this. The first link did. It is not unconditional. The same rule at the weaker link failed on sources occupying four times as much of the pass, so the rate depends on link quality the device cannot know. And the companion position carrying real traffic measured worse rather than better: a -57 dBm event in a source-off control both produced false positives and suppressed counts by lifting the median, which means Focus is least reliable exactly where a band is busiest. Wording an activity indication that survives that is a product decision now, not an open measurement. A placement analysis was attempted and discarded rather than reported. It tried to time each pulse against its window from log timestamps, but TX_STARTED reaches the host only when the harness next polls the socket, so the timestamps record host polling rather than RF timing -- and the output said so, classifying "missed the window" trials as detected at 71-97% while "inside" detected 0/2. Timing a pulse against the window needs a device-side timestamp. Also reconciles the design doc, which had silently missed its edit in each of the last three documentation commits: in both a4bc335 and 9d5a286 the assert that failed was the design doc's, not STATUS's, and the follow-up fixes patched the wrong file. It now carries the consolidated current position -- the rule's fractional form, the 100 ms sampling floor, the withdrawn occupancy claim, and this result -- rather than a state from three commits ago. The governing gate document being the stale one is the worst place for that error to land. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
version.h records an 11 dB under-read on Cell from sampling RSSI before the AGC settles after a light retune, and says outright that whether Sweep shares it is unknown. It does, and larger: adding a 5 ms settle moves Pass A's silent floor for bin 66 from -115.0 to -101.5 dBm, and 20 ms holds it at -101.7. Focus independently measures the ambient floor at that frequency as -99 to -101 dBm across hundreds of passes, so the settled readings are the real ones and the shipped configuration is about 14 dB low. It matters because Pass A's peak decision compares a bin against a noise floor plus a margin, and both terms come from under-read samples. The margin constant was calibrated against those same values so it is not obviously wrong, but it is calibrated on a scale that does not correspond to real dBm. Deliberately does not claim the other half. Whether Pass A can flag a bin carrying traffic is still open: even at 20 ms settle the source produced only +3.5 dB where Focus reads the same transmitter 25 dB above the floor, and the candidate explanations -- partial overlap with a 3 ms bin visit, bridge latency making "on air during this specific 3 ms" unguaranteeable, and a FULL begin() row that reads lower than the settled light path -- are untested. Two earlier attempts were invalid because firing only for a lap's duration radiates after the lap ends, which read as a silent band in every arm; both are recorded so the next attempt does not repeat them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eight settle values, five laps each, five bins, transmitter idle. Silence is the design: the floor is a property of the receiver and its retune, so measuring it with no source removes every timing problem that invalidated the first attempts at Sweep's detection question. Readings are flat from 3 ms through 40 ms. The shipped 0 ms configuration under-reads by 9.5 dB at bin 0 and up to 17.6 dB at bin 40; 1 ms recovers only about half. The cost is one settle per bin times 85 bins, so 3 ms predicts 0.26 s added per lap and the measured 1.70 -> 1.74 s is that within noise, against 5.60 s at 40 ms. Roughly 15% of lap time for 10-18 dB of accuracy. A designed control failed and is recorded as such. Bin 0 was meant to be unaffected because it takes a full begin(), and it moved 9.5 dB. The settle is skipped only when the global FULL retune mode is on -- the condition is !benchSweepRetuneFullEveryBin(), not a per-bin test -- so in shipped LIGHT mode bin 0 gets both the full begin and the settle. It shifted least, which is consistent with a full begin settling partially, but that is an observation rather than a demonstrated mechanism, and the clean control is FULL against LIGHT+3 ms across all bins. Not acted on. A 3 ms settle is the obvious fix and cannot be made alone: the margin constant was calibrated against under-read values, so moving the settle without recalibrating the margin changes Sweep's peak decisions in an untested direction. Also unexplained: the settled floor is not flat across the band, bin 40 sitting about 8 dB above bins 20 and 66 at every settle from 2 ms up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fully passive: 68 laps with two live repeaters as the sources and the bench transmitter explicitly quieted, so this injected nothing into a live mesh. Settle alternated 0 ms and 3 ms lap by lap, and seven control bins were read in the same lap as the two traffic bins, so the comparison needs no external ground truth. The margin constant is 35 dB above the noise floor. Applying it to what was measured: at the shipped 0 ms settle the best traffic reading sat 19.9 dB above the floor and would not have been flagged; at 3 ms the same traffic sat 46.3 dB above and would have been. Pass A would have missed both repeaters as shipped and caught both with the settle. The reason a 13 dB floor error becomes a functional failure is that the signal is under-read much harder than the floor: the same repeater burst read -95.3 dBm at 0 ms and -56.5 dBm at 3 ms. That matches version.h's note that the under-read grows with signal strength, and it means the two errors do not cancel -- the excursion the margin tests collapses from 46 dB to 20 dB and falls under the threshold. Deliberately claims no rate. Two detections in 34 laps per configuration is a tiny count, the hit rate is bounded by a ~3 ms bin visit coinciding with a burst rather than by sensitivity, and the 0 ms versus 3 ms hit counts are not distinguishable at that size. The evidence here is the magnitude of the excursion, not the number of hits. Recommendation recorded but not applied: settle and re-derive the margin as one change. Shipping the settle alone would apply a margin calibrated against under-read values to correctly-read ones. Workstream 17 moves to Design entry. The run ended at lap 68 of 200 on a USB-CDC transport timeout; this harness lacks the retry hardening the Focus matrix runner has. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
300 laps, zero skipped, 62.7 minutes, transmitting nothing -- two live repeaters as the sources, alternating 0/3 ms settle so both configurations saw the same traffic, and seven control bins read in the same lap. Judged by Sweep's own 35 dB threshold: the shipped 0 ms configuration flagged neither traffic bin in 150 laps, with a best excursion of 19.0 dB. The settled configuration flagged both, 3/150 each, with a best excursion of 51.6 dB. The shipped arm was not a near miss; it was short of its own threshold by a factor of two in dB. Specificity is clean in both arms. No adjacent bin and no control bin was ever flagged -- about 1,650 non-traffic bin observations per arm, zero false flags. When the settled configuration flags something it flags the two channels that actually carry traffic. The flag rates remain statistically unseparated and the writeup says so: 0/150 is [0.000, 0.025] and 3/150 is [0.007, 0.057], overlapping on [0.007, 0.025]. Separating those needs about 300 laps per arm and this run was sized before that arithmetic was done. It does not weaken the conclusion, because the rate follows from the margin arithmetic rather than evidencing it -- a configuration whose best excursion is 19.0 dB cannot clear 35 dB at any sample size. Also recorded: the ~2% rate at 3 ms is a coincidence rate set by the ~3 ms per-bin visit, not a sensitivity figure, and it binds a fixed Sweep too. Fix still unapplied: settle and re-derive the margin as one change. Co-Authored-By: Claude Opus 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.
docs/ROADMAP.md becomes the forward-looking V2 board: five workstreams
(12-16) with an explicit status/gate model, where every workstream passes
design entry, engineering, device behavior, claim truth, and release in
order. The completed v1 phase narrative, feasibility analysis, and exit
criteria move unedited to docs/history/ROADMAP_V1.md at the stable v1.0.7
tag, so v1 questions still have an answer without the active board carrying
1,000 lines of finished history.
CLAUDE.md, AGENTS.md, and the README indexes point at the V2 board and its
design-entry documents first, and at the v1 archive only when a question is
actually about phases 0-11. version.h's versioning comment and build.yml's
one ROADMAP reference follow the same move; neither changes behavior.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com