Character sheets: convert a sheet from another system, as a draft (#958) #1365

Merged
claude-bot merged 4 commits from feat/958-convert-sheet into main 2026-11-07 06:51:17 +00:00
Collaborator

Part of #958 (IS-424; D5 as amended by lane 9, and the spec's "a sheet from another system is converted by the LLM into the campaign's definition and reviewed as a draft"). Opened by the orchestrator for the builder, which had no Forgejo tools.

Where it's offered

  • On a sheet whose system isn't the campaign's (SheetOut.other_system), for a GM or the character's player. A notice reads "This sheet is for D&D 5e", with Convert into PF2e, as a draft and Keep it as it is. "Keep" is remembered per sheet, and the More menu still offers the conversion.
  • With no AI set up, Convert is disabled with the reason shown.

How it converts

  • The prompt covers:
    • the old sheet's values with their labels, the numbers it worked out, and its rows;
    • the new definition's writable fields with what each takes (type, range, choices, rank names);
    • the lists the new definition has and what kinds each takes.
  • The answer is structured output: values as strings with a confidence, a note and what each came from. Entries are named with a kind.
  • Retries: an unreadable answer is retried once, then gives a 503.
  • What isn't converted: computed, rows, richtext, resource and track fields are never offered. Rows-type fields such as PF2e Lore aren't converted in v1.

What's validated

  • Values: every value goes through the importer's fit() against the new definition. Anything that doesn't fit is listed as "Not converted", with the reason. Old values nothing cited are listed too.
  • Entries: matched to the target system's rules by name (the importer's matcher), otherwise kept as text.
  • Locks (security fix):
    • a non-GM gets a 409 if any GM lock wouldn't carry over: a missing path, a whole-sheet * lock, or a definition-level lock;
    • a GM may convert, and mappable locks carry over;
    • the original locks are recorded in source_ref.converted_from.
  • Apply re-checks everything from the posted draft without calling the model, and needs expected_version (409 if stale) and replace: true.

Applying

  • In place: the page has one sheet row. Before and after, it writes conversion versions stamped with their system.
  • Old versions are kept but can't be restored: versions from the old system list as restorable: false, and restoring one is a 409.
  • Cleared: resources, the roll log, overrides and level-waiting state.
  • Migration d958c0a1b2c3: nullable character_sheet_versions.system_id, backfilled. The head pin moves.

API

  • GET …/lore/{eid}/sheet/convert
  • POST …/sheet/convert/draft (saves nothing; returns a report with converted, kept, not_converted, entries, warnings, locks, and usage)
  • POST …/sheet/convert/apply
  • All errors are {"detail": ...}. The details are in docs/API.md.

Wording for review

On the owner's Wording Review page as part "ab".

