H1+H2: headquarters_body is reset+derived every run (DB is never source); optional authored frontmatter override, hard-validated; tiebreak now population DESC -> city-bearing body -> type rank -> body_id, preserving belt tenancies and fixing GJ702B to GJ702Bb. T2: NULL-reset pass for corp_specialization/hq_placement before authored re-apply (poison-tested). T1: wiki/corporations/*.md globbed into IMPORT_ECONOMICS_SOURCES. M1: licensed_clinical_services vocabulary value (31st, NonPhysical->CityTenant) + somatic-futures retag. Fixpoint verified stable across 4 consecutive regens (0 diffs); regen systems.db. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
262 lines
11 KiB
Python
262 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
generator_sources — single source of truth for systems.db generator source sets.
|
|
|
|
Each generator that writes a stamp row to systems.db's ``meta`` table records the
|
|
SHA-1 of the concatenated bytes of its source files (sorted by path). This module
|
|
is the ONE place that defines which files belong to each generator's stamped set
|
|
(T-1067). It replaces the formerly triplicated lists in:
|
|
|
|
- ``IMPORT_ECONOMICS_SOURCES`` in tooling/economy-db/import_economics.py
|
|
- ``GENERATOR_SOURCES`` in tooling/check-systems-db-stamp
|
|
- the hardcoded watch list in .claude/skills/pr-process/SKILL.md
|
|
(now derived via ``python3 tooling/generator_sources.py --list``)
|
|
|
|
Consumers:
|
|
- tooling/economy-db (the importer) — imports ``IMPORT_ECONOMICS_SOURCES`` and
|
|
``file_sha1`` to write the stamp after a successful run.
|
|
- tooling/check-systems-db-stamp — imports ``GENERATOR_SOURCES`` and
|
|
``file_sha1`` to verify the stamp before a push.
|
|
- /pr-process (skill) — shells out to the ``--list`` CLI for its watch list.
|
|
|
|
This file is itself a member of the stamped source set: a registry change (adding
|
|
or removing a source) must flip the stamp and force ``make regen-db``, otherwise
|
|
edits to the list would be invisible to staleness detection.
|
|
|
|
The economy_import package modules are collected by glob rather than listed by
|
|
hand, so a module added during a future split is stamped automatically — a
|
|
hand-maintained list that misses a module silently weakens staleness detection.
|
|
|
|
Usage:
|
|
python3 tooling/generator_sources.py --list [generator_name]
|
|
"""
|
|
|
|
import argparse
|
|
import hashlib
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# import_economics source set
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Entrypoint + split module package (T-1067).
|
|
IMPORT_ECONOMICS_ENTRYPOINT: Path = (
|
|
REPO_ROOT / "tooling" / "economy-db" / "import_economics.py"
|
|
)
|
|
ECONOMY_IMPORT_PACKAGE_DIR: Path = (
|
|
REPO_ROOT / "tooling" / "economy-db" / "economy_import"
|
|
)
|
|
|
|
# Rust sources for the generate_brands subroutine. import_economics shells out
|
|
# to tooling/generate-brands as part of its normal flow (see economy_import/
|
|
# brands.py), so all of these contribute to its effective source SHA: any change
|
|
# to them must invalidate the meta stamp even though Python hasn't changed.
|
|
GENERATE_BRANDS_RS: Path = (
|
|
REPO_ROOT / "server" / "src" / "bin" / "generate_brands" / "main.rs"
|
|
)
|
|
GENERATE_BRANDS_NAMES_RS: Path = (
|
|
REPO_ROOT / "server" / "src" / "bin" / "generate_brands" / "names.rs"
|
|
)
|
|
# Shared surname corpus extracted from the two names.rs copies (T-1064).
|
|
GENERATE_BRANDS_SURNAMES_RS: Path = (
|
|
REPO_ROOT / "server" / "src" / "bin" / "shared" / "surname_corpus.rs"
|
|
)
|
|
GENERATE_BRANDS_WRAPPER: Path = REPO_ROOT / "tooling" / "generate-brands"
|
|
|
|
# D-237 authored specialization layer: these data TOMLs feed the DB, so a change
|
|
# to either must flip the stamp and force a regen (#1013).
|
|
SPECIALIZATION_VOCAB_TOML: Path = (
|
|
REPO_ROOT / "wiki" / "economics" / "specialization_vocabulary.toml"
|
|
)
|
|
SYSTEM_SPECIALIZATION_TOML: Path = (
|
|
REPO_ROOT / "wiki" / "economics" / "system_specialization.toml"
|
|
)
|
|
# D-242 corp-HQ settlement model (T-1074): specialization -> {CityTenant,
|
|
# Standalone} placement map, keyed on the reused D-237 vocabulary above.
|
|
CORP_HQ_PLACEMENT_TOML: Path = (
|
|
REPO_ROOT / "wiki" / "economics" / "corp_hq_placement.toml"
|
|
)
|
|
# The corp wiki pages themselves (PR #177 review T1): their frontmatter
|
|
# (corp_specialization, headquarters, optional headquarters_body) now shapes
|
|
# the DB — specialization drives hq_placement, headquarters_body, and the
|
|
# Standalone-HQ settlement rows in atlas_city_names — so a page edit without a
|
|
# regen must trip the stamp check exactly like a TOML edit does. Defined here
|
|
# (not in economy_import/paths.py) per the stamped-paths-live-here convention;
|
|
# paths.py re-exports it.
|
|
CORPORATIONS_DIR: Path = REPO_ROOT / "wiki" / "corporations"
|
|
# D-242 population/settlement_class bake (T-1075): the small authored
|
|
# NameLocked hero-city override list.
|
|
SETTLEMENT_NAME_LOCKED_TOML: Path = (
|
|
REPO_ROOT / "wiki" / "economics" / "settlement_name_locked.toml"
|
|
)
|
|
# D-232 architecture-flavor trait-template catalog + sparse hero bias (#993).
|
|
ARCHITECTURE_TRAIT_CATALOG_TOML: Path = (
|
|
REPO_ROOT / "wiki" / "economics" / "architecture_trait_catalog.toml"
|
|
)
|
|
ARCHITECTURE_TRAIT_BIAS_TOML: Path = (
|
|
REPO_ROOT / "wiki" / "economics" / "architecture_trait_bias.toml"
|
|
)
|
|
# ObjectTag vocabulary registry (T-995, Q-049): read by traits.py alongside the
|
|
# catalog above to validate allow_tags/block_tags/visual_bundle references and
|
|
# the fallback-graph (V-TT-03/V-TT-04), so a registry edit must flip the stamp
|
|
# exactly like a catalog edit does.
|
|
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, ...]:
|
|
"""All Python modules of the economy_import package, collected by glob.
|
|
|
|
Globbing (rather than hand-listing) guarantees a future module is stamped
|
|
the moment it exists. Fail closed if the package is missing — an empty
|
|
set here would silently weaken staleness detection.
|
|
"""
|
|
modules = tuple(sorted(ECONOMY_IMPORT_PACKAGE_DIR.glob("*.py")))
|
|
if not modules:
|
|
raise RuntimeError(
|
|
f"economy_import package not found at {ECONOMY_IMPORT_PACKAGE_DIR} — "
|
|
"the import_economics stamp source set would be incomplete"
|
|
)
|
|
return modules
|
|
|
|
|
|
def _corporation_pages() -> tuple[Path, ...]:
|
|
"""All corp wiki pages the importer reads, collected by glob (T1, PR #177).
|
|
|
|
`load_wiki_corps` (economy_import/corporations.py) reads every
|
|
wiki/corporations/*.md EXCEPT index.md — the frontmatter feeds
|
|
corp_specialization/hq_placement/headquarters_body and the Standalone-HQ
|
|
settlement rows, so the read-set is stamped with the same glob-not-list +
|
|
fail-closed discipline as `_economy_import_modules`. index.md is excluded
|
|
because the importer skips it by name: stamping it would flag a false
|
|
stale on edits that cannot change DB output.
|
|
"""
|
|
pages = tuple(
|
|
sorted(p for p in CORPORATIONS_DIR.glob("*.md") if p.name != "index.md")
|
|
)
|
|
if not pages:
|
|
raise RuntimeError(
|
|
f"corporation pages not found at {CORPORATIONS_DIR} — "
|
|
"the import_economics stamp source set would be incomplete"
|
|
)
|
|
return pages
|
|
|
|
|
|
# Canonical source set for import_economics' meta stamp. Covers the Python
|
|
# entrypoint and its module package, the Rust binary it invokes (generate_brands
|
|
# main.rs + names.rs + surname corpus + wrapper script), the shared schema
|
|
# version constant, the authored data TOMLs, the corp wiki pages whose
|
|
# frontmatter the importer bakes (T1, PR #177), and this registry itself.
|
|
IMPORT_ECONOMICS_SOURCES: tuple[Path, ...] = (
|
|
Path(__file__).resolve(), # registry changes must stale the stamp
|
|
IMPORT_ECONOMICS_ENTRYPOINT,
|
|
*_economy_import_modules(),
|
|
*_corporation_pages(),
|
|
GENERATE_BRANDS_RS,
|
|
GENERATE_BRANDS_NAMES_RS,
|
|
GENERATE_BRANDS_SURNAMES_RS,
|
|
GENERATE_BRANDS_WRAPPER,
|
|
REPO_ROOT / "tooling" / "schema_version.py",
|
|
SPECIALIZATION_VOCAB_TOML,
|
|
SYSTEM_SPECIALIZATION_TOML,
|
|
CORP_HQ_PLACEMENT_TOML,
|
|
SETTLEMENT_NAME_LOCKED_TOML,
|
|
ARCHITECTURE_TRAIT_CATALOG_TOML,
|
|
ARCHITECTURE_TRAIT_BIAS_TOML,
|
|
OBJECT_TAG_VOCABULARY_TOML,
|
|
ARCHITECTURE_ZONE_BIAS_TOML,
|
|
COLOR_REGISTER_BANDS_TOML,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Registry
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Maps generator_name (as stored in meta.generator_name) to the source file(s)
|
|
# whose SHA is stamped. The SHA is computed as SHA-1 of the concatenated bytes
|
|
# of all files in sorted order.
|
|
#
|
|
# The atlas geometry generator (generate_atlas) was retired in #951 (D-223).
|
|
# import_economics is now the sole regen-db generator that writes systems.db; it
|
|
# owns the atlas index (names-only pool + empty geometry tables). The surviving
|
|
# planet-gen importers (import_heightmaps, import_province_boundaries) are
|
|
# one-time build imports baked into the committed DB, not part of regen-db, so
|
|
# they are intentionally not stamped here.
|
|
GENERATOR_SOURCES: dict[str, tuple[Path, ...]] = {
|
|
"import_economics": IMPORT_ECONOMICS_SOURCES,
|
|
}
|
|
|
|
|
|
def file_sha1(*paths: Path) -> str:
|
|
"""SHA-1 of concatenated file contents (sorted paths).
|
|
|
|
Missing files raise FileNotFoundError rather than silently contributing
|
|
an empty-string hash (H2): a ghost SHA could mask real breakage when
|
|
stored and current SHAs converge on the empty-bytes digest.
|
|
"""
|
|
h = hashlib.sha1()
|
|
for p in sorted(paths):
|
|
if not p.exists():
|
|
raise FileNotFoundError(f"generator source not found: {p}")
|
|
h.update(p.read_bytes())
|
|
return h.hexdigest()
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(
|
|
description="Single source of truth for systems.db generator source sets"
|
|
)
|
|
parser.add_argument(
|
|
"--list",
|
|
action="store_true",
|
|
help="Print the stamped source files, one repo-relative path per line",
|
|
)
|
|
parser.add_argument(
|
|
"generator",
|
|
nargs="?",
|
|
default=None,
|
|
help="Restrict --list to one generator (default: all)",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
if not args.list:
|
|
parser.print_help()
|
|
sys.exit(2)
|
|
|
|
if args.generator is not None:
|
|
if args.generator not in GENERATOR_SOURCES:
|
|
print(
|
|
f"error: unknown generator '{args.generator}' — "
|
|
f"known: {sorted(GENERATOR_SOURCES)}",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(1)
|
|
names = [args.generator]
|
|
else:
|
|
names = sorted(GENERATOR_SOURCES)
|
|
|
|
seen: set[Path] = set()
|
|
for name in names:
|
|
seen.update(GENERATOR_SOURCES[name])
|
|
for path in sorted(seen):
|
|
print(path.relative_to(REPO_ROOT).as_posix())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|