# Sengol v3.6 — Cursor Rules (Phases 1–2)

## Identity
Sengol — Apache 2.0 Python 3.12 SDK. AI governance for regulated financial institutions.
Building Phases 1–2: SDK core + OSFI E-23 PDF compliance report.

## Critical Rules — Never Violate

### 1. PolicyId is open str — NEVER Literal
```python
# ✓ CORRECT — any org can define their own
PolicyId = str   # "OSFI_E23", "MY_BANK_POLICY_V3", "MAS_TRM" all valid

# ✗ WRONG — breaks for Singapore, Saudi, Indian banks
PolicyId = Literal["OSFI_E23", "FINTRAC_6F"]
```

### 2. EvalScore.passed is binary bool — NEVER float
```python
# ✓ CORRECT
EvalScore(passed=True, failure_mode=None)
EvalScore(passed=False, failure_mode="REQUIRED_DISCLOSURE_MISSING")

# ✗ WRONG
EvalScore(score=0.87)  # NEVER — pre-commit blocks this
```

### 3. Deterministic before LLM — asyncio.gather() for LLM
```python
# EvalSuite runs in this order:
# 1. All DeterministicEvaluator — sync, <1ms, no API
# 2. Short-circuit if CRITICAL failure (skip LLM)
# 3. All LLMEvaluator — asyncio.gather() — O(1) not O(n)
```

### 4. ControlBook drives reports — never bypass it
```python
# ✓ CORRECT — works for any regulation
ComplianceReportGenerator(controlbook=controlbook).generate(agent_id, period)

# ✗ WRONG — hardcoded, breaks for non-OSFI customers
OSFIReportGenerator().generate(agent_id, period)
```

### 5. Audit records: sign before write, append-only
```python
# ✓ CORRECT
signed = record.sign(signing_key)
await audit_store.write(signed)

# ✗ WRONG — audit_store has no update() method
await audit_store.update(record_id, new_data)
```

## Code Patterns to Suggest

### Deterministic evaluator skeleton
```python
from sengol.core.evaluator import DeterministicEvaluator
from sengol.core.types import LLMEvalInput, EvalScore

class RequiredDisclosureEvaluator(DeterministicEvaluator):
    """Checks OSFI E-23 required disclosure phrases. Observed at: Canadian retail bank."""
    name = "RequiredDisclosureEvaluator"
    failure_mode = "REQUIRED_DISCLOSURE_MISSING"
    policies: list[str] = ["OSFI_E23"]
    version: str = "1.0.0"

    def evaluate(self, input: LLMEvalInput) -> EvalScore:
        passed = "not a guarantee" in input.response.lower()
        return EvalScore(
            evaluator=self.name,
            passed=passed,
            failure_mode=None if passed else self.failure_mode,
            reason="Disclosure present" if passed else "Missing required disclosure",
        )
```

### LLM evaluator stub (Phase 1)
```python
class FaithfulnessEvaluator(LLMEvaluator):
    name = "FaithfulnessEvaluator"
    failure_mode = "FAITHFULNESS_FAILURE"
    policies: list[str] = ["OSFI_E23"]

    async def evaluate_async(self, input: LLMEvalInput) -> EvalScore:
        # Phase 1 stub — Phase 3 wires real LLM judge
        return EvalScore(evaluator=self.name, passed=True, failure_mode=None)
```

### AuditRecord (sign before write)
```python
signed = record.sign(os.getenv("SENGOL_SIGNING_KEY"))
await audit_store.write(signed)
assert await audit_store.verify(signed.record_id)
```

### ControlBook-driven report
```python
report = ComplianceReportGenerator(
    audit_store=audit_store,
    controlbook=controlbook,
).generate(
    agent_id="rag-advisor-001",
    period_from=datetime(2026, 1, 1),
    period_to=datetime(2026, 5, 1),
)
# report.pdf_path — hand to CRO
# report.json_path — machine-readable evidence
```

## Never Suggest
- `PolicyId = Literal[...]` — must be `str`
- `EvalScore(score=0.87)` — float primary output
- `print()` — use `structlog`
- `logging.basicConfig()` — use `structlog`
- Sequential LLM evaluator calls — use `asyncio.gather()`
- `UPDATE` or `DELETE` on `audit_records`
- Hardcoded OSFI-only report generator
- `import openai` — respect `ControlBook.judge_by_tier`

## File Locations
- New evaluator → `sengol/evaluators/{deterministic|llm}/{name}.py`
- New type → `sengol/core/types.py` (check spec §5 first)
- New API route → `sengol/api/routes/{name}.py`
- New CLI command → `sengol/cli/main.py` (Typer)
- Policy catalog → `sengol/policies/catalog.yaml`

## Logging
```python
import structlog
log = structlog.get_logger()
log.info("eval.complete", evaluator="MyEval", passed=True, latency_ms=12)
```
