From 1b1f6ec7e211931be33f2fc97d31434ccdedc305 Mon Sep 17 00:00:00 2001 From: Eva Date: Tue, 23 Jun 2026 14:11:57 +0700 Subject: [PATCH 1/2] =?UTF-8?q?feat(viewer):=204=20sat=E2=86=927=20web-vie?= =?UTF-8?q?wer=20UX/correctness=20fixes=20(battle-log=20sanitize,=20dice-h?= =?UTF-8?q?eader=20strip,=20roll=20context,=20drop-confirm)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From the binding-sweep per-persona friction analysis: 1. BATTLE-LOG SANITIZER COVERAGE (adversarial friction): the combat Battle Log rendered l.title/l.text RAW, so DM engine meta-text (wrapper-progress lines, roll-summary headers, scaffolding) leaked there — the Chronicle's guard never reached this panel. BattleLogLine now routes both fields through the SAME window-exported sanitizeNarration (screen-combat.jsx). 2. DICE-RESULT-HEADER STRIP (narrative's only major — 'cracked the 9/10'): extend the sanitizer to drop (a) the roll-result-summary header form ('… lands at 18; the quiet interpose at 16.') and (b) a leading/standalone Markdown horizontal rule ('---'/'***'/'___'/'— —'). Verb+bare-integer anchored and whole-line for the chained header so genuine prose ('the bell strikes at 12', 'the arrow lands at his feet', em-dash asides) is never eaten (screen-table.jsx). 3. DIE-ROLL BUTTON CONTEXT (adversarial minor): the d20/d12/d8/d6 buttons fired a contextless 'requests a dN roll', bypassing the Declare guard. They now bind the roll to the player's typed intent, else attach the latest narrative line — parity with the Declare text-guard (composeRollMove, screen-table.jsx). 4. DROP-CONFIRM ON EQUIPPED GEAR (adversarial minor): a one-click Drop on equipped body armor was irreversible. Both Drop affordances (context-menu + detail pane) now confirm via window.confirm ONLY for currently-equipped items; loose stash items are never nagged (isItemEquipped, screen-inventory.jsx). Tests: extend test_sanitize_narration.py (dice-header + leading-rule strip + a hardened negative-prose suite), add test_battle_log_sanitize.py (BattleLogLine meta-text strip + real-combat-text survival) and test_viewer_ux_guards.py (composeRollMove context + isItemEquipped/drop-confirm). Local viewer suite green (only pre-existing test_portrait_gen pydantic-env failure is unrelated). --- viewer/openworlds/screen-combat.jsx | 16 ++- viewer/openworlds/screen-inventory.jsx | 45 ++++++- viewer/openworlds/screen-table.jsx | 91 ++++++++++++- viewer/tests/test_battle_log_sanitize.py | 134 ++++++++++++++++++ viewer/tests/test_sanitize_narration.py | 73 ++++++++++ viewer/tests/test_viewer_ux_guards.py | 165 +++++++++++++++++++++++ 6 files changed, 513 insertions(+), 11 deletions(-) create mode 100644 viewer/tests/test_battle_log_sanitize.py create mode 100644 viewer/tests/test_viewer_ux_guards.py diff --git a/viewer/openworlds/screen-combat.jsx b/viewer/openworlds/screen-combat.jsx index 27473083..83d3cc42 100644 --- a/viewer/openworlds/screen-combat.jsx +++ b/viewer/openworlds/screen-combat.jsx @@ -754,13 +754,25 @@ function ApBadge({ used, label }) { function BattleLogLine({ l }) { const meta = Array.isArray(l.meta) ? l.meta : []; + // SAT→7 (adversarial friction): the player-facing Battle Log must get the SAME read/projection + // hygiene as the main Chronicle. The /move + combat-event tail can carry DM engine META-TEXT — a + // wrapper-progress line ("Momentum carries through the scene…", "Your choice takes hold…"), a + // roll-summary header, or other scaffolding — which leaked here RAW because this panel never + // routed through the Chronicle's guard. Reuse the SAME window-exported sanitizeNarration (the engine + // stays sole writer; this is a projection filter only). Defensive: fall back to the raw string if the + // sibling