Checks (docker-test, committed code, host load 22–40)

  • lint is clean, eslint has no errors, and checks pass.
  • Backend:
    • the convert and lock tests pass, as do the title-reader guard after a fix and the migration pin;
    • the full backend suite ran to 100% with no failures (the exact count wasn't captured).
  • Frontend:
    • ConvertSheet.test.jsx passes 10/10;
    • the full vitest run gave 5,774 passed and 1 failed: DesignReference.test.jsx timed out under load, and passed 5/5 when rerun alone.
  • Not run: a render (no seeded 5e sheet in a PF2e campaign, and no AI on the stack), the build suite, and a literal alembic round trip against a full database. The migration's round trip is tested on a scratch schema.

Open for the owner

  1. The draft is a preview in a dialog, not stored in Suggested wiki updates as lane 9's caption says. There's no draft table and it can't go stale, but it's lost if the dialog closes. The Questions page asks.
  2. AI usage is counted and logged but not stored, since there's no per-feature usage table outside sessions.
  3. "Show the table" reveals aren't withdrawn on conversion. The importer doesn't withdraw them either.

🤖 Generated with Claude Code

Part of #958 (IS-424; D5 as amended by lane 9, and the spec's "a sheet from another system is converted by the LLM into the campaign's definition and reviewed as a draft"). Opened by the orchestrator for the builder, which had no Forgejo tools. ## Where it's offered - On a sheet whose system isn't the campaign's (`SheetOut.other_system`), for a GM or the character's player. A notice reads "This sheet is for D&D 5e", with **Convert into PF2e, as a draft** and **Keep it as it is**. "Keep" is remembered per sheet, and the More menu still offers the conversion. - With no AI set up, Convert is disabled with the reason shown. ## How it converts - **The prompt** covers: - the old sheet's values with their labels, the numbers it worked out, and its rows; - the new definition's writable fields with what each takes (type, range, choices, rank names); - the lists the new definition has and what kinds each takes. - **The answer** is structured output: values as strings with a confidence, a note and what each came from. Entries are named with a kind. - **Retries:** an unreadable answer is retried once, then gives a 503. - **What isn't converted:** computed, rows, richtext, resource and track fields are never offered. Rows-type fields such as PF2e Lore aren't converted in v1. ## What's validated - **Values:** every value goes through the importer's `fit()` against the new definition. Anything that doesn't fit is listed as "Not converted", with the reason. Old values nothing cited are listed too. - **Entries:** matched to the target system's rules by name (the importer's matcher), otherwise kept as text. - **Locks (security fix):** - a non-GM gets a **409** if any GM lock wouldn't carry over: a missing path, a whole-sheet `*` lock, or a definition-level lock; - a GM may convert, and mappable locks carry over; - the original locks are recorded in `source_ref.converted_from`. - **Apply re-checks everything** from the posted draft without calling the model, and needs `expected_version` (409 if stale) and `replace: true`. ## Applying - **In place:** the page has one sheet row. Before and after, it writes `conversion` versions stamped with their system. - **Old versions are kept but can't be restored:** versions from the old system list as `restorable: false`, and restoring one is a 409. - **Cleared:** resources, the roll log, overrides and level-waiting state. - **Migration `d958c0a1b2c3`:** nullable `character_sheet_versions.system_id`, backfilled. The head pin moves. ## API - `GET …/lore/{eid}/sheet/convert` - `POST …/sheet/convert/draft` (saves nothing; returns a report with `converted`, `kept`, `not_converted`, `entries`, `warnings`, `locks`, and `usage`) - `POST …/sheet/convert/apply` - All errors are `{"detail": ...}`. The details are in `docs/API.md`. ## Wording for review On the owner's Wording Review page as part "ab". ## Checks (docker-test, committed code, host load 22–40) - lint is clean, eslint has no errors, and `checks` pass. - **Backend:** - the convert and lock tests pass, as do the title-reader guard after a fix and the migration pin; - the **full backend suite** ran to 100% with no failures (the exact count wasn't captured). - **Frontend:** - `ConvertSheet.test.jsx` passes 10/10; - the full vitest run gave 5,774 passed and 1 failed: `DesignReference.test.jsx` timed out under load, and passed 5/5 when rerun alone. - **Not run:** a render (no seeded 5e sheet in a PF2e campaign, and no AI on the stack), the `build` suite, and a literal alembic round trip against a full database. The migration's round trip is tested on a scratch schema. ## Open for the owner 1. **The draft is a preview in a dialog, not stored in Suggested wiki updates** as lane 9's caption says. There's no draft table and it can't go stale, but it's lost if the dialog closes. The Questions page asks. 2. **AI usage** is counted and logged but not stored, since there's no per-feature usage table outside sessions. 3. **"Show the table" reveals aren't withdrawn** on conversion. The importer doesn't withdraw them either. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
A sheet saved for another game system than the campaign's can be converted
by the campaign's own AI into the campaign's definition, as a draft the
person reviews (IS-424, D5 as amended by lane 9).

- app/services/sheet_convert.py: the offer, the draft (prompt from the old
  sheet's values and the new definition's fields, a json_schema with the
  new paths as an enum, values held to the definition with importers.fit,
  entries matched by name to the new system's rules), and the apply (the
  draft checked afresh, the model not asked again; the page's sheet moved
  to the new definition in place, the old one kept as a conversion version).
- Routes GET .../sheet/convert, POST .../sheet/convert/draft and /apply.
- SheetOut.other_system, sent to a reader who may edit.
- Migration d958c0a1b2c3: character_sheet_versions.system_id (backfilled);
  a version of the old system lists as kept and cannot be put back (409).
- A GM's locks hold: a player may not convert when a lock would not carry
  over to the new sheet; the original locks are recorded in source_ref.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(webapp): convert a sheet from another system, the page and the docs (#958)
Some checks failed
CI / Does this change touch the recording path? (pull_request) Successful in 30s
CI / Which journeys does this change need? (pull_request) Successful in 29s
CI / Backend lint (ruff) (pull_request) Successful in 31s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Bot/backend version sync (pull_request) Successful in 46s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 58s
CI / Bot tests and audit (pull_request) Successful in 2m4s
CI / Feedback site tests (pull_request) Successful in 2m8s
CI / Docker image build (pull_request) Successful in 5m41s
CI / Frontend (shard 2/2) (pull_request) Successful in 10m27s
CI / Backend migration, tests, and audit (pull_request) Has been cancelled
CI / Frontend (shard 1/2) (pull_request) Has been cancelled
CI / End-to-end journeys (desktop) (pull_request) Has been cancelled
0036bc10c8
The notice on the sheet page ("This sheet is for D&D 5e": Convert into PF2e,
as a draft, or Keep it as it is), the draft's review dialog (values with a
confidence and note, edit or leave out, entries in the rules or kept as text,
Not converted with reasons), and Versions listing the old game's rows as kept.
Changelog fragment, docs/API.md, the IS-424 catalogue row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge main into feat/958-convert-sheet
Some checks failed
CI / Does this change touch the recording path? (pull_request) Successful in 30s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Bot/backend version sync (pull_request) Successful in 52s
CI / Which journeys does this change need? (pull_request) Successful in 24s
CI / Backend lint (ruff) (pull_request) Successful in 1m3s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m7s
CI / Bot tests and audit (pull_request) Successful in 2m4s
CI / Feedback site tests (pull_request) Successful in 2m50s
CI / Docker image build (pull_request) Successful in 7m37s
CI / Frontend (shard 2/2) (pull_request) Successful in 9m1s
CI / Frontend (shard 1/2) (pull_request) Successful in 11m15s
CI / End-to-end journeys (desktop) (pull_request) Successful in 21m10s
CI / Backend migration, tests, and audit (pull_request) Has been cancelled
bb13319367
Conflicts in api/sheets.js and SheetPage.jsx: Pathbuilder import (#1363)
and conversion both add actions, state and banners; both kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(webapp): the conversion draft's buttons stay in reach, long values wrap, rows line up (#958)
All checks were successful
CI / Does this change touch the recording path? (pull_request) Successful in 18s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Which journeys does this change need? (pull_request) Successful in 20s
CI / Bot/backend version sync (pull_request) Successful in 30s
CI / Backend lint (ruff) (pull_request) Successful in 32s
CI / Feedback site tests (pull_request) Successful in 1m32s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m25s
CI / Docker image build (pull_request) Successful in 2m42s
CI / Bot tests and audit (pull_request) Successful in 3m44s
CI / Frontend (shard 2/2) (pull_request) Successful in 12m42s
CI / End-to-end journeys (desktop) (pull_request) Successful in 10m26s
CI / Frontend (shard 1/2) (pull_request) Successful in 18m2s
CI / Backend migration, tests, and audit (pull_request) Successful in 1h2m22s
1674ebfc8a
From the #1365 render check: Cancel and Convert sat ~2000 px down a
phone-length draft (now the Modal's sticky foot); a long text value was
clipped in a one-line input (now a textarea); "Inspiration  : yes" had a
flex gap before the colon; and Leave out dropped onto its own line on a
phone (rows are a two-column grid now).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.