Skip to content

fix: repair eleven confirmed bugs across search, storage, formats, and logging - #20

Merged
rasros merged 1 commit into
mainfrom
fix/confirmed-bugs
Jul 28, 2026
Merged

fix: repair eleven confirmed bugs across search, storage, formats, and logging#20
rasros merged 1 commit into
mainfrom
fix/confirmed-bugs

Conversation

@rasros

@rasros rasros commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Every bug here was reproduced before being fixed and verified after, several end-to-end through the real CLI. 434 tests pass, up from 402; the 32 new ones are regression guards, because most of these shipped precisely because nothing covered the path.

--epoch-variance crashed every run that used it. search/engine.py compared the imported score_std function to a float after the local was renamed to pool_std, so any positive value raised TypeError on the first epoch-end check, roughly three seconds in. Ruff cannot see it — score_std is a bound name. Nothing in the suite touched the low-variance path, which is now covered.

--llm-rate 0 still called the LLM. Epoch-0 seed tasks set force_llm, which bypassed the rate entirely, so there was no offline mode. The rate is now a hard off switch that overrides force_llm, extracted into _should_use_llm so the decision is testable rather than inline. A genuinely offline run now completes: verified across all three formats with zero LLM calls in the log.

A run with no usable seeds idled until the wall clock and exited 0. _dispatch_tasks stalls in epoch 0 until something is accepted, and nothing detected "all seeds finished, none accepted" — so a user with a bad API key burned the full hour default, printed nothing, and got no output. It now aborts with the underlying cause. Measured: 2 seconds instead of 60, exit 1, message naming the 401.

Console logging died mid-run. setup_logger replaced the stderr handler when given a log file, and runner.py calls it again once the run directory exists — so piped or scripted runs lost every later message, including "no valid candidate found" and "best candidate written". The file handler is now added alongside stderr, with console=False only when the dashboard owns the terminal. Combined with the two bugs above, a piped run could produce no file, print nothing, and exit 0.

An extensionless -o silently discarded the result. project_dir was parent/stem, which for an extensionless path is the output path; initialize() created it as a directory and save_best could never write the file. The failure was invisible because engine.py wrapped the call in contextlib.suppress(Exception). The project dir is now suffixed so collision is impossible, and the save failure is logged at error level — still swallowed so it cannot mask an in-flight shutdown exception, but never silently.

--no-write-lineage did nothing. FileStorageAdapter had no such parameter; the flag only gated an in-memory data URL. It now suppresses the per-node files and lineage.csv while leaving --save-raster / --save-heatmap independent.

Graphviz crossover died on ordinary DOT labels. The donor attribute block was concatenated into an re.sub replacement template, so its backslashes were interpreted and \l, \r, \N produced re.error: bad escape \l. It propagated out into the worker's handler, so every crossover against such a parent was silently logged invalid. Now spliced with slicing. This was the only one of 17 re.sub calls interpolating untrusted text into a replacement.

Typst splicing permanently destroyed mutable sites. _reorder_elements and the crossover split with keepends=True and rejoined, so moving a final line lacking a newline fused two elements onto one physical line. _ELEMENT_LINE_RE is anchored per line, so the fused element became invisible to every later mutation — the genome silently lost sites as the run progressed. Lines are now normalized before splicing.

Unmatched LLM diff blocks were reported as successful edits. str.replace is silent on a miss, so a hallucinated SEARCH block returned the parent unchanged; that byte-identical child entered the pool carrying its parent's signature and dragged measured diversity toward zero, tripping epoch transitions. Now raises NoEditAppliedError, which the worker records as invalid — so the LLM call still counts toward --max-llm-calls (the counters increment before the validity check) but the duplicate never reaches the pool, which is the requested semantics. None still means "no blocks found" and keeps the whole-file fallback working. Partial application is warned about, not rejected, since it is not a duplicate.

Runs started in the same second shared a directory, interleaving appends into one stats.csv and lineage.csv. Now suffixed on collision, which keeps names readable.

inf-scored node files were unparseable in three places. save_node writes f"{score:.6f}", which renders INVALID_SCORE as a bare inf, but both filename grammars required leading digits — so clean_runs.py ignored those files entirely and its (?:inf)? branch was dead. The --resume-top sort separately re-parsed the score out of the stem, raising an unhandled ValueError on any file with a non-numeric prefix; it now uses the value from the match.

Also extracted _variance_fraction from the dashboard so the zero-spread case is testable, and fixed it: zero spread is the most collapsed a pool can be, so the bar read empty at exactly the moment the criterion was satisfied.

Verified beyond the unit tests: all three formats complete a real offline multi-epoch search with --epoch-variance 0.02 set, producing valid output (svg 0.0039, graphviz 0.1459, typst 0.0665).

Not included, as they are not bug fixes: #47 (reader/writer round-trip test), #48 (legacy-run plot labels), #51 (metrics registry), #52 (tournament size flag).

@rasros
rasros merged commit 92e96af into main Jul 28, 2026
1 check passed
@rasros
rasros deleted the fix/confirmed-bugs branch July 28, 2026 15:33
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.

1 participant