Metadata-Version: 2.4
Name: trail-atlas
Version: 0.2.0
Classifier: Environment :: Console
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Rust
License-File: LICENSE
Summary: A local CLI log of how people and agents interact with books, articles and other materials
License-Expression: MIT
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Issues, https://github.com/jointsome0-lgtm/atlas/issues
Project-URL: Source, https://github.com/jointsome0-lgtm/atlas

# Trail Atlas

Trail Atlas is a command-line log for people and agents working with books, articles and other materials. Save an account such as "Vera Example: I opened the introduction", find it later, and correct it when needed. Atlas keeps the records as JSON files in a Git repository, the store, and makes one commit per change.

## Install

The package is `trail-atlas`. It installs the `tatlas` command:

```sh
pip install trail-atlas
```

Run this in a Python environment where you can install packages. Supported platforms need neither Rust nor uv. Atlas runs `git`, version 2.28 or newer, from PATH, and commits need a Git identity (`user.name` and `user.email`). See [other installation methods](#other-installation-methods) for platform requirements and source builds.

## Create a store and save a record

The following example uses invented Vera Example data in a fresh temporary directory. Choose the setup line for your shell.

Bash on Linux or macOS:

```sh
STORE="$(mktemp -d /tmp/vera-example-atlas.XXXXXX)"
```

PowerShell on Windows:

```powershell
$STORE = Join-Path ([IO.Path]::GetTempPath()) ("vera-example-atlas-" + [guid]::NewGuid())
```

Then run these commands in the same shell:

```sh
tatlas --store "$STORE" init
tatlas --store "$STORE" profile --id vera-example --kind person --name "Vera Example"
tatlas --store "$STORE" profile --id vera-example-agent --kind agent --name "Vera Example's agent"
tatlas --store "$STORE" --json add --id vera-example-1 --material https://example.org/book --actor vera-example --recorded-by vera-example-agent --copy vera-example-copy --model vera-example-model --original "Vera Example: I opened the introduction."
tatlas --store "$STORE" --json get --id vera-example-1
```

`init` creates the store: a Git repository on branch `main`. `profile` creates a person or agent profile, a logical identity that records and marks refer to by ID. `add` saves the record and returns the `commit` it made and the record's `version`. `get` reads it back by ID.

- `--material` identifies the material by an exact URL or reference. Atlas does not fetch it or rewrite the reference.
- `--actor` is the profile whose interaction this is. `--recorded-by` is the profile that records it. Atlas never infers the actor from the recorder, so an agent's reading is recorded with the agent as actor and never stands for a person's.
- `--copy` names the short-lived copy, for example an agent run, that writes the record; `--parent-copy` names the copy that started it, if any; `--model` names the model used at that moment.
- `--original` stores the actor's words exactly. `--commentary` holds the recorder's commentary, kept apart from them.

Saving a reference alone leaves the action unspecified and never establishes reading or understanding. `tatlas add --help` lists the optional fields.

For real records, choose a fixed, absolute, private directory outside public code checkouts and other Git work trees; `init` refuses a directory inside a work tree. Pass the store's top directory as `--store` every time. Each long-lived actor, a person or a long-lived agent, has one store; a person records through an agent into the person's own store.

`--json` produces one-line results on stdout and errors on stderr; without it, results print as indented JSON. Help remains text. `--store` and `--json` work before or after the command name.

## Records and marks

A record describes one interaction with a material. If Vera Example opens a book today and reads a chapter tomorrow, those are two records. Use `add` for another interaction or a new opinion, `edit` to correct a record, and `rm` to retract one. A retraction replaces the content with a tombstone; the ID stays reserved and the content stays in Git history.

A mark is one profile's current priority for an exact material: `focus` to work on now, `later` to revisit, or `none` to clear it. Each profile has at most one mark per material, and profiles mark independently. Marks need no record, never change records, and stay when a record is retracted.

## Choose a command

| To | Use |
| --- | --- |
| Create a store | `init` |
| Create or rename a profile | `profile` |
| Read profiles | `profiles` |
| Save another interaction | `add` |
| Find saved records | `list` |
| Read a record by ID | `get` |
| Correct a record or clear optional fields | `edit` |
| Retract a record | `rm` |
| Set or clear a profile's mark | `mark` |
| Read marks | `marks` |
| Validate a commit before promotion | `check` |

Run `tatlas COMMAND --help` for its fields.

`list` puts the newest recordings first, then ascending IDs, and omits retracted records. Filter by an exact reference with `--material`, or by a case-sensitive substring with `--context`. A record must match every supplied filter.

## Change existing data

A version is the Git object ID of an object's file in the current commit. Pass the version you read as `--if-version`; Atlas rejects the write with exit code 3 and `current_version` when the object changed. Read the current data and reconsider your change before trying again.

- Before `edit` or `rm`, use `get --id ID` and pass its `record.version`.
- Before changing a mark or a profile, read `marks` or `profiles`. `mark` and `profile` default to `--if-version absent`, which is correct only for a new mark or profile.

`edit` changes only the fields you supply; repeat `--clear FIELD` to clear optional fields. The ID, recorder, copy, model and recording time never change. A request that changes nothing fails without a commit.

## Retry and recover

Choose and retain `--request` before a write. If the response is lost, repeat the same command with the same request: Atlas finds the commit carrying that request and returns it with `replayed: true` and no new commit. The commit keeps a fingerprint of the command's fields and `--if-version`, so the same request with any other field is a conflict. Without `--request`, Atlas generates one, and without `--id`, the record ID is the request ID.

Atlas writes a file and then commits it. If it stops in between, the store keeps an uncommitted change. Reads use only commits, so that change is never accepted memory, and writes refuse until the working tree is clean. Inspect it with `git -C "$STORE" status`. If Git reports a leftover `index.lock` while no Git process runs, remove it. Set the change aside with `git -C "$STORE" stash push --include-untracked`, then retry with the same request.

Exit codes are `0` for success, `1` for an operation or storage error, `2` for flag parsing errors and `3` for version, ID or request conflicts.

## Copies, sync and promotion

Give the store an origin, for example a private repository, and publish `main`:

```sh
git -C "$STORE" remote add origin URL
git -C "$STORE" push -u origin main
```

Each short-lived copy writes in its own branch. On the same machine, add a worktree and pass its directory as `--store`. Elsewhere, clone the origin and create a branch.

```sh
git -C "$STORE" worktree add "$COPY" -b vera-example-copy
tatlas --store "$COPY" --json profiles
```

Turn sync on with `git -C "$STORE" config atlas.sync true`. Atlas then releases its lock and pushes the current branch to `origin` after each commit, stopping the push after `atlas.pushTimeout` seconds, a whole number from 1 to 3600 (60 by default); a value outside that range refuses the write before any change. On Unix the push runs in its own process group led by a `/bin/sh` supervisor, which kills the whole group once the push succeeds, fails or times out, or once Atlas exits for any reason, including Ctrl-C, so transport processes in that group never outlive Atlas. It reports `sync` as `pushed`, or `failed` with `sync_error`; the write stands either way, and the next synced write or `git push` retries. With sync off, `sync` is `off`.

`list`, `marks` and `profiles` read the current branch. With `--fresh`, they fetch `origin` first and search every local and remote branch, main and copies; each result lists its `refs`. Each branch must first pass the validation that `check` applies, with every commit beyond `origin/main` treated as new. A failed fetch or an invalid branch fails the read. An absent record does not prove that nobody read a material.

Atlas never merges. A coordinator merges a copy branch with Git, for example `git -C "$STORE" merge --no-ff vera-example-copy` on `main`, so that the merge commit's first parent is the accepted main. Record, profile and mark files are marked `-merge` in `.gitattributes`, so when both sides changed one file, the merge stops with the current side in the working tree. Keep it, or take the other side with `git checkout BRANCH -- FILE`, then `git add` and commit. Run `tatlas --store "$STORE" check` on the merge commit. It requires regular files, the schema marker, `.gitattributes` exactly as `init` wrote it, `-merge` in effect for every stored file in this worktree, including `.git/info/attributes`, and existing profiles. It also checks every record change in the commits the merge brings in beyond its first parent: a record may not disappear, a retracted record may not come back, and its recorder, recording time, copy, parent copy and model never change. Push `main` only after `check` passes; otherwise correct the files and amend the commit.

## Storage details

The store contains `atlas.json` (schema version 2), `.gitattributes` (`*.json -merge`), `profiles/<id>.json`, `records/<id>.json` and `marks/<profile>/<key>.json`. A mark key is a 64-bit FNV-1a hash of the exact material reference; the file keeps the reference, and Atlas refuses a key collision. IDs use 1 to 64 lowercase ASCII letters, digits, hyphens or underscores; Windows device names are reserved on every platform. Unknown fields and invalid identities fail visibly, and lists fail without partial output.

Atlas stores the recording time in UTC as `recorded_at`, for example `2026-01-31T12:00:00.000Z`. The optional `--interaction-date` describes when the interaction happened and does not change list order.

A write holds the lock file `atlas.lock` in the worktree's Git directory from its checks to its commit. It requires the top directory of a store worktree on a branch, a clean working tree, no unfinished merge and no symbolic link on the path it writes. Each commit changes one file and ends with `Atlas-Request` and `Atlas-Fingerprint` trailers. Atlas runs Git with `core.hooksPath` set to the `HEAD` file of the worktree's Git directory and refuses a store whose `HEAD` is not a regular file, so no hook can exist below that path and none runs. Git ignores Atlas's lock, so do not run Git commands that change a worktree while Atlas writes in it. On Unix, `init` makes the store directory private to its owner, even when it already existed; permissions of clones and worktrees remain the owner's responsibility.

## Other installation methods

Ready-made packages cover Linux with glibc 2.28 or newer on x86_64 and ARM64, macOS 13 or newer on Intel and Apple silicon, and Windows x86_64. Other targets require a source build and have no native CI coverage.

With uv, use `uv tool install trail-atlas`. For one run, use `uvx --from trail-atlas tatlas --help`.

A source build needs current stable Rust and a native linker. This implementation was checked with Rust 1.98.1. Pip also needs these tools if no ready-made package matches the platform.

```sh
cargo build --locked --release
cargo install --locked --path .
```

The built executable is `target/release/tatlas`, or `tatlas.exe` on Windows. It has no server; it reaches the network only through Git, when sync is on or a read uses `--fresh`.

## Development

Read AGENTS.md and GOALS.md for repository rules. Check changes with:

```sh
cargo fmt --all -- --check
cargo clippy --locked --all-targets -- -D warnings
cargo test --locked
python3 scripts/limits.py
```

Stage new files before running limits. The public-hygiene check reads staged blobs, so an unstaged edit cannot hide an unmarked staged fixture or missing ignore rule. Known path checks do not detect every personal fact; review data before committing. The optional pre-commit hook runs formatting and limits without changing Git hook configuration. Tests run Git with an invented Vera Example identity, without system or global Git configuration, against stores and bare origins in temporary directories.

CI runs the checks on all five supported platforms, then builds and installs each wheel through pip, uv tool and uvx. One Linux job also builds and installs the source archive. The publisher uploads those same tested artifacts to PyPI; the GitHub release attaches them and SHA256SUMS. It does not rebuild packages. Rust SBOM generation is disabled because its metadata includes local build paths.

Publishing uses repository `jointsome0-lgtm/atlas`, workflow `ci.yml` and environment `pypi`. A tag `vX.Y.Z` must match Cargo.toml and Cargo.lock and point to a commit on main. The tag must also be allowed by the environment's deployment policy. Changes to publishing access require the owner's approval.

## Map

- `examples/`: Rust syntax checker used by the limits entrypoint.
- `scripts/`: Repository limits, distribution contents and installed CLI checks.
- `src/`: CLI parsing, record and profile rules, and the Git-backed store.
- `tests/`: Product and limits tests through executable boundaries.
- `tests/support/`: Temporary stores, isolated Git and subprocess helpers for CLI tests.

