An imported campaign's rules link back to their content pack (export schema 19) (#1070) #1094

Merged
claude-bot merged 1 commit from feat/1070-rule-cards-relink into main 2026-10-05 05:26:31 +00:00
Collaborator

Closes #1070.

The facts first

The issue was written from a summary, so the premise was checked before building. It holds: the link did not travel. prep_shelf_archive.py said so in its docstring, and the round-trip tests asserted reference_entry_id is None after an import.

Campaign row Column Before
A rule card on a session's shelf shelf_cards.reference_entry_id snapshot only, no link after import
A pinned rule shelf_pins.reference_entry_id snapshot only, no link
A rule on the prep sheet prep_items.reference_entry_id snapshot only, no link
A wiki page made from a rule ("Based on …") lore_entries.source_reference_entry_id not written at all
A character sheet's content rows entries[].ref in JSON written with the old server's ids; not changed here

A pack and a record can be named across servers without a migration:

  • a pack by content_packs.key, unique among instance packs and kept when the same source is loaded again;
  • a record by (kind, slug) within its pack, which survives a reload where its id does not.

Summary

  • In the file: a rule carries {"pack": {"key", "scope", "system_key", "version", "sha256"}, "kind", "slug"}. No id is written or read. Its own content travels as before, so it reads correctly with or without the link.
  • On import, looked up only among active instance packs with the same key and game system:
    • same pack and version (equal sha256, or equal stated version): linked;
    • a different or unstated version: linked if a record of that kind and slug exists; otherwise the rule stands alone;
    • no such pack: the rule stands alone.
  • Never linked: a pack that is disabled, still importing, failed, of another game system, under an undecided rightsholder notice, or a campaign's own. The report counts all of these as "no such pack", so it never reveals that a taken-down pack exists.
  • A malformed reference is ignored; it never refuses the import.
  • The import's answer gains rule_links: {relinked, other_version, left_alone, no_pack, no_entry}, shown on the result screen.
  • Schema 19. A file claiming 18 or less links nothing, with or without the new fields.
  • No migration, no bot change.

Limits

  • A pack's key comes from its manifest, or from its file name where the format has none. The same pack loaded under a different file name on another server will not match. docs/OPERATIONS.md says so.
  • The report counts rows. A rule pinned across four sessions is five rows, and pages are included, so the screen says "rules".
  • At most 5,000 distinct records are looked up per import. Rules past that stand alone and are counted as "no such pack".
  • "Under a notice" means a notice that is received or being reviewed. Once a notice is decided, the pack's own status is what counts.

Not done here, to be filed

Character sheet content rows keep the old server's pack_id and entry_id. sheet_service.resolve_entries looks records up by that stored id, so an id from a file is honoured there today, behind its own _usable guard. Rewriting those references with the new resolver touches what the sheet evaluator keys on.

Changelog

  • changelog.d/1070-rule-cards-relink.added.md

Test plan

  • Full backend suite at the head commit: 7984 passed, 14 skipped, 107 deselected.
  • New tests/export/test_export_rule_links.py, 28 cases: the three import cases; a disabled or noticed pack; a malformed reference; a schema 18 file with and without the new fields; the report's counts; nothing linked by an id from the file. "Another server" is a reload of the pack after the export, which gives every record a new id.
  • Three existing assertions changed meaning: two reference_entry_id is None checks now expect the link, and "two imports share nothing" allows the shared pack record.
  • Lint clean; version and changelog checks OK; all 16 coverage scripts report unclassified: none.
  • Frontend: ImportCampaignForm, NewCampaign, AdminImport: 62 passed; eslint 0 errors.
  • Not run: the full frontend suite, journeys, bot and synthetic (CI). The new banner was not looked at in a browser.
  • No independent review was run on this one.

🤖 Generated with Claude Code

