Merge main into the Phase 17 refactor

`main` changed the four files this branch split into packages, so all four
came back as modify/delete conflicts. Each change is ported to the module
that now holds the code, unchanged in behavior:

- `delete_action` moves to `routers/adventures/actions.py`. It reaches the
  turn lock as `turns.acquire_turn_lock` and `turns._active_turns`, which is
  the rule this branch set: the package root no longer re-exports the names a
  test rebinds.
- `EMIT_RULE` moves to `worldstate/parse.py`, and `_describe_stat` and
  `render_reference` to `worldstate/render.py`.
- The chip-wrap rules move to `styles/schema-editor.css`, and
  `StateChangeChips` to `pages/Play/reports.jsx`. `index.css` keeps this
  branch's import list.

`test_delete_state.py` needed four edits to run here. It drops the eight-line
database prologue, because `conftest.py` does that once now. It imports the
shared `ScriptedProvider` from `fakes.py` instead of carrying a copy. It
patches `adventures.turns.OpenAICompatibleProvider` and reaches the lock the
same way. And it no longer passes `index=0` when it builds the start action,
because migration 71 drops that column.

564 tests pass. `npm run lint` and `npm run build` are clean, and the CSS
bundle is 56.71 kB, which is the pre-merge size plus main's new rules.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parththakkar106
2026-08-30 18:03:36 +05:30
co-authored by Claude Opus 5
7 changed files with 334 additions and 35 deletions
+3 -2
View File
@@ -53,9 +53,10 @@ function StateChangeChips({ changes }) {
// neither an update nor a refusal, and "+0" read as the former.
const blocked = c.clamped && d === 0
const dir = blocked ? 'refused' : typeof d === 'number' ? (d > 0 ? 'up' : d < 0 ? 'down' : 'flat') : 'flat'
const signed = !blocked && typeof d === 'number'
const txt = blocked
? 'no change — at its limit'
: typeof d === 'number' ? (d > 0 ? `+${d}` : `${d}`) : `→ ${c.value}`
: signed ? (d > 0 ? `+${d}` : `${d}`) : `→ ${c.value}`
const title = blocked
? c.fix || 'The story asked to change this and it is already at the limit the scenario allows.'
: c.clamped
@@ -63,7 +64,7 @@ function StateChangeChips({ changes }) {
: undefined
return (
<span key={i} className={`chg chg-stat chg-${dir}`} title={title}>
{nice(c.label)} <span className="chg-val">{txt}</span>
{nice(c.label)} <span className={signed ? 'chg-val chg-num' : 'chg-val'}>{txt}</span>
{c.clamped && !blocked ? <span className="chg-limited"> (limited)</span> : null}
</span>
)
+10 -4
View File
@@ -208,13 +208,19 @@
font-variant-numeric: tabular-nums;
/* A chip must never be wider than the column it sits in: an NPC stat like
"bandit leader aggression +10" overruns a phone, and the chip row is
inside the story text, which has nothing to scroll. The label may break
mid-word as a last resort; the value is glued to it by its own nowrap so
the sign and number never end up on separate lines. */
inside the story text, which has nothing to scroll. Text may break
mid-word as a last resort, which is what keeps a chip's minimum width
small enough for the flex row to shrink it. */
max-width: 100%;
overflow-wrap: anywhere;
}
.chg-val { white-space: nowrap; }
.chg-val { overflow-wrap: anywhere; }
/* Only a signed number is held on one line, so the sign never ends up on a
line of its own. The other values a chip can carry are phrases — a refusal
reason, "no change — at its limit", or the whole new value of a free-text
stat — and holding those together set the chip's minimum width to the width
of the phrase, which is what pushed a chip out of the message on a phone. */
.chg-num { white-space: nowrap; }
.chg-up { color: var(--player, #4caf82); border-color: color-mix(in srgb, var(--player, #4caf82) 40%, var(--border)); }
.chg-down { color: var(--danger, #e5484d); border-color: color-mix(in srgb, var(--danger, #e5484d) 40%, var(--border)); }
.chg-flag { color: var(--accent-bright); }