From 4d3bf61d5664df7da9e3adfe1baa64bcc97eb48c Mon Sep 17 00:00:00 2001 From: Mikkeli Date: Wed, 12 Aug 2026 22:23:10 +0900 Subject: [PATCH] Regenerate _text/ on commit, so the readable copy cannot drift _text/ is derived from the PNG tEXt chunks and settings.json, and derived data regenerated by hand goes stale silently -- which is worse than absent, because a stale card reads as current. A pre-commit hook now regenerates and stages it. Blocking, unlike the halogen repo's advisory doc-check: that one reports a judgement call, this one rebuilds a file. Install with `./st-export.py --install-hook`, since hooks are not versioned. Verified by renaming a persona in the untracked settings.json and committing WITHOUT running the export -- _text/personas.json updated itself and was included. Test edit reverted. --- st-export.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/st-export.py b/st-export.py index 2b86b6b..d0f5ddf 100755 --- a/st-export.py +++ b/st-export.py @@ -115,7 +115,40 @@ def export_personas() -> int: return len(out.get("personas") or {}) +HOOK = """#!/usr/bin/env bash +# Installed by st-export.py --install-hook. +# Regenerates _text/ from the PNGs and settings.json, then stages it, so the +# readable copy can never drift from the artifact SillyTavern actually loads. +set -e +cd "$(git rev-parse --show-toplevel)" +python3 ./st-export.py >/dev/null +git add -A _text +""" + + +def install_hook() -> int: + import subprocess + root = subprocess.run(["git", "rev-parse", "--git-dir"], capture_output=True, + text=True).stdout.strip() + if not root: + print("not a git repository") + return 1 + path = os.path.join(root, "hooks", "pre-commit") + os.makedirs(os.path.dirname(path), exist_ok=True) + open(path, "w").write(HOOK) + os.chmod(path, 0o755) + print(f"armed: {path}") + # ⚠ BLOCKING ON PURPOSE, unlike the halogen repo's advisory doc-check. That + # one reports a judgement call; this one regenerates a derived file. If it + # fails, _text/ would silently describe a card that no longer exists — and a + # stale derived copy is worse than no copy, because it reads as current. + print("⚠ runs on every commit; a failure blocks it, which is the point") + return 0 + + def main() -> int: + if "--install-hook" in sys.argv: + return install_hook() os.makedirs(OUT, exist_ok=True) cards = export_cards() personas = export_personas()