@codai/axiom-apply
v2.4.0
Published
Transactional two-phase-commit apply engine for AXIOM v2 manifest bundles: containment, staging, journal, scoped rollback
Maintainers
Readme
@codai/axiom-apply
Transactional apply engine for AXIOM v2 ManifestBundles (design §4). apply() never throws; every
failure is an AxiomError code from @codai/axiom-schema surfaced in ApplyResult.error.
Guarantees
- Nothing outside
rootis ever written. Every artifact path is schema-validated (POSIX-relative, NFC, Windows reserved names rejected on every OS), checked for case-insensitive collisions, walked segment by segment withlstat(symlink/junction →ERR_SYMLINK_IN_PATH) and realpath-contained (ERR_CONTAINMENT). - Content is what the manifest says. Blobs are re-hashed on resolution (
ERR_DIGEST_MISMATCH,ERR_SIZE_MISMATCH) and again after every write; the whole bundle is resolved before the first write. - All-or-nothing on the user tree. Phase 1 stages under
.axiom/staging/<digest>/(tmp → fsync → rename). Phase 2 re-verifies each target's pre-image right before its rename (ERR_PREIMAGE_CHANGED, closes the check-then-apply TOCTOU window), backs up pre-images, then renames. Any phase-2 error rolls back in reverse order, scoped to exactly the manifest's paths, and returnsstatus: "rolled-back". - Single writer per root.
.axiom/lock(O_EXCL,{pid, hostname, startedAt, manifestDigest}); stale if the pid is dead or older than 1 h; 30 s wait →ERR_LOCKED. - Idempotent. Same digest applied twice with matching on-disk content →
status: "noop". - Crash-safe. A journal left in
committing/rolling-backis rolled back at the nextapply;rollback(root, digest)is exposed for the CLI. mode: "dry-run"runs phase 1 only, returns a unified diff (≤ 1 MiB) and leaves the tree untouched.mode: "pr"wraps the fs apply in a git branch + commit (git.ts, spawn args array, no shell): branch validated by regex +git check-ref-format --branch, default nameaxiom/<name>/<digest12>(deterministic), touched paths must be clean (git status --porcelain -- <paths>),git switch -c, fs apply,git add -- <paths>explicit only,git commit --quiet -F -with the message on stdin,result.git = { branch, commit, compareUrl? }. Hooks are honoured. No push, no PR creation, no network — rungh pr create --head <branch>afterwards. Failures:ERR_GIT_NOT_REPO,ERR_GIT_DIRTY,ERR_GIT_BRANCH_EXISTS,ERR_GIT_BRANCH_INVALID,ERR_GIT_NOT_FOUND,ERR_GIT_FAILED; an fs or commit failure rolls the apply back and drops the branch.
Non-guarantees
- No protection against a concurrent writer that ignores
.axiom/lock; the pre-image check narrows the window to the instant before rename but cannot close it on POSIX/NTFS without a snapshotting FS. - Directory
fsyncis skipped on Windows; file mode0755is recorded but not applied on Windows. refartifact sources are not fetched in v2.0 (ERR_REF_OFFLINE); only inline blobs and the local CAS.- Backups (
keepBackups, default 3 manifests) are a convenience, not a version-control system.
.axiom/ layout
.axiom/lock single-writer lockfile
.axiom/staging/<hex>/ full new tree, deleted after commit or on any failure
.axiom/journal/<hex>.json Journal (phase + steps), fsynced before phase 2
.axiom/backup/<hex>/ pre-images of overwritten/deleted files (hardlink, else copy)
.axiom/applied/<hex>.json ApplyResult; presence == idempotency marker
.axiom/cas/sha256/<aa>/<hex> optional content-addressed store read by resolveContent
.axiom/tmp/ case-sensitivity probe scratchNote: this package keeps isolatedDeclarations: true from the base tsconfig.
