feat(coding-dev): require domain values to be typed, not primitives

A value with rules of its own — a URL, a path, a closed set, an identifier —
gets a type of its own. Passing it as a bare string discards those rules at the
first function boundary and leaves every consumer re-deriving them.

Added as a subsection of Architecture Preferences, with a table mapping the
common cases to the type to reach for in any language, the reasoning, and the
boundary rule: parse once at the edge, keep the type inward, serialize back only
where the wire actually demands a string. Notes the counter-case, so this is not
read as a mandate to wrap every string.

python.md gains the concrete choices: yarl.URL (already present wherever aiohttp
is), pathlib, StrEnum, and NewType for non-interchangeable identifiers.
This commit is contained in:
Bluscream committed 2026-09-18 18:22:26 +02:00
1 parent 3171595940
commit b08fe50e92
2 files changed
+47

No files matched your search

+17
View File
@@ -48,6 +48,23 @@ disallow_untyped_defs = true
- Third-party library missing stubs: install `types-*` if it exists, otherwise a narrowly
scoped `# type: ignore[import-untyped]` with a comment — never a blanket module-level ignore.
### Value types over primitives
The general rule is in [`coding-dev.md` §7](skills-public/coding-dev.md); Python's concrete
choices are:
- **`yarl.URL` for URLs**, not f-string assembly. It ships with `aiohttp`, so any async HTTP
project already has it. `URL(base) / "api" / "v4" / "users"` joins without double slashes,
and `.with_query({...})` escapes parameters that string formatting would corrupt.
- **`pathlib.Path` for paths**, never `os.path` + `str`. Ruff's `PTH` rules enforce this;
keep `os.fdopen` where a descriptor rather than a name is being adopted.
- `enum.StrEnum` (3.11+) where a closed set must also serialize as a string, so the wire
format stays a string without the code comparing bare literals.
- `typing.NewType` for identifiers that should not be interchangeable — a `UserId` and a
`RepoId` are both `int` and nothing otherwise stops them being swapped.
- Parse at the boundary: take `str` in the public signature if callers supply one, convert
immediately, and keep the real type internal.
---
## 3. Linting