Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions decisions/decisions/adr-129-confined-rename-writes.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
schema_version: 1
id: RAC-01K8Q7MCP407
Expand Down Expand Up @@ -27,9 +27,10 @@
the offending path; no file is written.
- Application repeats the containment and symlink checks immediately before
reading each file and immediately before replacing it.
- Unix replacement opens the final path with `O_NOFOLLOW` as a final-component
race guard. Read-only discovery remains unchanged and may still report
symlinked Markdown files.
- Unix staging opens each temporary final component with `O_NOFOLLOW` as a
final-component race guard; same-directory replacement uses the staged file
after the immediate root checks. Read-only discovery remains unchanged and
may still report symlinked Markdown files.

## Status

Expand All @@ -49,8 +50,8 @@

The protection is deliberately narrow. It does not change read-only walk
parity, rename ordering, identity semantics, or the exact-line stale-plan
check. `O_NOFOLLOW` closes the final-component race on Unix; the immediate
rechecks provide the same root-boundary policy on other platforms.
check. `O_NOFOLLOW` closes the final-component race while staging on Unix; the
immediate rechecks provide the same root-boundary policy on other platforms.

## Alternatives Considered

Expand Down Expand Up @@ -87,7 +88,7 @@
kind: require_pattern
path_glob: "rust/rac-engine/src/rename.rs"
pattern: "O_NOFOLLOW"
message: "Unix rename replacement must refuse a final-component symlink race."
message: "Unix rename staging must refuse a final-component symlink race."
```

## Related Decisions
Expand Down
115 changes: 115 additions & 0 deletions decisions/decisions/adr-130-transactional-rename-application.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
---
schema_version: 1
id: RAC-01K8Q7MCP408
type: decision
---
# ADR-130: Transactional Rename Application

## Context

An artifact-id rename edits the target identity and every inbound reference.
Writing those files one at a time can leave the corpus half-renamed when a
later stale check, permission check, or filesystem replacement fails. A green
process exit must never hide a split identity/reference state.

## Decision

`decided rename --apply` uses a deterministic local transaction for all files in
the plan.

- Every affected file is read, checked for exact `old_line` staleness, and
rendered in memory before any corpus path is replaced.
- Each rendered result is written and flushed to a hidden sibling staging file
in the same directory. Staging uses exclusive creation; Unix opens the
temporary final component with `O_NOFOLLOW`.
- During commit, each original moves to a hidden sibling backup and its staged
replacement moves into the original path. Files are processed in the
plan's first-seen path order.
- Any backup, replacement, or containment failure rolls committed files back
in reverse order from their backups. A successful rollback says `corpus
restored`; an incomplete rollback is reported explicitly with the paths that
could not be recovered.
- Successful commits remove all staging and backup files. Cleanup failures are
reported as a committed-but-cleanup-incomplete result; they never masquerade
as a clean success.

The transaction remains bounded to the canonical root and the root-confined
mutation checks in ADR-129. Read-only walk behavior is unchanged.

## Status

Accepted

## Category

Technical

## Consequences

The identity and inbound references move together or the engine reports an
explicit failure. A later filesystem error can still make rollback impossible
if an external actor replaces a path during recovery, but the command reports
that condition rather than claiming success. Temporary siblings stay on the
same filesystem, so each rename operation is atomic at the individual-path
level and does not require a cross-volume coordination service.

The commit is intentionally not a database transaction: no filesystem-wide
multi-path atomic primitive exists across the supported platforms. Backups and
reverse-order restoration provide deterministic recovery within the corpus
boundary.

## Alternatives Considered

### Continue writing files sequentially in place

Rejected. A late stale or permission failure can leave references and identity
out of sync, which is precisely the integrity failure this decision closes.

### Stage files but do not retain backups

Rejected. Staging protects against a failure before commit, but cannot restore
files already replaced when a later rename fails.

### Use a database or filesystem snapshot

Rejected. The corpus is ordinary Markdown on filesystems with different
snapshot capabilities. Sibling backups preserve portability and keep the
mutation contract local and inspectable.

## Code Constraints

```yaml
version: 1
eligibility: eligible
reason: "Transactional rename ordering and rollback are deterministic filesystem behavior."
rules:
- id: rename-preflights-before-commit
kind: require_pattern
path_glob: "rust/rac-engine/src/rename.rs"
pattern: "PreparedRenameFile"
message: "Rename must render all affected files before replacing any corpus path."
- id: rename-stages-sibling-files
kind: require_pattern
path_glob: "rust/rac-engine/src/rename.rs"
pattern: "create_new"
message: "Rename staging must use exclusive sibling temporary files."
- id: rename-rolls-back-on-failure
kind: require_pattern
path_glob: "rust/rac-engine/src/rename.rs"
pattern: "rollback_transaction"
message: "Rename commit failures must attempt deterministic reverse-order recovery."
```

## Related Decisions

- adr-007
- adr-023
- adr-129

## Applies To

- rust/rac-engine/src/rename.rs
- rust/rac-engine/src/output.rs
- rust/rac-engine/tests/rename.rs
- rust/PORT-CONTRACT.d/16-closure-scaffold-writes.md
- docs/cli.md
5 changes: 5 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,11 @@ untouched and exits `1`.
ordered by path then line (ADR-002).
- **Reversible** — applying `rename <new> <old>` after a rename restores the
original bytes. No semantic inference happens anywhere.
- **Transactional** — every affected file is preflighted and staged before
replacement. Same-directory backups allow reverse-order rollback when a
later replacement fails; the command reports `corpus restored` or an
explicit incomplete-recovery error rather than silently leaving a partial
rename (ADR-130).
- **Clean afterwards** — after `--apply`, `decided relationships <dir> --validate` is
clean: every inbound reference resolves to the renamed artifact.

Expand Down
11 changes: 8 additions & 3 deletions rust/PORT-CONTRACT.d/16-closure-scaffold-writes.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,14 @@ under that root. A symlinked path or a path that cannot be resolved is a
whole-plan refusal with reason `symlink-path` or `path-outside-root`; the
human dry-run includes the offending path and JSON keeps it in
`target_path`. Apply repeats the checks immediately before each read and
replacement; Unix replacement also uses `O_NOFOLLOW` for the final path
component. Read-only discovery still yields symlinked Markdown files for
parity.
replacement; Unix staging opens its final component with `O_NOFOLLOW` and
same-directory replacement uses the staged file. All affected files are
preflighted and staged before any replacement. Originals move to sibling
backups and a later failure triggers reverse-order rollback; the engine reports
`corpus restored` or an explicit `rollback incomplete` error. Successful
commits remove staging and backups; cleanup failures are reported rather than
silently ignored. Read-only discovery still yields symlinked Markdown files
for parity.

## 7. `rac migrate {metadata} <directory> [--dry-run] [--top-level] [--recursive] [--json]`

Expand Down
Loading
Loading