refactor(backup): move .backupignore seed from code to templates/ data file

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.
This commit is contained in:
AIOSAI
2026-06-24 09:39:51 -07:00
parent 70cf31eb3e
commit 4af5b8bf63
8 changed files with 110 additions and 65 deletions
+11
View File
@@ -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
@@ -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
+4 -4
View File
@@ -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.
@@ -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.
@@ -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 = {
+4
View File
@@ -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.
@@ -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
+49 -17
View File
@@ -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."""