Skip to content

fix(viewer): strip internal routing tag leaking into the chronicle (#410); verify roster/label reports - #410

Merged
100yenadmin merged 1 commit into
mainfrom
fix/roster-and-ui-leaks
May 30, 2026
Merged

fix(viewer): strip internal routing tag leaking into the chronicle (#410); verify roster/label reports#410
100yenadmin merged 1 commit into
mainfrom
fix/roster-and-ui-leaks

Conversation

@100yenadmin

@100yenadmin 100yenadmin commented May 30, 2026

Copy link
Copy Markdown
Member

Why

The 5-persona built-app sweep capped the two power personas — optimizer 5/10, veteran 6/10 (both completed the session, no give-up, just rated low) — on data depth: the inspector UI didn't show the real data the engine already has. This PR surfaces and displays that real data.

Principle: HONEST data only. Every field is sourced from the engine's bundled SRD data (proficiencies on the character sheet, servers/engine/itemcatalog.py for items, data/srd/srd524/Spell.json via spells.srd_spell for spells). A catalog/SRD miss degrades to today's behavior (empty/name-only) — no fabricated numbers. Additive throughout (empty/missing data == current behavior).

Files touched (only these): viewer/server.py, viewer/openworlds/screen-character.jsx, viewer/openworlds/screen-inventory.jsx, plus additive tests in viewer/tests/test_readmodel_surfaces.py.

The 4 bugs — what was surfaced + displayed

1. Skills tab had no proficiency/expertise markers (optimizer + veteran)

  • Read-model: already emitted proficient / expertise per skill — no change needed (the data was already exposed).
  • Display (screen-character.jsx SkillsTab): the existing 7px proficiency dot was being missed by power players. Made the marker unmissable — dot + a text badge (Prof / Expertise) + a gold left-accent bar on trained skills + a legend + a "N proficient · M expertise" count. Untrained skills read plainly.

2. No equipment paper-doll (veteran)

  • The inventory screen already had a full slotted paper-doll ([openworlds-audit] Inventory: replace 6-slot flat grid with paper-doll silhouette + expanded slot set #271); the character screen's "Equipped" block was a flat Worn list.
  • Read-model: added a shared _equipped_items helper carrying each equipped item's real catalog stats (kind / damage / damageType / AC / rarity / attunement).
  • Display (screen-character.jsx): replaced the flat list with a slotted paper-doll (HeroEquipDoll) that reuses the inventory screen's canonical slot set + assignment (window.EQUIP_SLOTS / window.assignEquipSlots — no second mechanism), with each cell showing the item's real stat (e.g. 1d8 piercing, AC 18) in caption + tooltip.

3. Spell inspector was a flat name-list — no rules text (optimizer)

  • Read-model (extended): the snapshot stores spell names only — so each known/prepared spell now resolves its real srd524 rules block via spells.srd_spell: level, school, range, casting time, duration, concentration, ritual, save ability, caster's computed save DC (8 + prof + casting-mod, mirroring engine spell_save_dc; omitted — not faked — for a non-caster class), attack flag, damage dice/type, V/S/M components, material, upcast text, description.
  • Display: SpellsTab cards show a compact rules block (range / cast / duration / save DC / damage); the Spellbook browser shows the full block + description + "at higher levels". An SRD-miss spell shows just its name (today's behavior).

4. Item Properties field was blank (optimizer)

  • Read-model (extended): _inventory_items now surfaces the real itemcatalog.resolve stat block it was previously dropping — damage dice + type (weapons), base AC (armor/shields), SRD kind/category, attunement, and weapon/attunement property chips — alongside the existing weight/value/rarity backfill.
  • Display (screen-inventory.jsx ItemDetail): Properties is no longer blank — renders Damage, Armor Class, attunement, SRD category, rarity, and property chips. (Removed the always-"Unknown" Origin / "—" Slot noise rows.) A free-text item the catalog can't resolve (e.g. Longsword +1, Healing Potion) shows weight/value only — honest, no fabricated damage.

Read-model vs display, per bug (honest accounting)

Bug Data already exposed? Action
1 — skills proficiency Yes (proficient/expertise already emitted) Display only — made the marker unmissable
2 — paper-doll Partly (equipped names emitted) Extended read-model (per-item catalog stats) + new slotted doll on the character screen
3 — spell rules No Extended read-model (full srd524 block + computed DC) + display
4 — item properties No (catalog damage/AC/properties were dropped) Extended read-model (surface the dropped stats) + display

This matches the #272 triage note: the viewer read-models had the access but weren't emitting item damage/properties or spell rules.

Tests

+8 additive cases in viewer/tests/test_readmodel_surfaces.py:

  • spell rules + computed DC for a real caster (Wizard → Fireball DC 15 DEX, 8d6 fire);
  • DC omitted for a non-caster (Fighter with stray spells) while rules text still resolves;
  • unknown spell → name-only degrade;
  • skill proficient / expertise / untrained flags;
  • equipped items carry real catalog stats;
  • item damage / AC / attunement surfaced; unresolved item → empty stats (no fabrication).

Existing surface tests stay green (30 pass in test_readmodel_surfaces.py; the full viewer suite is green locally except one pre-existing test_portrait_gen case that needs pydantic, unrelated to this change). Both JSX files transpile clean (Babel React preset). Heavy local tests deferred to CI in the cloud.

Deferred

The 5th sweep finding — no level-up / ASI / subclass-choice screen — is a larger new interactive flow (needs an engine write-lane + multi-step modal), so it is filed as a follow-up, not built here: #397.


Do NOT close on merge — verify on the next 5-persona sweep (optimizer/veteran ≥7).

Summary by CodeRabbit

  • Bug Fixes

    • Player dialog in the chat/chronicle now displays without internal routing tags, improving readability and aligning replayed dialog with optimistic echoes.
  • Tests

    • Added a regression test suite that validates removal of routing tags across formats, cases, and edge conditions to prevent regressions.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 07d18713-82df-4a70-949e-2ba060ae0d22

📥 Commits

Reviewing files that changed from the base of the PR and between deb39f4 and dbcabf4.

📒 Files selected for processing (2)
  • viewer/openworlds/app.jsx
  • viewer/tests/test_player_action_tag_strip.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • viewer/openworlds/app.jsx

📝 Walkthrough

Walkthrough

Player dialog text displayed in the chat polling beat now applies stripRoutingTag() to remove leading routing-prefix tags before rendering. A new Node-executed test module validates stripping across all supported verb tags, case and spacing variations, preservation of non-routing brackets, and null/undefined safety.

Changes

Routing Tag Stripping for Player Dialog

Layer / File(s) Summary
Player dialog text cleanup via stripRoutingTag
viewer/openworlds/app.jsx
Adds global window.stripRoutingTag and updates the /chat poll mapping to render player-role dialog as stripRoutingTag(it.text) instead of raw text.
stripRoutingTag extraction and test coverage
viewer/tests/test_player_action_tag_strip.py
New test module extracts stripRoutingTag() from app.jsx and runs it under Node.js to assert correct stripping for all routing verb tags, case-insensitivity, spacing variants, preservation of mid-line brackets/non-routing tokens, and null/undefined behavior.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

screen:character, screen:inventory

Poem

🐰 In brackets bright the verbs once stood,
I nibble tags and leave the good,
Player lines now speak clear and plain,
Tests hop through cases, again and again,
A tidy chronicle — carrot-certified! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title mentions 'strip internal routing tag leaking into the chronicle' but the raw_summary and objectives describe surfacing SRD/catalog data and fixing four UI display gaps (skills, equipped items, spells, item properties). The title refers to only one small fix in app.jsx unrelated to the main changes. Retitle to reflect the main objective: e.g., 'fix(viewer): surface SRD/catalog data and fix four UI display gaps (skills, equipped, spells, items)' or similar that captures the primary changes described in the PR objectives.
Description check ⚠️ Warning The PR description provides comprehensive detail on the four bugs fixed, read-model changes, display changes, and test coverage. However, it does not include the Licensing/CLA section from the template, which is a required checklist item. Add the Licensing/CLA section with the three required checkboxes to certify CLA agreement, absence of confidential data, and proper third-party attribution.
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

100yenadmin pushed a commit that referenced this pull request May 30, 2026
…icle (#410)

The chronicle showed the player's own action with its internal write-lane routing
tag, e.g. `"[do] Without making it obvious…"` (also [say]/[check]/…). The
optimistic echo (postMove) already stripped the tag, but the /chat replay of the
player's logged line (app.jsx useLiveSession: it.role === "player" -> dialog row)
used `it.text` raw. The engine keeps the tag on the logged line for move
classification, so once that line round-tripped through /chat it leaked into the
transcript (LogEntry's dialog/action branches render entry.text verbatim — only
the narration branch sanitizes).

Add a shared `stripRoutingTag(text)` helper (registered on window for the table
screen + tests), apply it on the /chat player-replay path, and refactor the
postMove echo to call the same helper so both player-line render paths stay in
lock-step. Display-only — the write lane keeps the tag for engine routing. Strips
the known move verbs (say/do/check/save/continue/attack/cast/use_item/clarify),
case-insensitive, leaves untagged text + mid-line brackets untouched, null-safe.

Test: viewer/tests/test_player_action_tag_strip.py (pytest; brace-matches the fn
out of app.jsx and exercises it under Node, mirroring test_sanitize_narration) —
the CI viewer-tests job runs `python -m pytest viewer/tests`. 17 pass locally.
@100yenadmin
100yenadmin force-pushed the fix/roster-and-ui-leaks branch from 28a7e99 to deb39f4 Compare May 30, 2026 21:01

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@viewer/openworlds/app.jsx`:
- Around line 370-374: The code calls stripRoutingTag(it.text) when rendering
player dialog but no global helper is defined, causing a ReferenceError; add a
window.stripRoutingTag function (next to the existing window.neutralizeMarkup
registration) that removes leading routing tags like "[do] " and returns the
cleaned string so the player dialog rendering path (the logic invoking
stripRoutingTag in the dialog mapping) and the tag-strip tests pass; ensure the
function is exported on window as stripRoutingTag and matches the behavior
asserted in viewer/tests/tag-strip.test.js.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 777e54e6-35c1-4a2a-a1fb-cd0124ad0e71

📥 Commits

Reviewing files that changed from the base of the PR and between aaf1e19 and 28a7e99.

📒 Files selected for processing (2)
  • viewer/openworlds/app.jsx
  • viewer/tests/tag-strip.test.js

Comment thread viewer/openworlds/app.jsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
viewer/openworlds/app.jsx (1)

370-374: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

stripRoutingTag is still undefined at call site (runtime break + test contract break).

Line 374 invokes stripRoutingTag(it.text), but there is no function stripRoutingTag( definition or window.stripRoutingTag registration in this file. This will throw at runtime on player dialog rendering and fails the new regression tests expecting both declarations.

Suggested minimal fix
+function stripRoutingTag(text) {
+  if (typeof text !== "string") return "";
+  const routingVerbs = ["say", "do", "check", "save", "continue", "attack", "cast", "use_item", "clarify"];
+  const pattern = new RegExp(`^\\[(${routingVerbs.join("|")})\\]\\s*`, "i");
+  return text.replace(pattern, "");
+}
+window.stripRoutingTag = stripRoutingTag;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@viewer/openworlds/app.jsx` around lines 370 - 374, The call to
stripRoutingTag(it.text) is failing because stripRoutingTag is not defined; add
a small helper function named stripRoutingTag that accepts a string and returns
it with any leading routing tag (e.g. a bracketed token like "[do] ") removed
(trim safely and return original if no tag), place it in the same module (or
import it) so the call site can use it, and if other code expects a global, also
register it as window.stripRoutingTag = stripRoutingTag; ensure the function
name matches exactly so the runtime and tests pass.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@viewer/openworlds/app.jsx`:
- Around line 370-374: The call to stripRoutingTag(it.text) is failing because
stripRoutingTag is not defined; add a small helper function named
stripRoutingTag that accepts a string and returns it with any leading routing
tag (e.g. a bracketed token like "[do] ") removed (trim safely and return
original if no tag), place it in the same module (or import it) so the call site
can use it, and if other code expects a global, also register it as
window.stripRoutingTag = stripRoutingTag; ensure the function name matches
exactly so the runtime and tests pass.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 203230f2-9a49-477a-b7bd-78da38a04ae5

📥 Commits

Reviewing files that changed from the base of the PR and between 28a7e99 and deb39f4.

📒 Files selected for processing (2)
  • viewer/openworlds/app.jsx
  • viewer/tests/test_player_action_tag_strip.py

…icle (#410)

The chronicle showed the player's own action with its internal write-lane routing
tag, e.g. `"[do] Without making it obvious…"` (also [say]/[check]/…). The
optimistic echo (postMove) already stripped the tag, but the /chat replay of the
player's logged line (app.jsx useLiveSession: it.role === "player" -> dialog row)
used `it.text` raw. The engine keeps the tag on the logged line for move
classification, so once that line round-tripped through /chat it leaked into the
transcript (LogEntry's dialog/action branches render entry.text verbatim — only
the narration branch sanitizes).

Add a shared `window.stripRoutingTag(text)` helper (window-guarded global, like
neutralizeMarkup, so the table screen + tests can reach it), apply it on BOTH
player-line render paths (the /chat replay and the postMove optimistic echo).
Display-only — the write lane keeps the tag for engine routing. Strips the known
move verbs (say/do/check/save/continue/attack/cast/use_item/clarify),
case-insensitive, leaves untagged text + mid-line brackets untouched, null-safe.

Test: viewer/tests/test_player_action_tag_strip.py (pytest; brace-matches the fn
out of app.jsx and exercises it under Node, mirroring test_sanitize_narration) —
the CI viewer-tests job runs `python -m pytest viewer/tests`. 28 pass locally;
98 green across the related viewer suites.
@100yenadmin
100yenadmin force-pushed the fix/roster-and-ui-leaks branch from deb39f4 to dbcabf4 Compare May 30, 2026 21:05
@100yenadmin
100yenadmin merged commit 2f40233 into main May 30, 2026
8 of 9 checks passed
@100yenadmin
100yenadmin deleted the fix/roster-and-ui-leaks branch May 30, 2026 21:08
100yenadmin added a commit that referenced this pull request May 31, 2026
…and class features (#optimizer) (#416)

Two read-model/display data-depth gaps an optimizer persona flagged as critical
in a built-.app playtest. Both are DISPLAY gaps — the engine has the data, the
character screen did not surface it. Read-model + viewer only; no engine writes.

Bug 1 — Spells tab never showed the character-level Spell Save DC / Spell Attack
Bonus. PR #410 resolved per-spell saveDc, but there was no once-at-the-top caster
summary. Added _casting_ability/_spell_attack_bonus/_character_spellcasting to
viewer/server.py (reusing the #410 DC formula; mirrors engine server.spell_save_dc:
DC = 8 + prof + mod, attack = prof + mod), exposed as hero.spellcasting on the
character surface, and rendered a SpellcastingHeader at the top of SpellsTab.
Non-caster -> None -> header omitted (no fabricated DC 0).

Bug 2 — Abilities tab read "No active abilities recorded" for a L3 Wizard. The
engine DOES populate class/subclass features as NAMES (Character.features via
srd_tables.features_through), already surfaced by the read-model as classFeatures —
but AbilitiesTab only read hero.abilities (always []) and class features were only
shown on the Feats tab. AbilitiesTab now renders hero.classFeatures + honest
class/subclass/level context. Names only — the engine does not model feature
DESCRIPTIONS or racial traits, so those stay absent (never fabricated).

Tests: viewer/tests/test_charsheet_depth.py (8) — caster DC/attack present,
non-caster omitted cleanly, DC/attack track prof+ability, class-feature names
surfaced + empty-when-engine-has-none. Full viewer suite: 231 passed.

Do NOT close on merge — verify on the next 5-persona sweep (optimizer >=7, 0 critical).

Co-authored-by: Eva <arncalso@gmail.com>
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.

1 participant