packages feed

ansido-1.0.0: docs/persistence.md

# Persistence

The application keeps its state in a plain text file, with no database or binary format.

## Location

The data file is resolved in this order:

1. The `ANSIDO_FILE` environment variable, when set and non-empty.
2. `<home>/.ansido/board.tsv`, where `<home>` comes from `getHomeDirectory`. On Unix this reads `HOME`; on Windows it uses the platform profile directory, so `HOME` does not need to be set.
3. If neither is available, `defaultDataFile` returns `FilePathUnavailable` and the application prints the reason.

`ANSIDO_FILE` is useful for tests and for keeping a separate board per project. The path is never hardcoded in the source.

## Format

The current format is version 2. The first line is the version header:

```
ansido	2
```

Each following line is a tab-separated record. Empty lines are ignored.

Category record:

```
category	<id>	<name>
```

Task record:

```
task	<id>	<category id>	<status>	<title>	<note>
```

`<status>` is one of `todo`, `doing` or `done`. Status labels shown on screen are title case, but the file always uses the stable lowercase keys. `<note>` may be empty; the field is still written, so the line ends with a tab when the note is empty. Identifiers are positive integers.

Example:

```
ansido	2
category	1	Work
category	2	Personal
task	1	1	todo	Write the architecture document	Remember the pure core section.
task	2	1	doing	Implement the board	Use groups and borders.
task	3	1	done	Create the project	Initial commit.
task	4	2	todo	Buy coffee	Personal errand.
```

## Parsing

`decodeWorkspace` is pure and returns `Either PersistenceError Workspace`. It checks the header, parses identifiers with `readMaybe` as positive `Natural` values, parses status keys, validates names, titles and notes through the domain smart constructors and finally calls `mkWorkspace` to enforce referential integrity. Carriage returns are removed before splitting the lines, so files written or copied on Windows parse the same way on every platform.

Version 1 files are still accepted. A version 1 task has no note field, so its note becomes empty. Version 2 files require the note field, even when it is empty.

Failures are data:

| Error | Meaning |
| --- | --- |
| `UnsupportedHeader` | The first line is not the version 1 or version 2 header. |
| `MalformedLine` | A record has the wrong field count, an invalid identifier or an unknown status. |
| `InvalidWorkspace` | The records are well formed but violate a workspace invariant, such as a task pointing at a missing category. |

A missing file is treated as an empty workspace, not as an error. An empty file is also treated as an empty workspace.

## Writing

`saveWorkspace` writes to `<path>.tmp` and then renames it over the target. `renameFile` replaces an existing destination on every supported platform and is atomic on POSIX; on Windows it uses `MoveFileEx` with `MOVEFILE_REPLACE_EXISTING`. The parent directory is created when needed.

Files are read and written as UTF-8 and newline translation is disabled, so the board keeps Unix line endings and non-ASCII text on every platform. File system exceptions are caught at the edge and converted to `ReadFailed` or `WriteFailed`. The application does not throw for domain or input errors; a startup failure prints a message and exits with a failure code.

## Compatibility

The format is versioned through the header. The current release writes version 2 and reads both version 1 and version 2. A future format should add a new header version and keep the decoder able to read the previous versions, or reject them explicitly with `UnsupportedHeader`.

## Manual editing

The file is intentionally readable and editable by hand. Keep one record per line, use tabs between fields, preserve unique identifiers and make sure every task points at an existing category. For a version 2 task, keep the trailing tab when the note is empty. Invalid files are rejected with a line number instead of being partially loaded.