Skip to content

docs: remove docs/adr; move the decisions that survive to the docs and the code - #480

Merged
lesnik512 merged 5 commits into
mainfrom
docs/adr-revision
Sep 12, 2026
Merged

docs: remove docs/adr; move the decisions that survive to the docs and the code#480
lesnik512 merged 5 commits into
mainfrom
docs/adr-revision

Conversation

@lesnik512

@lesnik512 lesnik512 commented Sep 12, 2026

Copy link
Copy Markdown
Member

What

docs/adr/ is removed. Judged against the three-part test (hard to reverse, surprising without context, a real trade-off), none of the 30 records earned its place: most described code as it is, several duplicated the design-decisions page verbatim, and the rest were user-facing stances that belong on that page where users and the triage check can read them.

Where each surviving decision went

decision new home
explicit-only validation (0022) design-decisions §6
closed provider set (0013), cache= not Singleton (0002) design-decisions §5
no multibinding (0005), no generator creators (0004), no enter_scope (0003), no tracing (0023, with the +37% number) design-decisions Non-goals
error text is not the contract (0009) errors page, one paragraph
find_container is the one supported override; resolve / resolve_provider are not hooks (0024, 0026) advanced-api page
decorator-free only where the framework evaluates a default as a provider (0021) writing-integrations, decorator path
Typer covers Click (0027) typer page, one sentence
no mkdocs-redirects (0006) comment in docs/requirements.txt
rejected resolver designs and their numbers (0030) performance page, "why the results look this way"
alias not bound at compile time (0025), no inlined scope map (0024) one-line comments at _compile_alias and _navigate
lazy child allocation declined (0016) measurement posted on #478
alias binding for 4.0 (your challenge on 0025) #481
grpc registration guard (0010) belongs as a one-line comment in modern-di-grpc, separate repo

Dropped with nothing to move (they described code, or were reversible without surprise): 0001, 0007, 0008, 0011, 0012, 0014, 0015, 0018 (verbatim duplicate of the Non-goals entry), 0019, 0020 (its rows are already the lifecycle rules), 0029 (its three invariant tests carry the rationale).

Agent files

docs/agents/issue-tracker.md mapped the triage skill's .out-of-scope/ corpus (the prior-rejection check reads it; a rejection writes to it) onto docs/adr/. It now maps onto the design-decisions Non-goals section, both for reading and writing. docs/agents/domain.md and AGENTS.md stop referring to ADRs; mkdocs.yml drops the /adr/ exclusion; the pyproject.toml per-file-ignore comment loses its ADR path.

Gates

Lint, ty, 540 tests, strict docs build all pass.

