diff --git a/.gitignore b/.gitignore index d5967b0..1ccf52a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,5 +19,5 @@ dist/ .python-version .venv uv.lock -plan.md +/plan.md site/ diff --git a/CLAUDE.md b/CLAUDE.md index af1de4f..cb1324d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,6 +32,8 @@ uv run pytest ## Architecture +> Quick orientation. The authoritative, code-current account of each capability lives in [`architecture/`](architecture/). + ### Scope hierarchy `Scope` is an `IntEnum` with five levels: `APP=1 → SESSION=2 → REQUEST=3 → ACTION=4 → STEP=5`. Providers are bound to a scope; a provider can only be resolved from a container of the same or deeper (higher int) scope. Trying to resolve a REQUEST-scoped provider from an APP container raises a clear error. @@ -40,7 +42,7 @@ uv run pytest `Container` is the central object. A root container is created with `Container(scope=Scope.APP, groups=[MyGroup])`. Child containers are created via `container.build_child_container(scope=Scope.REQUEST, context={...})`. Child containers share the parent's `providers_registry` and `overrides_registry` but have their own `cache_registry` and `context_registry`. -Pass `validate=True` to run cycle detection on the provider graph at container creation time (zero cost when disabled). Can also be called explicitly via `container.validate()`. +Pass `validate=True` to check the provider graph at container creation time — cycle detection plus transitive scope validation through aliases (via `effective_scope`); zero cost when disabled. Can also be called explicitly via `container.validate()`. ### Group and Provider declaration @@ -59,7 +61,7 @@ class MyGroup(Group): 1. `container.resolve(SomeType)` → looks up type in `providers_registry` → calls `resolve_provider(provider)` 2. `resolve_provider` checks `overrides_registry` first (returns override immediately if found) -3. Finds the container at the correct scope via `find_container(scope)` walking the parent chain +3. Finds the container at the correct scope via `find_container(scope)`, an O(1) lookup in the precomputed `scope_map` 4. Checks `cache_registry`; if cached, returns immediately 5. Compiles kwargs: for each parsed parameter, finds a matching provider by type and resolves it recursively 6. Calls the creator, stores result in cache if `cache_settings` configured @@ -82,7 +84,8 @@ class MyGroup(Group): - `modern_di/types_parser.py` — Signature introspection engine (parses type hints for DI wiring) - `modern_di/scope.py` — Scope enum - `modern_di/group.py` — Group base class for provider namespaces -- `modern_di/errors.py` — Error message templates +- `modern_di/exceptions.py` — exception class hierarchy (`ModernDIError` → `ContainerError`/`ResolutionError`/`RegistrationError` subclasses) +- `modern_di/errors.py` — error message-template strings used by those exceptions ### Testing patterns @@ -93,6 +96,23 @@ class MyGroup(Group): - `asyncio_mode = "auto"` in pytest config — async test functions work without extra markers - Downstream projects can install **`modern-di-pytest`** to expose DI dependencies as pytest fixtures. It ships two callables: `modern_di_fixture(type_or_provider)` for single fixtures and `expose(*groups)` to bulk-generate one fixture per provider across one or more `Group` subclasses (duplicate attribute names raise `ValueError`). The package itself does **not** depend on `modern-di-pytest`; the integration lives in a sibling repository (`modern-python/modern-di-pytest`). +## Workflow + +Planning follows a portable two-axis convention (shared with +`faststream-outbox`); full details in [`planning/README.md`](planning/README.md). + +- **`architecture/`** (repo root) is the **truth home** — living capability + prose, the promotion target on every ship. The `## Architecture` section + above is quick orientation; `architecture/` holds the authoritative, + up-to-date account. +- **`planning/changes/{active,archive}//`** are change + bundles: `design.md` + `plan.md` (full lane), or `change.md` (lightweight). + Tiny changes (typo, dep bump, CI tweak) skip bundles entirely. +- Templates live in [`planning/_templates/`](planning/_templates/). +- **Shipping a change** hand-edits the affected `architecture/.md`, + then moves the bundle from `active/` to `archive/` with `status: shipped`, + `pr:`, and `outcome:` filled. + ## Code Style - Line length: 120 characters diff --git a/architecture/README.md b/architecture/README.md new file mode 100644 index 0000000..0893a50 --- /dev/null +++ b/architecture/README.md @@ -0,0 +1,26 @@ +# Architecture + +The living truth about what `modern-di` does **now** — one file per capability, +updated by hand whenever a change ships. The *why* and *how it got here* live in +[`../planning/changes/`](../planning/changes/); this directory is the present. + +These files carry **no frontmatter** — they are prose, dated by git. + +## Capabilities + +- [scopes.md](scopes.md) — the `Scope` hierarchy and the resolution rule. +- [containers.md](containers.md) — the `Container`, its registries, child + containers, and lifecycle. +- [providers.md](providers.md) — `Group`, `Factory`/caching, `ContextProvider`, + `Alias`. +- [resolution.md](resolution.md) — how `resolve()` wires dependencies from type + hints. +- [validation.md](validation.md) — `validate()` cycle and scope checks. +- [testing-and-overrides.md](testing-and-overrides.md) — overrides and the + `modern-di-pytest` integration. + +## Promotion rule + +Shipping a change hand-edits the affected capability file(s) here to match the +new reality, then archives the change bundle under +[`../planning/changes/archive/`](../planning/changes/archive/). diff --git a/architecture/containers.md b/architecture/containers.md new file mode 100644 index 0000000..5bf1932 --- /dev/null +++ b/architecture/containers.md @@ -0,0 +1,136 @@ +# Containers + +`Container` is the central entry point for the dependency injection system. Every interaction with +providers — resolution, scoping, overriding — flows through a `Container`. + +## Creating a root container + +```python +from modern_di import Container, Scope +from modern_di.group import Group + +class MyGroup(Group): + ... + +container = Container(scope=Scope.APP, groups=[MyGroup]) +``` + +Constructor parameters: + +| Parameter | Default | Effect | +|---|---|---| +| `scope` | `Scope.APP` | The scope level this container occupies. Must be an `IntEnum`. | +| `groups` | `None` | One or more `Group` subclasses whose providers are registered into `providers_registry`. | +| `context` | `None` | Mapping of `type → object` pre-populated into `context_registry`. | +| `use_lock` | `True` | Wraps resolution in a `threading.RLock`; set `False` for single-threaded use. | +| `validate` | `False` | If `True`, runs cycle and scope-ordering checks immediately after construction. | + +A root container (no `parent_container`) creates fresh `ProvidersRegistry` and `OverridesRegistry` +instances. It also auto-registers `container_provider` under the `Container` type so that any +provider declaring `Container` as a dependency receives the resolving container instance directly. + +## Child containers + +```python +child = container.build_child_container(scope=Scope.REQUEST, context={MyRequest: request_obj}) +``` + +`build_child_container` creates a new `Container` whose `parent_container` is the current one. +Rules: + +- The child's scope must be strictly greater (deeper) than the parent's scope. Passing `scope=None` + auto-increments to the next `IntEnum` value; if the parent is already at the maximum scope, + `MaxScopeReachedError` is raised. +- Building a child from a closed container raises `ContainerClosedError`. + +The child gets its own, independent `scope_map` dict that includes all ancestors plus itself, +enabling `find_container(scope)` to walk up to any ancestor scope in O(1). + +## Registry sharing + +The four registries split into two categories: + +| Registry | Shared across container tree? | Purpose | +|---|---|---| +| `ProvidersRegistry` | Yes — all containers share one instance | Maps `type → AbstractProvider`; populated once at root construction time from `groups`. | +| `OverridesRegistry` | Yes — all containers share one instance | Maps `provider_id → override object`; used by tests to substitute real instances. | +| `CacheRegistry` | No — each container has its own | Maps `provider_id → CacheItem`; stores resolved singleton instances and compiled kwargs for this scope level. | +| `ContextRegistry` | No — each container has its own | Maps `type → runtime object`; populated via `context=` at construction or `container.set_context()` after the fact. | + +Because `ProvidersRegistry` and `OverridesRegistry` are shared, registering a group or setting an +override on any container in the tree is immediately visible to all other containers in the same +tree. + +## `container_provider` + +A singleton instance of `_ContainerProvider` is registered under the `Container` type in the +`ProvidersRegistry` of every root container. Its `resolve` method returns the container passed to +it, so resolving `Container` from any child yields that child container — not the root. This lets +providers at any scope depth receive the container they are resolved from as a plain constructor +dependency. + +`_ContainerProvider` has `scope=Scope.APP` and `bound_type=None` (it is registered explicitly +under `Container` rather than inferred from a type annotation). + +## Lifecycle: close and reopen + +### Closing + +`close_sync()` and `close_async()` both do two things in order: + +1. **Finalizers** — iterate over the container's `CacheRegistry._creation_order` list in **reverse + (LIFO)** order and call each `CacheItem`'s finalizer if one is configured and the item has not + already been finalized. On `close_sync()`, any item whose finalizer is async raises + `AsyncFinalizerInSyncCloseError`; those items are left in `_creation_order` so a subsequent + `close_async()` can clean them up. + +2. **`closed = True`** — always set in a `finally` block, even if finalizers raised. Subsequent + calls to `build_child_container` or `resolve_provider` raise `ContainerClosedError`. + +Additionally, when `close_sync()` or `close_async()` is called on a **root** container (one with +no `parent_container`), all overrides are cleared from the shared `OverridesRegistry` before the +cache is finalized. + +Child containers only finalize their own `CacheRegistry`; the shared `OverridesRegistry` is left +alone. + +### `clear_cache` per `CacheItem` + +After running a finalizer, each `CacheItem` calls `_clear()`. This checks the item's +`CacheSettings.clear_cache` flag. If `True`, the cached instance is removed (`cache` is reset to +`UNSET`) and `finalized` is reset to `False`, leaving the slot ready to be re-populated on the +next resolution. If `False` (or no `CacheSettings`), the cached value is retained after +finalization; the item is simply marked finalized. + +### Reopen (context-manager protocol) + +`Container` implements both sync (`__enter__` / `__exit__`) and async (`__aenter__` / `__aexit__`) +context managers. + +- `__enter__` / `__aenter__` set `self.closed = False` and return `self`. No other state is reset; + `cache_registry` and `context_registry` are left as-is. +- `__exit__` calls `close_sync()`; `__aexit__` calls `close_async()`. + +Concretely: using the same container object as a context manager a second time reopens it (clears +`closed`), resolves providers fresh if `clear_cache=True` was set on their `CacheSettings` (since +close removed those cached values), and then closes it again on exit. Providers whose +`CacheSettings.clear_cache` is `False` retain their cached instances across reopen cycles. + +## `validate()` + +`container.validate()` runs a depth-first traversal of all providers in `ProvidersRegistry`, +detecting circular dependencies and scope-ordering violations (a provider at a wider scope +depending on one at a narrower scope). Pass `validate=True` to the constructor to run this at +creation time, or call `container.validate()` explicitly at any point. It raises +`ValidationFailedError` with all collected errors if any are found. + +## `set_context()` + +```python +container.set_context(MyRequest, request_obj) +``` + +Registers a runtime value directly into the container's `ContextRegistry`. Also invalidates all +compiled kwargs in the container's `CacheRegistry` (resets `kwargs_compiled`, `provider_kwargs`, +and `static_kwargs` on every `CacheItem`) so that subsequent resolutions pick up the new context +value rather than using a stale compiled-kwargs snapshot. diff --git a/architecture/providers.md b/architecture/providers.md new file mode 100644 index 0000000..025f400 --- /dev/null +++ b/architecture/providers.md @@ -0,0 +1,166 @@ +# Provider Catalog + +This document describes every provider type in `modern-di`. It is the authoritative reference; if anything here +conflicts with other documentation, the code governs. + +--- + +## `Group` — provider namespace + +`Group` is a non-instantiable base class. Attempting to instantiate it (or any subclass) raises +`GroupInstantiationError`. Its sole purpose is to act as a namespace for declaring providers as class-level +attributes: + +```python +from modern_di import providers, Group, Scope + +class AppProviders(Group): + db_pool = providers.Factory(scope=Scope.APP, creator=create_pool) + user_repo = providers.Factory(scope=Scope.REQUEST, creator=UserRepository) +``` + +`Group.get_providers()` walks the MRO and collects every class attribute that is an `AbstractProvider` instance, +respecting inheritance order and de-duplicating by name. + +--- + +## `Factory` — the universal provider + +`Factory` is the main building block. Every provider that calls a constructor or factory function is a `Factory`. + +### Signature + +```python +Factory( + *, + scope: IntEnum = Scope.APP, + creator: Callable[..., T], + bound_type: type | None = UNSET, + kwargs: dict[str, Any] | None = None, + cache_settings: CacheSettings[T] | None = None, + skip_creator_parsing: bool = False, +) +``` + +### Declaration-time signature parsing + +When `skip_creator_parsing=False` (the default), `Factory.__init__` calls `types_parser.parse_creator(creator)` +immediately. This extracts the return type (used as the provider's `bound_type` unless overridden) and a mapping +of parameter names to `SignatureItem` descriptors. Dependency resolution is therefore type-driven: at resolution +time each parameter is matched against the container's `providers_registry` by its annotated type. + +If `bound_type` is supplied explicitly it overrides the inferred return type (useful when the creator returns a +protocol or base class narrower than the concrete type). + +### Recursive resolution + +When a `Factory` is resolved, `_compile_kwargs` iterates the parsed parameter map. For each parameter it looks up +a matching provider by type in the registry and recurses into `container.resolve_provider(dep_provider)`. +Resolution errors are annotated with a breadcrumb describing the current factory, so the full chain appears in the +exception. + +### Static kwargs — `kwargs={}` + +Pass `kwargs` to supply static (non-DI-resolved) arguments that bypass type-based resolution. These are merged +last, overriding any provider-resolved value for the same key. Supplying a key that does not appear in the +creator's signature (and whose creator has no `**kwargs`) raises `UnknownFactoryKwargError` at declaration time. + +### `skip_creator_parsing=True` + +Disables signature introspection entirely — useful for callables whose signatures cannot be reflected (built-in C +extensions, `functools.partial`, etc.). When set without an explicit `bound_type`, a `UserWarning` is emitted +because the provider cannot be resolved by type. + +--- + +## `CacheSettings` — singleton behavior + +There is **no separate `Singleton` class**. Singleton behavior is opted into by passing a `CacheSettings` instance +to `Factory(cache_settings=...)`: + +```python +providers.Factory(scope=Scope.APP, creator=Database, cache_settings=providers.CacheSettings()) +``` + +`CacheSettings` is a `dataclass` with the following fields: + +| Field | Type | Default | Purpose | +|---|---|---|---| +| `clear_cache` | `bool` | `True` | Whether the cached instance is evicted when the container closes. | +| `finalizer` | `Callable[[T], None \| Awaitable[None]] \| None` | `None` | Optional teardown called on container close, before cache eviction. | +| `is_async_finalizer` | `bool` | *(computed)* | Set automatically in `__post_init__`; `True` when `finalizer` is a coroutine function. | + +`is_async_finalizer` is not an init parameter — it is derived by `inspect.iscoroutinefunction(finalizer)` in +`__post_init__`. The container uses it to decide whether to `await` the finalizer. + +Without `cache_settings`, `Factory.resolve` calls the creator on every resolution and returns a fresh instance +each time. + +--- + +## `ContextProvider` — runtime-injected values + +`ContextProvider` holds a value that is supplied at container-creation time via the `context` mapping rather than +being constructed by a factory: + +```python +providers.ContextProvider(scope=Scope.REQUEST, context_type=HttpRequest) +``` + +At resolution time it looks the value up in the container's `context_registry` for the matching scope. If no +value was supplied (the key is absent), `resolve` returns `None`. `Factory._compile_kwargs` handles the +absent-context case explicitly: if the dependent parameter has a default or is nullable it is silently satisfied; +otherwise an `ArgumentResolutionError` is raised. + +--- + +## `Alias` — re-exporting a type under a different name + +`Alias` delegates resolution to another registered provider, located by the source type: + +```python +providers.Alias(source_type=ConcreteDatabase, bound_type=DatabaseProtocol) +``` + +This lets code that depends on `DatabaseProtocol` receive the `ConcreteDatabase` instance without the registry +needing a separate `Factory` for the protocol. `Alias.resolve` calls `container.resolve_provider(source_provider)`, +so caching and lifecycle are fully governed by the source. + +`Alias.effective_scope` follows alias chains transitively to the terminal non-alias provider and returns that +provider's scope. This is what `Container.validate()` and scope-error reporting use — the alias's own `scope` +attribute is only a stored default. + +### Deprecated `scope=` parameter + +Passing `scope=` to `Alias.__init__` emits a `DeprecationWarning`: + +> "The `scope` parameter of Alias is deprecated and ignored: an alias's effective scope is derived from its +> source. It will be removed in a future release." + +The parameter is accepted for backwards compatibility but has no effect on resolution. It will be removed in a +future release. + +--- + +## `container_provider` — the container itself + +`container_provider` is a pre-built singleton exported from `modern_di.providers`. It is automatically registered +in every container and resolves to the `Container` instance at the appropriate scope. Use it when a class needs to +accept the container as a dependency. + +--- + +## Public exports + +All provider types and `CacheSettings` are re-exported from `modern_di.providers`: + +```python +from modern_di import providers + +providers.Factory +providers.CacheSettings +providers.ContextProvider +providers.Alias +providers.AbstractProvider +providers.container_provider +``` diff --git a/architecture/resolution.md b/architecture/resolution.md new file mode 100644 index 0000000..02a30e3 --- /dev/null +++ b/architecture/resolution.md @@ -0,0 +1,125 @@ +# Resolution + +How `modern-di` wires an object graph from type hints — from the first `resolve()` call to the returned instance. + +## Entry points + +There are two ways to trigger resolution from a `Container`: + +- `container.resolve(SomeType)` — resolves by type. Looks up `SomeType` in `providers_registry`; raises + `ProviderNotRegisteredError` (with closest-match suggestions) if no provider is registered for that type. Then + delegates to `resolve_provider`. +- `container.resolve_provider(provider)` — resolves by provider reference directly, skipping the registry lookup. + +## Step 1 — Override short-circuit + +`resolve_provider` is the single choke-point through which every resolution passes. Before touching scope or cache it +checks the override registry: + +```python +if ( + self.overrides_registry.overrides + and (override := self.overrides_registry.fetch_override(provider.provider_id)) is not types.UNSET +): + return override +``` + +The outer guard (`self.overrides_registry.overrides`) is a cheap truthiness check on a dict; `fetch_override` is only +called when at least one override exists. If an override is registered for this provider, it is returned immediately — +no scope walk, no cache check, no creator call. + +## Step 2 — Scope walk (inside Factory.resolve) + +After the override check, `resolve_provider` calls `provider.resolve(self)`. For `Factory` the first thing `resolve` +does is walk to the container at the provider's declared scope: + +```python +container = container.find_container(self.scope) +``` + +`find_container` looks up `self.scope_map`, a dict built at container construction time that maps each scope level to +the container at that level. If the scope is deeper than the current container (not yet initialized) a +`ScopeNotInitializedError` is raised; if it was skipped when building the child chain, a `ScopeSkippedError` is raised. +From this point all cache and context operations use the scope-correct container. + +## Step 3 — Cache hit + +With the correct container in hand, `Factory.resolve` fetches (or creates) a `CacheItem` for this provider: + +```python +cache_item = container.cache_registry.fetch_cache_item(self) + +if self.cache_settings and cache_item.cache is not types.UNSET: + return cache_item.cache +``` + +If `cache_settings` is configured **and** the cache slot already holds a value, the cached instance is returned +immediately. (A `Factory` with no `cache_settings` always re-runs the creator.) + +## Step 4 — kwargs compilation + +If no cached instance is returned, `Factory` compiles the keyword arguments needed to call the creator. This is done +once per container per provider via `_ensure_kwargs_cached`, which stores the split result on `CacheItem` so subsequent +calls (within the same container lifetime) skip recompilation. + +`_compile_kwargs` iterates over `_parsed_kwargs` — the `SignatureItem` map produced at provider-declaration time by +`types_parser.parse_creator`. For each parameter: + +1. **Provider lookup** — `_find_dep_provider` searches `providers_registry` for a provider matching the parameter's + resolved type (`arg_type`) or, for union types, any of the union members (`args`). Self-references are excluded. + +2. **Context provider with missing value** — if the found provider is a `ContextProvider` and it has no value set in + the current context registry, the parameter falls through to the nullable/default logic below rather than resolving + the provider. + +3. **No provider found / missing context value** — resolution falls back in this order: + - If the parameter has a default, it is omitted from the compiled kwargs (the creator's own default applies). + - If `SignatureItem.is_nullable` is `True` (the annotation included `None` in a union, e.g. `X | None` or + `Optional[X]`), the parameter is set to `None`. + - Otherwise, `ArgumentResolutionError` is raised. + +4. **Static kwargs override** — after the loop, `result.update(self._kwargs)` applies any static kwargs supplied at + provider-declaration time. These are written last, so they overwrite any provider resolved for the same key. This + is the mechanism for supplying literal values that bypass type-based wiring. + +The compiled result is split into `provider_kwargs` (values that are `AbstractProvider` instances, to be resolved +recursively) and `static_kwargs` (plain values including `None` injected for nullable parameters). + +## Step 5 — Recursive resolution + +With compiled kwargs in hand: + +```python +resolved_kwargs = dict(static_kwargs) +for k, v in provider_kwargs.items(): + resolved_kwargs[k] = container.resolve_provider(v) +``` + +Each dependency provider is resolved by calling back into `container.resolve_provider`, which re-enters this same +sequence from Step 1 — override check, then `provider.resolve(container)`. The recursion bottoms out at providers with +no dependencies or at already-cached instances. + +## Step 6 — Creator call and caching + +```python +instance = self._call_creator(resolved_kwargs) +``` + +If `cache_settings` is `None`, the instance is returned immediately with no caching. If `cache_settings` is set, a +lock (when `use_lock=True`) guards a double-checked read of the cache slot to handle concurrent first-resolves, then +the instance is stored and returned. + +## Nullable wiring + +`types_parser.SignatureItem` carries an `is_nullable: bool` field. It is set to `True` when the parameter annotation +is a union that includes `NoneType` — that is, `X | None`, `Optional[X]`, or any union spelled with `typing.Union` +that contains `None`. When `_compile_kwargs` cannot find a provider for the parameter (or finds a `ContextProvider` +with no value set) and the parameter has no default, `is_nullable=True` causes `None` to be injected rather than +raising an error. + +## Thread safety + +When `use_lock=True` (the default), the container holds a `threading.RLock`. The lock is acquired only around the +cache-write critical section inside `Factory.resolve` — kwargs compilation and recursive resolution happen outside the +lock. The double-checked locking pattern ensures that if two threads race to resolve the same uncached provider, only +one calls the creator and the other uses the freshly stored result. diff --git a/architecture/scopes.md b/architecture/scopes.md new file mode 100644 index 0000000..c798816 --- /dev/null +++ b/architecture/scopes.md @@ -0,0 +1,106 @@ +# Scopes + +`Scope` is an `IntEnum` defined in `modern_di/scope.py`. It has five named levels: + +``` +APP = 1 → SESSION = 2 → REQUEST = 3 → ACTION = 4 → STEP = 5 +``` + +Higher integer values represent deeper (more short-lived) scopes. The ordering is significant: the integer value +determines both the scope hierarchy and the validity rules for provider resolution. + +## Resolution rule + +Every provider is bound to a scope at declaration time. The rule is: + +> A provider bound to scope **S** may only be resolved from a container whose scope is **S or deeper** +> (i.e., `container.scope >= provider.scope`). + +Attempting to resolve a provider from a container whose scope is shallower than the provider's scope raises one of +two exceptions, depending on what went wrong: + +- **`ScopeNotInitializedError`** — raised when the required scope is deeper than the resolving container's scope + (the child container for that scope has not been built yet). Message shape: + + ``` + Provider of scope {provider_scope} cannot be resolved in container of scope {container_scope}. + ``` + +- **`ScopeSkippedError`** — raised when the required scope is shallower than the resolving container's scope but + is not present anywhere in the ancestor chain (i.e., the chain was started at a scope that skipped it). Message + shape: + + ``` + No {provider_scope}-scope container exists in this chain; this chain starts at {container_scope}. + Build a {provider_scope}-scope container as the root. + ``` + +Both exceptions inherit from `ContainerError → ModernDIError → RuntimeError`. + +## How the container locates the right-scope container + +Each `Container` maintains a `scope_map: dict[IntEnum, Container]`. When a root container is created, the map is +`{scope: self}`. Each child container extends the map: `{**parent.scope_map, child_scope: child}`. + +`Container.find_container(scope)` performs the lookup: + +1. If `scope` is in `scope_map`, return the corresponding container immediately — no tree walk needed. +2. If `scope` is not in `scope_map` and `scope > self.scope`, raise `ScopeNotInitializedError` (the required + child container has not been built yet). +3. If `scope` is not in `scope_map` and `scope <= self.scope`, raise `ScopeSkippedError` (the scope was + never present in this chain). + +The `scope_map` is built incrementally at construction time, so lookups are O(1). There is no runtime +parent-chain traversal during resolution. + +## Custom scopes + +`Scope` is a convenience enum, but `Container` accepts any `enum.IntEnum` member as its scope. Teams that need +more levels (or different names) can define their own `IntEnum` and use it throughout. The same integer-ordering +rules apply. Passing a non-`IntEnum` value raises `InvalidScopeTypeError`. + +## Worked example + +```python +from modern_di import Container, Scope +from modern_di import providers + +class AppGroup(Group): + # Resolved once and cached for the lifetime of the app container. + db_pool = providers.Factory(scope=Scope.APP, creator=DatabasePool, cache_settings=CacheSettings()) + + # Resolved once per request container. + current_user = providers.Factory(scope=Scope.REQUEST, creator=UserFromRequest) + +# Root container at APP scope. +app_container = Container(scope=Scope.APP, groups=[AppGroup]) + +# Works: db_pool is APP-scoped, container is APP-scoped (same scope). +pool = app_container.resolve(DatabasePool) + +# Fails: current_user is REQUEST-scoped, but the container is APP-scoped (too shallow). +# Raises ScopeNotInitializedError: +# "Provider of scope REQUEST cannot be resolved in container of scope APP." +app_container.resolve(UserFromRequest) # raises + +# Build a child container for the request boundary. +request_container = app_container.build_child_container(scope=Scope.REQUEST, context={...}) + +# Works: current_user is REQUEST-scoped, container is REQUEST-scoped. +user = request_container.resolve(UserFromRequest) + +# Works: db_pool is APP-scoped; find_container(APP) returns the parent app_container via scope_map. +pool_again = request_container.resolve(DatabasePool) +``` + +`build_child_container` enforces that the child scope is strictly deeper than the parent scope. Passing a scope +with a lower or equal integer value raises `InvalidChildScopeError`: + +``` +Scope of child container cannot be {child_scope} if parent scope is {parent_scope} +(child scope value must be strictly greater than parent scope value). +Possible scopes are {allowed_scopes}. +``` + +Calling `build_child_container()` with no `scope` argument auto-increments to the next integer in the same +`IntEnum` class. If the parent is already at the maximum defined value, `MaxScopeReachedError` is raised. diff --git a/architecture/testing-and-overrides.md b/architecture/testing-and-overrides.md new file mode 100644 index 0000000..ad5f549 --- /dev/null +++ b/architecture/testing-and-overrides.md @@ -0,0 +1,138 @@ +# Testing and Overrides + +This document describes how `modern-di` supports test isolation via overrides and how tests wire up containers. For +the `modern-di-pytest` integration (a sibling package), see the dedicated section below. + +## Overrides + +### The OverridesRegistry + +`OverridesRegistry` is a thin dataclass holding a single `dict[int, Any]` keyed by `provider_id` (the integer +identity of the provider object). It is created once on the root container and **shared** across the entire container +tree — all child containers hold a reference to the same registry instance. + +### container.override and container.reset_override + +```python +container.override(provider: AbstractProvider[T], override_object: T) -> None +container.reset_override(provider: AbstractProvider[T] | None = None) -> None +``` + +`container.override(provider, obj)` writes `obj` into the shared `OverridesRegistry` under the provider's id. +`container.reset_override(provider)` removes that entry. Calling `reset_override()` with no argument (or `None`) +clears **all** overrides from the registry. + +Because the registry is shared, calling either method on a child container has the same effect as calling it on the +root — the override is visible tree-wide. `close_async` and `close_sync` on the root container also call +`reset_override()` automatically, clearing all overrides when the root is torn down. + +### How overrides short-circuit resolution + +`resolve_provider` checks the registry before delegating to the provider: + +```python +def resolve_provider(self, provider): + if self.overrides_registry.overrides and \ + (override := self.overrides_registry.fetch_override(provider.provider_id)) is not types.UNSET: + return override # returned immediately, no cache, no factory call + return provider.resolve(self) +``` + +The override value is returned directly, bypassing the scope check, cache lookup, and creator invocation. This +means the override object does not need to be an instance of the provider's declared type at runtime (Python does +not enforce it), but callers should pass a compatible object for type safety. See `resolution.md` for the full +resolution flow that overrides short-circuit. + +### Scope behaviour under overrides + +An overridden provider is resolved from whichever container `resolve_provider` is called on — the scope of the +original provider is irrelevant because the short-circuit fires before `find_container`. In practice this means a +REQUEST-scoped provider can be overridden and resolved from an APP container without raising +`ScopeNotInitializedError`, which is often what tests want. + +Overrides do not interact with the cache. If a singleton (cached factory) was already resolved before +`container.override(...)` is called, subsequent calls to `resolve_provider` return the override value, not the +cached instance. After `reset_override`, the original cache entry (if any) is still present and is returned again. + +## Testing patterns + +### Declaring providers + +Define a `Group` subclass with providers as class-level attributes and pass it to `Container`: + +```python +from modern_di import Container, Scope, providers +from modern_di.group import Group + +class MyGroup(Group): + service = providers.Factory(scope=Scope.APP, creator=MyService) + repo = providers.Factory(scope=Scope.APP, creator=MyRepo) + +container = Container(scope=Scope.APP, groups=[MyGroup]) +``` + +### Resolving in tests + +Resolve by provider reference (most precise — no type lookup): + +```python +instance = container.resolve_provider(MyGroup.service) +``` + +Resolve by type (matches the type registered in `ProvidersRegistry`): + +```python +instance = container.resolve(MyService) +``` + +Both methods go through `resolve_provider` and therefore respect overrides. + +### Testing scope chains + +Build child containers to test providers that require a deeper scope: + +```python +app_container = Container(scope=Scope.APP, groups=[MyGroup]) +request_container = app_container.build_child_container(scope=Scope.REQUEST) +instance = request_container.resolve_provider(MyGroup.request_scoped_service) +request_container.close_sync() +``` + +Child containers share the parent's `providers_registry` and `overrides_registry` but have independent +`cache_registry` instances, so each child starts with a cold cache. + +### Injecting overrides + +```python +app_container = Container(scope=Scope.APP, groups=[MyGroup]) +app_container.override(MyGroup.repo, FakeRepo()) + +result = app_container.resolve_provider(MyGroup.service) # receives FakeRepo + +app_container.reset_override(MyGroup.repo) # restore +``` + +Overrides set on the app container are visible in all child containers built afterward (shared registry), and in +child containers already in existence too, since the registry object is the same reference. + +### Cleanup + +Call `container.reset_override()` (no argument) after a test to clear all overrides, or rely on +`close_sync`/`close_async` on the root container — both clear the registry automatically. + +## modern-di-pytest integration + +`modern-di-pytest` is a **separate package** in a sibling repository. `modern-di` does not depend on it. + +The package exposes two callables for turning DI providers into pytest fixtures: + +**`modern_di_fixture(type_or_provider)`** — creates a single pytest fixture that resolves the given type or +provider from a container fixture already present in the test session. + +**`expose(*groups)`** — bulk-generates one pytest fixture per provider across one or more `Group` subclasses. +Duplicate attribute names across the supplied groups raise `ValueError`. The generated fixtures are named after the +attribute and resolve the corresponding provider automatically. + +Both callables are meant to be used at module level (or in a `conftest.py`) to declare fixtures. At test time, +requesting a fixture by name resolves the provider through the normal `resolve_provider` path, which means overrides +applied to the container before the fixture is invoked are honoured. diff --git a/architecture/validation.md b/architecture/validation.md new file mode 100644 index 0000000..bbb0f00 --- /dev/null +++ b/architecture/validation.md @@ -0,0 +1,163 @@ +# Container Validation + +`Container.validate()` audits the static provider graph for wiring errors before any dependency is resolved. It +is the authoritative catch-all for three classes of bug: **circular dependencies**, **inverted scope +dependencies**, and **missing required dependencies**. + +## Enabling validation + +Pass `validate=True` when constructing a container to run validation immediately after all groups are registered: + +```python +container = Container(scope=Scope.APP, groups=[MyGroup], validate=True) +``` + +Or call `container.validate()` explicitly at any point after construction: + +```python +container = Container(scope=Scope.APP, groups=[MyGroup]) +container.validate() # raises ValidationFailedError if any issue found +``` + +When `validate=False` (the default), no graph traversal occurs and there is zero runtime cost. The `validate=True` +path is equivalent to `Container(...); container.validate()` — the call is made inside `__init__` only if the +flag is set. + +## What validate() checks + +`validate()` performs a depth-first search (DFS) over every provider in `providers_registry`. It collects **all +errors** across the entire walk before raising, so a single call surfaces all wiring bugs at once rather than +stopping at the first one. + +If any errors are found, `validate()` raises `exceptions.ValidationFailedError`, whose `.errors` attribute is a +list of all individual exceptions encountered. `ValidationFailedError.__str__` renders a human-readable count +plus per-error detail. + +### Circular dependencies + +During the DFS, a provider encountered a second time while it is still on the active path (i.e., it appears in +`visiting`) means a cycle exists. `validate()` records a `CircularDependencyError` with a `.cycle_path` list of +type names showing the loop (e.g., `["A", "B", "A"]`). The recursive walk does **not** continue into the cycle, +but the rest of the graph continues to be checked. + +### Inverted scope dependencies + +For every dependency edge `provider → dep`, `validate()` compares their **effective scopes** (see below). If +`dep`'s effective scope is strictly deeper than `provider`'s effective scope, the dependency is inverted: a +shallower-lived provider cannot hold a reference to a deeper-lived one. The error is recorded as +`InvalidScopeDependencyError`, which names the provider, the parameter, the dependent provider, and the offending +scopes. The walk continues into the dependency so further issues in that subtree are also surfaced. + +Error message template (from `errors.INVALID_SCOPE_DEPENDENCY_ERROR`): + +``` +Provider {provider_name} (scope {provider_scope}) declares parameter +{parameter_name!r} typed as a provider of {dep_name} at deeper scope +{dep_scope}. A provider cannot depend on a deeper-scoped provider. +``` + +### Missing required dependencies + +Before recursing into a provider's dependencies, `validate()` calls `provider.iter_validation_issues(container)` +and appends any returned exceptions to the error list. `Factory` implements this hook to yield +`ArgumentResolutionError` for each constructor parameter that has no matching provider in `providers_registry`, +no default value, and no static `kwargs` entry. + +## Effective scope and alias transparency + +`validate()`'s scope-ordering check uses `provider.effective_scope(container)` on both sides of every dependency +edge — not `provider.scope` directly. + +For most providers, `effective_scope` simply returns `self.scope`. `Alias` overrides it to follow the alias chain +to its terminal non-alias target and return **that provider's scope**: + +``` +Alias.effective_scope(container) + → follow chain: Alias → Alias → ... → concrete Factory + → return concrete_factory.scope +``` + +This makes validation transitive through aliases. Consider: + +``` +Factory(scope=APP, creator=Caller) # depends on IFace +Alias(source_type=Impl, bound_type=IFace) # no scope parameter +Factory(scope=REQUEST, creator=Impl) +``` + +The alias's effective scope is `REQUEST` (the scope of `Impl`). When `validate()` checks the `Caller → IFace` +edge, it compares `APP` against `REQUEST` and raises `InvalidScopeDependencyError`. Without `effective_scope`, +the alias's internal `scope` attribute (defaulting to `APP`) would mask the true depth of the dependency. + +Two edge cases in `Alias.effective_scope` are handled safely: + +- **Alias cycle**: if the same alias is encountered twice during the chain walk, the method falls back to + `self.scope` and returns immediately. The cycle itself is separately detected and reported by the DFS cycle + check. +- **Dangling source**: if the alias's source type is not registered, the method falls back to `self.scope`. The + dangling source is separately detected and reported by `iter_validation_issues` or by the dependency lookup + raising `AliasSourceNotRegisteredError` during the walk. + +## The deprecated `Alias(scope=...)` parameter + +`Alias` accepts a `scope` keyword argument that was historically intended to convey the alias's position in the +scope hierarchy. Because an alias is a transparent redirect — resolution always delegates to the source, and (as +of the `effective_scope` mechanism) validation now evaluates the source's scope transitively — the `scope` +parameter has no effect on either behavior. + +Passing `scope=` to `Alias` now emits a `DeprecationWarning`: + +``` +The `scope` parameter of Alias is deprecated and ignored: an alias's effective +scope is derived from its source. It will be removed in a future release. +``` + +The stored value is kept internally only so that cosmetic consumers (`__repr__`, registry suggestions) continue +to display it. The parameter is scheduled for removal in a future release. New code should omit it. + +## DFS algorithm summary + +``` +validate(): + visiting = set() # providers currently on the active path + visited = set() # providers fully processed + path = list() # ordered active path (for cycle reporting) + errors = list() + + for each provider in providers_registry: + _visit(provider) + + if errors: + raise ValidationFailedError(errors=errors) + +_visit(provider): + if provider in visited: return # already processed; skip + if provider in visiting: # back-edge → cycle + record CircularDependencyError + return # don't recurse into the cycle + + mark visiting; append to path + errors.extend(provider.iter_validation_issues(container)) + + provider_scope = provider.effective_scope(container) + for dep_name, dep_provider in provider.get_dependencies(container): + dep_scope = dep_provider.effective_scope(container) + if dep_scope > provider_scope: + record InvalidScopeDependencyError + _visit(dep_provider) # recurse regardless of scope error + + remove from path; unmark visiting; mark visited +``` + +`provider.get_dependencies(container)` is a pure registry lookup — it does not touch `cache_registry`, does not +call `find_container`, and does not perform any runtime context check. This means `validate()` works correctly on +a root APP-scope container even when the graph contains SESSION-, REQUEST-, ACTION-, or STEP-scoped providers. + +## Exception types + +| Exception | Base | Raised by | +|---|---|---| +| `ValidationFailedError` | `ContainerError` | `Container.validate()` — aggregate wrapper | +| `CircularDependencyError` | `ResolutionError` | recorded inside `validate()` on cycle detection | +| `InvalidScopeDependencyError` | `RegistrationError` | recorded inside `validate()` on inverted scope edge | +| `ArgumentResolutionError` | `ResolutionError` | yielded by `Factory.iter_validation_issues()` | diff --git a/planning/README.md b/planning/README.md new file mode 100644 index 0000000..f0fd85f --- /dev/null +++ b/planning/README.md @@ -0,0 +1,118 @@ +# Planning + +Specs, plans, and change history for `modern-di`. The living truth about *what +the system does now* lives in [`architecture/`](../architecture/) at the repo +root; this directory records *how it got there*. + +## Conventions + +> This section is the portable convention — identical across the +> modern-python repos. The Index below is repo-specific. To adopt elsewhere, +> copy this section plus [`_templates/`](_templates/) and point that repo's +> `CLAUDE.md` Workflow + truth home at it. + +### Two axes, never mixed + +- **`architecture/` (repo root) — the present.** One file per capability, + living prose, updated whenever a change ships. The truth home. +- **`planning/changes/` — the past-and-pending.** One folder per change, + frozen once shipped. + +Shipping a change **promotes** its conclusions into the affected +`architecture/.md` by hand, then archives the bundle. That +hand-edit is what keeps `architecture/` true; the archived bundle carries the +*why*. + +### Change bundles + +A change is a folder `changes/active/YYYY-MM-DD.NN-/`: + +- `YYYY-MM-DD` — proposal date; `.NN` — zero-padded intra-day counter + (`.01`, `.02`, …) that breaks same-date ties so the timeline sorts stably. +- `` — kebab-case description, not a story ID. + +On merge the folder moves to `changes/archive/` with `status: shipped`, `pr:`, +and `outcome:` filled, and its line moves from **Active** to **Archived** in +the Index below. + +### Three lanes + +| Lane | Artifacts | Use when | +|------|-----------|----------| +| **Full** | `design.md` + `plan.md` | design judgment; new file/module; public-API change; cross-cutting/multi-file; non-trivial test design | +| **Lightweight** | `change.md` | small-but-real: ≲30 LOC net, ≤2 files, no new file, no public-API change, single straightforward test | +| **Tiny** | none — conventional commit | typo, dep bump, linter/formatter/CI tweak, mechanical rename, single-line config | + +Heavier lane wins on ambiguity. A `change.md` that outgrows its lane splits +into `design.md` + `plan.md`. + +### Artifacts at a glance + +- **`design.md`** — the spec: the *thinking* (why, design, trade-offs, scope). +- **`plan.md`** — the plan: the *sequencing* (the executor's task checklist). +- **`change.md`** — both, condensed, for the lightweight lane. +- **`releases/.md`** — per-release user-facing notes. +- **`audits/-.md`** — findings from a code/docs/bug-hunt sweep; + spawns fix changes. +- **`retros/-.md`** — what we learned after a body of work. +- **`deferred.md`** — real-but-unscheduled items, each with a revisit trigger. + +Templates live in [`_templates/`](_templates/). + +### Frontmatter + +`design.md` / `change.md`: `status` (draft|approved|shipped|superseded), +`date`, `slug`, `supersedes`, `superseded_by`, `pr`, `outcome`. +`plan.md`: `status`, `date`, `slug`, `spec`, `pr`. Files in `architecture/` +carry **no** frontmatter — living prose, dated by git. + +## Index + +### Active + +- **[portable-planning-convention](changes/active/2026-06-13.03-portable-planning-convention/design.md)** + (2026-06-13) — Adopt the two-axis convention: `architecture/` truth + + `changes/` bundles + portable README, copied from faststream-outbox. + +### Archived (shipped) + +- **[alias-scope-transparency](changes/archive/2026-06-13.02-alias-scope-transparency/plan.md)** + (#207, 2026-06-13) — Deprecate decorative `Alias(scope=...)`; `validate()` + checks scope transitively via `effective_scope` (X-4). Plan-only; spec = the + code-docs audit report. +- **[audit-fixes-round2](changes/archive/2026-06-13.01-audit-fixes-round2/plan.md)** + (#203, 2026-06-13) — Round-2 fixes for the 21 deferred code+docs audit + findings. Plan-only; spec = the audit report. +- **[audit-fixes](changes/archive/2026-06-12.02-audit-fixes/plan.md)** + (#202, 2026-06-12) — First batch of code+docs audit fixes. Plan-only; spec = + the audit report. +- **[code-docs-audit](changes/archive/2026-06-12.01-code-docs-audit/design.md)** + (2026-06-12) — Full code+docs audit harness; produced the 57-finding report. +- **[migration-guide-from-that-depends](changes/archive/2026-06-09.02-migration-guide-from-that-depends/design.md)** + (2026-06-09) — Migration guide from `that-depends`. Design-only. +- **[docs-improvements](changes/archive/2026-06-09.01-docs-improvements/design.md)** + (2026-06-09) — Docs-site improvements. Design-only. +- **[scheduled-dep-check](changes/archive/2026-06-08.01-scheduled-dep-check/design.md)** + (2026-06-08) — Weekly scheduled dependency-check workflow. +- **[mkdocs-github-pages-migration](changes/archive/2026-06-07.01-mkdocs-github-pages-migration/design.md)** + (2026-06-07) — Docs hosting moved to GitHub Pages. +- **[validate-rework](changes/archive/2026-06-05.03-validate-rework/design.md)** + (2.15.0, 2026-06-05) — Reworked `validate()` for transitive cycle/scope + checks. +- **[singleton-rlock](changes/archive/2026-06-05.02-singleton-rlock/design.md)** + (2.15.0, 2026-06-05) — RLock-guarded singleton creation. +- **[bug-hunt-audit](changes/archive/2026-06-05.01-bug-hunt-audit/design.md)** + (2.15.0, 2026-06-05) — Four-dimension bug-hunt audit harness + report. + +## Other + +- **[`architecture/`](../architecture/)** at the repo root — the living + capability truth (scopes, containers, providers, resolution, validation, + testing & overrides). This is the promotion target on every ship. +- **[audits/](audits/)** — findings reports (2026-06-05 bug-hunt, 2026-06-12 + code+docs). +- **[deferred.md](deferred.md)** — real-but-unscheduled items with revisit + triggers. +- **[scripts/bug-hunt-audit.workflow.mjs](scripts/bug-hunt-audit.workflow.mjs)** + — repo-specific extra (the reusable audit harness), not part of the portable + core. diff --git a/planning/_templates/change.md b/planning/_templates/change.md new file mode 100644 index 0000000..0fe24c0 --- /dev/null +++ b/planning/_templates/change.md @@ -0,0 +1,38 @@ +--- +status: draft +date: YYYY-MM-DD +slug: my-change +supersedes: null +superseded_by: null +pr: null +outcome: null +--- + +# Change: One-line capitalized title + +**Lane:** lightweight — ≲30 LOC net, ≤2 files, no new file, no public-API +change, a single straightforward test. If it outgrows this, split into +`design.md` + `plan.md`. + +## Goal + +One or two sentences: what changes and why. + +## Approach + +The shape of the change in brief — enough that a reviewer sees the design +without a full spec. Link the truth home (`architecture/.md`) if a +capability contract moves. + +## Files + +- `path/to/file.py` — what changes +- `tests/test_x.py` — test added / updated + +## Verification + +- [ ] Failing test first — command + expected error. +- [ ] Apply the change. +- [ ] Test passes — command. +- [ ] `just test` — full suite green. +- [ ] `just lint` — clean. diff --git a/planning/_templates/design.md b/planning/_templates/design.md new file mode 100644 index 0000000..fb0fe5b --- /dev/null +++ b/planning/_templates/design.md @@ -0,0 +1,55 @@ +--- +status: draft +date: YYYY-MM-DD +slug: my-change +supersedes: null +superseded_by: null +pr: null +outcome: null +--- + +# Design: One-line capitalized title + +## Summary + +One paragraph. What changes, at the level a reader needs to decide if this +spec is worth reading in full. + +## Motivation + +Why now. What is broken or missing. Concrete observations / numbers, not +abstract complaints. Link to memory entries or earlier specs when relevant. + +## Non-goals + +What is deliberately out of scope and (when nontrivial) why. Each item is +a sentence; one line each. + +## Design + +### 1. + +What changes, in enough detail that a reader who has not seen the codebase +can follow. Code samples / diagrams welcome. + +### 2. + +... + +## Operations + +Out-of-repo steps (DNS, infra, external account changes). Omit if none. + +## Out of scope + +Already covered above under Non-goals if appropriate. Repeat-list of +explicitly-excluded follow-ups belongs here when the list is long. + +## Testing + +How we know it landed correctly. New pytest? Smoke check on live URL? +Lint pass? Be specific. + +## Risk + +What could go wrong, ranked by likelihood × impact. Mitigations. diff --git a/planning/_templates/plan.md b/planning/_templates/plan.md new file mode 100644 index 0000000..f2b90e8 --- /dev/null +++ b/planning/_templates/plan.md @@ -0,0 +1,56 @@ +--- +status: draft +date: YYYY-MM-DD +slug: my-change +spec: my-change +pr: null +--- + +# — implementation plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use +> superpowers:subagent-driven-development (recommended) or +> superpowers:executing-plans to implement this plan task-by-task. Steps +> use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** One sentence — what shipping this plan achieves. No design +rationale; link to the spec for that. + +**Spec:** [`design.md`](./design.md) + +**Branch:** `feat/my-change` (or `fix/`, `chore/`, etc.) + +**Commit strategy:** Per-task commits / single commit / squash on merge. +Whichever fits. + +--- + +### Task 1: + +**Files:** +- Modify: `path/to/file.py` +- Create: `path/to/new.py` + +One sentence on what this task accomplishes. No deeper reasoning — that's +in the spec. + +- [ ] **Step 1: ** + + Run / edit / verify command. Expected output. + +- [ ] **Step 2: ** + + ... + +- [ ] **Step 3: Commit** + + ```bash + git add path/to/file.py + git commit -m ": + + Co-Authored-By: Claude Opus 4.7 (1M context) " + ``` + +--- + +### Task 2: ... diff --git a/planning/audits/2026-06-05-bug-hunt-audit-report.md b/planning/audits/2026-06-05-bug-hunt-audit-report.md index cd40292..7de3b1e 100644 --- a/planning/audits/2026-06-05-bug-hunt-audit-report.md +++ b/planning/audits/2026-06-05-bug-hunt-audit-report.md @@ -1,7 +1,7 @@ # Bug-Hunt Audit Report — 2026-06-05 -**Spec:** planning/specs/2026-06-05-bug-hunt-audit-design.md -**Plan:** planning/plans/2026-06-05-bug-hunt-audit-plan.md +**Spec:** planning/changes/archive/2026-06-05.01-bug-hunt-audit/design.md +**Plan:** planning/changes/archive/2026-06-05.01-bug-hunt-audit/plan.md **Survivors:** 42 findings post-verify, 41 after dedup ## Summary diff --git a/planning/audits/2026-06-12-code-docs-audit-report.md b/planning/audits/2026-06-12-code-docs-audit-report.md index 4e2e64d..159fbe1 100644 --- a/planning/audits/2026-06-12-code-docs-audit-report.md +++ b/planning/audits/2026-06-12-code-docs-audit-report.md @@ -1,6 +1,6 @@ # Code & Docs Audit Report — 2026-06-12 -**Spec:** planning/specs/2026-06-12-code-docs-audit-design.md +**Spec:** planning/changes/archive/2026-06-12.01-code-docs-audit/design.md **Baseline:** lint-ci PASS, pytest PASS (100% coverage), at commit 9f07b08 **Prior audit:** planning/audits/2026-06-05-bug-hunt-audit-report.md diff --git a/planning/changes/active/2026-06-13.03-portable-planning-convention/design.md b/planning/changes/active/2026-06-13.03-portable-planning-convention/design.md new file mode 100644 index 0000000..d8c890a --- /dev/null +++ b/planning/changes/active/2026-06-13.03-portable-planning-convention/design.md @@ -0,0 +1,194 @@ +--- +status: draft +date: 2026-06-13 +slug: portable-planning-convention +supersedes: null +superseded_by: null +pr: null +outcome: null +--- + +# Design: Adopt the portable planning convention + +## Summary + +Replace this repo's flat `planning/specs/` + `planning/plans/` layout with the +two-axis convention already running in `faststream-outbox`: a living +`architecture/` truth home at the repo root plus `planning/changes/{active,archive}/` +folder bundles. The portable README "Conventions" section and the three +`_templates/` are copied byte-identical from `faststream-outbox`; only the +README "Index" and the back-authored `architecture/` prose are repo-specific. +Every existing spec/plan is migrated into a dated change bundle; all of them are +shipped, so they land in `archive/`. This adoption change itself is the lone +occupant of `active/`, dogfooding the convention. + +## Motivation + +`modern-di` has no truth home today. The "how it works now" knowledge is spread +across `CLAUDE.md`, the user-facing `docs/` site, and a pile of point-in-time +specs/plans under `planning/specs/` and `planning/plans/` — none of which is +authoritative once a release ships. The flat layout also never had an archive: +shipped work and (hypothetical) in-flight work sit in the same two directories, +distinguished only by filename. + +`faststream-outbox` (sibling repo, same maintainer) solved this with a portable +convention: an `architecture/` directory of living capability prose as the +single promotion target, and `planning/changes/` folder bundles that freeze the +*why* once shipped. Adopting the same convention here keeps the two repos +consistent and gives `modern-di` a real truth home for the first time. + +## Non-goals + +- Not changing the user-facing `docs/` mkdocs site — it stays the published + documentation; `architecture/` is the internal capability truth. +- Not rewriting or re-validating the historical specs/plans — they are relocated + and have frontmatter normalized, not edited for content. +- Not introducing `retros/` — the convention describes it, but the repo has none + and we create none speculatively. +- Not promoting this adoption change into an `architecture/` capability file — + it is tooling/process; its "promotion" is the CLAUDE.md + README edits it + already makes (mirrors how `faststream-outbox` archived its own adoption). + +## Design + +### 1. Target layout + +``` +architecture/ # NEW truth home — living prose, no frontmatter, outside the docs build + README.md # index naming it the promotion target + scopes.md containers.md providers.md resolution.md validation.md testing-and-overrides.md +planning/ + README.md # NEW: Conventions (byte-identical) + repo-specific Index + _templates/ # NEW: design.md, plan.md, change.md (copied verbatim from faststream-outbox) + changes/ + active/2026-06-13.03-portable-planning-convention/ # this change + archive/<11 migrated bundles>/ + audits/ # unchanged (2 reports; already plural) + releases/ # unchanged (inbound links inside are repointed) + scripts/workflow.mjs # unchanged (repo-specific extra, not part of the portable core) + deferred.md # unchanged +``` + +`planning/specs/` and `planning/plans/` are removed — every file folds into a +`changes/` bundle. + +### 2. Back-authored `architecture/` capability set + +Six living-prose files, no frontmatter (dated by git), sourced from the existing +specs, the `docs/` site, and the code. Each describes a capability *as it is +now*, not its history. + +| File | Covers | +|------|--------| +| `scopes.md` | `Scope` IntEnum (APP→SESSION→REQUEST→ACTION→STEP), the same-or-deeper resolution rule, `find_container` parent-chain walk | +| `containers.md` | `Container` as the entry point, the four registries (`ProvidersRegistry`/`OverridesRegistry` shared vs `CacheRegistry`/`ContextRegistry` per-container), `build_child_container`, close/reopen lifecycle, `container_provider` | +| `providers.md` | `Group` namespace declaration, `Factory` + `CacheSettings` (singleton via caching), sync/async finalizers, `kwargs`/`skip_creator_parsing`, `ContextProvider`, `Alias` | +| `resolution.md` | `resolve`/`resolve_provider` flow, overrides-first short-circuit, `types_parser` introspection, kwargs precedence, `X \| None` nullable injection | +| `validation.md` | `validate=True` / `container.validate()`, cycle detection, transitive scope check via `effective_scope`, deprecated decorative `Alias(scope=...)` | +| `testing-and-overrides.md` | `override`/`reset_override`, `OverridesRegistry`, the sibling `modern-di-pytest` integration (`modern_di_fixture`, `expose`) | + +`architecture/README.md` lists these files and states the promotion rule: +shipping a change hand-edits the affected capability file(s) here, then archives +the bundle. + +This set is the proposed default; splits/merges are easy to adjust during +execution. + +### 3. Change-bundle inventory and `.NN` assignment + +The slug is the current filename minus the date prefix and the +`-design`/`-plan` suffix. Inside each bundle, files are renamed to `design.md` / +`plan.md`, and frontmatter is normalized to the convention (`status: shipped`, +plus `pr:` and `outcome:` filled from the release notes and git log; `plan.md` +gets `spec:`). `.NN` within a colliding date follows merge order. + +All eleven migrate to `changes/archive/`: + +| Bundle id | Files | Source PR / release | +|-----------|-------|---------------------| +| `2026-06-05.01-bug-hunt-audit` | design + plan | 2.15.0 (#188–#197) | +| `2026-06-05.02-singleton-rlock` | design + plan | 2.15.0 | +| `2026-06-05.03-validate-rework` | design + plan | 2.15.0 | +| `2026-06-07.01-mkdocs-github-pages-migration` | design + plan | docs hosting move | +| `2026-06-08.01-scheduled-dep-check` | design + plan | shipped (`.github/workflows/scheduled.yml`) | +| `2026-06-09.01-docs-improvements` | design only | shipped (docs) | +| `2026-06-09.02-migration-guide-from-that-depends` | design only | shipped (`docs/migration/from-that-depends.md`) | +| `2026-06-12.01-code-docs-audit` | design + plan | 2.16.0 | +| `2026-06-12.02-audit-fixes` | plan only | #202 / 2.16.0 | +| `2026-06-13.01-audit-fixes-round2` | plan only | #203 / 2.16.0 | +| `2026-06-13.02-alias-scope-transparency` | plan only | #207 / 2.17.0 | + +The one active bundle, `2026-06-13.03-portable-planning-convention/`, holds this +`design.md` (and its `plan.md` once writing-plans runs). It is in-flight and +promotes to `archive/` on merge. `active/` is therefore non-empty: it +demonstrates the convention with exactly this change. + +`.NN` ties broken by merge order: 06-05 audit→singleton→validate; 06-12 +audit-run→fixes; 06-13 round2 (#203) → alias (#207) → this adoption (`.03`). +The 06-09 pair order (`docs-improvements` `.01`, `migration-guide` `.02`) is a +judgment call — both are same-day docs designs with no strict merge ordering. + +### 4. Orphan handling + +- **design-only** bundles (`docs-improvements`, `migration-guide-from-that-depends`): + keep only `design.md`. The convention permits a full-lane bundle that never + needed a separate plan. +- **plan-only** bundles (`audit-fixes`, `audit-fixes-round2`, + `alias-scope-transparency`): keep only `plan.md`; the frontmatter `spec:` + points at the relevant report in `audits/` (the de-facto spec for fix work). + The README Index line notes "plan-only; spec = audit report." + +### 5. Repointing, README, and CLAUDE.md + +- **Inbound links** in `planning/releases/2.15.0.md`, `2.16.0.md`, and + `2.17.0.md` that target `planning/specs/…` or `planning/plans/…` are repointed + to `planning/changes/archive//design.md|plan.md`. Links to + `planning/audits/…` stay valid (audits/ is unchanged). +- **`planning/README.md`** is created with the "Conventions" section copied + byte-identical from `faststream-outbox/planning/README.md` and a repo-specific + Index: **Active** lists this adoption; **Archived** lists the eleven bundles + newest-first; **Other** lists `architecture/` (the promotion target), + `audits/`, `deferred.md`, and `scripts/workflow.mjs` (the repo-specific extra, + in place of faststream's `lint-suppressions.md`). +- **CLAUDE.md** gains a new `## Workflow` section (none exists today) describing + the lanes and bundle layout and naming `architecture/` as the promotion + target. It adds a pointer that CLAUDE.md's existing `## Architecture` section + is quick orientation while `architecture/` holds the living capability truth. + +### 6. Spec location override + +The brainstorming skill's default spec path is +`docs/superpowers/specs/YYYY-MM-DD--design.md`. Because this change +*establishes* the new location, the spec is written directly to +`planning/changes/active/2026-06-13.03-portable-planning-convention/design.md` +instead — dogfooding the convention and avoiding a file in a path we would +immediately deprecate. + +## Testing + +- `just lint-ci` passes clean (eof-fixer + ruff format + ruff check + ty, no + auto-fix). +- The docs build (`mkdocs build`, as `.github/workflows/docs.yml` runs it) + succeeds. `docs_dir: docs` excludes both `planning/` and `architecture/`, so a + green build confirms the migration caused no collateral breakage rather than + testing the new files directly. +- A `grep` over the repo confirms no remaining inbound references to + `planning/specs/` or `planning/plans/` outside the migrated bundles + themselves. + +## Risk + +- **Broken relative links inside migrated files.** Plans/specs reference each + other and the audit reports by relative path; moving them two levels deeper + (`changes/archive//`) shifts every `../` prefix. *Mitigation:* the grep + check in Testing, plus per-bundle link review during execution. Likelihood + medium, impact low (dead links, not broken code). +- **Frontmatter drift.** Hand-filling `pr:`/`outcome:` across eleven bundles + risks wrong PR numbers. *Mitigation:* derive each from the release notes and + git log already mapped in this spec; where a bundle spans multiple PRs (the + 2.15.0 trio), record the release and PR range rather than a single number. + Likelihood low, impact low. +- **`architecture/` divergence from reality.** Back-authored prose can be subtly + wrong. *Mitigation:* source each file from the shipped code and the existing + reviewed specs/docs, not from memory; this is the same content the maintainer + already validated. Likelihood low, impact medium (it becomes the truth home). diff --git a/planning/changes/active/2026-06-13.03-portable-planning-convention/plan.md b/planning/changes/active/2026-06-13.03-portable-planning-convention/plan.md new file mode 100644 index 0000000..788bc93 --- /dev/null +++ b/planning/changes/active/2026-06-13.03-portable-planning-convention/plan.md @@ -0,0 +1,787 @@ +--- +status: draft +date: 2026-06-13 +slug: portable-planning-convention +spec: design.md +pr: null +--- + +# Portable planning convention — implementation plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use +> superpowers:subagent-driven-development (recommended) or +> superpowers:executing-plans to implement this plan task-by-task. Steps use +> checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the flat `planning/specs/` + `planning/plans/` layout with the +faststream-outbox two-axis convention — an `architecture/` truth home plus +`planning/changes/{active,archive}/` bundles — and back-author the +`architecture/` capability prose. + +**Architecture:** Pure docs/repo-structure change; no Python source is touched. +Migration uses `git mv` to preserve history. `architecture/` and `planning/` +both sit outside `docs_dir: docs`, so the published mkdocs site is unaffected; +the docs build is run only as a no-collateral-breakage check. Frozen historical +bundles keep their internal prose untouched — only live inbound links in +`planning/releases/` are repointed. + +**Tech Stack:** Markdown, `git mv`, `just` (`lint-ci`), `mkdocs`, `ripgrep`. + +**Branch:** `chore/portable-planning-convention` (already created; the spec is +committed there). + +**Commit strategy:** Per-task commits. + +**Canonical source to copy from:** +`/Users/kevinsmith/src/pypi/faststream-outbox/planning/README.md` (Conventions +section) and `/Users/kevinsmith/src/pypi/faststream-outbox/planning/_templates/`. + +--- + +### Task 1: Scaffold directories and copy templates + +**Files:** +- Create: `planning/_templates/design.md`, `plan.md`, `change.md` (copied) +- Create dirs: `planning/changes/active/`, `planning/changes/archive/`, `architecture/` + +Empty dirs aren't tracked by git; no `.gitkeep` is needed because Task 2 fills +`archive/` and the active bundle already populates `active/`. + +- [ ] **Step 0: Scope the `plan.md` gitignore rule to root (PREREQUISITE)** + + `.gitignore:22` has a bare `plan.md` that ignores **every** `plan.md` in the + repo — which would silently drop every bundle plan. Scope it to root-only: + + ```bash + cd /Users/kevinsmith/src/pypi/modern-di + # change the line `plan.md` to `/plan.md` + git check-ignore planning/_templates/plan.md # expect: no output after the fix + ``` + (May already be applied on the branch — verify with the `git check-ignore` + above returning nothing before proceeding.) + +- [ ] **Step 1: Create the directory skeleton** + + ```bash + cd /Users/kevinsmith/src/pypi/modern-di + mkdir -p planning/changes/active planning/changes/archive planning/_templates architecture + ``` + +- [ ] **Step 2: Copy the three templates byte-for-byte** + + ```bash + cp /Users/kevinsmith/src/pypi/faststream-outbox/planning/_templates/design.md planning/_templates/design.md + cp /Users/kevinsmith/src/pypi/faststream-outbox/planning/_templates/plan.md planning/_templates/plan.md + cp /Users/kevinsmith/src/pypi/faststream-outbox/planning/_templates/change.md planning/_templates/change.md + ``` + +- [ ] **Step 3: Verify the templates copied** + + Run: `ls planning/_templates/` + Expected: `change.md design.md plan.md` + +- [ ] **Step 4: Commit** + + ```bash + git add planning/_templates/ + git commit -m "chore: add planning _templates (design/plan/change) + + Co-Authored-By: Claude Opus 4.8 (1M context) " + ``` + + (The empty `changes/` dirs are committed in Task 3 once they hold files; no + `.gitkeep` is committed.) + +--- + +### Task 2: Migrate the 17 spec/plan files into 11 archive bundles + +Each `git mv` both relocates and renames to `design.md` / `plan.md`. Do **not** +edit file contents in this task — only move. Frontmatter is normalized in Task 3. + +**Files (move map):** + +| From | To | +|------|----| +| `planning/specs/2026-06-05-bug-hunt-audit-design.md` | `planning/changes/archive/2026-06-05.01-bug-hunt-audit/design.md` | +| `planning/plans/2026-06-05-bug-hunt-audit-plan.md` | `planning/changes/archive/2026-06-05.01-bug-hunt-audit/plan.md` | +| `planning/specs/2026-06-05-singleton-rlock-design.md` | `planning/changes/archive/2026-06-05.02-singleton-rlock/design.md` | +| `planning/plans/2026-06-05-singleton-rlock-plan.md` | `planning/changes/archive/2026-06-05.02-singleton-rlock/plan.md` | +| `planning/specs/2026-06-05-validate-rework-design.md` | `planning/changes/archive/2026-06-05.03-validate-rework/design.md` | +| `planning/plans/2026-06-05-validate-rework-plan.md` | `planning/changes/archive/2026-06-05.03-validate-rework/plan.md` | +| `planning/specs/2026-06-07-mkdocs-github-pages-migration-design.md` | `planning/changes/archive/2026-06-07.01-mkdocs-github-pages-migration/design.md` | +| `planning/plans/2026-06-07-mkdocs-github-pages-migration.md` | `planning/changes/archive/2026-06-07.01-mkdocs-github-pages-migration/plan.md` | +| `planning/specs/2026-06-08-scheduled-dep-check-design.md` | `planning/changes/archive/2026-06-08.01-scheduled-dep-check/design.md` | +| `planning/plans/2026-06-08-scheduled-dep-check-plan.md` | `planning/changes/archive/2026-06-08.01-scheduled-dep-check/plan.md` | +| `planning/specs/2026-06-09-docs-improvements-design.md` | `planning/changes/archive/2026-06-09.01-docs-improvements/design.md` | +| `planning/specs/2026-06-09-migration-guide-from-that-depends.md` | `planning/changes/archive/2026-06-09.02-migration-guide-from-that-depends/design.md` | +| `planning/specs/2026-06-12-code-docs-audit-design.md` | `planning/changes/archive/2026-06-12.01-code-docs-audit/design.md` | +| `planning/plans/2026-06-12-code-docs-audit.md` | `planning/changes/archive/2026-06-12.01-code-docs-audit/plan.md` | +| `planning/plans/2026-06-12-audit-fixes.md` | `planning/changes/archive/2026-06-12.02-audit-fixes/plan.md` | +| `planning/plans/2026-06-13-audit-fixes-round2.md` | `planning/changes/archive/2026-06-13.01-audit-fixes-round2/plan.md` | +| `planning/plans/2026-06-13-alias-scope-transparency.md` | `planning/changes/archive/2026-06-13.02-alias-scope-transparency/plan.md` | + +- [ ] **Step 1: Create the 11 bundle directories** + + ```bash + cd /Users/kevinsmith/src/pypi/modern-di + cd planning/changes/archive + mkdir -p 2026-06-05.01-bug-hunt-audit 2026-06-05.02-singleton-rlock 2026-06-05.03-validate-rework \ + 2026-06-07.01-mkdocs-github-pages-migration 2026-06-08.01-scheduled-dep-check \ + 2026-06-09.01-docs-improvements 2026-06-09.02-migration-guide-from-that-depends \ + 2026-06-12.01-code-docs-audit 2026-06-12.02-audit-fixes \ + 2026-06-13.01-audit-fixes-round2 2026-06-13.02-alias-scope-transparency + cd /Users/kevinsmith/src/pypi/modern-di + ``` + +- [ ] **Step 2: `git mv` each file per the move map above** + + Run each move (17 total), e.g.: + + ```bash + git mv planning/specs/2026-06-05-bug-hunt-audit-design.md planning/changes/archive/2026-06-05.01-bug-hunt-audit/design.md + git mv planning/plans/2026-06-05-bug-hunt-audit-plan.md planning/changes/archive/2026-06-05.01-bug-hunt-audit/plan.md + ``` + + …continuing for all 17 rows. + +- [ ] **Step 3: Confirm the old dirs are empty and remove them** + + ```bash + ls planning/specs planning/plans # expect: empty + rmdir planning/specs planning/plans + ``` + +- [ ] **Step 4: Verify the bundle tree** + + Run: `find planning/changes/archive -type f | sort` + Expected: 17 files — every bundle has `design.md` and/or `plan.md` matching the + move map (paired bundles have both; `docs-improvements` and `migration-guide` + have only `design.md`; `audit-fixes`, `audit-fixes-round2`, + `alias-scope-transparency` have only `plan.md`). + +- [ ] **Step 5: Commit** + + ```bash + git add -A planning/ + git commit -m "chore: migrate specs/plans into changes/archive bundles + + 17 files relocated via git mv into 11 dated bundles; specs/ and plans/ removed. + + Co-Authored-By: Claude Opus 4.8 (1M context) " + ``` + +--- + +### Task 3: Normalize frontmatter on the migrated bundles + +Set the top YAML frontmatter of each migrated file to match the convention. If a +file already has a frontmatter block (delimited by the first pair of `---`), +replace its keys to match; if it has none, prepend one. **Only touch the +frontmatter block — leave the body untouched.** + +`design.md` frontmatter shape: +```yaml +--- +status: shipped +date: +slug: +supersedes: null +superseded_by: null +pr: +outcome: +--- +``` +`plan.md` frontmatter shape: +```yaml +--- +status: shipped +date: +slug: +spec: +pr: +--- +``` + +**Per-bundle values** (`date` and `slug` come from the bundle id; `spec` in +`plan.md` is `design.md` for paired bundles, or the audit-report path for +plan-only bundles): + +| Bundle | pr | outcome (design.md) | plan.md `spec` | +|--------|----|--------------------|----------------| +| `2026-06-05.01-bug-hunt-audit` | `null` | `Four-dimension bug-hunt audit harness; report in audits/2026-06-05-bug-hunt-audit-report.md; 18 findings actioned in 2.15.0 (#188–#197).` | `design.md` | +| `2026-06-05.02-singleton-rlock` | `null` | `RLock guards singleton creation; shipped in 2.15.0.` | `design.md` | +| `2026-06-05.03-validate-rework` | `null` | `validate() reworked for transitive cycle/scope checks; shipped in 2.15.0.` | `design.md` | +| `2026-06-07.01-mkdocs-github-pages-migration` | `null` | `Docs hosting moved to GitHub Pages at modern-di.modern-python.org.` | `design.md` | +| `2026-06-08.01-scheduled-dep-check` | `null` | `Weekly scheduled dependency-check workflow (.github/workflows/scheduled.yml).` | `design.md` | +| `2026-06-09.01-docs-improvements` | `null` | `Docs-site improvements shipped.` | _(design-only — no plan.md)_ | +| `2026-06-09.02-migration-guide-from-that-depends` | `null` | `docs/migration/from-that-depends.md published.` | _(design-only — no plan.md)_ | +| `2026-06-12.01-code-docs-audit` | `null` | `Full code+docs audit harness; produced the 57-finding report in audits/2026-06-12-code-docs-audit-report.md.` | `design.md` | +| `2026-06-12.02-audit-fixes` | `#202` | _(plan-only — no design.md)_ | `../../../audits/2026-06-12-code-docs-audit-report.md` | +| `2026-06-13.01-audit-fixes-round2` | `#203` | _(plan-only — no design.md)_ | `../../../audits/2026-06-12-code-docs-audit-report.md` | +| `2026-06-13.02-alias-scope-transparency` | `#207` | _(plan-only — no design.md)_ | `../../../audits/2026-06-12-code-docs-audit-report.md` | + +- [ ] **Step 1: Set frontmatter on each `design.md`** + + For each of the 8 `design.md` files, edit the frontmatter to the design shape + with `status: shipped`, the bundle's `date`/`slug`, `pr` and `outcome` from + the table. Leave `supersedes`/`superseded_by` as `null`. + +- [ ] **Step 2: Set frontmatter on each `plan.md`** + + For each of the 9 `plan.md` files, edit the frontmatter to the plan shape with + `status: shipped`, the bundle's `date`/`slug`, `spec` and `pr` from the table. + +- [ ] **Step 3: Verify every migrated file starts with `status: shipped`** + + ```bash + cd /Users/kevinsmith/src/pypi/modern-di + for f in $(find planning/changes/archive -name '*.md'); do + head -2 "$f" | grep -q '^status: shipped' || echo "MISSING frontmatter: $f" + done + ``` + Expected: no output. + +- [ ] **Step 4: Commit** + + ```bash + git add planning/changes/archive/ + git commit -m "chore: normalize frontmatter on archived change bundles + + Co-Authored-By: Claude Opus 4.8 (1M context) " + ``` + +--- + +### Task 4: Create `planning/README.md` + +Copy the faststream-outbox README, then adapt only the preamble (repo name) and +replace everything from `## Index` onward with the modern-di Index. The +Conventions section stays byte-identical and is verified by diff. + +**Files:** +- Create: `planning/README.md` + +- [ ] **Step 1: Copy the source README** + + ```bash + cd /Users/kevinsmith/src/pypi/modern-di + cp /Users/kevinsmith/src/pypi/faststream-outbox/planning/README.md planning/README.md + ``` + +- [ ] **Step 2: Replace the preamble (first 5 lines) with the modern-di version** + + Replace the title + intro paragraph (everything before `## Conventions`) with: + + ```markdown + # Planning + + Specs, plans, and change history for `modern-di`. The living truth about *what + the system does now* lives in [`architecture/`](../architecture/) at the repo + root; this directory records *how it got there*. + ``` + +- [ ] **Step 3: Replace everything from `## Index` to end of file with the modern-di Index** + + ```markdown + ## Index + + ### Active + + - **[portable-planning-convention](changes/active/2026-06-13.03-portable-planning-convention/design.md)** + (2026-06-13) — Adopt the two-axis convention: `architecture/` truth + + `changes/` bundles + portable README, copied from faststream-outbox. + + ### Archived (shipped) + + - **[alias-scope-transparency](changes/archive/2026-06-13.02-alias-scope-transparency/plan.md)** + (#207, 2026-06-13) — Deprecate decorative `Alias(scope=...)`; `validate()` + checks scope transitively via `effective_scope` (X-4). Plan-only; spec = the + code-docs audit report. + - **[audit-fixes-round2](changes/archive/2026-06-13.01-audit-fixes-round2/plan.md)** + (#203, 2026-06-13) — Round-2 fixes for the 21 deferred code+docs audit + findings. Plan-only; spec = the audit report. + - **[audit-fixes](changes/archive/2026-06-12.02-audit-fixes/plan.md)** + (#202, 2026-06-12) — First batch of code+docs audit fixes. Plan-only; spec = + the audit report. + - **[code-docs-audit](changes/archive/2026-06-12.01-code-docs-audit/design.md)** + (2026-06-12) — Full code+docs audit harness; produced the 57-finding report. + - **[migration-guide-from-that-depends](changes/archive/2026-06-09.02-migration-guide-from-that-depends/design.md)** + (2026-06-09) — Migration guide from `that-depends`. Design-only. + - **[docs-improvements](changes/archive/2026-06-09.01-docs-improvements/design.md)** + (2026-06-09) — Docs-site improvements. Design-only. + - **[scheduled-dep-check](changes/archive/2026-06-08.01-scheduled-dep-check/design.md)** + (2026-06-08) — Weekly scheduled dependency-check workflow. + - **[mkdocs-github-pages-migration](changes/archive/2026-06-07.01-mkdocs-github-pages-migration/design.md)** + (2026-06-07) — Docs hosting moved to GitHub Pages. + - **[validate-rework](changes/archive/2026-06-05.03-validate-rework/design.md)** + (2.15.0, 2026-06-05) — Reworked `validate()` for transitive cycle/scope + checks. + - **[singleton-rlock](changes/archive/2026-06-05.02-singleton-rlock/design.md)** + (2.15.0, 2026-06-05) — RLock-guarded singleton creation. + - **[bug-hunt-audit](changes/archive/2026-06-05.01-bug-hunt-audit/design.md)** + (2.15.0, 2026-06-05) — Four-dimension bug-hunt audit harness + report. + + ## Other + + - **[`architecture/`](../architecture/)** at the repo root — the living + capability truth (scopes, containers, providers, resolution, validation, + testing & overrides). This is the promotion target on every ship. + - **[audits/](audits/)** — findings reports (2026-06-05 bug-hunt, 2026-06-12 + code+docs). + - **[deferred.md](deferred.md)** — real-but-unscheduled items with revisit + triggers. + - **[scripts/bug-hunt-audit.workflow.mjs](scripts/bug-hunt-audit.workflow.mjs)** + — repo-specific extra (the reusable audit harness), not part of the portable + core. + ``` + +- [ ] **Step 4: Verify the Conventions section is byte-identical to the source** + + ```bash + cd /Users/kevinsmith/src/pypi/modern-di + diff <(sed -n '/^## Conventions/,/^## Index/p' planning/README.md) \ + <(sed -n '/^## Conventions/,/^## Index/p' /Users/kevinsmith/src/pypi/faststream-outbox/planning/README.md) + ``` + Expected: no output (identical through the `## Index` line). + +- [ ] **Step 5: Commit** + + ```bash + git add planning/README.md + git commit -m "docs: planning README — portable Conventions + modern-di Index + + Co-Authored-By: Claude Opus 4.8 (1M context) " + ``` + +--- + +### Task 5: Confirm the active bundle is in place + +The active bundle already exists +(`2026-06-13.03-portable-planning-convention/` with `design.md` + `plan.md`, +committed on the branch). + +- [ ] **Step 1: Verify active holds exactly this change** + + Run: `ls planning/changes/active/` + Expected: `2026-06-13.03-portable-planning-convention` + + No commit needed — this is a confirmation checkpoint only. + +--- + +### Task 6: Back-author `architecture/README.md` + +**Files:** +- Create: `architecture/README.md` + +This is the index for the truth home. No frontmatter (living prose). + +- [ ] **Step 1: Write `architecture/README.md`** + + Content (full file): + + ```markdown + # Architecture + + The living truth about what `modern-di` does **now** — one file per capability, + updated by hand whenever a change ships. The *why* and *how it got here* live in + [`../planning/changes/`](../planning/changes/); this directory is the present. + + These files carry **no frontmatter** — they are prose, dated by git. + + ## Capabilities + + - [scopes.md](scopes.md) — the `Scope` hierarchy and the resolution rule. + - [containers.md](containers.md) — the `Container`, its registries, child + containers, and lifecycle. + - [providers.md](providers.md) — `Group`, `Factory`/caching, `ContextProvider`, + `Alias`. + - [resolution.md](resolution.md) — how `resolve()` wires dependencies from type + hints. + - [validation.md](validation.md) — `validate()` cycle and scope checks. + - [testing-and-overrides.md](testing-and-overrides.md) — overrides and the + `modern-di-pytest` integration. + + ## Promotion rule + + Shipping a change hand-edits the affected capability file(s) here to match the + new reality, then archives the change bundle under + [`../planning/changes/archive/`](../planning/changes/archive/). + ``` + +- [ ] **Step 2: Commit** + + ```bash + git add architecture/README.md + git commit -m "docs(architecture): add truth-home index + + Co-Authored-By: Claude Opus 4.8 (1M context) " + ``` + +--- + +> **Tasks 7–12 author one `architecture/.md` each.** They are +> independent and ideal for parallel subagents. For every file: **no +> frontmatter**; living prose; verify each claim against the cited source files +> before writing it (source from the code and shipped docs, not memory); 120-char +> line wrap; end with a single trailing newline. After writing, run +> `uv run ruff format --check .` is **not** applicable to markdown — instead the +> repo's eof-fixer runs in `just lint`; a final `just lint-ci` in Task 14 covers +> formatting. Each task commits its one file. + +### Task 7: Author `architecture/scopes.md` + +**Files:** +- Create: `architecture/scopes.md` +- Read first: `modern_di/scope.py`, `modern_di/container.py` (`find_container`), + `docs/introduction/`, `CLAUDE.md` (Scope hierarchy). + +- [ ] **Step 1: Write the file** covering, as present-tense prose: + - `Scope` is an `IntEnum` with `APP=1 → SESSION=2 → REQUEST=3 → ACTION=4 → STEP=5`. + - The resolution rule: a provider bound to scope S resolves only from a + container at scope S or deeper (higher int); resolving a deeper-scoped + provider from a shallower container raises a clear error (name the error and + quote its shape from `modern_di/errors.py`). + - How `find_container(scope)` walks the parent chain to locate the container at + the right scope. + - A short worked example (APP service vs REQUEST service). + +- [ ] **Step 2: Verify claims** — re-open `scope.py` and confirm the enum values + and `find_container` behavior match the prose. Fix any drift. + +- [ ] **Step 3: Commit** + + ```bash + git add architecture/scopes.md + git commit -m "docs(architecture): scopes capability + + Co-Authored-By: Claude Opus 4.8 (1M context) " + ``` + +--- + +### Task 8: Author `architecture/containers.md` + +**Files:** +- Create: `architecture/containers.md` +- Read first: `modern_di/container.py`, `modern_di/providers/container_provider.py`, + the registry classes (`providers_registry`, `cache_registry`, + `context_registry`, `overrides_registry`), `docs/` container pages, `CLAUDE.md` + (Container tree, Registries). + +- [ ] **Step 1: Write the file** covering: + - `Container` is the entry point; root via `Container(scope=Scope.APP, groups=[...])`. + - `build_child_container(scope=..., context={...})` creates children that share + the parent's `providers_registry` and `overrides_registry` but get their own + `cache_registry` and `context_registry`. + - The four-registry table (which are shared vs per-container) and why. + - Close/reopen lifecycle (finalizers run on close; reopen semantics) — confirm + exact behavior from `container.py`. + - `container_provider` resolves to the `Container` itself. + +- [ ] **Step 2: Verify claims** against `container.py` and the registry sources. + +- [ ] **Step 3: Commit** + + ```bash + git add architecture/containers.md + git commit -m "docs(architecture): containers capability + + Co-Authored-By: Claude Opus 4.8 (1M context) " + ``` + +--- + +### Task 9: Author `architecture/providers.md` + +**Files:** +- Create: `architecture/providers.md` +- Read first: `modern_di/group.py`, `modern_di/providers/factory.py`, + `modern_di/providers/context_provider.py`, the `Alias` provider source, + `modern_di/providers/__init__.py`, `docs/providers/`, `CLAUDE.md` (Group and + Provider declaration). + +- [ ] **Step 1: Write the file** covering: + - `Group` is a non-instantiable namespace; providers are class-level attributes. + - `Factory(scope=..., creator=...)` parses the creator's `__init__` hints at + declaration time; recursive type-based resolution. + - Singleton = `Factory(cache_settings=CacheSettings())`; there is no separate + `Singleton` class. Sync and async finalizers via `CacheSettings`. + - `kwargs={}` supplies static args that bypass type resolution; + `skip_creator_parsing=True` for un-introspectable callables. + - `ContextProvider` for runtime-injected values; `Alias` and its **deprecated** + decorative `scope=` parameter (removal in 3.0). + +- [ ] **Step 2: Verify claims** against `factory.py` and the provider sources — + especially the deprecation wording and `CacheSettings` fields. + +- [ ] **Step 3: Commit** + + ```bash + git add architecture/providers.md + git commit -m "docs(architecture): providers capability + + Co-Authored-By: Claude Opus 4.8 (1M context) " + ``` + +--- + +### Task 10: Author `architecture/resolution.md` + +**Files:** +- Create: `architecture/resolution.md` +- Read first: `modern_di/container.py` (`resolve`, `resolve_provider`), + `modern_di/types_parser.py`, `docs/` resolution/recipes pages, `CLAUDE.md` + (Resolution flow). + +- [ ] **Step 1: Write the file** covering the numbered flow: + 1. `resolve(SomeType)` → lookup in `providers_registry` → `resolve_provider`. + 2. `resolve_provider` checks `overrides_registry` first (override short-circuit). + 3. `find_container(scope)` locates the right-scope container. + 4. `cache_registry` hit returns immediately. + 5. kwargs compiled by matching each parsed parameter type to a provider, + resolved recursively; `kwargs=` precedence over type resolution. + 6. creator called; result cached if `cache_settings` set. + - `X | None` parameters with no provider are injected as `None` (nullable + wiring); confirm exact behavior from the code. + +- [ ] **Step 2: Verify claims** against `container.py` and `types_parser.py`. + +- [ ] **Step 3: Commit** + + ```bash + git add architecture/resolution.md + git commit -m "docs(architecture): resolution capability + + Co-Authored-By: Claude Opus 4.8 (1M context) " + ``` + +--- + +### Task 11: Author `architecture/validation.md` + +**Files:** +- Create: `architecture/validation.md` +- Read first: the `validate()` implementation in `modern_di/container.py`, the + `effective_scope` logic, `errors.py`, the + `changes/archive/2026-06-05.03-validate-rework/` and + `changes/archive/2026-06-13.02-alias-scope-transparency/` bundles, `CLAUDE.md`. + +- [ ] **Step 1: Write the file** covering: + - `validate=True` at container creation, or `container.validate()` explicitly; + zero cost when disabled. + - Cycle detection over the provider graph. + - Transitive scope check through aliases via `effective_scope` (X-4). + - The decorative `Alias(scope=...)` is exempt from the scope-order check and is + deprecated (removal in 3.0). + +- [ ] **Step 2: Verify claims** against the `validate()` source and the X-4 bundle. + +- [ ] **Step 3: Commit** + + ```bash + git add architecture/validation.md + git commit -m "docs(architecture): validation capability + + Co-Authored-By: Claude Opus 4.8 (1M context) " + ``` + +--- + +### Task 12: Author `architecture/testing-and-overrides.md` + +**Files:** +- Create: `architecture/testing-and-overrides.md` +- Read first: the override methods in `modern_di/container.py` + (`override`/`reset_override`), the `OverridesRegistry` source, `CLAUDE.md` + (Testing patterns), and the `modern-di-pytest` description in `CLAUDE.md`. + +- [ ] **Step 1: Write the file** covering: + - `container.override(provider, mock)` / `reset_override(provider)`; backed by + the shared `OverridesRegistry`; override short-circuits resolution (link to + `resolution.md`). + - Test patterns: `Group` subclass with providers as attributes; resolve by + reference (`resolve_provider`) or by type (`resolve`); scope-chain tests via + `build_child_container`. + - The sibling `modern-di-pytest` package: `modern_di_fixture(type_or_provider)` + and `expose(*groups)` (duplicate attr names raise `ValueError`); note that + `modern-di` itself does not depend on it. + +- [ ] **Step 2: Verify claims** against the override source and `CLAUDE.md`. + +- [ ] **Step 3: Commit** + + ```bash + git add architecture/testing-and-overrides.md + git commit -m "docs(architecture): testing & overrides capability + + Co-Authored-By: Claude Opus 4.8 (1M context) " + ``` + +--- + +### Task 13: Repoint inbound links in `planning/releases/` + +Only the `specs/`/`plans/` directory links break; `audits/`, `scripts/`, and +`deferred.md` links stay valid. Frozen bundle-internal references are **not** +touched. + +**Files:** +- Modify: `planning/releases/2.15.0.md` (lines 72–73) +- Modify: `planning/releases/2.16.0.md` (line 64) +- Modify: `planning/releases/2.17.0.md` (line 25) + +- [ ] **Step 1: Edit `2.15.0.md`** — replace the two lines: + + ```markdown + - Specs: [`planning/specs/`](../specs/) — audit design, singleton RLock, validate rework + - Plans: [`planning/plans/`](../plans/) — implementation plans for each major fix + ``` + with: + ```markdown + - Change bundles: [`bug-hunt-audit`](../changes/archive/2026-06-05.01-bug-hunt-audit/design.md), [`singleton-rlock`](../changes/archive/2026-06-05.02-singleton-rlock/design.md), [`validate-rework`](../changes/archive/2026-06-05.03-validate-rework/design.md) — design + plan in each. + ``` + +- [ ] **Step 2: Edit `2.16.0.md`** — replace line 64: + + ```markdown + - Plans: [`planning/plans/2026-06-12-code-docs-audit.md`](../plans/2026-06-12-code-docs-audit.md), [`2026-06-12-audit-fixes.md`](../plans/2026-06-12-audit-fixes.md), [`2026-06-13-audit-fixes-round2.md`](../plans/2026-06-13-audit-fixes-round2.md) + ``` + with: + ```markdown + - Plans: [`code-docs-audit`](../changes/archive/2026-06-12.01-code-docs-audit/plan.md), [`audit-fixes`](../changes/archive/2026-06-12.02-audit-fixes/plan.md), [`audit-fixes-round2`](../changes/archive/2026-06-13.01-audit-fixes-round2/plan.md) + ``` + +- [ ] **Step 3: Edit `2.17.0.md`** — replace line 25: + + ```markdown + - Plan: [`planning/plans/2026-06-13-alias-scope-transparency.md`](../plans/2026-06-13-alias-scope-transparency.md) + ``` + with: + ```markdown + - Plan: [`alias-scope-transparency`](../changes/archive/2026-06-13.02-alias-scope-transparency/plan.md) + ``` + +- [ ] **Step 4: Verify no live link still points at the old dirs** + + ```bash + cd /Users/kevinsmith/src/pypi/modern-di + rg -n 'planning/specs/|planning/plans/|\.\./specs/|\.\./plans/' planning/releases/ + ``` + Expected: no output. + +- [ ] **Step 5: Commit** + + ```bash + git add planning/releases/ + git commit -m "docs: repoint release notes at the new change bundles + + Co-Authored-By: Claude Opus 4.8 (1M context) " + ``` + +--- + +### Task 14: Add the `## Workflow` section to `CLAUDE.md` + +**Files:** +- Modify: `CLAUDE.md` (insert a new section after `## Architecture`, before + `## Code Style`) + +- [ ] **Step 1: Insert the section** + + ```markdown + ## Workflow + + Planning follows a portable two-axis convention (shared with + `faststream-outbox`); full details in [`planning/README.md`](planning/README.md). + + - **`architecture/`** (repo root) is the **truth home** — living capability + prose, the promotion target on every ship. The `## Architecture` section + above is quick orientation; `architecture/` holds the authoritative, + up-to-date account. + - **`planning/changes/{active,archive}//`** are change + bundles: `design.md` + `plan.md` (full lane), or `change.md` (lightweight). + Tiny changes (typo, dep bump, CI tweak) skip bundles entirely. + - Templates live in [`planning/_templates/`](planning/_templates/). + - **Shipping a change** hand-edits the affected `architecture/.md`, + then moves the bundle from `active/` to `archive/` with `status: shipped`, + `pr:`, and `outcome:` filled. + ``` + +- [ ] **Step 2: Verify placement** + + Run: `rg -n '^## ' CLAUDE.md` + Expected order: `Project Overview`, `Commands`, `Architecture`, `Workflow`, + `Code Style`. + +- [ ] **Step 3: Commit** + + ```bash + git add CLAUDE.md + git commit -m "docs: add Workflow section naming architecture/ as truth home + + Co-Authored-By: Claude Opus 4.8 (1M context) " + ``` + +--- + +### Task 15: Promote this change & finalize frontmatter + +This adoption is now shipping. Update the active bundle's own frontmatter and +move it to `archive/` (the Index already lists it under Active — update that too, +or leave the move for the merge step per the maintainer's preference). + +- [ ] **Step 1: Decision checkpoint** + + Confirm with the maintainer whether to archive this bundle now (pre-merge) or + on merge. Default: leave it in `active/` until the PR merges, then a follow-up + commit moves it to `archive/2026-06-13.03-portable-planning-convention/`, sets + `status: shipped` + `pr:` + `outcome:`, and moves its README Index line from + Active to Archived. **No action this task if deferring to merge.** + +--- + +### Task 16: Final verification + +- [ ] **Step 1: Lint** + + ```bash + cd /Users/kevinsmith/src/pypi/modern-di + just lint-ci + ``` + Expected: clean (eof-fixer + ruff format + ruff check + ty, no changes needed). + +- [ ] **Step 2: Docs build (no-collateral-breakage check)** + + ```bash + uv run mkdocs build --strict + ``` + Expected: build succeeds. (`docs_dir: docs` excludes `planning/` and + `architecture/`, so this confirms the migration didn't disturb the site. If + `--strict` flags pre-existing unrelated warnings, drop `--strict` and confirm a + plain build still succeeds.) + +- [ ] **Step 3: No stray references to the removed dirs** + + ```bash + rg -n 'planning/specs/|planning/plans/' --glob '!planning/changes/**' + ``` + Expected: no output (frozen references inside bundles are intentionally + excluded). + +- [ ] **Step 4: Bundle integrity** + + ```bash + find planning/changes -type f -name '*.md' | sort + ``` + Expected: 17 archived files across 11 bundles + the active bundle's `design.md` + and `plan.md`. + +- [ ] **Step 5: Push and open the PR** (if the maintainer wants the PR now) + + ```bash + git push -u origin chore/portable-planning-convention + gh pr create --fill + ``` diff --git a/planning/specs/2026-06-05-bug-hunt-audit-design.md b/planning/changes/archive/2026-06-05.01-bug-hunt-audit/design.md similarity index 97% rename from planning/specs/2026-06-05-bug-hunt-audit-design.md rename to planning/changes/archive/2026-06-05.01-bug-hunt-audit/design.md index 2f78ae4..978b54c 100644 --- a/planning/specs/2026-06-05-bug-hunt-audit-design.md +++ b/planning/changes/archive/2026-06-05.01-bug-hunt-audit/design.md @@ -1,3 +1,13 @@ +--- +status: shipped +date: 2026-06-05 +slug: bug-hunt-audit +supersedes: null +superseded_by: null +pr: null +outcome: Four-dimension bug-hunt audit harness; report in audits/2026-06-05-bug-hunt-audit-report.md; 18 findings actioned in 2.15.0 (#188–#197). +--- + # Bug-Hunt Audit — Design **Date:** 2026-06-05 diff --git a/planning/plans/2026-06-05-bug-hunt-audit-plan.md b/planning/changes/archive/2026-06-05.01-bug-hunt-audit/plan.md similarity index 99% rename from planning/plans/2026-06-05-bug-hunt-audit-plan.md rename to planning/changes/archive/2026-06-05.01-bug-hunt-audit/plan.md index 48d6060..11c0811 100644 --- a/planning/plans/2026-06-05-bug-hunt-audit-plan.md +++ b/planning/changes/archive/2026-06-05.01-bug-hunt-audit/plan.md @@ -1,3 +1,11 @@ +--- +status: shipped +date: 2026-06-05 +slug: bug-hunt-audit +spec: design.md +pr: null +--- + # Bug-Hunt Audit Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. diff --git a/planning/specs/2026-06-05-singleton-rlock-design.md b/planning/changes/archive/2026-06-05.02-singleton-rlock/design.md similarity index 97% rename from planning/specs/2026-06-05-singleton-rlock-design.md rename to planning/changes/archive/2026-06-05.02-singleton-rlock/design.md index 82b0ba9..d75bf04 100644 --- a/planning/specs/2026-06-05-singleton-rlock-design.md +++ b/planning/changes/archive/2026-06-05.02-singleton-rlock/design.md @@ -1,3 +1,13 @@ +--- +status: shipped +date: 2026-06-05 +slug: singleton-rlock +supersedes: null +superseded_by: null +pr: null +outcome: RLock guards singleton creation; shipped in 2.15.0. +--- + # Singleton Re-Entrant Lock Fix — Design **Date:** 2026-06-05 diff --git a/planning/plans/2026-06-05-singleton-rlock-plan.md b/planning/changes/archive/2026-06-05.02-singleton-rlock/plan.md similarity index 99% rename from planning/plans/2026-06-05-singleton-rlock-plan.md rename to planning/changes/archive/2026-06-05.02-singleton-rlock/plan.md index b0d80f7..8a74fd8 100644 --- a/planning/plans/2026-06-05-singleton-rlock-plan.md +++ b/planning/changes/archive/2026-06-05.02-singleton-rlock/plan.md @@ -1,3 +1,11 @@ +--- +status: shipped +date: 2026-06-05 +slug: singleton-rlock +spec: design.md +pr: null +--- + # Singleton RLock Fix Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. diff --git a/planning/specs/2026-06-05-validate-rework-design.md b/planning/changes/archive/2026-06-05.03-validate-rework/design.md similarity index 99% rename from planning/specs/2026-06-05-validate-rework-design.md rename to planning/changes/archive/2026-06-05.03-validate-rework/design.md index cdacabd..c39787e 100644 --- a/planning/specs/2026-06-05-validate-rework-design.md +++ b/planning/changes/archive/2026-06-05.03-validate-rework/design.md @@ -1,3 +1,13 @@ +--- +status: shipped +date: 2026-06-05 +slug: validate-rework +supersedes: null +superseded_by: null +pr: null +outcome: validate() reworked for transitive cycle/scope checks; shipped in 2.15.0. +--- + # `Container.validate()` Rework — Design **Date:** 2026-06-05 diff --git a/planning/plans/2026-06-05-validate-rework-plan.md b/planning/changes/archive/2026-06-05.03-validate-rework/plan.md similarity index 99% rename from planning/plans/2026-06-05-validate-rework-plan.md rename to planning/changes/archive/2026-06-05.03-validate-rework/plan.md index e8a49b9..329fb6a 100644 --- a/planning/plans/2026-06-05-validate-rework-plan.md +++ b/planning/changes/archive/2026-06-05.03-validate-rework/plan.md @@ -1,3 +1,11 @@ +--- +status: shipped +date: 2026-06-05 +slug: validate-rework +spec: design.md +pr: null +--- + # `Container.validate()` Rework Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. diff --git a/planning/specs/2026-06-07-mkdocs-github-pages-migration-design.md b/planning/changes/archive/2026-06-07.01-mkdocs-github-pages-migration/design.md similarity index 96% rename from planning/specs/2026-06-07-mkdocs-github-pages-migration-design.md rename to planning/changes/archive/2026-06-07.01-mkdocs-github-pages-migration/design.md index 0f4b5b7..4410e2a 100644 --- a/planning/specs/2026-06-07-mkdocs-github-pages-migration-design.md +++ b/planning/changes/archive/2026-06-07.01-mkdocs-github-pages-migration/design.md @@ -1,3 +1,13 @@ +--- +status: shipped +date: 2026-06-07 +slug: mkdocs-github-pages-migration +supersedes: null +superseded_by: null +pr: null +outcome: Docs hosting moved to GitHub Pages at modern-di.modern-python.org. +--- + # Migrate docs from Read the Docs to GitHub Pages **Date:** 2026-06-07 diff --git a/planning/plans/2026-06-07-mkdocs-github-pages-migration.md b/planning/changes/archive/2026-06-07.01-mkdocs-github-pages-migration/plan.md similarity index 99% rename from planning/plans/2026-06-07-mkdocs-github-pages-migration.md rename to planning/changes/archive/2026-06-07.01-mkdocs-github-pages-migration/plan.md index 6dd03f6..6d46f7b 100644 --- a/planning/plans/2026-06-07-mkdocs-github-pages-migration.md +++ b/planning/changes/archive/2026-06-07.01-mkdocs-github-pages-migration/plan.md @@ -1,3 +1,11 @@ +--- +status: shipped +date: 2026-06-07 +slug: mkdocs-github-pages-migration +spec: design.md +pr: null +--- + # MkDocs to GitHub Pages Migration Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. diff --git a/planning/specs/2026-06-08-scheduled-dep-check-design.md b/planning/changes/archive/2026-06-08.01-scheduled-dep-check/design.md similarity index 97% rename from planning/specs/2026-06-08-scheduled-dep-check-design.md rename to planning/changes/archive/2026-06-08.01-scheduled-dep-check/design.md index e5013c1..8a32a99 100644 --- a/planning/specs/2026-06-08-scheduled-dep-check-design.md +++ b/planning/changes/archive/2026-06-08.01-scheduled-dep-check/design.md @@ -1,3 +1,13 @@ +--- +status: shipped +date: 2026-06-08 +slug: scheduled-dep-check +supersedes: null +superseded_by: null +pr: null +outcome: Weekly scheduled dependency-check workflow (.github/workflows/scheduled.yml). +--- + # Scheduled dependency-breakage check **Date:** 2026-06-08 diff --git a/planning/plans/2026-06-08-scheduled-dep-check-plan.md b/planning/changes/archive/2026-06-08.01-scheduled-dep-check/plan.md similarity index 99% rename from planning/plans/2026-06-08-scheduled-dep-check-plan.md rename to planning/changes/archive/2026-06-08.01-scheduled-dep-check/plan.md index 1a2609f..f889350 100644 --- a/planning/plans/2026-06-08-scheduled-dep-check-plan.md +++ b/planning/changes/archive/2026-06-08.01-scheduled-dep-check/plan.md @@ -1,3 +1,11 @@ +--- +status: shipped +date: 2026-06-08 +slug: scheduled-dep-check +spec: design.md +pr: null +--- + # Scheduled Dependency-Breakage Check Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. diff --git a/planning/specs/2026-06-09-docs-improvements-design.md b/planning/changes/archive/2026-06-09.01-docs-improvements/design.md similarity index 98% rename from planning/specs/2026-06-09-docs-improvements-design.md rename to planning/changes/archive/2026-06-09.01-docs-improvements/design.md index 56b241b..137097c 100644 --- a/planning/specs/2026-06-09-docs-improvements-design.md +++ b/planning/changes/archive/2026-06-09.01-docs-improvements/design.md @@ -1,3 +1,13 @@ +--- +status: shipped +date: 2026-06-09 +slug: docs-improvements +supersedes: null +superseded_by: null +pr: null +outcome: Docs-site improvements shipped. +--- + # Spec: Docs improvements — recipes + concept pages **Date:** 2026-06-09 diff --git a/planning/specs/2026-06-09-migration-guide-from-that-depends.md b/planning/changes/archive/2026-06-09.02-migration-guide-from-that-depends/design.md similarity index 98% rename from planning/specs/2026-06-09-migration-guide-from-that-depends.md rename to planning/changes/archive/2026-06-09.02-migration-guide-from-that-depends/design.md index 3200a8d..33065d5 100644 --- a/planning/specs/2026-06-09-migration-guide-from-that-depends.md +++ b/planning/changes/archive/2026-06-09.02-migration-guide-from-that-depends/design.md @@ -1,3 +1,13 @@ +--- +status: shipped +date: 2026-06-09 +slug: migration-guide-from-that-depends +supersedes: null +superseded_by: null +pr: null +outcome: docs/migration/from-that-depends.md published. +--- + # Spec: Rewrite `from-that-depends.md` migration guide **Date:** 2026-06-09 diff --git a/planning/specs/2026-06-12-code-docs-audit-design.md b/planning/changes/archive/2026-06-12.01-code-docs-audit/design.md similarity index 92% rename from planning/specs/2026-06-12-code-docs-audit-design.md rename to planning/changes/archive/2026-06-12.01-code-docs-audit/design.md index b6e7a4e..d472be5 100644 --- a/planning/specs/2026-06-12-code-docs-audit-design.md +++ b/planning/changes/archive/2026-06-12.01-code-docs-audit/design.md @@ -1,3 +1,13 @@ +--- +status: shipped +date: 2026-06-12 +slug: code-docs-audit +supersedes: null +superseded_by: null +pr: null +outcome: Full code+docs audit harness; produced the 57-finding report in audits/2026-06-12-code-docs-audit-report.md. +--- + # Code & Docs Audit — Design **Date:** 2026-06-12 diff --git a/planning/plans/2026-06-12-code-docs-audit.md b/planning/changes/archive/2026-06-12.01-code-docs-audit/plan.md similarity index 99% rename from planning/plans/2026-06-12-code-docs-audit.md rename to planning/changes/archive/2026-06-12.01-code-docs-audit/plan.md index d2fa0d0..6d7efc1 100644 --- a/planning/plans/2026-06-12-code-docs-audit.md +++ b/planning/changes/archive/2026-06-12.01-code-docs-audit/plan.md @@ -1,3 +1,11 @@ +--- +status: shipped +date: 2026-06-12 +slug: code-docs-audit +spec: design.md +pr: null +--- + # Code & Docs Audit Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. diff --git a/planning/plans/2026-06-12-audit-fixes.md b/planning/changes/archive/2026-06-12.02-audit-fixes/plan.md similarity index 99% rename from planning/plans/2026-06-12-audit-fixes.md rename to planning/changes/archive/2026-06-12.02-audit-fixes/plan.md index 5f01ee7..af583c3 100644 --- a/planning/plans/2026-06-12-audit-fixes.md +++ b/planning/changes/archive/2026-06-12.02-audit-fixes/plan.md @@ -1,3 +1,11 @@ +--- +status: shipped +date: 2026-06-12 +slug: audit-fixes +spec: ../../../audits/2026-06-12-code-docs-audit-report.md +pr: "#202" +--- + # Audit Fixes Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. diff --git a/planning/plans/2026-06-13-audit-fixes-round2.md b/planning/changes/archive/2026-06-13.01-audit-fixes-round2/plan.md similarity index 99% rename from planning/plans/2026-06-13-audit-fixes-round2.md rename to planning/changes/archive/2026-06-13.01-audit-fixes-round2/plan.md index e10f913..98e4aee 100644 --- a/planning/plans/2026-06-13-audit-fixes-round2.md +++ b/planning/changes/archive/2026-06-13.01-audit-fixes-round2/plan.md @@ -1,3 +1,11 @@ +--- +status: shipped +date: 2026-06-13 +slug: audit-fixes-round2 +spec: ../../../audits/2026-06-12-code-docs-audit-report.md +pr: "#203" +--- + # Audit Fixes — Round 2 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. diff --git a/planning/plans/2026-06-13-alias-scope-transparency.md b/planning/changes/archive/2026-06-13.02-alias-scope-transparency/plan.md similarity index 99% rename from planning/plans/2026-06-13-alias-scope-transparency.md rename to planning/changes/archive/2026-06-13.02-alias-scope-transparency/plan.md index afe4da2..6ef525e 100644 --- a/planning/plans/2026-06-13-alias-scope-transparency.md +++ b/planning/changes/archive/2026-06-13.02-alias-scope-transparency/plan.md @@ -1,3 +1,11 @@ +--- +status: shipped +date: 2026-06-13 +slug: alias-scope-transparency +spec: ../../../audits/2026-06-12-code-docs-audit-report.md +pr: "#207" +--- + # Alias Scope Transparency (X-4) Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. diff --git a/planning/releases/2.15.0.md b/planning/releases/2.15.0.md index ef1e2ef..5c39a1e 100644 --- a/planning/releases/2.15.0.md +++ b/planning/releases/2.15.0.md @@ -69,5 +69,4 @@ If you only ever called `validate()` and let exceptions propagate at startup, no - Audit report: [`planning/audits/2026-06-05-bug-hunt-audit-report.md`](../audits/2026-06-05-bug-hunt-audit-report.md) - Audit harness (reusable): [`planning/scripts/bug-hunt-audit.workflow.mjs`](../scripts/bug-hunt-audit.workflow.mjs) -- Specs: [`planning/specs/`](../specs/) — audit design, singleton RLock, validate rework -- Plans: [`planning/plans/`](../plans/) — implementation plans for each major fix +- Change bundles: [`bug-hunt-audit`](../changes/archive/2026-06-05.01-bug-hunt-audit/design.md), [`singleton-rlock`](../changes/archive/2026-06-05.02-singleton-rlock/design.md), [`validate-rework`](../changes/archive/2026-06-05.03-validate-rework/design.md) — design + plan in each. diff --git a/planning/releases/2.16.0.md b/planning/releases/2.16.0.md index 7c99dc6..f1306b5 100644 --- a/planning/releases/2.16.0.md +++ b/planning/releases/2.16.0.md @@ -61,5 +61,5 @@ class Service: ## References - Audit report: [`planning/audits/2026-06-12-code-docs-audit-report.md`](../audits/2026-06-12-code-docs-audit-report.md) -- Plans: [`planning/plans/2026-06-12-code-docs-audit.md`](../plans/2026-06-12-code-docs-audit.md), [`2026-06-12-audit-fixes.md`](../plans/2026-06-12-audit-fixes.md), [`2026-06-13-audit-fixes-round2.md`](../plans/2026-06-13-audit-fixes-round2.md) +- Plans: [`code-docs-audit`](../changes/archive/2026-06-12.01-code-docs-audit/plan.md), [`audit-fixes`](../changes/archive/2026-06-12.02-audit-fixes/plan.md), [`audit-fixes-round2`](../changes/archive/2026-06-13.01-audit-fixes-round2/plan.md) - Deferred follow-ups: [`planning/deferred.md`](../deferred.md) diff --git a/planning/releases/2.17.0.md b/planning/releases/2.17.0.md index 68acd52..678f445 100644 --- a/planning/releases/2.17.0.md +++ b/planning/releases/2.17.0.md @@ -22,4 +22,4 @@ Mostly additive. **One behavior change in `Container.validate()`** is called out ## References - Audit finding X-4: [`planning/audits/2026-06-12-code-docs-audit-report.md`](../audits/2026-06-12-code-docs-audit-report.md) -- Plan: [`planning/plans/2026-06-13-alias-scope-transparency.md`](../plans/2026-06-13-alias-scope-transparency.md) +- Plan: [`alias-scope-transparency`](../changes/archive/2026-06-13.02-alias-scope-transparency/plan.md)