An imported character sheet's rules link by name, and no id from a file is stored (export schema 20) (#1095) #1101

Merged
claude-bot merged 1 commit from feat/1095-sheet-rows-relink into main 2026-10-05 07:42:20 +00:00
Collaborator

Closes #1095.

The facts first

A sheet holds a rules record's ids in four places, not one. All are JSON; all were exported and stored as they were.

Where What reads the ids
entries[].ref resolve_entries and the evaluator: the only place a sheet is worked out from
resources.conditions.<slug>.ref nothing; only its name is used
choice values in data nothing; display uses name
source_ref (what was picked at creation and at each level) only "which class", with a fallback by name
  • Rewriting is safe. resolve_entries builds its map keyed by the stored entry_id, and the evaluator looks up by the same stored value, so the two agree whatever id is stored.
  • Sheet versions do not travel (#1053 exports only a count), so there is no history to rewrite.
  • Nothing else stores a pack or record id from a file. Definitions are matched by digest.

Summary

  • In a schema 20 file the ids are gone from all four places. Each reference carries #1094's rule_ref (pack key, kind, slug) beside what remains.
  • On import each is resolved with #1094's resolver, in the same pass and under the same 5,000-record budget and lock as the shelf's rules. The row gets this server's pack_id and entry_id, or both are cleared and the row stands on its own snapshot. rule_ref is never stored.
  • A file of schema 19 or less has every entry_id and pack_id dropped in all four places. name, slug, kind and the snapshot are kept.
  • Content rows are counted in the import's existing rule_links summary. Conditions and kept choices are linked without being counted. No sentence on the result screen changed.
  • The export names only instance packs and the campaign's own. A sheet that holds another campaign's ids, from an older import, gets no reference, so an export cannot reveal that pack's key.
  • No migration, no bot change.

What changes for someone

  • A sheet evaluates identically after a round trip with the same pack. Tested with the built-in PF2e definition, on untouched ids and after a pack reload that gives every record a new id.
  • With the pack absent, the sheet evaluates from its snapshot to the same numbers.
  • An older export imported on the server it came from loses its links. It used to find its records by id; those ids are now cleared. The remedy is to export again after updating. Sheets have been in an export only since 3 October and in no release, so this touches prerelease files only.

Not done

  • Sheets imported before this fix still hold foreign ids. They are harmless (_usable guards every read, and a test proves it) and are not cleaned up; that would need a data migration.
  • A NUL or lone surrogate inside a sheet's reference still refuses the import, because the whole-sheet check runs first. Every other malformed shape is ignored.

Changelog

  • changelog.d/1095-sheet-rows-relink.fixed.md

Test plan

  • Full backend suite at the head commit: 8001 passed, 14 skipped, 107 deselected.
  • New tests/export/test_export_sheet_links.py, 17 cases:
    • the same pack, another version with and without the record, an absent and a disabled pack;
    • another campaign's pack named by id in a hand-made file (schema 19 and 20): not stored, not resolved, its record's name nowhere in the read;
    • a schema 20 file holds no pack or record id in a sheet;
    • evaluation equality.
  • Frontend, with the sheets mount: ImportCampaignForm, src/pages/sheet, src/sheets: 720 passed; eslint 0 errors. Two earlier runs timed out in 16 and 11 different tests at a host load over 20; the clean run was at about 8.
  • Lint clean; version and changelog checks OK; all 16 coverage scripts pass.
  • Not run: the full frontend suite, journeys, bot and synthetic (CI).
  • No independent review was run.

🤖 Generated with Claude Code

Closes #1095. ## The facts first A sheet holds a rules record's ids in four places, not one. All are JSON; all were exported and stored as they were. | Where | What reads the ids | |---|---| | `entries[].ref` | `resolve_entries` and the evaluator: the only place a sheet is worked out from | | `resources.conditions.<slug>.ref` | nothing; only its `name` is used | | choice values in `data` | nothing; display uses `name` | | `source_ref` (what was picked at creation and at each level) | only "which class", with a fallback by name | - **Rewriting is safe.** `resolve_entries` builds its map keyed by the stored `entry_id`, and the evaluator looks up by the same stored value, so the two agree whatever id is stored. - **Sheet versions do not travel** (#1053 exports only a count), so there is no history to rewrite. - **Nothing else stores a pack or record id from a file.** Definitions are matched by digest. ## Summary - **In a schema 20 file** the ids are gone from all four places. Each reference carries #1094's `rule_ref` (pack key, kind, slug) beside what remains. - **On import** each is resolved with #1094's resolver, in the same pass and under the same 5,000-record budget and lock as the shelf's rules. The row gets this server's `pack_id` and `entry_id`, or both are cleared and the row stands on its own snapshot. `rule_ref` is never stored. - **A file of schema 19 or less** has every `entry_id` and `pack_id` dropped in all four places. `name`, `slug`, `kind` and the snapshot are kept. - **Content rows are counted** in the import's existing `rule_links` summary. Conditions and kept choices are linked without being counted. No sentence on the result screen changed. - **The export names only instance packs and the campaign's own.** A sheet that holds another campaign's ids, from an older import, gets no reference, so an export cannot reveal that pack's key. - No migration, no bot change. ## What changes for someone - **A sheet evaluates identically after a round trip with the same pack.** Tested with the built-in PF2e definition, on untouched ids and after a pack reload that gives every record a new id. - **With the pack absent, the sheet evaluates from its snapshot to the same numbers.** - **An older export imported on the server it came from loses its links.** It used to find its records by id; those ids are now cleared. The remedy is to export again after updating. Sheets have been in an export only since 3 October and in no release, so this touches prerelease files only. ## Not done - **Sheets imported before this fix still hold foreign ids.** They are harmless (`_usable` guards every read, and a test proves it) and are not cleaned up; that would need a data migration. - **A NUL or lone surrogate inside a sheet's reference still refuses the import**, because the whole-sheet check runs first. Every other malformed shape is ignored. ## Changelog - [x] `changelog.d/1095-sheet-rows-relink.fixed.md` ## Test plan - [x] **Full backend suite** at the head commit: `8001 passed, 14 skipped, 107 deselected`. - [x] New `tests/export/test_export_sheet_links.py`, 17 cases: - the same pack, another version with and without the record, an absent and a disabled pack; - another campaign's pack named by id in a hand-made file (schema 19 and 20): not stored, not resolved, its record's name nowhere in the read; - a schema 20 file holds no pack or record id in a sheet; - evaluation equality. - [x] Frontend, with the sheets mount: `ImportCampaignForm`, `src/pages/sheet`, `src/sheets`: `720 passed`; eslint 0 errors. Two earlier runs timed out in 16 and 11 different tests at a host load over 20; the clean run was at about 8. - [x] Lint clean; version and changelog checks OK; all 16 coverage scripts pass. - [ ] Not run: the full frontend suite, journeys, bot and synthetic (CI). - [ ] No independent review was run. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(webapp): an imported sheet's rows find their content pack again, and no id from a file is stored (#1095)
All checks were successful
CI / Does this change touch the recording path? (pull_request) Successful in 41s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m4s
CI / Bot/backend version sync (pull_request) Successful in 1m4s
CI / Backend lint (ruff) (pull_request) Successful in 1m22s
CI / Feedback site tests (pull_request) Successful in 1m56s
CI / Bot tests and audit (pull_request) Successful in 2m5s
CI / Docker image build (pull_request) Successful in 2m9s
CI / Frontend (shard 1/2) (pull_request) Successful in 9m23s
CI / Frontend (shard 2/2) (pull_request) Successful in 16m39s
CI / End-to-end journeys (phone) (pull_request) Successful in 21m18s
CI / End-to-end journeys (keyboard) (pull_request) Successful in 23m17s
CI / End-to-end journeys (desktop) (pull_request) Successful in 23m24s
CI / Backend migration, tests, and audit (pull_request) Successful in 29m52s
c6345a737b
A row of a character sheet or stat block that came from a pack, a
condition, a choice made from content, and what creation and a level-up kept
each held the pack's id and the record's, in JSON. The export wrote them and
the import stored them as given: on another server every such row read as
removed, and the ids in the database were whatever the file said.

Export schema 20. No pack or record id is written in a sheet. Wherever one
was, the file has `rule_ref` (the pack by key, scope, game system, version
and source hash; the record by kind and slug), as #1070 did for the shelf.
Only an instance pack or the campaign's own is ever named.

An import resolves them in the same pass, under the same bound and the same
rule as the shelf's rules, and writes this server's ids or none. A row with
none keeps the rest of its ref and its snapshot and reads from them. Rows
are counted in `rule_links`; the other places are linked without counting.
The sheet's one `import` version is written from the stored sheet, so it
holds the same.

A file older than 20 carries the old server's ids. They are dropped, in
every place, and its rows come in standing alone, including on the server
the file came from.

No migration. The evaluator keys a row's live record by the row's own stored
`ref.entry_id`, so rewriting it is self-consistent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-bot deleted branch feat/1095-sheet-rows-relink 2026-10-05 07:42:21 +00:00
rbrooks referenced this pull request from a commit 2026-10-05 07:42:48 +00:00
Sign in to join this conversation.
No description provided.