Files
settled-reach/docs/workshops/system-economic-specialization/burnelli-sheldon-round1.md
T
jpmschweitzerandClaude Opus 4.8 33c083f0ef docs(workshops): system-economic-specialization rounds + outcomes
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>
2026-05-31 18:40:08 +02:00

24 KiB
Raw Blame History

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.850.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 36 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:

  1. System anchor — always included: system.dominant_commodity_id
  2. Role preferred — 12 commodities per economic_role (table below)
  3. Planet-class preferred — 02 commodities based on bodies.planet_class
  4. Corp HQ commodity — 01 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_classBulkClass and production_ubiquityProductionUbiquity.

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_000
  • processed_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 34 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:

  1. Coverage density — already modulated by density_class (D-220), which is derived from population and economic_role.
  2. Count of specialized blocks — already modulated by settlement footprint size, which is derived from population and prosperity_baseline (D-197).
  3. Labor draw for residential follow-on — already set by BulkClass extraction 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 = major but population = 50_000, which wins?
  • If economy_size = minor but prosperity_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 = monopolisticMonopolySource → D-233 concentration rule: "MonopolySource concentrates contiguous block groups (the mine is the settlement, everything else is support)." The infrastructure is there.
  • prosperity_baseline for an extraction site with corp HQ presence will be elevated.
  • density_class for 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:

  1. Monopoly-class check: Does the lore actually support a monopolistic production_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.)
  2. 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.
  3. 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.
  4. 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 12 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 (~3040). 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 ~3040 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 (pin counts 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 ~3040 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.

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 (~3040) ~3040 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:

  1. wiki/economics/system_specialization.toml — new authored source file. One dominant_commodity_id per inhabited system.
  2. systems-schema.sql — add dominant_commodity_id TEXT REFERENCES commodities(commodity_id) to system_economy.
  3. import_economics.py — add import_system_specialization() step, five new CI validation rules, coverage report output.
  4. city_context_reader.rs — replace the two #982 design-blocked stub fields with a DB query that reads the commodity catalog join, runs the weighted draw algorithm, and populates dominant_bulk_class + dominant_production_ubiquity.
  5. MIGRATION_SQL in import_economics.pyALTER 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.