From 4af5b8bf635c5b8cf74e6bdcec919d113e92e7ea Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Wed, 24 Jun 2026 09:39:51 -0700 Subject: [PATCH] refactor(backup): move .backupignore seed from code to templates/ data file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backup was the outlier — its default .backupignore content was hardcoded as the BUILTIN_IGNORES Python list + assembled in _build_backupignore(). Moved it to a template DATA FILE (backup/templates/backupignore.template), matching the AIPass convention (flow/spawn/memory all keep templates as files). - New: templates/backupignore.template (header + patterns, incl logs/). - _build_backupignore() reads the template via __file__-relative pathlib and RAISES FileNotFoundError if it's missing — never silently empty (an empty .backupignore = back up everything = crash). Behavior-preserving otherwise. - Retired BUILTIN_IGNORES (only setup.py consumed it). Runtime load_spec path untouched. - Tests expanded (30 pass): per-pattern template assertions + reads-template + raises-on-missing-template. Docs/comments repointed to the template. seedgo @backup 100%. td-30. --- CHANGELOG.md | 11 ++++ .../backup/.aipass/aipass_local_prompt.md | 2 +- src/aipass/backup/README.md | 8 +-- .../backup/apps/handlers/ignore/patterns.py | 33 +--------- .../backup/apps/handlers/project/setup.py | 22 +++---- src/aipass/backup/templates/README.md | 4 ++ .../backup/templates/backupignore.template | 29 ++++++++ .../backup/tests/test_ignore_pathspec.py | 66 ++++++++++++++----- 8 files changed, 110 insertions(+), 65 deletions(-) create mode 100644 src/aipass/backup/templates/backupignore.template diff --git a/CHANGELOG.md b/CHANGELOG.md index db243ca9..192693ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,17 @@ PyPI version — not the changelog header. the sub-plan handed to it, but the master stays the orchestrator's to close on completion (td-6). +### Changed + +- **Backup `.backupignore` default moved out of code into a template file** — the + seed content backup writes into a new project's `.backupignore` now lives in + `backup/templates/backupignore.template` (loaded at register), matching the + AIPass convention that templates are data files, not hardcoded Python. Retired + the `BUILTIN_IGNORES` list; `_build_backupignore()` reads the template and + **raises** if it's missing — never silently empty, since an empty + `.backupignore` would back up everything and crash. Docs/comments repointed to + the template (td-30). + ### Removed - **Dead `bulletin_created` trigger handler** — the event handler that wrote a diff --git a/src/aipass/backup/.aipass/aipass_local_prompt.md b/src/aipass/backup/.aipass/aipass_local_prompt.md index 881d433d..382a2e8f 100644 --- a/src/aipass/backup/.aipass/aipass_local_prompt.md +++ b/src/aipass/backup/.aipass/aipass_local_prompt.md @@ -65,7 +65,7 @@ apps/ - Project-owned design: .backup/ and .backupignore live in the TARGET project, not centrally - Normal citizen namespace: uses `from aipass.backup.apps.modules.*` / `from aipass.backup.apps.handlers.*` - Entry point sets AIPASS_BRANCH_NAME env var for Prax -- BUILTIN_IGNORES in patterns.py is the single source for default ignore patterns +- templates/backupignore.template is the single source for default ignore patterns ## Known Gotchas diff --git a/src/aipass/backup/README.md b/src/aipass/backup/README.md index 000dadb2..6d5cf52e 100644 --- a/src/aipass/backup/README.md +++ b/src/aipass/backup/README.md @@ -109,12 +109,12 @@ The root `.gitignore` covers all three with a single `.backup/` entry. Two layers — seed and runtime: -1. **`BUILTIN_IGNORES`** (`handlers/ignore/patterns.py`) — the **seed**. Written into a new project's `.backupignore` at `register` time (`setup._build_backupignore()`). Never consulted at backup time. -2. **`.backupignore`** — the **runtime source of truth**. `load_spec()` reads it on every backup; `BUILTIN_IGNORES` is not applied. True pathspec/gitwildmatch semantics: `#` comments, `!` negation, trailing `/` for dirs, last-match-wins. +1. **`templates/backupignore.template`** — the **seed**. Read by `setup._build_backupignore()` and written into a new project's `.backupignore` at `register` time. Never consulted at backup time. If this file is missing, registration raises — an empty seed would back up everything and crash the machine. +2. **`.backupignore`** — the **runtime source of truth**. `load_spec()` reads it on every backup; the seed template is not applied. True pathspec/gitwildmatch semantics: `#` comments, `!` negation, trailing `/` for dirs, last-match-wins. -There is no static fallback. The seed IS the safety mechanism — an empty or missing `.backupignore` means back up everything (`.venv`, `node_modules`, `.git`), which can crash the machine. Keep `BUILTIN_IGNORES` sane. +There is no static fallback. The seed IS the safety mechanism — an empty or missing `.backupignore` means back up everything (`.venv`, `node_modules`, `.git`), which can crash the machine. Keep the template sane. -- To change defaults for **new** projects → edit `BUILTIN_IGNORES` in `patterns.py` +- To change defaults for **new** projects → edit `templates/backupignore.template` - To change ignores for an **existing** project → edit its `.backupignore` The repo-root `/.backupignore` ships intentionally as the curated default so users don't snapshot junk. diff --git a/src/aipass/backup/apps/handlers/ignore/patterns.py b/src/aipass/backup/apps/handlers/ignore/patterns.py index f85876c8..5e3c47f9 100644 --- a/src/aipass/backup/apps/handlers/ignore/patterns.py +++ b/src/aipass/backup/apps/handlers/ignore/patterns.py @@ -17,42 +17,11 @@ import pathspec from ..json import json_handler from ..path import builder -# Seed for new-project .backupignore (written at register by setup._build_backupignore). -# NOT applied at backup time — runtime ignores come from .backupignore via load_spec(). -# Keep sane: empty seed = back up everything = crash. -BUILTIN_IGNORES = [ - ".backup/", - ".git/", - ".svn/", - ".hg/", - "__pycache__/", - ".pytest_cache/", - "*.pyc", - "*.pyo", - "*.egg-info/", - ".venv/", - "venv/", - ".tox/", - "node_modules/", - ".vscode/", - ".idea/", - "*.swp", - "*.swo", - ".DS_Store", - "Thumbs.db", - "build/", - "dist/", - "*.log", - "logs/", - ".ruff_cache/", - ".coverage", -] - def load_spec(project_root: str) -> pathspec.PathSpec: """Load a PathSpec from .backupignore at the project root. - This is the runtime source of truth — BUILTIN_IGNORES is not consulted here. + This is the runtime source of truth — the seed template is not consulted here. Reads raw lines — pathspec handles #comments, blanks, !negation, anchoring, dir-only trailing /, and last-match-wins natively. diff --git a/src/aipass/backup/apps/handlers/project/setup.py b/src/aipass/backup/apps/handlers/project/setup.py index e0aa3b24..e66cb890 100644 --- a/src/aipass/backup/apps/handlers/project/setup.py +++ b/src/aipass/backup/apps/handlers/project/setup.py @@ -15,22 +15,22 @@ inside a target project path, and a ``.backupignore`` at the project root. from datetime import datetime, timezone from pathlib import Path -from ..ignore.patterns import BUILTIN_IGNORES from ..json import json_handler from ..path import builder +_TEMPLATE_PATH = Path(__file__).resolve().parents[3] / "templates" / "backupignore.template" + def _build_backupignore() -> str: - """Generate .backupignore content from BUILTIN_IGNORES.""" - lines = [ - "# Backup System ignore patterns (gitignore-style)", - "# Lines starting with # are comments. Blank lines are ignored.", - "# Edit this file to customize. Source defaults: handlers/ignore/patterns.py", - "", - ] - for pattern in BUILTIN_IGNORES: - lines.append(pattern) - return "\n".join(lines) + "\n" + """Read the seed .backupignore content from the template file. + + Raises: + FileNotFoundError: If the template is missing. + OSError: If the template cannot be read. + """ + if not _TEMPLATE_PATH.exists(): + raise FileNotFoundError(f"Seed template missing: {_TEMPLATE_PATH} — cannot create a safe .backupignore") + return _TEMPLATE_PATH.read_text(encoding="utf-8") DEFAULT_CONFIG = { diff --git a/src/aipass/backup/templates/README.md b/src/aipass/backup/templates/README.md index b4d52f82..5d279a05 100644 --- a/src/aipass/backup/templates/README.md +++ b/src/aipass/backup/templates/README.md @@ -3,3 +3,7 @@ Branch-specific templates for `BACKUP`. Any templates this branch provides to the system or uses internally. Examples: plan templates (flow), trinity templates (memory), test templates (seedgo). + +## Files + +- **`backupignore.template`** — Seed content for a new project's `.backupignore`. Written at `register` time by `setup._build_backupignore()`. Edit this to change the default ignore patterns for newly registered projects. diff --git a/src/aipass/backup/templates/backupignore.template b/src/aipass/backup/templates/backupignore.template new file mode 100644 index 00000000..5b1c0735 --- /dev/null +++ b/src/aipass/backup/templates/backupignore.template @@ -0,0 +1,29 @@ +# Backup System ignore patterns (gitignore-style) +# Lines starting with # are comments. Blank lines are ignored. +# Edit this file to customize. Source defaults: templates/backupignore.template + +.backup/ +.git/ +.svn/ +.hg/ +__pycache__/ +.pytest_cache/ +*.pyc +*.pyo +*.egg-info/ +.venv/ +venv/ +.tox/ +node_modules/ +.vscode/ +.idea/ +*.swp +*.swo +.DS_Store +Thumbs.db +build/ +dist/ +*.log +logs/ +.ruff_cache/ +.coverage diff --git a/src/aipass/backup/tests/test_ignore_pathspec.py b/src/aipass/backup/tests/test_ignore_pathspec.py index 6b0a03bc..49fd36b9 100644 --- a/src/aipass/backup/tests/test_ignore_pathspec.py +++ b/src/aipass/backup/tests/test_ignore_pathspec.py @@ -11,7 +11,6 @@ import pathspec from aipass.backup.apps.handlers.ignore.patterns import ( - BUILTIN_IGNORES, is_ignored, load_spec, ) @@ -240,28 +239,61 @@ class TestDotfilesIncluded: class TestSeedTemplate: - """BUILTIN_IGNORES is used for seeding, not runtime merge.""" + """Seed template (backupignore.template) provisions new projects.""" - def test_builtin_has_ruff_cache(self): - """Seed defaults include .ruff_cache/.""" - assert ".ruff_cache/" in BUILTIN_IGNORES + def test_template_has_ruff_cache(self): + """Seed template includes .ruff_cache/.""" + from aipass.backup.apps.handlers.project.setup import _TEMPLATE_PATH - def test_builtin_has_coverage(self): - """Seed defaults include .coverage.""" - assert ".coverage" in BUILTIN_IGNORES + content = _TEMPLATE_PATH.read_text(encoding="utf-8") + assert ".ruff_cache/" in content - def test_builtin_has_logs_dir(self): - """Seed defaults exclude logs/ directories.""" - assert "logs/" in BUILTIN_IGNORES + def test_template_has_coverage(self): + """Seed template includes .coverage.""" + from aipass.backup.apps.handlers.project.setup import _TEMPLATE_PATH - def test_build_backupignore_content(self): - """Seed template includes ruff_cache, coverage, and pycache.""" - from aipass.backup.apps.handlers.project.setup import _build_backupignore + content = _TEMPLATE_PATH.read_text(encoding="utf-8") + assert ".coverage" in content + + def test_template_has_logs_dir(self): + """Seed template excludes logs/ directories.""" + from aipass.backup.apps.handlers.project.setup import _TEMPLATE_PATH + + content = _TEMPLATE_PATH.read_text(encoding="utf-8") + assert "logs/" in content + + def test_template_has_git(self): + """Seed template excludes .git/.""" + from aipass.backup.apps.handlers.project.setup import _TEMPLATE_PATH + + content = _TEMPLATE_PATH.read_text(encoding="utf-8") + assert ".git/" in content + + def test_template_has_venv(self): + """Seed template excludes .venv/.""" + from aipass.backup.apps.handlers.project.setup import _TEMPLATE_PATH + + content = _TEMPLATE_PATH.read_text(encoding="utf-8") + assert ".venv/" in content + + def test_build_backupignore_reads_template(self): + """_build_backupignore returns the template content.""" + from aipass.backup.apps.handlers.project.setup import _TEMPLATE_PATH, _build_backupignore content = _build_backupignore() - assert ".ruff_cache/" in content - assert ".coverage" in content - assert "__pycache__/" in content + assert content == _TEMPLATE_PATH.read_text(encoding="utf-8") + + def test_build_backupignore_raises_on_missing_template(self, tmp_path): + """Missing template raises FileNotFoundError, not empty content.""" + from unittest.mock import patch + + import pytest + + from aipass.backup.apps.handlers.project import setup + + fake_path = tmp_path / "nonexistent.template" + with patch.object(setup, "_TEMPLATE_PATH", fake_path), pytest.raises(FileNotFoundError): + setup._build_backupignore() def test_seed_writes_only_when_absent(self, tmp_path): """Seeding does not overwrite an existing .backupignore."""