fix(backup): rename store dir .backup_system to .backup + remove dead versions/ (FPLAN-0269 follow-up)

Backup root is now .backup/ via BACKUP_DIR (builder.py:19); tracker.py uses backup_root() not a hardcoded path; patterns.py BUILTIN_IGNORES + docstrings/README updated. Removed the orphaned per-timestamp versions/ scaffold (setup.py) and unused build_versioned_path() — both superseded by the Phase-3 versioned/ baseline+diff store. .backup/ coexists with flow's .backup/processed_plans/. Repo-root .backupignore now ignores both .backup/ and (until manual deletion) .backup_system/ (also carries Patrick's *logs rule). Verified by artifact (seedgo 100%, 220 tests) + live (throwaway writes to .backup/, no versions/, Drive reads .backup/versioned/).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
AIOSAI
2026-06-12 22:06:50 -07:00
co-authored by Claude Opus 4.8
parent a0016669b7
commit c5a96cbdcf
13 changed files with 51 additions and 55 deletions
+2
View File
@@ -3,6 +3,7 @@
# Edit this file to customize. Source defaults: handlers/ignore/patterns.py
.backup_system/
.backup/
.git/
.svn/
.hg/
@@ -26,3 +27,4 @@ dist/
*.log
.ruff_cache/
.coverage
*logs
+8
View File
@@ -26,6 +26,14 @@ PyPI version — not the changelog header.
the default. `pathspec` (pure-Python, cross-OS) declared. Verified by artifact
(seedgo 100%, 220 tests incl. 26 new gitignore-parity tests) + live (a dotfile
flows into the store, `!` negation re-includes end-to-end).
- **Backup store dir renamed `.backup_system/` → `.backup/`, dead `versions/`
removed (FPLAN-0269 follow-up).** The backup root is now `.backup/` (shorter,
coexists with `@flow`'s `.backup/processed_plans/`); the orphaned per-timestamp
`versions/` scaffold and the unused `build_versioned_path()` — both superseded
by the Phase-3 `versioned/` baseline+diff store — are gone. Drive sync confirmed
reading `.backup/versioned/` + `.backup/drive_tracker.json` via the shared
`backup_root()`. Verified by artifact (seedgo 100%, 220 tests) + live (a
throwaway project writes to `.backup/`, no `versions/` dir).
### Fixed
+3 -3
View File
@@ -13,7 +13,7 @@
### What I Do
- Back up any project directory on the system (not just AIPass projects)
- Each project owns its backup config (`.backup_system/`) and ignore patterns (`.backupignore`)
- Each project owns its backup config (`.backup/`) and ignore patterns (`.backupignore`)
- Snapshot mode: full mirror copy
- Versioned mode: incremental timestamped backups with automatic pruning
- Project registry for name-based lookups (`backup snapshot @AIPass`)
@@ -49,7 +49,7 @@ apps/
├── ignore/ # .backupignore patterns + whitelist
├── json/ # JSON persistence, atomic writes, ops log
├── path/ # Backup path building
├── project/ # Config, registry, setup (.backup_system/)
├── project/ # Config, registry, setup (.backup/)
├── report/ # Result formatting
├── scan/ # Directory walking + filtering
├── state/ # Changelog, metadata, timestamps
@@ -78,4 +78,4 @@ backup --version # Show version
- @cli — Rich console output
### Provides To
- Any project on the PC — backups are project-owned (.backup_system/ in target root)
- Any project on the PC — backups are project-owned (.backup/ in target root)
+1 -1
View File
@@ -67,7 +67,7 @@ def print_help() -> None:
console.print(" [green]snapshot[/green] Full mirror backup of a project")
console.print(" [green]versioned[/green] Incremental timestamped backup")
console.print(" [green]all[/green] Run snapshot then versioned in sequence")
console.print(" [green]register[/green] Register a project + scaffold its .backup_system/")
console.print(" [green]register[/green] Register a project + scaffold its .backup/")
console.print(" [green]status[/green] Show backup info and recent history")
console.print(" [green]settings[/green] View/edit backup settings")
console.print(" [green]drive_sync[/green] Sync backups to the remote drive")
@@ -10,7 +10,7 @@
Maintains a persistent mapping of local file paths to Drive metadata
(file ID, mtime, size) so repeat syncs can skip unchanged files.
Tracker is stored at ``<project>/.backup_system/drive_tracker.json``.
Tracker is stored at ``<project>/.backup/drive_tracker.json``.
"""
from __future__ import annotations
@@ -27,11 +27,13 @@ TRACKER_FILENAME = "drive_tracker.json"
def _tracker_path(project_root: str) -> Path:
"""Return the tracker file path for a project."""
return Path(project_root) / ".backup_system" / TRACKER_FILENAME
from ..path.builder import backup_root
return backup_root(project_root) / TRACKER_FILENAME
def load_tracker(project_root: str) -> dict:
"""Load tracker from .backup_system/drive_tracker.json.
"""Load tracker from .backup/drive_tracker.json.
Returns:
Dict keyed by relative file path with metadata values.
@@ -46,7 +48,7 @@ def load_tracker(project_root: str) -> dict:
def save_tracker(project_root: str, tracker: dict) -> None:
"""Save tracker to .backup_system/drive_tracker.json."""
"""Save tracker to .backup/drive_tracker.json."""
path = _tracker_path(project_root)
json_handler.save_json(str(path), tracker)
json_handler.log_operation(
@@ -18,7 +18,7 @@ from ..json import json_handler
from ..path import builder
BUILTIN_IGNORES = [
".backup_system/",
".backup/",
".git/",
".svn/",
".hg/",
@@ -9,38 +9,29 @@
"""Path builder handler.
Computes destination paths for backup modes. All paths are relative to the
target project's .backup_system/ directory.
target project's .backup/ directory.
"""
from pathlib import Path
from ..json import json_handler
BACKUP_DIR = ".backup_system"
BACKUP_DIR = ".backup"
def backup_root(project_root: str) -> Path:
"""Return the .backup_system/ path for a project."""
"""Return the .backup/ path for a project."""
return Path(project_root) / BACKUP_DIR
def build_snapshot_path(project_root: str) -> Path:
"""Snapshot destination: <project>/.backup_system/snapshots/"""
"""Snapshot destination: <project>/.backup/snapshots/"""
json_handler.log_operation("build_snapshot_path", {"project_root": project_root})
return backup_root(project_root) / "snapshots"
def build_versioned_path(project_root: str, timestamp: str) -> Path:
"""Versioned destination: <project>/.backup_system/versions/<timestamp>/"""
json_handler.log_operation(
"build_versioned_path",
{"project_root": project_root, "timestamp": timestamp},
)
return backup_root(project_root) / "versions" / timestamp
def build_config_path(project_root: str) -> Path:
"""Config file: <project>/.backup_system/config.json"""
"""Config file: <project>/.backup/config.json"""
return backup_root(project_root) / "config.json"
@@ -50,22 +41,22 @@ def build_ignore_path(project_root: str) -> Path:
def build_timestamps_path(project_root: str) -> Path:
"""Timestamps file: <project>/.backup_system/timestamps.json"""
"""Timestamps file: <project>/.backup/timestamps.json"""
return backup_root(project_root) / "timestamps.json"
def build_changelog_path(project_root: str) -> Path:
"""Changelog file: <project>/.backup_system/changelog.json"""
"""Changelog file: <project>/.backup/changelog.json"""
return backup_root(project_root) / "changelog.json"
def build_log_dir(project_root: str) -> Path:
"""Log directory: <project>/.backup_system/logs/"""
"""Log directory: <project>/.backup/logs/"""
return backup_root(project_root) / "logs"
def build_versioned_store(project_root: str) -> Path:
"""Persistent versioned store: <project>/.backup_system/versioned/"""
"""Persistent versioned store: <project>/.backup/versioned/"""
json_handler.log_operation("build_versioned_store", {"project_root": project_root})
return backup_root(project_root) / "versioned"
@@ -8,7 +8,7 @@
"""Project configuration handler.
Reads and writes the per-project ``.backup_system/config.json`` that stores mode
Reads and writes the per-project ``.backup/config.json`` that stores mode
preferences, size limits, and drive-sync settings.
"""
@@ -1,6 +1,6 @@
# =================== AIPass ====================
# Name: setup.py
# Description: Project setup handler — scaffold .backup_system/ directory in target
# Description: Project setup handler — scaffold .backup/ directory in target
# Version: 1.0.0
# Created: 2026-04-16
# Modified: 2026-04-23
@@ -8,7 +8,7 @@
"""Project setup handler.
Creates the ``.backup_system/`` scaffold (config, snapshots/, versions/, logs/)
Creates the ``.backup/`` scaffold (config, snapshots/, logs/)
inside a target project path, and a ``.backupignore`` at the project root.
"""
@@ -45,13 +45,13 @@ DEFAULT_CONFIG = {
def create_backup_dir(project_path: str) -> Path | None:
"""Create the ``.backup_system/`` scaffold inside a project path.
"""Create the ``.backup/`` scaffold inside a project path.
Args:
project_path: Absolute filesystem path to the target project.
Returns:
Path to the created ``.backup_system/`` directory, or None on failure.
Path to the created ``.backup/`` directory, or None on failure.
"""
root = Path(project_path)
if not root.is_dir():
@@ -61,7 +61,6 @@ def create_backup_dir(project_path: str) -> Path | None:
backup_dir = builder.backup_root(project_path)
subdirs = [
backup_dir / "snapshots",
backup_dir / "versions",
backup_dir / "logs",
]
@@ -9,7 +9,7 @@
"""Changelog state handler.
Appends and reads structured changelog entries describing each backup run
for a project. Stored at .backup_system/changelog.json.
for a project. Stored at .backup/changelog.json.
"""
from ..json import json_handler
+3 -3
View File
@@ -1,12 +1,12 @@
# =================== AIPass ====================
# Name: register.py
# Description: Register module — adds a project to backup and creates .backup_system/
# Description: Register module — adds a project to backup and creates .backup/
# Version: 1.0.0
# Created: 2026-04-17
# Modified: 2026-04-23
# =============================================
"""Register Module — register a project for backup and scaffold its .backup_system/."""
"""Register Module — register a project for backup and scaffold its .backup/."""
import sys
from pathlib import Path
@@ -88,7 +88,7 @@ def handle_command(command: str, args: list) -> bool:
backup_dir = create_backup_dir(project_path)
if backup_dir is None:
console.print(f"[red]Error:[/red] Failed to create .backup_system/ in {project_path}")
console.print(f"[red]Error:[/red] Failed to create .backup/ in {project_path}")
return True
register_project(name, project_path)
+6 -12
View File
@@ -173,9 +173,7 @@ class TestDriveClient:
mock_service = MagicMock()
client._drive_service = mock_service
mod.api_call_with_retry = MagicMock(
return_value={"files": [{"id": "folder_123", "name": "AIPass Backups"}]}
)
mod.api_call_with_retry = MagicMock(return_value={"files": [{"id": "folder_123", "name": "AIPass Backups"}]})
result = client.get_or_create_backup_folder()
assert result == "folder_123"
@@ -220,9 +218,7 @@ class TestDriveClient:
client._drive_service = mock_service
client.backup_folder_id = "root_folder"
mod.api_call_with_retry = MagicMock(
return_value={"files": [{"id": "proj_folder_789", "name": "myproject"}]}
)
mod.api_call_with_retry = MagicMock(return_value={"files": [{"id": "proj_folder_789", "name": "myproject"}]})
result = client.get_or_create_project_folder("myproject")
assert result == "proj_folder_789"
@@ -472,7 +468,7 @@ class TestDriveTracker:
mod = _fresh_import("aipass.backup.apps.handlers.drive.tracker")
project = tmp_path / "project"
project.mkdir()
backup_dir = project / ".backup_system"
backup_dir = project / ".backup"
backup_dir.mkdir()
result = mod.clear_all(str(project))
@@ -694,7 +690,7 @@ class TestDriveSync:
"""Empty versioned store -- skip upload."""
project = tmp_path / "project"
project.mkdir()
bs = project / ".backup_system" / "versioned"
bs = project / ".backup" / "versioned"
bs.mkdir(parents=True)
mod = _fresh_import("aipass.backup.apps.modules.drive_sync")
@@ -776,7 +772,7 @@ class TestDriveSync:
"""Files present -- upload called."""
project = tmp_path / "project"
project.mkdir()
bs = project / ".backup_system" / "versioned"
bs = project / ".backup" / "versioned"
bs.mkdir(parents=True)
for i in range(3):
@@ -1018,9 +1014,7 @@ class TestThreadSafety:
client._drive_service = MagicMock()
client.file_tracker = {"existing.txt": {"drive_id": "abc"}}
mod.api_call_with_retry = MagicMock(
return_value={"files": [{"id": "found_folder", "name": "AIPass Backups"}]}
)
mod.api_call_with_retry = MagicMock(return_value={"files": [{"id": "found_folder", "name": "AIPass Backups"}]})
result = client.get_or_create_backup_folder()
assert result == "found_folder"
@@ -117,12 +117,12 @@ class TestProjectSetup:
"""Test project setup -- creates_files, .exists(), mkdir, makedirs tokens."""
def test_create_backup_dir(self, tmp_path: Path) -> None:
"""create_backup_dir creates .backup_system/ -- mkdir, .exists()."""
"""create_backup_dir creates .backup/ -- mkdir, .exists()."""
with patch("aipass.backup.apps.handlers.json.json_handler.log_operation"):
from aipass.backup.apps.handlers.project.setup import create_backup_dir
create_backup_dir(str(tmp_path))
backup_dir = tmp_path / ".backup_system"
backup_dir = tmp_path / ".backup"
assert backup_dir.exists()
def test_create_backup_dir_idempotent(self, tmp_path: Path) -> None:
@@ -132,7 +132,7 @@ class TestProjectSetup:
create_backup_dir(str(tmp_path))
create_backup_dir(str(tmp_path))
assert (tmp_path / ".backup_system").exists()
assert (tmp_path / ".backup").exists()
class TestProjectConfig:
@@ -161,16 +161,16 @@ class TestPathBuilder:
"""Test path builder handler -- module coverage for 'path' package."""
def test_backup_root(self, tmp_path: Path) -> None:
"""backup_root returns .backup_system path."""
"""backup_root returns .backup path."""
with patch("aipass.backup.apps.handlers.json.json_handler.log_operation"):
from aipass.backup.apps.handlers.path.builder import backup_root
result = backup_root(str(tmp_path))
assert isinstance(result, Path)
assert result.name == ".backup_system"
assert result.name == ".backup"
def test_build_snapshot_path(self, tmp_path: Path) -> None:
"""build_snapshot_path returns snapshots/ under .backup_system."""
"""build_snapshot_path returns snapshots/ under .backup."""
with patch("aipass.backup.apps.handlers.json.json_handler.log_operation"):
from aipass.backup.apps.handlers.path.builder import build_snapshot_path