# Agent Guidelines for mon-nlp

This document contains guidelines for AI coding agents working on the mon-nlp codebase.

## Build, Lint, and Test Commands

### Running Tests
```bash
# Run all tests
uv run pytest

# Run a single test file
uv run pytest tests/test_g2p.py

# Run a specific test function
uv run pytest tests/test_number.py::test_single_digits

# Run tests with verbose output
uv run pytest -v

# Run tests matching a pattern
uv run pytest -k "test_convert"
```

### Linting and Formatting
```bash
# Run linter
uv run ruff check .

# Auto-fix linting issues
uv run ruff check --fix .

# Format code (if using ruff format)
uv run ruff format .
```

### Building the Package
```bash
# Install in development mode
uv pip install -e .

# Install with optional dependencies
uv pip install -e ".[transliterate]"
uv pip install -e ".[dev]"

# Build wheel
uv build
```

## Code Style Guidelines

### Python Version
- **Target:** Python 3.10+
- Use modern type hints: `list[str]`, `dict[str, str]`, `int | float` (not `List`, `Dict`, `Union`)

### Import Organization
Three-tier structure (strictly enforced by ruff's isort):
```python
# 1. Standard library (alphabetically sorted)
import json
import re
from pathlib import Path
from typing import Literal

# 2. Third-party packages
import pytest

# 3. Local imports
from mon_nlp.module import function
```

### Naming Conventions
```python
# Constants: SCREAMING_SNAKE_CASE
VOWELS_NORMAL = "аэиоуөүый"
NUMBER_NAMES = {...}

# Module singletons: _lower_snake_case
_default_g2p: G2P | None = None
_EMOJI_DATA: dict[str, str] | None = None

# Classes: PascalCase
class G2P:
class AbbreviationExpander:

# Functions/methods: snake_case
def num2words(number: int) -> str:
def to_uppercase(text: str) -> str:

# Private functions/methods: _leading_underscore
def _load_data() -> dict:
def _syllabify_word(self, word: str) -> list[str]:
```

### Type Hints
**Always include type hints** for function parameters and return values:
```python
# Good
def num2words(
    number: int | float,
    by_n_digits: int = 0,
    use_dot: bool = False,
) -> str:

# Module-level variables
_default_g2p: G2P | None = None
_EMOJI_DATA: dict[str, str] | None = None
```

### Docstrings
Use **Google-style docstrings**:
```python
def num2words(number: int | float, by_n_digits: int = 0) -> str:
    """Convert number to Mongolian words.

    Args:
        number: Number to convert (int or float)
        by_n_digits: If > 0, converts digit-by-digit in groups

    Returns:
        Mongolian word representation of the number

    Examples:
        >>> num2words(123)
        'нэгэн зуун хорин гурав'
        >>> num2words(3.14, use_dot=True)
        'гурав цэг арван дөрөв'
    """
```

**Docstring rules:**
- Module-level: One-line description
- Classes: Brief one-line description
- Public functions: Full Google-style with Args, Returns, Examples
- Private methods: Usually omitted (unless complex)

### Error Handling
Provide **helpful, actionable error messages**:
```python
# Good - includes context and solution
raise ImportError(
    "phonemizer package is required for English transliteration. "
    "Install with: pip install mon-nlp[transliterate]"
)

# Good - specific error with context
if char not in ROMAN_NUMERALS:
    raise ValueError(f"Invalid Roman numeral character: {char}")

# CLI error handling
try:
    result = function(input)
except ValueError as e:
    print(f"Error: {e}", file=sys.stderr)
    sys.exit(1)
```

### Line Length
- **Maximum: 100 characters** (enforced by ruff)
- Break long lines at logical points (after commas, before operators)

### Module Structure Pattern
Follow this organization in all modules:
```python
"""Module docstring - brief description."""

# 1. Imports (grouped: stdlib, third-party, local)
import json
from pathlib import Path

# 2. Constants and type aliases
CONSTANT_NAME = "value"
TypeAlias = Literal["option1", "option2"]

# 3. Module-level private variables
_module_data: dict | None = None

# 4. Private helper functions
def _load_data() -> dict:
    ...

# 5. Classes (if any)
class ClassName:
    ...

# 6. Module singleton and getter
_default_instance: ClassName | None = None

def _get_instance() -> ClassName:
    global _default_instance
    if _default_instance is None:
        _default_instance = ClassName()
    return _default_instance

# 7. Public API functions
def public_function(text: str) -> str:
    """Public function wrapping instance method."""
    return _get_instance().method(text)
```

## Testing Guidelines

### Test File Organization
- **Location:** `tests/test_<module_name>.py`
- **Naming:** `def test_<feature>()` or `def test_<scenario>()`

### Test Style
```python
# Import modules/functions directly
from mon_nlp import g2p
from mon_nlp.number import num2words

def test_single_digits():
    # Multiple related assertions per test
    assert num2words(0) == "тэг"
    assert num2words(1) == "нэг"
    assert num2words(5) == "тав"

def test_with_parameters():
    # Use tuple lists for parameterized tests
    test_cases = [
        (8, "найм"),
        (12, "арван хоёр"),
        (345, "гурав, дөчин тав"),
    ]
    for number_input, expected_output in test_cases:
        assert num2words(number_input, by_n_digits=2) == expected_output
```

### Conditional Tests
Skip tests when optional dependencies are missing:
```python
from importlib.util import find_spec
import pytest

HAS_PHONEMIZER = find_spec("phonemizer") is not None

@pytest.mark.skipif(not HAS_PHONEMIZER, reason="Requires phonemizer")
def test_transliterate():
    ...
```

## Common Patterns

### Singleton Pattern
Use lazy initialization for module-level instances:
```python
_default_converter: EnglishToCyrillic | None = None

def _get_converter() -> EnglishToCyrillic:
    global _default_converter
    if _default_converter is None:
        _default_converter = EnglishToCyrillic()
    return _default_converter
```

### Data Loading Pattern
Lazy load JSON data with caching:
```python
_DATA_DIR = Path(__file__).parent / "data"
_EMOJI_DATA: dict[str, str] | None = None

def _load_data() -> dict[str, str]:
    global _EMOJI_DATA
    if _EMOJI_DATA is None:
        with open(_DATA_DIR / "emojis.json", encoding="utf-8") as f:
            _EMOJI_DATA = json.load(f)
    assert _EMOJI_DATA is not None  # Type checker satisfaction
    return _EMOJI_DATA
```

## Project Structure
```
mon-nlp/
├── src/mon_nlp/           # Source code
│   ├── __init__.py        # Public API exports with __all__
│   ├── number.py          # Number conversion
│   ├── g2p.py             # Grapheme-to-phoneme
│   ├── abbreviation.py    # Abbreviation expansion
│   ├── punctuation.py     # Punctuation handling
│   ├── emoji.py           # Emoji conversion
│   ├── case.py            # Case conversion
│   ├── transliterate.py   # English to Cyrillic
│   ├── cli.py             # CLI entry point
│   └── data/              # JSON configuration files
├── tests/                 # Test files (mirror src structure)
├── pyproject.toml         # Project config
└── README.md
```

## Key Principles
1. **Zero runtime dependencies** for core features
2. **Pure Python** - no C extensions
3. **Mongolian language support** is the primary focus
4. **CLI and library API** - dual interface
5. **Test coverage** for all public functions
6. **Type safety** with strict type hints
