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:
Binary file not shown.
@@ -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}"
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user