Skip to content

Add the content_pack document kind: models, validator, and stamped-document round-trip - #36

Merged
mmacy merged 3 commits into
mainfrom
add-content-pack
Jul 30, 2026
Merged

Add the content_pack document kind: models, validator, and stamped-document round-trip#36
mmacy merged 3 commits into
mainfrom
add-content-pack

Conversation

@mmacy

@mmacy mmacy commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

Grows osrlib with osrlib.crawl.content_pack — the portable, geometry-free form of finished keyed content, released as 1.4.0 with osr-editor's content library (its phase 11) as first consumer, the SRD-stocking precedent.

  • ContentPackEntry is AreaSpec minus its geometry: prose, keyed encounter, room trap, treasure declaration, and features, no cells. The one validator that would otherwise be lost is restated (an entry trap must be kind="room"); the treasure-trap coupling needs no restatement because it lives on FeatureSpec itself.
  • PackSection makes the level grouping structural (panel rows, wandering's level scope, level-scoped captures all operate on it), carrying the optional WanderingSpec; entry ids stay unique pack-wide so consumers address entries by id alone.
  • ContentPack bundles the monster closure and enforces identity at construction — section ids, pack-wide entry ids, and monster ids each unique — because identity is what drops and provenance stand on.
  • The content_pack kind follows the character/party document pattern verbatim (stamp_document / check_documentmodel_validate, ValueError wrapped into ContentValidationError), with the acceptance rules documented as the pack's own: older schema versions load, newer fail with SaveVersionError, and any write re-stamps at current versions. No MIGRATIONS change — the deferral the editor spec pins for the first payload-breaking pack release.
  • validate_content_pack returns structured PackFindings (dotted snake_case codes, the Rejection discipline) instead of mirroring validate_adventure's raise-a-joined-string shape: its first consumer is a panel that lists findings, and the editor must never grow a second string-parser.
  • Housekeeping: the changelog's missing [1.3.0] link line and stale [Unreleased] compare range are repaired, and the release commit updates both this time.

Review provenance

Rubber-ducked by a skeptical reviewer against the editor's phase 11 plan (work item 1), the editor spec's content-library section, and this repo's AGENTS.md. First pass: SOLID with four non-blocking findings — one real (module docstring and changelog narrowed the closure to encounters; fixed to name the wandering rows), two recorded-deliberate (PackFinding carries no structural section id, matching the plan field-for-field; the dotted-code regex is a deliberate copy of core/validation.py's private pattern rather than a cross-boundary private import), one procedural (commit casing, resolved by this title). Re-verification confirmed the fix and re-ran the gates: SOLID.

Test plan

  • uv run pytest — 1628 passed, 58 skipped, including 21 new tests (identity validators both directions, document round-trip through JSON, accepted-older / newer-fails / re-stamp normalization, the findings matrix with bundled-monster resolution) plus the public-surface and docs-examples gates picking the module up automatically.
  • uv run ruff format --check, uv run ruff check, uv run pyright, uv run mkdocs build --strict — all clean.

Release procedure after merge: tag the merge commit v1.4.0 (the branch carries the version bump and changelog section, the 1.3.0 precedent).

https://claude.ai/code/session_01NcgHzqFDSVSmyX3MzroSQn

mmacy added 3 commits July 28, 2026 19:20
…cument round-trip (pre-review draft)

ContentPack/PackSection/ContentPackEntry are the portable, geometry-free
form of finished keyed content: AreaSpec minus its cells, sections as
level groupings with the wandering slot, and the bundled monster closure.
Identity (section ids, pack-wide entry ids, monster ids) is enforced at
construction; dangling references stay legal and validate_content_pack
reports them as structured PackFindings rather than raising — the first
consumer is a panel that lists findings, not a gate that wants pass/fail.
The content_pack kind follows the character/party document pattern, with
the acceptance rules documented as the pack's own: older schema versions
load, newer fail with SaveVersionError, any write re-stamps current.

Also repairs the changelog's missing [1.3.0] link line and stale
[Unreleased] compare range.

Claude-Session: https://claude.ai/code/session_01NcgHzqFDSVSmyX3MzroSQn
The module docstring and the changelog bullet narrowed the closure to the
entries' encounters; the validator (and the ContentPack class docstring)
union the sections' wandering-table rows too. Prose now matches the code.

