A campaign's export carries quest details, character sheets and parties (export schema 17) (#991) #1053

Merged
claude-bot merged 9 commits from feat/991-export-quests-sheets into main 2026-10-03 23:54:56 +00:00
Collaborator

Part 1 of #991. Part 2 (maps and handout files) waits on the owner's size limit, Q145, so #991 stays open.

Summary

Built on 1 October on top of #1016 (schema 16, since merged). Main has been merged in, and a privacy review was run before this PR.

  • What travels (schema 17). Quest details (objectives, giver, reward) and character sheets with their versions and kept rolls. Parties and their membership over time travel too, so an imported sheet's XP is what it was. The import says which sheets stayed behind (IS-469).
  • Older files.
    • A schema 16 file, and 15 and older, import as before. Every 17-only section is ignored in a file claiming 16 or lower, the way #1016 treated wiki_bin.
    • Two deliberate changes apply to every version; both are in the changelog fragment. A printed stat block's per-line audiences are narrowed on the way in: schema 16 wrote the old server's account ids there. And a page whose stats is not a stat block is refused rather than imported broken.
  • The merge. CATALOGUE.md was rebuilt from the sources with sources/build.ps1, not hand-merged. That exposed a drift on main: #1049 hand-edited the catalogue (the account menu's Accessibility row) without touching the sources, so a rebuild would have dropped it. The same words are now in sources/headers.md and the IS-197 ledger row. With that, main's sources reproduce main's catalogue byte for byte.

Privacy review

Audiences must never widen across a round trip. One defect was found and fixed, failing-first:

  • Imported quest sizes were unbounded. Sheets and parties were bounded; a quest's objective count and text lengths were not, so a hand-made file could bring in a quest its own form would refuse to save. quest_archive.plan() now refuses past the form's limits (50 objectives, text 1000, note 2000, giver note 1000, reward 2000) before anything is written. Six new tests failed on the old code.

Checked and judged sound, with reasons in the review notes:

  • Audiences. Quest objective, sheet field and page audiences narrow to GM when they cannot be carried (mine, chosen, a non-main party, anything unparseable). They are never dropped to a wider default.
  • Ids. Every database id is new or looked up by file key. Account ids come back as nobody.
  • Sheet definitions. The import installs none.
  • Unidentified names. They reach no player.
  • Human-readable files. Nothing binned is in one.
  • Imported text. It is not rendered as HTML.
  • Validation. archive_json checks shapes, NUL, lone surrogates, NaN/Infinity and depth.

Noted, not fixed here:

  • Decompressed size is uncapped. The import reads data.json with no cap on its decompressed size; only the 100 MB upload is capped. That predates this PR and affects every schema version, and it is filed separately.
  • Row and data sizes. A sheet row's name and a sheet's data have no import limit beyond the row and roll counts.

Changelog

  • Fragment on the branch (schema 17, and the two every-version changes).

Test plan

  • Backend, full, on the merge commit: 7072 passed, 14 skipped.
  • Backend, targeted, on the final tree: tests/export, parties, title readers and campaigns: 384 passed. test_export_quests_sheets.py at HEAD: 189 passed.
  • Lint (ruff 0.4.4): clean.
  • Frontend, full, run alone: eslint 0 errors; vitest 244 files, 3784 passed, 1 skipped.
  • Checks: version sync and changelog OK. All 16 coverage scripts exit 0.
  • Not re-run on the final head: the full backend suite. The two later commits touch only quest_archive.py and its test.
  • Left to CI: bot, build, synthetic and the journeys.

🤖 Generated with Claude Code

