feat(db): architecture zone-bias + color-register-band tables + V-TT-06/07 (T-988)

Ratified content baked into systems.db as two new tables:
- architecture_zone_bias (57 rows): sparse per-template, per-zone_type
  token-weight overrides (integer bps), Miri-authored — the D-235 step-2
  zone bias. V-TT-06: every token must exist in that template's own
  visual_bundle axis.
- color_register_bands (28 rows): per color_register integer HSV bands
  (hue centidegrees, sat/val bps), Araminta-authored. V-TT-07: full
  catalog coverage + valid integer bounds (min<max, in range).

Both TOMLs registered in generator_sources (stamp coverage); DDL in
systems-schema.sql + migration.py; validation wired into import_economics
steps 18/19 and test_traits.py failure-branch units (make test-tooling).
systems.db regenerated + re-stamped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 09:03:19 +02:00
co-authored by Claude Fable 5
parent 14b2eb69c5
commit ba781f9324
10 changed files with 1147 additions and 4 deletions
+30
View File
@@ -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
Binary file not shown.
@@ -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');
@@ -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",
+189
View File
@@ -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.<template_tag>.<zone_type_id>]`
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.<axis>` (the
generator's job, not this baker's).
V-TT-06: every referenced token must already appear in THAT template's own
`visual_bundle.<axis>` (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.<name>]` 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).
+13 -1
View File
@@ -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...")
+168 -3
View File
@@ -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)
+13
View File
@@ -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,
)
# ---------------------------------------------------------------------------
+380
View File
@@ -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.<axis>
# 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.<template_tag>.<zone_type_id>]
# wall = { <wall_token> = <bps>, ... } # optional per axis
# roof = { <roof_token> = <bps>, ... }
# facade = { <facade_token> = <bps>, ... }
# street = { <street_token> = <bps>, ... }
# ==========================================================================
# ==========================================================================
# 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."
# ==========================================================================
+330
View File
@@ -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.<value>]` 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.