Skip to content

Commit c30092f

Browse files
pdbethkeclaude
andcommitted
feat(loader): REQUIRED template adders attach as HD's getAssignedAdders() does
GenericObject.getAssignedAdders() clones every REQUIRED available adder not already assigned and marks it selected -- generic, on READ. HD-written files state these adders, so the corpus never exercised the attach; a hand-written element without them (the sink's Damage Over Time) got a different value (+1 vs HD's +3 1/4: base 1 + INCREMENTS first option 0.25 + TIMEBETWEEN first option 2.0). The engine now materialises them at the end of _build_modifier -- AFTER the element's own adders (attaching before them double-counted 24 corpus objects) -- resolving the SAME template variant the modifier application used (MINDLINK's nested INVISIBLE has no required adders; the global one does), priced at the first option, alias wired. Sink cost ledger 10 -> 8; the last prototype cell's reason now carries the loaded-vs-prototype cross-evidence. Suite 1,728; parity and display intact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PrVDXgpDfQpzEGErZTLjEs
1 parent 5b57d51 commit c30092f

3 files changed

Lines changed: 40 additions & 4 deletions

File tree

kirby_cost/io/hdc_loader.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1872,6 +1872,44 @@ def _build_modifier(self, elem, parent: GenericObject) -> Optional[Modifier]:
18721872
option_id=adder_opt if adder_opt else None)
18731873
mod.assigned_adders.append(adder)
18741874

1875+
# HD attaches every REQUIRED available adder the moment the list is
1876+
# read -- GenericObject.getAssignedAdders() clones any required
1877+
# available adder not already assigned and marks it selected -- and
1878+
# the attached adder prices at its FIRST option (the sink's Damage
1879+
# Over Time: base 1 + INCREMENTS "2" 0.25 + TIMEBETWEEN SEGMENT 2.0 =
1880+
# HD's +3 1/4, both first options). HD-written files always state
1881+
# these adders explicitly, so the corpus never exercises the attach;
1882+
# a hand-written element without them must still get HD's answer.
1883+
# Runs AFTER the element's own ADDER children are parsed -- attaching
1884+
# before them duplicated every stated required adder (24 corpus
1885+
# failures, caught 2026-08-31).
1886+
# Resolve the SAME template variant the modifier application used --
1887+
# a nested definition (MINDLINK's own INVISIBLE has NO required
1888+
# adders) beats the global one (whose EFFECTSTARGET/EFFECTSOTHER are
1889+
# REQUIRED); attaching from the global variant garbled nine MindLinks.
1890+
tmpl_late = None
1891+
provider = self._provider_in_use
1892+
if parent is not None and hasattr(provider, "get_nested_modifier"):
1893+
tmpl_late = provider.get_nested_modifier((parent.xmlid or "").upper(), xmlid)
1894+
if tmpl_late is None:
1895+
tmpl_late = self._get_template_data(xmlid)
1896+
for adder_xmlid, adder_tmpl in (getattr(tmpl_late, "adders", None) or {}).items():
1897+
if not getattr(adder_tmpl, "required", False):
1898+
continue
1899+
if GenericObject.find_object_by_id(mod.assigned_adders, adder_xmlid) is not None:
1900+
continue
1901+
adder = Adder()
1902+
adder.xmlid = adder_xmlid
1903+
adder._required = True
1904+
adder._selected = True
1905+
adder._alias = getattr(adder_tmpl, "alias", "") or getattr(adder_tmpl, "display", "")
1906+
adder.apply_adder_template(adder_tmpl)
1907+
opts = getattr(adder_tmpl, "options", None) or {}
1908+
if opts and not adder.base_cost:
1909+
first_id, first = next(iter(opts.items()))
1910+
adder.base_cost = getattr(first, "base_cost", 0.0) or 0.0
1911+
adder.option_id = first_id
1912+
mod.assigned_adders.append(adder)
18751913
return mod
18761914

18771915
def _build_adder(self, elem) -> Optional[Adder]:

tests/fixtures/included_known_gaps.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
"_comment": "SHRINK-ONLY. Cells where the engine's included() disagrees with Hero Designer's, with what each side said. Written once as the 2026-08-29 baseline before any fix; every later commit may only remove entries. The target is empty.",
33
"_baseline": "3770 of 8395 cells disagree (2026-08-29); now 1 remain (2026-08-30)",
44
"gaps": {
5-
"REDUCEDEND-on-SHAPESHIFT": "HD allows; unexplained. The visible Java path (ReducedEND.java:200-210) refuses on getEndUsage()==0, HD's own echo for this prototype says end_usage=0 and active_cost=0.0, and neither Shapeshift.java nor SenseAffectingPower.java overrides getEndUsage(). Suspect: the o.clone()/setParent(null) at ReducedEND.java:130-132 changing what the clone's getEndUsage() sees. Needs a live-HD trace; the last prototype cell (2026-08-30)."
5+
"REDUCEDEND-on-SHAPESHIFT": "HD allows; mechanism unlocated after a full read of ReducedEND.java:124-210 -- the visible path refuses on getEndUsage()==0, HD's own echo for this prototype says end_usage=0/active_cost=0.0, no getEndUsage/getActiveCost override exists in Shapeshift/SenseAffectingPower, and clone() re-selects the same (null) option. Cross-evidence: on the LOADED sink Shape Shift (stateful fixture, object 20260830000064) HD reports end_usage=1/active=8.0 and allows -- consistent with its rule -- and the engine agrees there. The divergence is confined to HD's PROTOTYPE tier. Last prototype cell; needs a live-HD debugger trace (2026-08-31)."
66
}
77
}

