Skip to content

feat(trade): settle a trade — in/out reconciliation with a per-card report (#332) - #346

Merged
untraceablez merged 2 commits into
mainfrom
feat/332-trade-commit
Aug 21, 2026
Merged

feat(trade): settle a trade — in/out reconciliation with a per-card report (#332)#346
untraceablez merged 2 commits into
mainfrom
feat/332-trade-commit

Conversation

@untraceablez

@untraceablez untraceablez commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Closes #332. Second half of the Trade Function milestone; #345 (the pool selector) has merged, so this now targets main directly.

Settling is the only step that touches the collection.

Why this is more than "apply the list"

The shape of src/trade_commit.py comes from ADR 0002 D3, which asked for one specific operation: apply this set of in/out moves atomically at the card level, reporting exactly which ones applied. That's more than a purely local trade needs, and paying for it now is deliberate — a trade's two halves will eventually be committed on two different machines, and the issue already asks for a trade that falls through on some cards mid-session to be representable.

So "atomic" means per card, not per trade:

  • a line that can only move some of its copies moves them and reports the shortfall, rather than failing the whole trade;
  • one line failing never rolls back the ones that worked;
  • but the whole commit is one transaction, so a crash halfway leaves the collection exactly as it was — a half-applied trade is worse than a failed one;
  • what's left staged is exactly what didn't happen, and Settle the rest picks it up later.

Reconciliation rules

  • Outgoing is re-resolved by copy kind (printing + finish + condition + language), never by the stack it was staged from — so the trade still settles after that stack was merged, split or deleted (D4), and trading away your plain copy never quietly consumes your foil. Copies come from the smallest matching stack first, so an odd single gets used up before a bigger stack is broken into.
  • Incoming gets the same intake as a manual addition — finish/condition/language from the trade, plus a binder/box to file them into on the way in.
  • Applied copies can't be unstaged. remove_item trims the line back to what was applied rather than deleting it, so the pool keeps an honest record of what really happened.

add_or_increment grew commit=False so the trade commit can own the transaction — reusing it rather than writing a second copy of the stack-key logic, which is exactly the drift #338 existed to fix.

Caught by running it, not by the tests

Driving a real partial commit against the dev collection produced a green "Trade settled" banner while two cards were still staged. Every chosen line had applied, so ok was true — but the trade wasn't done. The heading now keys on completed, distinguishes "Partly settled — N problem(s)" from "Cards moved — the rest of this trade is still staged", and offers Settle the rest whenever anything is outstanding.

Verification

  • 1235 passed, 100% coverage, 0 missed lines (24 new tests).
  • ruff check src tests clean; mkdocs build --strict clean.
  • Migration 0032 applies and downgrades cleanly.
  • Driven end-to-end against the real dev collection: staged Bayou + Wheel of Fortune out and a foil NM Force of Will in, then settled only the Bayou and the incoming line. Result: Bayou 2 → 1, Wheel of Fortune untouched and still staged, Force of Will added as foil / NM / filed in "Trade box", report read 1 card(s) left your collection + still staged. The follow-up review page then offered only the one outstanding line. Dev collection restored to its exact prior state afterwards.

Sonar

The first build failed the quality gate on one new-code smell — python:S3358, a nested conditional expression in the one place that decides whether a line applied, partly applied or failed. Real readability finding, so it's an if/elif chain now rather than a suppression.

Follow-up

With #331 and #332 landed, ADR 0002's decision (3) triggers: write ADR 0003 with the concrete QR payload and endpoints, jointly with scanme (scanme#325 is the consumer placeholder).

🤖 Generated with Claude Code

https://claude.ai/code/session_01LxrePqFWyzQTcWaroSk4sH

untraceablez and others added 2 commits August 21, 2026 12:13
…eport (#332)

Settling is the only step that touches the collection. The shape of `src/trade_commit.py` is
dictated by ADR 0002 (D3), which asked for one specific operation: *apply this set of in/out moves
atomically at the card level, reporting exactly which ones applied*. That is more than a purely
local trade needs, and paying for it now is deliberate — a trade's two halves will eventually be
committed on two different machines, and #332 already asks for a trade that falls through on some
cards mid-session to be representable.

So "atomic" means per card, not per trade:

* a line that can only move some of its copies moves them and reports the shortfall, rather than
  failing the whole trade;
* one line failing never rolls back the ones that worked;
* but the whole commit is a single transaction, so a crash halfway leaves the collection exactly
  as it was — a half-applied trade is worse than a failed one;
* what's left staged is exactly what didn't happen, and "Settle the rest" picks it up later.

Outgoing cards are re-resolved by copy kind (printing + finish + condition + language) rather than
by the stack they were staged from, so the trade still settles after that stack was merged, split
or deleted (D4) — and trading away your plain copy never quietly consumes your foil. Copies are
drawn from the smallest matching stack first. Incoming cards get the same intake a manual addition
gets, including filing them into a binder or box on the way in.

Once a copy has moved it can't be unstaged: `remove_item` trims the line back to what was applied
instead of deleting it, so the pool keeps an honest record of what really happened.

`add_or_increment` grew `commit=False` so the trade commit can own the transaction — reusing it
rather than writing a second copy of the stack-key logic, which is exactly the drift #338 existed
to fix.

Caught by driving it against the real dev collection: a commit where every chosen line applied but
other cards were left staged reported "Trade settled", which is not true. The result now keys its
heading on `completed` rather than `ok`, and offers "Settle the rest" whenever anything is left.

Migration 0032. 24 new tests; suite 1235 passing at 100% coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxrePqFWyzQTcWaroSk4sH
SonarQube python:S3358 on new code — a nested conditional expression. It was a real readability
smell in the one place that decides whether a line applied, partly applied, or failed, so an if/elif
chain rather than a suppression.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxrePqFWyzQTcWaroSk4sH
@untraceablez untraceablez reopened this Aug 21, 2026
@untraceablez
untraceablez force-pushed the feat/332-trade-commit branch from 27960bf to aa0016f Compare August 21, 2026 17:13
@untraceablez
untraceablez changed the base branch from feat/331-trade-pool to main August 21, 2026 17:13
@untraceablez
untraceablez force-pushed the feat/332-trade-commit branch from aa0016f to 27960bf Compare August 21, 2026 17:14
import sqlalchemy as sa
from alembic import op

revision: str = "0032_trade_commit"
from alembic import op

revision: str = "0032_trade_commit"
down_revision: str | None = "0031_trade_pool"
@untraceablez
untraceablez merged commit 49dd833 into main Aug 21, 2026
8 checks passed
@untraceablez
untraceablez deleted the feat/332-trade-commit branch August 21, 2026 17:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Trade pool in/out confirmation and collection reconciliation

1 participant