Claude-Session: https://claude.ai/code/session_01NcgHzqFDSVSmyX3MzroSQn
@mmacy
mmacy merged commit 94684da into main Jul 30, 2026
5 checks passed
@mmacy
mmacy deleted the add-content-pack branch July 30, 2026 02:14
mmacy added a commit to mmacy/osr-editor that referenced this pull request Jul 30, 2026
* phase 11 backend: pack projection, source opens, the stash act, and the sidecar's third addressed map

- library.py: pack_from_adventure (address-grammar ids, key-order entries,
  feature cells stripped, wandering carried exactly when authored via the one
  shared predicate, bundled-monster closure), the derived source open across
  its three branches under the registry lock, stash_pack_for_level and the
  stash compound act on the apply_stock template, and the vetting stash open.
- sidecar: StashedPack (wrapper identity beside the stamped envelope),
  stash_counter (monotonic — copy records outlive pack deletion), the copies
  map as addressed map #3, RecordCopy (append-with-dedupe) and RemoveStashPack
  patches on the unguarded channel.
- documents.py: copies joins the cascade union walk beside notes and streams;
  with_registry exposes the open-serialization lock the source open shares.
- routes: POST /api/sources/open, /library/stash, /library/stash/open;
  stash_pack_not_found joins the error vocabulary; aids key_order goes public
  for the projection's walk order.