tests/fixtures/validation_sink_cost_gaps.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
{
22
"_comment": "Shrink-only ledger for tests/test_validation_sink_costs.py. Regenerate the fixture: venv/bin/python -c 'from tests.validation_sink import write; write(\"/tmp/ValidationSink.hdc\")' ; (cd ../kirby-hd-oracle && ./hd6cli.sh /tmp/ValidationSink.hdc) > tests/fixtures/authored/ValidationSink.json. Seeded 2026-08-30: the corpus is HD-written and cannot see these; the hand-written sink can.",
3-
"_baseline": "23 sink cost values disagree with HD (2026-08-30); option-cost fix 2026-08-30: 13 closed, now 10 remain; 5E-fallback ruling + MULTIPLESFX state 2026-08-30: 5 closed, 5 new, now 10",
3+
"_baseline": "23 sink cost values disagree with HD (2026-08-30); option-cost fix 2026-08-30: 13 closed, now 10 remain; 5E-fallback ruling + MULTIPLESFX state 2026-08-30: 5 closed, 5 new, now 10; REQUIRED-adder attach 2026-08-31: 2 closed (DoT), now 8",
44
"gaps": {
55
"20260830000055:real_cost": "framework: slot/pool cost rule differs on this hand-written shape (HD zeroes or redistributes where the engine costs the slot standalone); needs its own read of List/Multipower/VPP cost paths -- HD 27.0 engine 40.0 (ENERGYBLAST 'VPP Blast Slot')",
66
"20260830000057:real_cost": "framework: slot/pool cost rule differs on this hand-written shape (HD zeroes or redistributes where the engine costs the slot standalone); needs its own read of List/Multipower/VPP cost paths -- HD 7.0 engine 11.0 (FORCEFIELD 'VPP Force Field Slot')",
77
"20260830000060:active_cost": "carrier math: Absorption's identical AOE closed with the option-cost fix, so the residual is the CARRIER's own cost rule on this shape (Images' +4m pricing / NakedModifier's base), not the AOE value -- HD 15.0 engine 10 (NAKEDMODIFIER 'Naked Advantage')",
88
"20260830000060:real_cost": "carrier math: Absorption's identical AOE closed with the option-cost fix, so the residual is the CARRIER's own cost rule on this shape (Images' +4m pricing / NakedModifier's base), not the AOE value -- HD 15.0 engine 10 (NAKEDMODIFIER 'Naked Advantage')",
99
"20260830000061:real_cost": "CustomPower real-cost rounding on a hand-written shape -- HD 0.0 engine 1.0 (CUSTOMPOWER 'Custom Power With END')",
10-
"20260830000074:active_cost": "modifier value: HD's DoT prototype carries template-REQUIRED adders (damage increments, frequency) the sink never stated; engine sums neither -- HD 170.0 engine 80 (ENERGYBLAST 'Blast Damage Over Time')",
11-
"20260830000074:real_cost": "modifier value: HD's DoT prototype carries template-REQUIRED adders (damage increments, frequency) the sink never stated; engine sums neither -- HD 170.0 engine 80.0 (ENERGYBLAST 'Blast Damage Over Time')",
1210
"20260830000124:total_cost": "carrier math: Absorption's identical AOE closed with the option-cost fix, so the residual is the CARRIER's own cost rule on this shape (Images' +4m pricing), not the AOE value -- HD 10.0 engine 16.0 (IMAGES 'Images')",
1311
"20260830000124:active_cost": "carrier math: Absorption's identical AOE closed with the option-cost fix, so the residual is the CARRIER's own cost rule on this shape (Images' +4m pricing), not the AOE value -- HD 12.0 engine 20 (IMAGES 'Images')",
1412
"20260830000124:real_cost": "carrier math: Absorption's identical AOE closed with the option-cost fix, so the residual is the CARRIER's own cost rule on this shape (Images' +4m pricing), not the AOE value -- HD 12.0 engine 20.0 (IMAGES 'Images')"

0 commit comments

Comments
 (0)