States, observations, and candidate solutions
A state is a complete path-to-artifact mapping with causal references. It is
independent of whichever state HEAD currently names. A snapshot uses HEAD as its
base unless --base <state-id> explicitly selects another existing state.
S0 / \ S1 S2Both S1 and S2 remain discoverable even after HEAD moves to S2. No named branches
are necessary. To produce siblings, capture S1 with --base S0, edit the normal
workspace into the complete desired alternative, and capture S2 with --base S0.
Selecting a base does not restore any files. V1 deliberately has no checkout or
automatic synthesis. Parents, operations, and intents are arrays in the schema;
v1 permits one of each for non-root states until synthesis semantics are defined.
Materialization
aip materialize <state-id|HEAD> --to <directory> reconstructs a state into a
new directory outside the source repository. It resolves HEAD once and verifies
the complete state graph before creating output. Source files, HEAD, references,
and objects are not modified, even when the working directory has unsaved changes.
The output contains only the state’s files and their required parent directories;
no AIP repository metadata is generated. Recorded nested .aip paths, if any,
are ordinary artifact paths and are reproduced like any other content.
The destination’s parent MUST already exist. Relative destinations are resolved against the caller’s working directory, including when called in a source subdirectory. Parent symlinks are resolved; the returned destination is absolute with its parent resolved. Physical ancestry checks reject destinations inside the source, including aliases and case-insensitive spellings. Any existing final destination, including an empty directory or dangling symlink, MUST be refused. The command uses exclusive mkdir to claim the destination and exclusive file creation to avoid overwrites. Concurrent attempts on the same destination cannot both succeed.
File contents MUST exactly match the referenced artifacts. The executable boolean becomes mode 0755 when true and 0644 when false; original non-executable permission bits are not recoverable from the protocol. The destination root is created with mode 0700, intermediate directories with 0755 subject to the process umask. Symlinks are not reconstructed. Materialized files are new regular files, not hard links to CAS objects. Editing them cannot modify canonical history.
Unrepresentable names, file/directory conflicts, case folding, and Unicode name
normalization MUST fail rather than silently change the path map. The reference
implementation observes the complete destination and compares paths, artifact
IDs, and executable flags with the source state before reporting success. An
empty state materializes as an empty directory. Verification observes the
complete destination without ignore rules: the implementation wrote every byte
there, and a state may legitimately record paths that its own .aipignore
would hide from a live workspace.
The final destination is visible while files are being written; readers MUST wait for successful command completion. This is not atomic directory publication or a filesystem durability guarantee. Ordinary failures remove only the newly claimed output directory, with cleanup errors reported. A detected replacement of the claimed directory prevents cleanup. A crash may leave partial output; the operator must inspect and remove it before retrying. The destination parent and output tree must not be moved or edited during the operation. Hostile local writers, mount changes, and arbitrary external filesystem mutation are outside the local filesystem threat model.
Materialization adds no canonical object fields or types and records no evidence. Testing the materialized files is a separate action; results may be reported against the original state with the execution location described in notes.
Observation and operations
Snapshot requires an existing intent. It records every regular file recursively
except the repository’s top-level .aip directory and paths excluded by the
ignore rules below. There is no implicit hidden-file
exclusion. Empty directories, ownership, mtime, ACLs,
and non-executable permission bits are not versioned. Executability is true if
any of a file’s three executable bits is set. Symlinks, sockets, devices, and
other non-regular entries fail explicitly. There is no Git-specific handling.
Files contain arbitrary bytes, including empty files and invalid UTF-8. Paths must follow objects.md. Filesystem case folding and Unicode normalization are not protocol equivalences; materialization must detect unrepresentable names on the destination filesystem rather than silently overwrite them.
The observer uses rooted filesystem access to prevent following paths outside the workspace. It checks file identity, size, mode, and modification time around reads and rejects detected concurrent changes. This is not an OS-level atomic snapshot and cannot detect every external race, including writes that restore metadata. Callers MUST quiesce editors/build tools when coherent observation is required. Repository locking coordinates AIP writers, not arbitrary processes.
Snapshot compares the complete observation with the chosen base. Each changed path creates an exact before/after effect, including deletions and mode changes. Artifact inputs are the before-image artifacts. The resulting State references the Operation and Intent; the Operation references the base and its inputs/effects. Recorded effects are assertions that can be independently validated against state maps, not a replay of an editor’s keystrokes or a claim about agent reasoning. The intent preserves declared rationale and constraints, and Decisions provide structured additional rationale.
A no-change snapshot is permitted and records an empty operation tied to an intent. It still creates a child of the selected base. Repeating an identical observation with the same explicit base and intent yields the same operation and state IDs. Recording another child of the new HEAD has a different base and ID.
Ignore rules
An optional regular file named .aipignore at the repository root lists paths
that observation does not record. It is the only ignore file; nested files and
per-user rules are not read. The file is an ordinary workspace file, so it is
recorded in states, travels with materialization, and can never be ignored by
its own rules. The rules are independent of Git and have no Git-specific
defaults.
Syntax, one pattern per line after trimming surrounding whitespace:
- Blank lines and lines beginning with
#are comments. *matches any run of characters within one path segment,?matches one character, and[...]matches a character class, all as in Go’spath.Match.- A segment consisting solely of
**matches zero or more path segments. - A pattern without
/matches a single name at any depth. - A pattern containing
/is anchored at the repository root. A leading/anchors a single name there. - A trailing
/restricts the pattern to directories. Symlinks are not directories. - Negation (
!), backslashes, empty segments,., and..are errors. A malformed line fails the command with its line number; a symlinked or otherwise non-regular.aipignorefails explicitly.
A path is ignored when it or any ancestor directory matches a rule. An ignored directory is skipped without being read, so unsupported entries inside it, such as the symlinks in a dependency tree, do not block observation. An unignored symlink or special file still fails explicitly.
Observation is a function of the workspace and the rules only; it does not depend on HEAD or the chosen base. A recorded path that later matches a rule is therefore absent from the next observation: status and snapshot report it as a deletion, and the next state omits it while the file remains on disk. Already recorded artifacts and states are immutable and remain in history. Status, snapshot, and evaluation inspection apply the same rules. Materialization verification applies none.
Reported and executed evidence
evidence add resolves the state once, defaults to HEAD, and records the supplied
command/result with provenance: reported. It also records state operations,
current repository-relative working directory, platform, reporter, notes, and
recording time. Reporter defaults to unspecified, not an inferred identity.
The command is never run by evidence add. A reporter is responsible for testing
the actual referenced state; dirty-workspace execution and fabricated claims
cannot be authenticated by this mechanism. The CLI does not equate passing
reported evidence with trusted evaluation.
evaluate <state> -- <program> [args...] materializes the state and runs the
explicit command, saving executed evidence v2 with argv, outcome, timing, output
artifacts, and a before/after workspace comparison. It records observation, not
authenticated truth. See evaluation.md for the execution contract.
V1 reports and v2 observations coexist and are both found by reverse lookup.
Evidence added later does not change the state or operation IDs. Reverse lookup finds those associations. Decisions likewise reference existing objects without rewriting them. The current CLI has no decision-creation command; the primitive is supported by canonical storage and the reference implementation’s object API.
Diff and traversal
File changes are semantic facts at this milestone: path addition, deletion, content replacement, or executable-bit change. Textual diff is a derived representation of two states, not a canonical object or an operation input. The API supplies a list of typed representations so future adapters can add symbol, contract, schema, dependency, or behavioral comparisons.
UTF-8 content without NUL uses unified text. A bounded one-hunk algorithm retains
up to three lines of outside context and may repeat unchanged interior lines;
it does not promise a minimal edit script. Missing final newlines are identified.
Other content yields binary while before/after entries retain exact identities.
Paths in structured JSON remain authoritative for names containing control
characters; textual headers are for display, not an apply-patch protocol.
History follows state parents, visits each state once, and emits parents before children. All stored candidates are included, not just HEAD ancestors. The reference implementation iterates states in ascending ObjectID order, recursively emitting unseen parents before emitting each state; parent arrays are also in ObjectID order. This specifies a deterministic depth-first traversal, not a timestamp sort or a globally ID-sorted list. There is no canonical wall-clock order for states or operations.