Commit graph

2 commits

Author SHA1 Message Date
2c1317bdb5 fix(ledger): normalize every load rejection to ValueError at the ledger's own entrance
SavingsLedger.load unpacked the payload with `**`, so a valid-JSON but
non-object book ([], "x", 3, null) escaped as a raw TypeError — a failure mode
no caller catching ValueError would see. The run path was already covered:
valuereport.load_ledger caught the TypeError and re-raised it as ValueError,
and `run.py --goals` goes through that function. The leak reached only callers
outside that one path, which is why the suite stayed green.

The fix moves the normalization DOWN into ledger.py, where the public boundary
is, and deletes the now-dead patch in valuereport.load_ledger. One except
clause now covers the whole boundary: unparsable bytes (JSONDecodeError),
non-object top level (explicit check), wrong-shaped object (ValidationError).

Load-bearing (§11): the new TestLoadHasOneFailureType went RED before the fix
with exactly the TypeError it exists to forbid — pytest.raises(ValueError) does
not swallow it. Detach point named in the class docstring: drop the isinstance
check and the array/string cases raise TypeError again.

Found by cross-checking MAF's 7dab2df; queued in STATE as post 2b, approved by
the operator this session. 604 -> 612 passed, ruff + mypy --strict clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQu2xxwedckjU56byu1aUG
2026-07-25 15:29:27 +02:00
698e8f21dd feat(ledger): K1 — savings ledger + goal contract (parity rows 2-3)
New ledger.py: typed SavingsLedger; realize is fail-closed on an APPROVED
FeedbackContract + a named expert + an explicit timestamp (the §6 determinism
rule — no wall-clock default). The sum key is DIMENSION-FREE (the dimension
label is annotation only and never participates in the mint), so the same
realized saving surfaced via two dimensions lands in one first-write-wins
slot and is never double-counted. Deterministic JSON persistence
(sort_keys, indent 2, LF, trailing newline), schema-validated on load.

New goals.py: GoalContract (absolute target, hard/soft, fail-fast §10).
A hard goal reached raises GoalReached, a structured stop event carrying
target + observed — never a silent stop; soft flags without stopping.
The percent-goal baseline is D-E-gated: the field is reserved and
construction refuses with an explicit NotImplementedError.

Semantics are marked STACK-LOCAL in the docstrings — mirrored from the MAF
plan's capability description, never from MAF code; format shareability
stays a proposed decision point in the brief.

Two detach proofs delivered (decision gate removed -> red; dimension into
the key mint -> the double-counting test red). 400 -> 426 tests; README
synced (test count + a Value layer module block).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 04:00:14 +02:00