Part 1 of #991. Part 2 (maps and handout files) waits on the owner's size limit, Q145, so #991 stays open. ## Summary Built on 1 October on top of #1016 (schema 16, since merged). Main has been merged in, and a privacy review was run before this PR. - **What travels (schema 17).** Quest details (objectives, giver, reward) and character sheets with their versions and kept rolls. Parties and their membership over time travel too, so an imported sheet's XP is what it was. The import says which sheets stayed behind (IS-469). - **Older files.** - A schema 16 file, and 15 and older, import as before. Every 17-only section is ignored in a file claiming 16 or lower, the way #1016 treated `wiki_bin`. - Two deliberate changes apply to every version; both are in the changelog fragment. A printed stat block's per-line audiences are narrowed on the way in: schema 16 wrote the old server's account ids there. And a page whose `stats` is not a stat block is refused rather than imported broken. - **The merge.** `CATALOGUE.md` was rebuilt from the sources with `sources/build.ps1`, not hand-merged. That exposed a drift on main: #1049 hand-edited the catalogue (the account menu's Accessibility row) without touching the sources, so a rebuild would have dropped it. The same words are now in `sources/headers.md` and the IS-197 ledger row. With that, main's sources reproduce main's catalogue byte for byte. ## Privacy review Audiences must never widen across a round trip. One defect was found and fixed, failing-first: - **Imported quest sizes were unbounded.** Sheets and parties were bounded; a quest's objective count and text lengths were not, so a hand-made file could bring in a quest its own form would refuse to save. `quest_archive.plan()` now refuses past the form's limits (50 objectives, text 1000, note 2000, giver note 1000, reward 2000) before anything is written. Six new tests failed on the old code. Checked and judged sound, with reasons in the review notes: - **Audiences.** Quest objective, sheet field and page audiences narrow to GM when they cannot be carried (`mine`, `chosen`, a non-main party, anything unparseable). They are never dropped to a wider default. - **Ids.** Every database id is new or looked up by file key. Account ids come back as nobody. - **Sheet definitions.** The import installs none. - **Unidentified names.** They reach no player. - **Human-readable files.** Nothing binned is in one. - **Imported text.** It is not rendered as HTML. - **Validation.** `archive_json` checks shapes, NUL, lone surrogates, NaN/Infinity and depth. Noted, not fixed here: - **Decompressed size is uncapped.** The import reads `data.json` with no cap on its decompressed size; only the 100 MB upload is capped. That predates this PR and affects every schema version, and it is filed separately. - **Row and data sizes.** A sheet row's name and a sheet's `data` have no import limit beyond the row and roll counts. ## Changelog - [x] Fragment on the branch (schema 17, and the two every-version changes). ## Test plan - [x] **Backend, full**, on the merge commit: 7072 passed, 14 skipped. - [x] **Backend, targeted**, on the final tree: `tests/export`, parties, title readers and campaigns: 384 passed. `test_export_quests_sheets.py` at HEAD: 189 passed. - [x] **Lint** (ruff 0.4.4): clean. - [x] **Frontend**, full, run alone: eslint 0 errors; vitest 244 files, 3784 passed, 1 skipped. - [x] **Checks:** version sync and changelog OK. All 16 coverage scripts exit 0. - [ ] **Not re-run on the final head:** the full backend suite. The two later commits touch only `quest_archive.py` and its test. - [ ] **Left to CI:** bot, build, synthetic and the journeys. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ledger-IS.md: both sides kept, IS-469 (this branch) between main's IS-468
and IS-470. CATALOGUE.md is rebuilt with sources/build.ps1, not merged by
hand. Main's catalogue carried a hand edit from d89b2632 (#367: the account
menu's Accessibility row on SH-3 and IS-197) that its sources did not have,
so a rebuild would have dropped it; the same words go into headers.md and
ledger IS-197, and a rebuild of main's sources alone now reproduces main's
catalogue byte for byte. The merged catalogue differs from main's only by
IS-469.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A quest's details came in from the file at any length: a hundred
objectives, or a reward of a megabyte, made by hand, would arrive as a
quest no GM could save again (the form refuses more than 50 objectives,
and its own lengths). The import now refuses them as it refuses any other
part of a quest that is not one, with the form's own numbers, which a test
holds equal to QuestDetailsUpdate's and QuestObjectiveIn's. Every quest an
export carries is within them, so no real file is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An older file's parties and award targets are not read either (#991)
All checks were successful
CI / Bot/backend version sync (pull_request) Successful in 27s
CI / Backend lint (ruff) (pull_request) Successful in 35s
CI / Feedback site tests (pull_request) Successful in 46s
CI / Does this change touch the recording path? (pull_request) Successful in 29s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Bot tests and audit (pull_request) Successful in 2m7s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m34s
CI / Docker image build (pull_request) Successful in 4m31s
CI / Frontend (shard 2/2) (pull_request) Successful in 7m39s
CI / Frontend (shard 1/2) (pull_request) Successful in 9m3s
CI / End-to-end journeys (phone) (pull_request) Successful in 14m35s
CI / End-to-end journeys (keyboard) (pull_request) Successful in 15m37s
CI / End-to-end journeys (desktop) (pull_request) Successful in 15m40s
CI / Backend migration, tests, and audit (pull_request) Successful in 24m24s
2942d6d4f0
The schema-17-only test covered a page's quest and sheet, the linked
system, how characters level and when an award was given. It now covers the
other two things 17 added, the parties and what an award was for: in a file
that claims 15 or 16 a "parties" 17 would refuse is no refusal, and an award
that says it was one character's is the party's, as it always was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-bot deleted branch feat/991-export-quests-sheets 2026-10-03 23:54:58 +00:00
rbrooks referenced this pull request from a commit 2026-10-04 06:57:14 +00:00
Sign in to join this conversation.
No description provided.