Skip to content

Strip BrainlessLab back to a library for 0.3.0 - #8

Draft
btgaskin wants to merge 67 commits into
mainfrom
agent/minimal-docs-benchmarks
Draft

Strip BrainlessLab back to a library for 0.3.0#8
btgaskin wants to merge 67 commits into
mainfrom
agent/minimal-docs-benchmarks

Conversation

@btgaskin

@btgaskin btgaskin commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Strips BrainlessLab back to a library for the 0.3.0 release. It ships the canonical node, the core task protocols, and the machinery to extend both. It ships no experimental results and no benchmark record.

This is breaking. Scores computed with 0.2.0 do not carry over.

Why

A review of the repository at HEAD found that the two scoring paths disagreed, that a completed evolution run produced 3.0 GB of checkpoints beside 3.6 MB of results, that the one committed benchmark record could no longer be reproduced by the library shipping beside it, and that Pkg.test() reported success after running a fifth of the tests.

What changed

Scoring windows

simulate and the composition path used min(ticks, default_window), silently discarding the rest of a longer run — a 2,000-tick tracking simulation scored its final 200 ticks, where the between-seed spread is four times the mean. The typed evaluation path already scored the full post-warmup interval, so the two disagreed.

Both now score the whole run. TaskSpec gains minimum_scored_ticks — Pong 6,000 (it emits roughly one ball event per 305 ticks), Tracking 2,000, Wall 200 — and an evaluation below the minimum is rejected rather than scored. An explicit window acknowledges a deliberately short diagnostic run. A task whose default_ticks falls below its own minimum is rejected at registration. The effective window is recorded on every outcome and in every record.

Anchors

No anchor value changed. Provenance now records scored_ticks, because calibration rate-matches the reference firing rate over the scoring window: an anchor measured over a different window is a different anchor. Over 32 seeds, Wall's floor moves 0.81609 → 0.77244 between window 200 and window 1,000. Every shipped anchor was already measured at exactly the interval the v2 protocol scores.

Evolution records

Each generation wrote the entire candidate archive to date, so total checkpoint disk was quadratic in generations: 3.0 GB for a 60-generation run. Checkpoints now carry strategy state only and retain the newest two, reconstructing the archive from the record's own CSV tables. Per-checkpoint size is constant in generation count — measured at 23,047 and 23,053 bytes for generations 9 and 10 of a 10-generation run.

This also closed a records defect: for a custom measure, the per-trial values behind every aggregate previously existed only inside checkpoints. Retention alone would have deleted them. candidates.csv is now written incrementally and per-trial values persist as measure_value.

Surface

  • :falandays is the only name for the canonical node; the :falandays_base duplicate registration is removed.
  • Nine Falandays variants removed. Registered nodes are :falandays, :null_random, :sorn, :compartmental_dense, :compartmental_structured, :homeostatic_flow_v2. The Dale, UnsignedAxis and OosawaDrive mechanisms stay on the base reservoir — they are what the sealed parity fixtures exercise, and all three parity cases still pass at near_fraction = 0.0.
  • bench removed. It was the only evaluation path producing no record, and its default protocol scored Pong over a single ball event: raw values across eight seeds were [0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0]. Cross-node comparison is a BenchmarkPlan; parameter exploration is a SweepPlan.
  • Benchmark v1 retired: its normalised column no longer reproduced after the anchors were recalibrated (pong 0.2396 recorded vs 0.2956 current), and it predated the Pong window fix. v2 is the single core protocol, shipped as a declared protocol with no record.
  • Pre-pipeline contribution support generalised from one hardcoded record to validated generic support, with path-containment and record-bundle checks.
  • Manifest.toml untracked, and each record now captures the dependency graph that produced it as environment/Manifest.toml, sealed in the record inventory and checksums. A downstream resolver ignores a dependency's manifest anyway, reading only Project.toml and [compat]; all eight deps and every test extra carry bounds. A single tracked manifest drifts with the branch, whereas the per-record copy pins the resolution that actually ran.
  • Top-level layout: 28 tracked entries → 25. archive/ removed; calibration/, configs/ and sweep/ folded into tools/.
  • The evolution experiment plan is retired. Its run completed and the frozen vector did not survive held-out evaluation — Pong +0.036 (1.3 SEM), tracking −0.115, wall 0.984 → 0.000 on 40 of 40 seeds — so no comparison arm ships.

Tests