- osrlib floor moves to >=1.4,<2 via a git pin at the 1.4.0 release commit
  (PR mmacy/osrlib-python#36), the phase 7 interim arrangement; the three
  byte-stability fixtures re-stamp under the new engine, payloads identical.
- suites: projection goldens over small_module, the three source-open
  branches with refusals, stash capture/stale/counter/vetting/newer-engine,
  three-way cascade parity extended in place, copies dedupe and dormancy,
  stash-bearing sidecar byte-stability.

Claude-Session: https://claude.ai/code/session_01NcgHzqFDSVSmyX3MzroSQn

* phase 11 typegen: content-pack schemas, the phase 11 alias block, fixture and type-level growth

SourceState (carrying ContentPack, PackSection, ContentPackEntry, PackFinding)
and the stash request models are route-referenced and generate automatically;
the injection list stays exactly [ApiError]. AnySidecarPatch picks up
record_copy and remove_stash_pack from the union with no hand edit; the
type-level assertions and the sidecar fixture grow the three new fields.

Claude-Session: https://claude.ai/code/session_01NcgHzqFDSVSmyX3MzroSQn

* phase 11 frontend: the library panel, placement, the drop mapper, and the stash offers

- library-drop.ts: the pure drop mapping in the import-mapping discipline —
  carried-kind collision predicate, merge-by-default dropOps honoring per-kind
  replace-or-keep, sequential feature re-minting, the five-case closure
  resolution (identical-shadow stands, identical bundle reuses, definition
  twins rewrite so repeat drops mint one clone total, free ids add as carried,
  taken ids re-mint through cloneId), and the wandering copy with the same
  closure treatment.
- library-panel.tsx + library-badges.ts: the collapsible aside (SourcePagesPane
  pattern) — source opens with the sticky picker and a this-project shortcut,
  the stash list with fully-used state and the destructive delete confirm,
  sections as level rows with the wandering-copy affordance exactly when the
  section carries an entry, entries as drag sources with kind glyphs and used
  badges from the copies map.
- use-library.ts: open packs, per-pack collision memory (panel-lifetime, dying
  with its pack), the armed entry with render-time invalidation, the drop
  commit with its shadowed-shipped-definition fetch, and provenance chained
  onto the single-flight queue.
- Placement: MapCanvas grows placing/onPlace on the select tool's
  press-versus-pan deferral (panning untouched, a missed click stays armed),
  the renderer a dashed placement highlight; MapEditor arbitrates tools versus
  arming (choosing disarms, Escape disarms), resolves drops through the
  context menu's own hit-test + areaAt shape, and fires the collision dialog
  only for unanswered kinds.
- Stash offers: the import dialog's window.confirm becomes a dialog step with
  an act-accurate tally (level-scope features and wandering excluded — they
  survive the swap) and the default-on stash checkbox; clear level content
  gains the identical checkbox; both sequence capture-then-batch through the
  store's new stashLevel action and stop when the capture fails.
- vitest: 42 new tests across the mapper, the panel, the collision memory,
  and the two offers.

Claude-Session: https://claude.ai/code/session_01NcgHzqFDSVSmyX3MzroSQn

* phase 11 e2e: the milestone's deterministic half and the drag spec

The milestone spec stocks an OPD-imported level from a forge-workdir source
(temp copy — assembly writes forge's own artifacts) entirely by
click-to-place: the collision dialog fires once and the remembered choice
covers the rest, the closure carries the module's bundled template, undoing a
drop keeps the badge, a second native source contributes its wandering table,
and the swap-in replaces the level with the stash offer accepted, re-places a
displaced room from the stash pack, and publishes — the published document
carries the re-placed room, the surviving copied wandering table, and the
closure template. A separate spec drives the HTML5 drag gesture itself.

The panel toggles from the map toolbar and mounts only while open — the
fit-to-level precedent — after the standing collapsed strip proved to cost
the canvas 94px and put navigation.spec's honestly-empty paper corner back on
the grid; open packs and collision memory ride the library hook and survive
the toggle.

Claude-Session: https://claude.ai/code/session_01NcgHzqFDSVSmyX3MzroSQn

* phase 11 docs: the content-library guide, its screenshots, README, CHANGELOG

docs/guides/content-library.md joins the published guides with three new
shots (library-panel with used badges, collision-dialog, and the stash offer
photographed in clear-content-dialog, entering the set as a new slug). The
toolbar's Library toggle re-captured the five committed shots framing the
map chrome; import-dialog's pixels did not move — the offer is a separate
dialog step whose canonical photograph is the clear-content dialog.

amend phase 11 plan: the panel's collapse affordance is the toolbar toggle
(the standing strip cost the canvas 94px and tripped navigation.spec's
empty-paper guard), and the screenshot changed-surface set is corrected to
what actually moved.

Claude-Session: https://claude.ai/code/session_01NcgHzqFDSVSmyX3MzroSQn

* address rubber-duck review findings

- B1: the monster-rename parity test now performs the monster rename it
  claims to prove — set_monster_template moves the monster-keyed note while
  the area-keyed copy records stay put, through undo — instead of duplicating
  the dungeon-rename case (the behavior was correct; the test was hollow).
- N1: the clear-content stash offer gets its own suite — capture-precedes-
  batch ordering, the declined offer, the failed capture stopping the clear,
  and the new capturable gate.
- N2: the source-open conversion-busy refusal joins the exclusivity test —
  the same rule that keeps every other document act off an active workdir,
  now asserted for /api/sources/open.
- N3: ArmedEntry has one home (the library hook); the panel imports it.
- N4: the ValidationError-shaping block is payload_invalid_error in
  documents.py, shared by project open and the library's native source read
  instead of existing three times.
- N5: both stash offers say the wandering table banks 'when one is authored',
  and the clear-content offer shows only when the capture would hold
  something — a level whose only content is level-scope features gets no
  offer for an empty pack.

Claude-Session: https://claude.ai/code/session_01NcgHzqFDSVSmyX3MzroSQn

* fix the library panel clipping: pin the scroll content to the viewport width

Radix's scroll viewport wraps content in a min-width:100% display:table div
sized by the widest max-content row, so the panel's unpinned column grew to
the longest untruncated label (a section row at 384px in a 320px panel) and
everything past the edge — kind glyphs, used badges, the Place buttons —
clipped silently under overflow:hidden. The content column now pins to the
viewport's own width, so min-w-0 + truncate engage and every control stays
in frame; truncated entry and section labels carry their full text as
titles. The milestone spec grows the import dialog's own honesty check —
clientWidth === scrollWidth on the panel viewport, which read 384 vs 320
before the fix — and the library-panel shots re-capture (clear-content-dialog
rides along, picking up the review round's revised offer copy).

Claude-Session: https://claude.ai/code/session_01NcgHzqFDSVSmyX3MzroSQn

* remember each project's loaded libraries in the view state, restoring them on return

The open-source list rides the sidecar's view_state (library_sources, an
additive field — the active-level and camera species), written as sources
open and close and restored on the map surface's next mount with an ordinary
fresh open per source. Leave for the home screen or another adventure, come
back, and placement resumes at room 3 without re-opening anything; a source
that no longer opens surfaces its refusal once and is forgotten. Stash
identities (stash-<n>) and resolved paths share the one list — they can
never collide. Collision memory stays panel-lifetime and unpersisted, the
spec's pinned lifetime. Owner-directed; the plan and the spec's derived
bullet record the amendment, and the milestone spec grows the
home-and-back leg.

Claude-Session: https://claude.ai/code/session_01NcgHzqFDSVSmyX3MzroSQn
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant