# Receipt Writer Contract

## Atomicity Guarantee

All receipt writes use atomic rename with fsync to ensure crash safety and durability.

### Write Pattern

```python
1. Write to .tmp file in same directory (ensures same filesystem)
2. fsync() to force disk write (survives power loss)
3. os.rename() for atomic replace (POSIX guarantee)
4. No partial files ever exist
```

### Implementation

```python
from receipts.storage import write_bytes_atomic, write_text_atomic

# Parquet receipts (ledger)
write_bytes_atomic(ledger_path, parquet_buffer.getvalue(), tenant)

# JSON receipts (inbox)
write_text_atomic(inbox_path, canonical_json, tenant)
```

### Crash Safety

**Guarantee**: Any crash or power loss leaves **zero partial receipts**.

- ✅ Crash before write → no file exists
- ✅ Crash during write → temp file discarded
- ✅ Crash before fsync → temp file discarded
- ✅ Crash after fsync, before rename → temp file discarded
- ✅ Crash after rename → complete file persisted

### KMS Integration

When `LOOM_KMS_PROVIDER` is set, atomic writes include encryption:

```python
1. Encrypt payload with KMS
2. Write encrypted envelope atomically
3. Same crash-safety guarantees
```

### Testing

Chaos tests in `tests/receipts/test_writer_atomic.py`:

```bash
pytest tests/receipts/test_writer_atomic.py -v
```

Tests verify:
- No partial files on crash (simulated via mock)
- Temp files cleaned on all error paths
- Concurrent writes don't interfere
- KMS encryption failures don't leak partials
- Existing files unchanged on failure

### Performance

Atomic writes add minimal overhead:
- ~1-2ms per fsync on modern SSD
- Worth it for legal-artifact durability
- Parquet + JSON writes batched per receipt

### Storage Backends

Atomic writes work on:
- ✅ Local filesystem (POSIX)
- ✅ NFS (if atomic rename supported)
- ✅ EFS, GCS Fuse (with caveats - check vendor)
- ❌ S3 (use atomic upload via PUT with ETag)

For S3, see `receipts/storage_s3.py` (TODO).

## API

### `write_bytes_atomic(path, data, tenant)`

Write bytes atomically with fsync.

**Args:**
- `path`: Destination file path
- `data`: Raw bytes to write
- `tenant`: Tenant identifier (for KMS encryption)

**Raises:**
- `OSError`: Disk full, permission denied, etc.
- `ApiError`: KMS encryption failure

**Example:**
```python
write_bytes_atomic(
    path=Path("/data/receipts/ledger/part-0001.parquet"),
    data=parquet_buffer.getvalue(),
    tenant="acme_health"
)
```

### `write_text_atomic(path, text, tenant)`

Write UTF-8 text atomically with fsync.

**Args:**
- `path`: Destination file path
- `text`: String to write
- `tenant`: Tenant identifier

**Example:**
```python
write_text_atomic(
    path=Path("/data/receipts/inbox/PROP-123.json"),
    text='{"receipt_id": "PROP-123", ...}',
    tenant="acme_health"
)
```

## Migration

Old code using `write_bytes()` / `write_text()`:
```python
# Before (not atomic)
write_bytes(path, data, tenant)

# After (atomic + fsync)
write_bytes_atomic(path, data, tenant)
```

No API changes needed - drop-in replacement.

## References

- Implementation: `receipts/storage.py:58-126`
- Tests: `tests/receipts/test_writer_atomic.py`
- Writer integration: `receipts/writer.py:120-126`
