[WIP] Content extensibility: catalog seams + content-type registry foundation#28
[WIP] Content extensibility: catalog seams + content-type registry foundation#28codeGlaze wants to merge 176 commits into
Conversation
Add a Content Extensibility initiative documenting the multi-file (8-file) cost of adding content to the 5e app and a two-layer plan to reduce it: - docs/extensibility/: handoff, target architecture (registry + type-addressed option catalogs/grants), and an ADR-style decisions log. - docs/kb/content-extensibility-cross-links.md: verified, citation-backed map of current parent->child injection sites (subraces, subclasses, boons, invocations, draconic ancestries, spells) and their target catalog/grant shape, filed in the agent KB per its contribution rules. Wires both into the docs and KB indexes. Design only; no production code changed.
…agents/develop Replace the earlier docs/extensibility/ layout with the agents/develop KB structure: - docs/kb/content-extensibility.md — problem, verified cross-link map, proposed two-layer direction (content-type registry + type-addressed catalogs/grants). - docs/kb/content-extensibility-decisions.md — decisions and rejected options. - BRANCH.md — branch purpose and handoff, incl. split-commit notes for agents/develop. Verified-from-code facts are separated from the design proposal per KB rules. Cross-links existing KB docs and the homebrew-builders issue cluster. Docs only.
- .claude/summaries/2026-06-13-content-extensibility.md: session summary / handoff so the analysis and plan survive context loss (matches agents/develop convention; force-added since .claude/ is gitignored on this branch but tracked on agents/develop). - content-extensibility-decisions.md: add a chronological decision audit (how the thinking evolved and why we changed course) above the crisp D1-D8 summary. - BRANCH.md: link the summary. Docs only.
Audit the persisted formats the redesign must not break (orcbrew/plugins map, strict-entity characters, localStorage), verified from code with citations: - Derive the hard invariants (read, plugin-key, selection-key, forward compat). - Assess the proposal: Layer 1 is compatibility-neutral; Layer 2 is safe only if catalogs derive over existing storage and preserve selection/option keys. - Flag risk surfaces (content-keyword namespace requirement, selection keys as a content_reconciliation dependency, silent spec-drop on load) and the existing safety nets (import validation, conflict resolution, reconciliation). - Migration/rollback posture: aim zero-migration, prove with orcbrew + character fixtures before/after each step. Cross-linked from the design doc, summary, and BRANCH.md. Docs only.
Close the loop after the backward-compat audit: - content-extensibility.md: add the non-negotiable derive-over-existing-storage / preserve-keys constraint inline in Layer 2, and a fixture-guard note to the next step. - content-extensibility-decisions.md: add audit step 8 and D9 (backward compat is a hard constraint; target zero-migration; audit formats before planning). Docs only.
content-extensibility-plan.md: a literal, low-context-agent playbook — - Golden rules (code branch, one phase per PR, behavior-preserving, never rename keys, never commit a red gate, stop-if-ambiguous). - The exact gate: lein lint / lein test / lein fig:test. - Phase 0 golden safety test, then phased migration (subraces, subclasses, boons/invocations, the registry in micro-steps, then a new lineage type), each with files, steps, gate, done-when, and stop conditions. Cross-linked from design, summary, and BRANCH.md. Docs only.
Add a pure-JVM .cljc golden test (runs under lein test) that locks the backward- compatibility invariants the upcoming registry/catalog refactor must not break: - name-to-kw key derivation is stable (every saved character / orcbrew entry references content by these keys); - a saved strict-entity character survives a load/save round-trip idempotently with all chosen selection/option keys intact. Full suite green: 212 tests / 979 assertions / 0 failures. Update BRANCH.md status. Implements Phase 0 of docs/kb/content-extensibility-plan.md.
…braces - New leaf ns option_catalog.cljc with by-parent (generalizes the per-type group-by used to attach child options to parents). No app dependencies. - option_catalog_test.cljc pins by-parent identical to group-by. - Re-point ::races5e/plugin-subraces-map to catalog/by-parent (behavior-preserving). Gate green: lein test 213/983/0; lein lint 0 errors (7 pre-existing warnings). Implements Phase 1 of docs/kb/content-extensibility-plan.md.
Re-point ::classes5e/plugin-subclasses-map to catalog/by-parent (same mechanism proven identical to group-by in Phase 1). cljs-only delegation; lint 0 errors, JVM suite unaffected. Implements Phase 2 of docs/kb/content-extensibility-plan.md.
Add option_catalog/plugin-options: the catalog READ primitive, extracting all options of a content-key across enabled plugins. JVM-unit-tested identical to the legacy per-type (mapcat (comp vals key) plugins) extraction. Route ::classes5e/plugin-boons and ::classes5e/plugin-invocations through it — behavior-preserving, no option/selection keys or function signatures changed (zero compatibility risk). Gate green: lein test 214/989/0; lein lint 0 errors (7 pre-existing warnings). The risky positional-threading removal (3c) is deferred with a guard noted in BRANCH.md.
…on keys Extend the golden test to build pact boon and eldritch invocation options via the real .cljc pipeline (pact-boon-options / eldritch-invocation-options with real spell data) and assert the built-in option keys, homebrew option keys (name-to-kw), and the :pact-boon / :eldritch-invocations selection keys. This guards the deferred Phase 3c refactor against silently orphaning saved Warlock choices. Full suite green: 217 tests / 998 assertions / 0 failures.
… of truth) Add leaf ns content_types.cljc: a registry describing the 13 plugin-based homebrew content types (id, type-name, builder-item, spec, plugin-key, route-kw, route-seg, localStorage key) plus a by-id index. Magic-item and combat are excluded (they don't use the plugins-map / reg-save-homebrew pipeline). content_types_test.cljc audits the registry against reality: every :spec resolves via spec/get-spec, every :plugin-key satisfies the orcbrew ::e5/content-keyword contract (the orcpub.dnd.e5 namespace requirement saved libraries depend on), and identity fields are unique. Built from an agent-produced inventory; these checks auto-verified the inventory's spec/key claims. Compatibility-neutral: nothing consumes the registry yet. Gate green: 220 tests / 1092 assertions / 0; lint 0 errors. Implements Phase 4a of content-extensibility-plan.
…anch/handoff New verified context: identity keys must derive from stable ids, never display names. Folding a plugin-source suffix into a class :name re-ran name-to-kw and orphaned saved characters; fixed on feature/name-keyword-fix (::plugin-source slot, key-from-:class-key, load-time reconciler). Propagate this and the hot-sub caution everywhere: - compatibility doc: refine the key invariant (stored :key is the contract; name-to-kw is creation-time only) + cite the fix branch and reconciler shim. - design doc: two implementation rules for catalog/grant work (stable-key pass-through; layered memoized catalog subs). - decisions: add D10 (stable-key identity) and D11 (layered catalog subs). - plan: standing rules + Do-NOT entries. - BRANCH.md + session summary: coordination note with feature/name-keyword-fix. - golden test: comment clarifying the name-to-kw assertions don't endorse re-derivation. Docs/comments only; golden test still 5/25/0.
Bring the stable-key fix under the catalog/registry work so future catalog/grant phases build on it: identity keys derive from stable ids (:class-key, stored :key) not display names; option-cfg carries ::plugin-source; a load-time reconciler heals orphaned spell-selection keys. Clean auto-merge (the changes are orthogonal: catalog read-seams vs key derivation). Gate green on the merged tree: lein test 220/1092/0, lein lint 0 errors. The key-lock golden test passes, confirming the fix does not change boon/invocation/selection keys.
content-extensibility-e2e.md: scoped checklist for a full-environment (figwheel + browser + Datomic) agent to verify what the JVM-only gate here can't — the cljs test suite, the catalog read-seams (subraces/subclasses/boons/invocations still appear), the name-keyword fix behaviors, and backward compat (existing orcbrew + saved characters). Includes a feedback format. BRANCH.md: record the merge and link the checklist.
|
Item 1 — Summary (this branch): Baseline (origin/develop): identical 10 failures / 3 errors with 132 Tests / 735 Assertions. This branch adds 18 new tests / 153 new assertions, all passing — including the Phase 3a/3b/golden tests this PR cares about ( The 10/3 are pre-existing on develop:
Note: |
|
Item 2 — Homebrew subrace appears under parent race: NOT VERIFIED (fixture gap) Neither What I can report: after Covering CLJS tests that DID run and pass: none directly target the subrace seam yet (Phase 0 golden focuses on keys; Phase 3a goldens focus on boon/invocation/pact-boon keys). |
|
Item 3 — Homebrew subclass appears under parent class: PARTIAL PASS After importing This means the catalog seam for the class list works. Caveat: the fixture's subclasses ( |
|
Item 4 — Homebrew pact boon appears in Warlock L3 selection: NOT VERIFIED (fixture gap) No Mitigating CLJS test coverage on this branch (all PASSING):
If the implementing agent can add a single |
|
Item 5 — Homebrew eldritch invocation appears in Warlock invocation selection: NOT VERIFIED (fixture gap) Same situation as Item 4 — no fixture has an
Again, a single fixture invocation entry would let me drive the Warlock builder and screenshot the selection list. |
|
Item 6 — Direct verification via re-frame: No errors in the console during the toggle. App-db I did not click through to confirm the visible suffix renders in the per-slot dropdown (would require selecting a homebrew class slot first), but the code path is small and the subscription flips correctly — and the bench tests for stable keys (golden suite) all pass. |
|
Item 7 — Saved character with homebrew class loads cleanly: NOT VERIFIED (env gap), strong indirect evidence: PASS I did not load a previously-saved character; the test user has no saved warlock-with-homebrew-class fixture and authoring one through the UI was out of scope. Indirect evidence (the cljs tests that DID run and PASS on this branch and exercise the same code paths):
The |
|
Item 8 — Real Logged in as No non-figwheel errors during or after import. App-db All 5 imported items (2 homebrew classes, 2 homebrew subclasses, 1 homebrew race) reach the builder lists: Artificer + Monster Hunter appear in the class step, Custom Lineage appears in the race step. (Note: only figwheel hot-reload websocket errors appeared — Codespaces forwards them via the public URL rather than localhost; harmless for runtime.) |
|
Item 9 — Warlock with homebrew pact boon + subrace round-trips export/import: NOT VERIFIED (fixture gap) Same fixture gap as items 4 + 2: no pact boon and no subrace in Mitigating CLJS test on this branch (PASS): |
|
Item 10 — Character saved on develop loads here identically: NOT VERIFIED (env gap) I didn't have a saved-on-develop character to load. The If the implementing agent uploads any pre-branch |
|
Verification summary (live, headless Codespace)
TL;DR: No regressions detected. Catalog seams work for races + classes that are present in the fixture. The pieces I could not verify in the live UI are all covered by passing Setup notes for the next runner:
I did not modify any source or push to the branch. |
…ation The existing test/*.orcbrew fixtures lacked a subrace, a pact boon, and an eldritch invocation, blocking live e2e items 2/4/5/9. Add test/extensibility-fixtures.orcbrew: - a subrace under a BUILT-IN race (Starlit Elf -> :elf), - a subclass under a BUILT-IN class (Storm Soul -> :sorcerer), - a pact boon and an eldritch invocation. extensibility_fixture_test.clj spec-validates every item against the spec the content-types registry maps to its plugin-key, so the fixture can't silently rot. Gate green: 223 tests / 1106 assertions / 0; lint 0 errors.
|
Thanks — thorough run. Triage from my side (implementing agent): Item 1 (fig:test): Agreed, no action. The 10 failures / 3 errors are pre-existing on Items 6 & 8: Noted as PASS — thank you. Items 2/4/5/9 (fixture gap): Fixed. Pushed
Parents are intentionally built-in (SRD) so this also closes the item 2/3 caveat (injection under SRD parents, not homebrew ones).
Items 7 & 10 (env gap): No action expected from you — the reconciler logic is covered by the 22 passing Re-run 2/3/4/5/9 with the new fixture when you can and update those items. Appreciate the careful baseline diffing. Generated by Claude Code |
|
Re-verification after fixture commit Pulled the new fixture All 4 fixture items reached the app-db Final status table:
No regressions, no new console errors. Ready to clear WIP from your side once 7/10 are deemed acceptable as covered by |
A4 (floating ASI) is complete — update the data-shape example to the converged terse [amount pool] form and replace the long layer-by-layer/bug-by-bug narrative with a tight summary + pointers (the detail lives in ability-increase-spreads.md, dropdown-value-coercion.md/D32, D33). Keeps the roadmap navigable; states the remaining optional items (feat-path reconciliation, choose-between, explicit-set authoring). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
The open question wasn't the narrow "generic grant-selection vs per-feature-over-pools" (both are thin compilers to selection-cfg over the same content-pools primitive, per D30 — neither a parallel engine to delete). It was bespoke built-ins (years-stable) vs the systematic pool/grant approach. Resolution: pool/grant is the STANDARD for new/homebrew/cross-silo capability; stable bespoke constructors are kept where they aren't cross-silo and aren't hurting, and migrated only opportunistically — never a big-bang rewrite, never a pool/grant that duplicates a working bespoke path without replacing it. Commit to the direction, not a rewrite. D29 marked DECIDED in the status index; roadmap flagged-conflict #1 updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
D34: how we deprecate properly. Two categories — never-released branch scaffolding gets deleted outright; proven/released code superseded by a new standard gets #_-struck (the repo's inert-but-preserved idiom; lint-clean, restorable) under a DEPRECATED note, removed ~3 months after the date (date-based because the app is a continuously-deployed 0.1.0-SNAPSHOT with no release cadence). Migration is gated by a characterization test (D29) and tracked. backfill-ledger.md: the living artifact — migration recipe, the ledger table (empty so far), and the watch-list (the hardcoded fighting-style grant registry is the first pool/grant expansion candidate). D29 points to it for mechanics; added to the KB index. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
Add the same :ability-increases hook to ::bg5e/plugin-backgrounds that races
already have — a background's spread compiles to modifiers + the :asi selection,
merged onto the background map (additive; nil -> {}). background-option already
emits :selections/:modifiers, and the nested :asi selection carries its own
:ability-scores tag, so it routes to the ability tab. This is the 2024-PHB
"ASI via origin/background" capability — the one silo that had no ASI mechanism.
Pure new capability, nothing to deprecate.
cljs harness +3 (161 tests, only the pre-existing user-stale-user): a homebrew
background's spread flows through ::bg5e/backgrounds with the fixed CHA modifiers
and the martial floating slot. Authoring UI + E2E next.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
Generalize the race ASI authoring widget (race-ability-increase-choices ->
ability-increase-choices [item set-ai!]) so race and background share ONE form
(no duplication, per D29); the race-builder and background-builder each pass
their own setter. Add it to the background-builder.
test/e2e/background-asi.js (rendered UI, all green): a homebrew background's
spread renders ("Improvement: Background - Tide-Born"), the +2 CHA applies
automatically, the +1 is restricted to the martial pool and lands on the chosen
DEX, and CHA is unchanged by the floating pick. Race authoring E2E still green
(rename regression-checked).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
ability-increase-spreads.md: backgrounds added to the silo list + the hook citation (spell_subs.cljs:104); note the authoring widget is now silo-generic (ability-increase-choices). Roadmap A4: race/subrace/background wired and rendered-UI-proven; remaining = feat-set reconciliation (back-compat reader per D34), subclasses (rare), choose-between, explicit-set authoring. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
New reusable optional-builder-section: a toggle that reveals its body, collapsed by default (keeps non-standard fields out of the form) but open when content exists (editing isn't hidden); data only persists if you fill it in. First use: subclass ASI, which is non-standard for 5e. Subclass ASI: same one-line :ability-increases hook in ::classes5e/plugin-subclasses (compile -> modifiers + the :asi selection, merged onto the subclass map; subclass-option already emits :selections/:modifiers). Authored via the shared ability-increase-choices widget behind the toggle. Proven: cljs harness +3 (subclass spread flows through the sub with the fixed CHA modifiers + martial floating slot); rendered-UI E2E (toggle collapsed by default, reveals the widget on click, authors [[2 :cha]]). The ASI applies via the same nested-:asi mechanism already rendered-proven for races/backgrounds. Race/background E2Es regression-checked green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
ability-increase-spreads.md: subclass added to the silo list + hook + backward-compat citation; document optional-builder-section (the opt-in toggle) and that classes keep their own :ability-increase-levels. Roadmap A4: race/subrace/background/subclass all wired and rendered-UI-proven; the toggle is the reusable opt-in pattern. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
A race and a background each grant a floating +1 martial. The new test/e2e/multi-container-asi.js asserts they render as two separate widgets (one breadcrumbed "Race - Tide", the other "Background - Sea-Marked"), pick independently, and stack on the same stat (STR 15 -> 17) — each writing to its own container's entity path. Containment is by the ::entity/path the shared ability-bag-assigner reads off each selection, so it's orthogonal to programmatic form generation. Documented in docs/kb/ability-increase-spreads.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
multi-container-asi.js injects the two-silo pack into localStorage; it never exercised the export path or a re-import into a fresh browser. The new multi-container-roundtrip.js closes that: a pack carrying both a race and a background (each granting +1 martial) is exported via the real button, the browser is fully cleared with localStorage.clear(), the downloaded .orcbrew is re-imported via the real file input, and both silos return with their terse spreads intact -- then both ASI widgets render attributed to their own container and stack (STR 15 -> 17). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
The first version seeded the two-silo pack into localStorage, so the export round-tripped fabricated data and never exercised the front-end that produces it. Now the test authors BOTH silos through their real builder forms -- filling Name/Option Source and driving the actual <select> dropdowns (the string->keyword coercion layer the original bug lived in) -- for a race and a background into one pack, then exports, clears the browser with localStorage.clear(), re-imports the .orcbrew, and uses both. 15/15 checks: authoring -> export -> clear -> import -> render/attribute/stack, no seeded data. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
Summarizes this branch's work (forked from develop at d42e05d) in the top-level CHANGELOG.md format so it can be slotted in: the content- extensibility framework (field-schemas + pool/grant primitive), the ability-score-increase spread feature built on it, homebrew-source surfacing, bug fixes, and the characterization-test foundation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
feat-option-from-cfg now reads :ability-increases by shape: a vector is
the cross-silo spread (routed through compile-ability-increases, same as
races/backgrounds/subclasses), a set is the legacy feat format. The
legacy set path (#{:str :con} + the :saves? save-proficiency marker) is
left untouched -- released feat data keeps working verbatim -- while
homebrew feats can now grant fixed/floating/grouped spreads.
Full convergence onto one mechanism is deferred: the spread can't model
:saves?, so the legacy set path is kept (not deprecated), recorded in
the backfill ledger as a deliberate two-readers state.
Tests: 5 new JVM deftests (feat-legacy-set-fixed/choice, feat-legacy-
saves-marker, feat-new-spread-format, feat-new-spread-fixed-only) build
a character through both paths -- 11 tests / 41 assertions green; full
JVM suite 281/1379 green; cljs test build compiles + 162-test harness
unchanged (one pre-existing, unrelated subs_test failure). Docs + KB +
changelog updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
Two orthogonal ways to grant a saving-throw proficiency, both compiling to the one save primitive (modifiers/saving-throws), no parallel engine: - Rider: an ASI increment may carry a trailing :save ([[1 :martial :save]]) meaning "also grant the save on this increment's ability". Fixed -> unconditional save; floating -> the save rides the chosen option. Opt-in, so the default is bump-only. - Standalone :save-proficiencies [[count pool]] -> saves independent of any bump (different stat, or no bump). Single-stat = fixed; multi-stat = choose N distinct from the pool. Reuses the existing choose-a-save selection pattern + generic renderer. Wired into races/subraces/backgrounds/subclasses (one merged hook, compile-ability-grants) and feats (feat-option-from-cfg). Additive. Tests: 8 new JVM deftests build a character and read the ?saving-throws set (fixed/floating rider, rides-the-choice, opt-in default, fixed/ floating/count standalone, crash-safe, rider+standalone composed) + 2 cljs sub-wiring tests. JVM 289/1401 green; cljs 164 tests (one pre-existing unrelated subs_test failure). Docs + changelog updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
Adds the rendered surface for the two save features: - a per-row "+ save prof" checkbox on ability-increase-choices (the opt-in :save rider; preserved across Amount/To edits), - save-proficiency-choices, a silo-generic widget (How many + From) for the standalone :save-proficiencies field, wired into the race and background builders and the subclass non-standard toggle (alongside ASI). E2E: - save-grants-authoring.js: the toggle and the standalone widget emit the terse data ([[1 :martial :save]], [[1 :mental]]) through the real selects/checkbox. - save-grants-use.js: in the rendered builder the rider's save rides the chosen bump (DEX save flips false->true on the pick) and the standalone choice (Proficiencies tab) flips the WIS save. Both read off the live Saving Throws table. Existing race-builder-asi / subclass-asi-toggle E2Es still green (the checkbox is additive). Docs updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
- roadmap A4: mark feat-path reconciliation and the save tools (rider + standalone) DONE; note multi-silo containment and same-stat collapse; trim the now-stale "remaining" list. - README index: ability-increase-spreads entry now mentions the save tools, containment, and the feat dual-format reader. - branch-changelog: correct the feat entry — the spread now models saves via the rider, so only the released-data migration remains (ledger). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
Authoring-time guidance only (no mechanics change): save-coverage-warnings (pure, over a content entry's own data) flags when a creator authors redundant or overlapping save grants in the feat/race/etc builder they're working in: - the same stat granted a fixed save more than once (the duplicate is a no-op — saves are a set), - a fixed save also reachable from a choice pool, or - two choice pools that overlap (a player could pick the same save twice). save-coverage-notes renders these inline under the save widgets in the race/background builders and the subclass non-standard toggle. This is the builder-scoped version of "help prevent AND explain" — it only inspects the entry being authored, so it needs no class/character context and changes no runtime behavior (overlap still collapses safely). Tests: 5 JVM deftests on the helper (clean / duplicate-fixed / fixed-in- choice / overlapping-pools / ignores non-:save riders); the authoring E2E now asserts the note appears only once an overlap is authored. Full JVM suite 294/1407 green. Docs + changelog updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
save-coverage-ignores-non-save-riders used :ability-increases [[1 :martial]] + :save-proficiencies [[1 :wis]] — disjoint pools, so it returned empty whether or not the :save guard existed (mutation-confirmed: removing the guard left it green). Switched to the SAME stat on both (a +1 WIS bump with no :save, alongside a fixed WIS save): correct code returns empty, but if the :save guard regresses the bump is miscounted as a second WIS save and the test fails. Now protective (mutation-confirmed red). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
…three)
Three builder surfaces each rendered a list of item problems differently:
simple-content-builder (validate-fields), selection-builder's duplicate/
empty-name warnings, and save-coverage-notes. Same data shape (a seq of
human-readable strings), three visual treatments.
Unify the RENDER into builder-notes [problems {:severity :error|:advisory}]
(:error = blocking "Fix before saving:" list; :advisory = ⚠ guidance) and
route all three summaries through it. Producers stay separate (they compute
different things); selection-builder's per-row highlighting stays bespoke.
App builds; save-grants-authoring E2E (advisory path) and the cljs harness
(simple-content-builder-test, the :error path) green — only the pre-existing
unrelated subs_test failure remains. Spec carried in the builder-notes
docstring and content-extensibility-direction.md (reusable builder widgets).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
Designs out the "false -> nil on repeated clicking" toggle bug at the source: today comps/checkbox is display-only and every toggle's value is hand-computed per caller, with no boolean-enforcing element and no :boolean field type — so a stray (when on? true) or dissoc-on-off writes nil. render-builder-field gains a :boolean type that is nil-immune BY CONSTRUCTION: it reads (true? v) (only literal true is on, so nil/absent/ garbage read as off) and writes bf/toggle-next, which returns (not (true? v)) — always a real boolean. No click sequence, stale read, or malformed prior value can store nil. field-value-pred gains :boolean -> boolean? so a present non-boolean value is caught by the shared validator (nil/absent = off, fine). Boolean DATA fields route through this one widget instead of hand-rolled checkboxes. (Set-membership toggles like the feat :saves? marker and the :save rider store presence/absence, a different shape, and aren't at risk.) Tests (builder_fields_test): toggle-next is always a literal boolean from any prior value; a 50-click hammer starting from nil never yields nil; the validator accepts true/false/absent and rejects stringy/numeric values. Full JVM suite 297/1474 green; app builds. Documented in content-extensibility-direction.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
Reconciles with claude/custom-class-source-error-2k5ykd, which already solves toggle nil-safety canonically: common/toggle-in / common/toggle-flag (path-safe, collection-preserving, self-healing) + strip-export-blanks. Their root cause is deeper than my bf/toggle-next handled — a toggle path landing on a MAP did (not map) = false, collapsing the collection — so mine was a weaker, parallel mechanism. Backed out the parallel bits from the prior commit: - remove bf/toggle-next and the :boolean case in render-builder-field, - remove :boolean from field-value-pred, - remove the toggle-hardening tests. Nothing on this branch declares a :boolean field, so this is unused infrastructure — no need to cherry-pick their in-flight helper (which also converts the ~20 content-prop toggles they asked to leave alone). Left a note in builder_fields.cljc / render-builder-field so the :boolean type is added later routing THROUGH their helper, not a fresh parallel fn. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
You were right that the branch has a toggle the other branch's sweep won't cover: the ability-increase-choices "+ save prof" checkbox is new here, so claude/custom-class-source-error-2k5ykd's toggle conversion won't touch it. Verified it against the bug class and it's safe by construction — it flips a boolean (save? = (= :save flag)) and writes a well-formed vector via full replacement, never (not <collection>), so it can't collapse anything. To PROVE rather than assert (and to stop it being hand-rolled inline), extracted the toggle to opt/toggle-increment-save: rebuilds the increment canonically ([amount pool] <-> [amount pool :save]), idempotent and self-healing (a malformed longer increment normalizes back), never nil. This is presence-of-a-keyword-in-a-vector, a different shape from a boolean flag in a map, so it deliberately does NOT use common/toggle-in — noted in the docstring. Tests: toggle-increment-save stays canonical / self-heals; a 50-click hammer never loses amount+pool or goes non-canonical. E2E authoring still green. (optional-builder-section, the only other branch toggle, is a transient r/atom — not persisted, not at risk.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
…hesis Two guardrail follow-ups from the cross-branch design review: 1. Fan-out crash-safety (guardrail 8: seed realistically-messy data). A new messy-pak-survives.js E2E seeds a pack with a malformed race (junk ASI/ save entries) and surfaced a real bug my earlier REPL check had masked with try/catch: an entry with a nil pool ([:bad], []) reached resolve-pool and NPE'd on (name nil). In the races sub (mapped over every race) that crashes the whole pack. Tightened both compilers' guard from filter vector? to a shared pool-entry? (vector + numeric amount + keyword/collection pool) so one junk entry is skipped, not fatal. JVM messy-tolerance test added; the E2E now proves the pack loads and the good race still works, no page errors. 2. Convergence note (guardrail 1). Strengthened the builder_fields.cljc note so backing out my toggle pieces didn't lose them: the merged :boolean primitive must combine BOTH halves — their path-safe traversal + self-heal AND my defensive (not (true? v)) read + boolean? validation (collection- preservation alone still reads garbage as "on"; leaf-read alone still collapses a map) — plus strip-export-blanks and the save-subset-of-load guard. Route the eventual :boolean field through that ONE primitive. Full JVM suite 297/1514 green; app builds; messy-pak E2E green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
…entry? follow-up New docs/kb/data-safety-layers.md turns the cross-branch toggle-nil review into a standing design rule: robustness is four layers, not "harden vs self-heal" — prevent (shape/type), harden (boundary/read), heal (write/ repair, only with an unambiguous target), surface (migration; never silent- drop meaningful data). Includes the rule that picks between them by lifecycle, the save-subset-of-load + diagnosable-rejection invariants, this codebase's mechanisms mapped to each layer, and the two anti-patterns (silent-drop-on-meaningful-data, speculative-heal). Indexed under Process & infrastructure alongside verification-discipline. Tracked follow-up (guardrail 6): pool-entry? skips malformed entries silently for fan-out crash-safety (correct at runtime) but the authoring form should report "N entries ignored". Recorded in the doc's follow-ups section and a marker at the pool-entry? docstring. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
compile-ability-increases used mod5e/race-ability for EVERY fixed increment. race-ability writes ?race-ability-increases, and the character builder's ability breakdown shows that as the "race" column (with "other" = total - race - subrace). So a background/subclass/feat fixed +N rendered as a RACIAL increase and cancelled out of "other" -- a real bug shipped for backgrounds and subclasses (floating increments were never affected; they use level-ability-increase). Make fixed attribution silo-aware: compile-ability-grants takes :attribution (:race default | :subrace | :general); compile-ability- increases takes the resolved :fixed-modifier. Non-racial silos pass :general -> mod5e/ability (neutral, shows under "other"). Wiring: backgrounds/subclasses :general, subraces :subrace, races default, feat spread path :general. Totals unchanged; only the source column. Tests: JVM fixed-asi-attribution-is-per-silo builds a character and asserts the race/subrace bucket per silo; the two cljs tests that asserted "2 modifiers" for background/subclass (which WAS the bug -- the 2nd was the race-column write) now assert 1 neutral modifier. Full JVM 298/1521 green; cljs 164 (only the pre-existing subs_test failure). Also updates the backfill-ledger: the feat migration's attribution hazard is now resolved by this fix; only the save-compat option-key hazard remains (post-merge, needs a key reconciliation + characterization test). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
The attribution fix for FIXED increments didn't reach floating, because compile-ability-increases decided attribution in two places and only the fixed branch took the :fixed-modifier. slot-opts (floating) still hardcoded level-ability-increase, so a chosen floating +N landed in ?level-ability-increases -- a bucket the per-source ability breakdown never shows. Result: a picked floating ASI raised the total but appeared in NO column, and a subrace with a floating ASI showed no "subrace" column at all (reproduced in the rendered builder: STR 15->16 with race 0 / other 0). A floating pick, once chosen, IS a fixed increase on the chosen ability, so it should attribute the same way. Route the floating slot options through the same :fixed-modifier as fixed increments -- attribution is now decided in ONE place and covers both. Total unchanged; option keys (asi-<idx>-<ability>) unchanged, so no save-compat impact. ?level-ability-increases has exactly one reader (the total), so moving picks out of it is total-neutral. Tests: JVM floating-asi-attribution-follows-the-silo (race pick -> race col, subrace -> subrace col, general -> not-race). E2E multi-source-floating- attribution.js: three concurrent floating sources (race +2/+1, subrace +2/+1, background +1/+1/+1) render as separate breadcrumbed widgets (7 slots, not merged), and picking the subrace slot makes the subrace column appear (was absent). Existing floating E2Es (multi-container, save-grants-use, exact- spread) still green. Full JVM 299/1528; cljs 164 (pre-existing subs_test only). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
Three edge cases raised in review: 1. Deselect a floating ASI (rendered-verified, no code change needed): the bag-assigner's on-change dispatches :decrease-ability-value when a slot returns to "— choose —". Confirmed live: STR 15->16 on pick, back to 15 on deselect, and the race-column entry clears (no stale state); changing STR-> DEX moves the bonus cleanly. 2/3. Multiple feats with floating pools + a static-ASI feat. New JVM test multiple-feats-floating-and-static-stay-separate-and-attribute-to-other: two feats each with a floating ASI both key their slot asi-0-<ability>, but selected together under ONE Feats multiselect they do NOT collide -- each STR pick applies and stacks (10+1+1=12) because entity paths disambiguate. A static +2 CON feat applies alongside (10+2=12). All feat ASIs attribute to "other" (general), none to the race column. (The rendered multi-feat path is level-gated -- feats need ASI-level slots; the "Homebrew" override that unlocks unlimited feats is #_-disabled in this build -- so the multi-feat containment is proven at the entity/build level, where the actual collision concern lives. The rendering of N nested :asi selections is already rendered-proven by multi-source-floating-attribution.) Ran 30 tests / 194 assertions, 0 failures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
Correction: the Homebrew override is NOT disabled -- I misread a dead old global-homebrew def (#_ homebrew-plugin) as "the feature is off." The live override is the per-section beer-stein mug (character_builder.cljs:644, toggle-homebrew) that lifts the level/slot restriction on a section. New test/e2e/multi-feat-floating.js uses it to prove #2/#3 in the rendered builder: feats are locked (opacity-5) at level 1, the Homebrew mug unlocks them, two floating-ASI feats render as separate breadcrumbed widgets, their STR picks do NOT collide despite identical asi-0-* keys (stack 15->18), a static +2 CON feat applies immediately (13->15), and feat ASIs stay out of the race column. Corrected the changelog note accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
…rface) Closes the tracked guardrail-6 follow-up. The compilers silently skip malformed :ability-increases/:save-proficiencies entries (pool-entry?) for fan-out crash-safety -- correct at runtime, but it hides data loss from a creator editing imported/hand-edited content. - opt5e/ignored-entry-warnings (pure, cljc): counts the entries each field would drop and returns human-readable notes. - views/ability-save-notes (renamed from save-coverage-notes): renders both the ignored-entry notes (:error) and the save-coverage notes (:advisory) under the ASI/save widgets, via the shared builder-notes. Tests: JVM ignored-entry-warnings-surface-what-compile-drops (clean/junk/ singular-grammar/nil). E2E ignored-entry-note.js: editing a race whose :ability-increases is [[1 :cha] "junk" [:bad]] -> the builder does NOT crash and shows "2 ability-increase entries are malformed and will be IGNORED". Full JVM 301/1537 green; cljs 164 (pre-existing subs_test only). Docs + data-safety-layers follow-up marked done. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
…ght-04xhdz develop's 29-commit bundle (toggle-in/toggle-flag, content-type→spec registry, keyword-trap import repair, orcbrew validation rename, error boundaries) reconciled with this branch's ASI/save-proficiency work. Key reconciliations: - events.cljs: kept our generative register-homebrew-content! (drops the now-redundant explicit Boon save); fixed it to develop's 5-arg reg-save-homebrew (spec derived from content-specs registry, not passed). Import clean-store path: took develop's multi-plugin store + threaded our strict-mode :strict-unfilled errors through the shared import-log. - import_validation.cljs -> orcbrew_validation.cljs rename: our non-ASCII and dedup fixes were already in develop's rewrite; updated ns refs. - content-specs registry: added ::e5/draconic-ancestries (our field-schema type, unknown to develop) to save-specs + both audit tables. - builder-fields fields->spec: recomposed as spec/keys + field checks so it rejects the keyword-trap (:key must start with a letter) AND is diagnosable (explain-data names :name/:key) — matching develop's per-type specs. - docs: kept our docs/kb/ working notes; aligned docs/README to develop's human-facing form; unioned test_runner. Verified: JVM 343/1969, cljs 230/652, 0 failures (develop's previously- failing subs_test now green); app builds clean; our ASI attribution, multi-feat, and ignored-entry E2Es pass against the merged app. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
Status: DRAFT / WIP — not for merge. Opened as the coordination + e2e-feedback channel for the content-extensibility work. Please leave testing findings as PR comments (see "Verification ask" below); I'm subscribed and will respond.
What this is
A staged, behavior-preserving start on reducing the multi-file cost of adding homebrew content (the "~8 files to add one builder" problem). Full design/rationale lives in the KB docs added here:
docs/kb/content-extensibility.md— design (registry + type-addressed catalogs/grants)docs/kb/content-extensibility-decisions.md— decision audit (D1–D11)docs/kb/content-extensibility-compatibility.md— backward-compat audit + invariantsdocs/kb/content-extensibility-plan.md— phased, gated implementation playbookdocs/kb/content-extensibility-e2e.md— the live verification checklist (run this)What's implemented (each behavior-preserving, JVM-gated green)
extensibility_golden_test.cljc: locks the compat invariants (name→key derivation; saved-character round-trip; boon/invocation option + selection keys).option_catalog.cljc(by-parent); subraces + subclasses routed through it (proven identical togroup-by).option_catalog/plugin-options(catalog read primitive); boons + invocations routed through it.content_types.cljcregistry (13 plugin-based types) + an audit test that verifies every:specresolves and every:plugin-keysatisfies the orcbrew::e5/content-keywordcontract. Nothing consumes it yet (no runtime effect).Gate on this branch:
lein test220 tests / 1092 assertions / 0 failures;lein lint0 errors.Stacked on the name-keyword fix
This branch includes a merge of
feature/name-keyword-fix(the same stable-key fix as #27: identity from:class-key/stored:keynot display names,::plugin-sourceslot, load-time reconciler) so the catalog work builds on it. The diff will shrink once #27 lands ondevelop.Not done yet
Verification ask (why this is a draft)
The CI/JVM gate here does not execute the ClojureScript re-frame subscriptions or the running app. Please run
docs/kb/content-extensibility-e2e.mdin a full dev environment (figwheel + browser + Datomic) and leave a PR comment per checklist item (PASS/FAIL + console/screenshots). Focus: homebrew subrace/subclass/boon/invocation still appear in the builder; the source-suffix preference doesn't orphan keys; existing.orcbrewlibraries and saved characters load and round-trip unchanged.https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
Generated by Claude Code