frostx 0.1.0

frostx monitors project directories for inactivity. Once a configured inactivity threshold elapses (e.g. "90 days since any file was modified"), frostx executes a pipeline of **actions** - e.g., checking git state, creating archives, uploading backups, deleting local copies. Automating the lifecycle of projects, frostx helps users manage disk space and maintain a clean workspace.
Documentation
# State Storage

frostx separates configuration from runtime state. `frostx.toml` is pure configuration and is never written to after
`frostx init` - it is safe to commit to version control.

## Location

Mutable state is stored under the [XDG Base Directory](https://specifications.freedesktop.org/basedir-spec/latest/) data
home:

```
$XDG_DATA_HOME/frostx/<uuid>.toml
```

`XDG_DATA_HOME` defaults to `~/.local/share/` when unset, giving:

```
~/.local/share/frostx/<uuid>.toml
```

The directory can be overridden at runtime with the `--state-dir` flag (applies to all commands):

```
frostx check --state-dir /mnt/shared/frostx-state ~/projects/my-app
```

The file is named after the project's UUID, so state survives the project folder being renamed or moved.

## State File Format

```toml
# last known absolute path - updated on every scan
project_path = "/home/user/projects/my-app"

# timestamp of the last frostx run against this project
last_scan = "2025-04-01T08:00:00Z"

# per-rule completion records (one entry per [[rule]] in frostx.toml)
[[rule]]
# SHA-256 of the rule's `after` value and `actions` list - identifies this rule
hash = "a3f1c2..."
completed = [  # actions that have been successfully executed and need not repeat
    "archive.compress",
    "backup.upload",
]
last_run = "2025-04-01T08:00:00Z"
rule_done = true  # set when the rule has once = true and all actions succeeded

[[rule]]
hash = "9e4b7d..."
completed = []
last_run = "2025-04-01T08:00:00Z"
```

### Rule identity and config changes

Each `[[rule]]` entry in the state file is keyed by a SHA-256 hash computed from the rule's `after` value and its
`actions` list. This means:

- **Reordering rules** in `frostx.toml` does not lose completion state — each rule is matched by content, not position.
- **Changing `after` or `actions`** produces a new hash. The old state entry is no longer matched and completion is
  silently reset — the rule is treated as if it has never run. This is intentional: a structurally different rule
  should re-execute its actions.
- **Changing only `name`** does not affect the hash and preserves completion state.
- **Old state files** (from versions that stored `index` instead of `hash`) have no `hash` field; those entries default
  to `hash = ""`, which never matches any rule, so completion state is silently discarded on first use.

## What Gets Recorded

Not all actions are recorded as completed:

| Action type                                                       | Recorded? | Reason                                                           |
|-------------------------------------------------------------------|-----------|------------------------------------------------------------------|
| **Checks** (`git.check_clean`, `backup.check`, ...)               | No        | Re-evaluated on every run; their result may change               |
| **Mutations** (`archive.compress`, `backup.upload`, `local.delete`) | Yes       | One-time operations; re-running would be destructive or wasteful |

Use `frostx run --force` to re-execute completed mutation actions.

### `once = true` rules

When a `[[rule]]` has `once = true`, an additional `rule_done = true` flag is written to the state file after **all**
actions (checks and mutations) succeed in a single run. On subsequent invocations the entire rule is skipped, not just
individual mutations. `--force` bypasses this flag.

The `rule_done` flag follows the same hash-based invalidation as per-action completion: changing `after` or `actions`
produces a new hash, discarding both the per-action completion list and the `rule_done` flag.

## UUID Collisions

A collision occurs when a project directory is copied - the copy inherits `frostx.toml` with the original's UUID, but
frostx's state file for that UUID records a different `project_path`.

**Detection:** on every command that loads state (`check`, `run`, `scan`), frostx compares the current working path
against the `project_path` in the state file. If they differ, frostx treats this as a collision.

**Behavior on collision:**

- The command is aborted with exit code `4`.
- An error is printed identifying the conflict:

```
error: UUID collision detected
  current path  /home/user/projects/my-app-copy
  state records /home/user/projects/my-app

This project appears to be a copy. Run `frostx init --force` to assign a new UUID
and start fresh state for this directory.
```

- The original project is unaffected; its state file is not modified.

**Resolution:** run `frostx init --force` in the copied directory. This generates a new UUID, writes it to
`frostx.toml`, and creates a clean state file. The copy is then treated as an independent project.

## Tracked-Project Registry

State files also serve as a registry of tracked projects. Use the `projects` subcommand to manage them explicitly:

```bash
frostx projects list               # list all tracked projects
frostx projects add ~/my-app       # register a project
frostx projects add --scan ~/src   # register every project found under ~/src
frostx projects rm ~/old-app       # unregister a project (deletes its state file)
```

`frostx projects check` and `frostx projects run` operate across all registered projects in one invocation.

## Stale State

If a project's `frostx.toml` is deleted or its UUID changes, the corresponding state file in `$XDG_DATA_HOME/frostx/`
becomes orphaned. Run `frostx gc` to find and remove them. Use `--dry-run` first to preview what would be deleted.