diff --git a/server/data/systems-schema.sql b/server/data/systems-schema.sql index 029034f32..8fb0c4466 100644 --- a/server/data/systems-schema.sql +++ b/server/data/systems-schema.sql @@ -588,6 +588,36 @@ CREATE TABLE IF NOT EXISTS atlas_body_trait_bias ( ); CREATE INDEX IF NOT EXISTS idx_atlas_body_trait_bias_body ON atlas_body_trait_bias(body_id); CREATE INDEX IF NOT EXISTS idx_atlas_body_trait_bias_tag ON atlas_body_trait_bias(template_tag); + +-- architecture_zone_bias: D-235 step-2 sparse bias table (T-988). Per +-- (template, zone_type) axis-token weight overrides within that template's +-- own visual_bundle; unlisted (template, zone_type) pairs — and unlisted +-- tokens within a listed entry — fall back to a uniform draw. Baked from +-- wiki/economics/architecture_zone_bias.toml. Importer-validated (V-TT-06): +-- every referenced token must already be in the template's visual_bundle. +CREATE TABLE IF NOT EXISTS architecture_zone_bias ( + template_tag TEXT NOT NULL REFERENCES trait_templates(tag) ON DELETE CASCADE, + zone_type_id TEXT NOT NULL, -- D-142 zone-type id (BuildingPropertyTag.zone_type_id) + bias TEXT NOT NULL, -- JSON {axis: {token: weight_bps}}, D-010 integer bps + PRIMARY KEY (template_tag, zone_type_id) +); +CREATE INDEX IF NOT EXISTS idx_architecture_zone_bias_template ON architecture_zone_bias(template_tag); + +-- color_register_bands: D-235 numeric HSV sampling band per trait-template +-- `color_register` label (T-988). One band per register; the fill seed +-- samples (hue, sat, val) uniformly within it per building. Baked from +-- wiki/economics/color_register_bands.toml. Importer-validated (V-TT-07): +-- every color_register referenced by trait_templates.visual_bundle must have +-- a band here, and every band's ranges must be in-bounds with min < max. +CREATE TABLE IF NOT EXISTS color_register_bands ( + color_register TEXT PRIMARY KEY, + hue_min INTEGER NOT NULL, -- centidegrees (degrees x 100), 0..36000 + hue_max INTEGER NOT NULL, + sat_min INTEGER NOT NULL, -- basis points, 0..10000 + sat_max INTEGER NOT NULL, + val_min INTEGER NOT NULL, -- basis points, 0..10000 + val_max INTEGER NOT NULL +); -- END TRAIT TEMPLATES (D-232, #993) -- Indexes diff --git a/server/data/systems.db b/server/data/systems.db index aaf68bf8c..f3ec2a6ff 100644 Binary files a/server/data/systems.db and b/server/data/systems.db differ diff --git a/tooling/economy-db/economy_import/migration.py b/tooling/economy-db/economy_import/migration.py index a77bd3831..308023cf7 100644 --- a/tooling/economy-db/economy_import/migration.py +++ b/tooling/economy-db/economy_import/migration.py @@ -255,6 +255,26 @@ CREATE TABLE IF NOT EXISTS atlas_body_trait_bias ( CREATE INDEX IF NOT EXISTS idx_atlas_body_trait_bias_body ON atlas_body_trait_bias(body_id); CREATE INDEX IF NOT EXISTS idx_atlas_body_trait_bias_tag ON atlas_body_trait_bias(template_tag); +-- D-235 exterior-grammar content (T-988). Mirrors the canonical DDL in +-- systems-schema.sql; here so the migration path (existing DBs) gets the +-- tables, not just fresh systems-schema.sql builds. +CREATE TABLE IF NOT EXISTS architecture_zone_bias ( + template_tag TEXT NOT NULL REFERENCES trait_templates(tag) ON DELETE CASCADE, + zone_type_id TEXT NOT NULL, + bias TEXT NOT NULL, + PRIMARY KEY (template_tag, zone_type_id) +); +CREATE INDEX IF NOT EXISTS idx_architecture_zone_bias_template ON architecture_zone_bias(template_tag); +CREATE TABLE IF NOT EXISTS color_register_bands ( + color_register TEXT PRIMARY KEY, + hue_min INTEGER NOT NULL, + hue_max INTEGER NOT NULL, + sat_min INTEGER NOT NULL, + sat_max INTEGER NOT NULL, + val_min INTEGER NOT NULL, + val_max INTEGER NOT NULL +); + -- Normalize bodies.economic_role to the D-194 canonical 10-value set (#911). -- Idempotent: each UPDATE is a no-op if the old value is already gone. UPDATE bodies SET economic_role = 'agricultural' WHERE economic_role IN ('agriculture', 'mixed-agriculture'); diff --git a/tooling/economy-db/economy_import/paths.py b/tooling/economy-db/economy_import/paths.py index 69d65370e..00c46d3ff 100644 --- a/tooling/economy-db/economy_import/paths.py +++ b/tooling/economy-db/economy_import/paths.py @@ -11,6 +11,8 @@ from pathlib import Path from generator_sources import ( ARCHITECTURE_TRAIT_BIAS_TOML, ARCHITECTURE_TRAIT_CATALOG_TOML, + ARCHITECTURE_ZONE_BIAS_TOML, + COLOR_REGISTER_BANDS_TOML, GENERATE_BRANDS_WRAPPER, OBJECT_TAG_VOCABULARY_TOML, REPO_ROOT, @@ -21,8 +23,10 @@ from generator_sources import ( __all__ = [ "ARCHITECTURE_TRAIT_BIAS_TOML", "ARCHITECTURE_TRAIT_CATALOG_TOML", + "ARCHITECTURE_ZONE_BIAS_TOML", "BRANDS_TOML", "CHAINS_TOML", + "COLOR_REGISTER_BANDS_TOML", "COMMODITIES_TOML", "CORPORATIONS_DIR", "CURRENCY_ZONES_TOML", diff --git a/tooling/economy-db/economy_import/traits.py b/tooling/economy-db/economy_import/traits.py index 1cc63f629..13127453a 100644 --- a/tooling/economy-db/economy_import/traits.py +++ b/tooling/economy-db/economy_import/traits.py @@ -8,6 +8,8 @@ from .errors import ImportAborted from .paths import ( ARCHITECTURE_TRAIT_BIAS_TOML, ARCHITECTURE_TRAIT_CATALOG_TOML, + ARCHITECTURE_ZONE_BIAS_TOML, + COLOR_REGISTER_BANDS_TOML, OBJECT_TAG_VOCABULARY_TOML, ) @@ -250,6 +252,193 @@ def populate_trait_templates(conn: sqlite3.Connection, dry_run: bool) -> int: return len(rows) +def populate_architecture_zone_bias(conn: sqlite3.Connection, dry_run: bool) -> int: + """Bake the D-235 step-2 zone-type bias table (T-988, resolves the ticket's + "REAL authored table" requirement). + + Reads ARCHITECTURE_ZONE_BIAS_TOML (`[bias..]` + stanzas; each is an optional `{axis: {token: weight_bps}}` sub-table per + axis — wall/roof/facade/street). Sparse/fallback model: a (template, + zone_type) pair absent here — or a token absent within a listed entry — + uses a uniform draw across the template's own `visual_bundle.` (the + generator's job, not this baker's). + + V-TT-06: every referenced token must already appear in THAT template's own + `visual_bundle.` (checked against the just-baked `trait_templates` + row, not the catalog TOML in memory, so the check can never silently pass + against a stale in-process parse) — this file never expands a template's + palette, only re-weights within it. Must run AFTER populate_trait_templates + (FK + visual_bundle lookup). Absent source -> 0 rows. + """ + if not ARCHITECTURE_ZONE_BIAS_TOML.exists(): + if not dry_run: + conn.execute("DELETE FROM architecture_zone_bias") + return 0 + with open(ARCHITECTURE_ZONE_BIAS_TOML, "rb") as f: + data = tomllib.load(f) + bias_root = data.get("bias", {}) + + known_tags: set[str] = set() + template_axis_tokens: dict[str, dict[str, set[str]]] = {} + for tag, vb_json in conn.execute("SELECT tag, visual_bundle FROM trait_templates"): + known_tags.add(tag) + vb = json.loads(vb_json) if vb_json else {} + template_axis_tokens[tag] = {axis: set(vb.get(axis) or []) for axis in _TAG_AXES} + + errors: list[str] = [] + rows: list[tuple] = [] + for template_tag, zone_map in bias_root.items(): + if template_tag not in known_tags: + errors.append(f"architecture_zone_bias '{template_tag}': not in trait_templates") + continue + if not isinstance(zone_map, dict): + errors.append( + f"architecture_zone_bias '{template_tag}': expected a table of zone_type_id entries" + ) + continue + for zone_type_id, axes in zone_map.items(): + if not isinstance(axes, dict): + errors.append( + f"architecture_zone_bias '{template_tag}.{zone_type_id}': expected a table of axis entries" + ) + continue + for axis, weights in axes.items(): + if axis not in _TAG_AXES: + errors.append( + f"architecture_zone_bias '{template_tag}.{zone_type_id}': axis '{axis}' " + f"not one of {_TAG_AXES}" + ) + continue + allowed = template_axis_tokens.get(template_tag, {}).get(axis, set()) + for token, weight in (weights or {}).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: + errors.append( + f"architecture_zone_bias '{template_tag}.{zone_type_id}.{axis}.{token}': " + f"weight_bps must be a positive integer, got {weight!r}" + ) + try: + bias_json = json.dumps(axes) + except (TypeError, ValueError): + errors.append( + f"architecture_zone_bias '{template_tag}.{zone_type_id}': not JSON-serialisable" + ) + continue + rows.append((template_tag, zone_type_id, bias_json)) + + if errors: + print(f" ARCHITECTURE ZONE BIAS ERRORS ({len(errors)}):") + for e in errors: + print(f" - {e}") + raise ImportAborted() + + if not dry_run: + conn.execute("DELETE FROM architecture_zone_bias") + conn.executemany( + "INSERT INTO architecture_zone_bias (template_tag, zone_type_id, bias) VALUES (?,?,?)", + rows, + ) + return len(rows) + + +# D-235 numeric HSV axis bounds (T-988): hue is centidegrees (0..36000 = 0-360 +# deg x 100); sat/val are basis points (0..10000 = 0-100.00%). +_COLOR_AXIS_BOUNDS: dict[str, int] = {"hue": 36000, "sat": 10000, "val": 10000} + + +def populate_color_register_bands(conn: sqlite3.Connection, dry_run: bool) -> int: + """Bake the D-235 numeric color-register HSV sampling bands (T-988). + + Reads COLOR_REGISTER_BANDS_TOML (`[register.]` stanzas; each of + `hue`/`sat`/`val` a 2-element `[min, max]` integer array). The fill seed + samples a single (hue, sat, val) point uniformly within a template's + register band per building (D-235) — this baker only validates + stores + the bands. + + V-TT-07: (1) every band's ranges are in-bounds (hue 0..36000, sat/val + 0..10000) with `min < max`; (2) every `color_register` referenced by + `trait_templates.visual_bundle.color_register` (checked against the + just-baked table, same self-consistency reasoning as V-TT-06) has a band + here — coverage, not just shape. Must run AFTER populate_trait_templates. + Absent source -> 0 rows. + """ + if not COLOR_REGISTER_BANDS_TOML.exists(): + if not dry_run: + conn.execute("DELETE FROM color_register_bands") + return 0 + with open(COLOR_REGISTER_BANDS_TOML, "rb") as f: + data = tomllib.load(f) + registers = data.get("register", {}) + + errors: list[str] = [] + rows: list[tuple] = [] + for name, band in registers.items(): + values: dict[str, tuple[int, int]] = {} + for axis, bound in _COLOR_AXIS_BOUNDS.items(): + pair = band.get(axis) if isinstance(band, dict) else None + if not (isinstance(pair, list) and len(pair) == 2): + errors.append( + f"V-TT-07: color_register_bands '{name}': '{axis}' must be a 2-element " + "[min, max] array" + ) + continue + lo, hi = pair + if isinstance(lo, bool) or isinstance(hi, bool) or not ( + isinstance(lo, int) and isinstance(hi, int) + ): + errors.append( + f"V-TT-07: color_register_bands '{name}.{axis}': min/max must be integers, " + f"got {pair!r}" + ) + continue + if not (0 <= lo < hi <= bound): + errors.append( + f"V-TT-07: color_register_bands '{name}.{axis}': range [{lo}, {hi}] must " + f"satisfy 0 <= min < max <= {bound}" + ) + continue + values[axis] = (lo, hi) + if len(values) == 3: + rows.append(( + name, + values["hue"][0], values["hue"][1], + values["sat"][0], values["sat"][1], + values["val"][0], values["val"][1], + )) + + # Coverage half of V-TT-07: every color_register the catalog actually + # references must have a band. + referenced: set[str] = set() + for (vb_json,) in conn.execute("SELECT visual_bundle FROM trait_templates"): + vb = json.loads(vb_json) if vb_json else {} + reg = vb.get("color_register") + if reg: + referenced.add(reg) + banded = {r[0] for r in rows} + for reg in sorted(referenced - banded): + errors.append(f"V-TT-07: color_register '{reg}' referenced by trait_templates but has no band") + + if errors: + print(f" COLOR REGISTER BAND ERRORS ({len(errors)}):") + for e in errors: + print(f" - {e}") + raise ImportAborted() + + if not dry_run: + conn.execute("DELETE FROM color_register_bands") + conn.executemany( + """INSERT INTO color_register_bands + (color_register, hue_min, hue_max, sat_min, sat_max, val_min, val_max) + VALUES (?,?,?,?,?,?,?)""", + rows, + ) + return len(rows) + + def populate_atlas_body_trait_bias(conn: sqlite3.Connection, dry_run: bool) -> int: """Bake the sparse per-body hero pins into atlas_body_trait_bias (#993). diff --git a/tooling/economy-db/import_economics.py b/tooling/economy-db/import_economics.py index 43aaa2080..51e6eb031 100755 --- a/tooling/economy-db/import_economics.py +++ b/tooling/economy-db/import_economics.py @@ -228,10 +228,22 @@ def main() -> None: print(f" {n_tilt} bodies updated with axial_tilt_deg") # 17. biosphere_class from body frontmatter override + two-gate default (D-247, T-1085) - print(" [17/17] Populating biosphere_class (D-247)...") + print(" [17/19] Populating biosphere_class (D-247)...") n_bio = bodies.populate_biosphere_class(conn, args.dry_run) print(f" {n_bio} bodies updated with biosphere_class") + # 18. Architecture zone-type bias table (T-988, D-235 step 2) — must + # follow trait_templates (V-TT-06 validates against its visual_bundle). + print(" [18/19] Baking architecture_zone_bias table (D-235)...") + n_zone_bias = traits.populate_architecture_zone_bias(conn, args.dry_run) + print(f" {n_zone_bias} zone-bias rows") + + # 19. Color register bands (T-988, D-235) — numeric HSV sampling bands + # per trait-template color_register; also follows trait_templates (V-TT-07). + print(" [19/19] Baking color_register_bands table (D-235)...") + n_color_bands = traits.populate_color_register_bands(conn, args.dry_run) + print(f" {n_color_bands} color register bands") + # Validate structural integrity (FK, chain refs, chain completeness). # These errors indicate broken imported data — do NOT commit. print("\n Validating structural integrity...") diff --git a/tooling/economy-db/test_traits.py b/tooling/economy-db/test_traits.py index 03eb86b65..d8a5e55df 100644 --- a/tooling/economy-db/test_traits.py +++ b/tooling/economy-db/test_traits.py @@ -3,14 +3,16 @@ Unit tests for economy_import.traits validation (T-995, PR #173 review H2). Covers the failure branches of the ObjectTag registry loader/validator -(V-TT-03 existence/axis, V-TT-04 fallback-graph) and the V-TT-05 pin bound — -the `make test-tooling` dry-run only exercises the happy path against the -committed, already-valid registry. +(V-TT-03 existence/axis, V-TT-04 fallback-graph), the V-TT-05 pin bound, the +V-TT-06 zone-bias token-existence check, and the V-TT-07 color-register band +shape/coverage checks (T-988) — the `make test-tooling` dry-run only +exercises the happy path against the committed, already-valid content. Stdlib only (unittest) — run directly or via `make test-tooling`: python3 tooling/economy-db/test_traits.py """ +import json import sqlite3 import sys import tempfile @@ -309,5 +311,168 @@ class PinBoundTests(unittest.TestCase): self.populate(self.bias_toml(6)) +class ZoneBiasValidationTests(unittest.TestCase): + """populate_architecture_zone_bias: V-TT-06 token-existence check (T-988).""" + + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + self.tmp = Path(self._tmp.name) + self._orig_zone_bias = traits.ARCHITECTURE_ZONE_BIAS_TOML + self.conn = sqlite3.connect(":memory:") + self.conn.execute("CREATE TABLE trait_templates (tag TEXT PRIMARY KEY, visual_bundle TEXT)") + self.conn.execute( + """CREATE TABLE architecture_zone_bias ( + template_tag TEXT NOT NULL, zone_type_id TEXT NOT NULL, bias TEXT NOT NULL, + PRIMARY KEY (template_tag, zone_type_id))""" + ) + self.conn.execute( + "INSERT INTO trait_templates (tag, visual_bundle) VALUES (?, ?)", + ( + "test_template", + json.dumps({ + "wall": ["brick_wall", "rendered_wall"], + "roof": ["flat_roof"], + "facade": ["regular_facade"], + "street": ["paved"], + "color_register": "neutral_grey", + }), + ), + ) + + def tearDown(self): + traits.ARCHITECTURE_ZONE_BIAS_TOML = self._orig_zone_bias + self.conn.close() + self._tmp.cleanup() + + def populate(self, body: str): + path = self.tmp / "architecture_zone_bias.toml" + path.write_text(body, encoding="utf-8") + traits.ARCHITECTURE_ZONE_BIAS_TOML = path + return traits.populate_architecture_zone_bias(self.conn, dry_run=True) + + def test_valid_bias_within_visual_bundle_passes(self): + n = self.populate( + '[bias.test_template.commercial_market]\n' + 'wall = { brick_wall = 15000 }\n' + 'street = { paved = 13000 }\n' + ) + self.assertEqual(n, 1) + + def test_token_outside_visual_bundle_is_rejected(self): + # steel_frame is a real ObjectTag, but never appears in test_template's + # own visual_bundle.wall above — V-TT-06 must reject it. + import contextlib + import io + + out = io.StringIO() + with contextlib.redirect_stdout(out): + with self.assertRaises(ImportAborted): + self.populate( + '[bias.test_template.industrial_freight]\n' + 'wall = { steel_frame = 16000 }\n' + ) + self.assertIn("V-TT-06", out.getvalue()) + self.assertIn("steel_frame", out.getvalue()) + + def test_unknown_template_tag_is_rejected(self): + with self.assertRaises(ImportAborted): + self.populate( + '[bias.no_such_template.commercial_market]\n' + 'wall = { brick_wall = 15000 }\n' + ) + + def test_non_positive_weight_is_rejected(self): + with self.assertRaises(ImportAborted): + self.populate( + '[bias.test_template.commercial_market]\n' + 'wall = { brick_wall = 0 }\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) + + +class ColorRegisterBandTests(unittest.TestCase): + """populate_color_register_bands: V-TT-07 shape + coverage checks (T-988).""" + + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + self.tmp = Path(self._tmp.name) + self._orig_bands = traits.COLOR_REGISTER_BANDS_TOML + self.conn = sqlite3.connect(":memory:") + self.conn.execute("CREATE TABLE trait_templates (tag TEXT PRIMARY KEY, visual_bundle TEXT)") + self.conn.execute( + """CREATE TABLE color_register_bands ( + color_register TEXT PRIMARY KEY, hue_min INTEGER NOT NULL, hue_max INTEGER NOT NULL, + sat_min INTEGER NOT NULL, sat_max INTEGER NOT NULL, + val_min INTEGER NOT NULL, val_max INTEGER NOT NULL)""" + ) + self.conn.execute( + "INSERT INTO trait_templates (tag, visual_bundle) VALUES (?, ?)", + ("test_template", json.dumps({"color_register": "neutral_grey"})), + ) + + def tearDown(self): + traits.COLOR_REGISTER_BANDS_TOML = self._orig_bands + self.conn.close() + self._tmp.cleanup() + + def populate(self, body: str): + path = self.tmp / "color_register_bands.toml" + path.write_text(body, encoding="utf-8") + traits.COLOR_REGISTER_BANDS_TOML = path + return traits.populate_color_register_bands(self.conn, dry_run=True) + + def test_valid_band_covering_the_referenced_register_passes(self): + n = self.populate( + '[register.neutral_grey]\n' + 'hue = [20000, 21200]\n' + 'sat = [400, 1200]\n' + 'val = [4800, 6400]\n' + ) + self.assertEqual(n, 1) + + def test_missing_coverage_for_referenced_register_is_rejected(self): + import contextlib + import io + + out = io.StringIO() + with contextlib.redirect_stdout(out): + with self.assertRaises(ImportAborted): + # A band for a DIFFERENT register — neutral_grey (referenced by + # test_template above) has no band at all. + self.populate( + '[register.some_other_register]\n' + 'hue = [0, 100]\n' + 'sat = [0, 100]\n' + 'val = [0, 100]\n' + ) + self.assertIn("V-TT-07", out.getvalue()) + self.assertIn("neutral_grey", out.getvalue()) + + def test_hue_out_of_bounds_is_rejected(self): + with self.assertRaises(ImportAborted): + self.populate( + '[register.neutral_grey]\n' + 'hue = [30000, 40000]\n' # 40000 > 36000 bound + 'sat = [400, 1200]\n' + 'val = [4800, 6400]\n' + ) + + def test_min_not_less_than_max_is_rejected(self): + with self.assertRaises(ImportAborted): + self.populate( + '[register.neutral_grey]\n' + 'hue = [20000, 20000]\n' # min == max, not min < max + 'sat = [400, 1200]\n' + 'val = [4800, 6400]\n' + ) + + def test_missing_source_yields_zero_rows(self): + traits.COLOR_REGISTER_BANDS_TOML = self.tmp / "missing.toml" + self.assertEqual(traits.populate_color_register_bands(self.conn, dry_run=True), 0) + + if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/tooling/generator_sources.py b/tooling/generator_sources.py index d62aa8a6b..453352743 100644 --- a/tooling/generator_sources.py +++ b/tooling/generator_sources.py @@ -88,6 +88,17 @@ ARCHITECTURE_TRAIT_BIAS_TOML: Path = ( OBJECT_TAG_VOCABULARY_TOML: Path = ( REPO_ROOT / "wiki" / "economics" / "object_tag_vocabulary.toml" ) +# D-235 exterior-grammar content (T-988): per-(template, zone_type) axis-token +# bias overrides, and per-color_register integer HSV sampling bands. Both are +# baked alongside trait_templates (traits.py: populate_architecture_zone_bias, +# populate_color_register_bands), so an edit to either must flip the stamp +# exactly like the catalog/registry above. +ARCHITECTURE_ZONE_BIAS_TOML: Path = ( + REPO_ROOT / "wiki" / "economics" / "architecture_zone_bias.toml" +) +COLOR_REGISTER_BANDS_TOML: Path = ( + REPO_ROOT / "wiki" / "economics" / "color_register_bands.toml" +) def _economy_import_modules() -> tuple[Path, ...]: @@ -124,6 +135,8 @@ IMPORT_ECONOMICS_SOURCES: tuple[Path, ...] = ( ARCHITECTURE_TRAIT_CATALOG_TOML, ARCHITECTURE_TRAIT_BIAS_TOML, OBJECT_TAG_VOCABULARY_TOML, + ARCHITECTURE_ZONE_BIAS_TOML, + COLOR_REGISTER_BANDS_TOML, ) # --------------------------------------------------------------------------- diff --git a/wiki/economics/architecture_zone_bias.toml b/wiki/economics/architecture_zone_bias.toml new file mode 100644 index 000000000..a246569b4 --- /dev/null +++ b/wiki/economics/architecture_zone_bias.toml @@ -0,0 +1,380 @@ +# ========================================================================== +# Architecture zone-type bias table (T-988, D-235 step 2) +# +# D-235's BuildingExteriorTag derivation is 3 steps: (1) the district's +# dominant trait template (D-232) filters the available token set per axis +# via its own `visual_bundle`; (2) THIS FILE biases which of those +# already-filtered tokens a building is likely to draw, keyed on the +# building's FUNCTION (D-142 zone_type) — "an industrial block stays stone, +# not corrugated metal, if the template's allow-list says so"; (3) density +# sets setback_tier. This table is step 2 only. +# +# SPARSE / FALLBACK MODEL. This is deliberately NOT a full 28-template x +# 31-zone_type x 4-axis matrix (3,472 cells). It authors only the cases +# where cultural or functional logic genuinely demands a lean — a civic +# building reaching for stone and colonnade, a market stall staying in the +# lighter/cheaper register, a fishing quay pulling boardwalk over cobble. +# Every (template, zone_type) pair NOT listed here — and every token within +# a listed entry not given a weight — falls back to a UNIFORM draw across +# the template's visual_bundle for that axis. Absence of an entry is not an +# oversight; it means the template's own visual_bundle already tells the +# whole story for that function, or (rarely, see note at end) the template +# has no per-axis choice left to bias. +# +# OWNERSHIP. Same co-maintenance split as object_tag_vocabulary.toml +# (Q-049's resolution: split by concern, not by file) — +# - Miri owns the CULTURAL/FUNCTIONAL rationale: which zone_type leans +# toward which already-allowed token, and why (the inline comment above +# each entry below). +# - Araminta owns VISUAL-COHERENCE tuning of the bps weights themselves +# (does the resulting draw distribution actually read well against the +# shipped art, does a "boost" ever starve a token to invisibility). +# This pass (T-988 content deliverable) is Miri's cultural-logic +# authoring pass with initial weights; Araminta's visual-coherence review +# of the weights is a expected follow-up, same pattern as her parallel +# `color_register_bands.toml` pass in this same ticket. +# +# SCHEMA. Integer basis points (D-010): 10000 = 1.0x the uniform baseline. +# A weight above 10000 boosts a token above uniform likelihood; a weight +# below 10000 (never authored as 0 — no hard excludes here, that's what +# the template's own allow/block already did) suppresses it toward rare. +# Only tokens already present in the template's own visual_bundle. +# may appear (importer-validated, V-TT check) — this file never expands a +# template's palette, only re-weights within it. Unlisted tokens/axes/zones +# keep the uniform baseline. +# +# [bias..] +# wall = { = , ... } # optional per axis +# roof = { = , ... } +# facade = { = , ... } +# street = { = , ... } +# ========================================================================== + + +# ========================================================================== +# A. CROSS-CORRIDOR POOL +# ========================================================================== + +# mixed_market_vernacular: brick reads load-bearing/durable shopfront vs. +# rendered's softer domestic finish upstairs. +[bias.mixed_market_vernacular.commercial_market] +wall = { brick_wall = 15000 } +street = { cobble = 14000 } + +[bias.mixed_market_vernacular.residential_surface] +wall = { rendered_wall = 14000 } +street = { paved = 13000 } + +# speculative_boomtown: freight gets the rawest register (first thing +# thrown up); the market street is the town's "arrived" storefront. +[bias.speculative_boomtown.industrial_freight] +wall = { steel_frame = 16000 } +roof = { corrugated_roof = 16000 } +street = { packed_earth = 15000 } + +[bias.speculative_boomtown.commercial_market] +wall = { composite_panel = 14000 } +roof = { flat_roof = 13000 } +street = { paved = 13000 } + +# extraction_camp: the works itself is steel; worker housing is poured +# concrete, not rig-grade steel. +[bias.extraction_camp.extraction_platform] +wall = { steel_frame = 18000 } + +[bias.extraction_camp.residential_surface] +wall = { concrete_wall = 14000 } + +# industrial_utilitarian: the production floor is steel/corrugated/heavy- +# haul; the district's few storefronts keep the older brick frontage. +[bias.industrial_utilitarian.industrial_manufacturing] +wall = { steel_frame = 15000 } +roof = { corrugated_roof = 14000 } +street = { heavy_haul = 15000 } + +[bias.industrial_utilitarian.commercial_market] +wall = { brick_wall = 13000 } +roof = { flat_roof = 12000 } +street = { paved = 13000 } + +# cold_chain_works: the plant is permanent concrete with truck access; the +# produce-market front is the lighter composite-panel register. +[bias.cold_chain_works.industrial_processing] +wall = { concrete_wall = 14000 } +street = { heavy_haul = 15000 } + +[bias.cold_chain_works.commercial_market] +wall = { composite_panel = 13000 } +street = { paved = 13000 } + +# precision_arcology: labs get the showcase glass + green roof + clean +# raised walkways; the production floor behind it stays flat/paved/panel. +[bias.precision_arcology.research_station] +roof = { green_roof = 15000 } +street = { elevated_walkway = 14000 } + +[bias.precision_arcology.industrial_manufacturing] +wall = { composite_panel = 13000 } +roof = { flat_roof = 14000 } +street = { paved = 13000 } + +# information_spire: civic seats keep institutional restraint even at the +# top of the spire; retail/market level gets the reactive screen facade. +[bias.information_spire.administrative_civil] +facade = { regular_facade = 15000 } +street = { elevated_walkway = 13000 } + +[bias.information_spire.commercial_market] +facade = { screen_facade = 14000 } +street = { paved = 13000 } + +# civic_monumental: the courts get the fullest ceremonial treatment of any +# building in the catalog; a mere checkpoint in an Administrative district +# is NOT a courthouse — utilitarian concrete, ornament suppressed hard. +[bias.civic_monumental.administrative_civil] +wall = { stone_wall = 16000 } +roof = { vaulted_roof = 15000 } +facade = { colonnade = 15000 } +street = { cobble = 14000 } + +[bias.civic_monumental.administrative_judicial] +wall = { stone_wall = 16000 } +roof = { vaulted_roof = 16000 } +facade = { colonnade = 16000 } +street = { cobble = 14000 } + +[bias.civic_monumental.security_checkpoint] +wall = { concrete_wall = 15000 } +roof = { flat_roof = 15000 } +facade = { ornamental_facade = 6000 } + +# foreign_quarter: the transplant that draws a crowd gets the grand vaulted/ +# lattice register; the quiet domestic import stays plainer. +[bias.foreign_quarter.entertainment_venue] +roof = { vaulted_roof = 15000 } +facade = { lattice_screen = 14000 } + +[bias.foreign_quarter.residential_surface] +roof = { clay_tile_roof = 13000 } +facade = { screen_facade = 13000 } + + +# ========================================================================== +# B. PER-CORRIDOR BASELINE POOLS +# ========================================================================== + +# core_cosmopolitan: the old ministries stay stone/ornamental; modern retail +# layers glass over the old core; ordinary old-town housing stays brick. +[bias.core_cosmopolitan.administrative_civil] +wall = { stone_wall = 15000 } +facade = { ornamental_facade = 14000 } + +[bias.core_cosmopolitan.commercial_market] +wall = { glass_curtain_wall = 14000 } +facade = { regular_facade = 13000 } + +[bias.core_cosmopolitan.residential_surface] +wall = { brick_wall = 13000 } +roof = { pitched_roof = 13000 } + +# north_anglo_frontier: civic restraint in brick vs. timber farm sheds with +# corrugated roofs on packed-earth farm tracks. +[bias.north_anglo_frontier.administrative_civil] +wall = { brick_wall = 14000 } +facade = { regular_facade = 14000 } +street = { paved = 13000 } + +[bias.north_anglo_frontier.rural_agricultural] +wall = { timber_wall = 14000 } +roof = { corrugated_roof = 15000 } +street = { packed_earth = 15000 } + +# south_lusophone: the praça-facing arcade for the market vs. the shuttered +# private house. +[bias.south_lusophone.commercial_market] +facade = { arcade_facade = 15000 } +street = { cobble = 14000 } + +[bias.south_lusophone.residential_surface] +facade = { shuttered_facade = 14000 } +wall = { stucco_wall = 13000 } + +# west_germanic_ordered: engineered brick precision for manufacturing vs. +# the plain rendered residential street. +[bias.west_germanic_ordered.industrial_manufacturing] +wall = { brick_wall = 14000 } +street = { paved = 14000 } + +[bias.west_germanic_ordered.residential_surface] +wall = { rendered_wall = 13000 } +street = { cobble = 12000 } + +# west_compact_cooperative: the civic hall is visibly held in common +# (colonnade + green roof) vs. plain cooperative housing. +[bias.west_compact_cooperative.administrative_civil] +facade = { colonnade = 16000 } +roof = { green_roof = 14000 } + +[bias.west_compact_cooperative.residential_surface] +facade = { regular_facade = 13000 } +wall = { rendered_wall = 13000 } + +# east_dense_utilitarian: signage-saturated screen facade at the retail/ +# transit level vs. lattice-screened concrete housing above. +[bias.east_dense_utilitarian.commercial_market] +facade = { screen_facade = 15000 } +street = { elevated_walkway = 14000 } + +[bias.east_dense_utilitarian.residential_surface] +facade = { lattice_screen = 14000 } +wall = { concrete_wall = 13000 } +street = { paved = 13000 } + +# frontier_surname_settlement: the founder-family's one formal stone +# building vs. rammed-earth farm outbuildings on packed earth. +[bias.frontier_surname_settlement.administrative_civil] +wall = { stone_wall = 16000 } +facade = { regular_facade = 14000 } +street = { cobble = 15000 } + +[bias.frontier_surname_settlement.rural_agricultural] +wall = { rammed_earth_wall = 14000 } +roof = { flat_roof = 13000 } +street = { packed_earth = 15000 } + +# frontier_hardscrabble: only the wall axis has any choice here — a +# checkpoint gets the standardized defensive concrete; a frontier outpost +# gets the locally hand-built rammed earth. +[bias.frontier_hardscrabble.security_checkpoint] +wall = { concrete_wall = 15000 } + +[bias.frontier_hardscrabble.wilderness_frontier] +wall = { rammed_earth_wall = 14000 } + + +# ========================================================================== +# C. HERITAGE SUB-POOLS +# ========================================================================== + +# scottish_highland_vernacular: scattered crofts (pitched roof, packed +# earth track) vs. the one village market building (tile roof, cobble). +[bias.scottish_highland_vernacular.residential_dispersed] +roof = { pitched_roof = 14000 } +street = { packed_earth = 14000 } + +[bias.scottish_highland_vernacular.commercial_market] +roof = { clay_tile_roof = 13000 } +street = { cobble = 13000 } + +# west_african_compound: the market-facing arcade vs. the private family +# compound behind its shuttered rammed-earth wall. +[bias.west_african_compound.commercial_market] +facade = { arcade_facade = 15000 } +street = { cobble = 13000 } + +[bias.west_african_compound.residential_surface] +facade = { shuttered_facade = 14000 } +wall = { rammed_earth_wall = 13000 } +street = { packed_earth = 14000 } + +# iberian_hacienda: the estate-office colonnade reads institutional vs. the +# veranda-fronted domestic hacienda. +[bias.iberian_hacienda.administrative_civil] +facade = { colonnade = 15000 } +wall = { stone_wall = 14000 } + +[bias.iberian_hacienda.residential_surface] +facade = { arcade_facade = 14000 } +wall = { stucco_wall = 13000 } + +# atlantic_creole_maritime: the working waterfront (timber, boardwalk) vs. +# inland creole housing (render, cobble). +[bias.atlantic_creole_maritime.port_fishing] +wall = { timber_wall = 14000 } +street = { boardwalk = 15000 } + +[bias.atlantic_creole_maritime.residential_surface] +wall = { rendered_wall = 13000 } +street = { cobble = 13000 } + +# nordic_timber: the coastal fishing quay pulls boardwalk; inland housing +# keeps the turf/green-roof domestic register on cobble. +[bias.nordic_timber.port_fishing] +street = { boardwalk = 15000 } + +[bias.nordic_timber.residential_surface] +roof = { green_roof = 13000 } +street = { cobble = 13000 } + +# central_european_blok: civic solidity keeps its ornament in reserve (but +# still reaches for it) vs. the plain perimeter-block housing stock. +[bias.central_european_blok.administrative_civil] +facade = { ornamental_facade = 14000 } +wall = { brick_wall = 14000 } + +[bias.central_european_blok.residential_surface] +facade = { regular_facade = 13000 } +wall = { rendered_wall = 13000 } + +# east_asian_temple_enclave: ceremonial reception gets the full vaulted- +# colonnade register; the ordinary residential quarter around the temple +# stays in the softer timber/lattice domestic register. +[bias.east_asian_temple_enclave.diplomatic_elite] +roof = { vaulted_roof = 16000 } +facade = { colonnade = 15000 } + +[bias.east_asian_temple_enclave.residential_surface] +wall = { timber_wall = 14000 } +roof = { clay_tile_roof = 13000 } +facade = { lattice_screen = 13000 } +street = { cobble = 13000 } + +# vietnamese_water_village: working water trade backs onto the canal; +# land-fronting tube-houses face the boardwalk instead. +[bias.vietnamese_water_village.port_fishing] +wall = { timber_wall = 14000 } +street = { canal_way = 15000 } + +[bias.vietnamese_water_village.rural_aquaculture] +street = { canal_way = 15000 } + +[bias.vietnamese_water_village.residential_surface] +wall = { rendered_wall = 13000 } +street = { boardwalk = 13000 } + +# afrikaans_kraal: the werf/farmstead register (render, pitched roof, dirt +# track) vs. the more formal Cape civic building (stone, tile, cobble). +[bias.afrikaans_kraal.rural_pastoral] +wall = { rendered_wall = 14000 } +roof = { pitched_roof = 14000 } +street = { packed_earth = 14000 } + +[bias.afrikaans_kraal.administrative_civil] +wall = { stone_wall = 14000 } +roof = { clay_tile_roof = 13000 } +street = { cobble = 13000 } + +# arab_oasis_qanat: the shaded souk-adjacent market lane vs. the courtyard +# house with its wind-tower privacy screen. +[bias.arab_oasis_qanat.commercial_market] +wall = { stucco_wall = 14000 } +facade = { lattice_screen = 14000 } +street = { cobble = 14000 } + +[bias.arab_oasis_qanat.residential_surface] +wall = { rammed_earth_wall = 14000 } +roof = { terraced_roof = 14000 } +facade = { screen_facade = 14000 } +street = { packed_earth = 14000 } + + +# ========================================================================== +# NOT BIASED — flagged, not an oversight +# ========================================================================== +# generic_baseline is the one template with ZERO degrees of freedom: every +# axis (wall/roof/facade/street) carries exactly one token in its +# visual_bundle. There is nothing left to bias — any zone_type drawing this +# template gets that one token per axis regardless. This is correct: it is +# the corridor-neutral fallback the draw can always reach (catalog line 57), +# and a single-token axis is definitionally already "uniform." +# ========================================================================== diff --git a/wiki/economics/color_register_bands.toml b/wiki/economics/color_register_bands.toml new file mode 100644 index 000000000..2b57f691d --- /dev/null +++ b/wiki/economics/color_register_bands.toml @@ -0,0 +1,330 @@ +# ========================================================================== +# Color register bands (T-988, D-235) — numeric HSV bands per D-232 +# trait-template `color_register` label. Turns the free-form palette-cue +# string documented in object_tag_vocabulary.toml ("a palette cue, NOT an +# ObjectTag") into an authored, sampleable numeric range. +# +# THE MODEL: each color_register gets ONE integer HSV band. The fill seed +# picks a single (hue, sat, val) point uniformly within the band per +# building — same register, same family feel, different building each time. +# This is the register acting as intended (D-235): "always within the +# template's register," never a fixed single color, never unbounded. +# +# INTEGER UNITS (D-010 — no floats, ever; determinism is save-critical +# under D-227): +# hue 0..36000 centidegrees (degrees x 100; standard 0-360 deg wheel) +# sat 0..10000 basis points (0-100.00%) +# val 0..10000 basis points (0-100.00%) +# All three axes are closed [min, max] integer ranges, min < max, sampled +# uniformly (no wraparound support — no band crosses the hue 0/36000 seam). +# +# SATURATION CEILING (visual-hierarchy discipline, not a hard schema rule): +# every band in this file stays at or below sat = 3600 (36%). This keeps +# building material color — structure, the least important read — clearly +# under the saturation the project reserves for foreground signal (entity / +# relationship color has historically run 40-60% in this project's palette +# work). Buildings should never visually compete with the things standing +# in front of them. Even the two "brightest allowed" cultural-accent +# registers (bright_painted, falu_red_and_pine) stay under this ceiling. +# +# OWNERSHIP (Q-049 split, same as object_tag_vocabulary.toml): Miri owns +# whether a register's cultural placement is right for its template/corridor +# (the WHY); Araminta owns the numeric HSV authoring (the WHAT it looks +# like) and the cross-register visual discipline (distinctness, saturation +# hierarchy, mood). Changes to a band's numbers are Araminta's call; changes +# to which register a template carries are Miri's. Add both together when a +# new template ships a new color_register value. +# +# COVERAGE: every color_register value referenced by +# architecture_trait_catalog.toml's `visual_bundle.color_register` MUST have +# a `[register.]` entry here — the importer validates this (T-988). +# 28 values are banded below, one per current template. Two are flagged in +# the section comments as the most interpretive calls in the set — real +# entries, not placeholders, but worth a cultural sanity-check from Miri. +# +# The fill seed samples (h, s, v) once per building and stores the result as +# three plain integers on BuildingExteriorTag — no re-derivation, no drift. +# ========================================================================== + + +# ========================================================================== +# I. COOL NEUTRAL / INSTITUTIONAL & TECH — cool-cast hues (~190-220 deg), +# the lowest-saturation family in the file. Differ mainly by value +# (how pale) and a hair of saturation (how "designed" vs "clinical"). +# ========================================================================== + +[register.neutral_grey] +hue = [20000, 21200] # centidegrees (200-212 deg, cool blue-grey) +sat = [400, 1200] # bps (4-12%) +val = [4800, 6400] # bps (48-64%) +# generic_baseline — corridor-neutral concrete default; the fallback every +# other register is a deviation FROM. Mid-value, barely-there cool tint. + +[register.clean_pale] +hue = [19000, 20200] # centidegrees (190-202 deg, near-neutral, faint cool) +sat = [200, 800] # bps (2-8%) -- the lowest sat band in the catalog +val = [7600, 9000] # bps (76-90%) +# cold_chain_works — food/pharma hygiene register. Clinical pale, almost +# no color at all; reads as "clean" through value and absence of tint. + +[register.cool_clean] +hue = [20000, 21400] # centidegrees (200-214 deg, cool blue) +sat = [600, 1600] # bps (6-16%) +val = [7000, 8400] # bps (70-84%) +# precision_arcology — cleanroom/fab register. Quiet, controlled, a shade +# more saturated and a shade darker than clean_pale (expensive, not sterile). + +[register.glass_and_light] +hue = [20400, 21800] # centidegrees (204-218 deg, cool blue, glazing tint) +sat = [1000, 2200] # bps (10-22%) +val = [7200, 8800] # bps (72-88%) +# information_spire — glass curtain-wall office towers. Brightest, most +# reflective register in the set; the sat bump over cool_clean is the +# tinted-glazing read, not "more colorful," just "more glass." + +[register.neon_over_grey] +hue = [19200, 20400] # centidegrees (192-204 deg, cool cyan-grey) +sat = [1200, 2600] # bps (12-26%) +val = [4400, 6000] # bps (44-60%) +# east_dense_utilitarian — cyberpunk-density baseline. The grey base carries +# a restrained cool cast; actual neon is signage/lighting, a different +# system (environmental neutrality) -- this band stays a paint color, not a +# light source, so it never reads as garish. + + +# ========================================================================== +# II. WARM CIVIC PALE — the warm-hue counterpart to section I. One entry: +# civic authority reads warm and dignified, not cool and clinical. +# ========================================================================== + +[register.pale_stone] +hue = [3800, 5000] # centidegrees (38-50 deg, warm stone tan) +sat = [400, 1400] # bps (4-14%) +val = [6800, 8200] # bps (68-82%) +# civic_monumental — colonnades and institutional gravitas. Pale and low-sat +# like section I, but warm instead of cool: authority, not sterility. + + +# ========================================================================== +# III. INDUSTRIAL OXIDE & RAW — warm rust-to-greige hues, low-mid sat, +# low-mid value. The heavy-process / unfinished / hostile-world family. +# ========================================================================== + +[register.oxide_and_dust] +hue = [2000, 3000] # centidegrees (20-30 deg, rust orange) +sat = [1800, 3200] # bps (18-32%) +val = [3200, 4800] # bps (32-48%, dark and dusty) +# extraction_camp — mine/rig/well-is-the-town register. Darkest and +# grittiest of the industrial family; rust-streaked steel, ground dust. + +[register.oxide_and_steel] +hue = [1600, 2800] # centidegrees (16-28 deg, rust diluted toward neutral) +sat = [1000, 2200] # bps (10-22%, lower than oxide_and_dust) +val = [3600, 5200] # bps (36-52%) +# industrial_utilitarian — large-span sheds, tank farms. Steel dilutes the +# rust the extraction camp shows raw; less dust, more exposed structure. + +[register.raw_provisional] +hue = [3000, 4200] # centidegrees (30-42 deg, warm greige) +sat = [600, 1600] # bps (6-16%) +val = [5200, 6800] # bps (52-68%) +# speculative_boomtown — thrown-up-fast composite/steel. Low commitment +# color for a settlement still deciding what it is; unfinished, provisional. + +[register.hardened_earth] +hue = [2800, 3800] # centidegrees (28-38 deg, earth-neutral) +sat = [600, 1600] # bps (6-16%) +val = [3600, 5000] # bps (36-50%, dark) +# frontier_hardscrabble — hardened against a hostile primary. Dense concrete +# and rammed earth, minimal exposed surface; the darkest earth-family band. + + +# ========================================================================== +# IV. EARTH & MASONRY BASELINES — warm brown-tan mid-tones, the "everyday +# vernacular" family. Six corridor baselines that must read as siblings +# (all timber/brick/render/stone) while staying individually legible. +# ========================================================================== + +[register.mixed_warm] +hue = [1600, 3000] # centidegrees (16-30 deg, brick-orange) +sat = [1200, 2400] # bps (12-24%) +val = [4600, 6200] # bps (46-62%) +# mixed_market_vernacular — organic ground-floor-trade townscape. Warmest +# and most saturated of this section; decades of layered commercial signage. + +[register.muted_practical] +hue = [2600, 3800] # centidegrees (26-38 deg, muted brown) +sat = [800, 1800] # bps (8-18%) +val = [4600, 6000] # bps (46-60%) +# north_anglo_frontier — Commonwealth-frontier civic restraint. Weatherboard +# and brick, orderly and unshowy; the corridor's practical default. + +[register.ordered_earth] +hue = [3000, 4000] # centidegrees (30-40 deg, earth tan) +sat = [900, 1900] # bps (9-19%) +val = [5000, 6400] # bps (50-64%) +# west_germanic_ordered — disciplined massing, engineered tidiness. +# Everything has its place; the color is as restrained as the geometry. + +[register.communal_warm] +hue = [2800, 3800] # centidegrees (28-38 deg, warm, same family as ordered_earth) +sat = [1200, 2400] # bps (12-24%) +val = [5600, 7200] # bps (56-72%, brighter than ordered_earth) +# west_compact_cooperative — shared halls, civic infrastructure held in +# common. Brighter and a touch warmer than the Germanic baseline it sits +# beside -- "legible to its members" reads as more open, not more muted. + +[register.sober_masonry] +hue = [2600, 3600] # centidegrees (26-36 deg, muted brick-brown) +sat = [900, 1900] # bps (9-19%) +val = [4400, 5800] # bps (44-58%) +# central_european_blok — Polish/Czech masonry perimeter blocks. Ornament +# held in reserve; so is the color -- solid, civic, unornamented. + +[register.local_stone] +hue = [3200, 4400] # centidegrees (32-44 deg, warm stone-earth) +sat = [800, 1800] # bps (8-18%) +val = [5200, 6800] # bps (52-68%) +# frontier_surname_settlement — built from whatever the world offered. +# Self-reliant and coherent across centuries; unglamorous by design. + + +# ========================================================================== +# V. OCHRE / TERRACOTTA / SAND — warm orange-tan, mid-to-high sat and +# value, the sun-baked family. Distinguished from section IV by being +# more saturated (IV's earths are muted; these are declarative). +# ========================================================================== + +[register.warm_ochre] +hue = [3200, 4200] # centidegrees (32-42 deg, ochre-orange) +sat = [1800, 3000] # bps (18-30%) +val = [5400, 7000] # bps (54-70%) +# west_african_compound — rendered ochre walls around shared courtyards. +# Warmer and more saturated than the frontier-earth registers in section IV. + +[register.terracotta_and_lime] +hue = [1800, 2800] # centidegrees (18-28 deg, terracotta red-orange) +sat = [1600, 2800] # bps (16-28%) +val = [6000, 7600] # bps (60-76%) +# iberian_hacienda — stucco courtyards, clay-tile roofs, arcaded verandas. +# The band carries the terracotta warmth; lime-washed brightness lives at +# the high end of value. (One HSV point can't hold two named materials at +# once -- terracotta is the primary read here, lime is the value lift.) + +[register.sand_and_shade] +hue = [3600, 4600] # centidegrees (36-46 deg, sandy tan) +sat = [600, 1600] # bps (6-16%) +val = [6200, 7600] # bps (62-76%) +# arab_oasis_qanat — rammed-earth courtyard houses under harsh dryland +# light. Paler and less saturated than warm_ochre/terracotta -- sun-bleached +# rather than declarative. + + +# ========================================================================== +# VI. DEEP SATURATED CULTURAL ACCENTS — the most saturated bands in the +# file, still capped well under the entity-color saturation floor. +# Each is a specific, real-world-anchored callback, used sparingly by +# the heritage/import deviation system (D-232), not a corridor default. +# ========================================================================== + +[register.falu_red_and_pine] +hue = [600, 1400] # centidegrees (6-14 deg, deep barn red) +sat = [2200, 3400] # bps (22-34%) +val = [3200, 4600] # bps (32-46%, dark and saturated) +# nordic_timber — the iconic Scandinavian falu-red timber callback. Dark, +# saturated, unmistakably itself; pine restraint keeps it from drifting +# toward orange. + +[register.lacquer_and_timber] +hue = [200, 1000] # centidegrees (2-10 deg, deep lacquer red) +sat = [2000, 3200] # bps (20-32%) +val = [3000, 4400] # bps (30-44%, dark and lacquered) +# east_asian_temple_enclave — CJK temple-precinct callback. Darker and a +# touch closer to true red than falu_red_and_pine -- the two must not be +# confused for each other despite the shared "deep saturated red" family. + +[register.bright_painted] +hue = [1400, 2400] # centidegrees (14-24 deg, warm coral-orange) +sat = [2400, 3600] # bps (24-36%, the highest sat in the file) +val = [6400, 8000] # bps (64-80%) +# atlantic_creole_maritime — Cape-Verdean/Afro-Atlantic bright-painted +# timber and deep porches. Deliberately the boldest register in the whole +# set -- capped at 3600 bps (36%) to stay clear of the entity-color +# saturation range, so it reads as "the brightest building" without ever +# competing with a relationship-colored character standing in front of it. + + +# ========================================================================== +# VII. PASTEL & WHITEWASH — very low saturation, high value, pale-painted +# feel. Three whitewash-family callbacks that must stay distinct from +# each other at a glance. +# ========================================================================== + +[register.whitewash_and_azulejo] +hue = [3400, 4400] # centidegrees (34-44 deg, warm white cast) +sat = [300, 1000] # bps (3-10%) +val = [8000, 9200] # bps (80-92%, very pale) +# south_lusophone — whitewashed render around the praça. The azulejo blue +# tile trim is a facade/accent detail outside a single HSV band's scope; +# this band is the wall's warm-white base. + +[register.cape_whitewash] +hue = [3800, 4600] # centidegrees (38-46 deg, warm-neutral white) +sat = [300, 1000] # bps (3-10%) +val = [7800, 9000] # bps (78-90%) +# afrikaans_kraal — Cape gabled werf whitewash. Same mechanism as +# whitewash_and_azulejo; hue nudged a few degrees warmer and value nudged +# down slightly so the two whitewash registers stay tellable apart. + +[register.weathered_pastel] +hue = [16000, 18000] # centidegrees (160-180 deg, soft aqua-green pastel) +sat = [800, 1800] # bps (8-18%) +val = [6800, 8200] # bps (68-82%) +# vietnamese_water_village — canalside tube-houses. The one green-family +# register in the set, chosen for canal/water association and to stay +# clearly distinct from the file's several warm-whitewash bands above. +# FLAGGED FOR MIRI: real-world Vietnamese heritage townscapes (Hoi An) skew +# warm ochre/yellow more often than aqua -- this is a legibility trade-off +# (distinctness against 3 other pale-warm registers) over strict real-world +# color-matching. Worth a cultural sanity check; easy to re-hue if wrong. + + +# ========================================================================== +# VIII. VIOLET-COOL OUTLIER — the only violet-hued band in the file. Used +# once, deliberately, so it stays a signature rather than a habit. +# ========================================================================== + +[register.grey_stone_heather] +hue = [26000, 28000] # centidegrees (260-280 deg, violet-grey) +sat = [500, 1400] # bps (5-14%) +val = [4200, 5800] # bps (42-58%) +# scottish_highland_vernacular — drystone and slate against weather. The +# violet lean is a literal read of "heather" over grey stone; low-sat +# enough to stay a grey with a cast, not a purple building. +# FLAGGED FOR MIRI: the most interpretive hue placement in the file (the +# only band outside the neutral/warm-earth/cool-tech families) -- confirm +# it reads as "Highland stone" and not just "purple." + + +# ========================================================================== +# IX. DELIBERATELY WIDE / PLURAL — the two registers whose whole cultural +# point is variety, not a single tone. Hue bands here are 3-4x the width +# used everywhere else in this file, on purpose. +# ========================================================================== + +[register.layered_patina] +hue = [2000, 6000] # centidegrees (20-60 deg, warm brown through olive) +sat = [1000, 2400] # bps (10-24%) +val = [4800, 6800] # bps (48-68%) +# core_cosmopolitan — centuries-deep, no single grammar dominant, old +# infrastructure carrying new uses. The widest hue band in the file is the +# point: a "layered patina" city block should NOT sample as one fixed tone. + +[register.imported_accent] +hue = [1000, 5000] # centidegrees (10-50 deg, warm red-orange through tan) +sat = [1600, 3200] # bps (16-32%) +val = [5200, 7200] # bps (52-72%) +# foreign_quarter — the import-swerve template: a coherent transplant +# carried whole from another corridor (the Shinto temple in Amsterdam). +# Wide by design for the same reason as layered_patina -- it can arrive +# from anywhere, so it shouldn't sample as anywhere in particular.