fix(db): harden architecture_zone_bias axis-value validation (PR #174 review)

H1: guard that each axis value is a {token=weight} table before .items() —
a scalar (wall = 15000) or the array shape (wall = ["steel_frame"], a
plausible copy-paste from the sibling catalog's visual_bundle) now yields a
clean V-TT-06 error instead of a bare AttributeError. Mirrors the isinstance
guards already on zone_map and axes.
H2: exclude bool from the positive-integer weight check (weight = true is an
int subclass, previously slipped through as 1) — matches the guard
populate_color_register_bands already applies to its own values.
Two new ZoneBiasValidationTests cover both branches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 13:22:40 +02:00
co-authored by Claude Fable 5
parent 204359927a
commit b8e0b1b660
3 changed files with 34 additions and 2 deletions
Binary file not shown.
+11 -2
View File
@@ -309,14 +309,23 @@ def populate_architecture_zone_bias(conn: sqlite3.Connection, dry_run: bool) ->
f"not one of {_TAG_AXES}"
)
continue
if not isinstance(weights, dict):
errors.append(
f"V-TT-06: architecture_zone_bias '{template_tag}.{zone_type_id}.{axis}': "
f"each axis value must be a {{token = weight_bps}} table, got "
f"{type(weights).__name__}"
)
continue
allowed = template_axis_tokens.get(template_tag, {}).get(axis, set())
for token, weight in (weights or {}).items():
for token, weight in weights.items():
if token not in allowed:
errors.append(
f"V-TT-06: architecture_zone_bias '{template_tag}.{zone_type_id}.{axis}': "
f"token '{token}' not in this template's visual_bundle.{axis} {sorted(allowed)}"
)
if not isinstance(weight, int) or weight <= 0:
# bool is an int subclass — exclude it explicitly, matching
# populate_color_register_bands' guard on its own values.
if isinstance(weight, bool) or not isinstance(weight, int) or weight <= 0:
errors.append(
f"architecture_zone_bias '{template_tag}.{zone_type_id}.{axis}.{token}': "
f"weight_bps must be a positive integer, got {weight!r}"
+23
View File
@@ -388,6 +388,29 @@ class ZoneBiasValidationTests(unittest.TestCase):
'wall = { brick_wall = 0 }\n'
)
def test_non_dict_axis_value_reports_vtt06_not_a_traceback(self):
# A bare scalar or the array shape (a plausible copy-paste from the
# sibling catalog's `visual_bundle.wall = [...]`) must produce a clean
# V-TT-06 error, not an AttributeError crash on `.items()`.
import contextlib
import io
for bad in ("wall = 15000\n", 'wall = ["brick_wall"]\n'):
out = io.StringIO()
with contextlib.redirect_stdout(out):
with self.assertRaises(ImportAborted):
self.populate(f"[bias.test_template.commercial_market]\n{bad}")
self.assertIn("V-TT-06", out.getvalue())
self.assertIn("must be a", out.getvalue())
def test_bool_weight_is_rejected(self):
# bool is an int subclass; `true` must not slip through as weight 1.
with self.assertRaises(ImportAborted):
self.populate(
'[bias.test_template.commercial_market]\n'
'wall = { brick_wall = true }\n'
)
def test_missing_source_yields_zero_rows(self):
traits.ARCHITECTURE_ZONE_BIAS_TOML = self.tmp / "missing.toml"
self.assertEqual(traits.populate_architecture_zone_bias(self.conn, dry_run=True), 0)