mixle.evolve.ledger module¶
The evolution ledger: an in-memory, JSON-serializable record of every improvement attempt.
The library layer does no I/O, so the ledger is just an in-process list of rows – one per proposed
challenger – that an orchestrator can persist (e.g. into a registry version’s metadata). Each row is a
plain dict so it round-trips through json.dumps without custom encoders: the model objects
themselves are never stored, only their operator, measured delta, verdict, cost, and parent hash.
The ledger is the evidence for a claimed trail of autonomous improvement attempts, so it owns its
rows rather than handing out the live ones. Every row carries a sequence number, a schema version, and
a digest chained to its predecessor (_row_hash()); EvolutionLedger.verify() re-derives the
chain, so a changed delta, a rewritten verdict, a deleted attempt, or a reordering is detectable
rather than silent. EvolutionLedger.record() and the rows view both return deep copies:
mutating what you were handed cannot rewrite what was recorded.
- class EvolutionLedger(_rows=<factory>)[source]
Bases:
objectAn ordered, JSON-serializable, integrity-chained log of improvement attempts.
- property rows: tuple[dict[str, Any], ...]
a tuple of deep copies of the recorded rows.
The backing list used to be the public attribute holding the live row dicts, so a caller could edit a recorded delta or verdict in place, or
clear()the whole trail, with nothing to show it happened. Reading, indexing, iterating andjson.dumps-ing the returned rows all work as before; writing to them simply does not reach the ledger.- Type:
A read-only view
- record(*, operator, delta, verdict, cost, parent_hash, meta=None)[source]
Append one attempt row and return a copy of it.
The returned dict is a deep copy, not the stored object: the previous behaviour handed back the exact row still held by the ledger, so a caller that adjusted a field on “its” result silently rewrote the recorded attempt.
deltaandcostare the row’s two measurements and must be finite;costmust also be non-negative, since it is spend, not a refund. See_finite_number(). (Ananp-value insideverdictis a different thing entirely – a documented “no paired test was run” sentinel – and is preserved, exactly, by the serialization codec.)
- head()[source]
The chain’s current tip: the last row’s
row_hash, or_GENESIS_HASHif empty.This is the anchor
verify()needs to detect truncation. Record it somewhere the ledger ‘s own storage cannot reach – a run report, a build artifact, a separate durable store – each time you finish appending; passing it back toverify(expect_head=...)is what turns “these rows are self-consistent” into “these are the rows that were written”.- Return type:
- verify(*, expect_length=None, expect_head=None)[source]
Whether every row still matches its digest and its place in the chain.
Checks, per row: the sequence number matches its position,
prev_hashmatches the previous row’srow_hash(_GENESIS_HASHfor the first), androw_hashstill matches the row’s canonical content. A rewritten delta or verdict, a deleted or reordered attempt, and a front-truncated ledger all fail.Truncation from the end cannot be detected from the rows alone, and no hash chain can do it. Deleting the last row leaves a shorter chain that is perfectly self-consistent – every remaining
prev_hashstill matches, everyrow_hashstill verifies – because a prefix of a valid chain is a valid chain. Detecting it needs one fact the ledger does not hold: how long the chain is supposed to be, or what its tip was. Supply either and the check becomes real:head = ledger.head() # store this outside the ledger … ledger.verify(expect_head=head, expect_length=n)
expect_headis the stronger of the two: it pins the exact final row, so it also rejects a ledger truncated and then re-extended to the original length.expect_lengthalone catches only a change in count. Omitting both is still meaningful – it proves internal consistency – but it is not proof that nothing was removed from the end.
- to_json(**dumps_kwargs)[source]
Serialize the full ledger to a strict JSON string.
Non-finite floats anywhere in a row (a scalar-only verdict’s
nanp-value, say) are encoded into the reversible tagged form_FLOAT_TAGdocuments, andallow_nanis then forced off so anything the encoder missed fails loudly here rather than emitting a bareNaN/Infinitytoken a conforming JSON parser refuses to read back. Round-trips exactly throughfrom_json(), digests included.
- classmethod from_json(s)[source]
Rebuild a ledger from
to_json()output, checking the anchor it carries.A document written by
to_json()is an envelope holding the rows plus the length and chain tip they had when written; both are verified here, so a row deleted from the end – whichverify()alone cannot see – is caught on load.A bare JSON array is still accepted for documents written before the envelope existed. Those carry no anchor, so loading one proves only that the rows are internally consistent; pass a separately stored
expect_headtoverify()for the stronger check.- Parameters:
s (str)
- Return type:
EvolutionLedger
- LEDGER_SCHEMA_VERSION = 1
Version of the row schema, stamped on every row so a later reader can detect an older layout.
- FLOATENVELOPE_VERSION = 1
Version of the non-finite-float encoding
EvolutionLedger.to_json()writes (see_FLOAT_TAG).