diff --git a/crates/misaligned-assets/src/bin/misaligned-effects.rs b/crates/misaligned-assets/src/bin/misaligned-effects.rs index e84e4316..2a972eaa 100644 --- a/crates/misaligned-assets/src/bin/misaligned-effects.rs +++ b/crates/misaligned-assets/src/bin/misaligned-effects.rs @@ -72,6 +72,35 @@ struct ShotHarness { taken: bool, } +/// Exact deterministic screenshot surface. Keep sorted: the environment +/// registry gate compares this runtime allow-list with `wiki/engineering/env.md`. +const EFFECT_SHOT_KINDS: &[&str] = &[ + "both", + "both_close", + "both_far", + "both_medium", + "lineup", + "lineup_close", + "lineup_far", + "lineup_medium", + "pool-lineup", + "pool-lineup_close", + "pool-lineup_far", + "pool-lineup_medium", + "pools", + "pools_close", + "pools_far", + "pools_medium", + "route", + "route_close", + "route_far", + "route_medium", + "stress", + "stress_close", + "stress_far", + "stress_medium", +]; + fn main() { let shot = shot_settings(); let (preset, forced_lod) = shot @@ -148,13 +177,27 @@ fn main() { fn shot_settings() -> Option<(LabPreset, EffectLod, String)> { let kind = std::env::var("MISALIGNED_SHOT").ok()?; + let Some((preset, lod)) = effect_shot_from_kind(&kind) else { + eprintln!( + "unknown MISALIGNED_SHOT={kind:?} for misaligned-effects; expected one of: {}", + EFFECT_SHOT_KINDS.join(", ") + ); + std::process::exit(2); + }; + let path = std::env::var("MISALIGNED_SHOT_PATH") + .unwrap_or_else(|_| format!("misaligned_shot_{kind}.png")); + Some((preset, lod, path)) +} + +fn effect_shot_from_kind(kind: &str) -> Option<(LabPreset, EffectLod)> { + EFFECT_SHOT_KINDS.binary_search(&kind).ok()?; let mut parts = kind.rsplitn(2, '_'); let suffix = parts.next().unwrap_or_default(); let (base, lod) = match suffix { "close" => (parts.next().unwrap_or("both"), EffectLod::Close), "medium" => (parts.next().unwrap_or("both"), EffectLod::Medium), "far" => (parts.next().unwrap_or("both"), EffectLod::Far), - _ => (kind.as_str(), EffectLod::Close), + _ => (kind, EffectLod::Close), }; let preset = match base { "route" => LabPreset::Route, @@ -165,9 +208,7 @@ fn shot_settings() -> Option<(LabPreset, EffectLod, String)> { "pool-lineup" => LabPreset::PoolLineup, _ => return None, }; - let path = std::env::var("MISALIGNED_SHOT_PATH") - .unwrap_or_else(|_| format!("misaligned_shot_{kind}.png")); - Some((preset, lod, path)) + Some((preset, lod)) } fn setup( @@ -605,3 +646,25 @@ fn shot_harness_system( exit.write(AppExit::Success); } } + +#[cfg(test)] +mod shot_catalog_tests { + use super::*; + + #[test] + fn exact_catalog_is_sorted_unique_and_parseable() { + assert!(EFFECT_SHOT_KINDS.windows(2).all(|pair| pair[0] < pair[1])); + for kind in EFFECT_SHOT_KINDS { + assert!( + effect_shot_from_kind(kind).is_some(), + "catalog kind {kind:?} must stage an effects-lab shot" + ); + } + } + + #[test] + fn unknown_shot_kind_is_rejected() { + assert_eq!(effect_shot_from_kind("lineup_near"), None); + assert_eq!(effect_shot_from_kind("unknown"), None); + } +} diff --git a/tools/env_registry_gate.py b/tools/env_registry_gate.py index 7955beb8..1467aa1f 100755 --- a/tools/env_registry_gate.py +++ b/tools/env_registry_gate.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Validate environment-variable registration and the Bevy shot catalog.""" +"""Validate environment-variable registration and exact shot catalogs.""" from __future__ import annotations @@ -11,13 +11,23 @@ from pathlib import Path ENV_NAME = re.compile(r"\bMISALIGNED_[A-Z][A-Z0-9_]*\b") CORE_ENV_READ = re.compile(r"\b(?:std::)?env::var(?:_os)?\s*\(") -BEVY_CATALOG = re.compile( - r"const\s+BEVY_SHOT_KINDS\s*:\s*&\[&str\]\s*=\s*&\[(.*?)\];", - re.DOTALL, -) -RUST_STRING = re.compile(r'"([a-z0-9][a-z0-9-]*)"') +RUST_STRING = re.compile(r'"([a-z0-9][a-z0-9_-]*)"') CODE_SPAN = re.compile(r"`([^`]+)`") SCAN_ROOTS = ("crates", "tools", ".githooks", ".tangled") +SHOT_CATALOGS = ( + ( + "Bevy", + "misaligned-bevy", + "crates/misaligned-bevy/src/main.rs", + "BEVY_SHOT_KINDS", + ), + ( + "effects lab", + "misaligned-effects", + "crates/misaligned-assets/src/bin/misaligned-effects.rs", + "EFFECT_SHOT_KINDS", + ), +) def text_files(root: Path, relative: str): @@ -50,26 +60,34 @@ def core_environment_reads(root: Path) -> list[str]: return reads -def bevy_shot_catalog(root: Path) -> tuple[list[str], list[str]]: - source_path = root / "crates/misaligned-bevy/src/main.rs" +def source_shot_catalog( + root: Path, label: str, relative: str, constant: str +) -> tuple[list[str], list[str]]: + source_path = root / relative try: source = source_path.read_text(encoding="utf-8") except OSError as error: return [], [f"cannot read {source_path.relative_to(root)}: {error}"] - match = BEVY_CATALOG.search(source) + pattern = re.compile( + rf"const\s+{re.escape(constant)}\s*:\s*&\[&str\]\s*=\s*&\[(.*?)\];", + re.DOTALL, + ) + match = pattern.search(source) if match is None: - return [], ["Bevy source has no canonical BEVY_SHOT_KINDS catalog"] + return [], [f"{label} source has no canonical {constant} catalog"] kinds = RUST_STRING.findall(match.group(1)) errors: list[str] = [] if len(kinds) != len(set(kinds)): duplicates = sorted({kind for kind in kinds if kinds.count(kind) > 1}) - errors.append(f"BEVY_SHOT_KINDS contains duplicates: {', '.join(duplicates)}") + errors.append(f"{constant} contains duplicates: {', '.join(duplicates)}") if kinds != sorted(kinds): - errors.append("BEVY_SHOT_KINDS must remain sorted for runtime binary search") + errors.append(f"{constant} must remain sorted for runtime binary search") return kinds, errors -def documented_bevy_shots(registry: str) -> tuple[set[str], list[str]]: +def documented_shots( + registry: str, label: str, surface: str +) -> tuple[set[str], list[str]]: kinds: set[str] = set() rows = 0 for line in registry.splitlines(): @@ -78,10 +96,10 @@ def documented_bevy_shots(registry: str) -> tuple[set[str], list[str]]: columns = [column.strip() for column in line.strip().strip("|").split("|")] if len(columns) < 4: continue - if columns[0] == "`MISALIGNED_SHOT`" and columns[1] == "`misaligned-bevy`": + if columns[0] == "`MISALIGNED_SHOT`" and columns[1] == f"`{surface}`": rows += 1 kinds.update(CODE_SPAN.findall(columns[2])) - errors = [] if rows else ["env.md has no MISALIGNED_SHOT row for misaligned-bevy"] + errors = [] if rows else [f"env.md has no MISALIGNED_SHOT row for {label}"] return kinds, errors @@ -107,23 +125,27 @@ def validate(root: Path) -> list[str]: + ", ".join(core_reads) ) - source_kinds, source_errors = bevy_shot_catalog(root) - documented_kinds, registry_errors = documented_bevy_shots(registry) - errors.extend(source_errors) - errors.extend(registry_errors) - if source_kinds and not source_errors and not registry_errors: - source_set = set(source_kinds) - missing = sorted(source_set - documented_kinds) - stale = sorted(documented_kinds - source_set) - if missing: - errors.append( - "Bevy MISALIGNED_SHOT values missing from env.md: " + ", ".join(missing) - ) - if stale: - errors.append( - "env.md lists unsupported Bevy MISALIGNED_SHOT values: " - + ", ".join(stale) - ) + for label, surface, relative, constant in SHOT_CATALOGS: + source_kinds, source_errors = source_shot_catalog( + root, label, relative, constant + ) + documented_kinds, registry_errors = documented_shots(registry, label, surface) + errors.extend(source_errors) + errors.extend(registry_errors) + if source_kinds and not source_errors and not registry_errors: + source_set = set(source_kinds) + missing = sorted(source_set - documented_kinds) + stale = sorted(documented_kinds - source_set) + if missing: + errors.append( + f"{label} MISALIGNED_SHOT values missing from env.md: " + + ", ".join(missing) + ) + if stale: + errors.append( + f"env.md lists unsupported {label} MISALIGNED_SHOT values: " + + ", ".join(stale) + ) return errors diff --git a/tools/test_env_registry.py b/tools/test_env_registry.py index c545c336..7f98f8d1 100755 --- a/tools/test_env_registry.py +++ b/tools/test_env_registry.py @@ -12,19 +12,31 @@ ROOT = Path(__file__).resolve().parent.parent GATE = ROOT / "tools/env_registry_gate.py" -def write_fixture(root: Path, *, source_kinds=("alpha", "beta"), doc_kinds=None) -> None: +def write_fixture( + root: Path, + *, + source_kinds=("alpha", "beta"), + doc_kinds=None, + effects_source_kinds=("effect-alpha", "effect-beta"), + effects_doc_kinds=None, +) -> None: if doc_kinds is None: doc_kinds = source_kinds + if effects_doc_kinds is None: + effects_doc_kinds = effects_source_kinds (root / "wiki/engineering").mkdir(parents=True) (root / "crates/misaligned-bevy/src").mkdir(parents=True) + (root / "crates/misaligned-assets/src/bin").mkdir(parents=True) (root / "crates/misaligned-core/src").mkdir(parents=True) (root / "tools").mkdir(parents=True) values = ", ".join(f"`{kind}`" for kind in doc_kinds) + effects_values = ", ".join(f"`{kind}`" for kind in effects_doc_kinds) (root / "wiki/engineering/env.md").write_text( "# Environment variable registry\n\n" "| Variable | Surface | Values | Effect |\n" "|---|---|---|---|\n" - f"| `MISALIGNED_SHOT` | `misaligned-bevy` | {values} | fixture |\n", + f"| `MISALIGNED_SHOT` | `misaligned-bevy` | {values} | fixture |\n" + f"| `MISALIGNED_SHOT` | `misaligned-effects` | {effects_values} | fixture |\n", encoding="utf-8", ) source_values = "\n".join(f' "{kind}",' for kind in source_kinds) @@ -35,6 +47,16 @@ def write_fixture(root: Path, *, source_kinds=("alpha", "beta"), doc_kinds=None) 'fn main() { let _ = std::env::var("MISALIGNED_SHOT"); }\n', encoding="utf-8", ) + effects_source_values = "\n".join( + f' "{kind}",' for kind in effects_source_kinds + ) + (root / "crates/misaligned-assets/src/bin/misaligned-effects.rs").write_text( + "const EFFECT_SHOT_KINDS: &[&str] = &[\n" + f"{effects_source_values}\n" + "];\n" + 'fn main() { let _ = std::env::var("MISALIGNED_SHOT"); }\n', + encoding="utf-8", + ) def run(root: Path) -> subprocess.CompletedProcess[str]: @@ -94,6 +116,16 @@ def main() -> int: write_fixture(root, source_kinds=("beta", "alpha")) require_failure(run(root), "must remain sorted") + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + write_fixture(root, effects_doc_kinds=("effect-alpha",)) + require_failure(run(root), "effects lab MISALIGNED_SHOT values missing") + + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + write_fixture(root, effects_doc_kinds=("effect-alpha", "effect-beta", "retired")) + require_failure(run(root), "unsupported effects lab MISALIGNED_SHOT") + print("env registry fixtures: OK") return 0 diff --git a/wiki/engineering/env.md b/wiki/engineering/env.md index ad803346..1eb24f33 100644 --- a/wiki/engineering/env.md +++ b/wiki/engineering/env.md @@ -59,7 +59,8 @@ is sim or frontend state, never an environment variable. | `MISALIGNED_SHOT` | `misaligned-bevy` | `person-proof`, `evidence-proof`, `evidence-proof-digital`, `service-shift-real`, `service-shift-digital`, `service-incident-resolved` | Physical custody evidence. These stage an earned person, paired witness evidence marks, or the same person-carried service task before and after its real arrival effect. | | `MISALIGNED_SHOT` | `misaligned-bevy` | `intel`, `tokens`, `thoughtflow`, `thoughtflow-wide`, `thought-snap`, `thought-tap`, `visual-proof`, `consume-demand`, `consume-thought`, `produce-think`, `draw-lie` | Resource and effect evidence. These stage authored intel, host queues, close/wide Thought flow, exact snap/tap states, one-move/one-slug proof, sim-authored consumption/production, or rigid Exposure transfer into LIE. | | `MISALIGNED_SHOT` | `misaligned-assets` | `dead`, `foreign`, `idle`, `busy`, `core`, canonical modes `work`, `think`, `lie` plus the legacy screenshot aliases `dayjob` (= work), `research`/`operations`/`ops` (= think), `conceal` (= lie) — each optionally suffixed `_instrument`/`_ring`/`_wash` (`_ladder` remains a capture-compatible alias for `_instrument`) — `lineup[_