mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-10-19 11:01:43 -07:00
e1d1c1b84eb79d3fe3843841deafbf7339fc5554
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7b0f065a3f |
laser.emission-witness: the head is placed back only by a sample that read the window open
rapid_place searched the samples up to the first disarmed one, that one
included, and sample_to_disarm read each sample's X after its armed
flag. So the first sample to read the window closed could place the
head back with an X read after the close: a window that closed on the
return leg, up to about one sampling loop before the head was back,
placed the rapid inside the window with a disarm time of 0, and the
`0 <=` on that time could not fail.
rapid_place now searches the samples before the first disarmed one
alone, and sample_to_disarm reads the controller's report (its state
and X) before the sample, so a sample that reads the window open read
its X with the window open. The head stands back for 1 s before the M2,
not 0.5 s: the placement needs a sample whose X and armed flag both
come between the head's return and the close, and a slow sampling loop
needs that time. The judge only gets stricter. No energy or motion path
changes: the longer dwell runs dark after the M5 with the window open,
as the shorter one did.
Proof: tests/test_laser_dwell.py, the Unit tests step of forgetest-ci.yml
(python -B -m unittest discover -s tests), 13 tests OK on Windows with
python 3.14 and on Ubuntu 24.04 with python 3.12:
test_a_window_closed_before_the_head_is_back_does_not_place_the_rapid
gains the close at the first sample with the head back (2, None, 6),
and test_the_sampling_runs_on_past_the_close_and_ends_where_it_stopped
holds the order (each sample's X read before its armed flag).
test_cooling_suite.py (3) passes. Negative controls on scratch copies:
the search with the first disarmed sample included fails the first
case with (2, 6, 6); the report read after the sample fails the second.
emission_witness run whole on a fake clock with only its I/O stubbed
(the review's sweep: the armed flag served mid-GET, X read mid status
report, 40 phases of the rapid against sampling loops of 0.20 to
0.64 s): a window closing 0 to 2 s before the head is back fails the
placement at every phase and every loop, and the normal close passes
at 40 of 40 phases at every loop. The code before this change passed
early closes at some phases up to about one loop early (0.20 s at a
0.20 s loop, 0.5 s at a 0.64 s loop). With the new order and the 0.5 s
dwell, the normal close passed 39 of 40 at a 0.38 s loop and 12 of 40
at a 0.64 s loop. pyflakes reports no undefined name.
Acceptance: laser.emission-witness's fingerprint moves (its own text and
laser.py's shared text), laser.armed-kill and
laser.pause-resume-lid-cancel move through the shared text, and
cooling.flow-under-load and cooling.fail-tier-stop move because they
import from .laser in their own functions; the other 38 do not move
(source_sha compared before and after). Against the campaign this
series already needs, that adds no run:
|
||
|
|
bed46e961e |
laser.emission-witness: the read rule ends where the sampling stopped, and the rapid is placed inside the window
The dark stretch after the M5 runs on past the close, and three of its
judgments did not hold with it.
The read rule took the last sample served as the trail's last edge, and
sample() returns None for a request the daemon does not answer. So an
outage after the served close (one request that hangs to the client's
10 s timeout is enough) left a trail that ended at the close with no
gap, and the window that latches after the close was never read: the
test passed whole with a 30 ms fire at the end of the return rapid.
The sampling could also stop as early as about 1.8 s past the close,
and with a 0.9 s unread tail allowed, the last reading could come
before the latch of the window in flight: the review's tail model
(the kernel's ~1 s window latch, the sampler's GET timing, a fire just
before the close) finds 4 runs in 20000, 21 with a wider GET spread,
where that window goes unread and the judge passes.
The placement and the disarm timing searched the whole stretch. A
window that closed before the head was back (in the dwell at +20 mm
or on the return leg, up to about 2 s early) found the head back after
the close and passed "the rapid ran inside the dark stretch", with a
negative disarm time.
The rapid's sample count in the record ran past the close while its
span stopped at the close, so the record read about 8 Hz for a 5 Hz
loop.
sample_to_disarm moves from emission_witness to module level, ends two
windows and EMISSION_READ_GAP_S past the close, and returns the time
the sampling stopped beside the samples. read_gaps takes that time as
the trail's last edge (t_end), so a tail the daemon served none of is a
gap. rapid_place judges the placement over the armed part alone, up to
the first disarmed sample, and the disarm time must be 0 or more. The
rapid block counts its samples to the close and records how long the
sampling ran past it (sampled_past_close_s), and the log line says
both. The relit, HV and read judgments still cover the whole stretch.
The description says what is judged. tests/test_laser_dwell.py drops
relit(), a copy of the judge that no change to laser.py could fail.
Every change makes the judge stricter: no energy or motion path
changes.
Proof: tests/test_laser_dwell.py, the Unit tests step of
forgetest-ci.yml (python -B -m unittest discover -s tests), 13 tests OK
on Windows with python 3.14 and on Ubuntu 24.04 with python 3.12:
test_a_tail_the_daemon_never_served_fails (every sample served read,
then nothing for 2.9 s before the sampling stopped: gap 2.9 s, the rule
fails; served through, it holds),
test_the_sampling_runs_on_past_the_close_and_ends_where_it_stopped
(sample_to_disarm on a fake clock, the daemon serving the close and
nothing after it: the sampling ends at least 2.9 s past the close and
the rule fails), and
test_a_window_closed_before_the_head_is_back_does_not_place_the_rapid
(a close after the return places out, back and the close; a close in
the dwell at +20 mm leaves back None). test_cooling_suite.py (3) and
test_setup_burns.py (22) pass. Negative controls on scratch copies:
read_gaps without the end edge fails the first two cases; the end at
two windows fails the second; sample_to_disarm that returns the samples
alone errors in the second; rapid_place over the whole stretch fails
the third. emission_witness run whole on a fake clock with only its I/O
stubbed: a window closing 0.2, 1.0, 1.9 or 2.5 s before the head is
back fails the placement (the code before this change passed the first
three), and a normal close passes with 22 samples over 4.07 s to the
close in the record (32 before). Against a simulated daemon and kernel
latch with that 30 ms fire: an outage after the served close and one
request hung to the 10 s timeout fail the read rule (gaps 3.05 s and
10.32 s; both passed before); a tail served through with no fire
passes. In the review's tail model, the end edge alone and the longer
sampling alone each leave 0 of 20000 runs with the window in flight
unread and the judge passing, in both GET spreads. pyflakes reports no
undefined name.
Acceptance: laser.emission-witness's fingerprint moves (its own text
and laser.py's shared text), laser.armed-kill and
laser.pause-resume-lid-cancel move through the shared text, and
cooling.flow-under-load and cooling.fail-tier-stop move because they
import from .laser in their own functions; the other 38 do not move
(source_sha compared before and after). Against the campaign this
series already needs, that adds no run:
|
||
|
|
802cd3af8d |
laser.emission-witness: the LASER_ON count read through the rapid, and two windows past the close
The dark stretch after the M5 judged emission from the samples whose /status carried a laser block. forgectrl leaves that block out of a served /status when cnc/laser_on_sampled or cnc/laser_pgood_sampled does not read, and sample() records the count as None, which the relit judge (any sample with emission) and the HV judge (the maximum over the readings) both pass: a stretch whose count went unread after the dark point passed as dark. The kernel latches the count once per ~1 s window and holds it through the next, so a window with no read in it is a window in which a fire is not witnessed. The stretch also ended at the first disarmed sample, before the count for the window in flight at the close had latched, so the rapid's last second was never read. read_gaps(samples, key) reports, over a trail, the samples that carry a reading, the ones that do not, and the longest stretch of seconds between two readings, with the trail's ends as edges: an unread lead or tail is a gap, and a trail with no reading is one gap, its whole span. The judge requires RAPID_MIN_READS (4) readings of the count with no gap over EMISSION_READ_GAP_S (0.9 s), inside the EMISSION_WINDOW_S (1.0 s) latch period, and the same floor of readings for the HV current. Both checks come before the relit and HV-dark checks, so those never judge an unread stretch. sample_to_disarm keeps sampling for two windows after the first disarmed sample: the count for the window in flight at the close latches up to one window later and is held through the next, and the gap rule asks for a read of it in that tail, measured to the last sample served. The timeout bounds the wait for the close alone. The close is the first disarmed sample, which the disarm timing and the span are measured to; the evidence's rapid block carries emission_reads and hv_reads, the log line says how many samples carried the count and the longest unread stretch, and the description names the rule. Proof: tests/test_laser_dwell.py RapidReadTests, the Unit tests step of forgetest-ci.yml (python -B -m unittest discover -s tests), 10 tests OK on Windows and on Linux: every sample read (gap 0.2 s), one miss (0.4 s) and a 0.8 s hole are inside the rule; a 1.2 s hole, the block gone after the dark point (1 of 25 read, gap 4.8 s), the block gone for the last 1.6 s, and a one-sample stretch fail it. pyflakes reports no undefined name. Negative control on a scratch copy: with the trail's tail dropped from the edges, the last-window case and the block-gone case report no gap and their two tests catch it; with the gap bound raised to 1.3 s, the 1.2 s hole passes the rule and its test catches it. Acceptance: laser.emission-witness's fingerprint moves (its own text and laser.py's shared text), and laser.armed-kill and laser.pause-resume-lid-cancel move through the shared text; no other suite module imports .laser at module level. laser.emission-witness is always run, and the next campaign judges the rapid stretch's read cadence on the bench (about 5 Hz in the last campaign's record). |
||
|
|
0eb764bf75 | Added SPDX | ||
|
|
970f10a9e2 |
forgetest: the dwell-gap latch rule judges the hardware's unlocked window, the live runs keep a trail
laser.emission-witness required the hardware button latch clear in every sample the engine reported armed, and, after a first fix, in every sample up to the last nonzero emission count. Both windows were drawn from lagging signals: the engine's armed flag follows the controller's next report, and the emission counter latches once per second and reads nonzero about two seconds past the relock. Both reached into the tail where the job-end relock sets the button latch by design, and the rule refused three clean runs on image 20260902144848 (all four sides burned; the trail shows the latch clear from the press to the relock, emission through the fourth side, HV_ENABLE's dip in the dwell and its return). The rule now uses the window the hardware defines: from the first emission, in every sample whose readback word shows the laser latch unlocked, the button-latch bit of that same word must be clear. That spans the kernel-run gap of the dwell and ends at the relock, and no lagging flag can misplace it. dwell_gap() is a pure function; tests/test_laser_dwell.py holds the relocked tail, a set inside the gap, and a trail without emission. The recorded trail of the third run replays to a pass (47 unlocked samples, none set). The live runs keep a per-sample trail in the evidence (TRAIL_FIELDS: the readback word, the switches, the lock flag, the controller's state and messages), so a run's timeline can be read back without a rerun. A fourth run then errored on a name the refactor had removed and one later check still used; py_compile does not catch it and a live drill never executes on the host, so the CI job now fails on any undefined name in the harness (pyflakes). Catalog consequence: the laser implementation hashes move. |