fix(consider): a per-holding integrity warning excludes and discloses that holding instead of disabling the whole account (closes #673) - #695
Merged
Conversation
… that holding instead of disabling the whole account One `oversell` row anywhere in the history — a sell with no matching prior buy, ordinary for an export that starts mid-account — refused every `consider` call on the account, permanently: the rows derive from history that has already happened, so no future trade clears them. The review lane discloses the identical condition and delivers its card. `basis["integrity"]` was a whole-book summary of a per-holding fact, the same shape as `cost_basis` one line above it, which #515 had already stopped gating whole. `consequence.integrity_exclusions` now scopes each warning to the holding it names and excludes it with reason `integrity_oversell`, recorded in `excluded_holdings` and disclosed as `partial_book` — never silently. - `_canonical_consider_before` re-divides `sizing_projection`'s own per-holding values over the surviving holdings. Nothing is re-valued: no price, cost, share count or FX rate is read, and `math.fsum` is `value_partition`'s own exactly-rounded summation, so with nothing integrity-excluded the weights are bit-for-bit what they were. - The projection-agreement gate is checked against the valuation half of the excluded set only. An integrity exclusion is not a claim about valuation — the projection can value the holding; the share count is what is in doubt. - A premise about an excluded holding is refused by name, before `validate_premise` can call it "not currently held" — that is a claim about the book, and the recorded zero is exactly what the warning puts in doubt. - A warning that names no ticker, a warning this route has no reason for (every `bad_*` row, already dropped by `portfolio_basis`), and a book with no usable holding left all stay whole-book fatal. - The review lane's honesty statement and the canonical integrity record are unchanged; this is a route-specific use policy over the same facts. `EXCLUSION_REASONS` is the single vocabulary, mirrored by both schemas and locked by a drift test, the discipline `DISCLOSURES` already has. closes #673 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
atomchung
force-pushed
the
claude/673-per-holding-integrity
branch
from
July 31, 2026 18:45
68c217c to
a21bb45
Compare
atomchung
marked this pull request as ready for review
July 31, 2026 18:51
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #673. Third leaf of QA repair Front A, after the two cross-currency truth defects, and implements the maintainer disposition of 2026-07-31 exactly: per-holding integrity degrades locally, whole-book corruption still refuses.
The defect
consequence.pygated the entire basis on the integrity list being non-empty:ledger.pyrecords oneoversellrow per sell with no matching prior buy — ordinary for an export that starts mid-account — so a single such sell disabledconsiderfor the whole account, permanently: the rows derive from history that has already happened, so no future trade clears them. The review lane discloses the identical condition and delivers its card.Reproduced on
origin/main@dc654a1with a five-line synthetic mid-history CSV (prepare→consider):The gate was one line, and the docstring immediately above it already argued against it:
basis["integrity"]is the same shape of whole-book summary over a per-holding fact thatcost_basiswas, and #515 had already removed that one.What changed
consequence.integrity_exclusions(basis)is the single scoper: it maps each warning to the holding it names and to an exclusion reason (integrity_oversell), and refuses whole-book for anything it cannot scope.rows_from_portfolio_basisthen excludes that holding exactly the way #515 excludes an unvaluable one — recorded inexcluded_holdings, disclosed aspartial_book, and never silent.review._canonical_consider_beforere-dividessizing_projection's own per-holding values over the surviving holdings. Nothing is re-valued here — no price, cost, share count or FX rate is read — andmath.fsumisvalue_partition's own exactly-rounded summation, so with nothing integrity-excluded the sum is bit-for-bit the projection's denominator and every weight is the pre-[defect·P1] One unmatched sell anywhere in the history disables consider for the whole account — the review lane discloses the same condition and continues #673 weight.validate_premisecan call it "not currently held" — that is a claim about the book, and the wrong claim, since the recorded zero is what the warning puts in doubt.portfolio_basisstill stores every row.EXCLUSION_REASONSis the single vocabulary, mirrored bytrade-evaluation.schema.jsonandevaluation-challenge.schema.jsonand locked by a drift test — the same disciplineDISCLOSURESalready has.Proof
1. A mid-history export with one unmatched sell answers an unaffected premise, with the exclusion disclosed. End to end through
prepareon a CSV thenconsider(the real user route from the issue):The exclusion reaches the answer contract, not only the payload — the emitted challenge owes it:
{"topic": "excluded_holding", "anchor": "consequence.excluded_holdings.0.ticker", "value": "ORPH", "detail": {"reason": "integrity_oversell"}}2. A premise on the excluded holding refuses by name, buy and sell alike:
3. Whole-book conditions stay fatal. Three shapes, all pinned:
bad_*warning that does name a ticker (bad_avg_cost)no trustworthy canonical current book in …—portfolio_basis._bad_integritystill drops the whole basis; naming a holding does not make unknown state a bounded subsetledger.py's ownbad_trade_eventshape: issue +detail, no ticker)canonical PortfolioBasis has an integrity warning that cannot be scoped to one holding: bad_trade_eventcanonical PortfolioBasis has no usable holding: AAA (integrity_oversell), BBB (integrity_oversell)The last message replaces
no holding that can be valued: …, which is false for a book emptied by integrity warnings and would send the user after costs already on record. One existing assertion updated with it.4. The denominator provably shrinks. The regression fixture holds five positions of 1000 plus a damaged ORPH of 2000. Over the whole 6000 book AAA is 16.7%; over the 4000 usable book it is exactly 25%, and the test asserts the number:
Rule collisions inherit it: with a 20% cap, AAA is
already_overon the usable book and would readwould_breachon the whole one — so the collision state, not just thepartial_bookmarker, pins which denominator judged the rule.5. Mutation proof.
cpbackup/restore,__pycache__cleared, import-checked before each run so every red is semantic rather than a crash. All five reddenedtests/test_consider.py:FAIL test_a_premise_on_the_integrity_excluded_holding_refuses_by_name: expected failure, got exit 0… got 'canonical PortfolioBasis has no usable holding: 'and'canonical PortfolioBasis holdings disagree with consider adapter'FAIL test_an_integrity_warning_this_route_cannot_scope_refuses_the_whole_book: an integrity warning this route has no reason for (bad_avg_cost) must not yield a usable bookE is worth naming: its first version stayed green. The test then only covered the ticker-less half, so the issue-kind half of the gate was unpinned. Widened to both shapes, using
ledger.derive_holdings' own real row shapes, before it reddened.6. Offline suite.
python3 tests/run_all.py→PASS: all 49 suites passed.(tests/test_consider.py122/122).Scope
One leaf. #674 (what the user receives when computation genuinely cannot proceed) is the next one and is not implemented here; the fatal path's error payloads name their condition and their repair, so they do not get in its way. #528 (value-only positions) is untouched — a holding excluded for a missing cost still does not refuse a premise about it, deliberately.
🤖 Generated with Claude Code