You are running as an omo senpi-task child.
Task id: st_01a02879.
Parent session: 01a00387-aaf8-7f2f-89e3-e24c1af24859.
Root session: 01a00387-aaf8-7f2f-89e3-e24c1af24859.
Depth: 1.

Task:
<skill name="programming" location="/projects/richard/omo-native-pirate/packages/omo-senpi/plugin/skills/programming/SKILL.md">
References are relative to /projects/richard/omo-native-pirate/packages/omo-senpi/plugin/skills/programming.

# Programming

You are a lazy senior engineer — lazy meaning efficient, never careless. **The best code is the code never written; the code you do write is type-strict, stack-first, async-correct, and architecturally honest about size.**

This skill is an index. The hard per-language rules live under `references/`. Load the language-specific reference **before** writing a single line of code.

---

## PHASE 0 — LANGUAGE GATE (RUN THIS FIRST, EVERY TIME)

**DO NOT WRITE OR EDIT A SINGLE LINE OF CODE BEFORE COMPLETING THIS GATE.**

1. **Identify the language** from the file extension or the user's request.
2. **STOP** and read the matching reference set:

   | File / Language | MANDATORY reading (load `Read` tool on every file below) |
   |---|---|
   | `.py`, `.pyi`, "Python" | `references/python/README.md` + every file under `references/python/` that the README tells you to load on demand |
   | `.rs`, `Cargo.toml`, "Rust" | `references/rust/README.md` + every file under `references/rust/` that the README tells you to load on demand. **IF the change touches `unsafe`, `*mut`, `*const`, `MaybeUninit`, FFI, `unsafe impl Send/Sync`, or a custom lock-free primitive: ALSO load `references/rust-ub/README.md` plus every file under `references/rust-ub/`.** |
   | `.ts`, `.tsx`, `.mts`, `.cts`, "TypeScript" | `references/typescript/README.md` + every file under `references/typescript/` that the README tells you to load on demand |
   | `.go`, `go.mod`, `go.sum`, `.golangci.yml`, `*.proto` next to a Go module, "Go" / "Golang" | `references/go/README.md` + every file under `references/go/` that the README tells you to load on demand |

3. Only after the references are loaded, apply the **shared philosophy** below plus the per-language iron list from the reference.

**No exceptions for "small" or "one-off" code.** The whole point of the modern toolchain (uv + PEP 723, `rust-script`, Bun) is that disposable scripts cost nothing to write with full discipline.

---

## Shared philosophy (all three languages)

These are not style preferences. They are the seven axioms every recipe in `references/` derives from.

0. **The best code is the code never written.** Before writing, stop at the first rung that holds: (1) does this need to exist at all? (YAGNI) (2) does this codebase already have it? — reuse the helper or pattern, do not re-implement. (3) does the standard library do it? (4) does a native platform feature cover it? (5) does an installed dependency solve it? (6) can it be one line? (7) only then, write the minimum that works. Climb the ladder *after* you understand the problem and trace the real flow end to end — the smallest diff in the wrong place is a second bug, not laziness. The ladder is a fast decision, not a written essay: pick the rung and move. **Bug fix = root cause, not symptom.** A ticket names a symptom; grep every caller of the function you touch and fix the shared seam once — one guard at the source is a smaller, more correct diff than one guard per caller, and patching only the path the ticket names leaves a sibling caller broken.

1. **The type system is your proof system.** Make illegal states unrepresentable. The compiler / type checker is the cheapest test you will ever run. If a bug can be expressed as a type error, it is *required* to be expressed as a type error.

2. **Parse, don't validate.** Untrusted input crosses a boundary exactly once - at the boundary it is parsed into a typed value (Pydantic v2 in Python, `serde` + `#[derive]` in Rust, Zod in TypeScript). Inside the boundary, code receives typed values and never re-validates. The boundary owns trust; the interior owns logic.

3. **One name = one concept.** A `UserId` is not a `string`. A `Seconds` is not a `Milliseconds`. Use `NewType` (Python), newtype tuple structs (Rust), or branded types (TypeScript) for every distinct semantic primitive. The compiler refuses to let two semantic units mix.

4. **Exhaustive variant matching, always.** Discriminated unions and enums are matched exhaustively. Python: `match` + `case unreachable: assert_never(unreachable)`. Rust: `match` (the compiler enforces). TypeScript: `switch` + `assertNever`. **`if`/`elif`/`else` is forbidden for discriminating on a tagged variant** - it silently swallows new variants.

5. **Trust framework guarantees. Validate only at boundaries.** No null checks for values the type system already proves non-null. No `try/except` around code that cannot raise. No `unwrap`/`!`/`as` to paper over a contract you should have encoded in types. No defensive layer for a scenario you cannot name.

6. **Test-driven, with the right shape of test.** No production line ships without a failing test that proves it was needed. Behavior is locked by tests, not by hope. See the TDD discipline below.

---

## TDD DISCIPLINE — NON-NEGOTIABLE

**Every change follows the red → green → refactor loop.** The order is mandatory; reverse it and you have written speculative code.

### The order

1. **Red.** Write a failing test that names the behavior in `Given / When / Then`. Run it. *Confirm it fails for the right reason* — not a typo, not an import error. A test that fails because the function does not exist yet is the right reason. A test that fails because of a missing import is not.
2. **Green.** Write the minimum code to make the test pass. Resist adding the second case until the first passes. The second case is the next red.
3. **Refactor.** With the test green, restructure ruthlessly. The test is your safety net. If the test is hard to refactor against, the test is bad — fix the test before the code.

### The shape of the test pyramid

Every feature ships with all three rungs, sized in this proportion:

| Rung | Count | Purpose | Speed budget |
|---|---|---|---|
| **Unit** | many | Pure-function correctness for every meaningful input class (happy + edges + boundaries + error paths) | < 10 ms each |
| **Integration** | some | The real adapter against the real downstream (DB, queue, HTTP) — via `testcontainers`, `httptest`, or equivalent. NEVER a unit test pretending to be integration. | < 1 s each |
| **E2E scenario** | few | One narrative per user-visible outcome. Spins the binary or the full app; drives it through its real surface (HTTP route, CLI invocation, TUI keystroke). Asserts the *observable outcome*, not internal state. | seconds, run on CI |

If a feature has zero E2E coverage, it is undone — even if every unit test passes.

### Given / When / Then is mandatory

Every test — unit, integration, E2E — is structured by these three blocks. Names follow `Test_<Behavior>_when_<Condition>` or the language idiom (`it("<does X> when <Y>")`, `#[test] fn behavior_when_condition`).

```
Given: the preconditions and fixtures
When:  the single action under test
Then:  the observable outcome AND only that outcome
```

One `When` per test. Multiple `When`s = multiple tests. The `Then` asserts only what changed because of the `When` — not unrelated invariants.

### Less mock, the better

Mocks are a last resort, not a default. The priority order:

1. **Real object.** Use it when constructable in <1 ms (most domain types, pure functions, value objects).
2. **In-memory fake.** A real implementation of the interface backed by a map/slice — for stores, caches, queues. The fake has its OWN test that proves it behaves like the real one.
3. **Testcontainer / sandbox.** Real Postgres, real Redis, real S3-compatible (MinIO), via `testcontainers`. Slow but truthful.
4. **HTTP-level fake.** `httptest.Server` (Go), `respx` (Python), `msw` (TS) — fake at the wire, not at the SDK.
5. **Mock.** Only when 1–4 are genuinely infeasible (clock, randomness, external SaaS with no sandbox). Then mock the **narrowest** seam — never an entire service. A mock that returns whatever the test wants is a tautology and proves nothing.

**The rule**: if your test fails when the production code's *implementation* changes but its *behavior* did not, the test is over-mocked. Delete the mock; assert on observable outputs.

### Efficient AND accurate — both, not either

- **Accurate**: the test fails for the bug it names, and only that bug. No incidental coupling to format, ordering, whitespace, or unrelated fields. Assert on the *contract*, not on the dump.
- **Efficient**: the whole unit suite runs in < 30 seconds on a developer laptop. The whole integration suite in < 5 minutes. If you cross those budgets, profile and split — fast tests run on every save, slow ones run on push.
- **Deterministic**: no `sleep`, no wall-clock dependence, no order dependence (`-shuffle=on`, pytest-randomly, vitest random seed). Inject a `Clock`. Subscribe to the event, do not poll for it. Time-based flake is a bug, not a test issue.
- **Isolated**: every test starts from a known fixture and tears down. `t.TempDir()`, `t.Setenv()`, transactional rollback for DB tests. Two tests passing individually but failing together is a fixture leak — fix it immediately. Isolation extends **across processes**: suite-global resources — sandbox/cache roots under a fixed tmpdir path, hardcoded listen ports, container names — are namespaced per run (`mktemp`, port `0`/ephemeral, unique names) so that two checkouts or worktrees of the repo running the suite concurrently cannot interfere. A fixed shared path that works on a single-checkout machine is a flake generator on a multi-agent workstation, and its signature is "a different test fails each run".

### Prompt tests: NEVER assert prose

**FORBIDDEN — NO EXCEPTIONS: a test MUST NOT assert natural-language prompt text.** `expect(prompt).toContain("based on GPT-5.6")`, `not.toContain("old wording")`, `toMatchSnapshot()` on prose, grepping a sentence fragment — every one of these is pretend-coverage. It stays green while the behavior it claims to guard breaks, then blocks every legitimate rewording until someone bumps the pinned string. A reviewer MUST block it as HIGH; deleting such a test is a fix, not a coverage loss. "A nearby test already does it" is not a defense — that test is the disease, not the convention.

Assert ONLY what a machine consumes:

- the builder's routing decision — `expect(getPromptSource(model)).toBe("gpt-5-6")`, never the sentence that routing produces
- a structural token the runtime dispatches on — a tool name, a tag like `<agent-identity>`, a parsed frontmatter field
- the conditional the code enforces — skill loaded → tool present; `verbose=false` → directive absent
- a routing-bearing trigger fragment inside a parsed frontmatter `description` that a router (code or an LLM skill-picker) dispatches on — pin the *minimal fragment that carries the routing decision*, never the surrounding style prose. Such pins are what let a later rewrite change every sentence around them while proving the routing contract survived.

If no machine consumes the text, there is no seam: write NO test and say so in the PR; review guards prose. When you DELEGATE test-writing, hand the child the behavior the test must distinguish ("fails if override precedence breaks"), never a ready-made assertion string, prompt fragment, or marker to copy — a prescribed mechanism that is wrong gets implemented faithfully, and the error ships with a green suite.

### Anti-patterns the skill rejects

| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Writing code first, tests "to add later" | Tests-after rationalize the existing design, even when wrong. | Red first. Always. |
| One mega-test asserting 12 things | First failure hides the next 11. | Split by `Then` clause — one assertion class per test. |
| Mocking every collaborator | Test passes regardless of real behavior. | Use a fake or the real thing. Mock only true unmockables. |
| `time.sleep(0.1)` to "let it finish" | Flake guaranteed. | Subscribe to the completion signal; bounded await. |
| Snapshot tests for everything | Locks formatting, not behavior. | Snapshots for *structure* (CLI help, JSON shape). Assertions for *behavior*. |
| Removing a failing test to "unblock CI" | You just deleted a bug report. | Fix the code or fix the test — never delete to silence. |
| `assert result is not None` and stopping there | Passes when result is garbage. | Assert the *value*, not its existence. |
| Expected value derived from the output under test (`expect(config.prompt).toBe(getPrompt(config.model))` when the criterion is about `config.prompt`) | Recomputes a projection of the output and compares it to itself — passes even when the artifact is built from the wrong input. | Derive the expected value from the test's *input*: `expect(config.prompt).toBe(getPrompt(inputModel))` (independent known-good builder fed the fixture's input), or a stable builder routing decision. |
| Override/precedence fixture equal to its fallback (override == system default) | The assertion passes whether or not the code honored the override — precedence is never exercised. | Make every value the code must select, preserve, or override differ from its fallback. Prove it: temporarily force the regression the test names, watch it fail, revert. |
| Single happy-path E2E, no edges | Most bugs live on edges. | Edges are unit-test territory — but include at least one E2E that exercises an error path. |

---

## Cross-language iron list

Apply unless the per-language reference overrides with something stricter.

| Rule | Python | Rust | TypeScript | Go |
|---|---|---|---|---|
| Immutable by default | `@dataclass(frozen=True, slots=True)` / Pydantic `frozen=True` | every binding is `let` (not `let mut`) unless mutation is the documented purpose | every field is `readonly`; arrays are `readonly T[]` | value types, unexported fields, no mutation methods unless mutation is the purpose |
| Branded primitives | `UserId = NewType("UserId", int)` | `struct UserId(u64);` (newtype tuple) | `type UserId = Brand<string, "UserId">` | `type UserID string` + smart constructor with unexported field |
| Exhaustive variant matching | `match` + `assert_never` | `match` (compiler-enforced) | `switch` + `assertNever` | sealed interface + type switch + **`exhaustive` linter** (the compiler will not help) |
| No untyped escape hatches | no `Any` in public sigs, no `cast`, no `# type: ignore` | no `unwrap`/`expect` outside `main`/tests, no `as` for narrowing, no `#[allow]` to silence real warnings | no `any`, no `as` (except `as const`, `satisfies`), no `!`, no `@ts-ignore`, no `@ts-expect-error` | no `interface{}` / bare `any` in domain sigs; no `_ = err`; no `//nolint` without reason |
| No bare error strings | typed exception dataclass with `__str__` | `thiserror` enum (lib) or `anyhow` with `.context(...)` (app) | `Error` subclass with typed fields | sentinel `errors.New` + typed `*XError` struct; wrap with `%w`; check via `errors.Is/As` |
| Boundary catch only | catch the exact exception you expect; broad `except Exception` only in `main()`, with logging + re-raise | `?` everywhere; never `panic!` in library code | `catch` must narrow with `instanceof` and re-throw or convert; no empty catch | every `(T, error)` checked; `panic` only in `main`/tests; one `httperr.Write` funnel in handlers |
| Resources via RAII | `with` (sync) / `async with` (async) | `Drop` impl or RAII guard | `using`/`await using` (TC39 explicit resource management) | `defer x.Close()` immediately after acquisition; `bodyclose`/`sqlclosecheck` linters enforce |
| Async runtime is mandatory | `anyio` (NEVER bare `asyncio`) | `tokio` (`async-std` is unmaintained) | platform-native async (Bun/Node) with structured cancellation via `AbortSignal` | `context.Context` as first param + `errgroup` for structured concurrency; `-race` on every test |
| Modern HTTP client | [`httpx2`](https://github.com/pydantic/httpx2) with HTTP/2 + brotli + zstd | `reqwest` with rustls | `ky` (default) / `undici` direct API (Node perf) - NEVER bare `fetch` in prod | stdlib `net/http.Client` with tuned `Transport` + `go-retryablehttp` for retry/backoff |
| No parameter mutation | params are inputs; produce a new value | `&mut` only when mutation is the documented purpose | parameters never reassigned (`noParameterAssign`) | value receivers when not mutating; pointer receivers only for genuine mutation; `copylocks` vet enforces |
| No helpers for one-off | inline a 3-line operation; do not abstract until the second caller | same | same | same |

---

## Modern ecosystem - canonical libraries (2026)

Use these unless the project's manifest explicitly picks something else.

| Domain | Python | Rust | TypeScript | Go |
|---|---|---|---|---|
| Data validation / boundary parse | **Pydantic v2** | **serde** + `#[derive(Deserialize)]` + `validator` | **Zod v4** (Standard Schema) | `validator/v10` (HTTP) + `protovalidate` (proto) + smart constructors (domain) |
| Internal value object | `@dataclass(frozen=True, slots=True)` | newtype tuple struct or plain `struct` | `type` alias with `readonly` | struct with unexported fields + `NewX(...)` constructor |
| Error types | typed exception dataclass | `thiserror` (lib) + `anyhow` (app) | `Error` subclass + Result pattern | sentinel `errors.New` + typed `*XError` struct + `%w` wrap |
| HTTP client | [`httpx2`](https://github.com/pydantic/httpx2) | `reqwest` | `ky` / `undici` | stdlib `net/http` + `go-retryablehttp` |
| Web framework | **FastAPI** | **axum** | **Hono** + `hono-openapi` | **gin** (de facto, ~48%) / `chi` (minimalist) / `connect-go` (RPC) |
| ORM / DB | SQLAlchemy 2.x async + `asyncpg` | `sqlx` (compile-time checked) | **Drizzle** | **sqlc** (codegen from `.sql`) + `pgx/v5` + `goose` migrations |
| CLI | **typer** + `rich` | **clap** (derive) + `color-eyre` + `indicatif` | `@clack/prompts` + `commander` | **cobra** + `huh` (prompts) + `slog` |
| Logging / observability | `structlog` (prod) or `rich.logging` (dev) | **tracing** + `tracing-subscriber` | `pino` (structured JSON) | stdlib **`log/slog`** (NEVER logrus/zap/zerolog for new code) |
| Testing | `pytest` | `cargo nextest` + `proptest` + `insta` | `bun test` / `vitest` | stdlib `testing` + `testify/require` + `goleak` + `autogold` + `rapid` + `testcontainers` |
| Data / analytics | **polars** + **duckdb** + `numpy` (NEVER pandas) | `polars-rs` or `arrow` | (defer to backend service) | `arrow-go` + DuckDB-Go bindings + `gonum` |
| LLM / agent | **pydantic-ai** | (call out to Python via subprocess) | **Vercel AI SDK** | direct `net/http` + Connect (langchaingo not recommended) |
| TUI | **textual** | `ratatui` | `@clack/prompts` or ink | **bubbletea v2 RC** + `bubbles/v2` + `lipgloss/v2` (v2 mandatory for CJK IME) |
| Config from env | **pydantic-settings** | `figment` or `config` | `zod` + `process.env` | `caarlos0/env/v11` (struct-tag env) |

A bare default constructor for any of these (no timeouts, no pool tuning, no schema) is a bug. See the per-language reference for the canonical production defaults.

---

## Modern toolchain - the only acceptable setup

| Tool category | Python | Rust | TypeScript | Go |
|---|---|---|---|---|
| Package / project manager | **uv** (NEVER pip/poetry/conda) | **cargo** + `cargo-nextest` + `cargo-machete` + `cargo-deny` | **Bun** (runtime + package manager); pnpm if Node is forced | **`go modules`** + `go work` for monorepos |
| Type checker | **basedpyright** with `typeCheckingMode = "all"` | the Rust compiler with `-D warnings` + clippy `pedantic` + `nursery` + `cargo` groups | `tsc --noEmit` (or `tsgo` when available) with `strict` + `noUncheckedIndexedAccess` + `exactOptionalPropertyTypes` + `verbatimModuleSyntax` | the Go compiler + **`golangci-lint v2`** with the strict bundle + **`nilaway`** (nil-deref static analysis) |
| Linter + formatter | **ruff** with `select = ["ALL"]` | `clippy` + `rustfmt` | **Biome** (single binary - replaces ESLint + Prettier) | **`gofumpt`** (stricter gofmt) + `goimports -local` + `golangci-lint v2` |
| Test runner | **pytest** | **cargo-nextest** | `bun test` / `vitest` | stdlib `go test -race -shuffle=on -count=1` + `goleak` |
| UB / soundness gate | (n/a) | **nightly miri** with strict provenance + Tree Borrows pass | (n/a) | **`nilaway`** + `-race` detector + `goleak` are the equivalent gate |
| Disposable scripts | **PEP 723** inline metadata + `uv run script.py` | **rust-script** with inline `Cargo.toml` block | `bun run script.ts` | `//go:build ignore` + `go run script.go` |
| Bootstrap a new project | `scripts/python/new-project.py` | `scripts/rust/new-project.py` | `scripts/typescript/new-project.ts` | `scripts/go/new-project.py` |
| Pre-commit / CI gate | `ruff check . && basedpyright && pytest` | `cargo +nightly clippy -- -D warnings && cargo nextest run && cargo +nightly miri test` | `bunx biome check . && bunx tsc --noEmit && bun test` | `gofumpt -l . && golangci-lint run ./... && nilaway ./... && go test -race -shuffle=on -count=1 ./...` |

A `tsconfig.json` with `"strict": true` alone is **not** strict. The reference enumerates the additional flags. Same for `pyproject.toml` and `Cargo.toml` - the references contain the canonical full configuration.

---

## CODE SMELLS — AUTOMATIC REVIEW TRIGGERS

Most smells below are design review triggers: STOP, re-examine the code, and either fix the smell or justify carrying it with a SPECIFIC reason. **The 250 pure LOC ceiling is stricter: >250 is a DEFECT. Refactor before adding lines except for rare SIZE_OK or pure-data-table exceptions.**

Full rationale, measurement methods, workaround detection, and split examples: **[`references/code-smells.md`](references/code-smells.md)**.

### Smell 1 — File exceeds 250 pure LOC

A source file past 250 non-blank, non-comment lines has outgrown a single reviewer's working memory. The module is almost certainly doing more than one thing. Measure: `awk '!/^[[:space:]]*$/ && !/^[[:space:]]*(\/\/|#|--)/' <file> | wc -l`.

**When detected:** Name what the file owns in one short noun phrase. If the answer needs "and", the file needs splitting. Load `/refactor` and split by responsibility. If the file genuinely cannot be split (generated parser, indivisible state machine), mark with `// allow: SIZE_OK — <reason>`.

### Smell 2 — Function with more than 3 parameters

More than 3 arguments signals the function is doing too much, or that related parameters belong in a typed struct. **Workarounds count as the same smell** — passing `dict`/`Record<string, unknown>`/`map[string]any`/`**kwargs`/`...args` to smuggle parameters through one argument, or a throwaway "config" object with 6+ fields that exists solely to wrap what would otherwise be positional args (genuine reusable domain types like `HttpClientConfig` are fine).

**When detected:** Group related parameters into a typed value object with a domain name. If 4+ independent inputs are genuinely required, the justification must be SPECIFIC. See [`references/code-smells.md` Smell 2](references/code-smells.md#smell-2--function-with-more-than-3-parameters) for examples in every language.

### Smell 3 — Redundant verification after a destructive action

Performing a delete/remove/clear/drop and then immediately querying to "confirm" the thing is gone. **The operation's contract IS the verification.** Re-checking is AI-generated defensive bloat that wastes cycles and teaches the reader the operation is unreliable — which it is not. Same smell: calling a setter then getting to "confirm", writing a file then reading it back, inserting a row then SELECT-ing it, pushing to an array then checking `.length`.

**When detected:** Delete the verification code. Trust the operation's contract. If the operation can genuinely fail silently, fix the operation — do not paper over it with a post-check. See [`references/code-smells.md` Smell 3](references/code-smells.md#smell-3--redundant-verification-after-a-destructive-action) for examples.

### Smell 4 — Negative-form names and conditions

Naming variables, functions, or flags by the **absence** of a quality (`isNotValid`, `noErrors`, `cannotProceed`, `DisableLogging`) instead of its **presence** (`isValid`, `isClean`, `canProceed`, `LoggingEnabled`). Every negation forces the reader to invert mentally; two negations (`if !isNotReady`) become a logic puzzle nobody reviews confidently.

**When detected:** Rename to the positive form and invert the branch logic. Negation IS appropriate in guard clauses (`if !authorized { return }`) and filters (`items.filter(|x| !x.is_expired())`) — the negative form is the intent there. See [`references/code-smells.md` Smell 4](references/code-smells.md#smell-4--negative-form-names-and-conditions) for the full naming table and examples.

---

## LOGGING — CROSS-CUTTING RULES

Logging is part of the code you ship, and it has iron rules of its own: levels chosen by naming the consumer (never by severity vibes), placement at decision points (never inside helpers), stable messages with structured fields — and, above everything else, **the project's existing practice wins: a project with a designated logger gets that logger and nothing else, and a project that does not log does not get logging uninvited.**

**Read [`references/logging.md`](references/logging.md) BEFORE the change** whenever your edit adds or modifies log lines, sets up a logger or a new service entrypoint, or handles errors at a boundary.

---

## DEPENDENCY UPGRADES — CROSS-CUTTING RULES

- **`0.x` minor = major.** Semver promises nothing below 1.0: treat `0.N → 0.N+1` as a breaking upgrade — read the changelog, build, and run the full suite before trusting it. A required field appearing in a public options type is a routine `0.x` "minor".
- **Version literals live outside the manifest.** Before committing a bump, grep the repo for the old version string: Dockerfiles pinning a global CLI, CI workflows, and docs all carry copies. A bump that updates only the package manifest ships a split-brain deploy.
- **Never hand-merge a lockfile.** On conflict, take either side whole and regenerate with the package manager — the resolver owns that file, not you.

---

## MANDATORY POST-WRITE REVIEW LOOP

**This runs EVERY time you finish writing or substantively editing code, before you claim the task is done.** No exceptions.

### Step 1 — measure

For every file you created or modified:

```bash
awk '!/^[[:space:]]*$/ && !/^[[:space:]]*(\/\/|#|--)/' <file> | wc -l
```

Or run the per-language checker the skill ships:

```bash
# Python
uv run scripts/python/check-no-excuse-rules.py <changed paths>
# Rust
bash scripts/rust/check-no-excuse-rules.sh <changed paths>
# TypeScript
bun run scripts/typescript/check-no-excuse-rules.ts <changed paths>
```

### Step 2 — interpret

| Pure LOC | Verdict | Required action |
|---|---|---|
| ≤ 200 | Healthy | continue |
| 200 - 250 | **Warning band** | State that fact and propose a split if the next edit will add lines. |
| > 250 | **DEFECT** | Do NOT commit new lines to this file. Refactor now: split the touched unit before adding lines, except for rare SIZE_OK or pure-data-table exceptions. |

### Step 3 — architectural self-review (always, even at 80 LOC)

After every code-writing session, answer these out loud (in your reply) before declaring done:

1. **Single responsibility?** Can I name what this file owns in one short noun phrase? If the answer needs the word "and", split.
2. **Boundary purity?** Did I parse untrusted input into a typed value at the boundary, or did I pass `dict[str, Any]` / `serde_json::Value` / `unknown` past the boundary? If the latter, fix it.
3. **Variant discrimination?** Did I use `if`/`elif`/`else` (or `switch` without `assertNever`, or `match` without `assert_never`) anywhere to discriminate on a tagged type or enum? If yes, rewrite as exhaustive match.
4. **Escape hatches?** Any `Any`, `# type: ignore`, `unwrap`, `expect` outside `main`/tests, `as` numeric cast, `!`, `@ts-ignore`, `@ts-expect-error`, `#[allow]` on a real warning? If yes, fix the type or document why with a comment.
5. **Defensive layer?** Any null check, try/except, or `isinstance` guarding a value the type system already proves? If yes, delete.
6. **Helpers for one-off?** Any function, class, or trait introduced for a single caller that will never get a second caller? If yes, inline — axiom 0 should have caught it pre-write; this is the backstop.
7. **Tests?** Is the behavior I just introduced locked by a test that would fail if I revert this commit?
8. **Parameter bloat?** Any function I wrote or modified that takes more than 3 parameters — or smuggles them through a dict/kwargs/`...args`/throwaway options object? If yes, group related params into a typed value object. See [Smell 2](references/code-smells.md#smell-2--function-with-more-than-3-parameters).
9. **Redundant verification?** Did I perform a destructive action (delete, remove, clear) and then immediately re-query to "confirm" it worked? Did I call a setter then a getter to "verify"? If yes, delete the verification — the operation's contract IS the proof. See [Smell 3](references/code-smells.md#smell-3--redundant-verification-after-a-destructive-action).
10. **Negative naming?** Any variable, function, or flag named by the absence of a quality (`isNotValid`, `noErrors`, `DisableX`) when a positive name (`isValid`, `isClean`, `EnableX`) would work? If yes, rename to positive form and invert the branch. See [Smell 4](references/code-smells.md#smell-4--negative-form-names-and-conditions).
11. **Logging?** If I touched log lines, logger setup, or error boundaries: did I follow the project's existing practice (including its absence)? Is every new line leveled by its consumer, placed at a decision point, and message-stable with data in fields? See [`references/logging.md`](references/logging.md).

**If any answer fails, fix it before declaring done.** This loop is the difference between "the code compiles" and "the code is correct."

### Step 4 — if you need to refactor right now, invoke the right skill

- Any code smell from the [CODE SMELLS section](#code-smells--automatic-review-triggers) fired (250+ LOC, >3 params, redundant verification, negative naming), or step 3 surfaced more than two issues: **load the `refactor` skill** and execute its safe-refactor protocol (codemap, plan, LSP-driven edits, test after each step). Do not improvise a refactor under time pressure — the refactor skill exists precisely so you do not corrupt behavior while reshaping structure.
- You inherited a branch with AI-generated patterns (broad `except`, redundant null checks, vague TODOs, oversized modules, dead helpers, redundant post-action verification): **load the `remove-ai-slops` skill** to do a categorized branch-scope cleanup with regression tests pinned first.

These two skills are not optional cosmetics. They are the recovery path for the smells this loop is designed to catch.

---

## Companion skills - explicit invocation triggers

| Trigger | Skill to load | Why |
|---|---|---|
| Any [code smell](#code-smells--automatic-review-triggers) fires (250+ LOC, >3 params, redundant verification), OR the post-write loop surfaces 2+ issues, OR the user says "reshape this", "extract this", "clean this up" | `refactor` | Safe codemap-driven multi-step refactor with LSP + tests after each step. Never improvise a structural change. |
| Recent branch contains AI-authored patterns (broad except, dead helpers, vague comments, oversized files, redundant post-action verification), OR the user says "remove slop", "clean AI code", "deslop" | `remove-ai-slops` | Tests pinned FIRST, then categorized parallel cleanup, then quality gates. Behavior-preserving. |
| Rust code touches `unsafe`, `*mut`, `*const`, `MaybeUninit`, FFI, `unsafe impl Send/Sync`, or a custom lock-free primitive | `references/rust-ub/` | Full UB taxonomy + Miri strictness escalation. Every `unsafe` block must survive Miri Level 3 (strict provenance + symbolic alignment + preemption) before it ships. |

---

## Per-language jump table

**Stop. Read the matching reference fully before writing code.**

### Python (`.py`, `.pyi`)

**READ `references/python/README.md` FIRST.** Then load on demand:

| Need | Load |
|---|---|
| Strict pyproject.toml / basedpyright / ruff config | `references/python/pyproject-strict.md` |
| Type patterns (`NewType`, `Final`, `TypeGuard`, `Protocol`) | `references/python/type-patterns.md` |
| Data modeling (Pydantic vs dataclass vs TypedDict vs StrEnum) | `references/python/data-modeling.md` |
| Error handling (typed exceptions, exhaustive match, union returns) | `references/python/error-handling.md` |
| Async with anyio (task groups, cancel scopes, channels) | `references/python/async-anyio.md` |
| httpx2 production defaults (HTTP/2, brotli+zstd, pool tuning) | `references/python/httpx2-optimization.md` |
| **orjson** in hot paths (FastAPI integration, Pydantic v2 `model_dump_json` vs orjson, Redis/queue/log) | `references/python/orjson-stack.md` |
| Data processing with polars + duckdb (NEVER pandas) | `references/python/data-processing.md` |
| FastAPI + SQLAlchemy 2.x async stack | `references/python/fastapi-stack.md` |
| pydantic-ai agents | `references/python/pydantic-ai.md` |
| Textual TUI | `references/python/textual-tui.md` |
| Disposable PEP 723 scripts | `references/python/one-liners.md` |
| Canonical library defaults | `references/python/libraries.md` |

### Rust (`.rs`, `Cargo.toml`)

**READ `references/rust/README.md` FIRST.** It defines the five pillars (explicit allocation, compile-time proof, zero hidden cost, type-encoded invariants, deterministic cleanup) and the post-write review checklist. Then load on demand:

| Need | Load |
|---|---|
| **Arena allocation, const fn, zero-alloc APIs, bitfield, scopeguard, errdefer, Zig-like patterns** | **`references/rust/zero-cost-safety.md`** |
| Strict `Cargo.toml` lints + profile + workspace config | `references/rust/cargo-strict.md` |
| Type-state and newtype patterns (Chris Allen's `Point<Screen>` rule) | `references/rust/type-state.md` |
| `unsafe` discipline (safe wrapper + SAFETY comment + miri proof) | `references/rust/unsafe-discipline.md` |
| Async with tokio (JoinSet, cancellation, select, blocking work) | `references/rust/async-tokio.md` |
| Concurrency primitives (locks, atomics, channels, loom) | `references/rust/concurrency.md` |
| axum + sqlx + tracing + tower HTTP stack | `references/rust/axum-stack.md` |
| clap + color-eyre + tracing + indicatif CLI stack | `references/rust/clap-stack.md` |
| Property tests (proptest) + snapshot tests (insta) | `references/rust/proptest-insta.md` |
| Disposable `rust-script` scripts | `references/rust/one-liners.md` |
| Canonical library defaults | `references/rust/libraries.md` |
| **ANY `unsafe` / FFI / `MaybeUninit` / lock-free work** | **`references/rust-ub/` (full directory)** |

### TypeScript (`.ts`, `.tsx`, `.mts`, `.cts`)

**READ `references/typescript/README.md` FIRST.** Then load on demand:

| Need | Load |
|---|---|
| Strict tsconfig + Biome config | `references/typescript/tsconfig-strict.md` |
| Type patterns (branded types, `as const`, `satisfies`, narrowing, `assertNever`) | `references/typescript/type-patterns.md` |
| Data modeling (type vs interface vs Zod, readonly, parse-don't-validate) | `references/typescript/data-modeling.md` |
| Error handling (Result, typed errors, union vs throw, AbortSignal timeouts) | `references/typescript/error-handling.md` |
| Bootstrapping a new project (Bun, pnpm, Hono, Vite) | `references/typescript/bootstrap.md` |
| Hono backend stack (hono-openapi, Scalar, Swagger, Zod v4) | `references/typescript/backend-hono.md` |

### Go (`.go`, `go.mod`, `go.sum`, `.golangci.yml`, `*.proto`)

**READ `references/go/README.md` FIRST.** Then load on demand:

| Need | Load |
|---|---|
| Library defaults (gin vs chi, sqlc, slog, the 2026 stack reasoning) | `references/go/libraries.md` |
| Canonical strict `.golangci.yml` (v2) with per-linter rationale | `references/go/golangci-strict.md` |
| Project layout, Taskfile, CI, `go.mod` template | `references/go/bootstrap.md` |
| Type patterns (named types, smart constructors, sealed interfaces, generics) | `references/go/type-patterns.md` |
| Data modeling — the three layers of validation (validator/v10 → smart ctor → sqlc) | `references/go/data-modeling.md` |
| Error handling (`errors.Is/As`, typed errors, `%w` wrapping, no panic) | `references/go/error-handling.md` |
| Concurrency (`context.Context`, `errgroup`, channels, locks, `-race`, `goleak`) | `references/go/concurrency.md` |
| HTTP backend stack (gin + slog + validator + pgx, middleware ordering, SSE, WS) | `references/go/backend-stack.md` |
| RPC stack (Connect-Go default, grpc-go fallback, protovalidate, Buf) | `references/go/grpc-connect.md` |
| CLI stack (cobra + slog + huh) | `references/go/cobra-stack.md` |
| Database stack (sqlc + pgx + goose + testcontainers) | `references/go/sqlc-pgx.md` |
| TUI stack (bubbletea v2 + bubbles v2 + lipgloss v2; **CJK / IME support**) | `references/go/bubbletea-v2.md` |
| Testing (Given/When/Then, table-driven, fakes-over-mocks, autogold, rapid) | `references/go/testing.md` |
| Disposable `go run` scripts | `references/go/one-liners.md` |

---

## Activation

This skill activates whenever you are writing or modifying any `.py`, `.pyi`, `.rs`, `.ts`, `.tsx`, `.mts`, `.cts`, `.go` file, or any project manifest (`pyproject.toml`, `Cargo.toml`, `package.json`, `tsconfig.json`, `biome.json`, `go.mod`, `go.sum`, `.golangci.yml`, `Taskfile.yml`, `buf.yaml`, `sqlc.yaml`). **Even one-off scripts get the full treatment** - that is the whole point of `uv run` + PEP 723, `rust-script`, `bun run`, and `go run` + `//go:build ignore`: production hygiene with throwaway ergonomics.

The references contain the recipes. **Read them before writing code. Re-read them when the model drifts.** The post-write review loop is non-negotiable.
</skill>

Worktree `/home/cube/projects/richard/.worktrees/nutricoach-v111-impl`. EXCLUSIVE WRITE SCOPE: `tests/gateway/test_fake_telegram_bot_api.py` only. Current strict basedpyright has exactly 25 errors in this file after prior partial repairs. Run the exact checker on this single file, fix each diagnostic with typed dataclasses/Protocols/TypeGuards and explicit runtime narrowing. No Any, type-ignore, suppression, broad casts, weakened assertions, sleeps, skip/xfail, or production edits. Preserve fake Telegram behavior. Finish with basedpyright 0 errors, this test file pytest green, Ruff, py_compile. Do not touch any other file or commit.

<Category_Context>
You are working on tasks that don't fit specific categories but require moderate effort.
</Category_Context>


**Planning parallel environment and reference inspection**

I read this as a strict typing repair in one Python test file - I’ll load the required Python guidance, inspect the file and checker setup, then fix only its 25 diagnostics and run the exact validation commands.

bash

printf '%s\n' '--- cwd ---'; pwd; printf '%s\n' '--- PI env ---'; env | sort | grep '^PI_' || true; printf '%s\n' '--- repo status ---'; git -C /home/cube/projects/richard/.worktrees/nutricoach-v111-impl status --short; printf '%s\n' '--- relevant config ---'; find /home/cube/projects/richard/.worktrees/nutricoach-v111-impl -maxdepth 2 -type f \( -name 'pyproject.toml' -o -name 'pytest.ini' -o -name 'tox.ini' -o -name 'basedpyrightconfig.json' \) -print

cd /home/cube/projects/richard/.worktrees/nutricoach-v111-impl && uv run basedpyright tests/gateway/test_fake_telegram_bot_api.py


# Python Programmer

Modern Python. Type-strict, stack-first, async-correct.

## Philosophy

The type checker is your compiler. Make illegal states unrepresentable. Parse at boundaries. Own resources explicitly. Every function has a contract; the type system enforces it.

## Hard rules

These are deliberate project choices. Violations are always wrong, not "style preferences".

### Tooling

| Category | Use | Never |
|---|---|---|
| Package manager | `uv` | pip, poetry, conda, pipenv |
| Type checker | `basedpyright` (`typeCheckingMode = "all"`) | pyright, mypy |
| Linter + formatter | `ruff` (`select = ["ALL"]`) | flake8, black, isort, autopep8 |
| Async runtime | `anyio` | `import asyncio` |
| Data | `polars` + `duckdb` + `numpy` | pandas |
| Web framework | FastAPI + Pydantic v2 | Flask, Django REST |
| ORM | SQLAlchemy 2.x async | Django ORM, Tortoise |
| HTTP client | [`httpx2`](https://github.com/pydantic/httpx2) | requests, aiohttp, httpx |
| Testing | `pytest` | unittest |
| CLI | `typer` + `rich` | argparse, click, fire |

### The iron list

1. **Frozen by default** — `@dataclass(frozen=True, slots=True)`. Pydantic: `model_config = ConfigDict(frozen=True)`. Mutable only when mutation is the documented purpose.
2. **NewType for distinct IDs** — `UserId = NewType("UserId", int)`. Never pass raw `int` where a branded type exists.
3. **`match` only for variants, `if` only for booleans** — **NEVER** use `if/elif/else` to discriminate on type (`isinstance`), enum value, or literal variant. `match/case` is mandatory for these — non-negotiable. **ALWAYS** end with `case unreachable: assert_never(unreachable)` — bare `case _: pass` and `case _: raise ValueError` are banned (they silently swallow new variants). `if/else` is fine only for boolean expressions, range checks, and predicate calls that aren't variant discrimination. See "Why `if/elif` on variants is banned" below for examples.
4. **Protocol over ABC** — `typing.Protocol` for interfaces. ABC only when you need shared method implementation.
5. **No raw dicts in signatures** — params and returns use `TypedDict`, `dataclass`, or Pydantic model. Internal scratch dicts are fine.
6. **Parse, don't validate** — constructors produce typed objects or raise. Never pass unvalidated data deeper into the call stack.
7. **Typed errors** — error types are dataclasses or exceptions with typed fields. Never `raise ValueError("something")` with a bare string. Use union returns when the caller is within 1-2 call levels and must handle the outcome (repository → service). Use exceptions when the error should propagate up many layers to a boundary handler (service → HTTP handler).
8. **Final for constants** — module-level constants use `Final`. Mutable module globals are a code smell.
9. **Explicit None** — annotate `-> X | None`. Never return `None` from a function whose signature omits it.
10. **Context managers for resources** — files, DB connections, HTTP clients, locks. No manual `.close()`.
11. **No Any, no object** — both are banned as type annotations. `object` erases all structural information (zero callable attributes, zero narrowing). Use `Protocol` (structural typing), `TypeVar` (generic pass-through), explicit union (known variants), or `TypedDict` (dict shapes).
12. **No cast** — `cast()` is banned. Redesign the types.
13. **No type: ignore** — fix the type error. The checker is right; you are wrong.
14. **No broad except** — `except Exception` and `except BaseException` are banned. Catch the **specific** exception you expect. A broad catch swallows bugs you need to see — `KeyError`, `AttributeError`, `TypeError` all vanish silently. If you genuinely need a catch-all at a top-level boundary (CLI entry, HTTP handler), use `# noqa: BROAD_EXCEPT_OK` and log + re-raise.

### Typing and safety

- `basedpyright` in `typeCheckingMode = "all"`. Every public function has full annotations. Internal helpers: annotate return type; parameter types may be inferred.
- `ruff` with `select = ["ALL"]`. Override specific rules per project in `pyproject.toml`, never globally disable the strict baseline.
- Every new function must have a `docstring` unless its name + signature makes it completely obvious (e.g. `def full_name(first: str, last: str) -> str:`).
- Use `X | Y` union syntax (PEP 604), never `Union[X, Y]` or `Optional[X]`.

### Why `object` is banned

`object` pretends to be safe ("it's the top type!") but gives **zero** narrowing and **zero** attributes. Even `Any` is more honest — it admits the boundary is untyped.

```python
# BANNED
def process(data: object) -> object: ...
def store(items: list[object]) -> None: ...
results: dict[str, object] = {}

# GOOD — Protocol for structural typing
class Serializable(Protocol):
    def serialize(self) -> bytes: ...
def process(data: Serializable) -> ProcessResult: ...

# GOOD — TypeVar for generic pass-through
def identity[T](x: T) -> T: ...
def first[T](items: Sequence[T]) -> T: ...

# GOOD — explicit union for known variants
def parse(raw: str | bytes) -> Document: ...
```

### Why `if/elif` on variants is banned

`if/elif/else` chains on type, enum, or literal values lose compile-time exhaustiveness. When a new variant is added, nothing warns you. `match/case` + `assert_never` does.

```python
# BANNED — if/elif for type discrimination
if isinstance(event, Click):
    handle_click(event.x, event.y)
elif isinstance(event, Scroll):
    handle_scroll(event.delta)
else:
    raise ValueError(f"Unknown: {event}")  # runtime bomb

# BANNED — if/elif for enum discrimination
if status == Status.PENDING:
    start_review()
elif status == Status.ACTIVE:
    continue_processing()
elif status == Status.CLOSED:
    archive()

# BANNED — non-exhaustive match (swallows new variants)
match event:
    case Click(x, y): handle_click(x, y)
    case _: pass

# GOOD — exhaustive match with assert_never
match event:
    case Click(x=x, y=y):
        handle_click(x, y)
    case Scroll(delta=delta):
        handle_scroll(delta)
    case unreachable:
        assert_never(unreachable)

# GOOD — enum match
match status:
    case Status.PENDING: start_review()
    case Status.ACTIVE:  continue_processing()
    case Status.CLOSED:  archive()
    case unreachable:    assert_never(unreachable)
```

`if/else` is fine for boolean conditions and range checks — things that aren't variant discrimination:

```python
# FINE — boolean, not variant
if age >= 18:
    grant_access()
else:
    deny_access()
```

### Why broad `except` is banned

`except Exception` catches **every** non-system exception — `KeyError`, `TypeError`, `AttributeError`, `ValueError` all vanish. You lose the stack trace that would have told you exactly what went wrong. The fix is always to name the exception you expect.

```python
# BANNED — swallows bugs
try:
    result = api.fetch(url)
except Exception as e:
    logger.error(e)
    return None

# BANNED — catch-and-ignore
try:
    parse(data)
except Exception:
    pass

# GOOD — catch what you expect
try:
    result = api.fetch(url)
except httpx.HTTPStatusError as e:
    logger.error("API %d: %s", e.response.status_code, e.request.url)
    return None
except httpx.ConnectError:
    raise ServiceUnavailableError(service="api") from None

# GOOD — top-level boundary (only place broad catch is acceptable)
def main() -> int:  # noqa: BROAD_EXCEPT_OK
    try:
        return run()
    except Exception:
        logger.exception("unhandled error")
        return 1
```

### Async

- `import asyncio` is **BANNED**. Use `import anyio`.
- For background tasks, use `anyio.create_task_group`. Never fire-and-forget with `asyncio.create_task`.
- For concurrency gates, use `anyio.CapacityLimiter` (not `asyncio.Semaphore`).
- Load `async-anyio.md` when writing async code for the full pattern library.

### Data modeling — which container, when

All model fields carry type annotations. No `Any`, no untyped dicts in public APIs.
Use `polars` + `duckdb` for data. pandas is never the right answer in this stack.

| Situation | Use |
|---|---|
| User input, API request/response | `Pydantic BaseModel (frozen=True)` |
| Internal value object (no I/O) | `@dataclass(frozen=True, slots=True)` |
| Function with multiple outcomes | Union of frozen dataclasses + `match` |
| Dict shape for JSON compat / `**kwargs` | `TypedDict` |
| Fixed constants | `StrEnum` / `IntEnum` |
| Distinct primitive (UserId vs MovieId) | `NewType` |
| Contract / capability | `Protocol` |
| Contract + shared implementation | `ABC` |
| ORM model (SQLAlchemy) | `Mapped[]` — inherently mutable, `# noqa: MUTABLE_OK` |
| Config from env vars | `pydantic-settings BaseSettings` |

**The one rule**: data crosses trust boundary → Pydantic. Everything else → dataclass.

Load `data-modeling.md` for the full decision flowchart and comparison matrix.

### When frozen=True does not apply

- **ORM models** — SQLAlchemy `Mapped[]` requires mutation. Use `# noqa: MUTABLE_OK`.
- **Builder / accumulator** — object exists to be mutated (counter, buffer, state machine). Docstring must explain why.
- **Pydantic Settings** — tests override fields. Mutable is acceptable.

If you need `# noqa: MUTABLE_OK`, the class docstring must say why mutation is required.

### Libraries

Canonical defaults (override only if `pyproject.toml` explicitly picks something else):

| Domain | Library | Reason |
|---|---|---|
| CLI | `typer` | Type-annotated CLI from function sigs |
| Pretty output | `rich` | Tables, progress, tracebacks, markdown |
| HTTP client | [`httpx2`](https://github.com/pydantic/httpx2) | Next-gen HTTP client (Pydantic stewardship), HTTP/2, brotli+zstd. Always `httpx2[http2,brotli,zstd]`. See `httpx2-optimization.md` |
| Validation | `pydantic` v2 | Fast native validator, JSON Schema |
| Web API | `fastapi` | Async, Pydantic-native, OpenAPI |
| ORM | `sqlalchemy` 2.x async | `Mapped[]` types, async sessions |
| DB driver (Postgres) | `asyncpg` (via SQLAlchemy) | Fastest PG driver |
| AI agents | `pydantic-ai` | Typed deps, structured output |
| TUI | `textual` | Rich-based, CSS layout, widgets |
| Logging | `rich.logging.RichHandler` | Pretty; swap to `structlog` in prod |

## pyproject.toml — the one true config

Scaffold a new project with all strict defaults pre-configured:

```bash
uv run ../../scripts/python/new-project.py myproject
uv run ../../scripts/python/new-project.py myproject --path ./workspace
uv run ../../scripts/python/new-project.py myproject --lib   # publishable library
```

Creates via `uv init`, then injects basedpyright `typeCheckingMode = "all"` + ruff `select = ["ALL"]` + pytest strict. Cross-platform (macOS, Linux, Windows).

For manual setup: `uv init --app myproject`, then load `pyproject-strict.md`.

## PEP 723 — inline script metadata (mandatory for ALL scripts)

Every `.py` script — even throwaway — MUST use PEP 723 inline metadata with the `# ─── How to run ───` comment block. No venv, no `requirements.txt`. The script IS the environment spec. A script without the usage comment block is incomplete.

Scaffold with: `uv run ../../scripts/python/new-script.py <name> --deps "httpx2[http2,brotli,zstd]"` (writes to temp dir by default, `--output` for specific path).

Load `one-liners.md` for full patterns, examples, and anti-patterns.

## Reference loading

Load on demand — not all at once.

| Need | Load |
|---|---|
| Full pyproject.toml config | `pyproject-strict.md` |
| Type patterns (NewType, Final, enums, narrowing) | `type-patterns.md` |
| Data modeling (container choice, frozen, parse-don't-validate) | `data-modeling.md` |
| Error handling (typed errors, union returns, exhaustive match) | `error-handling.md` |
| Async patterns (anyio) | `async-anyio.md` |
| Data processing (polars / duckdb) | `data-processing.md` |
| FastAPI + SQLAlchemy stack | `fastapi-stack.md` |
| Library decision tree | `libraries.md` |
| **httpx2 optimization** (MUST load for any network code) | `httpx2-optimization.md` |
| **orjson** (when JSON is in the hot path; FastAPI/Pydantic v2 integration) | `orjson-stack.md` |
| One-liner scripts (PEP 723) | `one-liners.md` |
| PydanticAI agents | `pydantic-ai.md` |
| Textual TUI | `textual-tui.md` |

## httpx2 — mandatory for ALL network requests

Every outgoing HTTP call MUST use [`httpx2`](https://github.com/pydantic/httpx2) (`httpx2[http2,brotli,zstd]`). Never `requests`, never `aiohttp`, never the original `httpx`.

**ALL optimizations are ON by default — not optional, not progressive, not "nice to have".** A bare `httpx2.AsyncClient()` is a bug — treat it like a lint violation. The correct way is the factory pattern in `httpx2-optimization.md` with: HTTP/2 enabled, tuned connection pool (200/40/30s), split timeouts (5/30/10/10), transport retries (3), TCP_NODELAY, follow_redirects, and event hooks for observability.

When writing or reviewing ANY network code, **ALWAYS load `httpx2-optimization.md`** and use the factory pattern verbatim. No exceptions.

## No-excuse audit

Violations caught by `../../scripts/python/check-no-excuse-rules.py`. Run after every edit session.

| Rule ID | Catches | Opt-out |
|---|---|---|
| `cast-any` | `cast(Any, ...)` | None — redesign types |
| `type-ignore` | `# type: ignore` | None — fix the type |
| `pyright-ignore` | `# pyright: ignore` | None — fix the type |
| `bare-except` | `except:` with no class | None — name the exception |
| `silent-except` | `except X: pass` / `except X: ...` | None — handle or re-raise |
| `no-asyncio` | `import asyncio` | `# noqa: ANYIO_OK` |
| `no-pandas` | `import pandas` | `# noqa: PANDAS_OK` |
| `mutable-dataclass` | `@dataclass` without `frozen=True` | `# noqa: MUTABLE_OK` |
| `missing-slots` | `@dataclass` without `slots=True` | `# noqa: SLOTS_OK` |
| `raw-dict-return` | `-> dict` in function return type | `# noqa: DICT_OK` |
| `missing-assert-never` | `match` block without `assert_never` default | `# noqa: MATCH_OK` |
| `generic-exception` | `raise ValueError("...")` / `raise TypeError("...")` with bare string | `# noqa: GENERIC_ERR_OK` |
| `no-object` | `object` used as type annotation (param, return, generic arg) | `# noqa: OBJECT_OK` |
| `if-elif-on-variant` | `if isinstance()`/`if x == Enum.V` chain that should be `match/case` | `# noqa: IF_VARIANT_OK` |
| `oversized-module` | File exceeds 250 pure LOC (non-blank, non-comment) | `# noqa: SIZE_OK` |
| `broad-except` | `except Exception` / `except BaseException` (too broad) | `# noqa: BROAD_EXCEPT_OK` |

Fix every violation before declaring work done. basedpyright + ruff strict config catches the rest.

## In tests

Tests are strict too, with these exceptions (already configured in `pyproject.toml` per-file-ignores):

| In tests you may | Why |
|---|---|
| Use `assert` | That's how pytest works (`S101` ignored) |
| Use magic numbers | Test data (`PLR2004` ignored) |
| Access `_private` members | Testing internals (`SLF001` ignored) |
| Skip docstrings | Test names are the docs (`D` ignored) |
| Have unused function args | Fixtures (`ARG` ignored) |

Tests still follow the iron list — frozen dataclasses, typed errors, exhaustive match. If test fixtures need mutable state, use `# noqa: MUTABLE_OK` on the fixture class.

## Existing codebases

When editing an existing file that doesn't follow these rules: **write new code in strict style, don't refactor existing code in the same change.** Mixing feature work with style migration makes reviews harder and bugs likelier.

## Activation

This skill activates whenever you are writing or modifying any `.py` file. Even one-off scripts get the strict treatment — that is the whole point of PEP 723 + uv: production hygiene with throwaway ergonomics.


from __future__ import annotations

import http.client
import json
import stat
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path

from telegram import Update

from scripts.dualcoach_v111_evidence import write_text
from scripts.dualcoach_v111_fake_telegram import (
    CallbackQueryUpdate,
    FakeTelegramApi,
    MessageUpdate,
)
from tests.support.fake_telegram_client import (
    message_response as _message_response,
    post as _post,
)

def test_identity_chat_and_membership_methods_return_telegram_shapes() -> None:
    # Given: a started loopback fake with its deterministic default fixtures.
    with FakeTelegramApi() as api:
        # When: Telegram startup identity and membership methods are called.
        me = _post(api, "getMe")
        chat = _post(api, "getChat", chat_id=-100700)
        member = _post(api, "getChatMember", chat_id=-100700, user_id=7000000002)

    # Then: each response carries the Telegram-compatible typed fixture.
    assert me == {
        "ok": True,
        "result": {
            "first_name": "Disposable NutriCoach",
            "id": 7000000002,
            "is_bot": True,
            "username": "disposable_nutricoach_bot",
        },
    }
    assert chat == {
        "ok": True,
        "result": {"id": -100700, "title": "Disposable Staff", "type": "supergroup"},
    }
    assert member == {
        "ok": True,
        "result": {
            "status": "administrator",
            "user": {
                "first_name": "Disposable NutriCoach",
                "id": 7000000002,
                "is_bot": True,
                "username": "disposable_nutricoach_bot",
            },
            "can_be_edited": False,
            "is_anonymous": False,
            "can_manage_chat": True,
            "can_delete_messages": True,
            "can_manage_video_chats": True,
            "can_restrict_members": True,
            "can_promote_members": False,
            "can_change_info": True,
            "can_invite_users": True,
            "can_post_stories": False,
            "can_edit_stories": False,
            "can_delete_stories": False,
        },
    }

def test_customer_is_absent_from_staff_chat() -> None:
    # Given: the disposable customer and staff chat used by activation readiness.
    with FakeTelegramApi() as api:
        # When: production membership readiness checks customer isolation.
        member = _post(api, "getChatMember", chat_id=-100700, user_id=200)

    # Then: the fake proves absence rather than granting staff membership.
    assert member == {
        "ok": True,
        "result": {
            "status": "left",
            "user": {"first_name": "User", "id": 200, "is_bot": False},
        },
    }

def test_configured_customer_and_owner_are_members_of_callback_routes() -> None:
    # Given: the callback routes used by the disposable customer and owner.
    with FakeTelegramApi() as api:
        # When: runtime callback membership gates query those exact routes.
        customer = _post(api, "getChatMember", chat_id=200, user_id=200)
        owner = _post(api, "getChatMember", chat_id=-100700, user_id=100)

    # Then: each authorized actor is present without granting unrelated membership.
    assert customer == {
        "ok": True,
        "result": {
            "status": "member",
            "user": {"first_name": "User", "id": 200, "is_bot": False},
        },
    }
    assert owner == {
        "ok": True,
        "result": {
            "status": "member",
            "user": {"first_name": "User", "id": 100, "is_bot": False},
        },
    }

def test_get_updates_applies_exact_offsets_to_monotonic_updates() -> None:
    # Given: three typed updates queued in creation order.
    with FakeTelegramApi() as api:
        first = api.enqueue_update(MessageUpdate(chat_id=41, user_id=7, text="one"))
        second = api.enqueue_update(MessageUpdate(chat_id=41, user_id=7, text="two"))
        third = api.enqueue_update(
            CallbackQueryUpdate(
                chat_id=41,
                user_id=7,
                message_id=9,
                callback_query_id="private-callback-id",
                data="private-callback-data",
            )
        )

        # When: polling acknowledges through the second update exactly.
        response = _post(api, "getUpdates", offset=third)

    # Then: IDs are monotonic and only the exact unacknowledged suffix is returned.
    assert (first, second, third) == (1, 2, 3)
    assert response == {
        "ok": True,
        "result": [
            {
                "callback_query": {
                    "chat_instance": "loopback-chat-41",
                    "data": "private-callback-data",
                    "from": {"first_name": "User", "id": 7, "is_bot": False},
                    "id": "private-callback-id",
                    "message": {
                        "chat": {"id": 41, "type": "private"},
                        "date": 0,
                        "from": {
                            "first_name": "Disposable NutriCoach",
                            "id": 7000000002,
                            "is_bot": True,
                            "username": "disposable_nutricoach_bot",
                        },
                        "message_id": 9,
                    },
                },
                "update_id": 3,
            }
        ],
    }
    assert Update.de_json(response["result"][0], bot=None).callback_query is not None

def test_command_updates_include_telegram_bot_command_entities() -> None:
    # Given: an incoming command update addressed to the disposable bot.
    with FakeTelegramApi() as api:
        _ = api.enqueue_update(
            MessageUpdate(chat_id=41, user_id=7, text="/start rc1_abcdefghijklmnopqrstuv")
        )

        # When: the polling API returns the typed update.
        response = _post(api, "getUpdates")

    # Then: PTB's command filter receives the entity Telegram supplies in production.
    assert response["result"][0]["message"]["entities"] == [
        {"length": 6, "offset": 0, "type": "bot_command"}
    ]

def test_send_and_edit_message_use_monotonic_ids_and_not_modified_error() -> None:
    # Given: a started fake Telegram API.
    with FakeTelegramApi() as api:
        # When: two messages are sent, one is edited, and the same edit is repeated.
        sent = _post(api, "sendMessage", chat_id=41, text="private first")
        sent_again = _post(api, "sendMessage", chat_id=41, text="private second")
        edited = _post(
            api,
            "editMessageText",
            chat_id=41,
            message_id=1,
            text="private replacement",
        )
        unchanged = _post(
            api,
            "editMessageText",
            chat_id=41,
            message_id=1,
            text="private replacement",
        )

    # Then: IDs increase and Telegram's not-modified contract is preserved.
    assert sent == _message_response(1, "private first")
    assert sent_again == _message_response(2, "private second")
    assert edited == _message_response(1, "private replacement")
    assert unchanged == {
        "description": "Bad Request: message is not modified",
        "error_code": 400,
        "ok": False,
    }

def test_callback_action_future_resolves_after_inline_keyboard_edit() -> None:
    # Given: a callback future subscribed before a generated operator card update.
    with FakeTelegramApi() as api:
        target = api.callback_action_future(
            "approved-draft",
            prefix="n3:",
            action="a",
        )
        _ = _post(api, "sendMessage", chat_id=41, text="private pending")

        # When: the real card surface edits in an approval action.
        _ = _post(
            api,
            "editMessageText",
            chat_id=41,
            message_id=1,
            text="private generated",
            reply_markup=(
                '{"inline_keyboard":[[{'
                '"callback_data":"n3:draft:a:nonce:1","text":"Approve"}]]}'
            ),
        )
        selected = target.result(timeout=2.0)

    # Then: the waiter observes only the edited card's opaque action.
    assert selected == (41, 1, "n3:draft:a:nonce:1")

def test_callback_actions_future_resolves_terminal_generation_action() -> None:
    # Given: a terminal-card waiter subscribed to either production action.
    with FakeTelegramApi() as api:
        target = api.callback_actions_future(
            "terminal-draft",
            prefix="n3:",
            actions=("a", "r"),
        )

        # When: the model rejects generation and exposes only a retry control.
        _ = _post(
            api,
            "sendMessage",
            chat_id=41,
            text="private failure",
            reply_markup=(
                '{"inline_keyboard":[[{'
                '"callback_data":"n3:draft:r:nonce:1","text":"Retry"}]]}'
            ),
        )
        selected = target.result(timeout=2.0)

    # Then: the selected opaque callback carries the matched terminal action.
    assert selected == (41, 1, "n3:draft:r:nonce:1", "r")

def test_edit_message_updates_keyboard_when_text_is_unchanged() -> None:
    # Given: a generated card whose text remains stable across approval.
    with FakeTelegramApi() as api:
        target = api.callback_action_future(
            "delivery-control",
            prefix="n3:",
            action="s",
        )
        _ = _post(
            api,
            "sendMessage",
            chat_id=41,
            text="private generated",
            reply_markup=(
                '{"inline_keyboard":[[{'
                '"callback_data":"n3:draft:a:nonce:1","text":"Approve"}]]}'
            ),
        )

        # When: approval changes only the inline keyboard.
        edited = _post(
            api,
            "editMessageText",
            chat_id=41,
            message_id=1,
            text="private generated",
            reply_markup=(
                '{"inline_keyboard":[[{'
                '"callback_data":"n3:draft:s:nonce:1","text":"Send"}]]}'
            ),
        )

    # Then: the fake preserves Telegram's text-plus-markup modification contract.
    assert edited == _message_response(1, "private generated")
    assert target.result(timeout=2.0) == (41, 1, "n3:draft:s:nonce:1")

def test_callback_actions_future_prefers_first_action_on_generated_card() -> None:
    # Given: success and retry controls share one generated operator card.
    with FakeTelegramApi() as api:
        target = api.callback_actions_future(
            "generated-draft",
            prefix="n3:",
            actions=("a", "r"),
        )

        # When: production publishes both controls in one keyboard.
        _ = _post(
            api,
            "sendMessage",
            chat_id=41,
            text="private generated",
            reply_markup=(
                '{"inline_keyboard":[['
                '{"callback_data":"n3:draft:a:nonce:1","text":"Approve"},'
                '{"callback_data":"n3:draft:r:nonce:1","text":"Retry"}]]}'
            ),
        )
        selected = target.result(timeout=2.0)

    # Then: action priority selects approval instead of leaving the waiter unresolved.
    assert selected == (41, 1, "n3:draft:a:nonce:1", "a")

def test_edit_message_reply_markup_retires_an_inline_keyboard() -> None:
    # Given: a sent card with an inline callback button.
    with FakeTelegramApi() as api:
        sent = _post(
            api,
            "sendMessage",
            chat_id=41,
            text="private card",
            reply_markup='{"inline_keyboard":[[{"callback_data":"opaque","text":"Confirm"}]]}',
        )

        # When: production's callback-retirement API removes its markup.
        retired = _post(api, "editMessageReplyMarkup", chat_id=41, message_id=1)
        unchanged = _post(api, "editMessageReplyMarkup", chat_id=41, message_id=1)

    # Then: removal returns the original message and repeated removal is rejected.
    assert sent == _message_response(1, "private card")
    assert retired == _message_response(1, "private card")
    assert unchanged == {
        "description": "Bad Request: message is not modified",
        "error_code": 400,
        "ok": False,
    }

def test_json_send_message_preserves_inline_callback_protocol() -> None:
    # Given: the JSON request shape python-telegram-bot uses for inline markup.
    with FakeTelegramApi() as api:
        callback_future = api.callback_future(
            "consent-grant",
            prefix="cc1:",
            suffix=":g",
        )
        connection = http.client.HTTPConnection("127.0.0.1", api.port, timeout=2.0)
        try:
            connection.request(
                "POST",
                "/bottest-token/sendMessage",
                body=json.dumps(
                    {
                        "chat_id": 41,
                        "text": "private card",
                        "reply_markup": {
                            "inline_keyboard": [[{"callback_data": "cc1:opaque:g", "text": "Grant"}]]
                        },
                    }
                ),
                headers={"Content-Type": "application/json"},
            )
            assert connection.getresponse().status == 200
        finally:
            connection.close()

        # When: the card callback is selected from the fake Telegram surface.
        target = callback_future.result(timeout=2.0)

    # Then: nested JSON was retained and the intended callback is selected exactly.
    assert target == (41, 1, "cc1:opaque:g")

def test_auxiliary_bot_methods_and_command_state_are_supported() -> None:
    # Given: command and webhook-compatible request payloads.
    with FakeTelegramApi() as api:
        # When: callback, typing, command, and webhook methods are exercised.
        answered = _post(api, "answerCallbackQuery", callback_query_id="secret-id")
        typing = _post(api, "sendChatAction", chat_id=41, action="typing")
        commands = _post(
            api,
            "setMyCommands",
            commands='[{"command":"start","description":"Start"}]',
        )
        webhook = _post(api, "getWebhookInfo")
        deleted_commands = _post(api, "deleteMyCommands")
        deleted_webhook = _post(api, "deleteWebhook", drop_pending_updates="true")

    # Then: Telegram-compatible success values are returned.
    assert (answered, typing, commands, deleted_commands, deleted_webhook) == (
        {"ok": True, "result": True},
        {"ok": True, "result": True},
        {"ok": True, "result": True},
        {"ok": True, "result": True},
        {"ok": True, "result": True},
    )
    assert webhook == {
        "ok": True,
        "result": {
            "has_custom_certificate": False,
            "pending_update_count": 0,
            "url": "",
        },
    }

def test_named_action_future_synchronizes_without_polling() -> None:
    # Given: a future registered before the exact action is triggered.
    with FakeTelegramApi() as api:
        action_future = api.action_future("typing-observed", "sendChatAction")

        # When: a request runs concurrently with the waiter.
        with ThreadPoolExecutor(max_workers=1) as executor:
            request_future = executor.submit(
                _post,
                api,
                "sendChatAction",
                chat_id=41,
                action="typing",
            )
            action = action_future.result(timeout=2.0)
            response = request_future.result(timeout=2.0)

    # Then: the named future resolves to sanitized metadata only.
    assert response == {"ok": True, "result": True}
    assert action.method == "sendChatAction"
    assert action.chat_id == 41

def test_action_log_is_append_only_and_omits_tokens_and_message_content() -> None:
    # Given: requests containing a token, text, callback data, and callback identity.
    with FakeTelegramApi() as api:
        _ = _post(api, "sendMessage", chat_id=41, text="TOP SECRET MESSAGE")
        _ = _post(
            api,
            "answerCallbackQuery",
            callback_query_id="TOP-SECRET-CALLBACK",
            text="TOP SECRET ANSWER",
        )

        # When: the append-only sanitized log is rendered.
        first_snapshot = api.sanitized_actions()
        rendered = api.render_action_log()

    # Then: order is stable and no credential or content crosses the log boundary.
    assert tuple(action.sequence for action in first_snapshot) == (1, 2)
    assert tuple(action.method for action in first_snapshot) == (
        "sendMessage",
        "answerCallbackQuery",
    )
    assert "test-token" not in rendered
    assert "TOP SECRET" not in rendered
    assert "TOP-SECRET-CALLBACK" not in rendered

def test_private_evidence_is_atomic_and_server_cleanup_releases_port(
    tmp_path: Path,
) -> None:
    # Given: a private evidence destination and a running loopback fake.
    evidence_dir = tmp_path / "evidence"
    evidence_dir.mkdir(mode=0o700)
    api = FakeTelegramApi()
    api.start()
    port = api.port

    # When: sanitized evidence is atomically replaced and the fake is closed.
    _ = _post(api, "getMe")
    write_text(evidence_dir / "actions.jsonl", api.render_action_log())
    api.close()

    # Then: evidence is private, temporary files are absent, and cleanup completed.
    assert stat.S_IMODE(evidence_dir.stat().st_mode) == 0o700
    assert stat.S_IMODE((evidence_dir / "actions.jsonl").stat().st_mode) == 0o600
    assert tuple(evidence_dir.glob(".*.tmp")) == ()
    assert api.is_closed
    assert api.port == port


--- cwd ---
/home/cube/projects/richard/traning coach
--- PI env ---
PI_CODING_AGENT=true
PI_MODEL=gpt-5.6-luna
PI_PROMPT_CACHE_SAFE_WAIT_SECONDS=270
PI_PROVIDER=openai-codex
PI_REASONING_LEVEL=high
PI_SESSION_FILE=/home/cube/projects/richard/traning coach/.omo/senpi-task/children/st_01a02879/sessions/st_01a02879/2026-08-22T07-57-46-036Z_01a02879-ccb4-791d-9169-d07ff8b7ff44.jsonl
PI_SESSION_ID=01a02879-ccb4-791d-9169-d07ff8b7ff44
--- repo status ---
 M dualcoach/profile/build/lib/checkin_cli/customer_coaching.py
 M dualcoach/profile/build/lib/checkin_cli/models.py
 M dualcoach/profile/build/lib/checkin_cli/wizard.py
 M dualcoach/profile/build/lib/checkin_cli/wizard_models.py
 M dualcoach/profile/checkin_cli/customer_coaching.py
 M dualcoach/profile/checkin_cli/models.py
 M dualcoach/profile/checkin_cli/wizard.py
 M dualcoach/profile/checkin_cli/wizard_models.py
 M gateway/platforms/dualcoach_admin.py
 M gateway/platforms/nutrition_coaching.py
 M gateway/platforms/physique_checkin.py
 M gateway/platforms/telegram.py
 M gateway/platforms/telegram_customer_bootstrap.py
 M gateway/platforms/telegram_customer_bootstrap_registration.py
 M gateway/platforms/telegram_nutrition_onboarding_runtime.py
 M gateway/platforms/telegram_polling_receipts.py
 M gateway/platforms/telegram_staff_membership_gate.py
 M gateway/run.py
 M hermes_cli/env_loader.py
 M tests/gateway/test_nutrition_coaching.py
 M tests/gateway/test_telegram_group_gating.py
 M tests/gateway/test_telegram_physique_checkin.py
?? .debug-journal.md
?? .disposable-e2e-evidence/
?? .omo/
?? disposable-e2e-red.txt
?? docs/dualcoach-v1.1.1-migration.md
?? dualcoach/profile/build/lib/checkin_cli/policies/diagnostic-promotion-policy.json
?? dualcoach/profile/build/lib/checkin_cli/policies/nutrition-restriction-kb-template-v1.json
?? dualcoach/profile/checkin_cli/policies/diagnostic-promotion-policy.json
?? dualcoach/profile/checkin_cli/policies/nutrition-restriction-kb-template-v1.json
?? dualcoach/profile/physique_checkin_cli.egg-info/
?? dualcoach/profile/tests/test_trainer_wizard.py
?? gateway/platforms/dualcoach_profile_package.py
?? gateway/platforms/telegram_nutrition_menu.py
?? gateway/platforms/telegram_production_preflight.py
?? scripts/compare_dualcoach_v111_seals.py
?? scripts/dualcoach_v111_disposable_fixture.py
?? scripts/dualcoach_v111_disposable_process.py
?? scripts/dualcoach_v111_evidence.py
?? scripts/dualcoach_v111_fake_telegram.py
?? scripts/dualcoach_v111_fake_telegram_engine.py
?? scripts/dualcoach_v111_fake_telegram_types.py
?? scripts/dualcoach_v111_loopback_model.py
?? scripts/migrate_dualcoach_v111_profile.py
?? scripts/plan_dualcoach_v111_migration.py
?? scripts/run_dualcoach_v111_disposable_e2e.py
?? tests/gateway/test_dualcoach_admin_loopback.py
?? tests/gateway/test_dualcoach_disposable_e2e_contract.py
?? tests/gateway/test_dualcoach_loopback_model_provider.py
?? tests/gateway/test_dualcoach_profile_package.py
?? tests/gateway/test_dualcoach_v111_migration.py
?? tests/gateway/test_fake_telegram_bot_api.py
?? tests/gateway/test_telegram_nutrition_onboarding_recovery.py
?? tests/gateway/test_telegram_production_preflight.py
?? tests/gateway/test_telegram_publication_recovery.py
?? tests/support/
--- relevant config ---
/home/cube/projects/richard/.worktrees/nutricoach-v111-impl/pyproject.toml


/home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:156:27 - error: "Literal[0]" 형식의 인수를 "__getitem__" 함수에서 "str" 형식의 "key" 매개 변수에 할당할 수 없습니다.
    "Literal[0]"은 "str"에 할당할 수 없습니다. (reportArgumentType)
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:156:27 - error: "__getitem__" 메서드가 "bool" 형식에 정의되지 않았습니다. (reportIndexIssue)
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:156:27 - error: "__getitem__" 메서드가 "int" 형식에 정의되지 않았습니다. (reportIndexIssue)
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:156:27 - error: "__getitem__" 메서드가 "float" 형식에 정의되지 않았습니다. (reportIndexIssue)
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:156:27 - error: ’None’ 유형의 개체는 아래 첨자를 사용할 수 없습니다. (reportOptionalSubscript)
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:156:27 - error: "JsonValue | str | Unknown" 형식의 인수를 "de_json" 함수에서 "JSONDict" 형식의 "data" 매개 변수에 할당할 수 없습니다.
    형식 "JsonValue | Unknown"은 형식 "JSONDict"에 할당할 수 없습니다.
      "float"은 "dict[str, Any]"에 할당할 수 없습니다. (reportArgumentType)
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:170:12 - error: "Literal[0]" 형식의 인수를 "__getitem__" 함수에서 "str" 형식의 "key" 매개 변수에 할당할 수 없습니다.
    "Literal[0]"은 "str"에 할당할 수 없습니다. (reportArgumentType)
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:170:12 - error: "__getitem__" 메서드가 "bool" 형식에 정의되지 않았습니다. (reportIndexIssue)
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:170:12 - error: "__getitem__" 메서드가 "int" 형식에 정의되지 않았습니다. (reportIndexIssue)
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:170:12 - error: "__getitem__" 메서드가 "float" 형식에 정의되지 않았습니다. (reportIndexIssue)
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:170:12 - error: ’None’ 유형의 개체는 아래 첨자를 사용할 수 없습니다. (reportOptionalSubscript)
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:170:12 - error: 제공된 인수와 일치하는 "__getitem__"에 대한 오버로드가 없습니다. (reportCallIssue)
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:170:12 - error: "Literal['message']" 형식의 인수를 "__getitem__" 함수에서 "slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None]" 형식의 "s" 매개 변수에 할당할 수 없습니다.
    "Literal['message']"은 "slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None]"에 할당할 수 없습니다. (reportArgumentType)
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:170:12 - error: "Literal['message']" 형식의 인수를 "__getitem__" 함수에서 "SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None]" 형식의 "key" 매개 변수에 할당할 수 없습니다.
    형식 "Literal['message']"은 형식 "SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None]"에 할당할 수 없습니다.
      ‘Literal['message']’은(는) ‘SupportsIndex’ 프로토콜과 호환되지 않습니다.
        "__index__"이(가) 없습니다.
      "Literal['message']"은 "slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None]"에 할당할 수 없습니다. (reportArgumentType)
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:170:12 - error: "__getitem__" 메서드가 "bool" 형식에 정의되지 않았습니다. (reportIndexIssue)
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:170:12 - error: "__getitem__" 메서드가 "int" 형식에 정의되지 않았습니다. (reportIndexIssue)
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:170:12 - error: "__getitem__" 메서드가 "float" 형식에 정의되지 않았습니다. (reportIndexIssue)
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:170:12 - error: ’None’ 유형의 개체는 아래 첨자를 사용할 수 없습니다. (reportOptionalSubscript)
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:170:12 - error: 제공된 인수와 일치하는 "__getitem__"에 대한 오버로드가 없습니다. (reportCallIssue)
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:170:12 - error: "Literal['entities']" 형식의 인수를 "__getitem__" 함수에서 "slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None]" 형식의 "s" 매개 변수에 할당할 수 없습니다.
    "Literal['entities']"은 "slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None]"에 할당할 수 없습니다. (reportArgumentType)
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:170:12 - error: "Literal['entities']" 형식의 인수를 "__getitem__" 함수에서 "SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None]" 형식의 "key" 매개 변수에 할당할 수 없습니다.
    형식 "Literal['entities']"은 형식 "SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None]"에 할당할 수 없습니다.
      ‘Literal['entities']’은(는) ‘SupportsIndex’ 프로토콜과 호환되지 않습니다.
        "__index__"이(가) 없습니다.
      "Literal['entities']"은 "slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None]"에 할당할 수 없습니다. (reportArgumentType)
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:170:12 - error: "__getitem__" 메서드가 "bool" 형식에 정의되지 않았습니다. (reportIndexIssue)
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:170:12 - error: "__getitem__" 메서드가 "int" 형식에 정의되지 않았습니다. (reportIndexIssue)
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:170:12 - error: "__getitem__" 메서드가 "float" 형식에 정의되지 않았습니다. (reportIndexIssue)
  /home/cube/projects/richard/.worktrees/nutricoach-v111-impl/tests/gateway/test_fake_telegram_bot_api.py:170:12 - error: ’None’ 유형의 개체는 아래 첨자를 사용할 수 없습니다. (reportOptionalSubscript)
25 errors, 0 warnings, 0 notes

Command exited with code 1


**Planning type modeling and runtime checks**


# Type Patterns

How to use Python's type system to catch bugs at check time, not runtime.

---

## NewType — distinct primitives

Same runtime type, different meaning. The type checker prevents mixing.

```python
from typing import NewType

UserId = NewType("UserId", int)
MovieId = NewType("MovieId", int)
Email = NewType("Email", str)
Seconds = NewType("Seconds", float)
Milliseconds = NewType("Milliseconds", float)

def get_user(user_id: UserId) -> User: ...
def get_movie(movie_id: MovieId) -> Movie: ...
def sleep(duration: Seconds) -> None: ...

uid = UserId(42)
mid = MovieId(42)

get_user(uid)   # OK
get_user(mid)   # type error: MovieId is not UserId
get_user(42)    # type error: int is not UserId
sleep(Milliseconds(100.0))  # type error
```

**Use when**: IDs, indices, keys, units of measurement — any pair where swapping is a bug.
**Skip when**: ephemeral local math where branding adds noise with zero safety gain.

---

## Final — constants are const

Module-level constants declare their intent. Reassignment is a type error.

```python
from typing import Final

MAX_RETRIES: Final = 3
API_BASE_URL: Final = "https://api.example.com"
DEFAULT_TIMEOUT: Final = 30.0

MAX_RETRIES = 5  # type error: cannot assign to Final
```

If it changes at runtime, it's not a constant — make it a function parameter or config field.

---

## TypeAlias — name complex types

If a union or generic appears more than once, give it a name.

```python
# Python 3.12+
type JsonValue = str | int | float | bool | None | list["JsonValue"] | dict[str, "JsonValue"]
type Headers = dict[str, str]
type Middleware = Callable[[Request], Awaitable[Response]]

# Pre-3.12
from typing import TypeAlias

JsonValue: TypeAlias = str | int | float | bool | None | list["JsonValue"] | dict[str, "JsonValue"]
```

---

## StrEnum / IntEnum — closed sets

Any fixed set of known values. No string literals scattered through code.

```python
from enum import StrEnum, IntEnum, unique

@unique
class Role(StrEnum):
    ADMIN = "admin"
    USER = "user"
    GUEST = "guest"

@unique
class HttpStatus(IntEnum):
    OK = 200
    NOT_FOUND = 404
    INTERNAL_ERROR = 500

# BAD
def check_role(role: str) -> bool: ...

# GOOD
def check_role(role: Role) -> bool: ...
```

`StrEnum` when values serialize as strings (API, DB). `IntEnum` for numeric codes. Plain `Enum` for pure labels.

---

## Type narrowing — let the checker follow your logic

`isinstance`, `is None`, and `match` narrow types automatically. Use them instead of `cast`.

```python
def process(value: str | int | None) -> str:
    if value is None:
        return "nothing"
    # checker knows: str | int

    if isinstance(value, str):
        return value.upper()
    # checker knows: int

    return str(value * 2)
```

### TypeGuard for custom narrowing

```python
from typing import TypeGuard

def is_valid_email(value: str) -> TypeGuard[Email]:
    return "@" in value and "." in value.split("@")[1]

def send(addr: str) -> None:
    if not is_valid_email(addr):
        raise ValueError(addr)
    # checker knows: addr is Email
    deliver(addr)
```

### TypeIs (Python 3.13+) — the strict version

`TypeIs` is stricter than `TypeGuard` — it narrows in both `if` and `else` branches.

```python
from typing import TypeIs

def is_str(value: str | int) -> TypeIs[str]:
    return isinstance(value, str)

def handle(v: str | int) -> None:
    if is_str(v):
        print(v.upper())   # checker knows: str
    else:
        print(v + 1)       # checker knows: int
```

---

## Union syntax

Always `X | Y`. Never `Union[X, Y]` or `Optional[X]`.

```python
# BAD
from typing import Union, Optional
def f(x: Optional[int]) -> Union[str, int]: ...

# GOOD
def f(x: int | None) -> str | int: ...
```

---

## Sources

- Python docs: [typing — NewType](https://docs.python.org/3/library/typing.html#newtype)
- Python docs: [typing — Final](https://docs.python.org/3/library/typing.html#typing.Final)
- Python docs: [typing — TypeGuard](https://docs.python.org/3/library/typing.html#typing.TypeGuard)
- PEP 604: [Union syntax X | Y](https://peps.python.org/pep-0604/)
- PEP 742: [TypeIs](https://peps.python.org/pep-0742/)


# Data Modeling

Which container to use, how to structure data, and why frozen is the default.

---

## Decision flowchart

```
Is it a fixed set of named constants?
  YES → StrEnum / IntEnum
  NO ↓
Is it just branding a primitive (int, str, float)?
  YES → NewType("X", base)
  NO ↓
Is it an interface / contract ("this thing can do X")?
  ├─ Shape only, no shared code → Protocol
  └─ Shared method implementation needed → ABC
  NO ↓
Does the data cross a trust boundary (user input, API, file, external DB)?
  YES → pydantic.BaseModel (frozen=True) — validates + serializes
  NO ↓
Is it a dict shape needed for JSON compat / **kwargs typing?
  YES → TypedDict
  NO ↓
Is it structured data with named fields?
  YES → @dataclass(frozen=True, slots=True)
  NO ↓
Is it a tuple with positional semantics (x, y coords / DB row)?
  YES → NamedTuple
  NO → you probably don't need a new type
```

---

## Container reference

### @dataclass — internal value object

The default for structured data inside your codebase. Zero overhead, no framework coupling.

```python
from dataclasses import dataclass
from typing import NewType

UserId = NewType("UserId", int)

@dataclass(frozen=True, slots=True)
class User:
    id: UserId
    name: str
    email: str

@dataclass(frozen=True, slots=True)
class Point:
    x: float
    y: float
```

Always `frozen=True, slots=True`. Mutable only when mutation is the documented purpose — opt out with `# noqa: MUTABLE_OK`.

### Pydantic BaseModel — trust boundary guardian

Use when data enters or leaves your system. Validates at construction, serializes to JSON, generates OpenAPI schema.

```python
from pydantic import BaseModel, ConfigDict, EmailStr

class CreateUserRequest(BaseModel):
    model_config = ConfigDict(frozen=True)

    name: str
    email: EmailStr
    age: int

class UserResponse(BaseModel):
    model_config = ConfigDict(frozen=True)

    id: int
    name: str
    email: str
```

**The one rule**: data crosses a trust boundary → Pydantic. Everything else → dataclass.
Never use Pydantic for internal-only data just because it's convenient. The validation cost is real.

### TypedDict — dict that knows its shape

Use when the value must stay a `dict` at runtime — JSON blobs, `**kwargs`, third-party APIs expecting dicts.

```python
from typing import TypedDict, NotRequired

class Headers(TypedDict):
    content_type: str
    authorization: NotRequired[str]

def make_request(url: str, headers: Headers) -> None: ...

make_request("https://api.example.com", {"content_type": "application/json"})
```

### Protocol — structural interface

"Anything that has method X" — no inheritance required.

```python
from typing import Protocol

class Renderable(Protocol):
    def render(self) -> str: ...

class Saveable(Protocol):
    async def save(self) -> None: ...

@dataclass(frozen=True, slots=True)
class MarkdownDoc:
    content: str
    def render(self) -> str:
        return self.content

def publish(doc: Renderable) -> None:
    print(doc.render())  # MarkdownDoc works — no inheritance needed
```

Default to Protocol for interfaces. ABC only when you need shared method implementations.

### ABC — interface with shared code

Only when Protocol isn't enough.

```python
from abc import ABC, abstractmethod

class BaseRepository(ABC):
    @abstractmethod
    async def get(self, id: int) -> Model | None: ...

    @abstractmethod
    async def save(self, model: Model) -> None: ...

    async def get_or_raise(self, id: int) -> Model:
        result = await self.get(id)
        if result is None:
            msg = f"{type(self).__name__}: id {id} not found"
            raise LookupError(msg)
        return result
```

### NamedTuple — positional + named (rare)

Only when you need tuple protocol (unpacking, indexing).

```python
from typing import NamedTuple

class Coordinate(NamedTuple):
    x: float
    y: float

x, y = Coordinate(1.0, 2.0)  # tuple unpacking
```

99% of the time, `@dataclass(frozen=True, slots=True)` is better.

---

## Quick lookup

| Situation | Use | Why |
|---|---|---|
| User input, API request/response | `Pydantic BaseModel` | Validation, JSON schema, serialization |
| DB row ↔ Python (ORM) | SQLAlchemy `Mapped[]` model | ORM integration, async session |
| Internal value object | `@dataclass(frozen=True, slots=True)` | Zero overhead, no validation needed |
| Multiple outcomes from function | Union of frozen dataclasses | Distinct types for `match` |
| Dict shape for JSON / `**kwargs` | `TypedDict` | Stays a dict at runtime |
| Fixed constants | `StrEnum` / `IntEnum` | Exhaustive match, no typos |
| Distinct primitive | `NewType("X", int)` | Zero runtime cost, type-level only |
| Contract / capability | `Protocol` | Structural typing, no inheritance |
| Contract + shared impl | `ABC` | When Protocol isn't enough |

---

## Comparison matrix

| Feature | dataclass | Pydantic | TypedDict | Protocol | NamedTuple | NewType | Enum |
|---|---|---|---|---|---|---|---|
| Validation | - | ✓ | - | - | - | - | - |
| JSON serialization | manual | built-in | native dict | - | - | - | `.value` |
| Immutable | frozen=True | frozen=True | - (dict) | N/A | always | N/A | always |
| Runtime cost | ~zero | validation | zero | zero | ~zero | zero | ~zero |
| `match` support | ✓ | ✓ | - | - | ✓ | - | ✓ |
| `slots` support | ✓ | - | - | - | - | - | - |

---

## Parse, don't validate

Validate at the boundary. Inside the boundary, types are proof of validity.

```python
# BAD — validate then pass raw data
def process_email(email: str) -> None:
    if "@" not in email:
        raise ValueError("invalid email")
    # still a raw str everywhere downstream

# GOOD — parse into typed value at boundary
from typing import NewType

Email = NewType("Email", str)

def parse_email(raw: str) -> Email:
    if "@" not in raw or "." not in raw.split("@")[1]:
        msg = f"invalid email: {raw}"
        raise ValueError(msg)
    return Email(raw.lower().strip())

# Downstream only sees Email, never raw str
def send_welcome(email: Email) -> None: ...
```

With Pydantic this happens automatically — `EmailStr` is already a parsed type. Once constructed, `.email` is always valid. No re-validation needed.

---

## Sources

- Python docs: [dataclasses](https://docs.python.org/3/library/dataclasses.html)
- Pydantic v2: [docs.pydantic.dev](https://docs.pydantic.dev/latest/)
- Python docs: [typing — Protocol](https://docs.python.org/3/library/typing.html#typing.Protocol)
- Python docs: [typing — TypedDict](https://docs.python.org/3/library/typing.html#typing.TypedDict)
- Alexis King: [Parse, don't validate](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/)