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: object

An ordered, JSON-serializable, integrity-chained log of improvement attempts.

Parameters:

_rows (list[dict[str, Any]])

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 and json.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.

delta and cost are the row’s two measurements and must be finite; cost must also be non-negative, since it is spend, not a refund. See _finite_number(). (A nan p-value inside verdict is a different thing entirely – a documented “no paired test was run” sentinel – and is preserved, exactly, by the serialization codec.)

Parameters:
Return type:

dict[str, Any]

head()[source]

The chain’s current tip: the last row’s row_hash, or _GENESIS_HASH if 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 to verify(expect_head=...) is what turns “these rows are self-consistent” into “these are the rows that were written”.

Return type:

str

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_hash matches the previous row’s row_hash (_GENESIS_HASH for the first), and row_hash still 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_hash still matches, every row_hash still 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_head is 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_length alone 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.

Parameters:
  • expect_length (int | None)

  • expect_head (str | None)

Return type:

bool

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 nan p-value, say) are encoded into the reversible tagged form _FLOAT_TAG documents, and allow_nan is then forced off so anything the encoder missed fails loudly here rather than emitting a bare NaN / Infinity token a conforming JSON parser refuses to read back. Round-trips exactly through from_json(), digests included.

Parameters:

dumps_kwargs (Any)

Return type:

str

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 – which verify() 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_head to verify() 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).