Pkg.test() now runs every suite. It defaulted to core: 14 files of 67 assigned, after which it printed "tests passed". The tiers still parallelise CI and give a fast local loop via BRAINLESSLAB_TEST_SUITE=core, but they no longer define what "tested" means.

Verification

Full suite on the final commit: 8232 passed, 0 broken, 0 failed, 0 errored. The suite has no skipped assertions.

The passing count moved deliberately: −27 removing duplicate alias assertions, +5 generalising pre-pipeline contributions, −454 removing the Falandays variants with their tests (431 from five deleted variant-only files, 23 variant-only assertions in shared files). Tests that merely used a variant as a fixture were retargeted rather than deleted.

Site builds clean at 37 pages with no route to the removed benchmark.

Wall cross-implementation discrepancy — resolved

Two @test_skips had recorded an unexplained mismatch against single_agent_wall.npz. It was never a bug: 6552af5 deliberately re-based WallBox off the v0.2 Python crho implementation onto the Falandays authors' conventions — sensor rays from the sensor point rather than the centre, translation along the old heading before rotating, clamp-and-slide collisions, and a post-collision turn from the new heading. The fixture predates that commit. Switching the first two back reproduces it bit-exactly across all 120 ticks.

The recorded "deviation of exactly 1.0" was a red herring: measured from the default centre pose rather than the fixture's env_draws, and with effectors drawn from {0, 0.25} and no collisions, distance is always a multiple of 0.125 — eight quanta is coincidence, not a fencepost.

The skips are deleted rather than enabled, since they can never pass against a crho-derived trajectory, and replaced by what the fixture does prove independent of wall geometry: driving the pinned reservoir with its own recorded sensor currents reproduces its spikes and effectors to 1e-9. The four conventions are now recorded in WallBox.jl; their existing only in a commit message is why this read as a mystery.

Review round

An external review found eight defects, all verified and fixed. Six were introduced by this branch.

Plans failed at execution. minimum_scored_ticks was enforced in evaluate() rather than in validation, so all five shipped plans passed check and died at runplans/examples/* scored 100 ticks against a 2,000-tick tracking minimum, and the structured-CTRNN smoke scored 2. SKILL.md:99 calls these "executable syntax checks". The constraint moved into plan validation across every operation, and the plans were resized (keeping budgets small by cutting replication rather than by weakening the guard). All five verified executing to DONE.

Unscored tasks were gated by an objective minimum. :torus declares score_key = nothing but inherited a 1,000-tick minimum, so profiling it short failed — contradicting SKILL.md:189. The minimum is now skipped when a task declares no scalar outcome.

Records could not be replayed. A record stored only the SHA-256 of a locally generated manifest, which cannot reconstruct a dependency graph. Records now embed environment/Manifest.toml as a sealed artifact.

Proof caches could mark untested work as proven. Suite digests contained no dependency-graph information after the manifest was untracked, so a dependency moving within compat left the key unchanged; and the digest still named calibration/configs after they moved to tools/, silently contributing nothing. Both fixed, and a declared-but-missing digest root now throws rather than quietly weakening the key.

Three correctness gaps in evolution records. A partial models/ directory permanently blocked resume (model artifacts now stage outside the record and rename atomically); held-out rows omitted measure_value, so a custom measure's aggregate could not be audited; and minimisation runs recorded reversed convergence extrema.

Known-unresolved

Nothing outstanding.

btgaskin and others added 24 commits July 26, 2026 00:38
The include loop had no enclosing testset, so the first file that threw
aborted the whole tier and every later file silently never ran. A real
oracle run stopped at file 5 of 15 with nothing reporting the other ten
as skipped.

This is the keystone of the batch: several long-standing failures were
invisible purely because of it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The calibration harness was broken three independent ways, all hidden
behind the runtests fail-fast:

- `_metric_value` was called in Calibration.jl but defined nowhere, so
  `calibrate_task` raised UndefVarError for every task.
- `configs/core_task_calibration.toml` was deleted in abc395d while
  calibration/core_tasks.jl still defaulted to it. Restored: it carries
  the tracking/pong roster and the blind/random/reference opportunity
  conditions.
- `_sim_score` was removed in the same commit along with the legacy
  sweep layer, but test/scoring_calibration.jl still called it. It was a
  wrapper turning `task_outcome`'s nothing into a NaN CSV row; the test
  now asserts the public contract directly.

Also resolves the provenance SHA against the package repository rather
than the process working directory, so a record's git_sha no longer
depends on where the process was launched (carried from PR #2).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
metrics(::PongEnv, window) accepted a window and then immediately did
`bounds = eachindex(env.hit_flags)`, scoring every tick ever recorded.
default_ticks is 2000 and default_window is 1000, and rollout! passes
the window explicitly, so the documented "scores the final 1,000 ticks
of a 2,000-tick run" has never been true. mean_align had the same
defect. TrackingEnv two hundred lines away does it correctly via
_tail_bounds; this now follows that pattern.

CHANGES NUMBERS. Every Pong score this repository has produced is a
whole-run average, including the stored null anchor and the published
benchmark record. Anchors are deliberately NOT updated here --
recalibration must follow this commit, not accompany it.

Consequence of the old behaviour: a node that missed everything for
1000 ticks and then played perfectly scored identically to one that was
mediocre throughout, which made any adaptation claim measured this way
unfalsifiable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…registration

Grouped because the Unsigned rename touches the same node files as the
correctness fixes; splitting further would need hunk-level staging.

Node correctness:
- pack/unpack silently discarded learn_on (7 packed values, 8 struct
  fields, learn_on hardcoded true on unpack), so a FreezePlasticity
  ablation re-enabled itself through any genome round-trip while the
  record still claimed it was applied. Type-based unpack now requires
  the keyword explicitly; instance-based preserves it.
- plasticity() returned OnlinePlasticity unconditionally for the
  Falandays family and SORN, so a frozen node still claimed to be
  adaptive and bench treated it as a fair untrained baseline.
  HomeostaticFlow already did this correctly; both now match.
- targets were initialised to a hardcoded 1.0 rather than
  params.targ_min, starting every node below its own homeostatic floor
  whenever targ_min != 1.0. Default is 1.0, so default runs are
  byte-identical (verified by trace SHA).
- the Dale off-mask sweep in Delays.jl sat inside the destination loop
  and shadowed its index, making heterogeneous-delay Dale learning
  O(N^3). Axes.jl had the same sweep correctly placed. Adds the Dale
  path test that was entirely absent.

API:
- exported Unsigned collided with Core.Unsigned, so any downstream
  `using BrainlessLab` hit UndefVarError at the point of use. Renamed
  to UnsignedAxis. (Base.wrap is not exported, so `wrap` does not
  collide and is left alone.)
- the legacy registry silently overwrote on duplicate registration, so
  register_node!(:falandays, ...) could replace the reference node for
  a whole session. It now rejects, matching the typed registry's
  documented policy.
- seven registered analyses lacked the (experimental) label their
  neighbours carry, including branching_ratio_mr and avalanches, whose
  estimators have confirmed defects. The broken ones read as the more
  supported ones.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
compare_contribution computed data_equal by byte-comparing every data/
and summary/ artifact between submission and maintainer replay, and
nothing ever read it. validate_contribution only checked that
comparison.json was not stale.

Meanwhile request_equal, resolved_equal and seeds_equal all throw
earlier on mismatch, so they are hardcoded literal true in the
comparison document -- and those were the two fields the CLI printed.
Both were structurally incapable of printing false.

Net effect: a replay producing different numbers validated as accepted
and entered research/catalogue.json with reproduction_state "accepted"
and independent_evidence true, while research/README.md stated "the
replay checks reproducibility".

Validation now rejects any operation whose replay data or summaries
differ, and the CLI reports data equality. Adds a regression test that
mutates replay data with inventories and checksums kept current, and
confirms validation rejects it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…cord roots

The proof cache skipped install, precompile and the test body on a key
hit, and the oracle suite's external inputs omitted configs/, which
test_core_calibration.jl transitively depends on. A commit deleting only
configs/ therefore left the digest unchanged, hit the cache, and
reported success without running -- exactly the state this branch was
in. Adds configs, bin and test/oracle to the suites that depend on them.

The legacy tier was gated on push/schedule/dispatch, so a PR breaking
dyad or replay compatibility went green until after merge.

.gitignore covered runs/ and sweeps/ but not records/ or
experiment-records/, the roots the README tells users to write to.
Since _record_git uses git status --porcelain (which reports untracked
files) and _validate_record_bundle rejects anything not clean,
following the README twice produced a permanently uncontributable
record. Also ignores .wrangler/, .claude/ and .codex/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The legacy genome-evaluation rollout helper was removed with the old driver layer, leaving the extension smoke test unable to run. Unpack the registered genome type, pass the typed parameters through simulate, and evaluate the declared task outcome instead.

Record the extension model's gain so the test also proves that the user constructor received the decoded genome.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Map every Falandays genome coordinate through a documented bounded sigmoid bijection so ordinary optimiser values cannot become unbounded physical learning rates or scales. Preserve learn_on outside the coordinate vector, add numerical round-trip coverage, and reject non-finite dynamic state and weight updates during step!.

BREAKING: existing Falandays coordinate vectors now decode to different physical parameters. Bump model-artifact and checkpoint formats to version 2 so version-1 evolution state is rejected instead of silently reinterpreted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… API

ParameterSpec no longer owns evolution metadata, and the copy-ready node could not load with its removed evolve keyword. Move physical bounds into bounded pack/unpack bijections, validate configuration values separately, and let the typed builder consume NodeBuildContext.model.

Remove the obsolete evolve parameter set and unsupported EvolutionPlan guidance, update the sample plan to format version 2, and cover model round-tripping plus an end-to-end typed simulation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The bounded bijections introduced in 01e4ed0 fed _falandays_invrange
straight into _inverse_sigmoid, which clamps its argument to
[1e-12, 1-1e-12]. A parameter above its declared ceiling therefore
packed to a large finite coordinate and decoded back as the ceiling
itself: FalandaysParams(input_weight=20.0) round-tripped to
15.999999999984 with no error.

That is silent corruption in exactly the machinery added to prevent
silent corruption. Packing now throws and names the offending
parameter. Values exactly on a bound remain packable, since the
inverse-sigmoid clamp handles the endpoints.

In-range behaviour is unchanged: the default parameter set and the
collective input weight of 12.5 both still round-trip to within
floating-point tolerance.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ut_amp override

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nput_amp

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The file implementing the canonical node carried no reference to the work
it implements, and the README's citation block had been dropped in an
earlier revision. Several statements also claimed more than was true.

- src/nodes/Falandays.jl: adds the full citation and states plainly that
  this is an independent reimplementation, not vendored upstream code.
- src/core/Catalog.jl: "Falandays et al. authors-derived Julia
  implementation" was ambiguous enough to read as adapted-from-their-code.
  Now "independent Julia reimplementation of ..." with the DOI.
- README.md: restores a citation section asking users to cite the original
  work as well as BrainlessLab.
- CITATION.cff: adds the authors' public source repository as a software
  reference, recording that it is read and cited but not redistributed,
  and that it carries no licence file.
- test/FIXTURES.md: new. Records what produced every sealed fixture and
  what each group does and does not establish.

The last of those is the substantive one. The authors_*.jld2 files are
named for what they model, not where they came from: they are generated
by test/oracle/authors_falandays.jl, a transcription in this repository,
and the upstream code was never executed to produce them. So
test_authors_parity.jl is a refactor gate, not evidence of fidelity to
the publication -- both sides were written by the same hand from the same
reading, and a shared misreading passes at 1e-9. The genuinely
independent cross-implementation evidence is the .npz group, which comes
from a separate Python codebase.

The documentation lives in test/FIXTURES.md rather than inside
test/fixtures/ because the manifest seals every file in that directory,
so an in-place README is an unlisted file and correctly fails
test_fixture_integrity.jl.

Upstream is public at github.com/bfalandays/ReservoirModel_followups and
carries no licence file, so it is cited and read, never vendored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d target error to core

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
.wrangler/ was added to .gitignore in 2faa540, but ignoring a path does
not untrack files already in the index. The cache remained committed,
exposing a Cloudflare account id and account name on a public repository.

The files stay on disk; only the index entries are removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rimental names

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e core API

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tions

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
btgaskin and others added 16 commits July 27, 2026 03:19
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four hot-path defects, none of which touch core numerics.

- Recorder stored channels as Dict{Symbol,Vector{Any}}, so every one of the
  ~30 call sites in src/analysis/ re-inferred the element type per element.
  Channels are now created as Vector{T} on first record!, which removes the
  per-element inference. getchannel still declares Vector, since the Dict
  holds heterogeneous concrete vectors, but the cost was never the accessor.

- parallel_map spawned one task per item with no cap, so peak memory scaled
  with item count rather than thread count -- and nested use in the bench
  pipeline made concurrency n_cells x n_trials. Now bounded. Its byte-identity
  guarantee remains verified under -t 4.

- begin_encoding! recomputed sensor_width every agent every tick, walking
  _encoder_portspec -> portspec -> _base_receptor_ports, which allocated a
  Vector{Port} and interned Symbol("bearing_", i) by string interpolation for
  every ray -- a compile-time constant rebuilt per agent per tick, and 54% of
  swarm step! wall time. Now cached on EmbodimentState. Measured 29,856 ->
  9,060 B/agent/tick, a 3.3x reduction. portspec(::SpectralCamera) had the
  same defect and is cached too.

- LogisticResponse recomputed its normalisation constants on every scalar
  call, though lo/hi depend only on immutable slope/midpoint, and it is
  applied per receptor per tick.

Implemented by Codex; the run was interrupted after completing the work but
before committing, so this was reviewed and committed separately.
Verified: core 605, oracle 4622 (+2 documented skips).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
827d0bc removed the :node_state, :body, :task and :mechanism seed streams
because they were derived, written into seeds.csv and every run record, and
read by nothing. It removed them from the defaults but not from the plan
surface: EvaluationSpec accepted arbitrary stream names, so a plan could
declare a removed stream -- or invent one through a typo -- and it would be
silently created, realised, and recorded.

Verified before the fix: EvaluationSpec(; streams=(:topology, :world,
:node_state, :bogus)) returned all four. The checked-in structured-CTRNN
example plan still declared all six original streams and validated cleanly,
so the removal was reversible by accident.

That is the same defect class as a registered ablation reporting itself
applied while doing nothing, or a typo'd task_options key being swallowed:
configuration that looks effective and is not. Unknown streams now throw and
name the available set. The example plan is updated to the two real streams.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five stored NULL_MEASURED floors were derived from the old null reservoir,
which emitted zero spikes and U(0,1) effectors averaging 0.5 against the
canonical node's task-specific rate. They were also 8-seed point estimates
quoted to 17 significant figures. Re-measured at 32 seeds against the
per-task rate-matched null, with the standard error now recorded in the
provenance instead of implied precision:

  wall              0.775625   -> 0.816094  (sem 0.0120, +3.4 sem)
  pong              0.353175   -> 0.270447  (sem 0.0137, -6.0 sem)
  pong_hitrate      0.353175   -> 0.270447  (sem 0.0137)
  cartpole_swingup  0.050260   -> 0.023001  (sem 0.0018, -15 sem)
  forage            0.458266   -> 0.453099  (sem 0.0037, -1.4 sem)

Pong moved most and in the direction that matters: its floor was too high,
so every published Pong normalized score was scaled against too harsh a
baseline.

Tracking keeps its analytic 0.0. A 32-seed rate-matched null measures
0.0599 +/- 0.0691 (sd 0.3907), which is consistent with zero, so the
analytic anchor is vindicated by measurement rather than replaced by a
noisier estimate. The note records this. That large spread is itself
informative: the null's tracking score varies enormously seed to seed,
which is why a 4-seed pilot of the same measurement read 0.2623.

Also narrows the seed-stream guard added in e944fab. That version
allowlisted only the two consumed streams, which broke the legitimate
extensibility case -- derive_seed is name-derived and stable, so an
extension consuming its own stream may declare one, and test_contract_kernel
exercises exactly that with :environment, :node_construction and :bootstrap.
The guard now rejects only the four streams retired in 827d0bc, which is the
actual regression risk: the checked-in example plan still declared all six
until it was caught.

Adds plans/falandays_evolved_pong.toml, the search that produces the
:falandays_evolved comparison arm. Not yet runnable -- the evolution plan
validator currently admits only compartmental nodes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
C1 added minimum_scored_ticks but left tracking declaring default_ticks=1000
against a minimum of 2000, so simulate(:tracking) with no arguments threw. It
was the only inconsistent task, and it surfaced only in the oracle tier, which
C1's verification boundary excluded.

Raise TrackingEnv default_ticks to 2000 and reject the inconsistency at
registration: a task whose own default run cannot satisfy its own minimum is
the silent-inconsistency class minimum_scored_ticks exists to remove. The old
default ran 1,000 ticks and scored the last 200, where the between-seed spread
is four times the mean, so the out-of-the-box call returned near-noise.

Record scored_ticks in anchor provenance, generated and stored. calibrate_task
rate-matches: it measures the reference firing rate over the scoring window,
so an anchor measured over a different window is a different anchor. Measured
over 32 seeds, wall's floor moves 0.81609 -> 0.77244 (3.6 SEM) between window
200 and 1000; pong's moves 0.27045 -> 0.26530 (0.38 SEM). No shipped anchor
value changes: each was already measured at exactly the interval the v2 core
protocol scores (pong 6000, wall 200, tracking analytic). Only the no-argument
calibrate_task call had drifted, so pin the calibration test to those
intervals rather than recalibrating.

Replace the tracking floor's 0.05 < x < 0.35 assertion with consistency with
zero. Tracking's shipped floor is analytic 0.0 by symmetry, so a band
asserting a positive value contradicted the anchor it was checking; it encoded
the sampling noise of the 200-tick window. Five disjoint 8-seed blocks at the
new default measure 0.0022 (sd 0.0167, range [-0.0224, 0.0225]).

Full suite: 8639 passed, 2 broken, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
falandays_evolved_pong.toml searched the seven Falandays genome coordinates on
Pong to ship a :falandays_evolved comparison arm. The run completed and the
frozen vector did not survive held-out evaluation: Pong gained 0.036 (1.3 SEM,
34/60 seeds), tracking lost 0.115, and wall collapsed from 0.984 to exactly
0.000 on 40 of 40 seeds, with input_weight pinned at its 16.0 ceiling. The
search found a degenerate high-drive regime, not a better neuron.

The release ships the canonical node alone, so the plan has no consumer. No
:falandays_evolved preset was ever registered, so nothing else unwinds.

Operations suite: 529 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The default was `core`: 14 of 72 files, about a fifth of the assertions, after
which the runner printed "tests passed". That is what a contributor runs, what
a reviewer runs, and what anyone evaluating the package runs, so the default
was making a coverage claim it could not back. PR #8's body still cites a
passing default Pkg.test() as evidence for exactly this reason.

It is not hypothetical. The window-semantics commit earlier on this branch was
verified against core, runtime and operations, all green, while the oracle tier
holding the anchors and calibration went unrun; it was hiding a defect that made
simulate(:tracking) throw plus two stale expectations.

Default to `all` and opt into a tier with BRAINLESSLAB_TEST_SUITE=core for the
fast local loop. Every CI job already sets the variable explicitly, so CI is
unchanged. The tiers keep their purpose -- parallelising CI, fast local
iteration -- they just stop defining what "tested" means.

Legacy suite: 23 passed. Default selector resolves to all, 72 files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Remove the duplicate node registration and route paper defaults through the single canonical :falandays name. Rename the sealed oracle fixture without changing its contents and update existing tests to use the canonical registration.
bench/ was a separate sub-project that swept every registered neuron across a
task grid. It bypassed the plan machinery entirely -- _run_cell called simulate
directly and emitted ad hoc CSVs with no record, no provenance and no
checksums. It was the only evaluation path in the repository that produced no
record, and it was the one that was silently mis-scoring; those are the same
fact.

Its default config ran ticks = 300 across wall, tracking, pong, cartpole and
cartpole_swingup, three of which declare longer scoring windows. Pong emits one
ball event per ~305 ticks, so a pong cell scored a single event: raw values over
eight seeds were [0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0], a coin flip. After
the minimum_scored_ticks guard the config would refuse to run at all.

Cross-node comparison remains expressible as a BenchmarkPlan with one target per
node, which plans/examples/benchmark_core.toml already demonstrates. Parameter
exploration on a new task is SweepPlan, a first-class typed operation with
sweepable_axes, a max_rollouts guard and full records; sweep/run.jl is a thin
CLI over it. Both produce records; bench did not.

Ignore /bench/ wholesale rather than its three former output subdirectories.
Removing those entries alone un-ignored the leftover local artefacts, and 26
evolved genome files were briefly swept into the index; they are untracked
again and left on disk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Delete archive/2026-07-legacy-research/. It held 116 tracked files with no
inbound reference from src/, test/, docs/, README.md or site/src. Git history
retains it.

Fold three top-level directories holding four files -- calibration/, configs/
and sweep/ -- into tools/. This also resolves the sweep/ versus sweeps/
collision, where one was tracked and the other gitignored.

Two path references were assembled with joinpath from separate string
components, so a search for "calibration/" could not find them:
test_core_calibration.jl included the harness by path, and the harness itself
read its config the same way. Both now resolve under tools/. Searching for the
quoted directory name rather than the path fragment is what surfaced them.

Also updated: experiments/README.md and the skills CLI reference, which both
pointed readers at the archived experiment runner, and ci.yml's proof-cache
path filter, which listed calibration/**.

Tracked top-level entries: 28 -> 25.

Full suite: 8619 passed, 2 broken, 0 failed, matching the pre-change baseline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Anchor recalibration changed the normalised scores, so the committed v1 outputs no longer re-derive under the library that ships beside them. Keep generic pre-pipeline catalogue support while shipping no historical result.
Bump the package and citation versions and write the release notes.

Fix two documented examples that the minimum_scored_ticks guard rejects. The
README quickstart and the first-simulation tutorial both ran tracking for 300
ticks against a 2,000-tick minimum, so the first command a new user copies out
of this repository errored. Both now run 2,000 ticks. Swept every documented
ticks= value across README, docs, site, examples and skills; those two were the
only genuine breaks, since wall's minimum is 200 and the object-world examples
declare their own short defaults.

The tutorial now also explains why the minimum exists and mentions the recorded
scoring window, because that page is where a reader first meets the concept.

Full suite: 8170 passed, 2 broken, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@btgaskin btgaskin changed the title Unify documentation, CI proof tiers, and fixed-design evolution Strip BrainlessLab back to a library for 0.3.0 Aug 1, 2026
btgaskin and others added 13 commits August 1, 2026 16:29
A dependency's manifest is ignored by a downstream resolver, which reads only
Project.toml and [compat]. A committed manifest therefore never reaches a user;
it only pinned this repository's own CI to one resolution out of the range the
package claims to support. Julia's guidance splits on exactly this: libraries
do not commit a manifest, applications and analyses do, and this is a library.

The preconditions hold. All eight dependencies and every test extra carry
compat bounds, so the bounds are a complete contract on their own. CI already
treated the manifest as non-authoritative: manifest-free-core deletes it and
re-resolves on every run, and suite-input-digest.jl already had an
include_manifest=false path. Its input_files skips absent paths, so the
proof-cache digest simply stops hashing a file the repository no longer
controls.

Reproducibility was never carried by the manifest. Each record stores its Julia
version, git revision, artifact inventory and SHA-256 checksums -- per-result
provenance. One tracked manifest drifts with the branch, so the one at HEAD is
not the one that produced an older record. That is the same false assurance
that made the version-one benchmark record unreproducible, at environment level
instead of anchor level.

The root manifest was the lone exception; test/, demo/ and the project template
were already ignored.

Core suite: 595 passed. Digest script verified against the absent file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Not a bug on either side. Commit 6552af5 ("make :falandays_base +
wall/tracking/pong worlds authors-faithful") deliberately re-based WallBox off
the v0.2 Python crho implementation and onto the Falandays authors'
conventions. Four differ: sensor rays cast from the sensor point rather than
the agent's centre, translation along the old heading before rotating,
clamp-and-slide collision response crediting partial translation, and a
post-collision turn taken from the new heading. The fixture predates that
commit and was never regenerated, so its metric_*, sensors and pose keys
describe the superseded conventions.

Reimplementing WallBox with the first two switched back reproduces the fixture
bit-exactly across all 120 ticks on sensors, spikes, effectors, pose and
xy_path. First divergence is tick 1 in sensors, uniformly high by
0.023570226039551584, which is exactly r/dist_max = 0.5/sqrt(2*15^2) -- the
radius offset. The other two conventions are unexercised because both sides
record zero collisions.

Two claims in the retired note were wrong. The "deviation of exactly
1.0000000000000018" was measured from the default centre pose, not from the
fixture's env_draws as the note asserted; and effectors here are drawn from
{0, 0.25}, so with no collisions the distance is always a multiple of 0.125 and
landing on eight quanta is arithmetic coincidence, not a fencepost. The claim
that the fixture could not be regenerated was also wrong: the crho workspace
sits beside this repository and reproduces it at 0.0e0 deviation.

Delete both @test_skip assertions rather than enabling them -- they can never
pass against a crho-derived trajectory. Replace them with what the fixture does
prove, independent of wall geometry: driving the pinned reservoir with its own
recorded sensor currents reproduces its spikes and effectors to 1e-9. A dead
fixture becomes live cross-implementation evidence.

Record the four conventions at _ray_distance and step! in WallBox.jl. Their
existing only in a commit message is why this read as a numerical mystery
rather than an intended divergence.

Full suite: 8172 passed, 0 broken, 0 failed. The suite now has no skipped
assertions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three jobs -- manifest-free core, visual, and CLI/template smoke -- compute a
proof-cache digest by resolving an environment BEFORE running Pkg.instantiate().
That resolve is the first Pkg operation in the job, so it needs a registry, and
all three assumed one was already present in the depot.

They failed with "expected package JLD2 [033835bb] to be registered". A resolve
against a complete manifest is effectively a no-op that never consults the
registry, so the assumption held while a manifest was cached; julia-actions/cache
keys the depot partly on Manifest.toml, so it cold-starts when that changes.

Guard each resolve with an idempotent registry install. This is correct
independent of what triggered it: a step that resolves from scratch must not
depend on a warm depot.

Note that manifest-free core deleted the checked-in manifest and resolved from
scratch even before it was untracked, so it always carried this exposure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test/visual and examples/templates/new_project each declare a compat bound on
BrainlessLab, and both still said "0.2". After the root package became 0.3.0
their environments could not resolve:

  ERROR: empty intersection between BrainlessLab@0.3.0 and project
  compatibility 0.2

This was a miss in the release commit, which bumped Project.toml and
CITATION.cff but not the two downstream declarations. Both verified resolving
against the developed 0.3.0 package.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BrainlessLabMakieExt failed to precompile outright:

  ERROR: LoadError: ArgumentError: view registry key :raster is already
  registered

which took down both the Visual and CLI/template smoke jobs.

BrainlessLab registers all ten view keys at load against the generic functions
declared in src/viz/Views.jl (`function rasterplot end` and friends). This
extension supplies their methods, so the already-registered entry gains them
automatically -- there is nothing to re-register. The extension bound the
identical function objects a second time, and _register! throws on a duplicate
key rather than replacing.

The collision has existed since 4b50354 introduced both sides. It surfaced only
now because these two jobs were restructured on this branch and had been
failing earlier for unrelated reasons, which masked it; main's equivalent job
is configured differently and passes.

Verified: the extension loads, resolve_view(:raster) returns the generic
function, and it carries the extension's method. Visual suite passes locally,
5 of 5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The template declared name, uuid and version in its Project.toml while having
no src/ directory, so Julia treated it as a package and Pkg.precompile() failed:

  Error: Missing source file for Base.PkgId(UUID("cc9b7197-..."),
  "BrainlessLabNewProject")

It is a script environment. Its README tells you to run
`julia --project=. run.jl`, and run.jl includes my_node.jl, my_task.jl and
my_metric.jl directly. Nothing anywhere references the package name.

Like the view-registry collision, this was latent: the smoke job was
restructured on this branch and now runs Pkg.precompile(), which the previous
configuration did not.

Verified the template both precompiles and runs end to end
(`run.jl --ticks 300 --seed 1 --n-nodes 40`), producing metrics and a figure.
Its own task declares default_ticks 300 against default_window 100, so it
satisfies the new minimum_scored_ticks invariant unchanged -- worth confirming,
since this template is the canonical example of adding a task.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The smoke step ran the template with --ticks 20 while its MyTrackingEnv
declares default_window 100, so minimum_scored_ticks is 100 and the run was
rejected:

  ERROR: task :my_task scored interval is 20 ticks, but minimum_scored_ticks
  is 100. Increase ticks, or pass an explicit window to acknowledge a shorter
  diagnostic run.

The guard is behaving correctly; 20 ticks could not have produced a meaningful
score. A smoke test should exercise the template the way a user would, so run
it at 100. Still fast at 12 nodes.

I swept documented examples for calls the guard now rejects when the release
landed, but only documented ones -- this is a CI argument, so it was missed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The review-round fix moved the minimum_scored_ticks constraint from execution
into plan validation for Profile, Sweep, Ablation and Benchmark. Evolution was
missed because validate(::EvolutionPlan) lives in src/operations/Evolution.jl
rather than beside the other operation validators, and that file was owned by a
different parallel worktree during the fix.

So evolution plans alone kept the original defect: a plan whose scored interval
falls below a task's minimum validated cleanly and failed minutes into a run.
Verified before the fix that a 120-tick tracking evolution plan passed `check`;
it now errors there, naming the task, the interval and the minimum.

Add the regression the other four operations already had. Its absence is what
let this seam survive the round that was meant to close it.

Operations suite: 594 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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