Skip to content

fix(ledger): a position the user never sold keeps its cycle across a new declaration (closes #539, closes #536) - #659

Merged
atomchung merged 3 commits into
mainfrom
claude/ownership-design-review-w1sb0o
Jul 31, 2026
Merged

fix(ledger): a position the user never sold keeps its cycle across a new declaration (closes #539, closes #536)#659
atomchung merged 3 commits into
mainfrom
claude/ownership-design-review-w1sb0o

Conversation

@atomchung

@atomchung atomchung commented Jul 31, 2026

Copy link
Copy Markdown
Owner

What changed

A declaration says what is held, never since when, so every adopted position whose start the declaration did not restate fell back to that declaration's own as_ofcycle_id reminted, and the next review asked again for the theses the user had already written, against the previous generation of ids. This carries the start itself across adoption, through one primitive both lanes reach, and closes #539 and #536 together because they are one defect seen from two lanes.

The design review that produced this shape, with receipts, is on #539 and #536; the owner disposition that revised it is here and the revision is f9d4d55. Findings that changed the plan #27 was carrying:

The stamp was never the fact worth carrying. _carry_forward_provenance was gated on a since_basis stamp, and the appearance question that produces one is only asked for a position that appears after the book exists (#531). Every position in a user's first declaration is therefore permanently unstampable — so unifying the two lanes, which is what #536's fix shape and #27's Front A both described, would have closed #536 and left #539 exactly where it was. Reproduced on main@d4c7d2e: an appearance adoption moved the untouched ACME/WIDGET from #2026-06-30#1 to #2026-07-15#1, and a second declaration through the review lane re-asked both theses and wrote a second generation of thesis rows.

What is carried now is the start itself, read off the derived book — which also fixes the same defect in the case where the engine already knew the answer: a position opened by a real trade had its ledger-proven start overwritten by the next declaration too.

since_basis is an evidence vocabulary, not a transport marker. The first cut stamped every carried start recorded_book; the owner refused it, correctly. That value says how a date was transported and destroys why it is believed, collapsing an exact start the ledger watched open together with a lower bound inferred from the day a declaration first listed a position — irrecoverably, because after an adoption origin describes the new snapshot writer rather than the original evidence. The vocabulary is now trade_event | snapshot_anchor | user_estimate | unknown, carried unchanged.

The branch order is the protection rather than the vocabulary: a position the record still traces to its anchor keeps that anchor's own basis whatever it says, and only one the anchor does not describe is labelled fresh. Recomputing from origin in both cases passes every single-adoption test and demotes an adopted trade-proven start on the second declaration, silently and permanently — so the trades-origin test adopts twice, and that is the assertion mutation D fails on.

cycle_seq is not a separate nicety. A cycle_id is ticker#since#seq, so a position flattened and bought back on one day differs from the cycle it replaced in the sequence alone. Carrying the date without the sequence does not merely misname the live position — it hands it the sold cycle's exact identity, and with it that cycle's thesis, standing conditions and closed/falsified status. Verified reachable through an ordinary adoption that asks the user nothing, and #539 widens the door to it (before, the live position landed on the declaration's own date and collided only by coincidence), so the two land together.

The seam is one layer below where #536 proposed it. Threading the prior anchor into snapshot_adapter.prepare would have added a second allow_engine_provenance=True call site, which test_only_the_refresh_lane_may_write_engine_assigned_provenance counts. Instead book_refresh.carry_recorded_starts assembles its own derived/recorded inputs and holds the single privileged call site; build_adoption reaches it for the refresh lane and review._carried_declaration for the review lane, before snapshot_adapter.prepare derives anything — so the plan's cycle ids and the ledger anchor come from one stamped envelope. Doing it at finalize would have corrected the ledger after the review had already re-asked. No call reordering was needed: provenance keys never enter snapshot_reconciliation's diff.

Nothing in the question layer changed. missing_thesis_positions is still positions whose cycle has no thesis; a position nobody sold simply finds the one it always had. Nothing reads the new provenance distinction either — #532 owns visibility — so what this owes is that the distinction survives, which the repeated-adoption assertions gate.

Tests

  • python3 tests/run_all.py complete offline suite passes (paste the tail below).
  PASS  Copy ratchet (#368 Phase 1)  (tests/test_copy_ratchet.py)
  PASS  Copy corpus golden (#402 knife 5)  (tests/copy_corpus.py)
  PASS  Episode checker probes (#417)  (tests/test_episode_checkers.py)
  PASS  Question-episode bank, mechanical half (#417)  (evals/run_episodes.py)
  PASS  Trigger matrix corpus and scorer (#458)  (tests/test_triggers.py)
  PASS  Persona sweep (personas x locales x reviews x variants)  (tests/persona_sweep.py)

PASS: all 49 suites passed.

Note for anyone reproducing: the suite needs skills/fomo-kernel/requirements.txt installed. Without pandas/yfinance present, nine suites fail on main as well as on this branch, for reasons unrelated to either.

New coverage, each against the lane it belongs to:

Test Owns
test_book_refresh.py::test_a_position_nobody_ever_asked_about_keeps_its_cycle_too #539's core — the unstampable majority
test_book_refresh.py::test_a_start_the_ledger_can_prove_is_not_overwritten_by_a_declaration the trades-origin symptom, and that its evidence survives a second adoption
test_book_refresh.py::test_a_carried_start_never_hands_a_live_position_a_sold_cycles_identity the same-day round trip, its fixpoint, and a lower bound staying a lower bound
test_review_v2.py::test_a_second_declaration_does_not_ask_for_theses_it_already_has #539's acceptance, end to end through prepare → preview → finalize
test_review_v2.py::test_a_review_lane_adoption_keeps_the_holding_duration_the_user_answered #536's acceptance
test_ledger.py::test_every_dated_basis_reads_the_same_and_says_something_different that the bases change provenance and never arithmetic

If this adds or changes a checker, gate, or test

  • Mutation evidence: the matching intentional mutation fails. A checker that stays green under its mutation is not evidence (docs/development-guide.md §2).

A — carry only the stamp (the pre-#539 behavior). _carried_start returns unchanged when the recorded row has no since_basis.

FAIL  test_a_position_nobody_ever_asked_about_keeps_its_cycle_too
FAIL  test_a_start_the_ledger_can_prove_is_not_overwritten_by_a_declaration
FAIL  test_a_stamped_position_keeps_the_canonical_current_book_usable
killed: test_a_second_declaration_does_not_ask_for_theses_it_already_has
killed: test_a_review_lane_adoption_keeps_the_holding_duration_the_user_answered

B — the review lane stops carrying (the pre-#536 behavior). snapshot=_carried_declaration(...)snapshot=None.

killed: test_a_second_declaration_does_not_ask_for_theses_it_already_has
killed: test_a_review_lane_adoption_keeps_the_holding_duration_the_user_answered
SURVIVED: test_a_differing_second_declaration_is_recorded_first_then_reviewed
test_book_refresh.py: 41 passed, 0 failed

The survivor is the correct partition rather than a gap: that scenario adopts through the refresh lane, and test_book_refresh.py owns that lane, so neither should notice a review-lane-only mutation.

C — carry the start but not the sequence.

FAIL  test_a_carried_start_never_hands_a_live_position_a_sold_cycles_identity
FAIL  test_a_stamp_never_survives_onto_a_different_cycle

D — recompute the basis from origin instead of carrying the recorded one. The demotion the owner's revision exists to prevent; it survives every single-adoption test.

FAIL  test_a_start_the_ledger_can_prove_is_not_overwritten_by_a_declaration:
      a start's evidence survives every later adoption; it is not recoverable
      afterwards, because by then `origin` describes the snapshot writer
41 passed, 1 failed

If this touches a renderer or output path

  • Surfaces reached by the changed code path are listed below, with the oracle covering each (guide §2 — the harness surface must be the delivery surface).

No renderer changed. The changed path reaches the snapshot anchor and the cycle ids derived from it, which two surfaces read:

Surface Oracle
Ledger anchor rows (since/since_basis/cycle_seq) test_book_refresh.py — the tests above plus test_a_stamped_position_keeps_the_canonical_current_book_usable, the pre-existing cross-lane oracle for a new anchor-position field, which now pins the full stamp map
The Review Plan's cycle ids and missing_thesis_positions test_review_v2.py's two new end-to-end tests, driven through the CLI rather than the builder
portfolio_basis.query_current_book (what consider reads) unchanged by construction — _normalized_anchor builds a fresh six-key dict, so provenance is dropped before its exact-key gate and two declarations of one book keep one state_version; asserted by the same cross-lane oracle

Mirrored surfaces updated: docs/maintainer-guide.md's snapshot-position-provenance row (which claimed a call site that no longer exists — a drift the mechanical count could not catch, since the literal is still one and still in book_refresh.py), docs/prd-ledger.md, references/data-contract.md, flows/book-refresh.md step 3, and schemas/book-refresh.schema.json's lane description. The flow and the data contract now state the rendering rule per basis, which the single-value model could not express: an exact start may be given as a day, a lower bound may not.

Not in scope

…new declaration (closes #539, closes #536)

A declaration says what is held, never since when. So every adopted position
whose start the declaration did not restate fell back to that declaration's own
`as_of`, `cycle_id` reminted, and the review asked again for the theses the user
had already written — against the previous generation of ids.

`_carry_forward_provenance` was gated on a `since_basis` stamp, and the
appearance question that produces one is only asked for a position that appears
*after* the book exists. Every position in a user's first declaration was
therefore permanently unstampable, so unifying the two lanes (#536) would have
left #539 exactly where it was. What is carried now is the start itself, read
off the derived book: an answered estimate stays the user's while the record
still traces the position to its anchor, and everything else still held takes
the record's own start, stamped `recorded_book`.

Reading it from the derived book removes the sold-and-rebought hazard rather
than gating around it — a rebought position's derived start *is* its rebuy date
— and it fixes the same defect where the engine already knew the answer: a
position opened by a real trade had its ledger-proven start overwritten too.

`cycle_seq` rides with the start, and that half is not optional. A `cycle_id` is
`ticker#since#seq`, so a position flattened and bought back on one day differs
from the cycle it replaced in the sequence alone: carrying the date without it
hands the live position the sold cycle's identity, with that cycle's thesis,
standing conditions and closed status. Verified reachable through an ordinary
silent adoption, and #539 widens the door to it, which is why it lands here.

One primitive, `book_refresh.carry_recorded_starts`, assembles its own inputs
and holds the single `allow_engine_provenance=True` call site. Both lanes reach
it: `build_adoption` for the refresh lane, `review._carried_declaration` for the
review lane, which calls it before `snapshot_adapter.prepare` so the plan's
cycle ids and the ledger anchor come from one stamped envelope. Doing it at
finalize would have corrected the ledger after the review had already re-asked.

Nothing in the question layer changed: `missing_thesis_positions` is still
positions whose cycle has no thesis. A position nobody sold simply finds the one
it always had.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ftXNmYcwBXsntrvPdpbKx
… on (#539)

Owner disposition on #539 accepted identity preservation, the derived-book
input and the lower seam, and refused the one-value provenance model this
branch shipped first.

`since_basis: recorded_book` described how a date was transported into the new
declaration, not why it is believed. It collapsed an exact start the ledger
watched a cycle open together with a lower bound inferred from the day a
declaration first listed the position, and the collapse is not recoverable
afterwards: once a book is adopted, `origin` describes the new snapshot writer
rather than the original evidence for the start. A renderer asked to state a
holding period later would have had nothing left to distinguish "bought on the
14th" from "on the books since at least the 14th".

So the basis is an evidence vocabulary now — `trade_event | snapshot_anchor |
user_estimate | unknown` — and it travels unchanged rather than being restamped.
The branch order is the protection: a position the record still traces to its
anchor keeps that anchor's own basis whatever it says, and only one the anchor
does not describe is labelled fresh. Recomputing from `origin` instead would
have demoted an adopted trade-proven start to a lower bound on the very next
declaration, silently — which is why the trades-origin test now adopts twice,
and why that second adoption is what the matching mutation fails on.

One guard narrowed with it: an equal-to-`as_of` start is skipped only when it
would restate the `snapshot_anchor` default. For the other bases the stamp
carries something the default does not, so a position bought on the day it was
declared keeps its exact label.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ftXNmYcwBXsntrvPdpbKx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants