Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3e66c42
test: add the invariant census and mark the frame-budget invariants
lesnik512 Aug 11, 2026
7974cde
test: mark the invariants architecture/ used to state
lesnik512 Aug 11, 2026
98a2a5c
test: attribute the unwireable-override and skip-navigation claims to…
lesnik512 Aug 11, 2026
8d1c1de
test: narrow the validated-flag invariant to what its test actually p…
lesnik512 Aug 11, 2026
e19a607
test: mark the never-auto-validates invariant on its four tests
lesnik512 Aug 11, 2026
6c998c5
docs(comments): cite invariant tests instead of architecture/ pages
lesnik512 Aug 13, 2026
031c31a
test: pin the one-graph, fails-loudly and import-purity invariants
lesnik512 Aug 13, 2026
69ad3d6
fix(test): catch relative imports in the scope.py import-purity check
lesnik512 Aug 13, 2026
b8451c5
docs(decisions): record the contracts architecture/ carried
lesnik512 Aug 13, 2026
d316254
docs(decisions): correct thinning-commit churn figures in drop-archit…
lesnik512 Aug 13, 2026
3677492
docs: fold the free-threaded support level into design decisions
lesnik512 Aug 13, 2026
d3a6ba3
docs(planning): four homes and the admission check replace the truth …
lesnik512 Aug 13, 2026
e0718e9
docs: drop architecture/; invariants are tests, contracts are decisions
lesnik512 Aug 13, 2026
10021ea
fix(docs): close out final review of drop-architecture-dir
lesnik512 Aug 13, 2026
79e761d
fix(planning): widen census sentence, extract the union-degradation i…
lesnik512 Aug 13, 2026
8e4d158
docs(decisions): drop the five 2026-08-11 records
lesnik512 Aug 13, 2026
5f52566
docs(decisions): drop the free-threaded Beta record
lesnik512 Aug 13, 2026
05a825c
docs(planning): drop the negative-contract route from the admission c…
lesnik512 Aug 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,17 @@ effect. State the numbers, not "benchmarked".

### Before merging

