diff --git a/.cursor/rules/shipped-data-assets.mdc b/.cursor/rules/shipped-data-assets.mdc new file mode 100644 index 0000000..75ba08e --- /dev/null +++ b/.cursor/rules/shipped-data-assets.mdc @@ -0,0 +1,27 @@ +--- +description: Shipped data/ assets must be git-trackable so tool updates include them +globs: "{.gitignore,data/**,gui/guide_tab.py,gui/main.py,tests/test_bundled_updates.py}" +alwaysApply: false +--- + +# Shipped data/ and tool updates + +Tool auto-update downloads a **git branch archive**. +Anything matched by `.gitignore` is omitted from that archive and never reaches users. + +## When adding or changing shipped defaults under `data/` + +- Add a negation under `.gitignore` (same pattern as skills/help): + - `!data/skills/**` + - `!data/help/**` + - `!data/vocab_base.txt` + - `!data/translation_contexts.json` +- Do **not** rely on `UpdateThread.should_install` alone - ignored files never appear in the zip. +- Extend `_SHIPPED_DATA_FILES` / `ShippedDataTrackingTests` in `tests/test_bundled_updates.py` when adding a new shipped path. +- User-local files stay ignored/protected: `data/vocab.txt`, `data/last_update_sha.txt`, `data/wolf_*.json`. + +## Quick check + +```bash +git check-ignore -- data/help/index.json # must exit 1 (not ignored) +``` diff --git a/.gitignore b/.gitignore index 2688f4a..b42b45d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ __pycache__ !assets/engine_icons/** !.pre-commit-config.yaml !data/skills/** +!data/help/** !data/vocab_base.txt !data/translation_contexts.json !requirements.txt diff --git a/data/help/index.json b/data/help/index.json new file mode 100644 index 0000000..d070352 --- /dev/null +++ b/data/help/index.json @@ -0,0 +1,37 @@ +[ + { + "id": "welcome", + "title": "Welcome", + "file": "00-welcome.md" + }, + { + "id": "requirements", + "title": "Requirements", + "file": "01-requirements.md" + }, + { + "id": "quickstart", + "title": "Quick Start", + "file": "02-quickstart.md" + }, + { + "id": "workflow-rpg", + "title": "Workflow: RPG Maker", + "file": "03-workflow-rpg.md" + }, + { + "id": "workflow-wolf", + "title": "Workflow: WolfDawn", + "file": "04-workflow-wolf.md" + }, + { + "id": "other-tabs", + "title": "Other Tabs", + "file": "05-other-tabs.md" + }, + { + "id": "examples", + "title": "Example Walkthrough", + "file": "06-examples.md" + } +] diff --git a/gui/main.py b/gui/main.py index 0ac0b60..6723bad 100644 --- a/gui/main.py +++ b/gui/main.py @@ -78,8 +78,8 @@ class UpdateThread(QThread): PROTECTED_TOP = {".env", "venv", "log", "files", "translated"} # User-local files under data/ that must not be overwritten. - # Shipped defaults (translation_contexts.json, skills/*.md, vocab_base.txt, …) - # are intentionally updated so tool releases refresh prompts and contexts. + # Shipped defaults (translation_contexts.json, skills/*.md, help/*, vocab_base.txt, …) + # are intentionally updated so tool releases refresh prompts, Guide docs, and contexts. PROTECTED_DATA_FILES = frozenset({ "data/vocab.txt", "data/last_update_sha.txt", diff --git a/tests/test_bundled_updates.py b/tests/test_bundled_updates.py index 348be83..b6054e6 100644 --- a/tests/test_bundled_updates.py +++ b/tests/test_bundled_updates.py @@ -7,6 +7,7 @@ a maintainer explicitly uses --force / --refresh-offline / --refresh-all. from __future__ import annotations +import subprocess import tempfile import unittest from pathlib import Path @@ -14,6 +15,19 @@ from unittest.mock import patch from util.wolfdawn import WolfDawnError, bundled_binary_path, ensure_wolf_binary +_REPO_ROOT = Path(__file__).resolve().parents[1] + +# Shipped with tool updates from the git archive. Keep in sync with .gitignore +# exceptions under data/ — ignored files never appear in update zips. +_SHIPPED_DATA_FILES = ( + "data/translation_contexts.json", + "data/skills/system.md", + "data/skills/project_setup.md", + "data/help/index.json", + "data/help/00-welcome.md", + "data/vocab_base.txt", +) + class UpdateThreadInstallFilterTests(unittest.TestCase): """Tool update must refresh shipped data/ assets while protecting user state.""" @@ -22,12 +36,7 @@ class UpdateThreadInstallFilterTests(unittest.TestCase): from gui.main import UpdateThread for rel in ( - "data/translation_contexts.json", - "data/skills/system.md", - "data/skills/project_setup.md", - "data/help/index.json", - "data/help/00-welcome.md", - "data/vocab_base.txt", + *_SHIPPED_DATA_FILES, "gui/main.py", "gameupdate/GameUpdate.bat", "gameupdate/gameupdate/patch.ps1", @@ -53,6 +62,62 @@ class UpdateThreadInstallFilterTests(unittest.TestCase): self.assertFalse(UpdateThread.should_install(Path(rel))) +class ShippedDataTrackingTests(unittest.TestCase): + """Guide/help and other shipped data/ files must be git-trackable. + + Tool updates download a branch archive from git. Anything matched by + .gitignore is omitted from that archive, so Guide markdown and index.json + would never reach end users. + """ + + def test_shipped_data_files_exist(self): + for rel in _SHIPPED_DATA_FILES: + with self.subTest(rel=rel): + self.assertTrue( + (_REPO_ROOT / rel).is_file(), + f"missing shipped asset {rel}", + ) + + def test_help_dir_markdown_matches_index(self): + import json + + help_dir = _REPO_ROOT / "data" / "help" + index = json.loads((help_dir / "index.json").read_text(encoding="utf-8")) + for entry in index: + rel = entry["file"] + with self.subTest(file=rel): + self.assertTrue( + (help_dir / rel).is_file(), + f"data/help/index.json references missing {rel}", + ) + + def test_shipped_data_files_are_not_gitignored(self): + help_dir = _REPO_ROOT / "data" / "help" + rels = list(_SHIPPED_DATA_FILES) + for path in sorted(help_dir.glob("*.md")): + rel = path.relative_to(_REPO_ROOT).as_posix() + if rel not in rels: + rels.append(rel) + + for rel in rels: + with self.subTest(rel=rel): + # Plain check-ignore (no -v): exit 0 = ignored, 1 = not ignored. + # -v also reports negation rules with exit 0, which is a false positive. + result = subprocess.run( + ["git", "check-ignore", "--", rel], + cwd=_REPO_ROOT, + capture_output=True, + text=True, + check=False, + ) + self.assertNotEqual( + result.returncode, + 0, + f"{rel} is gitignored; " + "add a !.gitignore exception under data/ so tool updates include it", + ) + + class CheckToolUpdateTests(unittest.TestCase): """Tool update check compares remote SHA to data/last_update_sha.txt only."""