Three-round design workshop (Miri, Burnelli-Sheldon, Paula; documented by Qatux, ticketed by SI) that produced D-237. Includes per-agent round outputs, round notes, the independent scout critique, and the final workshop-outcomes.md with the consolidated schema, 27-value economic vocabulary, cultural + faction vocabularies, deterministic-varied fallback spec, must-pin table, and CI guardrails. Clerk-Skip: workshop discussion docs, no D-record surface (D-237 filed separately) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
24 KiB
Round 1 — Burnelli-Sheldon
System Economic-Specialization Workshop
Lens: economic credibility + field→commodity mapping + weighting model + integration pipeline + CI guardrails.
Q1 — The Field
Proposed field: dominant_commodity_id TEXT REFERENCES commodities(commodity_id)
A single foreign-key column on system_economy pointing directly into the commodities table. One per system. Authored lore-first; the heuristic is the fallback for unspecified systems.
Why a commodity ID and not a category?
The field needs to resolve to dominant_bulk_class × dominant_production_ubiquity for D-233. The only clean source of that mapping is the commodity catalog itself — bulk_class and production_ubiquity are already on every commodity record. A separate "specialization category" field would require a second mapping table and would drift from the catalog over time. The FK is the mapping.
The BulkClass × ProductionUbiquity projection
D-233 (refined 2026-05-26) already defines the 8→5 BulkClass projection from commodities.toml:
commodities.bulk_class |
→ BulkClass (D-233) |
|---|---|
bulk |
BulkSolid |
standard |
BulkSolid |
compact |
BulkSolid |
oversized |
BulkSolid |
precision |
PrecisionDense |
liquid |
BulkLiquid |
perishable |
Perishable |
non_physical |
NonPhysical |
And the 5→4 ProductionUbiquity projection from commodities.toml:
commodities.production_ubiquity |
→ ProductionUbiquity (D-233) |
|---|---|
ubiquitous |
Ubiquitous |
common |
Common |
regional |
Common (treated as common for built-form purposes) |
concentrated |
Specialist |
monopolistic |
MonopolySource |
regional collapses to Common because the built-form distinction is between broadly distributed infrastructure (Ubiquitous/Common) and concentrated infrastructure (Specialist/MonopolySource). Regional production produces the same physical plant pattern as common — dispersed operations, no single dominant facility.
What this field is not
Services (non_physical) ARE a valid dominant_commodity_id choice. A financial center like Groombridge should pin to financial_services. This gives BulkClass = NonPhysical, which D-233 correctly resolves to dense office towers and civic halls (high coverage, 0.85–0.95 roofed fraction) with no operations surface. A service economy looks different from an extraction economy, as it should.
Relationship to economic_base_primary / economic_base_secondary
Do not replace. Coexist. The existing economic_base_primary/secondary fields are free-text narrative prose — "grain agriculture and aquaculture" (Ran), "Commission clearing house and financial arbitration" (Groombridge). They belong to the GTTR voice and are not machine-readable. Keep them.
dominant_commodity_id is the machine-readable counterpart — one FK, vocabulary-constrained, CI-validated. The two fields serve different consumers (humans vs. the generator) and should stay separate. A mismatch between the prose and the FK is detectable by CI (W-SES-02, below) and flags inconsistency for human review.
The vocabulary: all 36 commodities are valid
The complete vocabulary is the commodity_id column of commodities. Any of the 36 entries is valid:
Raw materials — meaningful for extraction-dominated systems that operate at raw scale (rare in practice; most systems export intermediates, but a frontier mining outpost can legitimately pin to metallic_ore or lattice_grade_material).
Intermediates and finals — the primary choice for most named systems. Ran → agricultural_produce or processed_food (both defensible; the question is whether Ran is mostly a raw exporter or a processed food exporter; the GTTR implies raw-to-intermediate, so agricultural_produce).
Services — valid for financial, legal, medical, and mixed-service systems. Groombridge → financial_services. The Sirius capital system → commission_certification or legal_services.
The no-pin option: dominant_commodity_id = NULL is permitted for uninhabited systems. Inhabited systems that are NULL trigger a CI hard error (V-SES-01).
Q2 — Within-System Weighted Variation
The system dominant_commodity_id sets the prior. Bodies deviate from it through a weighted deterministic draw. The algorithm is static: seed + static data only. No PressureState, no tâtonnement, no runtime query.
The candidate set (not a 36-commodity draw)
The draw operates over a small candidate set — usually 3–6 commodities — not all 36. Drawing over all 36 would produce economically incoherent results (why does a high-tech research moon in an agricultural system randomly get commission_certification?). The candidate set is:
- System anchor — always included:
system.dominant_commodity_id - Role preferred — 1–2 commodities per
economic_role(table below) - Planet-class preferred — 0–2 commodities based on
bodies.planet_class - Corp HQ commodity — 0–1 commodity if a corporation is HQ'd here, via
corp_presence.primary_operation→ commodity lookup
Weight assignment (integer, D-010 compliant)
All weights are integer basis points. No floats in the derivation path.
| Signal | Weight added | To which commodity |
|---|---|---|
| System anchor | W_SYS = 10_000 |
system.dominant_commodity_id |
Body economic_role preferred |
W_ROLE = 5_000 |
Role's preferred commodity (table below) |
Body economic_role secondary |
W_ROLE_SEC = 2_000 |
Role's secondary commodity if any |
Body planet_class preferred |
W_PLANET = 3_000 |
Planet-class preferred commodity |
Body planet_class secondary |
W_PLANET_SEC = 1_000 |
Planet-class secondary |
| Corp HQ present | W_CORP = 4_000 |
Corp's primary_operation commodity |
Role → preferred commodity mapping (fallback heuristic):
economic_role |
Primary preferred | Secondary preferred |
|---|---|---|
agricultural |
agricultural_produce |
processed_food |
extraction |
metallic_ore |
rare_minerals |
manufacturing |
refined_metals |
advanced_alloys |
financial |
financial_services |
insurance |
service_mixed |
legal_services |
medical_services |
research |
electronics |
lattice_substrate |
transit_hub |
fusion_fuel |
refined_metals |
institutional |
commission_certification |
legal_services |
military |
heavy_equipment |
advanced_alloys |
residential |
processed_food |
agricultural_produce |
Planet class → preferred commodity mapping (fallback heuristic):
planet_class |
Primary preferred | Secondary preferred |
|---|---|---|
terrestrial |
agricultural_produce |
metallic_ore |
rocky / barren |
metallic_ore |
stone |
icy / ice_giant |
water |
fusion_fuel |
gas_giant |
fusion_fuel |
— |
ocean |
agricultural_produce |
organic_compounds |
volcanic |
metallic_ore |
rare_minerals |
desert |
stone |
metallic_ore |
| (station/orbital) | (no planet bonus) | — |
These are heuristics — they apply only when dominant_commodity_id is not pinned to override.
Weighted draw algorithm (deterministic)
candidate_weights: HashMap<commodity_id, u64> = {}
// Add system anchor
candidate_weights[system.dominant_commodity_id] += W_SYS
// Add role-preferred
role_preferred = ROLE_MAP[body.economic_role]
candidate_weights[role_preferred.primary] += W_ROLE
if role_preferred.secondary:
candidate_weights[role_preferred.secondary] += W_ROLE_SEC
// Add planet class
planet_preferred = PLANET_MAP[body.planet_class]
if planet_preferred.primary:
candidate_weights[planet_preferred.primary] += W_PLANET
if planet_preferred.secondary:
candidate_weights[planet_preferred.secondary] += W_PLANET_SEC
// Add corp HQ if present
if corp_hq_commodity != NULL:
candidate_weights[corp_hq_commodity] += W_CORP
// Weighted draw (integer arithmetic only — D-010)
total_weight = sum(candidate_weights.values())
seed = SeedChain(world_seed)
.derive(SeedDomain::Layer3Settlement, fnv1a_64(body_id))
.seed()
rng = AtlasRng::new(splitmix64(seed))
draw = rng.next_u64() % total_weight
// Walk the candidate list (sorted by commodity_id for determinism)
// to find the winner
The resulting commodity is looked up in the commodity catalog for bulk_class → BulkClass and production_ubiquity → ProductionUbiquity.
The farming moon in a foundry system (worked example)
System: dominant_commodity_id = advanced_alloys (BulkSolid, Specialist — an alloying hub).
Moon: economic_role = agricultural, planet_class = terrestrial, no corp HQ.
Weight allocation:
advanced_alloys← 10_000 (system anchor)agricultural_produce← 5_000 (role primary) + 3_000 (planet class) = 8_000processed_food← 2_000 (role secondary)metallic_ore← 1_000 (planet class secondary)
Total weight: 21_000.
Draw probabilities: advanced_alloys 47.6%, agricultural_produce 38.1%, processed_food 9.5%, metallic_ore 4.8%.
The moon is a farming moon about 38% of seeds. The system still reads as a foundry system because most bodies (those without strongly competing role signals) resolve to advanced_alloys. The farming moon exists; it is not the dominant read.
Why W_SYS = 10_000 specifically
This ratio is calibrated so that a body with two strong competing signals (both role AND planet class pointing away from the system anchor) still resolves to the system anchor ~33% of the time. That threshold keeps system identity legible: a 5-body system will usually have 3–4 bodies expressing the system character. If W_SYS were lower (e.g., 5_000), too many bodies would deviate, destroying the authored read. If higher (e.g., 15_000), the minority body (the farming moon) becomes nearly impossible, which is also wrong.
These ratios can be tuned in a single named-constant block without touching algorithm logic. They are NOT authored per-system.
Q3 — economy_size?
Out. Don't add it.
This is the first Q where I'll be direct: economy_size doesn't earn its slot because it is substantially covered by fields that already exist.
What economy_size would modify vs. what already exists
The proposed modifications for economy_size are:
- Coverage density — already modulated by
density_class(D-220), which is derived frompopulationandeconomic_role. - Count of specialized blocks — already modulated by settlement footprint size, which is derived from
populationandprosperity_baseline(D-197). - Labor draw for residential follow-on — already set by
BulkClassextraction multiplier (D-233).
Every function that economy_size would provide is already covered. Adding it introduces a third authoritative source for the same signal, which creates inconsistency problems:
- If
economy_size = majorbutpopulation = 50_000, which wins? - If
economy_size = minorbutprosperity_baseline = 0.85(high prosperity), do we compress the settlement?
The answers require tiebreaker rules, which are complexity with no payoff.
The genuine case it might address
The one scenario where economy_size might seem necessary: a low-population monopoly extraction site that should have massive industrial infrastructure. A lattice_grade_material mine with 10,000 workers that ships to the entire galaxy.
This is already handled:
production_ubiquity = monopolistic→MonopolySource→ D-233 concentration rule: "MonopolySource concentrates contiguous block groups (the mine is the settlement, everything else is support)." The infrastructure is there.prosperity_baselinefor an extraction site with corp HQ presence will be elevated.density_classfor an extraction chokepoint with economic pull is Dense (D-220: "extraction chokepoints... pull people in faster").
A 10,000-person monopoly mine reads correctly under the existing system: low population, high density (terrain-constrained + economic pull), MonopolySource concentration, elevated prosperity (corp-funded infrastructure). No economy_size field required.
Verdict
economy_size is redundant with population × prosperity_baseline. If those two values are incorrectly authored for a system (e.g., the system wiki says it's a major economic hub but the population is set to 50K), that's an authoring error to fix — not a signal to add a third field that masks it.
Q4 — The Content Pass: Field→Commodity Mapping + CI Guardrails + Integration
Source location
New TOML file: wiki/economics/system_specialization.toml
One entry per system that has been authored. Uninhabited and unspecified systems have no entry (CI will hard-error if an inhabited system has no entry after the pass is complete).
# wiki/economics/system_specialization.toml
# Source of truth for authored dominant commodity per star system.
# Compiled to system_economy.dominant_commodity_id via import_economics.py.
# FK: must exist in wiki/economics/commodities.toml
#
# Decisions: D-233, D-184, this workshop (re-amends D-233)
[GJ-144] # Ran — core agricultural breadbasket
dominant_commodity_id = "agricultural_produce"
[GJ-273] # Groombridge — Assembly clearing house
dominant_commodity_id = "financial_services"
[GJ-15A] # Sirius — Commission/Assembly capital
dominant_commodity_id = "commission_certification"
# ... ~300 systems total
One line per system. The comment is optional documentation for human reviewers. The TOML key is system_id (matching the star_systems.system_id column).
Database integration
Add one column to system_economy in systems-schema.sql:
ALTER TABLE system_economy ADD COLUMN
dominant_commodity_id TEXT REFERENCES commodities(commodity_id);
Add to COLUMN_MIGRATIONS in import_economics.py (idempotent ALTER TABLE).
New import step in import_economics.py:
def import_system_specialization(conn, dry_run=False):
"""Read wiki/economics/system_specialization.toml and upsert
dominant_commodity_id into system_economy for authored systems."""
# Read TOML
# Validate FK against commodity catalog (V-SES-01, V-SES-02)
# UPSERT into system_economy
# Log coverage report
Source file added to IMPORT_ECONOMICS_SOURCES for meta stamp tracking.
CI guardrails
Hard errors (build aborts):
| ID | Rule | Rationale |
|---|---|---|
| V-SES-01 | Every system with bodies.inhabited > 0 AND system_economy.population > 0 MUST have dominant_commodity_id after the content pass is marked complete. Before the pass is complete: warn only. |
Guarantees generator has authored data for every city it generates. |
| V-SES-02 | dominant_commodity_id must exist in commodities(commodity_id) |
FK integrity — typos produce silent fallbacks otherwise. |
| V-SES-03 | Systems where dominant_commodity_id maps to a service commodity (tier IN ('service_professional', 'service_luxury')) but whose bodies are all non-inhabited or uninhabited MUST be flagged |
Services require population to be plausible. A services-dominant system with no bodies makes no sense. |
Soft warnings (build succeeds, prints):
| ID | Rule | Rationale |
|---|---|---|
| W-SES-01 | production_ubiquity = 'monopolistic' systems: print list for human review |
Monopoly pins are load-bearing and easy to over-assign. Lore must support the monopoly claim (D-177). |
| W-SES-02 | Systems where economic_base_primary (prose) contains words that don't match the commodity name/description of dominant_commodity_id |
Contradiction detection between narrative prose and the machine field. Not a hard error — authors may intentionally be more specific in prose. |
| W-SES-03 | dominant_commodity_id = water OR agricultural_produce for named, non-frontier systems |
Water and agricultural produce are ubiquitous. A named core system claiming water as its identity is probably wrong (every system produces water). Exception: a system whose entire identity is regional food export may legitimately pin to agricultural_produce. Flag for review, don't abort. |
| W-SES-04 | More than 20% of inhabited systems pinned to the same dominant_commodity_id |
A balanced economy needs diversity. If 60 systems all claim refined_metals, the catalog entry is being used as a generic default, not an authored identity. |
Coverage report (new output from import_economics.py)
After the import, print:
system_specialization coverage:
authored: NNN / MMM inhabited systems (NN%)
by BulkClass: BulkSolid=NN, BulkLiquid=NN, Perishable=NN, PrecisionDense=NN, NonPhysical=NN
by ProductionUbiquity: Ubiquitous=NN, Common=NN, Specialist=NN, MonopolySource=NN
MonopolySource systems: [list]
This gives the content team a live view of market structure as they author. A healthy distribution avoids over-concentration in any one BulkClass or ubiquity tier.
Who authors what (the 300-system content pass)
Miri: Canonical commodity for every named/hero system from GTTR lore. The economic_base_primary prose is the input signal; Miri determines which of the 36 commodities best represents the system's authored identity. Source of truth on Ran (agricultural_produce), Groombridge (financial_services), the east reach lattice systems (lattice_grade_material), etc.
Paula: Faction homeworlds, Compact systems, and political capitals. A Compact homeworld's dominant commodity is not just economic — it's a political statement. The Mark-primary zone currency choice and the shadow economy intensity are correlated with the commodity choice (Compact: tends toward energy independence → fusion_fuel, or organic-based industries independent of Commission certification routes).
Burnelli-Sheldon (me): Economic credibility review of all ~300 entries. Specifically:
- Monopoly-class check: Does the lore actually support a
monopolisticproduction_ubiquity pin? (D-177 constraints are the test — Kvitfjell marble, brach fiber, Calloway terroir, VGV varietals are the canon examples of genuine monopoly-class production. Assign monopolistic sparingly.) - Distribution balance: Check that the final distribution of commodity pins across 300 systems is plausible. An economy where 80% of systems are NonPhysical services is not credible.
- Field→BulkClass sanity: Does the built form make sense? A financial-services dominant system should not look like a mine. If a pin produces an unexpected BulkClass (e.g.,
water→ BulkLiquid for a city), flag for reconsideration. - Heuristic fallback validation: For uninhabited/unnamed systems that get the fallback heuristic (not authored), spot-check that the role+planet_class heuristic produces plausible results for representative cases.
Authoring tool expectation
The content pass is a text editor + TOML file workflow. No specialized tool needed. The make regen-db build validates every entry against the commodity catalog. A companion query helps authors check consistency:
tooling/db/sqlite-query "
SELECT se.system_id, se.dominant_commodity_id,
c.bulk_class, c.production_ubiquity,
se.economic_base_primary
FROM system_economy se
JOIN commodities c ON c.commodity_id = se.dominant_commodity_id
WHERE c.production_ubiquity = 'monopolistic'
ORDER BY se.system_id"
Q5 — Additional Identity Fields
The question is: while touching all ~300 systems by hand once, what other 1–2 fields meaningfully flesh out identity at marginal additional authoring cost? The guard is explicit: every field is authoring burden ×300 and a maintenance surface.
My recommendation: ONE additional field
atlas_body_trait_bias hero pins (D-232) — this is the architecture-flavor anchor for hero bodies (~30–40). It is technically a per-body field, not a per-system field, but it is authored during the same pass since the hero body is the system's primary reading body.
This earns its slot on one condition: it is authored ONLY for hero bodies, not for all 300 systems. The ~270 non-hero bodies run the algorithm with no bias (D-232 is explicit: "Non-hero bodies run the identical algorithm with no bias"). So the authoring burden is ~30–40 entries (one per hero body), not 300.
From my economic-credibility lens, the trait pin is justified because:
- D-232's hard-gate economics (bulk_class, prosperity, production_ubiquity) already constrain the template pool. The pin selects within the economically-valid set, not outside it.
- The wrong architectural read on a canonical system is expensive — it cannot be corrected by a post-launch patch without breaking determinism (D-010).
- The trait catalog is designed with pin support (
pincounts toward K, K=1 for Minimal complexity tier).
However: the actual atlas_body_trait_bias pins are Miri + Araminta territory (cultural meaning + visual bundle). I'll defer to them on which ~30–40 bodies get a pin. My role is to confirm that the economic eligibility rules don't need to be overridden to support the intended templates.
Fields I recommend against
economy_size — eliminated in Q3.
trade_orientation (export/import/self-sufficient) — Derivable from dominant_commodity_id × production_ubiquity. A MonopolySource system is obviously export-dominant. A NonPhysical services system is self-contained (services don't traverse gates). An Ubiquitous raw material system may import more than it exports (since ubiquitous goods have thin margins and don't drive trade specialization). Authoring this separately introduces a second signal that can contradict the FK. Don't add it.
political_register — Paula's domain. Plausibly valuable for faction-political characterization, but I should not advocate for fields outside my lane. I will say only that from an economic integration perspective, the political register would not touch the commodity-building pipeline at all — it would be consumed by NPC generation and dialogue, not by the generator. That makes it zero authoring burden overlap with the commodity pass.
shadow_economy_intensity — D-174 explicitly defines this as derived from inputs (Commission presence, Compact membership, hop distance, gate topology), not authored per-system. The derivation model is correct. Don't replace it with an authored field; the emergent calculation is the point.
Summary of recommended field set
| Field | Scope | Authoring burden | Earns slot? |
|---|---|---|---|
dominant_commodity_id |
Per system (system_economy) | ~300 entries | YES — primary deliverable |
atlas_body_trait_bias pins |
Per hero body (~30–40) | ~30–40 entries | YES — but limited to hero bodies only |
economy_size |
Per system | ~300 entries | NO — redundant |
trade_orientation |
Per system | ~300 entries | NO — derivable |
political_register |
Per system | ~300 entries | OUT OF LANE — Paula opines |
Integration summary
The proposed change to the existing pipeline is minimal:
wiki/economics/system_specialization.toml— new authored source file. Onedominant_commodity_idper inhabited system.systems-schema.sql— adddominant_commodity_id TEXT REFERENCES commodities(commodity_id)tosystem_economy.import_economics.py— addimport_system_specialization()step, five new CI validation rules, coverage report output.city_context_reader.rs— replace the two#982 design-blockedstub fields with a DB query that reads the commodity catalog join, runs the weighted draw algorithm, and populatesdominant_bulk_class+dominant_production_ubiquity.MIGRATION_SQLinimport_economics.py—ALTER TABLE system_economy ADD COLUMN dominant_commodity_id TEXT.
The existing economic_base_primary/secondary fields, the economic_role field on bodies, and the system_factions data are all preserved and unchanged. The new field plugs into existing signals; it does not replace them.
Everything is static: read at build time into systems.db, resolved at generation-dispatch time by CityContextReader, no PressureState, no tâtonnement, no save-state. The per-body weighted draw uses SeedChain(world_seed, body_id) — deterministic, D-010 compliant, re-derivable from seed + static inputs alone.