The sole generator of systems.db moves to tooling/domains/ledger/ and is now `reach ledger import`. economy_import/ keeps its name (Rust comments in server/src cite it); the entrypoint becomes service.py; schema_version.py moves with the importer, which is where the version is defined. The stamp survived the move, which is the thing that had to hold: - generated_brands.toml is byte-identical (sha256 e748531…) before and after - `reach check systems-db-stamp` reported STALE after the move (the registry saw it) and OK after the regen - the dry-run carries every count and warning of the baseline transcript, and exit 2 — imported and stamped, coverage gate unmet — still reaches the caller through @command `make regen-db` survives as a one-line delegate, per D-263's muscle-memory clause: about fifty files name it, including the headers of generated wiki TOMLs and the remedies the push gate prints. `make economy-db` is retired; it ran `reach generate brands` before the import, which the import already does as its first step. economy_import.errors is reconciled as DOMAINS.md asked. ImportAborted stays as internal rollback control flow and never reaches a caller; the service converts it to a ReachError carrying the remedy. regenerate_brands caught cargo_binary's ReachError, printed it and raised ImportAborted, dropping the remedy. It runs before the import transaction opens, so there is nothing to roll back — it now propagates. Both sys.path bootstraps are gone; they existed only because the directory was hyphenated. The step labels ran [1/10]…[10/13]…[17/19]; one 24-step counter now drives the event phase and progress. Stale pointers fixed on the way: MIGRATION_SQL has lived in economy_import/migration.py since T-1067, but the asset-pipeline rule, DEVOPS and the schema comments still sent readers to import_economics.py; the rule and DEVOPS also still named the check scripts T-1281 retired. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
267 lines
11 KiB
Python
267 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 replaced three formerly separate lists (in the importer, in the
|
|
old check-systems-db-stamp script, and in the pr-process skill).
|
|
|
|
Consumers:
|
|
- the ledger importer (tooling/domains/ledger/) — imports
|
|
``IMPORT_ECONOMICS_SOURCES`` and ``file_sha1`` to write the stamp after a
|
|
successful run.
|
|
- ``reach check systems-db-stamp`` — imports ``GENERATOR_SOURCES`` and
|
|
``file_sha1`` to verify the stamp before a push.
|
|
- ``reach pr watchlist-diff`` — reads ``GENERATOR_SOURCES`` for its watch set.
|
|
- /pr-process (skill) — shells out to the ``--list`` CLI for its watch list.
|
|
|
|
Stays a module rather than a verb (tooling/DOMAINS.md): it is a list of paths
|
|
read by gates, and nothing types it.
|
|
|
|
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" / "domains" / "ledger" / "service.py"
|
|
)
|
|
ECONOMY_IMPORT_PACKAGE_DIR: Path = (
|
|
REPO_ROOT / "tooling" / "domains" / "ledger" / "economy_import"
|
|
)
|
|
|
|
# Rust sources for the generate_brands subroutine. import_economics runs the
|
|
# generate_brands binary 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"
|
|
)
|
|
# The `tooling/generate-brands` bash wrapper used to be stamped here. It was one
|
|
# of three identical copies of build-if-missing-then-exec and was retired into
|
|
# core.process.cargo_binary (T-1286); the seed argument it carried now lives in
|
|
# economy_import/brands.py, which is already stamped via the package directory
|
|
# below. Nothing about the brand output stopped being covered.
|
|
|
|
# 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 + core/process.py), 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,
|
|
REPO_ROOT / "tooling" / "core" / "process.py", # runs the brand binary (T-1286)
|
|
REPO_ROOT / "tooling" / "domains" / "ledger" / "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
|
|
# `reach atlas planet` importers (import-heightmaps, import-provinces) 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()
|