Closes #1070. ## The facts first The issue was written from a summary, so the premise was checked before building. It holds: the link did not travel. `prep_shelf_archive.py` said so in its docstring, and the round-trip tests asserted `reference_entry_id is None` after an import. | Campaign row | Column | Before | |---|---|---| | A rule card on a session's shelf | `shelf_cards.reference_entry_id` | snapshot only, no link after import | | A pinned rule | `shelf_pins.reference_entry_id` | snapshot only, no link | | A rule on the prep sheet | `prep_items.reference_entry_id` | snapshot only, no link | | A wiki page made from a rule ("Based on …") | `lore_entries.source_reference_entry_id` | not written at all | | A character sheet's content rows | `entries[].ref` in JSON | written with the old server's ids; **not changed here** | **A pack and a record can be named across servers without a migration:** - a pack by `content_packs.key`, unique among instance packs and kept when the same source is loaded again; - a record by `(kind, slug)` within its pack, which survives a reload where its id does not. ## Summary - **In the file:** a rule carries `{"pack": {"key", "scope", "system_key", "version", "sha256"}, "kind", "slug"}`. No id is written or read. Its own content travels as before, so it reads correctly with or without the link. - **On import**, looked up only among active instance packs with the same key and game system: - same pack and version (equal `sha256`, or equal stated version): linked; - a different or unstated version: linked if a record of that kind and slug exists; otherwise the rule stands alone; - no such pack: the rule stands alone. - **Never linked:** a pack that is disabled, still importing, failed, of another game system, under an undecided rightsholder notice, or a campaign's own. The report counts all of these as "no such pack", so it never reveals that a taken-down pack exists. - **A malformed reference** is ignored; it never refuses the import. - **The import's answer gains** `rule_links: {relinked, other_version, left_alone, no_pack, no_entry}`, shown on the result screen. - **Schema 19.** A file claiming 18 or less links nothing, with or without the new fields. - No migration, no bot change. ## Limits - **A pack's key comes from its manifest, or from its file name** where the format has none. The same pack loaded under a different file name on another server will not match. `docs/OPERATIONS.md` says so. - **The report counts rows.** A rule pinned across four sessions is five rows, and pages are included, so the screen says "rules". - **At most 5,000 distinct records are looked up per import.** Rules past that stand alone and are counted as "no such pack". - **"Under a notice" means a notice that is received or being reviewed.** Once a notice is decided, the pack's own status is what counts. ## Not done here, to be filed **Character sheet content rows keep the old server's `pack_id` and `entry_id`.** `sheet_service.resolve_entries` looks records up by that stored id, so an id from a file is honoured there today, behind its own `_usable` guard. Rewriting those references with the new resolver touches what the sheet evaluator keys on. ## Changelog - [x] `changelog.d/1070-rule-cards-relink.added.md` ## Test plan - [x] **Full backend suite** at the head commit: `7984 passed, 14 skipped, 107 deselected`. - [x] New `tests/export/test_export_rule_links.py`, 28 cases: the three import cases; a disabled or noticed pack; a malformed reference; a schema 18 file with and without the new fields; the report's counts; nothing linked by an id from the file. "Another server" is a reload of the pack after the export, which gives every record a new id. - [x] Three existing assertions changed meaning: two `reference_entry_id is None` checks now expect the link, and "two imports share nothing" allows the shared pack record. - [x] Lint clean; version and changelog checks OK; all 16 coverage scripts report `unclassified: none`. - [x] Frontend: `ImportCampaignForm`, `NewCampaign`, `AdminImport`: `62 passed`; eslint 0 errors. - [ ] **Not run:** the full frontend suite, journeys, bot and synthetic (CI). The new banner was not looked at in a browser. - [ ] No independent review was run on this one. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(webapp): an imported campaign's rules find their content pack again (#1070)
All checks were successful
CI / Does this change touch the recording path? (pull_request) Successful in 17s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Bot/backend version sync (pull_request) Successful in 1m6s
CI / Backend lint (ruff) (pull_request) Successful in 1m8s
CI / Feedback site tests (pull_request) Successful in 1m34s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m49s
CI / Bot tests and audit (pull_request) Successful in 2m40s
CI / Docker image build (pull_request) Successful in 3m31s
CI / Frontend (shard 2/2) (pull_request) Successful in 9m45s
CI / Frontend (shard 1/2) (pull_request) Successful in 11m59s
CI / End-to-end journeys (phone) (pull_request) Successful in 13m9s
CI / End-to-end journeys (keyboard) (pull_request) Successful in 14m12s
CI / End-to-end journeys (desktop) (pull_request) Successful in 14m21s
CI / Backend migration, tests, and audit (pull_request) Successful in 34m17s
8297c069fb
A rule on the shelf, a pinned rule, a rule in a prep sheet and a wiki page
made from a rule each point at a record of a content pack. An export carried
no part of that link, so after an import every one of them stood alone.

Export schema 19. A shelf card, a pin and a prep item of a rule carry
`rule_ref`, and a page made from a record carries `based_on_rule`: the pack
by its key, scope, game system, version and source hash, and the record by
its kind and slug. Never a database id.

An import looks for each among this server's instance packs that are in use
and links the row where the pack has a record of that kind and slug, the
same version or another. Otherwise the row comes in as it always did, with
its own copy and no link. A link is never made to a pack that is disabled,
to one named by a notice that is received or being reviewed, to a pack of
another game system, or to a campaign's own pack. A reference that is not
one is ignored and never refuses the import; nothing of one is stored. A
file older than 19 is read as it was, and the new keys in one are not read.

The answer counts what was linked and what was not, and why (`rule_links`),
and the import's result screen says it in words (IS-491).

No migration: `content_packs.key` (unique per instance) and
`reference_entries (pack_id, kind, slug)` were already the stable names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-bot deleted branch feat/1070-rule-cards-relink 2026-10-05 05:26:32 +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.