Skip to content

Commit e69de0b

Browse files
lesnik512claude
andcommitted
docs: compact CLAUDE.md to invariant summaries + capability index
Collapse the Architecture-invariants / Module-layout / Protocol-seams / Testing deep-dives that duplicated architecture/*.md into terse invariant summaries plus a capability-to-file index table, and add the promotion reminder. Promote the orphan Code-conventions section into a new architecture/conventions.md, and add architecture/README.md as the capability index with the promotion rule. CLAUDE.md 113 to 60 lines. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9302850 commit e69de0b

3 files changed

Lines changed: 99 additions & 84 deletions

File tree

CLAUDE.md

Lines changed: 31 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -8,90 +8,45 @@ Guidance for AI agents (Claude Code, etc.) working in this repository.
88

99
**Where to find what:**
1010

11-
- [`architecture/`](architecture/) (repo root) — the per-capability living truth (overview, client, middleware, decoders, errors, resilience, optional extras, testing); the promotion target on every ship. **Read the relevant file before changing that capability.**
12-
- [`planning/README.md`](planning/README.md) — the planning conventions (two axes, change bundles, three lanes, frontmatter) + the change Index.
11+
- [`architecture/`](architecture/) (repo root) — the per-capability living truth, one file per capability ([`architecture/README.md`](architecture/README.md) is the index). The promotion target on every ship. **Read the relevant file before changing that capability.**
12+
- [`planning/README.md`](planning/README.md) — the planning convention (Quick path + the two-axis convention) and the generated change Index.
1313
- [`planning/changes/<YYYY-MM-DD.NN-slug>/`](planning/changes/) — per-change bundles (`design.md` + `plan.md`, or `change.md` for the lightweight lane).
14-
- [`planning/decisions/<YYYY-MM-DD>-<slug>.md`](planning/decisions/) — design decisions taken (esp. options rejected with a load-bearing reason), each with a Revisit trigger; listed by `just index`.
15-
- [`planning/audits/`](planning/audits/) — findings reports + `scripts/` tooling.
16-
- [`planning/retros/`](planning/retros/) — retrospectives.
17-
- [`planning/releases/`](planning/releases/) — per-version release notes (also published on GitHub Releases).
18-
- [`planning/deferred.md`](planning/deferred.md) — review-surfaced, not-yet-actionable items.
19-
- [`planning/_templates/`](planning/_templates/) — design/plan/change templates.
14+
- [`planning/decisions/`](planning/decisions/), [`planning/audits/`](planning/audits/), [`planning/retros/`](planning/retros/), [`planning/releases/`](planning/releases/), [`planning/deferred.md`](planning/deferred.md) — decisions, findings sweeps, retrospectives, release notes, and deferred items.
2015

21-
**Per-feature workflow:** brainstorming → `design.md` in `planning/changes/<id>/` → writing-plans → `plan.md` in the same bundle → executing-plans (or subagent-driven-development) → requesting-code-review → finishing-a-development-branch. On ship, promote the conclusions into the affected `architecture/<capability>.md` by hand and set `status: shipped` + `pr` + `outcome` in the implementing PR — there is no folder move. The change listing is generated: run `just index`. A design decision taken without a code change — especially a candidate rejected with a load-bearing reason — is recorded as `planning/decisions/YYYY-MM-DD-<slug>.md` (the `decision.md` template, frontmatter `status: accepted|superseded`), each with a **Revisit trigger** so future reviews don't re-litigate it; listed by `just index`. Topic slugs are kebab-case descriptions (`msgspec-decoder-adapter`), not story IDs.
16+
## Workflow
2217

23-
## Commands
24-
25-
This project uses `just` (task runner) and `uv` (package manager).
26-
27-
```bash
28-
just install # uv lock --upgrade && uv sync --all-extras --frozen --group lint
29-
just lint # eof-fixer + ruff format + ruff check --fix + ty check
30-
just lint-ci # same checks without auto-fixing (used in CI)
31-
just test # uv run pytest (with coverage by default)
32-
just test-branch # pytest with branch coverage
33-
```
34-
35-
`just test` passes extra args to pytest:
36-
37-
```bash
38-
just test tests/test_client.py
39-
just test tests/test_client.py -k test_get_returns_typed_response
40-
```
41-
42-
Without `just`:
43-
44-
```bash
45-
uv run ruff format . && uv run ruff check . --fix && uv run ty check
46-
uv run pytest
47-
```
48-
49-
## Architecture invariants
50-
51-
These are non-negotiable, but **most are NOT machine-checked — don't rely on CI to catch a violation.** Enforced by ruff: `print()` (`T201`) and a blanket `# type: ignore` (`PGH003`). Partially: `httpx2._` (ruff `SLF001` catches attribute access, not a *used* private import). Review-only: the future-import and global-logging bans.
18+
Planning follows the portable two-axis convention — `architecture/` (repo root) is the living **truth home** and promotion target; `planning/changes/` holds the per-change bundles. **Start at the [Quick path](planning/README.md#quick-path-start-here)** in `planning/README.md` to choose a lane (Full / Lightweight / Tiny), create a bundle, and ship — that file is the authoritative spec. Run `just check-planning` to validate bundles and `just index` to print the change listing.
5219

53-
- **No `httpx2` private API**: `grep -rE 'httpx2\._' src/httpware/` should return zero matches (run in review — not wired into CI). Public symbols only.
54-
- **No `from __future__ import annotations`**: Python 3.11+ floor; PEP 604/585 syntax is native.
55-
- **No `print()`**: enforced by ruff.
56-
- **No global logging config**: no `logging.basicConfig()`, no bare `logging.getLogger()`. Acquire `logging.getLogger("httpware")` or `logging.getLogger(f"httpware.{module}")` only.
57-
- **Type suppressions**: use `# ty: ignore[<rule>]`, never `# type: ignore` or `# mypy: ignore`.
58-
59-
## Code conventions
60-
61-
- **Modules**: `snake_case` (`client.py`, `errors.py`, `middleware/chain.py`).
62-
- **Classes**: `PascalCase`. `Http` is two letters: `AsyncClient`, not `ASYNCClient`.
63-
- **Methods**: `snake_case`. No `a` prefix on async methods (match `httpx2`); `aclose()` is the sole exception.
64-
- **Private symbols**: `_leading_underscore`. Cross-module private code lives in `_internal/`.
65-
- **Imports**: absolute paths inside `src/httpware/`; relative imports only within the same subpackage.
66-
- **Docstrings**: PEP 257. Module/class/public-method required; `D1` (missing docstring) is ignored.
67-
- **Exception construction**: status-keyed `StatusError` subclasses (the 4xx/5xx tree) take a single positional `response: httpx2.Response` and do NOT override `__init__` — all fields via `exc.response.*`. This rule scopes to `StatusError` only; non-status `ClientError` subclasses such as `DecodeError`, `MissingDecoderError`, `BulkheadFullError`, `RetryBudgetExhaustedError`, and `CircuitOpenError` deliberately define `__init__` with keyword-only fields. See `architecture/errors.md`.
20+
## Commands
6821

69-
## Module layout
22+
This project uses `just` (task runner) and `uv` (package manager). `just --list` is the source of truth; non-obvious notes:
7023

71-
```
72-
src/httpware/
73-
├── __init__.py # public exports + __all__
74-
├── client.py # AsyncClient + Client (thin wrappers over httpx2.AsyncClient / httpx2.Client)
75-
├── errors.py # status-keyed exception hierarchy holding httpx2.Response
76-
├── middleware/ # protocol, Next type, chain composition, phase decorators
77-
├── decoders/ # ResponseDecoder protocol + Pydantic/msgspec adapters
78-
├── _internal/ # private cross-module helpers
79-
└── py.typed
80-
```
24+
- `just lint` auto-fixes; `just lint-ci` is the read-only CI variant (and runs the planning validator).
25+
- `just test` runs pytest with coverage and forwards extra args: `just test tests/test_client.py -k test_name`.
26+
- Without `just`: `uv run ruff format . && uv run ruff check . --fix && uv run ty check && uv run pytest`.
8127

82-
## Protocol seams
28+
## Architecture
8329

84-
Three documented internal boundaries. AI agents must respect them — never cross a seam except through its documented protocol.
30+
> Quick orientation. The authoritative, code-current account of each capability lives in [`architecture/`](architecture/). **When a change alters a capability's behavior, update the matching `architecture/<capability>.md` in the same PR** — that promotion is what keeps `architecture/` true; code that changes without it silently rots the truth home.
8531
86-
1. **Seam A**`Client`/`AsyncClient``Middleware`/`AsyncMiddleware` — middleware chain composed at `Client.__init__` and `AsyncClient.__init__`, frozen for the client's lifetime. Internal terminal calls `httpx2.Client.send` or `httpx2.AsyncClient.send`, maps exceptions, raises `StatusError` on 4xx/5xx. Sync and async surfaces are kept at parity.
87-
2. **Seam B**`Client`/`AsyncClient``ResponseDecoder` list — both clients take `decoders: Sequence[ResponseDecoder] | None` (a *list*, not a single decoder; `None` resolves against installed extras, pydantic-first). When `response_model` is provided, `send`/`send_with_response` (sync and async alike) walk the list and the first decoder whose `can_decode(model: type) -> bool` returns True runs `decode(content: bytes, model: type[T]) -> T`; if no decoder claims the model, `MissingDecoderError` is raised *before* the HTTP call. Decoder exceptions are wrapped as `DecodeError` at the seam. Full contract: [`architecture/decoders.md`](architecture/decoders.md).
88-
3. **Seam C**`httpware` ↔ optional extras — each opt-in dependency imported only inside its dedicated module.
32+
`httpware` is a thin wrapper over `httpx2` (`httpx2.Request`/`httpx2.Response` are public surface, not abstracted away) built around three documented protocol seams. **Invariants that must not break:**
8933

90-
## Testing
34+
- **Three protocol seams, crossed only through their protocols.** Seam A: `Client`/`AsyncClient` ↔ middleware chain, composed at `__init__` and frozen for the client's lifetime; the internal terminal calls `httpx2.*.send`, maps exceptions, and raises `StatusError` on 4xx/5xx. Seam B: clients ↔ `decoders: Sequence[ResponseDecoder] | None` — first decoder whose `can_decode` is True runs; `MissingDecoderError` raises *before* the HTTP call if none claims the model; decoder failures wrap as `DecodeError`. Seam C: `httpware` ↔ optional extras — each opt-in dependency imported only inside its dedicated module.
35+
- **Sync/async parity.** `Client` and `AsyncClient` carry identical features (typed decoding, middleware, resilience, `stream()`); a change to one surface must mirror to the other.
36+
- **House invariants** (most review-only, not CI-checked): no `httpx2._` private API; no `from __future__ import annotations` (3.11+ floor); no `print()` (ruff `T201`); no global logging config (`logging.getLogger("httpware")` / namespaced children only); type suppressions use `# ty: ignore[<rule>]`, never `# type: ignore`.
37+
- **Errors.** Status-keyed `StatusError` subclasses take a single positional `response` and never override `__init__`; non-status `ClientError` subclasses (`DecodeError`, `MissingDecoderError`, `BulkheadFullError`, `RetryBudgetExhaustedError`, `CircuitOpenError`, `ResponseTooLargeError`) do.
9138

92-
- `pytest-asyncio` auto mode — async tests do NOT need `@pytest.mark.asyncio`.
93-
- Property-based tests (Hypothesis) for concurrency-sensitive code: `RetryBudget`, `Bulkhead`, retry interleaving. Files named `test_*_props.py`.
94-
- Tests inject `httpx2.MockTransport` via `AsyncClient(httpx2_client=httpx2.AsyncClient(transport=mock))` for async or `Client(httpx2_client=httpx2.Client(transport=mock))` for sync. No `respx`, no `RecordedTransport`.
39+
| Capability | File |
40+
|---|---|
41+
| What httpware is + architectural invariants + module layout | [`architecture/overview.md`](architecture/overview.md) |
42+
| Sync `Client` / async `AsyncClient` parity, `stream()` | [`architecture/client.md`](architecture/client.md) |
43+
| Seam A — middleware protocol, `Next`, chain composition | [`architecture/middleware.md`](architecture/middleware.md) |
44+
| Seam B — `ResponseDecoder` protocol, pydantic/msgspec resolution | [`architecture/decoders.md`](architecture/decoders.md) |
45+
| Status-keyed exception tree, construction invariant, redaction | [`architecture/errors.md`](architecture/errors.md) |
46+
| Resilience suite (retry, budget, bulkhead, circuit breaker, timeout) | [`architecture/resilience.md`](architecture/resilience.md) |
47+
| Seam C — optional extras isolation | [`architecture/extras.md`](architecture/extras.md) |
48+
| House code conventions (naming, imports, docstrings) | [`architecture/conventions.md`](architecture/conventions.md) |
49+
| Testing conventions | [`architecture/testing.md`](architecture/testing.md) |
9550

9651
## When in doubt
9752

@@ -100,14 +55,6 @@ Three documented internal boundaries. AI agents must respect them — never cros
10055

10156
## Agent skills
10257

103-
### Issue tracker
104-
105-
Issues live in GitHub Issues (`modern-python/httpware`), managed via the `gh` CLI; external PRs are not a triage surface. See `planning/agents/issue-tracker.md`.
106-
107-
### Triage labels
108-
109-
Canonical defaults — `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix` (the last already exists). See `planning/agents/triage-labels.md`.
110-
111-
### Domain docs
112-
113-
Single-context — one `CONTEXT.md` at the repo root + ADRs under `planning/adr/`. See `planning/agents/domain.md`.
58+
- **Issue tracker** — Issues live in GitHub Issues (`modern-python/httpware`), managed via the `gh` CLI; external PRs are not a triage surface. See `planning/agents/issue-tracker.md`.
59+
- **Triage labels** — Canonical defaults: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`. See `planning/agents/triage-labels.md`.
60+
- **Domain docs** — Single-context: one `CONTEXT.md` at the repo root + ADRs under `planning/adr/`. See `planning/agents/domain.md`.

architecture/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Architecture
2+
3+
This directory is the **living per-capability truth** about what `httpware`
4+
does *now* — one file per capability, living prose, dated by git (no
5+
frontmatter). It is the promotion target on every ship: the *why* and *how it
6+
got here* live in [`planning/`](../planning/), the *what it is today* lives
7+
here.
8+
9+
## Capability files
10+
11+
- **[overview.md](overview.md)** — what `httpware` is (a thin `httpx2` wrapper)
12+
and the cross-cutting architectural invariants.
13+
- **[client.md](client.md)** — the sync `Client` / async `AsyncClient` pair and
14+
their parity contract (typed decoding, middleware chain, resilience, `stream()`).
15+
- **[middleware.md](middleware.md)** — Seam A: the middleware protocol, `Next`
16+
type, and chain composition at client construction.
17+
- **[decoders.md](decoders.md)** — Seam B: the `ResponseDecoder` protocol and
18+
the pydantic / msgspec decoder resolution.
19+
- **[errors.md](errors.md)** — the status-keyed exception tree raised on 4xx/5xx
20+
and the `StatusError` construction invariant.
21+
- **[resilience.md](resilience.md)** — the stdlib resilience suite (retry, retry
22+
budget, bulkhead, circuit breaker, timeout) composed via the middleware chain.
23+
- **[extras.md](extras.md)** — Seam C: optional dependencies imported only inside
24+
their dedicated modules.
25+
- **[conventions.md](conventions.md)** — house code conventions: naming, imports,
26+
docstrings, exception construction.
27+
- **[testing.md](testing.md)** — the testing conventions (`pytest-asyncio` auto
28+
mode, mock transports, property-based tests).
29+
30+
## Promotion rule
31+
32+
When a change alters a capability's behavior, **hand-edit the matching
33+
`architecture/<capability>.md` in the same PR** — the edit rides in the same
34+
diff and is reviewed with the code, never applied as a separate post-merge step.
35+
That promotion is what keeps this directory true; code that changes without it
36+
silently rots the truth home.

architecture/conventions.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Code conventions
2+
3+
The naming, import, and docstring conventions every module in `src/httpware/`
4+
follows. These are house style, not machine-checked beyond what ruff enforces;
5+
treat a violation as a review blocker.
6+
7+
## Naming
8+
9+
- **Modules**: `snake_case` (`client.py`, `errors.py`, `middleware/chain.py`).
10+
- **Classes**: `PascalCase`. `Http` is two letters: `AsyncClient`, not
11+
`ASYNCClient`.
12+
- **Methods**: `snake_case`. No `a` prefix on async methods (match `httpx2`);
13+
`aclose()` is the sole exception.
14+
- **Private symbols**: `_leading_underscore`. Cross-module private code lives in
15+
`_internal/`.
16+
17+
## Imports
18+
19+
- Absolute paths inside `src/httpware/`; relative imports only within the same
20+
subpackage.
21+
22+
## Docstrings
23+
24+
- PEP 257. Module / class / public-method docstrings are required; `D1`
25+
(missing docstring) is ignored, so the requirement is review-enforced.
26+
27+
## Exception construction
28+
29+
Status-keyed `StatusError` subclasses take a single positional
30+
`response: httpx2.Response` and do **not** override `__init__`; non-status
31+
`ClientError` subclasses deliberately do. This invariant — and which subclasses
32+
fall on each side — lives in [`errors.md`](errors.md).

0 commit comments

Comments
 (0)