docs(P03): complete language-derived extension — v0.4

---ci---
project: atelier
phase: 3
milestone: v0.4
status: complete
phase_role: execution
phase_tag: v0.3.3
requirements:
  covered: [ATELIER-102, ATELIER-103, ATELIER-104, ATELIER-105]
  partial: []
---/ci---
This commit is contained in:
Jon Chery
2026-08-05 16:07:21 +00:00
parent 0fdf892d81
commit e20a08f03e
20 changed files with 1991 additions and 0 deletions
+171
View File
@@ -0,0 +1,171 @@
# Go Concurrency — Derived Application
> Applies Atelier's domain principles to Go's concurrency specifically. Go's distinctive strength (goroutines, channels, context) earns a dedicated concurrency doc rather than a `go-async.md`.
> Derives from `domains/` docs; introduces no new P-rules (D-063).
> See `languages/go.md` for the language first-principles stub.
## Goroutines and Structured Concurrency (Concurrency P1 Immutability by Default, C6 Composability)
- **`go f()` spawns a goroutine; ensure it does not outlive its parent:** an unstructured `go f()` leaks when the parent returns. Use `sync.WaitGroup`, `errgroup.Group`, or a `context`-scoped pattern to bound lifetime.
- **`errgroup.WithContext` for structured concurrency:** a `Group` cancels its context on first error; siblings see the cancellation and exit. Mirrors `TaskGroup` semantics cross-language.
- **Goroutines share only immutable inputs:** `go process(snap)` where `snap` is a copy. A goroutine sharing a mutable slice with the parent is a race (Concurrency P1 Immutability, P6 No Silent Races).
- **No `go` in a library function without a documented lifetime:** a library that spawns unbounded goroutines leaks them into the caller. Either accept a `context.Context` or return a `Stop()` method.
```go
import "golang.org/x/sync/errgroup"
func fetchAll(ctx context.Context, ids []string) ([]*User, error) {
g, ctx := errgroup.WithContext(ctx)
results := make([]*User, len(ids))
for i, id := range ids {
i, id := i, id // capture loop vars
g.Go(func() error {
u, err := fetchUser(ctx, id)
if err != nil { return err }
results[i] = u
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
```
## Channels: Bounded Queues and Backpressure (Concurrency P9 Bounded Queues, C6 Composability)
- **Bounded channels apply backpressure:** `make(chan T, N)` blocks the sender when full (Concurrency P9 — bounded queues). Unbounded `make(chan T)` lets the producer run ahead and OOM.
- **`select` with `default` for non-blocking send/receive:** a `default` case makes the channel a queue with try semantics; without it, the operation blocks.
- **Close channel from the sender, never the receiver:** closing a channel signals "no more sends." A receiver closing it is a race; the sender may still be writing.
- **One channel, one responsibility:** do not multiplex control and data on the same channel. Use a `select` over multiple channels instead.
- **Applies `messaging/queues`:** a bounded Go channel is an in-process broker — bounded buffer, backpressure, at-most-once handoff. The same semantics apply; the broker is local.
```go
func pipeline(ctx context.Context, in <-chan Job, out chan<- Result) {
for {
select {
case j, ok := <-in:
if !ok { return }
r := process(j)
select {
case out <- r:
case <-ctx.Done():
return
}
case <-ctx.Done():
return
}
}
}
// bounded: backpressure when out is full
out := make(chan Result, 16)
```
## context.Context for Cancellation (Concurrency P7 Cancellation Support, Concurrency P8 Timeout Discipline)
- **`context.Context` is the first parameter of every I/O function:** `func fetchUser(ctx context.Context, id string) (*User, error)`. A function that does I/O without a `ctx` cannot be cancelled (Concurrency P7).
- **`context.WithTimeout` for a deadline:** `ctx, cancel := context.WithTimeout(ctx, 5*time.Second); defer cancel()`. Every external call races against a deadline (Concurrency P8).
- **`cancel()` always called, even on success:** `defer cancel()` immediately after creating the context. A leaked context leaks its timer.
- **Never store a `context.Context` in a struct:** pass it as a parameter. A struct holding a `ctx` captures a request-scoped value into a long-lived object.
- **Applies `concurrency/P7`:** cancellation propagates via `ctx.Done()`. A `select` on `<-ctx.Done()` is the cancel-aware wait.
```go
func fetchWithTimeout(ctx context.Context, url string) (*Response, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return nil, ErrTimeout
}
return nil, err
}
return resp, nil
}
```
## select and Multiplexed Channels (Concurrency P7 Cancellation Support, C6 Composability)
- **`select` multiplexes channel operations:** it picks a ready case at random (fair). A `select` with `<-ctx.Done()` plus a data case is the cancel-aware wait.
- **`default` makes `select` non-blocking:** use for "send if ready, else drop" (a bounded queue with drop-oldest policy).
- **`select {}` blocks forever:** a `select{}` with no cases is a permanent block. Use only in a goroutine that should run until the process exits.
- **Applies `concurrency/P7`:** the `select` over `ctx.Done()` and a result channel is the canonical cancel pattern.
```go
func processUntilCancel(ctx context.Context, jobs <-chan Job) {
for {
select {
case <-ctx.Done():
return
case j, ok := <-jobs:
if !ok { return }
// ...
}
}
}
```
## sync Primitives and Lock Scope (Concurrency P3 Boundaries are Locks, Concurrency P5 Lock Minimization)
- **`sync.Mutex` scoped minimally:** not held across I/O (a `Send` on a channel, an HTTP call). Hold the lock, mutate, release — then do I/O (Concurrency P3 Lock Scope).
- **`sync.RWMutex` for read-heavy, `Mutex` for write-heavy:** RWMutex adds overhead; only prefer it when reads dominate by 10x+.
- **`sync.Map` for specific cases (append-only, disjoint keys):** not a general `map[K]V` replacement. For most maps, `Mutex` + `map` is clearer and often faster.
- **`sync.Once` for one-time init:** `var once sync.Once; once.Do(func(){ init() })`. Idempotent and race-free.
- **Applies `concurrency/P5` (lock minimization):** prefer channels over locks; when a lock is needed, hold it for the smallest possible scope.
```go
type Cache struct {
mu sync.Mutex
items map[string]*User
}
func (c *Cache) Get(id string) (*User, bool) {
c.mu.Lock()
defer c.mu.Unlock()
u, ok := c.items[id]
return u, ok
}
func (c *Cache) Set(id string, u *User) {
c.mu.Lock()
c.items[id] = u
c.mu.Unlock() // explicit unlock before any I/O
}
```
## Race Detection (Concurrency P6 No Silent Races)
- **`go test -race` enforces `P6`:** the race detector instruments memory accesses and fails on data races. See `go-tooling.md` for the CI gate.
- **Tests must exercise the concurrent path:** a serial test of a `Mutex`-protected map finds no race. Write tests with N goroutines hitting the map under `-race`.
- **Applies `concurrency/P6`:** a race detected at test time is a bug fixed; a race undetected is a production heisenbug.
```go
func TestCacheConcurrent(t *testing.T) {
c := &Cache{items: map[string]*User{}}
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
i := i
wg.Add(1)
go func() {
defer wg.Done()
c.Set(strconv.Itoa(i), &User{})
c.Get(strconv.Itoa(i))
}()
}
wg.Wait()
}
```
## Cross-References
- `domains/concurrency/patterns.md` — the cancellation/timeout/semaphore patterns applied here.
- `domains/concurrency/first-principles.md` — Concurrency P1, P3, P5, P6, P7, P8, P9 traced throughout.
- `domains/messaging/queues.md` — bounded Go channels as in-process brokers; backpressure parallels (IDEATE-40).
- `domains/errors/patterns.md``errgroup` and error propagation in concurrent code.
- `languages/go-types.md` — typed channels carry the named types defined there.
- `languages/go-tooling.md` — the `-race` CI gate that enforces Concurrency P6.
- `languages/go-testing.md` — concurrent tests that exercise the race detector.
+141
View File
@@ -0,0 +1,141 @@
# Go Testing — Derived Application
> Applies Atelier's domain principles to Go testing specifically.
> Derives from `domains/` docs; introduces no new P-rules (D-063).
> See `languages/go.md` for the language first-principles stub.
## Table-Driven Tests (Testing P1 Tests as Specification, C2 Clarity)
- **Table-driven is the Go idiom:** `cases := []struct{ name string; in X; want Y }{...}`; loop with `t.Run(c.name, ...)`. Each case is a subtest with its own name and failure output.
- **Test names read as a spec:** `{"rejects empty email", ...}`, `{"returns persisted id", ...}`. A reader understands the unit from the subtest names (Testing P1).
- **No `if got != want { t.Fatal() }` shared across cases:** each case asserts independently; a failure in case 3 does not skip cases 4 and 5.
- **`t.Run` enables `-run` filtering:** `go test -run TestCreateUser/rejects_empty_email` runs one case. Essential for debugging a single failure.
```go
func TestCreateUser(t *testing.T) {
cases := []struct {
name string
email string
wantErr bool
}{
{"rejects empty email", "", true},
{"rejects missing @", "no-at-sign", true},
{"accepts valid email", "a@b.co", false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
_, err := CreateUser(c.email)
if (err != nil) != c.wantErr {
t.Fatalf("err=%v, wantErr=%v", err, c.wantErr)
}
})
}
}
```
## t.Parallel for Independence (Testing P2 Independence, Concurrency P10 Test for Race Conditions)
- **`t.Parallel()` for independent subtests:** each subtest opts in; the runner executes them concurrently. A test that fails under `Parallel` has hidden state (Testing P2 Independence).
- **Capture loop variables:** `c := c` inside the loop, or rely on Go 1.22+ per-iteration scoping. A parallel subtest sharing `c` races on the last value.
- **Applies `concurrency/P10` (test for races):** parallel tests are the first line of race detection; combine with `-race` for the full safety net.
```go
for _, c := range cases {
c := c // capture for parallel
t.Run(c.name, func(t *testing.T) {
t.Parallel()
_, err := CreateUser(c.email)
if (err != nil) != c.wantErr {
t.Fatalf("err=%v, wantErr=%v", err, c.wantErr)
}
})
}
```
## t.Cleanup for Teardown (Testing P3 Determinism, Testing P2 Independence)
- **`t.Cleanup(func() { ... })` for teardown:** runs in LIFO order after the test (and its subtests) complete. Replaces `defer` in a helper that does not know when the test ends.
- **Per-test state, not shared:** a `setup(t)` helper creates resources and registers cleanup; each test gets its own. A package-level `var` shared across tests is order coupling.
- **`t.TempDir()` for filesystem tests:** creates a unique temp dir and cleans up automatically. No manual `os.RemoveAll` and no cross-test contamination.
- **Applies `Testing P3` (determinism):** cleanup is tied to the test lifecycle, not a global teardown that may run before or after depending on order.
```go
func setupStore(t *testing.T) *Store {
t.Parallel()
dir := t.TempDir() // auto-cleaned
s, err := OpenStore(filepath.Join(dir, "db"))
if err != nil { t.Fatal(err) }
t.Cleanup(func() { s.Close() })
return s
}
```
## Race Detector (Testing P9 Edge Case Coverage, Concurrency P6 No Silent Races)
- **`go test -race` in CI, always:** see `go-tooling.md`. The detector is the enforcement of `concurrency/P6`.
- **Tests must exercise the concurrent path:** a serial test of a `Mutex`-protected map finds no race. Write tests with N goroutines.
- **`-count=1` to disable result caching:** by default, Go caches passing tests. `-count=1` forces re-run; combine with `-race` and parallelism to surface heisenbugs.
- **Applies `Testing P9` (edge case coverage):** the race detector is the edge-case tool for concurrency — it finds the inputs the test author forgot to write.
```bash
# CI gate
go test -race -count=1 ./...
```
## Time and Determinism (Testing P3 Determinism, Testing P9 Edge Case Coverage)
- **No `time.Now()` in code under test:** inject a `Clock` interface. In tests, a fake clock advances deterministically.
- **`time.Sleep` in tests is a smell:** a sleep waits for a real timer, flaky under load. Use a channel or `Eventually`-style polling with a timeout.
- **`t.Deadline()` aware helpers:** a helper that may take long checks `t.Deadline()` and bails early. Prevents a slow test from timing out the suite.
```go
type Clock interface { Now() time.Time }
type fakeClock struct{ t time.Time }
func (f *fakeClock) Now() time.Time { return f.t }
func TestUserHasCreatedAt(t *testing.T) {
clk := &fakeClock{time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)}
u, _ := CreateUserWithClock("a@b.co", clk)
if u.CreatedAt.Year() != 2024 {
t.Fatalf("year=%d, want 2024", u.CreatedAt.Year())
}
}
```
## Mocks and Interfaces (Testing P7 Realism, API P1 Contract Fidelity)
- **Mock at the interface, not the struct:** `type Store interface { Get(id string) (*User, error) }` in production; `type mockStore struct{ ... }` in test. The interface is the contract (applies `api/P1`).
- **`httptest` for HTTP servers:** `httptest.NewServer` gives a real server on a loopback port; no manual socket plumbing.
- **`testify/mock` or hand-written mocks:** hand-written for one-off, `testify` for complex sequencing. Avoid mocking frameworks that generate code at runtime (reflection-heavy) — they hide failures behind stack traces.
- **Applies `Testing P7` (realism):** mock the boundary (HTTP, DB), not the unit. Mocking the unit under test tests the mock.
```go
type mockStore struct {
users map[string]*User
got []string
}
func (m *mockStore) Get(id string) (*User, error) {
m.got = append(m.got, id)
return m.users[id], nil
}
func TestGetUserLogs(t *testing.T) {
s := &mockStore{users: map[string]*User{"abc": {}}}
svc := NewService(s)
svc.GetUser("abc")
if len(s.got) != 1 || s.got[0] != "abc" {
t.Fatalf("got=%v", s.got)
}
}
```
## Cross-References
- `domains/testing/pyramid.md` — where unit/integration/race tests sit; the race job is its own layer.
- `domains/testing/fixtures.md``t.TempDir` and `t.Cleanup` as the fixture discipline.
- `domains/testing/first-principles.md` — Testing P1 Specification, P2 Independence, P3 Determinism, P9 Edge Coverage.
- `domains/concurrency/first-principles.md` — Concurrency P6 (race detector), P10 (test for races).
- `languages/go-types.md` — the named types tests assert.
- `languages/go-concurrency.md` — concurrent tests exercise the patterns from that doc.
- `languages/go-tooling.md` — the `go test` flags (`-race`, `-count`, `-run`) detailed here.
+97
View File
@@ -0,0 +1,97 @@
# Go Tooling — Derived Application
> Applies Atelier's domain principles to Go tooling specifically.
> Derives from `domains/` docs; introduces no new P-rules (D-063).
> See `languages/go.md` for the language first-principles stub.
## go vet and golangci-lint (DevOps P2 Automation, C2 Clarity)
- **`go vet` is the stdlib baseline:** it catches `printf` format mismatches, lock-copy-by-value, and unreachable code. Run on every build.
- **`golangci-lint` aggregates vet + dozens of linters:** enable `errcheck` (no `_ = err`), `govet`, `staticcheck`, `ineffassign`, `unused`, `gofmt`, `goimports`. Each enabled linter has a one-line `# reason:` in `.golangci.yml`.
- **`errcheck` enforces `errors/P2` (fail loudly):** a discarded error is a silent failure. `errcheck` fails the build on `_ = doX()`.
- **`goimports` over `gofmt`:** `goimports` adds missing imports and removes unused ones, in addition to formatting. The format is not debated in review (Clarity C2).
```yaml
# .golangci.yml
linters:
enable:
- errcheck # reason: Errors P2 — no swallowed errors
- govet
- staticcheck
- ineffassign
- unused
- gofmt
- goimports
linters-settings:
errcheck:
check-blank: true # fail on _ = fn()
```
## go test -race (Concurrency P6 No Silent Races)
- **`go test -race` in CI, always:** the race detector instruments memory accesses and fails on data races. It is the primary enforcement of `concurrency/P6` (no silent races).
- **`-race` adds overhead; run it in a separate CI job:** the race build is ~2x slower; keep the fast unit-test job and add a race job.
- **`-race` requires tests that actually exercise the concurrent path:** a test that calls `Get`/`Set` serially finds no race. Write tests that spawn goroutines hitting the same map.
- **Applies `concurrency/P6`:** a race detected is a bug fixed; a race undetected is a heisenbug in production. The detector is the safety net.
```bash
# CI race job
go test -race -count=1 ./...
```
## Module Discipline (DevOps P1 Reproducibility)
- **`go mod tidy` on every change that touches imports:** removes unused deps and adds missing ones. A `go.mod` with stale entries breaks reproducibility.
- **`go.sum` committed and verified:** `go mod verify` checks the checksums of the module cache against `go.sum`. A drifted `go.sum` is a supply-chain signal.
- **Pinned major versions in `go.mod`:** `require github.com/x/y v1.2.3` pins the minor; a `v1.2.4` patch may auto-update. For applications, consider a `go.mod` proxy that pins to exact commits.
- **`go mod vendor` for hermetic CI:** vendoring `vendor/` into the repo means CI builds without network. The trade-off is repo size; the win is reproducibility (DevOps P1).
```bash
# CI build gate
go mod tidy
go mod verify
go build ./...
go test -race ./...
```
## Reproducible Builds (DevOps P1 Reproducibility, C3 Simplicity)
- **One Go toolchain version, pinned:** `goenv` or `asdf` pins the Go version per repo; a `.go-version` file declares it. A CI job that uses "latest" Go drifts.
- **`CGO_ENABLED=0` for static binaries:** a static binary runs in a scratch container with no libc dependency. Set in CI for all release builds.
- **`-trimpath` and `-ldflags='-s -w'` for reproducible output:** strips the build path from the binary and removes debug info. Two builds of the same commit produce byte-identical binaries.
```bash
# Reproducible release build
CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o app ./cmd/app
```
## Documentation in the Pipeline (Documentation P1 Documentation is Code, DevOps P9 Documentation in the Pipeline)
- **`go doc` from comments:** package comments and exported-symbol comments are the API docs; `go doc` and `pkg.go.dev` render them. Missing comments on exported symbols fail `revive`/`golint` (Documentation P1).
- **`// Example` functions are run by `go test`:** an `ExampleUser` function with `// Output:` is a tested artifact; a stale output fails the build.
- **`README.md` and `docs/` are built by `mkdocs` or similar:** the pipeline validates links and renders; a broken link fails CI (Documentation P1).
```go
// GetUser fetches a user by id.
//
// Example:
//
// u, err := GetUser(id)
// if err != nil { ... }
func GetUser(id UserId) (*User, error) { /* ... */ }
func ExampleGetUser() {
u, err := GetUser("abc")
fmt.Println(u, err)
// Output: <nil> not found
}
```
## Cross-References
- `domains/devops/ci-cd.md` — the pipeline gates that host vet/lint/test.
- `domains/devops/first-principles.md` — DevOps P1 Reproducibility, P2 Automation.
- `domains/concurrency/first-principles.md` — Concurrency P6 No Silent Races (`-race`).
- `domains/documentation/first-principles.md` — Documentation P1 Documentation is Code.
- `languages/go-types.md` — the type rules staticcheck enforces reference this doc.
- `languages/go-testing.md` — the `go test` flags (`-race`, `-count`) detailed here.
+140
View File
@@ -0,0 +1,140 @@
# Go Type System — Derived Application
> Applies Atelier's domain principles to Go's type system specifically.
> Derives from `domains/` docs; introduces no new P-rules (D-063).
> See `languages/go.md` for the language first-principles stub.
## Named Types for Domain Concepts (C1 Correctness, Data P7 Type Fidelity)
- **Named types for domain IDs and values:** `type UserId string`, `type OrderId string`. Two named types are distinct even with identical underlying types; the compiler rejects the swap.
- **Constructors validate at the boundary:** `func NewUserId(s string) (UserId, error)` returns an error on bad input. A bare `UserId(s)` cast bypasses validation — only the constructor is exported.
- **Applies `data/P7` (type fidelity):** a named type carries the domain meaning through the call graph; a `string` parameter does not.
- **`any` is the wide type; narrow before use:** Go 1.18+ `any` is an alias for `interface{}`. Use it only at true boundaries (e.g., `json.Unmarshal`); narrow with a type assertion immediately.
```go
type UserId string
type OrderId string
func NewUserId(s string) (UserId, error) {
if !regexp.MustCompile(`^[a-z0-9]+$`).MatchString(s) {
return "", fmt.Errorf("invalid user id: %q", s)
}
return UserId(s), nil
}
func GetUser(id UserId) (*User, error) { /* ... */ }
// GetUser("abc") // compile error: string is not UserId
// GetUser(OrderId("abc")) // compile error: distinct named types
```
## Generics (C6 Composability, Data P7 Type Fidelity)
- **Generics (1.18+) preserve element types across containers:** `type Repository[T any] struct { ... }` keeps `T` through `Get`/`Save`, rather than widening to `any`.
- **Constrain with `comparable` for map keys, custom interfaces for behavior:** `func dedupe[T comparable](s []T) []T` uses `comparable`; a `Sortable[T]` constraint expresses the `Less` requirement.
- **Avoid generics where an interface suffices:** `io.Reader` is not improved by generics. Generics are for type-preserving containers; interfaces are for behavior.
- **No generic methods on generic types (not supported):** `func (r Repository[T]) Map[U any](f func(T) U) Repository[U]` is a compile error. Use a free function.
```go
type Entity interface { ID() string }
type Repository[T Entity] struct {
db map[string]T
}
func (r *Repository[T]) Get(id string) (T, bool) {
var zero T
t, ok := r.db[id]
if !ok { return zero, false }
return t, true
}
func (r *Repository[T]) Save(t T) { r.db[t.ID()] = t }
```
## Interfaces (C6 Composability, API P1 Contract Fidelity)
- **Interfaces defined by the consumer, not the producer:** a package defines its dependencies as interfaces (`type Store interface { Get(id string) (*User, error) }`), and accepts implementations. The producer does not pre-declare "the interface I implement."
- **Small interfaces (Go proverb):** `io.Reader` is one method. An interface with 5+ methods is a god-object; split it.
- **Accept interfaces, return concrete types:** return a `*UserRepo`, accept a `Store`. The caller gets the implementation; the callee depends on the abstraction.
- **Applies `api/P1` (contract fidelity):** the interface is the contract; the concrete type is the implementation. Tests mock the interface, not the struct.
```go
// consumer defines the interface
type UserStore interface {
Get(id string) (*User, error)
}
type Service struct { store UserStore }
func NewService(s UserStore) *Service { return &Service{store: s} }
// producer returns concrete; satisfies UserStore implicitly
type UserRepo struct { db map[string]*User }
func (r *UserRepo) Get(id string) (*User, error) { return r.db[id], nil }
```
## Type Assertion Discipline (C1 Correctness, Errors P1 Errors are Data)
- **Type assertions return `(T, bool)` — use the bool:** `v, ok := x.(UserId)` distinguishes "wrong type" from "zero value." A bare `x.(UserId)` panics on mismatch.
- **`switch x := x.(type)` for multi-variant narrowing:** each case narrows `x` to the case type. The default case is exhaustive (no `never`-style check; Go relies on review).
- **Applies `errors/P1` (errors are data):** a failed type assertion is a value (`ok == false`), not an exception. Handle it as a branch, not a panic.
- **Never assert across module boundaries silently:** an assertion on a type from another package couples to its internals. Prefer an interface method.
```go
func describe(x any) string {
switch v := x.(type) {
case UserId:
return "user " + string(v)
case OrderId:
return "order " + string(v)
default:
return fmt.Sprintf("unknown: %T", v)
}
}
// safe form, never panic
id, ok := raw.(UserId)
if !ok {
return fmt.Errorf("expected UserId, got %T", raw)
}
```
## Error Types and errors.Is/As (Errors P1 Errors are Data, Errors P3 Fail Specifically)
- **Sentinel errors for known cases:** `var ErrNotFound = errors.New("not found")`; check with `errors.Is(err, ErrNotFound)`. The sentinel is a value, not an exception class.
- **Custom error types for context:** `type ValidationError struct { Field, Msg string }`; check with `var ve *ValidationError; errors.As(err, &ve)`. The type carries structured data (Errors P4 Preserve Context).
- **Wrap with `%w`:** `fmt.Errorf("get user %s: %w", id, err)` preserves the chain. `errors.Is`/`As` unwrap it. Bare `%v` breaks the chain.
- **Applies `errors/P3` (fail specifically):** `ErrNotFound` is specific; `ErrFailed` is not. The error type names the failure mode.
```go
var ErrNotFound = errors.New("not found")
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string { return e.Field + ": " + e.Msg }
func GetUser(id UserId) (*User, error) {
u, ok := db[string(id)]
if !ok {
return nil, fmt.Errorf("user %s: %w", id, ErrNotFound)
}
return u, nil
}
// caller
if errors.Is(err, ErrNotFound) { /* 404 */ }
var ve *ValidationError
if errors.As(err, &ve) { /* 422 with ve.Field */ }
```
## Cross-References
- `domains/data/schema-design.md` — named types parallel schema design at the Go boundary.
- `domains/data/first-principles.md` — Data P7 Type Fidelity is the primary trace.
- `domains/api/rest.md` — contract fidelity for HTTP handlers using interfaces.
- `domains/errors/patterns.md``errors.Is`/`As` and the wrap-with-`%w` pattern.
- `languages/go-concurrency.md` — typed channels carry these named types.
- `languages/go-testing.md` — table-driven tests assert type-swap safety.
+7
View File
@@ -2,6 +2,13 @@
> How Atelier's domain principles apply in Go specifically. Derives from `domains/` docs.
## Derived Docs
- [go-types.md](go-types.md) — named types, generics, interfaces, type assertion discipline.
- [go-tooling.md](go-tooling.md) — go vet, golangci-lint, go test -race, module discipline.
- [go-concurrency.md](go-concurrency.md) — goroutines, channels, context, select, sync primitives.
- [go-testing.md](go-testing.md) — table-driven tests, t.Parallel, t.Cleanup, race detector.
## Type System (C1 Correctness, Data P7 Type Fidelity)
- **Named types for domain concepts:** `type UserId string`, not bare `string`.
+117
View File
@@ -0,0 +1,117 @@
# Python Async — Derived Application
> Applies Atelier's domain principles to Python async specifically.
> Derives from `domains/` docs; introduces no new P-rules (D-063).
> See `languages/python.md` for the language first-principles stub.
## asyncio and anyio (Concurrency P7 Cancellation Support, C2 Clarity)
- **`asyncio` for I/O-bound work; threads only for blocking libraries:** `async def` + `await` for network/disk; `run_in_executor` to wrap a blocking call. Mixing threads for I/O is the wrong default.
- **`anyio` for runtime portability:** `anyio` abstracts asyncio/trio; a library written against `anyio` runs on either. Use it for libraries; for applications, asyncio directly is fine.
- **One event loop, one thread:** `asyncio.run(main())` creates and runs the loop. Do not call `asyncio.run` inside an existing loop (raises `RuntimeError`); do not share a loop across threads.
- **Applies `concurrency/P7`:** every `async def` accepts cancellation as a first-class signal; `CancelledError` propagates unless explicitly suppressed (and suppressing it is almost always a bug).
```python
import asyncio
import anyio
async def fetch_user(id: str) -> User:
return await api.get(f'/users/{id}')
# asyncio application
async def main():
user = await fetch_user('abc')
asyncio.run(main())
# anyio library — portable across asyncio/trio
async def fetch_all(ids: list[str]) -> list[User]:
return await anyio.gather(*(fetch_user(i) for i in ids))
```
## Structured Concurrency (Concurrency P1 Immutability by Default, C6 Composability)
- **`asyncio.TaskGroup` (3.11+) for structured concurrency:** tasks created in a `TaskGroup` are awaited or cancelled together on exit. No orphan tasks outlive the block.
- **No `asyncio.gather(..., return_exceptions=False)` for fallible tasks:** `gather` returns partial results on first exception; `TaskGroup` cancels siblings and propagates the error atomically. Use `TaskGroup` for new code.
- **Applies `concurrency/P1` (immutability):** tasks share only immutable inputs; results are collected, not mutated in place. A task that writes to a shared list is a race waiting to happen.
- **`anyio.create_task_group()` mirrors `TaskGroup` cross-runtime:** same structured-concurrency guarantee, portable.
```python
import asyncio
async def fetch_all(ids: list[str]) -> list[User]:
results: list[User] = []
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(fetch_user(i)) for i in ids]
# all tasks done (or cancelled) by here
return [t.result() for t in tasks]
```
## Cancellation and Timeout (Concurrency P7 Cancellation Support, Concurrency P8 Timeout Discipline)
- **`asyncio.wait_for(coro, timeout)` for a deadline:** every external `await` races against a timeout. A bare `await` is an unbounded wait (Concurrency P8).
- **`asyncio.timeout()` (3.11+) as a context manager:** `async with asyncio.timeout(5): await op` — cleaner than `wait_for` for multi-await blocks.
- **`CancelledError` propagates; do not catch broadly:** `except Exception` swallows `CancelledError` in 3.7 (it was `BaseException`); in 3.8+ it's `BaseException` and `except Exception` skips it. Catch specifically, never bare `except:`.
- **Applies `concurrency/P7`:** cancellation is cooperative — a long synchronous block inside `async def` ignores cancellation. Yield with `await asyncio.sleep(0)` periodically in CPU-bound loops.
```python
import asyncio
async def fetch_with_timeout(id: str, timeout: float = 5.0) -> User:
async with asyncio.timeout(timeout):
return await fetch_user(id)
async def shutdown(token: asyncio.Event) -> None:
# cooperative cancel — long-running loop checks the token
while not token.is_set():
await do_chunk()
await asyncio.sleep(0) # yield so cancel can land
```
## Bounded Concurrency and Queues (Concurrency P9 Bounded Queues)
- **`asyncio.Semaphore(N)` to bound in-flight tasks:** a `Semaphore(8)` wrapping `gather` caps concurrency. Unbounded `gather` on a 10k-item list exhausts file descriptors (Concurrency P9 — bounded queues).
- **`asyncio.Queue(maxsize=N)` for producer/consumer:** a bounded queue applies backpressure to the producer. An unbounded queue lets the producer run ahead and OOM.
- **Applies `messaging/queues`:** an `asyncio.Queue` is an in-process broker — the same bounded-queue / backpressure semantics apply; the broker is just local.
```python
import asyncio
async def map_bounded(items: list[str], limit: int = 8) -> list[User]:
sem = asyncio.Semaphore(limit)
async def guarded(i: str) -> User:
async with sem:
return await fetch_user(i)
return await asyncio.gather(*(guarded(i) for i in items))
```
## Error Handling in Async (Errors P5 Recoverable When Possible, Errors P1 Errors are Data)
- **Retry with backoff for transient failures:** network blips are recoverable (Errors P5). Exponential backoff with jitter, capped retries, and an `anyio`-cancellation-aware `sleep`.
- **No retry for non-idempotent operations:** a `POST` creating a resource is not safely retryable without an idempotency key (applies `api/P6` Idempotency).
- **`except asyncio.CancelledError: raise`** is the only valid handling — re-raise so the cancellation propagates. Catching and continuing breaks structured concurrency.
- **Applies `messaging/delivery-semantics`:** a cancelable async operation is at-most-once; retry-on-cancel is at-least-once. The caller must declare which.
```python
import anyio
import random
async def fetch_retry(id: str, attempts: int = 3) -> User:
for i in range(attempts):
try:
return await fetch_user(id)
except (TimeoutError, ConnectionError):
if i == attempts - 1:
raise
await anyio.sleep((2 ** i) * 0.1 + random.random() * 0.1)
raise RuntimeError('unreachable')
```
## Cross-References
- `domains/concurrency/patterns.md` — the cancellation/timeout/semaphore patterns applied here.
- `domains/concurrency/first-principles.md` — Concurrency P1 Immutability, P7 Cancellation Support, P8 Timeout Discipline, P9 Bounded Queues.
- `domains/messaging/queues.md``asyncio.Queue` as an in-process broker; backpressure parallels (IDEATE-40).
- `domains/errors/patterns.md` — typed async errors and retry-with-backoff.
- `languages/py-types.md``Result` and exception hierarchy used in async error handling.
- `languages/py-tooling.md``pytest-asyncio` config that runs these tests.
+111
View File
@@ -0,0 +1,111 @@
# Python Testing — Derived Application
> Applies Atelier's domain principles to Python testing specifically.
> Derives from `domains/` docs; introduces no new P-rules (D-063).
> See `languages/python.md` for the language first-principles stub.
## pytest and Spec-Driven Tests (Testing P1 Tests as Specification, C2 Clarity)
- **`pytest` is the default; `unittest` only for stdlib-only libraries:** `pytest` fixtures, parametrize, and assertion rewriting beat `unittest`'s boilerplate (Clarity C2).
- **Tests co-located with source:** `user.py``test_user.py`. A test far from its subject rots (Documentation P5 Discoverability).
- **Test names read as a spec:** `def test_create_user_rejects_invalid_email():` — a reader understands the unit from the name. Avoid `def test_user1():`.
- **`assert` over `self.assertEqual`:** pytest rewrites `assert` to show the failing values; `assertEqual` is unittest's escape hatch and loses readability.
- **Applies `Testing P1`:** the test is a specification; the failure message is the spec violation.
```python
# test_user.py
import pytest
from user import create_user, ValidationError
def test_create_user_rejects_invalid_email():
with pytest.raises(ValidationError):
create_user(email='not-an-email')
def test_create_user_returns_persisted_id():
u = create_user(email='a@b.co')
assert u.id # truthy persisted id
```
## Factories and Fixture Discipline (Testing P2 Independence, Testing P7 Realism)
- **`factory_boy` or `pytest-factoryboy` over shared fixtures for mutable state:** `UserFactory.build()` returns a fresh object per call; a session-scoped fixture mutated across tests couples them (Testing P2 Independence).
- **Fixtures for setup/teardown, factories for data:** a `db` fixture sets up the DB once per test; a `make_user` factory produces fresh data per assertion. Conflating them produces order-dependent tests.
- **`scope='function'` is the default and the safe default:** `scope='session'` for read-only resources (a schema migration), never for mutable state.
- **Mock at the boundary, not the unit:** `mocker.patch('requests.get')` for HTTP; do not patch `user.User.save` (that mocks the unit under test — Testing P7 realism).
```python
import factory
from user import User
class UserFactory(factory.Factory):
class Meta:
model = User
email = factory.Sequence(lambda n: f'u{n}@b.co')
name = 'Test User'
def test_user_factory_is_fresh():
u1 = UserFactory.build()
u2 = UserFactory.build()
assert u1.email != u2.email # independent
```
## Parametrize and Edge Cases (Testing P9 Edge Case Coverage, Testing P3 Determinism)
- **`@pytest.mark.parametrize` for input tables:** one parametrized test runs N cases; each is an independent test with its own name and failure output (Testing P9).
- **Edge cases as rows, not special tests:** empty list, `None`, max int, unicode — each a row. An ad-hoc `test_handles_edge` with multiple asserts hides which case failed (Testing P6 Failure Specificity).
- **`pytest --randomly` catches order coupling:** a test passing alone but failing in a suite has hidden shared state. The random plugin makes it visible (Testing P2 Independence).
- **Property tests via `hypothesis`:** for invariants (e.g., "parse(serialize(x)) == x"), `hypothesis` generates hundreds of inputs and shrinks failures to a minimal counterexample.
```python
import pytest
@pytest.mark.parametrize('email, reason', [
('', 'empty'),
('a' * 1000 + '@b.co', 'too long'),
('no-at-sign', 'missing @'),
('a@b', 'missing TLD'),
])
def test_create_user_rejects(email, reason):
with pytest.raises(ValidationError):
create_user(email=email)
```
## Determinism and Time (Testing P3 Determinism, Testing P9 Edge Case Coverage)
- **No `datetime.now()`, `time.time()`, `uuid.uuid4()`, `random.random()` in code under test:** inject a `Clock`, `UUIDGen`, `Random` port. In tests, provide deterministic fakes.
- **`freezegun` for time:** `@freeze_time('2024-01-01')` makes `datetime.now()` deterministic. Do not call `datetime.now()` directly in code — wrap it in a `Clock` port so production and tests both inject.
- **`pytest --randomly-seed=last` to reproduce a failing order:** when `--randomly` finds an order bug, the seed is logged; re-run with it to debug deterministically.
```python
from freezegun import freeze_time
@freeze_time('2024-01-01')
def test_user_has_created_at():
u = create_user(email='a@b.co')
assert u.created_at.year == 2024
```
## Async Tests (Concurrency P10 Test for Race Conditions, Testing P1 Tests as Specification)
- **`pytest-asyncio` (or `anyio`'s pytest plugin) for `async def` tests:** `@pytest.mark.asyncio` runs the coroutine on a loop. Without it, an `async def` test is silently skipped (returns a coroutine, never awaited).
- **`anyio`'s plugin runs the same test on asyncio and trio:** one parametrized run across both runtimes catches runtime-specific bugs.
- **Race-sensitive tests use `--randomly` and bounded concurrency:** a `Semaphore(1)` test under random order surfaces hidden state.
- **Applies `concurrency/P10`:** async tests are the race detector's first line — if a test passes alone but fails under `gather` of N, there's a race.
```python
import pytest
@pytest.mark.asyncio
async def test_async_fetch_returns_user():
u = await fetch_user('abc')
assert u.email
```
## Cross-References
- `domains/testing/pyramid.md` — where unit/integration/property tests sit; hypothesis is the property layer.
- `domains/testing/fixtures.md` — factory-vs-fixture discipline applied via `factory_boy`.
- `domains/testing/first-principles.md` — Testing P1 Specification, P2 Independence, P3 Determinism, P9 Edge Coverage.
- `languages/py-types.md` — the `Result` and Pydantic models that tests assert.
- `languages/py-async.md` — async tests use the cancellation/timeout patterns from that doc.
- `languages/py-tooling.md` — the `pyproject.toml [tool.pytest]` config that runs these tests.
+99
View File
@@ -0,0 +1,99 @@
# Python Tooling — Derived Application
> Applies Atelier's domain principles to Python tooling specifically.
> Derives from `domains/` docs; introduces no new P-rules (D-063).
> See `languages/python.md` for the language first-principles stub.
## ruff for Lint and Format (DevOps P2 Automation, C2 Clarity)
- **`ruff` replaces flake8 + black + isort + pyupgrade:** one tool, one config, one order of magnitude faster. Format is not debated in review (Clarity C2).
- **Rule selection is principled, not "everything":** `select = ["E", "F", "I", "UP", "B", "SIM"]` — each rule group has a one-line `# reason:` in `pyproject.toml`. Rules without a rationale are noise (Documentation P1 — docs are code).
- **`ruff format` is the formatter, `ruff check` is the linter:** run both in CI; the formatter is deterministic, the linter surfaces smells.
- **Applies `devops/P2`:** the format/lint gate runs on every push; a developer never waits for a reviewer to comment on style.
```toml
# pyproject.toml
[tool.ruff]
target-version = "py311"
line-length = 100
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
# reason: E/F = pyflakes+pycodestyle; I = isort; UP = pyupgrade; B = bugbear; SIM = simplification
[tool.ruff.format]
quote-style = "double"
```
## mypy and Type-Check Gate (DevOps P2 Automation, Data P7 Type Fidelity)
- **`mypy --strict` in CI, not in the editor:** strict flags (`disallow_untyped_defs`, `no_implicit_optional`, `warn_return_any`) are the floor. The editor runs a relaxed mypy for speed; CI runs strict as the gate.
- **`pyright` for stricter/async-aware checking:** pyright understands `async` better and reports faster; mypy is the standard. Pick one as the gate, run the other as informational.
- **Per-module overrides only with a tracked reason:** `[[tool.mypy.overrides]] module = "legacy.*" ignore_errors = true` — each override block links to a ticket. Untracked overrides accumulate into a permanently untyped core.
- **`py.typed` marker for libraries:** ships the type info to consumers. Without it, downstream mypy treats the library as `Any`.
```bash
# CI gate
mypy --strict src/
pyright src/ || true # informational
```
## Dependency Management: poetry and uv (DevOps P1 Reproducibility)
- **`poetry` or `uv` for lockfile discipline:** both produce a deterministic lock (`poetry.lock` / `uv.lock`). `pip install` alone does not — it resolves at install time, producing different trees across machines.
- **`uv` for speed (Rust-based, 10100x faster):** newer tool, same lockfile semantics. Either is acceptable; do not mix within a repo.
- **Lockfile committed for applications:** for libraries, commit the lock for CI reproducibility even though consumers resolve their own tree.
- **`--frozen` install in CI:** `poetry install --no-dev --frozen` fails if the lock is out of sync. Prevents a "works on my machine" drift.
```bash
# CI install — deterministic
uv sync --frozen --no-dev
# or
poetry install --no-dev --frozen
```
## Virtualenv Discipline (DevOps P1 Reproducibility, C3 Simplicity)
- **One virtualenv per project, never the system Python:** `uv venv` or `python -m venv .venv`. System Python drift breaks reproducibility.
- **`uv` creates and pins the Python version:** `uv venv --python 3.12` ensures the same interpreter across machines. A pinned Python is part of the reproducibility contract, not just the lockfile.
- **No `pip install` into the system Python in CI:** use `uv`/`poetry`'s venv. A CI step that mutates system Python makes the next job non-hermetic.
```bash
uv venv --python 3.12
source .venv/bin/activate
uv pip install -r requirements.txt
```
## Documentation in the Pipeline (Documentation P1 Documentation is Code, DevOps P9 Documentation in the Pipeline)
- **`mkdocs` + `mkdocstrings` from docstrings:** API docs are generated from `google`- or `numpy`-style docstrings; the build fails on missing docstrings for public symbols (Documentation P1).
- **`doctest` blocks in docstrings are run by pytest:** a `>>>` example is a tested artifact; a stale example fails the build (Documentation P1, Testing P1).
- **`pyproject.toml` is the single source of tool config:** ruff, mypy, pytest, poetry all read from it. Do not scatter `.flake8`, `setup.cfg`, `mypy.ini`. One config file is one place to look (Clarity C2).
```python
def get_user(id: UUID) -> User:
"""Fetch a user by id.
Args:
id: the user's UUID.
Returns:
The User.
Raises:
NotFoundError: if the user does not exist.
Example:
>>> get_user(UUID('intentional-example-uuid'))
User(...)
"""
...
```
## Cross-References
- `domains/devops/ci-cd.md` — the pipeline gates that host ruff/mypy/poetry.
- `domains/devops/first-principles.md` — DevOps P1 Reproducibility, P2 Automation.
- `domains/documentation/first-principles.md` — Documentation P1 Documentation is Code.
- `languages/py-types.md` — the type rules mypy enforces reference this doc.
- `languages/py-testing.md` — the pytest config (`pyproject.toml [tool.pytest]`) detailed here.
+111
View File
@@ -0,0 +1,111 @@
# Python Type System — Derived Application
> Applies Atelier's domain principles to Python's type system specifically.
> Derives from `domains/` docs; introduces no new P-rules (D-063).
> See `languages/python.md` for the language first-principles stub.
## Type Hints and Gradual Typing (C1 Correctness, Data P7 Type Fidelity)
- **Type hints on every function signature:** `def get_user(id: UUID) -> User | None:`. Hints are annotations, not enforcement, but `mypy`/`pyright` make them a build gate.
- **Gradual typing is opt-in, not opt-out:** start with `--strict` on a package, fix the errors, then expand. A repo-wide `# type: ignore` is a gradual-typing failure.
- **`from __future__ import annotations` for forward refs:** all annotations are strings until resolved, so `class User: ...` referencing `User` works without quotes in 3.10+.
- **`Any` disables the checker; `object` is the wide type:** `Any` allows any operation; `object` requires narrowing. Use `object` for opaque inputs (e.g., `json.loads` return).
- **Applies `data/P7` (type fidelity):** a hint is the contract; the checker verifies it. A missing hint is a missing contract.
- **Pydantic for runtime validation at the boundary:** hints on `BaseModel` fields are validated at construction, catching bad input from network/config before it deepens into the system.
- **`extra='forbid'` by default:** Pydantic allows extra fields silently; forbid them to surface schema drift (e.g., a client sending a typo'd field name).
- **Custom types via `Annotated` with validators:** `Email = Annotated[str, validate_email]` keeps the type readable and the validator attached to the type, not the model.
```python
from pydantic import BaseModel, ConfigDict
from uuid import UUID
class UserCreate(BaseModel):
model_config = ConfigDict(extra='forbid')
email: str
name: str
class User(UserCreate):
id: UUID
```
## Pydantic and Schema Fidelity (C1 Correctness, API P1 Contract Fidelity, Data P7 Type Fidelity)
- **Pydantic models are the API contract:** a FastAPI handler taking `UserCreate` rejects malformed JSON with a 422 before the body runs. This is `api/P1` (contract fidelity) at the type boundary.
- **`ConfigDict(extra='forbid')` rejects unknown fields:** silently accepting extras is a contract leak — the server appears to handle fields it ignores.
- **Validators raise `ValueError`, not `Exception`:** Pydantic converts `ValueError` to a validation error response; a generic `Exception` becomes a 500 and hides the input bug.
- **Applies `api/P1`:** the model is the source of truth; the OpenAPI schema is generated from it, not hand-written. Drift between schema and code is impossible.
```python
from typing import Annotated
from pydantic import BaseModel, Field, StringConstraints
EmailStr = Annotated[str, StringConstraints(pattern=r'^[^@\s]+@[^@\s]+$')]
class Login(BaseModel):
email: EmailStr
password: Annotated[str, Field(min_length=8)]
```
## Errors as Data (Errors P1 Errors are Data, C1 Correctness)
- **`Union[T, Error]` over `Optional[T]` for expected failures:** `Optional[User]` cannot distinguish "not found" from "permission denied". A discriminated `Result` carries the cause.
- **Custom exception hierarchy rooted at `AppError`:** `class NotFoundError(AppError)` etc. — callers can `except AppError` for the broad case, or a specific subclass for handling.
- **`raise` for exceptional paths, `return Result` for expected:** "user not found" is expected (a `Result`); "DB connection lost" is exceptional (a `raise`). Conflating them makes error handling a guess.
- **Applies `errors/P1`:** errors are values, not control-flow magic. A `Result` type encodes this at the type level even where exceptions are the runtime mechanism.
```python
from dataclasses import dataclass
from typing import Generic, TypeVar, Union
T = TypeVar('T')
@dataclass(frozen=True)
class Ok(Generic[T]):
value: T
@dataclass(frozen=True)
class Err:
error: Exception
Result = Union[Ok[T], Err]
def find_user(id: UUID) -> Result[User]:
row = db.get(id)
if row is None:
return Err(NotFoundError(f'user {id}'))
return Ok(User.from_row(row))
```
## Generics and Protocols (C6 Composability, C1 Correctness)
- **`Protocol` for structural typing (PEP 544):** a `Repository` protocol defines `get`/`save` without requiring an inheritance hierarchy; any class matching the shape satisfies it.
- **`TypeVar` with bounds for generic functions:** `T = TypeVar('T', bound=Entity)` lets `serialize(t: T) -> dict` access `t.id`.
- **`Generic[T]` for container types:** a typed `Repository[T]` preserves the element type across `get`/`save`, rather than widening to `Any`.
- **`@overload` for callable overloads:** `def parse(s: str) -> int: ...` vs `def parse(s: bytes) -> int: ...` — the runtime body is one function; the overloads are the type contract.
```python
from typing import Protocol, TypeVar
T = TypeVar('T')
class Repository(Protocol[T]):
def get(self, id: str) -> T | None: ...
def save(self, t: T) -> None: ...
class UserRepo:
def get(self, id: str) -> User | None: ...
def save(self, u: User) -> None: ...
def use_repo(r: Repository[User]) -> None:
u = r.get('abc') # type: User | None
```
## Cross-References
- `domains/data/schema-design.md` — Pydantic models parallel schema design at the TS/JSON boundary.
- `domains/data/first-principles.md` — Data P7 Type Fidelity is the primary trace for this doc.
- `domains/api/rest.md` — contract fidelity for FastAPI handlers consuming Pydantic models.
- `domains/errors/patterns.md` — the `Result` discriminated union as error-as-data encoding.
- `languages/py-async.md` — typed async results built on the `Result` union here.
- `languages/py-tooling.md` — the `mypy`/`pyright` config that enforces these hints.
+7
View File
@@ -2,6 +2,13 @@
> How Atelier's domain principles apply in Python specifically. Derives from `domains/` docs.
## Derived Docs
- [py-types.md](py-types.md) — type hints + Pydantic, mypy/pyright, gradual typing.
- [py-tooling.md](py-tooling.md) — ruff, mypy, poetry, uv, virtualenv discipline.
- [py-async.md](py-async.md) — asyncio, anyio, cancellation, structured concurrency.
- [py-testing.md](py-testing.md) — pytest, factory_boy, fixture discipline, parametrize.
## Type System (C1 Correctness, Data P7 Type Fidelity)
- **Type hints on every function:** `def get_user(id: UUID) -> User | None:`.
+129
View File
@@ -0,0 +1,129 @@
# Rust Async — Derived Application
> Applies Atelier's domain principles to Rust async specifically.
> Derives from `domains/` docs; introduces no new P-rules (D-063).
> See `languages/rust.md` for the language first-principles stub.
## tokio and the Async Runtime (Concurrency P5 Lock Minimization, C6 Composability)
- **`tokio` is the default async runtime:** `#[tokio::main]` for the entry; `tokio::spawn` for a task. The runtime owns the reactor, the I/O driver, and the timer.
- **`tokio::spawn` returns a `JoinHandle` like `std::thread::spawn`:** a dropped `JoinHandle` detaches (the task keeps running); `await` the handle to join. Prefer await to detach.
- **`tokio::task::JoinSet` for structured concurrency:** a set of tasks awaited together; on drop, all remaining tasks are cancelled. Mirrors `errgroup`/`TaskGroup` semantics.
- **`runtime` features are explicit:** `tokio = { version = "1", features = ["full"] }` for a binary; `["rt", "rt-multi-thread", "macros"]` for a library. Pulling `full` into a library bloats downstream.
```rust
#[tokio::main]
async fn main() {
let mut set = tokio::task::JoinSet::new();
for id in ["a", "b", "c"] {
set.spawn(fetch_user(id.to_string()));
}
while let Some(res) = set.join_next().await {
match res {
Ok(Ok(u)) => println!("{}", u.name),
Ok(Err(e)) => eprintln!("err: {e}"),
Err(join_err) => eprintln!("panic: {join_err}"),
}
}
}
```
## Async Traits (Concurrency P7 Cancellation Support, C6 Composability)
- **`async fn` in traits stabilized in Rust 1.75:** `trait Repo { async fn get(&self, id: &str) -> Result<User, Error>; }`. No `async-trait` crate needed for new code on recent toolchains.
- **`Box<dyn Trait>` with async methods needs `dyn`-compatibility:** the returned future is `Pin<Box<dyn Future>>`; the compiler boxes it. For hot paths, use generics (`impl Trait`) over `dyn`.
- **`async-trait` crate for older toolchains:** macro that desugars to a `Pin<Box<dyn Future>>`. Migrate to native `async fn in trait` when the toolchain allows.
- **`Send` bounds on async traits for cross-thread spawn:** `trait Repo: Send { async fn get(&self, id: &str) -> Result<User, Error>; }` — the returned future must be `Send` to spawn on a multi-thread runtime.
```rust
trait UserRepo: Send + Sync {
async fn get(&self, id: &str) -> Result<User, Error>;
}
struct PgRepo { pool: PgPool }
impl UserRepo for PgRepo {
async fn get(&self, id: &str) -> Result<User, Error> {
sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
.bind(id).fetch_one(&self.pool).await.map_err(Error::from)
}
}
```
## Cancellation (Concurrency P7 Cancellation Support, Concurrency P8 Timeout Discipline)
- **Cancellation is cooperative via dropping the future:** `tokio::select!` drops the unselected branch, cancelling it. A dropped future stops at its next `.await` point.
- **`tokio::time::timeout` for a deadline:** `timeout(Duration::from_secs(5), op).await` returns `Ok(Ok(v))` on success, `Ok(Err(e))` on inner error, `Err(Elapsed)` on timeout. Every external `await` races against a deadline (Concurrency P8).
- **`tokio::select!` for cancel-aware waits:** `select! { res = op => res, _ = cancel => return Err(Cancelled), }`. The unselected branch is dropped, cancelling it.
- **Cancellation is not atomic:** a future dropped mid-`await` may have partial state. `Drop` runs on cancellation; clean up there (e.g., rollback a transaction).
- **Applies `concurrency/P7`:** cancellation is a first-class signal; the runtime propagates it via drop. No `CancelledError` to catch — the future is gone.
```rust
use tokio::time::timeout;
use std::time::Duration;
async fn fetch_with_timeout(url: &str) -> Result<Response, Error> {
match timeout(Duration::from_secs(5), fetch(url)).await {
Ok(Ok(r)) => Ok(r),
Ok(Err(e)) => Err(e.into()),
Err(_elapsed) => Err(Error::Timeout),
}
}
async fn cancellable(op: impl Future<Output=()>, mut cancel: tokio::sync::oneshot::Receiver<()>) {
tokio::select! {
_ = op => {},
_ = &mut cancel => println!("cancelled"),
}
}
```
## Pin and Self-Referential Futures (Concurrency P5 Lock Minimization, C1 Correctness)
- **`async fn` returns a `Future` that is often self-referential:** the generated state machine may hold a borrow into its own stack. Such a future must be `Pin`ned to move safely.
- **`Pin<Box<T>>` to box and pin:** `Box::pin(async { ... })` returns a `Pin<Box<dyn Future>>`. The cost is a heap alloc; the win is `Send`/`dyn`-compatibility.
- **`Pin<&mut T>` for in-place polling:** `Pin::new(&mut fut)` pins a stack future; the borrow checker prevents moving it. Use for stack-allocated futures in `select!`.
- **Do not `unsafe` unpin:** `Pin::get_unchecked_mut` opts out of the pin guarantees. Application code never needs it; library code uses it for `poll` implementations.
```rust
use std::pin::Pin;
async fn boxed() -> Pin<Box<dyn std::future::Future<Output = ()> + Send>> {
Box::pin(async {
// self-referential state machine is safe to move once pinned
})
}
```
## Bounded Channels and Backpressure (Concurrency P9 Bounded Queues)
- **`tokio::sync::mpsc::channel(N)` is bounded:** `send().await` blocks when full (backpressure, Concurrency P9). Unbounded `unbounded_channel()` lets the producer run ahead and OOM.
- **`tokio::sync::mpsc::Sender::try_send` for non-blocking send:** returns `Err(TrySendError::Full(v))` when full; the caller decides to drop, log, or back off. A bounded queue + `try_send` is the backpressure-aware pattern.
- **`tokio::sync::broadcast` for fan-out:** multiple receivers each get a copy; a slow receiver misses (lag). Use for telemetry, not for commands.
- **Applies `messaging/queues`:** a bounded tokio channel is an in-process broker — bounded buffer, backpressure, at-most-once handoff. The same semantics apply; the broker is local.
```rust
use tokio::sync::mpsc;
async fn producer(tx: mpsc::Sender<Job>) {
for j in jobs() {
if tx.send(j).await.is_err() { return; } // receiver dropped
}
}
async fn consumer(rx: mpsc::Receiver<Job>) {
while let Some(j) = rx.recv().await {
process(j).await;
}
}
let (tx, rx) = mpsc::channel::<Job>(16); // bounded: backpressure
```
## Cross-References
- `domains/concurrency/patterns.md` — the cancellation/timeout/semaphore patterns applied here.
- `domains/concurrency/first-principles.md` — Concurrency P5 Lock Minimization, P7 Cancellation Support, P8 Timeout Discipline, P9 Bounded Queues.
- `domains/messaging/delivery-semantics.md` — at-most-once vs at-least-once framing for async retry/cancel (IDEATE-40).
- `languages/rs-ownership.md``Send`/`Sync` bounds on futures build on the ownership model here.
- `languages/rs-tooling.md``tokio` feature flags and the `cargo` build profiles detailed there.
- `languages/rs-testing.md``#[tokio::test]` and async test patterns.
+136
View File
@@ -0,0 +1,136 @@
# Rust Ownership — Derived Application
> Applies Atelier's domain principles to Rust's ownership model specifically. Rust's distinctive strength (Send/Sync, lifetimes, borrowing) earns a dedicated ownership doc rather than an `rs-types.md`.
> Derives from `domains/` docs; introduces no new P-rules (D-063).
> See `languages/rust.md` for the language first-principles stub.
## Ownership and Move Semantics (Concurrency P1 Immutability by Default, C1 Correctness)
- **Ownership is unique:** at any time, exactly one owner holds a value. Assignment passes ownership (`let y = x;``x` is moved, not copied). The compiler rejects use-after-move.
- **`Copy` types (integers, `bool`, `&T`) duplicate on assignment; everything else moves.** A `struct` is `Copy` only if all fields are; opt in via `#[derive(Copy, Clone)]` only for small, cheap-to-copy types.
- **Pass by `&T` for read-only, `&mut T` for mutation:** a borrow does not transfer ownership; the caller retains the value after the callee returns.
- **Applies `concurrency/P1` (immutability by default):** `&T` is shared and immutable; `&mut T` is exclusive and mutable. The compiler enforces "one or many, never both" — aliasing XOR mutation, statically.
```rust
let s = String::from("hello");
let t = s; // s moved into t
// println!("{}", s); // error: use of moved value
let n = 5;
let m = n; // i32 is Copy: n still usable
println!("{} {}", n, m);
```
## Borrowing and Lifetimes (C1 Correctness, Data P7 Type Fidelity, Concurrency P3 Boundaries are Locks)
- **`&'a T` ties a borrow to a lifetime `'a`:** the borrow cannot outlive the owner. Lifetimes are static — the compiler rejects dangling references.
- **Lifetime elision when unambiguous:** `fn first<'a>(s: &'a str) -> &'a str` is elided to `fn first(s: &str) -> &str` (one input → output lifetime). When ambiguous, name the lifetime.
- **`'static` is the longest lifetime (the whole program):** not "until I drop it." Use `'static` only for values that genuinely live forever (string literals, `const`s); leaking to `'static` to satisfy the checker is a bug.
- **`Ref<'a, T>` and `RefMut<'a, T>` from `RefCell` are runtime-checked borrows:** the borrow rules still apply, checked at runtime instead of compile time. A second `RefMut` panics.
- **Applies `concurrency/P3` (boundaries are locks):** `&mut T` is the compile-time lock — exclusive access is the boundary; no runtime mutex needed for single-threaded aliasing discipline.
```rust
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b } // borrow tied to both inputs
}
fn dangling() -> &str { // compile error: missing lifetime
let s = String::from("local");
&s // error: s drops at end of fn
}
```
## Send and Sync (Concurrency P1 Immutability by Default, Concurrency P3 Boundaries are Locks, C1 Correctness)
- **`Send`:** a type `T: Send` may be moved across thread boundaries. Most types are `Send`; `Rc<T>` is not (shared non-atomically refcounted).
- **`Sync`:** a type `T: Sync` may be shared (`&T`) across threads. `RefCell<T>` is `!Sync` (interior mutability without atomics); `Mutex<T>` is `Sync` (it synchronizes).
- **The compiler enforces `Send`/`Sync` at the thread-spawn boundary:** `std::thread::spawn(move || { ... })` requires the closure's captures to be `Send`.
- **Applies `concurrency/P1` and `concurrency/P3`:** `Send` is the move-across-boundary contract; `Sync` is the share-across-boundary contract. Data races are a compile error, not a runtime detector. This is Rust's distinctive strength over Go's race detector.
```rust
use std::rc::Rc;
use std::sync::Arc;
let rc = Rc::new(5);
// std::thread::spawn(move || { println!("{}", rc) }); // error: Rc is !Send
let arc = Arc::new(5);
std::thread::spawn(move || { println!("{}", arc) }); // ok: Arc<T> is Send+Sync
```
## Shared Mutation: Arc, Mutex, RwLock (Concurrency P3 Boundaries are Locks, Concurrency P5 Lock Minimization)
- **`Arc<T>` for shared ownership across threads:** atomic refcounted. Clone increases the count; the last drop frees `T`.
- **`Mutex<T>` for exclusive mutation across threads:** `lock()` blocks until exclusive; the guard `MutexGuard<T>` derefs to `&mut T` and releases on drop.
- **`RwLock<T>` for read-heavy, `Mutex<T>` for write-heavy:** RwLock allows multiple readers or one writer. For most cases, `Mutex` is simpler and faster; prefer it unless reads dominate by 10x+.
- **Hold the lock for the smallest scope:** `let g = m.lock().unwrap();` then drop `g` before I/O. RAII releases on scope exit; explicit `drop(g)` clarifies intent.
- **Applies `concurrency/P5` (lock minimization):** prefer message passing (`mpsc` channels) over locks. When a lock is needed, scope it minimally.
```rust
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut g = c.lock().unwrap();
*g += 1;
// g drops here, lock released
}));
}
for h in handles { h.join().unwrap(); }
println!("{}", *counter.lock().unwrap());
```
## Interior Mutability (Concurrency P1 Immutability by Default, C1 Correctness)
- **`Cell<T>` for `Copy` types, `RefCell<T>` for non-`Copy`:** interior mutability moves the borrow check from compile time to runtime. `RefCell::borrow_mut()` panics on a second mutable borrow.
- **`Mutex<T>`/`RwLock<T>` for thread-safe interior mutability:** the runtime check is the lock, not a panic. Use these across threads; `RefCell` only single-threaded.
- **`UnsafeCell<T>` is the primitive; never use directly:** `Cell`, `RefCell`, `Mutex` are safe wrappers. Direct `UnsafeCell` is `unsafe` and opts out of the aliasing guarantee.
- **Applies `concurrency/P1`:** interior mutability is the exception, not the default. Reach for it when an API must present `&self` while mutating internally (e.g., a cache); document why.
```rust
use std::cell::RefCell;
struct Cache {
inner: RefCell<HashMap<String, User>>,
}
impl Cache {
fn get(&self, id: &str) -> Option<User> {
// &self (immutable) but mutates internally
self.inner.borrow_mut().entry(id.to_string()).or_insert_with(|| fetch()).clone()
}
}
```
## Drop and RAII (C1 Correctness, Concurrency P3 Boundaries are Locks)
- **`Drop` runs when the owner goes out of scope:** no `defer`, no `finally`. A `MutexGuard` releases, a `File` closes, a `JoinHandle`... does not join (a dropped `JoinHandle` detaches).
- **`Drop` is deterministic:** it runs at scope exit, not GC time. This is why `Arc`'s refcount is precise and `Mutex` release is timely.
- **`ManuallyDrop<T>` to opt out:** for FFI types whose destructor you must call manually. Rare in application code; common in `unsafe` bindings.
- **`Drop` order: fields in declaration order, then the struct itself.** A field that another field's `Drop` depends on must be declared last.
```rust
struct Resource { name: String }
impl Drop for Resource {
fn drop(&mut self) {
println!("dropping {}", self.name); // runs at scope end
}
}
fn main() {
let _r = Resource { name: "x".into() };
// _r drops here, prints "dropping x"
}
```
## Cross-References
- `domains/concurrency/first-principles.md` — Concurrency P1 Immutability, P3 Boundaries are Locks, P5 Lock Minimization.
- `domains/data/first-principles.md` — Data P7 Type Fidelity (lifetimes are the type-level fidelity for references).
- `domains/concurrency/patterns.md` — message-passing vs lock patterns applied via `Arc`/`Mutex`/`mpsc`.
- `domains/errors/patterns.md``?` propagation relies on ownership transfer of the error.
- `languages/rs-async.md` — async borrows (`Pin`/`&mut`) build on the lifetime model here.
- `languages/rs-testing.md``Send`/`Sync` tests and ownership-based property tests.
+159
View File
@@ -0,0 +1,159 @@
# Rust Testing — Derived Application
> Applies Atelier's domain principles to Rust testing specifically.
> Derives from `domains/` docs; introduces no new P-rules (D-063).
> See `languages/rust.md` for the language first-principles stub.
## #[test] and Co-located Tests (Testing P1 Tests as Specification, C2 Clarity)
- **`#[test]` on functions in a `#[cfg(test)] mod tests` block:** tests co-located with source, compiled only in `cargo test`. A test file far from its subject rots (Documentation P5 Discoverability).
- **Test names read as a spec:** `fn create_user_rejects_invalid_email()` — a reader understands the unit from the name. Avoid `fn test_user_1()`.
- **`assert!` / `assert_eq!` / `assert_ne!` over raw `panic!`:** the macros produce readable failure output (`assertion failed: left == right, left: 5, right: 3`). Raw `panic!` gives a message only (Testing P6 Failure Specificity).
- **Applies `Testing P1`:** the test is a specification; the failure message is the spec violation.
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_user_rejects_invalid_email() {
let r = create_user("not-an-email");
assert!(matches!(r, Err(Error::Validation(_))));
}
#[test]
fn create_user_returns_persisted_id() {
let u = create_user("a@b.co").unwrap();
assert!(!u.id.is_empty());
}
}
```
## proptest and Property Tests (Testing P9 Edge Case Coverage, Testing P1 Tests as Specification)
- **`proptest` (or `quickcheck`) for invariant tests:** declare a property (`parse(serialize(x)) == x`), the framework generates hundreds of inputs and shrinks failures to a minimal counterexample (Testing P9).
- **Strategy over hand-written generators:** `proptest::collection::vec(any::<u32>(), 0..100)` generates arbitrary `Vec<u32>`; do not hand-roll a generator for each property.
- **`proptest!` macro or `proptest! { ... }` block:** each `case (name) => { ... }` is a property. The block is the spec (Testing P1).
- **Property tests complement, not replace, example tests:** examples document the happy path; properties cover the edge space. Both are required.
```rust
use proptest::prelude::*;
proptest! {
#[test]
fn roundtrips_id(s in "[a-z0-9]{1,32}") {
let id = UserId::new(&s).unwrap();
assert_eq!(id.as_str(), s);
}
#[test]
fn rejects_invalid_id(s in "[^a-z0-9]+") {
assert!(UserId::new(&s).is_err());
}
}
```
## Mock Discipline (Testing P2 Independence, Testing P7 Realism)
- **Mock at the trait, not the struct:** `trait Store { fn get(&self, id: &str) -> Result<User, Error>; }` in production; `#[automock] trait Store` (via `mockall`) in test. The trait is the contract.
- **`mockall` for generated mocks:** `#[automock] trait Repo {}` generates `MockRepo` with `expect_*` methods. Each expectation is per-test; no shared mock state (Testing P2 Independence).
- **Mock the boundary, not the unit:** mock `Repo`, not `UserService` (the unit). Mocking the unit under test tests the mock (Testing P7 realism).
- **No `#[cfg(test)]` on production code paths to inject mocks:** instead, accept the trait as a generic or `dyn` parameter. Test-only branches in production code are dead code in prod.
```rust
use mockall::*;
#[automock]
trait UserRepo {
fn get(&self, id: &str) -> Result<User, Error>;
}
#[test]
fn get_user_returns_not_found() {
let mut repo = MockUserRepo::new();
repo.expect_get()
.with(eq("abc"))
.returning(|_| Err(Error::NotFound));
let svc = UserService::new(Box::new(repo));
assert!(matches!(svc.get_user("abc"), Err(Error::NotFound)));
}
```
## Async Tests (Concurrency P10 Test for Race Conditions, Testing P1 Tests as Specification)
- **`#[tokio::test]` for `async fn` tests:** runs the coroutine on a tokio runtime. Without it, an `async fn` test returns a future, never awaited (silently passes).
- **`#[tokio::test(flavor = "multi_thread")]` for concurrency-sensitive tests:** multi-thread runtime surfaces races that single-thread misses (Concurrency P10).
- **`tokio::time::pause()` and `advance()` for time:** freeze and advance the runtime clock deterministically. No `tokio::time::sleep(real)` in tests.
- **Race-sensitive tests use `loom` for model-checking:** `loom` simulates all thread interleavings; it catches races `-race`-style detectors miss. Use for lock-free data structures.
```rust
#[tokio::test]
async fn async_fetch_returns_user() {
let u = fetch_user("abc").await.unwrap();
assert!(!u.name.is_empty());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn concurrent_cache_is_safe() {
let c = Arc::new(Cache::new());
let mut h = vec![];
for i in 0..10 {
let c = c.clone();
h.push(tokio::spawn(async move { c.get(&i.to_string()).await; }));
}
for x in h { x.await.unwrap(); }
}
```
## Determinism and Time (Testing P3 Determinism, Testing P9 Edge Case Coverage)
- **No `SystemTime::now()` or `Instant::now()` in code under test:** inject a `Clock` trait. In tests, a fake clock advances deterministically.
- **`tokio::time::pause()` for async time:** freezes the runtime clock; `tokio::time::advance(dur)` moves it. A `sleep(5s)` in test resolves instantly.
- **`--test-threads=1` to reproduce order coupling:** by default, `cargo test` runs tests in parallel; a test that passes alone but fails in a suite has hidden shared state. `-1` reproduces.
```rust
trait Clock { fn now(&self) -> std::time::Instant; }
struct FakeClock(std::time::Instant);
impl Clock for FakeClock {
fn now(&self) -> std::time::Instant { self.0 }
}
#[test]
fn user_has_created_at() {
let clk = FakeClock(std::time::Instant::now());
let u = create_user_with_clock("a@b.co", &clk).unwrap();
assert_eq!(u.created_at, clk.now());
}
```
## Doc Tests (Documentation P1 Documentation is Code, Testing P1 Tests as Specification)
- **`cargo test --doc` runs `///` fenced blocks:** a `///` example with `#`-hidden setup is a tested artifact; a stale output fails the build (Documentation P1).
- **`no_run` for examples that should compile but not run:** ```` ```rust,no_run ```` — type-checks the example without executing. Use for examples that need a DB.
- **`ignore` for examples that should not compile-check:** ```` ```rust,ignore ```` — skips entirely. Rare; prefer `no_run`.
- **Applies `Testing P1`:** the doc example is the spec; the doc test is the spec's regression test.
```rust
/// Fetch a user by id.
///
/// # Example
///
/// ```
/// # use mycrate::{get_user, Error};
/// let u = get_user("abc").unwrap();
/// assert!(!u.name.is_empty());
/// ```
pub fn get_user(id: &str) -> Result<User, Error> { /* ... */ }
```
## Cross-References
- `domains/testing/pyramid.md` — where unit/property/doc tests sit; proptest is the property layer.
- `domains/testing/fixtures.md``t.Cleanup`-equivalent (`Drop` in tests) as fixture discipline.
- `domains/testing/first-principles.md` — Testing P1 Specification, P2 Independence, P3 Determinism, P9 Edge Coverage.
- `domains/concurrency/first-principles.md` — Concurrency P10 (test for races), `loom` model-checking.
- `languages/rs-ownership.md``Send`/`Sync` tests and ownership-based property tests.
- `languages/rs-async.md``#[tokio::test]` patterns from that doc.
- `languages/rs-tooling.md``cargo test` flags (`--doc`, `--test-threads`) detailed here.
+95
View File
@@ -0,0 +1,95 @@
# Rust Tooling — Derived Application
> Applies Atelier's domain principles to Rust tooling specifically.
> Derives from `domains/` docs; introduces no new P-rules (D-063).
> See `languages/rust.md` for the language first-principles stub.
## cargo and Build Discipline (DevOps P2 Automation, DevOps P1 Reproducibility)
- **`cargo build` for dev, `cargo build --release` for release:** release enables optimizations (LTO, codegen-units=1). The default profile is for fast iteration, not perf.
- **`Cargo.lock` committed for applications and CI:** for libraries, commit the lock for CI reproducibility even though consumers resolve their own tree. A drifted lock breaks reproducibility (DevOps P1).
- **`cargo update` periodically, with a CI check:** `cargo update` bumps patch versions in the lock; a CI job that fails on lock drift catches a forgotten `cargo update`.
- **`cargo vendor` for hermetic CI:** vendors `vendor/` into the repo; CI builds without network. The trade-off is repo size; the win is reproducibility.
```toml
# Cargo.toml — profile discipline
[profile.release]
lto = true
codegen-units = 1
panic = "abort" # smaller binary, no unwinding
```
## clippy (DevOps P2 Automation, C2 Clarity)
- **`cargo clippy` is the lint layer over `rustc`:** it catches `clone()` where a borrow would do, `unwrap()` in library code, and needless `Box`. Run on every build.
- **`cargo clippy -- -D warnings` in CI:** warnings are errors. A clippy warning is a smell; accumulating them erodes the signal (Clarity C2).
- **Per-lint allow only with a tracked reason:** `#[allow(clippy::needless_collect)] // reason: GH-123 — collect needed for len` — each allow links to a ticket. Untracked allows accumulate into a permanently lint-bypassed core.
- **`cargo clippy --fix` for safe auto-fixes:** applies the linter's suggested change. Review the diff; do not run blindly on a large commit.
```bash
# CI gate
cargo clippy --all-targets --all-features -- -D warnings
```
## cargo fmt (DevOps P2 Automation, C2 Clarity)
- **`cargo fmt` is the formatter; format is not debated in review:** run in CI as a check (`cargo fmt --check`), not a fix. A failing check blocks the PR.
- **`rustfmt.toml` for repo-wide settings:** if the defaults are wrong for the repo, override once and stop. Do not relitigate per-PR.
- **Applies `devops/P2`:** the format gate is automated; a reviewer never comments on style.
```bash
# CI gate — fail if unformatted
cargo fmt --check
```
## Edition Discipline (DevOps P1 Reproducibility, C5 Reversibility)
- **`edition` in `Cargo.toml` pins the language edition:** 2015, 2018, 2021, 2024. An edition is a coherent set of language changes; bumping it is a deliberate migration.
- **Edition is not the compiler version:** `rustc 1.75` supports edition 2021; edition 2024 needs a newer `rustc`. Pin the toolchain with `rust-toolchain.toml`.
- **Bump editions deliberately, not opportunistically:** `cargo fix --edition` applies the migration lint; review the diff. A bump mid-feature conflates two changes.
- **Applies `devops/P1` and `C5` (reversibility):** pinning the edition and toolchain makes the build reproducible; bumping is a controlled, reversible change.
```toml
# Cargo.toml
[package]
edition = "2021"
rust-version = "1.75"
```
```toml
# rust-toolchain.toml
[toolchain]
channel = "1.75"
components = ["clippy", "rustfmt"]
```
## Documentation in the Pipeline (Documentation P1 Documentation is Code, DevOps P9 Documentation in the Pipeline)
- **`cargo doc` from doc comments:** `///` on items generates API docs; `cargo doc --open` previews. The build fails on broken intra-doc links (`#![warn(rustdoc::broken_intra_doc_links)]`).
- **Doc tests are run by `cargo test`:** a `///` fenced block with `#`-hidden setup is a tested artifact; a stale example fails `cargo test --doc` (Documentation P1).
- **`#![warn(missing_docs)]` for libraries:** public items without doc comments fail the build. Documentation is a build gate, not an afterthought.
- **`cargo readme` or `cargo docs-rs` for landing pages:** the crate's `README.md` is rendered on docs.rs; keep it in sync with `lib.rs`'s top-level doc.
```rust
#![warn(missing_docs, rustdoc::broken_intra_doc_links)]
/// Fetch a user by id.
///
/// # Example
///
/// ```
/// # use mycrate::get_user;
/// let u = get_user("abc").unwrap();
/// println!("{}", u.name);
/// ```
pub fn get_user(id: &str) -> Result<User, Error> { /* ... */ }
```
## Cross-References
- `domains/devops/ci-cd.md` — the pipeline gates that host clippy/fmt/test.
- `domains/devops/first-principles.md` — DevOps P1 Reproducibility, P2 Automation.
- `domains/documentation/first-principles.md` — Documentation P1 Documentation is Code.
- `languages/rs-ownership.md``Send`/`Sync` clippy lints reference this doc.
- `languages/rs-async.md` — async-runtime tooling (`tokio` features) detailed here.
- `languages/rs-testing.md``cargo test` flags (`--doc`, `--no-run`) detailed here.
+7
View File
@@ -2,6 +2,13 @@
> How Atelier's domain principles apply in Rust specifically. Derives from `domains/` docs.
## Derived Docs
- [rs-ownership.md](rs-ownership.md) — Send/Sync, lifetimes, borrowing, ownership transfer.
- [rs-tooling.md](rs-tooling.md) — cargo, clippy, fmt, edition discipline.
- [rs-async.md](rs-async.md) — tokio, async traits, cancellation, pin.
- [rs-testing.md](rs-testing.md) — #[test], proptest, property testing, mock discipline.
## Type System (C1 Correctness, Data P7 Type Fidelity)
- **Newtypes for domain concepts:** `struct UserId(String);` — zero-cost, type-safe.
+115
View File
@@ -0,0 +1,115 @@
# TypeScript Async — Derived Application
> Applies Atelier's domain principles to TypeScript async specifically.
> Derives from `domains/` docs; introduces no new P-rules (D-063).
> See `languages/typescript.md` for the language first-principles stub.
## Promises and AbortSignal (Concurrency P7 Cancellation Support, C1 Correctness)
- **Every async function accepts an optional `AbortSignal`:** cancellation is a first-class parameter, not a side channel. The signal propagates to `fetch`, `setTimeout`, and downstream awaits.
- **`AbortController` is the producer side; `AbortSignal` is the consumer side:** a function takes a `signal` (read-only), the caller owns the `controller` and decides when to abort.
- **Abort propagates as a rejected `Promise`:** `fetch` rejects with `AbortError`; downstream code sees the rejection, not a silent no-op. This preserves `errors/P5` (recoverable when possible) — the caller can distinguish cancellation from a real failure.
- **Applies `concurrency/P7`:** no async operation runs without a path to cancel it. A long-running `await` with no signal is a hung request.
- **Never swallow `AbortError`:** re-throw or handle distinctly; cancellation is the caller's intent, not an error to log.
```typescript
async function fetchUser(id: UserId, signal?: AbortSignal): Promise<User> {
const ctrl = new AbortController();
signal?.addEventListener('abort', () => ctrl.abort());
const res = await fetch(`/users/${id}`, { signal: ctrl.signal });
if (!res.ok) throw new HttpError(res.status);
return res.json() as Promise<User>;
}
// caller controls cancellation
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 5000);
try {
const u = await fetchUser(id, ctrl.signal);
} finally {
clearTimeout(timer);
}
```
## async/await Discipline (Concurrency P8 Timeout Discipline, C2 Clarity)
- **`await` is the only async primitive in application code:** no `.then` chains, no callback pyramids. `async`/`await` reads top-to-bottom (Clarity C2).
- **Never `await` in a hot loop without batching:** sequential `await` in a `for` loop is O(n) latency. Use `Promise.all` for parallelism; `for await...of` only for genuine streams.
- **`Promise.race` for a timeout:** every external `await` has a deadline. `Promise.race([op, timeout])` rejects when the deadline passes.
- **`return` vs `return await`:** inside `try`/`finally`, `return await` runs the `finally`; bare `return` of a Promise defers the `finally` to the microtask. Prefer `return await` when cleanup must run.
- **Applies `concurrency/P8`:** a bare `await` with no timeout is an unbounded wait. External calls (network, disk) always race against a deadline.
```typescript
async function fetchWithTimeout(url: string, ms = 5000, signal?: AbortSignal): Promise<Response> {
const ctrl = new AbortController();
signal?.addEventListener('abort', () => ctrl.abort());
const timer = new Promise<never>((_, reject) =>
setTimeout(() => reject(new TimeoutError(ms)), ms)
);
try {
return await Promise.race([fetch(url, { signal: ctrl.signal }), timer]);
} finally {
clearTimeout(timer); // cleanup runs on success and on race-loss
}
}
```
## Error Handling in Async (Errors P5 Recoverable When Possible, Errors P1 Errors are Data)
- **Catch `unknown`, narrow with a type guard:** `catch (e: unknown)` — TS does not infer the error type. `instanceof` or a discriminator narrows it.
- **Retry with backoff for transient failures:** network blips are recoverable (Errors P5). Exponential backoff with jitter, capped retry count, and an `AbortSignal`-aware `setTimeout`.
- **No retry for non-idempotent operations:** a `POST` that creates a resource is not safely retryable without an idempotency key (applies `api/P6` Idempotency).
- **Typed errors over `Error` subclasses:** a discriminated union `AppError = Network | Timeout | Cancelled` carries context (Errors P4 Preserve Context) without `instanceof` chains.
```typescript
async function fetchRetry(url: string, attempts = 3, signal?: AbortSignal): Promise<Response> {
for (let i = 0; i < attempts; i++) {
try {
return await fetchWithTimeout(url, 5000, signal);
} catch (e: unknown) {
if (e instanceof AbortError) throw e; // do not retry cancellation
if (e instanceof TimeoutError && i < attempts - 1) {
await sleep(jitter(i), signal); // backoff before retry
continue;
}
throw e;
}
}
throw new Error('unreachable');
}
```
## Cancellation Propagation (Concurrency P7 Cancellation Support, Concurrency P9 Bounded Queues)
- **One signal, many consumers:** pass the same `AbortSignal` to every async call in a request. Aborting once cancels the whole tree.
- **Bounded concurrency with a semaphore:** a `Semaphore(N)` wrapping `Promise.all` caps in-flight requests (Concurrency P9 — bounded queues). Unbounded `Promise.all` on a 10k-item array exhausts file descriptors.
- **Cancellation is cooperative, not preemptive:** a long synchronous block inside an `async` function ignores the signal. Yield with `await Promise.resolve()` periodically in CPU-bound loops, or move to a worker.
- **Applies `messaging/delivery-semantics`:** a cancelable async operation is an at-most-once delivery — the caller may stop listening, the result may or may not arrive. Retry-on-cancel is at-least-once; the caller must declare which.
```typescript
async function mapBounded<T, U>(items: readonly T[], fn: (t: T, s: AbortSignal) => Promise<U>, limit = 8, signal?: AbortSignal): Promise<U[]> {
const ctrl = new AbortController();
signal?.addEventListener('abort', () => ctrl.abort());
const results: U[] = new Array(items.length);
let next = 0;
const workers = Array.from({ length: limit }, async () => {
while (true) {
const i = next++;
if (i >= items.length) break;
if (ctrl.signal.aborted) throw new AbortError();
results[i] = await fn(items[i], ctrl.signal);
}
});
await Promise.all(workers);
return results;
}
```
## Cross-References
- `domains/concurrency/patterns.md` — the cancellation/timeout/semaphore patterns applied here.
- `domains/concurrency/first-principles.md` — Concurrency P7 Cancellation Support, P8 Timeout Discipline, P9 Bounded Queues.
- `domains/messaging/delivery-semantics.md` — at-most-once vs at-least-once framing for async retry/cancel (IDEATE-40).
- `domains/errors/patterns.md` — typed async errors and retry-with-backoff.
- `languages/ts-types.md``Result<T, E>` and discriminated `AppError` used in async error handling.
- `languages/ts-tooling.md``no-floating-promises` lint rule that enforces these awaits.
+117
View File
@@ -0,0 +1,117 @@
# TypeScript Testing — Derived Application
> Applies Atelier's domain principles to TypeScript testing specifically.
> Derives from `domains/` docs; introduces no new P-rules (D-063).
> See `languages/typescript.md` for the language first-principles stub.
## Vitest and Jest (Testing P1 Tests as Specification, C2 Clarity)
- **Vitest for new TS projects; Jest for legacy:** Vitest shares `vite`'s transform pipeline (no separate `ts-jest` config); Jest's ecosystem is broader. Either is acceptable — pick one per repo, do not mix.
- **Tests co-located with source:** `user.ts``user.test.ts`. A test file far from its subject rots (Documentation P5 Discoverability).
- **`describe`/`it` mirror the public API:** the test block names read as a specification ("User", "rejects an invalid email", "returns the persisted id"). A reader should understand the unit from test names alone (Testing P1).
- **`expect` over `assert`:** Vitest/Jest matchers produce readable failure output (`expect(x).toBe(y)` → "expected 5, received 3"). Raw `assert` gives a stack trace and nothing else (Testing P6 Failure Specificity).
```typescript
// user.test.ts
import { describe, it, expect } from 'vitest';
import { createUser } from './user';
describe('createUser', () => {
it('rejects an invalid email', async () => {
await expect(createUser({ email: 'not-an-email' })).rejects.toThrow(ValidationError);
});
it('returns the persisted id', async () => {
const u = await createUser({ email: 'a@b.co' });
expect(u.id).toMatch(/^[a-z0-9]+$/);
});
});
```
## Mock Discipline (Testing P2 Independence, Testing P7 Realism)
- **Mock at the boundary, not the unit:** replace `fetch` or the DB client, not the function under test. Mocking the unit under test tests the mock, not the code (Testing P7 — realism).
- **No partial mocks of the system under test:** if a method must be stubbed, the unit is too large. Extract a collaborator and mock that.
- **Each test sets up and tears down its own state:** no shared mutable fixtures. A `beforeEach`/`afterEach` resets; a top-level `let` shared across tests is order-coupling (Testing P2 Independence).
- **`vi.useFakeTimers()` for time-dependent code:** never call `Date.now()` directly in code under test; inject a `Clock` port. In tests, fake timers make `setTimeout` synchronous.
```typescript
import { vi, beforeEach, afterEach } from 'vitest';
beforeEach(() => {
vi.useFakeTimers();
global.fetch = vi.fn(); // boundary mock
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
```
## Type-Level Tests (Testing P1 Tests as Specification, Data P7 Type Fidelity)
- **Type-level tests assert the type system, not runtime behavior:** `expectTypeOf<T>().toMatchTypeOf<U>` and `tsd`/`expect-type` fail the build when a type assertion is wrong.
- **Negative type tests are required:** `// @ts-expect-error` proves the compiler rejects what it should. A `@ts-expect-error` that no longer errors is itself an error (the comment must be consumed).
- **Branded types and utility types get type tests:** a `UserId` should not be assignable to `string`; a `Readonly<T>` should not allow assignment. These invariants are part of the spec (Testing P1).
- **Applies `data/P7` (type fidelity):** a type-level test is a regression test for the type checker — if a refactor silently widens a type, the test fails.
```typescript
import { expectTypeOf } from 'expect-type';
import type { User, UserPatch, UserId } from './user';
test('UserPatch omits id and makes fields optional', () => {
expectTypeOf<UserPatch>().toMatchTypeOf<{ name?: string; email?: string }>();
expectTypeOf<UserPatch>().not.toHaveProperty('id');
});
test('UserId is not assignable to bare string', () => {
// @ts-expect-error — brand prevents widening
const s: string = {} as UserId;
expect(s).toBeDefined();
});
```
## Parametrize and Factories (Testing P3 Determinism, Testing P9 Edge Case Coverage)
- **`it.each` / `test.each` for parametrized cases:** one table drives many runs; each row is an independent test with its own name and failure output.
- **Factories over fixtures:** `makeUser(overrides)` returns a fresh object per call. A shared `const user = {...}` across tests couples them and breaks determinism when one test mutates it (Testing P3).
- **Edge cases as rows, not special tests:** empty array, single element, max int, null, undefined — each a row in a `test.each` table. An ad-hoc `it('handles edge')` with multiple asserts hides which case failed (Testing P9 — edge case coverage, P6 failure specificity).
- **Property-style tests via `fast-check`:** for invariants (e.g., "parse(serialize(x)) === x"), `fast-check` generates hundreds of inputs and shrinks failures to a minimal counterexample.
```typescript
import { test, expect } from 'vitest';
import { makeUser } from './user.factory';
test.each([
{ input: '', reason: 'empty' },
{ input: 'a'.repeat(1000), reason: 'too long' },
{ input: 'not-an-email', reason: 'no @' },
])('rejects email: $reason', async ({ input }) => {
await expect(makeUser({ email: input })).rejects.toThrow(ValidationError);
});
```
## Determinism and Time (Testing P3 Determinism, Testing P9 Edge Case Coverage)
- **No `Date.now()`, `Math.random()`, or `crypto.randomUUID()` in code under test:** inject a `Clock`, `Random`, and `IdGen` port. In tests, provide deterministic fakes.
- **`--random` test order (Vitest `sequence.shuffle: true` default) catches order coupling:** a test that passes alone but fails in a suite has hidden state. The shuffle makes that state visible (Testing P2).
- **Race-detector parallelism for async tests:** run async tests concurrently by default; a test that assumes serial execution breaks under parallelism. Vitest's `concurrent` flag surfaces the bug.
```typescript
import { vi, test, expect } from 'vitest';
test.concurrent('parallel fetch does not interleave state', async () => {
const store = new Store();
await Promise.all([store.put('a', 1), store.put('b', 2)]);
expect(store.get('a')).toBe(1);
expect(store.get('b')).toBe(2);
});
```
## Cross-References
- `domains/testing/pyramid.md` — where unit/type/integration tests sit; the type-level tests here are the base layer.
- `domains/testing/fixtures.md` — factory-vs-fixture discipline applied via `makeUser`.
- `domains/testing/first-principles.md` — Testing P1 Specification, P2 Independence, P3 Determinism, P9 Edge Coverage.
- `languages/ts-types.md` — the branded types and utility types that type-level tests assert.
- `languages/ts-async.md` — async tests use the cancellation/timeout patterns from that doc.
- `languages/ts-tooling.md``ts-jest`/`vitest` config and the `expect-type`/`tsd` toolchain.
+114
View File
@@ -0,0 +1,114 @@
# TypeScript Tooling — Derived Application
> Applies Atelier's domain principles to TypeScript tooling specifically.
> Derives from `domains/` docs; introduces no new P-rules (D-063).
> See `languages/typescript.md` for the language first-principles stub.
## tsc and tsconfig Discipline (DevOps P2 Automation, DevOps P1 Reproducibility)
- **`strict: true` is the floor, not the ceiling:** it enables `strictNullChecks`, `noImplicitAny`, `strictFunctionTypes`, and more. Disable sub-flags only with a justification comment.
- **`tsc --noEmit` in CI:** type-checking is a build gate; emission is the bundler's job. Separate the two so a type error fails CI even when the bundler would have succeeded.
- **`tsconfig` is per-project, not inherited verbatim:** a shared base (`extends`) encodes org defaults; each project overrides the deltas it needs. Avoids the "one monoreto-config-fits-all" trap.
- **`noUncheckedIndexedAccess` for safety:** `arr[i]` becomes `T | undefined`, forcing narrowing. Costs little, prevents a class of out-of-bounds deref bugs.
- **Applies `devops/P1` (reproducibility):** pinned `typescript` version in `package.json` and `lockfile` ensure every CI run type-checks against the same compiler.
```jsonc
// tsconfig.json — base
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noEmit": true,
"moduleResolution": "bundler",
"isolatedModules": true
}
}
```
## ESLint and @typescript-eslint (DevOps P2 Automation, Documentation P9 Living Documents)
- **ESLint with `@typescript-eslint` strict ruleset:** `recommended-type-checked` enables rules that require the type checker (`no-floating-promises`, `no-misused-promises`).
- **Rules encode decisions, not taste:** every custom rule in the config has a one-line `// reason:` comment linking to the principle it enforces. This makes the config a living document (Documentation P9).
- **Format is Prettier's job; ESLint lints:** `eslint-config-prettier` disables conflicting format rules. Do not relitigate formatting in code review.
- **`no-floating-promises` enforces `concurrency/P8` (timeout discipline):** an un-awaited `Promise` is a fire-and-forget that swallows errors and timeouts. The rule forces `.catch()` or `await`.
```jsonc
// .eslintrc.json
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended-type-checked",
"prettier"
],
"parserOptions": { "project": "./tsconfig.json" },
"rules": {
// reason: enforce Concurrency P8 — no un-awaited promises
"@typescript-eslint/no-floating-promises": "error",
// reason: enforce Data P7 — no `any` escaping the type checker
"@typescript-eslint/no-explicit-any": "error"
}
}
```
## Project References and ts-jest (DevOps P2 Automation, C6 Composability)
- **Project references for monorepos:** `composite: true` + `references` let `tsc --build` incrementally type-check only changed projects, and enforce the dependency graph at the type level.
- **`paths` aliases mirror the import structure:** `@app/*``src/*`. Configure once in `tsconfig`, mirror in the bundler and the test runner so all three agree.
- **`ts-jest` (or `vitest`) with `isolatedModules: true`:** each test file is type-checked in isolation, matching how the bundler transpiles. Catches the "passes in `tsc` but fails in the bundler" gap.
- **Applies `devops/P2`:** the build pipeline (tsc → lint → test → bundle) is automated; a developer never runs a manual sequence.
```jsonc
// tsconfig.references.json
{
"files": [],
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/api" },
{ "path": "./packages/web" }
]
}
```
## Lockfile and Reproducible Install (DevOps P1 Reproducibility)
- **`npm ci` in CI, not `npm install`:** `ci` reads the lockfile exactly and fails on drift. `install` mutates the lockfile.
- **Lockfile committed for applications:** for libraries, commit `package-lock.json` for CI reproducibility even though consumers resolve their own tree.
- **No floating ranges in `package.json`:** `^` and `~` are CI's job to resolve; pin the resolved version in the lockfile. An unpinned `*` is a supply-chain attack surface.
```bash
# CI install step — deterministic
npm ci
# Type-check gate
npx tsc --noEmit
# Lint gate
npx eslint .
```
## Documentation in the Pipeline (Documentation P1 Documentation is Code, DevOps P9 Documentation in the Pipeline)
- **Type-checked JSDoc:** `typedoc` (or `TypeDoc`) generates API docs from `tsdoc` comments. The compiler enforces that `@param` names match real parameters.
- **`@example` blocks are compiled:** a `tsdoc` `@example` fenced block is type-checked as part of the doc build. Stale examples fail the pipeline (Documentation P1 — docs are code).
- **README badges reflect CI status:** the build/lint/test/type-check gates are the source of truth; badges surface them. Do not hand-edit status tables.
```typescript
/**
* Fetch a user by ID.
*
* @param id - a branded UserId (see ts-types.md).
* @throws {NotFoundError} if the user does not exist.
* @example
* ```ts
* const u = await getUser(userId('abc'));
* ```
*/
async function getUser(id: UserId): Promise<User> { /* ... */ }
```
## Cross-References
- `domains/devops/ci-cd.md` — the pipeline gates that host tsc/ESLint/ts-jest.
- `domains/devops/first-principles.md` — DevOps P1 Reproducibility, P2 Automation.
- `domains/documentation/first-principles.md` — Documentation P1 Documentation is Code.
- `languages/ts-types.md` — the type rules ESLint enforces reference this doc.
- `languages/ts-testing.md` — the test-runner config (`ts-jest`/`vitest`) detailed here.
+111
View File
@@ -0,0 +1,111 @@
# TypeScript Type System — Derived Application
> Applies Atelier's domain principles to TypeScript's type system specifically.
> Derives from `domains/` docs; introduces no new P-rules (D-063).
> See `languages/typescript.md` for the language first-principles stub.
## Nominal vs Structural Typing (C1 Correctness, Data P7 Type Fidelity, API P1 Contract Fidelity)
- **TypeScript is structurally typed:** two types with the same shape are assignable. This is convenient but erases domain boundaries — a `UserId` and `PostId` both `string` are interchangeable.
- **Branded (nominal) types for domain IDs:** intersect with a phantom brand to simulate nominal typing. The brand is never constructed at runtime; it exists only to the type checker.
- **Applies `data/P7` (type fidelity)** at the value boundary: a branded `UserId` cannot be passed where a `PostId` is expected, preventing an entire class of swap bugs.
- **Applies `api/P1` (contract fidelity):** branded types make API contracts explicit — handlers cannot accept "any string" for an ID.
- **Brand is opaque to consumers:** do not export the brand symbol; construction goes through a validated factory.
```typescript
type UserId = string & { readonly __brand: 'UserId' };
type PostId = string & { readonly __brand: 'PostId' };
function userId(s: string): UserId {
if (!/^[a-zA-Z0-9]+$/.test(s)) throw new Error('invalid id');
return s as UserId;
}
function getUser(id: UserId): User { /* ... */ }
getUser('abc'); // type error
getUser(userId('abc')); // ok
getUser(postId('xyz')); // type error — distinct brands
```
## Generics (C6 Composability, Data P7 Type Fidelity)
- **Generics preserve type information across boundaries:** a `Repository<T>` keeps the element type through `find`/`save` rather than widening to `any`.
- **Constrain with `extends`:** `<T extends Entity>` documents the contract and gives the body access to `T.id`.
- **Avoid unnecessary generics:** if a function accepts "any value and returns it unchanged," `T` is noise. Prefer `unknown` for truly opaque inputs.
- **Variance is structural:** TS does not enforce sound variance; mark mutation points with `readonly` to keep `T[]` assignable to `readonly T[]`.
```typescript
interface Entity { id: string }
class Repository<T extends Entity> {
constructor(private db: Map<string, T>) {}
find(id: string): T | undefined { return this.db.get(id); }
save(t: T): void { this.db.set(t.id, t); }
}
```
## Narrowing and Type Guards (C1 Correctness, Errors P1 Errors are Data)
- **Narrowing is how TS handles `unknown` and union types safely:** `typeof`, `in`, `instanceof`, and discriminators collapse a wide type to a precise one before use.
- **User-defined type guards (`x is T`) encode domain predicates:** `isUser(x): x is User` lets the checker track the narrow across call sites.
- **Applies `errors/P1` (errors are data):** a `Result<T, E>` discriminated union is narrowed with `if (r.ok)` — no `try`/`catch` needed for expected failures.
- **Never use `as` to widen past a check:** `as` lies to the compiler. If narrowing does not reach the type you need, the predicate is wrong, not the cast.
```typescript
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
function unwrap<T, E>(r: Result<T, E>): T {
if (r.ok) return r.value; // narrowed to { ok: true; value: T }
throw new Error(JSON.stringify(r.error));
}
function isUser(x: unknown): x is User {
return typeof x === 'object' && x !== null && 'id' in x && 'name' in x;
}
```
## Utility Types (C5 Reversibility, C6 Composability)
- **`Partial<T>`, `Pick<T,K>`, `Omit<T,K>`, `Readonly<T>` are derived views:** they derive from a source-of-truth `T` rather than redeclaring fields, so the source change propagates (reversibility).
- **`Readonly<T>` enforces immutability at the type level** — applies `concurrency/P1` (immutability by default) without runtime cost.
- **`Record<K, V>` over `{ [k: string]: V }`:** the index signature form allows any string key including prototype pollution vectors; `Record` is exact.
- **Compose, don't accumulate:** `type Patch<T> = Partial<Omit<T, 'id'>>` reads as a transformation; restate it if `T` changes shape, rather than maintaining a parallel `Patch` type.
```typescript
interface User { id: string; name: string; email: string; }
type UserPatch = Partial<Omit<User, 'id'>>;
type ReadonlyUser = Readonly<User>;
type UsersById = Record<string, User>;
```
## Discriminated Unions (C1 Correctness, Data P7 Type Fidelity, Errors P1 Errors are Data)
- **Discriminated unions over enums:** `type Status = { type: 'pending' } | { type: 'paid'; amount: number }` is exhaustive and carries payload per variant; an `enum` carries neither.
- **The discriminant is a literal `type` (or `kind`) field:** the checker narrows on it in `switch` and `if` without a custom guard.
- **Exhaustiveness via `never`:** assign the narrowed value to `never` in the default branch; if a variant is added, the default fails to compile.
- **Applies `errors/P1`:** model domain errors as a discriminated union `AppError = NotFound | Validation | Conflict`, not as exception classes — the type system carries the error set.
```typescript
type Status =
| { type: 'pending' }
| { type: 'paid'; amount: number }
| { type: 'refunded'; reason: string };
function describe(s: Status): string {
switch (s.type) {
case 'pending': return 'awaiting payment';
case 'paid': return `paid ${s.amount}`;
case 'refunded': return `refunded: ${s.reason}`;
default:
const _exhaustive: never = s; // compile error if a variant is added
throw new Error('unhandled');
}
}
```
## Cross-References
- `domains/data/schema-design.md` — schema-level fidelity parallels branded types at the TS boundary.
- `domains/data/first-principles.md` — Data P7 Type Fidelity, the primary trace for this doc.
- `domains/api/rest.md` — contract fidelity for API handlers consuming branded IDs.
- `domains/errors/patterns.md` — discriminated unions as the error-as-data encoding.
- `languages/ts-async.md` — typed async results built on the `Result` union here.
+7
View File
@@ -2,6 +2,13 @@
> How Atelier's domain principles apply in TypeScript specifically. Derives from `domains/` docs; this file is the language-specific lens.
## Derived Docs
- [ts-types.md](ts-types.md) — TS type system: nominal-via-branding, generics, narrowing, utility types, discriminated unions.
- [ts-tooling.md](ts-tooling.md) — tsc, ESLint, ts-jest, project references, tsconfig discipline.
- [ts-async.md](ts-async.md) — Promises + AbortSignal, async/await, error handling, cancellation.
- [ts-testing.md](ts-testing.md) — Vitest/Jest, mock discipline, type-level tests.
## Type System (C1 Correctness, Data P7 Type Fidelity)
- **Strict mode on:** `strict: true` in `tsconfig.json`. No `any` without justification.