diff --git a/server/data/systems.db b/server/data/systems.db index f3ec2a6ff..93ffd0844 100644 Binary files a/server/data/systems.db and b/server/data/systems.db differ diff --git a/tooling/economy-db/economy_import/traits.py b/tooling/economy-db/economy_import/traits.py index 13127453a..36af4e143 100644 --- a/tooling/economy-db/economy_import/traits.py +++ b/tooling/economy-db/economy_import/traits.py @@ -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}" diff --git a/tooling/economy-db/test_traits.py b/tooling/economy-db/test_traits.py index d8a5e55df..ac9da5d13 100644 --- a/tooling/economy-db/test_traits.py +++ b/tooling/economy-db/test_traits.py @@ -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)