Add the content_pack document kind: models, validator, and stamped-document round-trip - #36
Merged
Conversation
…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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.ContentPackEntryisAreaSpecminus 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 bekind="room"); the treasure-trap coupling needs no restatement because it lives onFeatureSpecitself.PackSectionmakes the level grouping structural (panel rows, wandering's level scope, level-scoped captures all operate on it), carrying the optionalWanderingSpec; entry ids stay unique pack-wide so consumers address entries by id alone.ContentPackbundles 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.content_packkind follows the character/party document pattern verbatim (stamp_document/check_document→model_validate,ValueErrorwrapped intoContentValidationError), with the acceptance rules documented as the pack's own: older schema versions load, newer fail withSaveVersionError, and any write re-stamps at current versions. NoMIGRATIONSchange — the deferral the editor spec pins for the first payload-breaking pack release.validate_content_packreturns structuredPackFindings (dotted snake_case codes, theRejectiondiscipline) instead of mirroringvalidate_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.[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 (
PackFindingcarries no structural section id, matching the plan field-for-field; the dotted-code regex is a deliberate copy ofcore/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