-
The build doc carries
AFFECTS_PRIMARY/AFFECTS_TOTAL. 0.6.0 made those flags decide whether a purchase contributes to a temporal characteristic, but the (legacy, curated) build doc did not emit them — so a consumer that stores the doc relationally and rebuilds from it took the class default of True and counted purchases HD keeps out of the character's totals. Measured downstream:Cobra.hdccarries a +2 DCV power markedAFFECTS_TOTAL="No", and the database rebuilt him at DCV 12 where the canonical load says 10. Emitted only when False, since True is the default on both, so a character with nothing situational produces the same doc as before. Read from the raw attributes rather than theaffect_*properties —affect_total's getter writes toaffects_total, and serialising a character is not allowed to change it.This is the flag-carrying half of 0.6.0's behaviour change. Consumers persisting the doc need columns for both, or they inherit the old answer.
The theme is one source of truth for what a build says. Three places had copies of a rule the engine already owned, and each copy was quietly wrong.
- A front door and a back door.
load_build(source, format=…)andhero.export(format=…), with formats registered (@import_format/@export_format) so adding one is additive rather than an edit to a dispatcher. Formats:hdc(bytes) andjson(dict). kirby_cost.model.modifiers—has_modifier/modifier_levels/find_modifier, the one walk answering "does this modifier bind this purchase", with recursion through containers and inheritance from an enclosing purchase (minus PRIVATE modifiers).LoadedHero.characteristic_states(xmlids)— the whole stat block from one walk of the purchases.characteristic_state(xmlid)is that call with a single xmlid.tests/test_build_doc_fidelity.py— the JSON chain held to the same attribute-level property as the.hdcchain, sharing one survey (tests/export_survey.py) and carrying its own shrink-only ledger.
- A purchase HD keeps out of the character's totals no longer contributes to
a temporal characteristic.
AFFECTS_PRIMARY/AFFECTS_TOTALis HD's own record of whether a purchase raises the characteristic or merely sits on the sheet as a situational ability, and it was not being read. Gorgon's "Tail" +20 STR isAFFECTS_TOTAL="No"with its limitation aliased "Only With Tail" — a restrainable limb — and it was making him STR 80 instead of 60 in every calculation, thrown-object damage included. Ravel's pooled "Reinforced String" +30 STR is the same shape. - JSON is now a transport encoding of the HDC element tree, not a curated
subset. The build doc was hand-written in both directions while the XML
side wrote from declared descriptors, so five fields had been lost one at a
time (TEXT, NOTES, a power's NAME, a modifier's ALIAS, AFFECTS_PRIMARY /
AFFECTS_TOTAL) — all cost-neutral, and the doc's only gate compared summed
cost. Measured over 794 corpus characters,
.hdc → hero → json → hero → .hdcwent from 0/794 clean to 791/794, the same score the.hdcchain gets, with the same twomatches_hdentries. - HD's element IDs survive the JSON trip. They were replaced by synthetic
O<n>, so a character came back as a document HD would not recognise, everyPARENTIDtarget renamed.extract_costskeysper_objectby the same rule, or those keys join to nothing.
BuildNodehad no.attrib, so a campaign's wholeRULESblock took the character down on the JSON path while the.hdcpath loaded it fine.hero_to_elementre-appends preserved elements withdeepcopyand lxml rejects aBuildNode: 15 corpus characters carrying an embedded template failed to write at all.- Statedness (which attributes the source stated, in its order) is carried
through the JSON encoding. Without it every explicitly-stated empty value
(
NAME="") was dropped and a dozen defaults per element invented. source_encodingrides in the JSON envelope; two characters came back XML-identical and byte-different without it.ChangeEnvironment.can_addremoved. It called asuper().can_addthat exists nowhere, so any call raisedAttributeError, while its docstring advertised logic its body (return True) did not contain. Nothing called it.
- Oracle parity: 656/656, residual ledger empty. None of this touches the
cost path —
characteristic_valueis untouched and remains what costs derive from. Suite 1798 passing.