# API Contract: rettxid v1

**Feature**: 001-rettxid-core  
**Date**: 2025-12-06  
**Version**: 1.0.0

---

## Module: `rettxid`

### Installation

```bash
pip install rettxid
```

### Import

```python
from rettxid import generate_rettx_id, validate_format, normalize
```

---

## Public Functions

### `generate_rettx_id() -> str`

Generate a new cryptographically random rettX ID.

**Signature**:
```python
def generate_rettx_id() -> str: ...
```

**Parameters**: None

**Returns**: `str` — A valid rettX ID in canonical format

**Raises**: 
- `RuntimeError` — If system entropy source is unavailable (extremely rare)

**Example**:
```python
>>> from rettxid import generate_rettx_id
>>> generate_rettx_id()
'rettx-8GZ4-MK3P-1Q9L'
>>> generate_rettx_id()
'rettx-2NVF-7KQR-4TXB'
```

**Guarantees**:
- Output always matches v1 format: `rettx-XXXX-XXXX-XXXX`
- Characters are from ambiguity-safe alphabet (no 0, 1, I, L, O, S)
- Uses `secrets` module for cryptographic randomness
- No two calls return the same value (with probability > 1 - 10⁻¹⁷)

---

### `validate_format(id: str) -> bool`

Check if a string matches the rettX ID v1 format.

**Signature**:
```python
def validate_format(id: str) -> bool: ...
```

**Parameters**:
- `id: str` — The string to validate

**Returns**: `bool` — `True` if valid format, `False` otherwise

**Raises**: Never raises (returns `False` for any invalid input including non-strings)

**Example**:
```python
>>> from rettxid import validate_format
>>> validate_format('rettx-8GZ4-MK3P-1Q9L')
True
>>> validate_format('rettx-8GZ4-MK3P')  # Missing group
False
>>> validate_format('RETTX-8GZ4-MK3P-1Q9L')  # Wrong prefix case
False
>>> validate_format('rettx-8GO4-MK3P-1Q9L')  # Contains 'O' (excluded)
False
>>> validate_format('')
False
>>> validate_format(None)  # type: ignore
False
```

**Validation Rules**:
1. Prefix must be exactly `rettx-` (lowercase)
2. Body must be exactly 3 groups of 4 characters
3. Groups must be separated by dashes
4. All body characters must be uppercase from safe alphabet
5. Total length must be exactly 20 characters

**Notes**:
- Validation is **strict** — lowercase body characters return `False`
- Use `normalize()` first if you need case-insensitive matching
- Does NOT verify uniqueness or patient association (out of scope)

---

### `normalize(id: str) -> str`

Normalize a string into canonical rettX ID form.

**Signature**:
```python
def normalize(id: str) -> str: ...
```

**Parameters**:
- `id: str` — The string to normalize

**Returns**: `str` — The canonical rettX ID

**Raises**:
- `ValueError` — If the input cannot be normalized to a valid rettX ID
- `TypeError` — If input is not a string

**Example**:
```python
>>> from rettxid import normalize
>>> normalize('rettx-8gz4-mk3p-1q9l')  # Lowercase
'rettx-8GZ4-MK3P-1Q9L'
>>> normalize('  rettx-8GZ4-MK3P-1Q9L  ')  # Whitespace
'rettx-8GZ4-MK3P-1Q9L'
>>> normalize('rettx8GZ4MK3P1Q9L')  # Missing dashes
'rettx-8GZ4-MK3P-1Q9L'
>>> normalize('RETTX-8GZ4-MK3P-1Q9L')  # Uppercase prefix
'rettx-8GZ4-MK3P-1Q9L'
>>> normalize('invalid')
Traceback (most recent call last):
    ...
ValueError: Invalid rettX ID: 'invalid'
```

**Normalization Steps**:
1. Strip leading and trailing whitespace
2. Convert prefix to lowercase
3. Convert body to uppercase
4. Remove internal whitespace
5. Insert dashes at correct positions
6. Validate result matches v1 format
7. Return canonical form or raise `ValueError`

**Properties**:
- Idempotent: `normalize(normalize(x)) == normalize(x)`
- Valid IDs normalize to themselves: `normalize(generate_rettx_id()) == generate_rettx_id()`

---

## Type Stubs

For static analysis, the module provides `py.typed` marker (PEP 561).

```python
# rettxid/__init__.pyi (auto-generated)
def generate_rettx_id() -> str: ...
def validate_format(id: str) -> bool: ...
def normalize(id: str) -> str: ...
```

---

## Error Handling Summary

| Function | Input | Behavior |
|----------|-------|----------|
| `generate_rettx_id` | N/A | Returns valid ID (rare `RuntimeError` on entropy failure) |
| `validate_format` | Valid ID | Returns `True` |
| `validate_format` | Invalid string | Returns `False` |
| `validate_format` | Non-string | Returns `False` |
| `normalize` | Normalizable string | Returns canonical ID |
| `normalize` | Invalid string | Raises `ValueError` |
| `normalize` | Non-string | Raises `TypeError` |

---

## Versioning

This contract defines **rettxid v1**. The format `rettx-XXXX-XXXX-XXXX` is stable.

- **v1.x.x**: Bug fixes, documentation, internal changes
- **v2.0.0**: Would require new format (not planned)

All IDs generated by v1 will remain valid in all future versions.
