From a5b76f27b7157a1abdac59b214e74e3b7fd3653f Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 30 Aug 2026 18:32:09 +0300 Subject: [PATCH] docs(planning): migrate deferred items to GitHub Issues and retire the concept Four of the five `planning/deferred/` items become GitHub issues labelled `needs-triage` (#434, #435, #436, #437), each carrying its full reasoning and its `## Revisit trigger` section verbatim, with relative links rewritten to absolute ones. The fifth, `2026-07-29-upstream-lean-convention`, is closed as resolved: #430 deleted `planning/.convention-version` and the vendored `planning/index.py`, which answers its question outright. #438 records that `ROADMAP.md`'s `## Under consideration` duplicates the queue; resolving it is not part of this change. `planning/scripts/perf-readability-audit.workflow.mjs` read `planning/deferred/` at run time, so its discovery step is repointed at open issues and its `deferred_items` bucket renamed `open_issues`. The `revisit_trigger` field goes with it: an issue carries no such section, and requiring one would install the convention this change retires. The repo now has two places for what outlives a PR: an open GitHub issue, or an ADR in `docs/adr/` recording a refusal. There is no third state, no `deferred` label, and no directory inviting new files in the retired format. Closes #431 Claude-Session: https://claude.ai/code/session_01FBic7HFGR6sdRfSW3cGEv2 --- .github/PULL_REQUEST_TEMPLATE.md | 5 +- CLAUDE.md | 14 ++- ROADMAP.md | 11 +- docs/adr/0017-exec-hot-path-declined.md | 3 +- planning/README.md | 14 +-- ...2026-07-17-warm-singleton-perf-headroom.md | 62 ---------- .../2026-07-19-free-threaded-throughput.md | 66 ----------- .../2026-07-29-upstream-lean-convention.md | 108 ------------------ ...26-08-01-compiler-provider-compile-spec.md | 50 -------- .../2026-08-03-performance-page-staleness.md | 44 ------- planning/scripts/bug-hunt-audit.workflow.mjs | 2 +- .../perf-readability-audit.workflow.mjs | 46 ++++---- pyproject.toml | 2 +- 13 files changed, 48 insertions(+), 379 deletions(-) delete mode 100644 planning/deferred/2026-07-17-warm-singleton-perf-headroom.md delete mode 100644 planning/deferred/2026-07-19-free-threaded-throughput.md delete mode 100644 planning/deferred/2026-07-29-upstream-lean-convention.md delete mode 100644 planning/deferred/2026-08-01-compiler-provider-compile-spec.md delete mode 100644 planning/deferred/2026-08-03-performance-page-staleness.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e0d16e9b..e0bf8ba1 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -42,7 +42,6 @@ effect. State the numbers, not "benchmarked". - [ ] **Rejected an alternative** with reasoning that would otherwise be re-litigated? File it as an ADR in [`docs/adr/`](../docs/adr/), numbered `NNNN-slug.md`, with a revisit trigger — not here. -- [ ] **Found real work you are not doing now?** File it in - [`planning/deferred/`](../planning/deferred/), self-contained, with a - revisit trigger — not here. +- [ ] **Found real work you are not doing now?** Open a GitHub issue, + self-contained enough to pick up cold — not here. - [ ] `just lint-ci` and `just test-ci` pass. diff --git a/CLAUDE.md b/CLAUDE.md index e3916d59..694951f8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,13 +67,15 @@ verification); it is reviewed with the diff. There is no change file and no lane to choose. A trivial PR (typo, dep bump, formatter) deletes the template and ships a conventional-commit title. -Two things outlive the PR: an alternative **rejected** with reasoning becomes an -ADR in [`docs/adr/`](docs/adr/) (`NNNN-slug.md`, sequential — see +Two things outlive the PR, and there are exactly two places to put them: an +alternative **rejected** with reasoning becomes an ADR in +[`docs/adr/`](docs/adr/) (`NNNN-slug.md`, sequential — see [`docs/agents/domain.md`](docs/agents/domain.md)), and real work **not -scheduled** goes to `planning/deferred/` (self-contained, with a revisit -trigger). There is no separate truth-home directory — the living truth about -behaviour is the code and its `INVARIANT:`-marked tests, and a behaviour change -is reviewed with the diff, not promoted to a page. See +scheduled** becomes a GitHub issue (see +[`docs/agents/issue-tracker.md`](docs/agents/issue-tracker.md)). There is no +third state. There is no separate truth-home directory either — the living truth +about behaviour is the code and its `INVARIANT:`-marked tests, and a behaviour +change is reviewed with the diff, not promoted to a page. See [`planning/README.md`](planning/README.md) for the admission check that decides where a given fact belongs. diff --git a/ROADMAP.md b/ROADMAP.md index 90d9394e..d3eb0512 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -61,11 +61,12 @@ Strawberry, Quart, RQ, APScheduler, Jobify, Flet, ag2. measurement drift. The bound stated when this was planned held: it trimmed the cell, it did not close it. dependency-injector's ~48 ns is a C-level slot read on a Cython core, which pure Python does not reach. - A third step remains open and is **deliberately deferred**: an APP-scoped - resolver could close over its `CacheItem` and reach ~16 ns, but the target is - only invariant because one registry belongs to one root, so the registry would - have to reference its root — the container reference cycle removed in 3.1.1. - That needs a weakref and a proof, for ~30 ns. + A third step remains open and is tracked in + [issue #434](https://github.com/modern-python/modern-di/issues/434): an + APP-scoped resolver could close over its `CacheItem` and reach ~16 ns, but the + target is only invariant because one registry belongs to one root, so the + registry would have to reference its root — the container reference cycle + removed in 3.1.1. That needs a weakref and a proof, for ~30 ns. ### Docs & ecosystem - **Canonical on-ramp per integration** — every official integration ships a diff --git a/docs/adr/0017-exec-hot-path-declined.md b/docs/adr/0017-exec-hot-path-declined.md index c4caff8f..78d5e094 100644 --- a/docs/adr/0017-exec-hot-path-declined.md +++ b/docs/adr/0017-exec-hot-path-declined.md @@ -26,4 +26,5 @@ row, and dissolving the dependency-purity framing manufactures no win the measur **Revisit trigger:** a user-reported, real-world resolve bottleneck on a high-arity node or a deep singleton/scoped chain — the two forms where `exec` could pay — that the closure resolver provably cannot close. A synthetic micro-benchmark or a hypothetical does not qualify. This is the -codegen-ceiling half of the open warm-singleton perf-headroom question. +codegen-ceiling half of the open warm-singleton perf-headroom question, +[issue #434](https://github.com/modern-python/modern-di/issues/434). diff --git a/planning/README.md b/planning/README.md index eb39540f..df166db2 100644 --- a/planning/README.md +++ b/planning/README.md @@ -17,7 +17,8 @@ the template and ship a conventional-commit title. - an alternative you **rejected** with reasoning → an ADR in [`docs/adr/`](../docs/adr/), numbered `NNNN-slug.md` -- work that is real but **not scheduled** → `deferred/` +- work that is real but **not scheduled** → a GitHub issue (see + [`docs/agents/issue-tracker.md`](../docs/agents/issue-tracker.md)) **3. Run `just check-links` before pushing.** @@ -66,11 +67,6 @@ whether that test or a sibling one catches a given regression. ## Artifacts -- **[`deferred/-.md`](deferred/)** — one file per open item, - each **self-contained**: it inlines the evidence and reasoning needed to pick - it up cold. A required `**Revisit trigger:**` section — an item with no trigger - is abandoned, not deferred. This directory is being retired in favour of GitHub - Issues; do not add to it. - **[`releases/.md`](releases/)** — one file per curated release, from `_templates/release.md`. Used **verbatim** as the GitHub Release body by [`release.yml`](../.github/workflows/release.yml), which fails a stable tag @@ -83,6 +79,6 @@ whether that test or a sibling one catches a given regression. `just check-links` and by `just lint-ci`. It covers the trees a site builder never sees. -A **deferred item's presence in `deferred/` is its status**. When it resolves: if -it ships, delete the file (its truth is now in the code and the release notes); if -it is declined, write the refusal as an ADR under [`docs/adr/`](../docs/adr/). +This directory holds no work queue. Open work is a GitHub issue and refused work +is an ADR under [`docs/adr/`](../docs/adr/); there is no third state and nothing +here to add a file to. diff --git a/planning/deferred/2026-07-17-warm-singleton-perf-headroom.md b/planning/deferred/2026-07-17-warm-singleton-perf-headroom.md deleted file mode 100644 index de9dfe13..00000000 --- a/planning/deferred/2026-07-17-warm-singleton-perf-headroom.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -summary: The C2 warm-singleton gap against the slot-memoized rivals stays open — the memo-swap technique that targeted it was measured and dropped, and closing it now requires removing the `resolve_provider` dispatch floor itself. ---- - -# Warm-singleton resolve headroom - -modern-di's warm singleton hit is the one scenario where it sits clearly behind -the field. Two levers were identified; both have since been ruled on, and what -remains open is narrower than the original item. - -## Why it is open - -The single-path compiled resolver (#334) replaced the interpreted recursion with -one memoized per-provider closure — a clean win everywhere (transient −37%, warm -singleton −31%, deep chain −61%, wide −62%). It left the warm hit still paying -`resolver_for` dispatch, the override front-guard, and `fetch_cache_item` on -every call. - -3.1.2 then inlined the two dict-lookup hit paths (`resolver_for` and -`fetch_cache_item`), calling the method only on a miss. That cut the warm hit -~25% — 170 → 128 ns by reference — and every top-level resolve benefits, since -one of the two sits in `resolve_provider`. It moved C2 against dishka to ~1.06x, -so modern-di is now the faster of the two on a by-reference basis. - -**What stays open:** the gap to the slot-memoized rivals. `dependency-injector` -reads a C-level slot on a Cython core (~48 ns), `that-depends` a lock-free slot, -`wireup` a self-modifying closure. Pure Python does not reach a C-level slot -read, so part of this gap is a floor, not a defect. The live comparative -numbers are in [`performance.md`](../../docs/introduction/performance.md), -regenerated by `just bench-report` — read that, not a snapshot here. - -Two levers, both now settled: - -- **The memo-swap** (swap the cached provider's resolver for a bare - `return value` closure) was built, fully tested, measured at ~1.6x, and - dropped — see - [`0015-warm-singleton-memo-swap-dropped`](../../docs/adr/0015-warm-singleton-memo-swap-dropped.md). - The decisive finding is that `resolve_provider`'s dispatch floor is upstream of - the swap and unremovable by it, so the technique provably cannot reach - "near-free". -- **The codegen ceiling on transient and deep-chain** is the cost of staying - `exec`-free: dishka and wireup inline dependency calls into generated source, - removing the per-node closure-call frame modern-di keeps. Re-declined — see - [`0017-exec-hot-path-declined`](../../docs/adr/0017-exec-hot-path-declined.md). - The supporting measurement: closures already capture ~80-90% of the available - ceiling, and `exec` buys a further 0-4% at fixed arity. So being modestly - behind the codegen leaders on construction-heavy graphs is an accepted floor, - not a bug. - -A third step was considered during the 3.1.2 work and deliberately not taken: an -APP-scoped resolver could close over its `CacheItem` and reach ~16 ns, but the -target is only invariant because one registry belongs to one root — so the -registry would have to reference its root container, reintroducing the reference -cycle removed in 3.1.1. That needs a weakref and a proof, for ~30 ns. It is also -recorded in `ROADMAP.md`. - -## Revisit trigger - -A user-reported warm-singleton bottleneck, **plus** a design that removes the -dispatch floor itself. Re-proposing the memo-swap unchanged is settled; so is -`exec` codegen. A `that-depends`-style per-APP-container slot array is the one -untried direction and would need its own measurement against the same bar. diff --git a/planning/deferred/2026-07-19-free-threaded-throughput.md b/planning/deferred/2026-07-19-free-threaded-throughput.md deleted file mode 100644 index 385f7c0a..00000000 --- a/planning/deferred/2026-07-19-free-threaded-throughput.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -summary: Concurrent resolve is thread-safe but does not scale with threads on a free-threaded build — diagnosed as CPython atomic refcount contention on shared hot-path objects, not the lock, and not liftable from this side; the fix is CPython's. ---- - -# Free-threaded resolve throughput does not scale - -On a free-threaded build (PEP 703), modern-di's concurrent resolve is thread-**safe** -but its throughput does not rise with thread count. Cached-hit batch time is -flat-to-worse as threads increase and matches the GIL build, while a pure-compute -control on the same harness scales ~3.5x at 4 threads. - -## Why it is open - -A thread-isolation sweep (fixed total work split across 4 threads on 3.14t, GIL -confirmed off; speedup = t1/t4, ceiling ~3.8x from a pure-compute control) pinned -exactly what serializes: - -| case | what is shared across threads | 4-thread speedup | -|---|---|---| -| pure compute | nothing | 3.8x (ceiling) | -| fully thread-local (distinct provider + container + value) | module globals only | 2.17x | -| distinct container per thread | + the **provider** object | 0.92x | -| distinct value, shared container | + provider + machinery | 0.84x | -| same singleton (the G14 case) | + the **returned value** | 0.64x | -| raw `dict.get` → one shared value | just the value's refcount | 0.28x | - -The read path is bounded by atomic reference counting on the objects every -resolve shares: most sharply the **returned singleton value** (a singleton *is* a -shared object — inherent), then the shared **provider** objects (distinct-container -0.92x against thread-local 2.17x is the provider), then the compiled-resolver -closures and their captured cells (every `LOAD_DEREF` of a shared capture increfs -it). The per-container lock is **not** the bottleneck. First-resolve does -additionally serialize on the double-checked creation lock — see -[design decisions](../../docs/introduction/design-decisions.md#the-thread-safety-boundary), -which states the supported thread-safety boundary and the Beta status of free-threaded support. - -A throwaway **immortalization experiment** (ctypes set of `ob_ref_local` on the -free-threaded build, offset verified against a known-immortal object) confirmed -the cause and quantified the ceiling: transitively immortalizing the shared -hot-path objects lifts the same-singleton case from ~0.6x to **~2.5x** (median of -3 runs, ≈68% of the ~3.7x machine ceiling). - -So it is squarely refcounting, and it *is* recoverable in principle — but the -only lever is unshippable: - -- There is **no public API** to immortalize user objects (PEP 683 is internal C-API). -- The `ctypes` poke is free-threaded-build-specific and unsafe. -- Part of the win requires immortalizing the user's **singleton value**, which is - then never freed — a memory leak, unacceptable in a general-purpose container. - -**The fix is CPython's, not ours.** Deferred reference counting expanding to -ordinary instances and cells lifts this for free, with no change here. - -Two adjacent benchmark axes were **declined** rather than deferred while this was -measured: a *comparative* override scenario (each framework's override/mock API -differs too much to compare fairly; G12 covers modern-di alone) and an -async-teardown scenario larger than G13's 10 resources (G13's LIFO loop already -captures the scaling). - -## Revisit trigger - -CPython deferred reference counting expands to cover ordinary instances and cells -— retest G14/G15, which should then scale for free. Or a user reports a real -resolve-throughput bottleneck, which is unlikely: resolution is a tiny fraction of -request work. A comparative version (against that-depends' lock-free slot) stays -out until the two contracts map onto each other fairly. diff --git a/planning/deferred/2026-07-29-upstream-lean-convention.md b/planning/deferred/2026-07-29-upstream-lean-convention.md deleted file mode 100644 index 3e0a2a37..00000000 --- a/planning/deferred/2026-07-29-upstream-lean-convention.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -summary: Whether the lean convention (no `changes/`, no `audits/`, spec in the PR body, and now a slimmed decision frontmatter with a locally forked `index.py`) goes upstream as planning-convention 3.0.0 — the harvest suggests it only pays off in a repo that already promotes reliably, which is a reason to soak longer than first planned. ---- - -# Upstream the lean convention, or keep it a local deviation - -`modern-di` currently **deviates** from -[`lesnik512/planning-convention`](https://github.com/lesnik512/planning-convention) -2.2.0 (the applied version in `planning/.convention-version`): `changes/`, -`audits/`, and `retros/` are gone, and the per-change spec lives in the PR body. -The plan was to prove it here for a few weeks, then cut 3.0.0 upstream and -re-apply via that repo's `APPLY.md` flow. - -## Why it is open - -**The harvest changed the argument for it.** The premise going in was that change -files hold nothing durable. That held — 113 change files plus 18 audit reports -yielded only 3 promotions to `decisions/`. But the *reason* was not the predicted -one. The why-nots were not missing from `changes/`; they had **already been -promoted** into `architecture/` (since removed — see the note below), -`ROADMAP.md`, `benchmarks/README.md`, and `docs/introduction/design-decisions.md` -at ship time. Of the 33 files examined, -16 were redundant because their content had been promoted or already had a -`decisions/` file. - -That is a conditional result, not a general one. Deleting `changes/` is safe -**because promotion discipline works here**. In a repo where the -promote-in-the-same-PR rule is followed loosely, the change files are the only -record, and deleting them destroys information rather than removing a duplicate. - -So the honest upstream question is not "does this work" but "what does a repo need -to be true of itself before this is safe" — and 3.0.0 would ship it to consumers -who may not meet that bar. Three routes: - -1. **Soak longer here**, then upstream as 3.0.0 with a stated precondition: - adopt only if your capability-documentation promotions are reliable - (`architecture/` was this repo's version of that target; see the note below). -2. **Upstream as an optional profile** alongside the current one, so repos choose. - This was considered when the plan was made and passed over; the harvest is an - argument to reconsider it. -3. **Keep it a permanent local deviation** — drop `.convention-version` and the - portability language, accept the loss of shared vocabulary across the org's - repos. - -## The second axis: the schema fork - -The deviation was layout-only when this item was filed. It is not any more. -Decision frontmatter dropped `status` and `supersedes`, keeping `summary` plus an -optional `superseded_by` whose *presence* means superseded — matching how -`deferred/` already treats location as status. That required editing -`planning/index.py`, which line 1 declares is **vendored into consumers' -`planning/`**. So this repo's copy of the tooling now differs from upstream -2.2.0, not just its directory layout. - -That widens the upstream question rather than changing its answer. A layout -deviation is something a consumer opts into by reading a README; a vendored-file -deviation is something that silently loses their edits the next time `APPLY.md` -re-vendors. Whichever route is taken, the schema has to travel with it or be -reverted — the two cannot ship apart. - -It also strengthens route 2 (an optional profile). The frontmatter change is -cheap to make optional — `DECISION_REQUIRED` is one tuple — in a way that -deleting `changes/` is not. - -Three things should inform whichever route is taken, and none has evidence yet: - -- Whether writing specs in PR bodies actually holds up over a month of real - changes, or quietly degrades into thin descriptions because nothing in the repo - enforces the shape the way `check-planning` enforced change-file frontmatter. -- Whether `decisions/` and `deferred/` grow at a rate that keeps them scannable. - They are 24 and 7 files as of 2026-07-30; the value of the whole design rests - on them staying small enough to read. -- Whether `superseded_by` is ever actually written. In 24 decisions the old - `status` field never once took its second value. If a year passes with no - supersession, the honest conclusion is that `decisions/` needs no state marker - at all and the key should go too. - -## Update: the promotion target this item relied on is gone - -`architecture/` was deleted 2026-08-11: -11 capability pages routed instead to code, a named `INVARIANT:`-marked test, -`decisions/`, or `docs/`, per the four-homes admission check now in -`planning/README.md`. The premise this item's central finding rested on — -deleting `changes/` was safe *because* `architecture/` promotion discipline -worked — is no longer demonstrable the same way, because the thing being -promoted *into* no longer exists as a single directory to point at. - -This does not resolve the upstream question; if anything it sharpens it. The -harvest's finding still holds as a historical fact (16 of 33 examined files -were redundant because their content had already reached a durable home by ship -time). But route 1's phrasing above — "adopt only if your `architecture/` -promotions are reliable" — no longer names a target this repo still has. A -2.30.0-vintage answer would need to generalize it to "adopt only if your -capability facts reliably reach *some* durable home (code, tests, -`decisions/`, `docs/`)," which is the four-homes model this repo now runs, not -the two-destination one (`architecture/` and everything else) the harvest was -measured against. Whether that generalized claim holds needs its own evidence; -none exists yet. The revisit trigger below is unchanged — this update narrows -what "check" means but does not move the date. - -## Revisit trigger - -After roughly a month of real changes under the new convention (so, from -late August 2026) — with three checks before deciding: re-read the last ten -merged PR bodies and judge whether they are specs or descriptions; count -`decisions/` + `deferred/` against the 24 + 7 baseline recorded here; and diff -`planning/index.py` and `planning/links.py` against upstream 2.2.0 to see how far -the vendored files have drifted. diff --git a/planning/deferred/2026-08-01-compiler-provider-compile-spec.md b/planning/deferred/2026-08-01-compiler-provider-compile-spec.md deleted file mode 100644 index 46175cc8..00000000 --- a/planning/deferred/2026-08-01-compiler-provider-compile-spec.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -summary: The resolver compiler reads provider internals across module lines (22 reads, 5 attributes, 4 provider types); a compile-spec handoff would remove the coupling, but it is a design change and the coupling is currently static, so SLF001 is suppressed per-file instead. ---- - -# Hand the compiler a compile-spec instead of reading provider internals - -`modern_di/resolver_compiler.py` builds each provider's resolver by reaching -directly into that provider's private attributes — `Factory._creator`, -`_parsed_kwargs`, `_kwargs`, `_resolution_step`, `_resolve_context_value`, -`_call_creator`, `_argument_resolution_error`, `_has_positional_only_gap`, -`Alias._find_source`. `SLF001` is now suppressed for that whole file via -`per-file-ignores` rather than on individual lines. The alternative — giving -each provider type a method that returns an explicit compile-spec, so no -underscored attribute crosses a module boundary — was not taken. - -## Why it is open - -The coupling is real but it is **static and small**: 22 private reads across 5 -attribute groups on 4 provider types (`Factory`, `Alias`, `ContextProvider`, -`_ContainerProvider`), and it has not grown as the compiler has. A per-file -suppression is an honest description of a file whose documented job is exactly -this: CLAUDE.md's key-files entry calls `resolver_compiler.py` "the **single -resolve path**", and the coupling is what lets each resolver hold its per-node -frame budget at 1. - -Doing the spec handoff properly is a design change, not a refactor, and it has -a cost on both sides: - -- Every provider type gains a compile-spec method, and the spec object becomes - a second surface that must stay in sync with the attributes it mirrors. -- `compile_resolver` already requires a new branch per provider type (it raises - `TypeError` for an unknown one); a spec adds a second thing a new provider - type must implement, so the "add a provider type" cost goes up, not down. -- It is compile-time only, so it buys **no** hot-path nanoseconds — the reads - are hoisted into closure locals once per provider. The whole case for it is - coupling hygiene. - -The reason not to do it opportunistically is that the per-file suppression is -only defensible while the coupling stays still. A suppression that quietly -absorbs new private reads stops being a description and becomes a blindfold. - -## Revisit trigger - -`resolver_compiler.py` needs to read a private attribute it does not already -read — most likely because a new provider type was added and `compile_resolver` -grew a branch. That is the signal that the coupling is growing rather than -sitting still, and it is checkable in a diff against the baseline recorded -above (22 reads, 5 attribute groups, 4 provider types). At that point the -per-file `SLF001` ignore should be replaced by the compile-spec handoff rather -than extended. diff --git a/planning/deferred/2026-08-03-performance-page-staleness.md b/planning/deferred/2026-08-03-performance-page-staleness.md deleted file mode 100644 index 49d9ae92..00000000 --- a/planning/deferred/2026-08-03-performance-page-staleness.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -summary: `docs/introduction/performance.md` goes stale on every performance release and nothing detects it — it was republished twice on 2026-08-03 alone, each time only because someone noticed. ---- - -# Nothing detects a stale comparative performance page - -`docs/introduction/performance.md` carries a `Measured with modern-di ` -line and four generated ratio tables. It is regenerated by hand with -`just bench-report`, and nothing checks that its stated version still matches -what ships. - -## Why it is open - -It went stale twice on 2026-08-03: once when 3.2.0 published while the page still -read 3.1.2, and again within hours when 3.3.0 published while it read 3.2.0. Both -were caught by a person noticing, not by a check. The second case mattered more -than the first — 3.3.0 moved the by-type column by 17-34% and crossed modern-di -ahead of dishka on two cells, so the published page understated the library -materially. - -The page is also the most public artifact in the repo: it makes named comparative -claims against four other frameworks. A stale one is not a cosmetic problem. - -The obvious check is cheap — compare the `modern-di ` in that line against -the newest bare-semver tag, and fail if the tag is newer: - -``` -grep -oE 'with modern-di [0-9]+\.[0-9]+\.[0-9]+' docs/introduction/performance.md -git tag --list --sort=-v:refname | head -1 -``` - -What makes it more than a one-liner is *where* it belongs. Running it in `lint-ci` -would red every PR between a release and the republish, which is a window that -legitimately exists — the table needs the comparative environment (four rival -packages) that `lint-ci` does not install, so the republish cannot be part of the -release itself. Candidates: a non-gating warning in the release workflow, a -scheduled check, or a release-checklist item in `planning/_templates/release.md` -that is verified rather than remembered. - -## Revisit trigger - -The next performance release — or the next time the page is found stale. If it -happens a third time, stop treating it as a checklist item and put the check in -`scheduled.yml`, which already runs without gating anything. diff --git a/planning/scripts/bug-hunt-audit.workflow.mjs b/planning/scripts/bug-hunt-audit.workflow.mjs index d194b490..b022d480 100644 --- a/planning/scripts/bug-hunt-audit.workflow.mjs +++ b/planning/scripts/bug-hunt-audit.workflow.mjs @@ -2,7 +2,7 @@ export const meta = { name: 'bug-hunt-audit', description: 'Four-dimension (UX/security/tests/logic) bug-hunt audit of modern-di with adversarial verify and triaged report.', whenToUse: 'Run when you want a fresh triaged backlog of bugs and quality risks across the modern-di repo.', - // The report is transient scratch: a sweep's durable output is a PR plus planning/deferred/ items. + // The report is transient scratch: a sweep's durable output is a PR plus a GitHub issue or an ADR. phases: [ { title: 'Discover', detail: 'map files, extract behavior claims' }, { title: 'Find', detail: 'four parallel dimension finders' }, diff --git a/planning/scripts/perf-readability-audit.workflow.mjs b/planning/scripts/perf-readability-audit.workflow.mjs index 9461a43c..ce374f34 100644 --- a/planning/scripts/perf-readability-audit.workflow.mjs +++ b/planning/scripts/perf-readability-audit.workflow.mjs @@ -1,10 +1,10 @@ export const meta = { name: 'perf-readability-audit', - description: 'Two-lens (performance + readability) decision-grade audit of modern_di, gated against the settled decisions/deferred corpus, producing a leverage-vs-risk report.', + description: 'Two-lens (performance + readability) decision-grade audit of modern_di, gated against known ground (settled ADRs, already-recorded open issues), producing a leverage-vs-risk report.', whenToUse: 'Run for a fresh both-axes refactor survey that separates new perf hypotheses from already-settled ground and finds off-hot-path readability seams.', - // The report is transient scratch: a sweep's durable output is a PR plus planning/deferred/ items. + // The report is transient scratch: a sweep's durable output is a PR plus a GitHub issue or an ADR. phases: [ - { title: 'Discover', detail: 'file map + settled corpus (decisions, deferred, guard scenarios)' }, + { title: 'Discover', detail: 'file map + known ground (ADRs, open issues, guard scenarios)' }, { title: 'Find', detail: 'two parallel lens finders: performance, readability' }, { title: 'Verify', detail: 'three lenses per finding, majority vote' }, { title: 'Synthesize', detail: 'dedup, leverage-vs-risk triage, write report' }, @@ -16,7 +16,7 @@ export const meta = { const CONTEXT_BLOB_SCHEMA = { type: 'object', additionalProperties: false, - required: ['baseline_commit', 'file_map', 'decisions', 'deferred_items', 'guard_scenarios', 'competitive_note', 'recent_commits'], + required: ['baseline_commit', 'file_map', 'decisions', 'open_issues', 'guard_scenarios', 'competitive_note', 'recent_commits'], properties: { baseline_commit: { type: 'string', description: 'short HEAD sha from `git rev-parse --short HEAD`' }, file_map: { @@ -45,17 +45,17 @@ const CONTEXT_BLOB_SCHEMA = { }, }, }, - deferred_items: { + open_issues: { type: 'array', - description: 'Every planning/deferred/ item: short title, one-line gist, revisit trigger.', + description: 'Every open GitHub issue: number, short title, one-line gist.', items: { type: 'object', additionalProperties: false, - required: ['title', 'gist', 'revisit_trigger'], + required: ['number', 'title', 'gist'], properties: { - title: { type: 'string' }, - gist: { type: 'string' }, - revisit_trigger: { type: 'string' }, + number: { type: 'integer' }, + title: { type: 'string' }, + gist: { type: 'string' }, }, }, }, @@ -72,7 +72,7 @@ const CONTEXT_BLOB_SCHEMA = { }, }, }, - competitive_note: { type: 'string', description: 'The current standing summary from planning/deferred/ (where modern-di sits vs rivals and the accepted floor).' }, + competitive_note: { type: 'string', description: 'Where modern-di sits vs rivals (docs/introduction/performance.md) and the accepted floor (the no-exec stance).' }, recent_commits: { type: 'array', items: { type: 'string' } }, }, } @@ -95,7 +95,7 @@ const RAW_FINDING_SCHEMA = { guard_scenario: { type: 'string', description: 'PERF: the G-id (G1-G15) that would confirm it, plus expected-leverage note. READABILITY: "n/a".' }, hot_path: { type: 'boolean', description: 'true if the code is on the resolve hot path' }, invariant_at_risk: { type: 'string', description: 'READABILITY: the invariant the change must not break (frame count / 100% cov / zero-dep / behavior), or "none". PERF: "n/a".' }, - settled_ref: { type: 'string', description: 'if you suspect this matches a decisions/ or deferred/ item, name it here; else "".' }, + settled_ref: { type: 'string', description: 'if you suspect this matches an ADR or an open issue, name it here; else "".' }, }, } @@ -117,7 +117,7 @@ const VERDICT_SCHEMA = { lens: { enum: ['read-real-code', 'decision-conflict', 'invariant-safety'] }, confirmed: { type: 'boolean', description: 'read-real-code: code matches claim. decision-conflict: finding is FRESH (not already settled). invariant-safety: change is safe & leverage-honest. Default false when uncertain.' }, reasoning: { type: 'string', description: '1-3 sentences citing what the code/decision actually is.' }, - settled_match: { type: 'string', description: 'decision-conflict lens only: the decision slug / deferred title this duplicates, or "" if genuinely fresh. Other lenses set "".' }, + settled_match: { type: 'string', description: 'decision-conflict lens only: the ADR slug / issue number this duplicates, or "" if genuinely fresh. Other lenses set "".' }, }, } @@ -154,11 +154,11 @@ Do exactly this: 3. decisions: read every file under docs/adr/. For each, record its slug (the filename without number/extension is fine) and a one-line holding — WHAT WAS DECIDED, especially what was rejected (e.g. "declined folding ContextRegistry into Container", "no exec codegen"). These are the settled-ground guardrail. -4. deferred_items: read every file in planning/deferred/. For each item, record a short title, a one-line gist, and its revisit trigger. Capture the perf items faithfully (warm-singleton memo-swap dropped, codegen ceiling, free-threaded non-scaling) — these must not be re-proposed as open. +4. open_issues: run \`gh issue list --state open --limit 100 --json number,title,body\`. For each, record its number, a short title, and a one-line gist. Capture the perf items faithfully (warm-singleton headroom, free-threaded non-scaling) — these are already known and must not be re-proposed as fresh. 5. guard_scenarios: read benchmarks/README.md and record the G1-G15 catalog: each id and what it isolates. -6. competitive_note: one paragraph from planning/deferred/ summarizing where modern-di currently sits vs rivals and what the accepted floor is (the no-exec stance, the C2 warm-singleton gap). +6. competitive_note: one paragraph summarizing where modern-di currently sits vs rivals and what the accepted floor is — the standing from docs/introduction/performance.md, the no-exec stance from docs/adr/0017-exec-hot-path-declined.md. 7. recent_commits: subject lines from \`git log --oneline -20\`. @@ -172,7 +172,7 @@ ${JSON.stringify(ctx, null, 2)} RULES: - Read the ACTUAL source before any finding (open modern_di/resolver_compiler.py, container.py, registries/*.py, providers/*.py, wiring.py, dependency_graph.py). The blob is a map, not the code. - Every finding is a HYPOTHESIS, not a claim. Set guard_scenario to the G-id (G1-G15) that would confirm it plus a one-line expected-leverage note. No prototyping, no bench runs — you are proposing what to measure, not measuring. -- SETTLED GROUND IS OFF LIMITS as an "open" proposal. If your idea matches a decisions/ ruling or a deferred/ item (warm-singleton memo-swap, child lazy-alloc, exec/codegen, free-threaded immortalization, per-provider compile seam, folding ContextRegistry), you may only raise it if you bring GENUINELY NEW evidence — and you MUST name the settled item in settled_ref. When in doubt, set settled_ref and let the verifier judge. +- KNOWN GROUND IS OFF LIMITS as an "open" proposal. If your idea matches an ADR ruling or an open issue (warm-singleton memo-swap, child lazy-alloc, exec/codegen, free-threaded immortalization, per-provider compile seam, folding ContextRegistry), you may only raise it if you bring GENUINELY NEW evidence — and you MUST name the settled item in settled_ref. When in doubt, set settled_ref and let the verifier judge. - Respect the stances as settled: no exec/codegen (zero-dep), conservative feature set, sync-only resolution. Do not propose them. - No lint/style. ruff and ty own those. - Set hot_path (is this on the resolve path?), leverage, risk, confidence. Set invariant_at_risk to "n/a" for perf. @@ -213,16 +213,16 @@ DO: open the file, read the cited line + 10-30 lines of context. If the code mat if (lens === 'decision-conflict') { return `You are the DECISION-CONFLICT verifier — the mature-repo guardrail. Decide whether this finding is GENUINELY FRESH or already settled. -SETTLED CORPUS (decisions + deferred items): +KNOWN GROUND — an ADR is a settled refusal, an open issue is work already recorded: decisions: ${JSON.stringify(ctx.decisions, null, 2)} -deferred_items: ${JSON.stringify(ctx.deferred_items, null, 2)} +open_issues: ${JSON.stringify(ctx.open_issues, null, 2)} FINDING: ${f} DO: -1. Check the finding against every decision holding and deferred item. -2. If it duplicates a settled ruling/deferred item WITHOUT genuinely new evidence: confirmed=false, and set settled_match to that decision slug / deferred title. Quote the holding in reasoning. +1. Check the finding against every ADR holding and open issue. +2. If it duplicates an ADR ruling or an already-open issue WITHOUT genuinely new evidence: confirmed=false, and set settled_match to that ADR slug / issue number. Quote the holding — or the issue's gist — in reasoning. 3. If it is genuinely fresh (or brings new evidence a skeptic would accept): confirmed=true, settled_match="". Default confirmed=false when the overlap is real and the "new evidence" is thin. Set lens="decision-conflict". Return the verdict.` } @@ -303,10 +303,10 @@ One paragraph: the dominant themes and the single most important takeaway (e.g. ## already-settled ### -- Matches: <decision slug / deferred title> +- Matches: <ADR slug / issue number> - Lens(es): … -**Why settled.** Quote the ruling/deferred holding. (These are recorded, not actioned.) +**Why settled.** Quote the ADR ruling or the issue. (These are recorded, not actioned.) After writing, return the structured summary (report_path + counts per bucket). If survivors is empty, still write the report with each bucket "(no findings)" and return zero counts. @@ -321,7 +321,7 @@ const context = await agent(DISCOVER_PROMPT, { schema: CONTEXT_BLOB_SCHEMA, model: 'haiku', }) -log(`discover: ${context.file_map.length} files, ${context.decisions.length} decisions, ${context.deferred_items.length} deferred items, ${context.guard_scenarios.length} guard scenarios @ ${context.baseline_commit}`) +log(`discover: ${context.file_map.length} files, ${context.decisions.length} decisions, ${context.open_issues.length} open issues, ${context.guard_scenarios.length} guard scenarios @ ${context.baseline_commit}`) phase('Find') const lenses = [ diff --git a/pyproject.toml b/pyproject.toml index 1b78c77f..203045a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ isort.no-lines-before = ["standard-library", "local-folder"] # The single resolve path compiles closures over provider internals: reading `Factory._creator` # and friends is the design, not an incident, and every compiled resolver returns `typing.Any`. # Suppressed per-file rather than on 31 individual lines. Growing this coupling is the revisit -# trigger on planning/deferred/2026-08-01-compiler-provider-compile-spec.md. +# trigger on https://github.com/modern-python/modern-di/issues/436. "modern_di/resolver_compiler.py" = ["SLF001", "ANN401"] # White-box tests assert on container and registry internals; that is what they are for. "tests/**" = ["SLF001"]