feat(add-cash): the cash ask accepts an amount or a percentage, and the engine owns the disclosed conversion (closes #662) - #698
Merged
Conversation
…he engine owns the disclosed conversion (closes #662) A user answering "30%" to the cash-anchor ask used to cost three free-form clarification round-trips, because the ask only accepted an absolute currency amount and nothing disambiguated that. Owner disposition 2026-08-01: accept both formats; the denominator a percentage names is the account's total value (cash plus current position market value, never the position value alone); state that denominator in plain words and confirm it once; the engine, never the agent, converts and discloses the derivation. trade_recap.resolve_cash_anchor_input is the one place a {currency, percent_of_total, as_of} anchor is accepted and converted to the absolute-amount shape cash_position has always taken, called from main() against the same frozen held_mv the card was priced from -- never a second market resolution. The algebra is p/(100-p) * held_mv, not p/100 * held_mv: 30% against a 70,000 position value is 30,000 on a 100,000 total, never the 21,000 a p/100 misreading of the denominator would produce. A non-finite or non-positive held_mv refuses rather than converting against a garbage denominator, and a multi-currency book accepts the percentage only as a single anchor in the aggregate currency, never inside the per-currency list -- absolute-amount anchors, single or listed, are untouched. review.py's cmd_add_cash surfaces the conversion's derivation (percent, position value, currency, formula, amount) as anchor_conversion only when a percentage was converted, so the absolute-amount response carries no new key at all. _cash_anchor_status's next_action, the flow docs, and references/data-contract.md's cash-anchor beat statement now state both accepted formats and the denominator in plain words. Proof: tests/test_engine_units.py pins the algebra and every fail-closed case (bad range, non-positive held_mv, both amount and percent, percent inside a multi-currency list, currency not the aggregate). tests/ test_review_v2.py drives the real CLI on an engine-priced review: the converted amount and the amended card's cash weight both land exactly on the requested percentage, the absolute-amount response is unchanged, and a fully-exited book refuses a percentage rather than converting against a zero position value. tests/test_tr_json_contract.py's STATE_KEYS registers the new cash_anchor_conversion field. Mutation-tested by hand (cp backup/restore, __pycache__ cleared, import-checked): breaking the algebra to p * held_mv reddens three tests with the wrong number (21000/55480) visible; dropping the derivation reddens two with a clear "not disclosed" assertion. Full offline suite green (49/49 suites). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Summary
A user answering "30%" to the cash-anchor ask used to cost three free-form clarification round-trips, because the ask only accepted an absolute currency amount and nothing disambiguated that (#662's trigger, a real
/fomo-qadogfood session). Maintainer disposition (2026-08-01, resolving the issue's three proposals as proposal 2 with the denominator named):What changed
skills/fomo-kernel/engine/trade_recap.py: newresolve_cash_anchor_input(raw_anchor, held_mv, aggregate_currency)— the one place a{currency, percent_of_total, as_of}anchor is accepted and converted to the absolute-amount shapecash_positionhas always taken. Wired intomain()right whereTR_CASHis parsed, against the sameheld_mvthe card was priced from (frozen prices, never a fresh fetch). NewCashAnchorInputErrorfails the whole run closed (caught in__main__besideMissingAggregateCurrencyRate) rather than silently reachingcash_positionas an anchor it can't use.build_state()gains acash_anchor_conversionfield (Noneon the unchanged absolute-amount path) so the derivation reaches the CLI layer.skills/fomo-kernel/engine/review.py:_cash_anchor_status'snext_actionnow states both accepted formats and the denominator in plain words, and gives both--cashshapes.cmd_add_cashsurfaces the derivation as a top-levelanchor_conversionkey only when a percentage was converted — the absolute-amount response carries no new key at all.add-cash --cash's argparse help documents the new shape.skills/fomo-kernel/references/data-contract.md: the cash-anchor beat statement now documents both formats, the algebra, the multi-currency scope, and the fail-closed case.skills/fomo-kernel/flows/first-review.md/flows/weekly-review.md: the short route-specific ask pointers now mention both formats and the one-confirmation rule.docs/maintainer-guide.md: the cash-anchor mirrored-surfaces row documents what [design·process] Cash-anchor ask has no format contract — a percentage answer needs three round-trips to resolve #662 adds and its readers.tests/test_engine_units.py(pure-function algebra + every fail-closed case),tests/test_review_v2.py(real-CLI end-to-end wiring + regression + fail-closed),tests/test_tr_json_contract.py(registers the newSTATE_KEYSfield).The algebra, spelled out exactly as the issue's disposition asked: with cash included in the denominator, a p% answer means
cash = p/(100-p) * position_value, notp × position_value. 30% against a 70,000 position value is 30,000 on a 100,000 total — never the 21,000 ap/100misreading of the denominator would produce.Proof (all mandatory items from the issue)
tests/test_engine_units.py::test_resolve_cash_anchor_input_converts_percent_with_the_correct_denominatorpins 30% / 70,000 → 30,000 (total 100,000), and asserts!= 21,000.tests/test_review_v2.py::test_add_cash_converts_a_percentage_against_this_sessions_frozen_position_valuedrives the real CLI on an engine-priced review, independently recomputes20/80 * position_value, and confirms the amended card'scash.weightlands exactly on0.20.anchor_conversion/derivationcarriespercent_of_total,position_value,currency,formula, andamount.test_add_cash_percent_leaves_the_absolute_amount_response_unchangedasserts"anchor_conversion" not in outfor an absolute-amount payload, and every pre-existingadd-cashtest intest_review_v2.py(the [defect·P1] add-cash refuses the session it exists for: the drift gate compares question_queue and state_snapshot whole, so the card-beat cash answer is unrecordable #665 recompute suite) still passes unmodified.test_add_cash_percent_refuses_without_a_usable_position_valueprepares a fully-exited (zero-holdings) book and confirms a percentage payload refuses with a named error, leaves no pending session or ledger row behind, while a plain absolute amount on the same book still works.cpbackup/restore,__pycache__cleared, import-checked each time):p * position_value(wrong denominator): reddenedtest_resolve_cash_anchor_input_converts_percent_with_the_correct_denominator(21000.0visible),test_resolve_cash_anchor_input_discloses_the_derivation(also pins30000.0), and the integration test ((55480.0, 69350.0, 277400.0)visible — 55480 = 20% of 277400, the wrong reading).return resolved, None): reddenedtest_resolve_cash_anchor_input_discloses_the_derivation("a percentage anchor was converted but the derivation was not disclosed...") and the integration test ("a percentage was converted but the response discloses nothing about it").python3 tests/run_all.py→PASS: all 49 suites passed.(also verified again after rebasing onto the latestorigin/main).Scope discipline
consider --cash(a separate code path, never routes throughtrade_recap.py) is untouched.copy/*.json) changes — the ask is agent-composed prose guided bynext_action, not a fixed locale template.Assumption flagged for owner review
Multi-currency percentage scope: per the issue's own guidance, a percentage answer on a multi-currency cash book is ambiguous (which currency's bucket?). I took the "smallest honest cut" the issue named: a percentage is accepted only as a single anchor stated in the book's own aggregate currency (
currency_meta.aggregate_currency— USD for a mixed book, the book's own currency otherwise), validated and refused on a currency mismatch rather than silently relabelled. Per-currency absolute anchors (the pre-existing list form) are completely unaffected.🤖 Generated with Claude Code