- [ ] **Behaviour changed?** Hand-edit the matching `architecture/<capability>.md`
**in this PR**, so the promotion is reviewed with the code. That edit is
what keeps `architecture/` true.
- [ ] **Behaviour changed?** If a wrong change here could pass silently, pin it with
a test whose name is the claim and whose docstring opens `INVARIANT:` and says
what breaks it. Do **not** write prose about mechanism — there is no page for
it. See [`planning/README.md`](../planning/README.md#where-a-fact-goes).
- [ ] **Adding a fact anywhere?** Run the admission check: derivable from
`modern_di/` → don't write it; enforceable → a test; a user needs it →
`docs/`; otherwise it does not get written.
- [ ] **Rejected an alternative** with reasoning that would otherwise be
re-litigated? File it in [`planning/decisions/`](../planning/decisions/)
with a revisit trigger — not here.
- [ ] **Found real work you are not doing now?** File it in
[`planning/deferred/`](../planning/deferred/), self-contained, with a
revisit trigger — not here.
- [ ] **New or sharpened domain term?** Update `architecture/glossary.md`.
- [ ] `just lint-ci` and `just test-ci` pass.
59 changes: 34 additions & 25 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,27 @@ This project uses `just` (task runner) and `uv` (package manager). The
or read it for every recipe and its intent. The non-obvious essentials:

- `just test [args]` — pytest, **no coverage**; targeted runs won't trip the
gate. Passes args through: `just test tests/providers/test_factory.py -k test_name`.
gate. Passes args through: `just test tests/providers/test_factory.py -k <substring>`.
- `just test-ci` — the **gated** full run (100% line coverage); this is what CI runs.
- `just lint` (autofix) / `just lint-ci` (no autofix; also validates planning bundles).
- `just check-planning` validates `planning/deferred/` + `planning/decisions/` frontmatter; `just index` prints that listing.

## Architecture

> Quick orientation only. The authoritative, code-current account of each capability lives in [`architecture/`](architecture/) — one file per capability. **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.

- **Scope** — `IntEnum`, `APP=1 → SESSION=2 → REQUEST=3 → ACTION=4 → STEP=5`. A provider resolves only from a container of the same or deeper (higher-int) scope; otherwise a clear error is raised.
- **Container** — the central object. Root: `Container(scope=Scope.APP, groups=[MyGroup])`; children via `container.build_child_container(scope=Scope.REQUEST, context={...})`. Children share the parent's providers/overrides registries; cache/context are per-container. Pass `validate=True` (or call `container.validate()`) for cycle + transitive-scope checks.

Where the detail lives — read the matching capability file before changing behavior:

| File | Covers |
|---|---|
| [architecture/scopes.md](architecture/scopes.md) | `Scope` hierarchy + the resolution rule |
| [architecture/containers.md](architecture/containers.md) | `Container`, registries, child containers, lifecycle/finalizers |
| [architecture/providers.md](architecture/providers.md) | `Group`, `Factory`/caching, `ContextProvider`, `Alias` |
| [architecture/resolution.md](architecture/resolution.md) | how `resolve()` wires deps from type hints |
| [architecture/validation.md](architecture/validation.md) | `validate()` cycle + scope checks |
| [architecture/testing-and-overrides.md](architecture/testing-and-overrides.md) | overrides + the `modern-di-pytest` integration |
| [architecture/integration-kit.md](architecture/integration-kit.md) | framework-agnostic primitives for building an integration adapter |
| [architecture/concurrency.md](architecture/concurrency.md) | thread-safety + free-threaded (PEP 703) support, at Beta |
| [architecture/performance.md](architecture/performance.md) | why the warm resolve path is shaped as it is: the per-node frame budget, inlined memo hits, how to measure |
There is no separate capability-page home for behavior detail — it lives in the code and its
`INVARIANT:`-marked tests. Before writing prose about a capability, run the admission check in
[`planning/README.md`](planning/README.md#where-a-fact-goes).

### Key files

Every module under `modern_di/` except the package `__init__.py` re-exports. If
you add a module, add it here.

- `modern_di/container.py` — Container class, the main entry point
- `modern_di/resolver_compiler.py` — the **single resolve path**: one flat closure compiled per provider, memoized on the registry. Each resolver front-guards its own override, navigates its scope once, and inlines the kwargs build and creator call to hold the per-node frame budget at 1 — the rationale lives in [architecture/performance.md](architecture/performance.md), and the budget is enforced by a test, so **do not extract a helper from these closures**. A new provider type must add a branch here or `compile_resolver` raises
- `modern_di/resolver_compiler.py` — the **single resolve path**: one flat closure compiled per provider, memoized on the registry. Each resolver front-guards its own override, navigates its scope once, and inlines the kwargs build and creator call to hold the per-node frame budget at 1 — the rationale lives in `test_resolve_costs_exactly_one_resolver_frame_per_node`, so **do not extract a helper from these closures**. A new provider type must add a branch here or `compile_resolver` raises
- `modern_di/wiring.py` — `WiringPlan`: partitions a creator's parsed parameters into provider / static / context buckets plus `unwireable`. A pure function of its inputs (no cache, scope, or live context), so it runs outside the container lock and is exercisable without a Container
- `modern_di/providers/factory.py` — Factory and CacheSettings (singleton pattern via caching + optional finalizer)
- `modern_di/providers/context_provider.py` — ContextProvider for runtime-injected values
Expand All @@ -55,7 +43,7 @@ you add a module, add it here.
- `modern_di/types.py` — the `UNSET` sentinel (`UnsetType`) that separates "not passed" from "explicitly `None`", plus the shared TypeVars. Load-bearing on the resolve path: it is the miss marker for both the override lookup and the cache slot
- `modern_di/types_parser.py` — Signature introspection engine (parses type hints for DI wiring)
- `modern_di/dependency_graph.py` — the one static graph walk (`DependencyGraph.walk`), consumed by `validate()` and the runtime cycle guard. Explicit-stack, never recursive: a caller runs it inside a `RecursionError` handler near CPython's stack limit. It walks `WiringPlan.edges`, so what `validate()` traverses is exactly what `resolve()` follows
- `modern_di/registries/` — the four registries: `providers_registry` (type → provider, plus the shared plan/resolver memos) and `overrides_registry` are shared tree-wide; `cache_registry` and `context_registry` are per-container. See [architecture/containers.md](architecture/containers.md)
- `modern_di/registries/` — the four registries: `providers_registry` (type → provider, plus the shared plan/resolver memos) and `overrides_registry` are shared tree-wide; `cache_registry` and `context_registry` are per-container
- `modern_di/integrations.py` — the integration kit: Layer 1 (`bind`, `classify_connection`) derives a child container's scope/context from `ContextProvider`s; Layer 2 (`Marker`, `from_di`, `parse_markers`, `resolve_markers`) is the `Annotated`-marker injector. Neither layer wraps `build_child_container`
- `modern_di/suggester.py` — what a suggestion *is* (the `Suggestion` record) and how to *find* one: `suggest(requested_type, providers)` owns the policy (hierarchy hints, typo matching, cap, ordering); `close_matches` is the shared difflib primitive (also used by `UnknownFactoryKwargError`). Carries no formatting
- `modern_di/scope.py` — Scope enum
Expand All @@ -69,7 +57,7 @@ you add a module, add it here.
- Overrides: `container.override(provider, mock_obj)` / `container.reset_override(provider)`
- Scope chains: `app_container.build_child_container(scope=Scope.REQUEST)`
- `asyncio_mode = "auto"` — async test functions work without extra markers
- The **`modern-di-pytest`** integration (a sibling repo/package, not a dependency here) → [architecture/testing-and-overrides.md](architecture/testing-and-overrides.md)
- The **`modern-di-pytest`** integration (a sibling repo/package, not a dependency here)

## Workflow

Expand All @@ -82,11 +70,12 @@ ships a conventional-commit title.
Two things outlive the PR and are committed under `planning/`: an alternative
**rejected** with reasoning goes to `planning/decisions/`, and real work **not
scheduled** goes to `planning/deferred/` (self-contained, with a revisit
trigger). `architecture/` (repo root) stays the living **truth home** — a
behaviour change hand-edits the matching capability page in the same PR.
See [`planning/README.md`](planning/README.md) for the full convention; it is a
documented local deviation from `planning-convention` 2.2.0. The `## Architecture`
section above is quick orientation; `architecture/` holds the authoritative account.
trigger). There is no separate truth-home directory — the living truth about
behaviour is the code and its `INVARIANT:`-marked tests, and a behaviour change
is reviewed with the diff, not promoted to a page. See
[`planning/README.md`](planning/README.md) for the full convention, including
the admission check that decides where a given fact belongs; it is a documented
local deviation from `planning-convention` 2.2.0.

- **Cutting a release (maintainers)** is tag-driven via
[`.github/workflows/release.yml`](.github/workflows/release.yml): write the
Expand Down Expand Up @@ -114,4 +103,24 @@ section above is quick orientation; `architecture/` holds the authoritative acco
- Docstrings: public API documents the contract; internal helpers get a
one-line contract, plus at most 1–2 lines for a genuinely non-obvious
constraint. Never narrate implementation or justify code to a reviewer —
cross-file rationale lives in `architecture/`.
cross-file rationale lives in an `INVARIANT:` test docstring or
`planning/decisions/`.

## Vocabulary

A term is listed only when there is a synonym to reject, or a meaning subtle enough that code and
docs must agree on it.

- **Container** — owns the registries and resolves within a scope. *Avoid:* injector.
- **Provider** — a declaration of *how to produce* a dependency; the recipe, not the value. *Avoid:* service,
dependency.
- **Scope** — one band in the container hierarchy. *Avoid:* lifetime, layer.
- **Group** — a non-instantiable namespace class declaring providers. *Avoid:* module.
- **Resolution** — producing a value from its provider. *Avoid:* injection (reserve that for passing a resolved
value into a handler).
- **Override** — a test-time replacement of a resolved value. *Avoid:* mock, patch (an override supplies a
concrete value; it does not wrap or spy).
- **Bound type** — the type a provider is registered under. *Avoid:* registered type, return type.
- **Wiring plan** — the partition of a creator's parameters by how each is satisfied. *Avoid:* compiled kwargs.
- **Finalizer** — a cleanup callback on a cached provider, run LIFO at close. *Avoid:* teardown, destructor.
- **Connection** — the framework object a unit of work carries. *Avoid:* request (too HTTP-specific).
2 changes: 1 addition & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ lint-ci:
uv run python planning/links.py

# Check every relative Markdown link and heading anchor. `mkdocs --strict` only sees
# docs/; architecture/ and planning/ live outside docs_dir and are read on GitHub.
# docs/; planning/ lives outside docs_dir and is read on GitHub.
check-links:
uv run python planning/links.py

Expand Down
51 changes: 0 additions & 51 deletions architecture/README.md

This file was deleted.

Loading
Loading