Every record is rewritten to Decision + Why, without the "revisit trigger"
block: 1002 -> 388 lines. 0017 (exec declined) is removed as superseded by
0030; 0031 is folded into 0025, which already recorded the alias-bind
measurement and now carries the one reason that still holds (the alias step in
runtime error chains). Facts the template resolver made stale are corrected
(0012, 0013, 0014, 0015, 0025, 0026, 0030). A one-line comment at the alias
compiler states why it is not bound; the pyproject SLF001 note and the
advanced-api page stop describing closures.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Details
Benchmark suite Current: 429cd6e Previous: 84b15b0 Ratio
benchmarks/test_guard_by_type.py::test_g16_resolve_by_type 4338966.106904256 iter/sec (stddev: 8.165667392528595e-9) 4111715.812015289 iter/sec (stddev: 1.737522845623885e-8) 0.95
benchmarks/test_guard_by_type.py::test_g17_resolve_by_type_large_registry 4265751.543565375 iter/sec (stddev: 6.4252700573881205e-9) 3966758.2485261136 iter/sec (stddev: 1.572822708461717e-8) 0.93
benchmarks/test_guard_cold.py::test_g8_cold_first_resolve 18074.51223666573 iter/sec (stddev: 0.00005323540731865018) 16004.72330958737 iter/sec (stddev: 0.0000625162597678295) 0.89
benchmarks/test_guard_cold.py::test_g8b_cold_first_resolve_cached 15420.585774739387 iter/sec (stddev: 0.00011270960281413739) 13497.312687765632 iter/sec (stddev: 0.00014530411370138543) 0.88
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[1] 536.5746096746725 iter/sec (stddev: 0.00003367265748370334) 540.6150637293446 iter/sec (stddev: 0.0001824312511385608) 1.01
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[2] 506.7095707515858 iter/sec (stddev: 0.00002060449382869758) 502.80389045688327 iter/sec (stddev: 0.00014883161929520402) 0.99
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[4] 454.80965191731065 iter/sec (stddev: 0.000027382167923633477) 448.1418084368608 iter/sec (stddev: 0.0001298107075192004) 0.99
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[1] 1859.9524903021604 iter/sec (stddev: 0.00018083629433317185) 1676.2273084921449 iter/sec (stddev: 0.0001944100925791444) 0.90
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[2] 1501.2566456439479 iter/sec (stddev: 0.0001693835070540942) 1333.8874005180764 iter/sec (stddev: 0.00022992267475147003) 0.89
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[4] 1078.2573485073071 iter/sec (stddev: 0.00017585993886011446) 962.9503798168239 iter/sec (stddev: 0.00024922809084355847) 0.89
benchmarks/test_guard_lifecycle.py::test_g6_build_child_container 694736.5353276125 iter/sec (stddev: 3.997407207754713e-8) 753265.49537359 iter/sec (stddev: 4.763568530631424e-8) 1.08
benchmarks/test_guard_lifecycle.py::test_g6b_build_child_container_auto_scope 635560.9979366362 iter/sec (stddev: 4.594295766454308e-8) 679431.5203257735 iter/sec (stddev: 6.040131584214356e-8) 1.07
benchmarks/test_guard_lifecycle.py::test_g7_request_lifecycle_batch 2406.236028312242 iter/sec (stddev: 0.000008002465432848277) 2399.581927113117 iter/sec (stddev: 0.00001753933625086649) 1.00
benchmarks/test_guard_lifecycle.py::test_g7c_event_loop_floor_control 58914.26903023947 iter/sec (stddev: 0.0000014744809224113097) 61074.751422245674 iter/sec (stddev: 0.000002462942606397808) 1.04
benchmarks/test_guard_lifecycle.py::test_g13_teardown_at_scale 48680.79524037967 iter/sec (stddev: 0.0000016124226115277146) 50503.96237302482 iter/sec (stddev: 0.000002069539854247738) 1.04
benchmarks/test_guard_resolve.py::test_g1_transient_resolve 2441030.2026165635 iter/sec (stddev: 3.349203708521621e-8) 2447111.1974663865 iter/sec (stddev: 2.9348890650137755e-8) 1.00
benchmarks/test_guard_resolve.py::test_g2_cached_resolve 4227781.181279056 iter/sec (stddev: 7.228763982627601e-9) 4081617.3261722233 iter/sec (stddev: 6.695477561497057e-8) 0.97
benchmarks/test_guard_resolve.py::test_g3_deep_chain 854535.1880785434 iter/sec (stddev: 6.3127058367329e-8) 847432.8863865049 iter/sec (stddev: 4.402585884241757e-8) 0.99
benchmarks/test_guard_resolve.py::test_g4_wide_resolve 500536.8132184154 iter/sec (stddev: 3.4553865635892693e-7) 524426.3503369563 iter/sec (stddev: 3.5818326365445204e-7) 1.05
benchmarks/test_guard_resolve.py::test_g5_cross_scope 1958921.6097631322 iter/sec (stddev: 2.485203461348961e-8) 1959504.0083899717 iter/sec (stddev: 3.0232594743763504e-8) 1.00
benchmarks/test_guard_resolve.py::test_g9_context_resolve 1132717.7012759626 iter/sec (stddev: 1.4908350639100557e-7) 1096218.4055127536 iter/sec (stddev: 1.7265430223555424e-7) 0.97
benchmarks/test_guard_resolve.py::test_g12_override_active_resolve 805629.0266822255 iter/sec (stddev: 5.236424805127281e-8) 840875.5313768031 iter/sec (stddev: 3.84640730267e-8) 1.04
benchmarks/test_guard_resolve.py::test_g18_alias_hop 2687340.8456300353 iter/sec (stddev: 6.231051141156874e-8) 2859243.2572375205 iter/sec (stddev: 1.3894440264295111e-8) 1.06
benchmarks/test_guard_validate.py::test_g10_validate_deep_chain 26831.73718333745 iter/sec (stddev: 0.00003917646951414279) 23875.75800457809 iter/sec (stddev: 0.00003180494080777584) 0.89
benchmarks/test_guard_validate.py::test_g11_validate_wide 15233.539452188917 iter/sec (stddev: 0.0002434609194822762) 14249.073539463352 iter/sec (stddev: 0.00026633961521020633) 0.94

This comment was automatically generated by workflow using github-action-benchmark.

`AsyncFinalizerInSyncCloseError`; `CacheRegistry.close_sync` preserves the items that raised it so
a later `close_async` can finish them, where the async path clears the creation order outright. A
unified method would carry both behaviours as conditional branches, adding complexity rather than
concentrating it. The area has been stable since the finalizer fixes shipped in 2.15.0.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

challenge this decision against https://github.com/mattpocock/skills convention, seems not earnd its place as ADR

**Why:** every alternative expresses caching twice. A `Singleton` class says "cached" in the class
name and again in a still-required `cache_settings` for finalizers, and the two can drift. A
`cached=True` flag beside `cache_settings=` needs a both-passed conflict rule and has no path to a
finalizer. Overloading `cache_settings=True` works but reads wrong. One argument, one model.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

Comment thread docs/adr/0003-no-enter-scope-alias.md Outdated
container is a real object with its own cache and context, and "enter scope" vocabulary would hide
the model the docs teach. A second spelling of the most-written call after `resolve()` also runs
against the conservative feature set. If discoverability turns out to be a problem, a vocabulary
table mapping `enter_scope` / `CreateScope` to `build_child_container` is the cheaper fix.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

Comment thread docs/adr/0004-no-generator-creators.md Outdated
for uncached factories, rejecting async generators at declaration, extracting `bound_type` from
`Iterator[T]`. One explicit spelling also keeps the property that async finalizers work under sync
resolution, which the generator form cannot express. A `Factory` subclass in userland can wrap a
generator creator and register the continuation through `CacheSettings(finalizer=)`.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread docs/adr/0005-no-multibinding.md Outdated
overriding `T` is ambiguous between the collection and one contributor; validation can no longer
tell an empty collection from a wiring mistake. That is permanent structural cost for demand
inferred from other ecosystems. The workaround costs one provider: a `Factory` that takes the
individual dependencies and returns the list.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like can be dropped at all

Comment thread docs/adr/0025-alias-binds-nothing.md Outdated
dependencies' resolvers by reference and an overridden alias compiles to its override before its
source is touched. Recovering the alias step at render time from the static graph, the way
`validate()` already draws it, would keep the diagnostic and take the win; that is an error-rendering
change, and the one worth making if the hop ever shows up in a real profile.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

challenge this, maybe dropping will help us to get performance and readability in next major release?

[ADR-0024](0024-scope-map-inline-declined.md). Together with
[ADR-0023](0023-debug-resolution-tracing-declined.md) this means modern-di offers no per-node
observation of resolution; a request for one should get a designed seam with a contract, not a
re-blessing of subclass overrides.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drop

Comment thread docs/adr/0027-no-click-integration.md Outdated
same contract against the lower-level API for no entrypoint that is not already reachable, at the
cost of a repository, a release cadence and a compatibility matrix. Vendoring Click support inside
the typer adapter would tie its surface to which of the two APIs the user built against. A
community-maintained adapter outside the org remains possible.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drop

`Alias.scope` still returns `APP`, since ordering depends on it: this is a rendering rule. The walk
moved into the graph module rather than being copied, because `Alias` cannot import it and
[ADR-0007](0007-unify-graph-traversal.md) keeps one traversal. The rule assumes a redirect owns no
lifetime of its own, which is true of `Alias` and of nothing else.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drop or move to code

Comment thread docs/adr/0030-exec-template-resolver.md Outdated
namespace mismatch is a test failure. Coverage cannot see generated lines. Kwarg names enter the
source only as `repr` string keys. Generated source is registered in `linecache` and the resolvers
carry a `__qualname__`, so tracebacks and profilers read normally. Overriding drops and recompiles
lazily and is not coordinated with concurrent resolves. The free-threaded CI job (3.14t) passes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why needed? can drop?

…d the code

Judged against the ADR test (hard to reverse, surprising without context, a
real trade-off), none of the records earned its place: most described code
as it is, several duplicated the design-decisions page, and the rest were
user-facing stances that belong on that page. The directory is gone.

