[WIP] Content extensibility: catalog seams + content-type registry foundation#28
[WIP] Content extensibility: catalog seams + content-type registry foundation#28codeGlaze wants to merge 186 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 ( |
Tight note on why a ::t/spread selection routes to the bag assigner while everything else keeps the increment UI. Completes the commenting pass over the ASI spread code (compile/helpers, widget, wiring, authoring all already documented; full spec in docs/kb/ability-increase-spreads.md). All three ASI E2Es green after the dead-code cleanup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ls9kkNatBXcX2b2T1ydzkZ
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
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