fix(card): a review with no persisted rule never claims one is standing - #644
Conversation
…ng (closes #546) The pre-commitment preview's no-commitment branch (card_renderer.py's _next_block) resolved state.rule_dim -- this period's fresh prescription -- into block_missing.rule_standing ("the standing rule remains: ...") without checking whether a prior commitment was ever recorded. cmd_preview always drafts with require_commitment=False, so this branch fires on every preview before the user has chosen a rule, not only a first-ever review; on a fresh root it told the user continuity the product never earned. Gate the wrapper on the same review_plan.state_snapshot.prior_commitment.rule predicate _reconciliation_lines already reads for #292: with a real prior commitment on record, the standing-rule copy is unchanged; with none, a new key (block_missing.rule_pending, synchronized across en/zh-TW/zh-CN) states the same {rule} as a recommendation awaiting the user's choice, replacing rather than adding a sentence. Both renderers share the branch through _card_structure, including Markdown's compact text-first scan. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review — verified by rendering, merging. One residual found and filed separately.Rendered both states myself through The fix worksSame prescription ( The false continuity claim is gone from a first review, the returning-user path keeps its copy, and the substitution is one line for one line — the card does not grow. The copy is right. Your broader finding is the more valuable half of this PR.
Residual, out of this issue's acceptance — filed as its own issue
A rule the user never chose, presented as the one that "remains". Same falsehood as #546, moved from first-time users to returning ones — and it only becomes visible once #546's fix routes returning users into that branch deliberately rather than everyone. This is explicitly permitted by #546's own acceptance ("Returning-user preview with a real prior commitment may use the standing-rule continuity copy") and past its stop condition, so it is not blocking here. Opening it separately. |
The false claim, and where it came from
On a fresh root with no prior finalized review, the pre-commitment preview rendered:
That claim is false: nothing was ever committed, so there is no standing rule to keep.
skills/fomo-kernel/engine/card_renderer.py's_next_block, no-commitment branch (previously lines 3435-3447), resolvedstandingfromstate["rule_dim"]— this run's engine prescription — and formattedblock_missing.rule_standingwithout first checking whether a prior commitment was ever persisted. Becausecmd_previewalways drafts withrequire_commitment=False, that branch fires on every preview before the user has chosen a rule, not only a first-ever review — so a returning user whose most recent finalize offered no candidate rule (real shape, confirmed against therotatormock persona below) hit it too.The persisted fact was already being read two thousand lines above in the same file, at
_reconciliation_lines(#292's breach path):bundle["review_plan"]["state_snapshot"]["prior_commitment"]. The predicate existed; it was simply not consulted here.The fix
Gate the wrapper on that same predicate. With a real prior commitment on record, the standing-rule copy is unchanged. With none, a new copy key states the same
{rule}as a recommendation awaiting the user's choice, rather than a thing that "remains":The issue's own alternative — the existing generic no-commitment fallback (
block_missing.rule, "No new rule commitment this time; you can pick one at the next review.") — is not used for this branch, per the issue's own reasoning: this preview shows before the choice, so telling the user the choice already didn't happen would be a second false claim. That generic fallback still applies unchanged whenstate.rule_dimhas nothing resolvable at all (no recommendation to state either way) — untouched by this PR.New copy key:
block_missing.rule_pendingOne key, added to all three locales, one-line rationale each:
enNo rule chosen yet this period; the recommendation is: "{rule}"rule_standing's two-clause shape (fact, then the quoted rule) so the two wrappers read as siblings, but states absence-of-choice instead of continuity — no "remain"/"keep"/"standing".zh-TW這期還沒選規矩;建議是:「{rule}」zh-CN这期还没选规矩;建议是:“{rule}”“…”vs zh-TW's「…」), matching howrule_standingitself differs only in glyph set and quote style between the two.All three keep the same
{rule}interpolationrule_standingalready uses (localized_rule(state.rule_dim, ...)), so a standing single-position-cap override still threads through correctly (test_standing_rule_placeholder_carries_the_user_cap_override, unmodified and still green).skills/fomo-kernel/copy/en.json,zh-TW.json,zh-CN.jsonkey-parity check (envszh-TW, the block_missing register): 0 diff before and after.block_missingremains an unclaimed register intests/copy_corpus.py(17 → 18 keys, none exercised by a scene), so the copy-corpus golden needed no regeneration — confirmed by running it before and after (PASS copy corpus: 438 rendered surfaces match the golden, byte-identical).Rendered evidence — both states, all three locales, both renderers
All renders below are from the committed fictional
sample_ai_holderpersona (skills/fomo-kernel/mock/sample_ai_holder.csv) in isolated roots (tests/persona_sweep.pyfor en/zh-TW, a directengine/review.py prepare/previewCLI walk in a scratch root for zh-CN, since the sweep only covers en/zh-TW). No real trade data anywhere below.State (a): fresh root, first-ever review, preview before choice
State-snapshot fact for this state:
prior_commitmentis absent (None) — confirmed by readingreview_plan.state_snapshot.prior_commitmentdirectly off the plan.Markdown, Next step block:
[*] Change only this next time: No rule chosen yet this period; the recommendation is: "Cap any single position at 20%. Trim if it goes over, and do not add."[*] 下次只改這一件:這期還沒選規矩;建議是:「單筆部位上限定死 20%;超過就減,不新增。」[*] 下次只改这一件:这期还没选规矩;建议是:“单笔部位上限定死 20%;超过就减,不新增。”HTML,
.sec.keysteprule panel:<p class="rmain">No rule chosen yet this period; the recommendation is: "Cap any single position at 20%. Trim if it goes over, and do not add."</p><p class="rmain">這期還沒選規矩;建議是:「單筆部位上限定死 20%;超過就減,不新增。」</p><p class="rmain">这期还没选规矩;建议是:“单笔部位上限定死 20%;超过就减,不新增。”</p>None of the six lines above contain "remain", "keep", "standing", or "維持"/"维持".
State (b): returning user, real prior commitment, preview before this period's choice
Seeded by actually finalizing a week-1 slice of the same persona's history first (so
state_snapshot.prior_commitmentis a real persisted fact, not a synthetic one), then previewing week 2. State-snapshot fact for this state:prior_commitment = {"rule": "Cap any single position at 20%. Trim if it goes over, and do not add.", "origin": "candidate", "source": "user_chosen", ...}.Markdown, Next step block:
[*] Change only this next time: No rule change this period; the standing rule remains: "Cap any single position at 20%. Trim if it goes over, and do not add."[*] 下次只改這一件:這期不改規矩;現行規矩維持:「單筆部位上限定死 20%;超過就減,不新增。」[*] 下次只改这一件:这期不改规矩;现行规矩维持:“单笔部位上限定死 20%;超过就减,不新增。”HTML,
.sec.keysteprule panel:<p class="rmain">No rule change this period; the standing rule remains: "Cap any single position at 20%. Trim if it goes over, and do not add."</p><p class="rmain">這期不改規矩;現行規矩維持:「單筆部位上限定死 20%;超過就減,不新增。」</p><p class="rmain">这期不改规矩;现行规矩维持:“单笔部位上限定死 20%;超过就减,不新增。”</p>This state renders byte-identically to
mainbefore this PR — the continuity copy is preserved exactly where it is true.Net line count, state (a)
ai_holder-en.first.preview.private.mdis 48 lines both before and after (wc -l), byte-identical everywhere else on the card. Checked programmatically (not just this one file): every one of the 12 markdown cards the persona sweep found drifted changes exactly 2 removed + 2 added lines (the "Next rule" text-first-scan quote plus the "Next step" block itself, both reading the same panel) — a pure substitution, never a growth, on every affected card.Cross-renderer / cross-locale proof:
tests/persona_sweep.py --baselineRan the sweep with
--enginepointed at this branch and--baselineat a pristineorigin/main@6723db1checkout. Every mock persona × locale × review (first/second) × variant (preview/committed/skip), 156 Markdown + 156 HTML cards, byte-compared:(
--baselinetreats any drift as the tool's own FAIL — that's the tool doing its job of forcing a human read, not a suite regression;tests/run_all.pydoes not run this comparison.) 144/156 Markdown and 144/156 HTML byte-identical; the other 12+12 diffed line-by-line and every single one is exactly therule_standing→rule_pendingwrapper swap, nothing else — verified programmatically (diffed every drifted file, asserted every changed line matches the wrapper-swap pattern) rather than eyeballed.rotator's second review is in the drift list too: its own week-1 review offered no candidate rule, so its week-2 preview also has no persistedprior_commitment— real evidence the predicate is "has a real prior commitment", not "is this literally review #1".Mutation table
Both directions of the new predicate, on the new focused test (
tests/test_card_html.py::test_first_review_preview_states_the_recommendation_as_pending_not_standing), backed up/restored withcp(nevergit checkout),__pycache__cleared between runs, exit codes read directly (not grepped):standing_key = "rule_standing"(predicate deleted, old unconditional behaviour restored)AssertionError: zh-TW: fresh-root Markdown preview must not claim rule continuity ('維持' found in Next step), exit code 1. The pre-existing#356test (test_standing_rule_placeholder_resolves_copy_not_the_v1_literal) stayed green under this mutation — it only checks the{rule}substring, not the wrapper — confirming the new test is the one actually covering this regression.standing_key = "rule_pending"(predicate inverted to always-false/pending)AssertionError: zh-TW: a returning user with a real prior commitment must keep the standing-rule continuity copy in Markdown, exit code 1 — the complementary assertion, confirming the test is not decorative in either direction.cpfrom backup)tests/test_card_html.py: 62/62; confirmed again in the full suite).Verification
python3 tests/run_all.py: PASS: all 49 suites passed (tail pasted below), run twice — once mid-flight, once as the final gate after fixing atest_doc_language.pycatch (my first draft of the changelog row above embedded literal zh-TW characters indocs/output-contract.md, which is implementation-doc-English-only territory; fixed by paraphrasing instead of quoting).test_standing_rule_placeholder_resolves_copy_not_the_v1_literal(the exact fix(card): pre-commitment preview leaks a hardcoded Chinese rule string into English cards #356 regression test for "Chinese leaking into an English card") stays green, untouched in its core assertions.Scope
Touches only
skills/fomo-kernel/engine/card_renderer.py,skills/fomo-kernel/copy/{en,zh-TW,zh-CN}.json,docs/output-contract.md, andtests/test_card_html.py. Does not touchevaluation_challenge.py,review.py'sconsiderpath, orreferences/trade-consequence.md(PR #643 / #618's surface) — confirmed by diffing file lists, no overlap. Does not touch candidate-rule generation,prescribe(), the final committed card, or the Next-step block's layout.closes #546
🤖 Generated with Claude Code