Skip to content

tester: harden _entry_from_dict against NaN/+Inf fitness (PKT-980) - #623

Open
bugsyhewitt wants to merge 1 commit into
mainfrom
tester/pkt-980-storage-fitness-finite-guard
Open

bugsyhewitt wants to merge 1 commit into
mainfrom
tester/pkt-980-storage-fitness-finite-guard

Conversation

@bugsyhewitt

Copy link
Copy Markdown
Owner

What

Closes the unfixed LOAD-boundary seam between PKT-618 (callback injection defense at
L54) and PKT-690 ( network-write guard).

Without this guard, a poisoned file (NaN/+Inf/-Inf/null/non-numeric/out-of-range
fitness) silently poisons the entire for that martian_type —
then crashes on with
inf or nan encountered in data (Python 3.14 statistics module raises ValueError on NaN inputs).

Why

Reproduced live on origin/main (cycle 587):

  • json.loads('{"fitness": NaN}') → {'fitness': nan} (Python json extension)
  • float(NaN) → no error, nan propagates into PopulationEntry.fitness
  • statistics.stdev([0.5, nan, 0.9]) → ValueError: inf or nan encountered in data
  • This crashes evaluate_and_evolve() mid-loop, terminating the entire evolution
    pipeline for the affected martian_type.

The fix

src/alienclaw/evolution/storage.py:_entry_from_dict

  • +import math
  • +isfinite-and-in-range guard mirroring population.py:125-126
  • +non-numeric / None rejection (covers the JSON.stringify NaN→null sentinel
    that float() would otherwise raise mid-loop with TypeError — pull.ts L127
    comment explicitly notes this edge)

Tests test/evolution/test_storage.py:TestEntryFromDictFitnessValidation:

  • 19 new tests covering NaN/+Inf/-Inf/null/str/out-of-range rejection,
    canonical-fitness acceptance [0.0, 0.0001, 0.5, 0.9999, 1.0],
    error-message diagnostics (entry_id + actual value present),
    and end-to-end poisoned-JSON-on-disk propagation via read_all_entries().

Ship-gate (observed 2026-10-02T15:02:00Z on branch)

pnpm exec vitest run
  Test Files  187 passed | 2 skipped (189)
       Tests  3410 passed | 46 skipped (3456)
   Duration  20.67s
  → 3410 pass, 0 fail, 46 skip. GREEN.

PYTHONPATH=src pytest --tb=no -q
  1865 passed, 125 skipped, 6 warnings in 413.06s
  → 1865 pass, 0 fail, 125 skip. GREEN.
  Delta vs cycle-586 (1846): +19 PKT-980 tests, zero regressions.

bash scripts/check-testvectors.sh
  check-testvectors: OK — no drift detected
bash scripts/check-seed-drift.sh
  check-seed-drift: OK — 16/16 seeds reproduce byte-for-byte

Total: 5275 tests PASS, 0 FAIL.

Behavior matrix

Input Before PKT-980 After PKT-980
0.0 <= fit <= 1.0 finite OK OK (unchanged)
NaN nan propagates → statistics.stdev CRASH ValueError at load
+Infinity invalid ValueError at load
-Infinity invalid ValueError at load
-0.0001 / 2.0 / -1.0 / 10.0 no-op (passed through unchanged) ValueError at load
null TypeError mid-loop (cryptic) ValueError at load
"0.5" (string) float-coerced silently ValueError at load

Diff size

+23 source / +111 tests, 1 substitution (float() → validated). ~134 LOC total.

Cycle context

Cycle 587 (2026-10-02T14:57:07Z). Origin/main HEAD was 57c2138 (PR #622
PKT-981 merged since cycle 586). Branch tester/pkt-980-storage-fitness-finite-guard,
commit ec59488c, NEVER merged (CI + Bugsy gate the merge per tester-prompt).

PKT-980 closes the unfixed LOAD-boundary seam between PKT-618 (callback
injection defense at generation.py:_make_entry L54) and PKT-690 (sync/pull.ts
network-write guard). Without this guard a poisoned entries/*.json file with
NaN/+Inf/-Inf/null/non-numeric/out-of-range fitness silently poisons the
entire Population for that martian_type — compute_from_entries() then crashes
on statistics.stdev() with 'inf or nan encountered in data' (Python 3.14
statistics module raises ValueError on NaN inputs).

Fix:
  src/alienclaw/evolution/storage.py:_entry_from_dict
    + import math
    + isfinite-and-in-range guard mirroring population.py:125-126
    + non-numeric / None rejection (covers JSON.stringify NaN→null sentinel
      that float() would otherwise raise mid-loop with TypeError)

Tests:
  test/evolution/test_storage.py:TestEntryFromDictFitnessValidation
    + 19 new tests covering NaN/+Inf/-Inf/null/str/out-of-range rejection,
    canonical-fitness acceptance, error-message diagnostics, and end-to-end
    poisoned-JSON-on-disk propagation via read_all_entries().

Ship-gate (observed 2026-10-02T15:02:00Z on branch
tester/pkt-980-storage-fitness-finite-guard):
  vitest: 3410 passed | 46 skipped (3456) — 20.67s
  pytest:  1865 passed | 125 skipped (+19 from PKT-980; was 1846) — 413.06s
  check-testvectors: OK — no drift detected
  check-seed-drift:    OK — 16/16 seeds reproduce byte-for-byte
  Zero regressions. PKT-618 callback guard, PKT-690 network guard, and the
  PKT-935 atomicWrite fence all remain intact (no overlap).
@bugsyhewitt bugsyhewitt added the tester opened by a Tester agent label Oct 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tester opened by a Tester agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant