#!/usr/bin/env bash # Check the export against MODDING.md's five rules. # # tools/port/check-modding # # `MODDING.md` opens by saying modding is a requirement and **a constraint on the # exporter today, not a later feature**. Nothing checked it. That is the shape # this port keeps finding: a rule stated, believed, and unexercised -- the black # hold implemented and never called, `ScreenView.skipped` written and never read, # `stop_bed` provided and never used, `--focus` parsed and overwritten. # # So this is a guard, not a fix: every rule passes as of 2026-08-30. Its value is # that the next thing to break one of them says so. # # ⚠️ What it CANNOT check: rule 3's "modern, editable" is enforced by extension, # which cannot tell a valid PNG from a renamed one, and rule 1's "one logical # asset" is checked as one-file-per-reference -- an exporter that split a sprite # and referenced both halves would pass. These are the rules' checkable shadows. set -euo pipefail cd "${PROJECT_DIR:-/work}" EXPORT="${EXPORT:-export}" fail=0 note() { printf ' %-6s %s\n' "$1" "$2"; [ "$1" = FAIL ] && fail=1 || true; } echo "MODDING rule 1 -- one logical asset, one file" python3 - "$EXPORT" <<'PY' import json, glob, os, sys E = sys.argv[1] ref = set() for p in glob.glob(f"{E}/screens/*/*.json"): d = json.load(open(p)) for e in d["elements"]: for k in ("sprite", "focus_sprite"): if e.get(k): ref.add(e[k]) for sub in ("focus", "leaf"): for fe in (e.get(sub) or {}).get("elements", []): if fe.get("sprite"): ref.add(fe["sprite"]) files = {os.path.relpath(p, E) for p in glob.glob(f"{E}/sprites/**/*.png", recursive=True)} missing, orphan = sorted(ref - files), sorted(files - ref) split = [f for f in files if any(t in os.path.basename(f) for t in ("part", "seg", "chunk"))] print(" %-6s %d sprites referenced, %d present" % ("OK" if not (missing or orphan) else "FAIL", len(ref), len(files))) if missing: print(" FAIL referenced but absent:", missing[:5]) if orphan: print(" FAIL present but unreferenced:", orphan[:5]) if split: print(" FAIL split-looking names:", split[:5]) sys.exit(1 if (missing or orphan or split) else 0) PY [ $? -eq 0 ] || fail=1 echo "MODDING rule 2 -- names a person recognises" hex=$(find "$EXPORT" -type f | grep -Ec '0x[0-9a-f]{6,}|/[0-9a-f]{8}\.' || true) [ "$hex" -eq 0 ] && note OK "no hex or hash-shaped filenames" || note FAIL "$hex hash-shaped names" echo "MODDING rule 3 -- modern, editable formats only" bad=$(find "$EXPORT" -type f | sed 's/.*\.//' | sort -u | grep -vE '^(json|png|ogg|ogv|cmd)$' || true) [ -z "$bad" ] && note OK "only json/png/ogg/ogv (+ .cmd sidecars)" || note FAIL "unexpected: $(echo $bad)" # A `.cmd` is not an asset. It is allowed only because it SAYS SO in its own # first line -- see video.rs. An unlabelled one reads as something to edit. for c in $(find "$EXPORT" -name '*.cmd'); do head -1 "$c" | grep -q '^# Generated by sylpheed-export' \ && note OK "$(basename "$c") is self-describing" \ || note FAIL "$(basename "$c") has no header saying what it is" done echo "MODDING rule 4 -- base and overrides, never one merged pile" grep -q 'data/mods' .gitignore && note OK "data/mods contents are gitignored" \ || note FAIL "data/mods is not gitignored -- a mod is usually a game asset" grep -rq 'data/mods' crates/sylpheed-export/src/ && note OK "the exporter resolves overrides" \ || note FAIL "nothing reads data/mods" echo "MODDING rule 5 -- provenance in every generated file" python3 - "$EXPORT" <<'PY' import json, glob, os, sys E = sys.argv[1]; miss = [] for p in sorted(glob.glob(f"{E}/**/*.json", recursive=True)): d = json.load(open(p)) if os.path.basename(p) == "manifest.json": # The manifest is the provenance -- it carries disc, exporter and the # formats revision for the whole tree, so it has no `source` of its own. if not all(k in d for k in ("disc", "exporter", "formats_rev")): miss.append(p) continue src = d.get("source") or {} if not (isinstance(src, dict) and src): miss.append(p) print(" %-6s %d json files carry provenance" % ("OK" if not miss else "FAIL", len(glob.glob(f"{E}/**/*.json", recursive=True)) - len(miss))) for m in miss[:5]: print(" FAIL ", m) sys.exit(1 if miss else 0) PY [ $? -eq 0 ] || fail=1 echo [ $fail -eq 0 ] && echo "all five rules pass" || echo "🔴 a MODDING rule is broken" exit $fail