What moved: explicit-only validation, the closed provider set and the
cache= argument become sections of design-decisions.md; multibinding,
generator creators, an enter_scope alias and resolution tracing join its
Non-goals (each with the measured or structural reason); "error text is not
the contract" goes on the errors page; "find_container is the one supported
override, resolve/resolve_provider are not hooks" on the advanced-api page;
the decorator-free rule into the integration manual; the Click answer on the
typer page; the mkdocs-redirects constraint into docs/requirements.txt; the
rejected resolver designs into the performance page; two one-line comments
at _navigate and _compile_alias. The lazy-allocation numbers are on #478 and
the alias-binding question is #481.

Agent files (AGENTS.md, docs/agents/domain.md, issue-tracker.md) point the
triage prior-rejection check at the Non-goals section instead of docs/adr.
@lesnik512 lesnik512 changed the title docs(adr): revise every decision record; drop the two overtaken ones docs: remove docs/adr; move the decisions that survive to the docs and the code Sep 12, 2026
@lesnik512

Copy link
Copy Markdown
Member Author

Third commit, per review: docs/adr/ stays, scoped to internal (non-user-facing) decisions; the agent files say so. It holds one record, the most needed one: the resolve hot path is generated source with one frame per node, with the measured reasons no helper can replace the template and the per-shape / per-provider trade-off as consequences. Everything user-observable stays on the design-decisions page.

@lesnik512
lesnik512 merged commit c082427 into main Sep 12, 2026
10 checks passed
@lesnik512
lesnik512 deleted the docs/adr-revision branch September 12, 2026 08:22
lesnik512 added a commit that referenced this pull request Sep 12, 2026
Brings every module under `modern_di/` (except `exceptions.py`, out of scope)
to the AGENTS.md rule: public API docstrings state the contract, internal
helpers get a one-line contract, and rationale lives in one place rather than
a copy at each site it applies to.

Removed: the comments that cited an invariant test to justify an inlined
lookup. The frame-budget rationale they repeated already lives in
`test_resolve_costs_exactly_one_resolver_frame_per_node`, which is unchanged.

Kept, as the two comments a reader would otherwise read as a bug: the scope map
holds ancestors only (a self-entry is a reference cycle), and the `_next_deeper`
memo is keyed by enum type as well as member (IntEnum members hash by value).

Rationale that was recorded nowhere else, moved rather than deleted:

- Container `__init__` "inlined, not a helper" keeps a one-line pointer, now at
  the benchmark that measures it (`test_g6_build_child_container`). The spec
  expected the invariant test it cited to be its home, but that test measures
  the resolve path; nothing measures child-container construction.
- `_handle_recursion_error`'s reason for being a separate call (the coverage
  tracer re-arms on the fresh boundary) moved into its docstring.
- `find_context`'s `in`/`[]` over `.get(key, UNSET)` keeps one line: `.get`
  skips a dict subclass's `__contains__`/`__getitem__`, and no test covers it.
- "a root seeds `container_provider`" was promoted from a comment into the
  `__init__` docstring, where it is a contract: `resolve(Container)` works.

Test prose: the three provider test modules whose commentary described the
per-resolver "override front-guard" and the compiled closures now speak of the
compiled constant resolver and the generated template. Two modules beyond the
three the spec named are also touched, both for dangling references #480
created: the arity comment in `test_resolver_compiler.py` described a 0/1/
generic closure split the template replaced, and five citations of deleted
`docs/adr/2026-*.md` files (four in `test_container.py`, one in
`test_context_provider.py`) now name the design-decisions section that absorbed
the decision, "## 6. Validation is explicit".

ADR-0030's free-threading sentence needed no correction: #480 deleted that
record, and `docs/introduction/design-decisions.md` already states the fact.

No signature, public attribute, exception class, `__all__` or docs slug moves.
Verified two ways: every module's AST with docstrings stripped is identical
before and after, for `modern_di/` and for `tests/`; and a dump of the exported
names, signatures, MROs and `docs_slug` values is identical against main.

Prose share of `modern_di/`: 25.4% -> 18.6% of non-blank lines; excluding the
out-of-scope `exceptions.py`, 24.7% -> 15.7%. The package figure stays above
the 12-15% the spec targets: `exceptions.py` is 158 prose lines that this
change may not touch, 6.6 points of the package total on its own.

Closes #472
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant