fix: repair eleven confirmed bugs across search, storage, formats, and logging - #20
Merged
Conversation
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.
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-variancecrashed every run that used it.search/engine.pycompared the importedscore_stdfunction to a float after the local was renamed topool_std, so any positive value raisedTypeErroron the first epoch-end check, roughly three seconds in. Ruff cannot see it —score_stdis a bound name. Nothing in the suite touched the low-variance path, which is now covered.--llm-rate 0still called the LLM. Epoch-0 seed tasks setforce_llm, which bypassed the rate entirely, so there was no offline mode. The rate is now a hard off switch that overridesforce_llm, extracted into_should_use_llmso 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_tasksstalls 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_loggerreplaced the stderr handler when given a log file, andrunner.pycalls 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, withconsole=Falseonly 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
-osilently discarded the result.project_dirwasparent/stem, which for an extensionless path is the output path;initialize()created it as a directory andsave_bestcould never write the file. The failure was invisible becauseengine.pywrapped the call incontextlib.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-lineagedid nothing.FileStorageAdapterhad no such parameter; the flag only gated an in-memory data URL. It now suppresses the per-node files andlineage.csvwhile leaving--save-raster/--save-heatmapindependent.Graphviz crossover died on ordinary DOT labels. The donor attribute block was concatenated into an
re.subreplacement template, so its backslashes were interpreted and\l,\r,\Nproducedre.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 17re.subcalls interpolating untrusted text into a replacement.Typst splicing permanently destroyed mutable sites.
_reorder_elementsand the crossover split withkeepends=Trueand rejoined, so moving a final line lacking a newline fused two elements onto one physical line._ELEMENT_LINE_REis 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.replaceis 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 raisesNoEditAppliedError, 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.Nonestill 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.csvandlineage.csv. Now suffixed on collision, which keeps names readable.inf-scored node files were unparseable in three places.save_nodewritesf"{score:.6f}", which rendersINVALID_SCOREas a bareinf, but both filename grammars required leading digits — soclean_runs.pyignored those files entirely and its(?:inf)?branch was dead. The--resume-topsort separately re-parsed the score out of the stem, raising an unhandledValueErroron any file with a non-numeric prefix; it now uses the value from the match.Also extracted
_variance_fractionfrom 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.02set, 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).