ADR-0006: Tiered module-size policy + enforced quality stack¶
Status¶
Accepted, implemented in EPIC #2046 PR 1 (issue #2047).
Context¶
SynthOrg is 100% AI-written and AI-maintained. The convention-gate
culture is strong: 51 custom check_*.py gates plus 21 check_*.sh
PreToolUse hooks, a meta-gate enforcing MANDATORY-to-gate parity, and
a manifest-driven ghost-wiring tracker for 118 components. Yet
architecture-level smells escape every existing gate:
- 54 files >800 lines in
src/synthorg/. The<800line inCLAUDE.mdwas a guideline, not a gate.api/state.py(2313),api/app.py(2152),api/controllers/providers.py(1530),meta/mcp/handlers/infrastructure.py(1345). - God-modules at the centre keep growing even when periphery
features stay clean. PR #2045 (charter feature, just merged) added
+113 LOC to
api/app.py, +16 tocore/enums.py, +12 toevents/persistence.py, +2 toapi/state.py. Every new charter file stayed under ~570 LOC: clean periphery, growing god-modules at the centre. - Junk drawers:
core/enums.py(57 unrelated StrEnums across 8 domains),observability/events/persistence.py(895 LOC of cross-sub-domain event constants). - Ruff thresholds default:
max-complexityimplicit;max-statements,max-branches,max-public-methodsnot set. File-level fan-in / cohesion / god-objects invisible to ruff. - No declarative import-layering beyond three custom gates that partially cover.
This ADR records the tiered module-size policy and the enforced quality stack landing alongside it.
Decision¶
Tiered module-size policy¶
A new # module-kind: <tier> header on the first non-blank,
non-shebang, non-encoding-declaration line of a file declares its
tier. The header position is strict: headers after the module
docstring or interleaved with imports are ignored. Tiers:
| Tier | LOC cap | Notes |
|---|---|---|
controller |
400 | API controllers, MCP handlers |
service / orchestrator |
600 | Long-lived stateful services, coordinators |
complex_service |
1100 | Audit-verdict tier for single-responsibility files that legitimately exceed the service / adapter caps. Reserved for #2052-style cohesion verdicts; see "Complex-service tier" below |
repository |
500 | Per-entity persistence repos |
adapter / integration |
700 | External-system adapters, browser/sandbox tools |
feature |
100 | A feature directory's feature.py manifest (lands in PR 2) |
code |
500 | Default for unheadered Python files |
tests |
800 | Anything under tests/ |
declarative |
exempt | Enums, event constants, settings definitions, DTO/schema modules |
generated |
glob-exempt | *.gen.*, *_pb2.py |
LOC counting matches check_baseline_growth.py::_count_text_entries:
physical lines excluding blank lines and #-prefixed comment-only
lines. Inline trailing comments DO count.
The shared helper scripts/_module_size_lib.py centralises LOC
counting, tier resolution, the tier table, and the generated-glob set
so the gate and the baseline generator cannot drift.
Existing offenders are absorbed via
scripts/_module_size_baseline.json. A baselined file may stay at or
below its recorded LOC; growth past the baseline fails. New files may
not exceed their tier cap regardless of baseline. Baselines shrink
monotonically (enforced by check_baseline_growth.py).
In addition, an explicit god-module allowlist (core/enums.py) must
net-shrink on every PR. This gate (check_no_growth_in_god_modules.py)
prevents the central files from absorbing more responsibility while PR 4
decomposes them.
The five api/ entries (app.py, state.py, auto_wire.py,
lifecycle.py, lifecycle_builder.py) drained from the allowlist in
PR 3 (#2049) once the controller decomposition brought each under its
tier cap; they are now governed by check_module_size_budget.py.
Complex-service tier¶
complex_service (cap 1100) is the audit-verdict tier: a single
cohesive responsibility expressed across more features than the 600 /
700 caps allow. It exists because the #2052 cohesion audit confirmed a
set of files as one responsibility each, at 642 to 1051 LOC. The
standard caps (service / orchestrator 600, adapter /
integration 700) cannot express "this file is cohesive AND larger
than 700", so the verdict could not be honoured under them: a 948-LOC
file tagged # module-kind: service stays over its cap and therefore
stays in the baseline. The 1100 cap lets a confirmed-cohesive file drop
out of the baseline while still imposing a real ceiling.
A cohesion verdict resolves to one of three states:
- Cohesive AND <= 1100 LOC -- tag
# module-kind: complex_service. The file falls under cap and drops from_module_size_baseline.jsonon the next regeneration. - Cohesive BUT > 1100 LOC -- tag
# module-kind: complex_service. The file stays in the baseline and is enforced against the 1100 ceiling with baseline absorption, exactly like any other tier. The 1100 cap binds even here, so the tier is not a way to exempt a file from a ceiling. - Two or more responsibilities -- not eligible. The file is reclassified as a decomposition target with a follow-up issue filed.
The tier is reserved for audit verdicts. It is assigned by a
cohesion-audit verdict, not opted into by new code. New service,
adapter, and controller files still hit their strict caps (600 /
700 / 400). A contributor facing an over-cap new file fixes the
cohesion or shrinks the file; reaching for complex_service to silence
the gate is a misuse of the tier.
Ten new custom gates¶
All baseline-driven, all wired into .pre-commit-config.yaml:
check_module_size_budget.py-- the tier-cap enforcer above.check_no_growth_in_god_modules.py-- god-module net-shrink rule.check_no_central_junk_drawer.py-- the dissolvedcore/enums.pymust not be recreated. (The formerAppState.__slots__count was drained to zero; that invariant now lives incheck_no_implicit_state_attribute.py.) Dissolution tracked in #2051.check_no_circular_imports.py-- AST-driven Tarjan SCC detection acrosssrc/synthorg/. ExcludesTYPE_CHECKINGand function-local imports.check_module_depth.py-- package-nesting depth ceiling (4 today).check_protocol_documented.py-- everyProtocolclass carries a non-trivial docstring (>=10 chars, notTODO/TBD/FIXME/...).check_no_module_level_io.py-- noopen()/subprocess/requests/httpx/socket/urllib.urlopen/Path.{read,write}_*at import time. Function bodies andif __name__ == "__main__":are exempt.check_state_slice_immutability.py-- state-slice classes must declareConfigDict(frozen=True, extra="forbid"). Empty baseline in PR 1 (state slices arrive in PR 2); the gate is in place so PR 2 cannot land slices that violate.check_strategy_protocol_injection.py-- factory-registered strategies must be referenced by Protocol type at callsites, not concrete impl.check_settings_namespace_complete.py-- everySettingNamespaceenum value has a correspondingsettings/definitions/<name>.pyfile.
Ruff tightening¶
[tool.ruff.lint.pylint]
max-args = 5
max-public-methods = 12
max-statements = 30
max-branches = 10
max-returns = 5
max-locals = 15
max-nested-blocks = 4
[tool.ruff.lint.mccabe]
max-complexity = 8
New selects: BLE, G, ERA, INP, DOC. DOC201/202/501 were
introduced under a broad per-file-ignore on src/synthorg/**, since
drained so they enforce across all of src/synthorg/ (Exemption Ledger
Section F).
Mypy strict++¶
disallow_any_explicit = true
disallow_any_generics = true
disallow_subclassing_any = true
no_implicit_reexport = true
warn_unreachable = true
extra_checks = true
strict_concatenate = true
enable_error_code = [
"ignore-without-code", "redundant-cast", "truthy-bool",
"narrowed-type-not-subtype", "unused-awaitable", "explicit-override",
"possibly-undefined", "deprecated",
]
Legacy packages with non-trivial Any usage carried minimal
[[tool.mypy.overrides]] blocks turning off only the flag that
fires; the override list was the technical-debt register that later
typing PRs lifted one package at a time. That drain is complete: no
package carries a disallow_any_explicit = false override any more.
The synthorg.* surface was drained by EPIC #2056 and the tests.*
surface by #2121, so tests.* now inherits the global
disallow_any_explicit = true with no Any-disabling override.
Pyright¶
Added as a CI artefact via .github/workflows/pyright.yml. The job
runs pyright --outputjson, uploads the report, and continue-on-error:
true. No pre-push gate.
New Python tools¶
Added to [dependency-groups.dev] and .pre-commit-config.yaml:
deptry (dependency hygiene), vulture (dead code), interrogate
(docstring coverage threshold), codespell (spelling), sqlfluff
(SQL lint, per-dialect).
Typeguard intentionally not landed in PR 1. It surfaces 200-500
TYPE_CHECKING-import sites that crash at runtime (typeguard resolves
annotations at runtime; TYPE_CHECKING-guarded names are unavailable
then). Fixing each site requires moving the import to runtime (risking
circular-import cycles) or rewriting the annotation as a string
forward-ref. The full fix-volume would blow the 10k LOC / 200 file
caps that bound this PR. PR 2 (feature-manifest substrate) restructures
the import graph anyway: feature modules become runtime-importable so
typeguard has far less to break on. Typeguard was deferred to a
dedicated multi-PR programme; #2182 (WARN activation) and #2183 (ERROR
hardening, via the EPIC #2303 decomposition) both landed, so typeguard
runs at the ERROR forward-ref policy across the full synthorg package.
The realised scope -- ~1,500 resolved-type mismatches plus
~1,055 TYPE_CHECKING-guarded modules -- proved far larger than this
PR-1 estimate. This was a deliberate scope choice, not a TODO.
New web tools¶
web/package.json gains knip, dpdm, madge, size-limit,
@lhci/cli. Per-route bundle cap 200 KB gzipped. Lighthouse
aggressive budgets (perf >= 90, a11y >= 95, CLS <= 0.05, LCP <=
2500ms, TBT <= 300ms), hard-blocking from day one. ESLint tightened
to match Python tier values (complexity 8, max-lines 400,
max-lines-per-function 80, max-params 5, no-restricted-imports for
feature isolation).
Tightened Go lint¶
cli/.golangci.yml enables gocyclo (min-complexity 10), funlen
(80/60), gocognit (15), nestif (4), stricter revive, and gosec
(security scanner; scoped exclusions for test files, the Windows
unsafe disk-free system call (G103), and the shell-completion installer's
perm/path findings (G301/G302/G304/G306)).
New docs / SQL / YAML tools¶
Landed: markdownlint, yamllint, sqlfluff, lychee (Markdown link
check). Deferred (see Exemption ledger): vale (Google style +
British dictionary).
Consequences¶
Positive¶
- Architectural badness has at least one mechanical gate. AI agents cannot grow god-modules unintentionally; the gate fires at pre-push.
- Baselines absorb today's reality so existing code passes. The day this merges, the bleeding stops.
- The codebase's enforcement substrate matches the discipline the team already practises in periphery features.
- PR 2 can land the manifest substrate on top without first having to land its own enforcement layer.
Negative¶
- Twelve new tools and ten new gates land in one PR. The pre-push surface grows; CI wall-clock grows by at most the longest new job (Lighthouse, ~2-3 min).
- Mypy
[[tool.mypy.overrides]]block list creates explicit technical debt that later typing PRs must drain (RESOLVED: EPIC #2056 drained thesynthorg.*surface and #2121 drainedtests.*; nodisallow_any_explicitoverride remains anywhere). - DOC per-file-ignore creates implicit technical debt that later
docstring PRs must drain (RESOLVED by #2065: the brace-expansion
ignore is deleted and
DOC201/202/501enforced acrosssrc/synthorg/). - Typeguard was wired but NOT activated in PR 1; activation (and the
~1,500 resolved-type mismatches plus ~1,055
TYPE_CHECKING-guarded modules it surfaces) was deferred to a dedicated programme. #2182 (WARN) and #2183 (ERROR, via EPIC #2303) both landed.
Neutral¶
- Header rollout in PR 1 covers only a small allowlist of obviously
declarative files (
core/enums.py,events/persistence.py,settings/definitions/*.py>500 LOC). Group-F audit (#2052) tags the rest. - Dissolution of
core/enums.pyinto per-domain files is tracked in #2051 (out of scope of this EPIC).
Alternatives considered¶
Blanket <N LOC ceiling¶
A single global file-size cap was the status quo (<800 lines in
CLAUDE.md). It false-positives on legitimate declarative blobs (event
constants, schema definitions) and forces real services to fragment
gratuitously. Rejected.
Tier-less ratchet via baseline alone¶
Capture today's max LOC per file, lock it, ratchet. Works for existing files but provides no signal for new files: a new 2000-line god-controller passes because nothing baselines it. Rejected.
Ruff config alone¶
Ruff measures function-level complexity. File-level fan-in / cohesion / god-objects are invisible to it. Tightening ruff is necessary but not sufficient; the custom gates fill the gap.
<800 guideline kept and enforced via reviewer attention¶
Existing data refutes this: PR #2045 grew api/app.py by 113 LOC
under reviewer attention. AI-generated code is too high-volume for
reviewer attention alone to enforce architecture. Rejected.
Exemption ledger¶
PR 1 declares many rules but cannot fix every existing violation in one PR. Each exemption below carries which subsequent PR or follow-up issue lifts it. Completing the EPIC #2046 sub-issues + the follow-ups below is the contract for "100% enforced".
A. Lifted naturally by PR 2 (#2048)¶
| Exemption | Mechanism | Acceptance |
|---|---|---|
_state_slice_immutability_baseline.txt (empty) |
PR 2 introduces every state slice; baseline must stay empty | Gate green after every PR 2 slice lands |
_settings_namespace_baseline.txt |
settings/definitions/settings.py already satisfies the namespace; the residual settings entry was a stale orphan |
LIFTED: baseline drained to 0 |
| Typeguard wiring | PR 2's manifest substrate was expected to eliminate most TYPE_CHECKING-only imports; in practice ~1,055 modules still guard signature types |
#2182 (WARN) and #2183 (ERROR, via EPIC #2303) both landed; see Section F |
B. Lifted naturally by PR 3 (#2049)¶
Status: LIFTED in PR 3 (#2049). api/app.py is a <200-LOC
discovery-based composition root (147 LOC); the eight multi-controllers,
api/auth/controller.py, and the two MCP-handler god-modules decomposed
into per-sub-domain packages; api/auto_wire.py, api/lifecycle.py, and
api/lifecycle_builder.py reduced to under-cap dispatchers/runners. The
five api/ entries (app.py, state.py, auto_wire.py, lifecycle.py,
lifecycle_builder.py) drained from the check_no_growth_in_god_modules.py
allowlist and are now governed at their tier cap by
check_module_size_budget.py; core/enums.py was deleted by #2051 and
dropped from the (now empty) allowlist.
api/state.py (354 LOC) is governed by the code-tier 500 cap (the EPIC's
<150 figure was descriptive, not a #2049 gate). The decomposed sub-package
files carry no baseline entries, so they are enforced at the controller-tier
400 cap directly; the stale baseline entries for the nine deleted monolith
files plus the four reduced api/ modules are dropped by the operator's
check_module_size_budget.py --update-baseline regeneration, the one remaining
mechanical step (the gate is green either way, since every file is already
under its cap). See ADR-0008.
| Exemption | Mechanism | Acceptance criterion in PR 3 |
|---|---|---|
Mypy override for synthorg.api.* (disallow_any_explicit, unused-awaitable) |
PR 3 decomposes 8 multi-controllers + api/app.py into per-sub-domain packages; new files written strict-clean |
Remove or narrow the synthorg.api.* override block to only synthorg.api.lifecycle* / synthorg.api.dto* |
_module_size_baseline.json entries for the 14 PR-3-named files (named multi-controllers + api/auth/controller.py, meta/mcp/handlers/{infrastructure,communication}.py, api/app.py, api/auto_wire.py, api/lifecycle*.py) |
PR 3 shrinks each below tier cap | Drop those 14 entries from the baseline. The remaining ~93 src/synthorg/api/**, meta/mcp/** entries in the baseline are covered by EPIC #2077 (Section F), not PR 3. |
Ruff BLE001/C901/PLR0911-15/DOC* per-file-ignore for src/synthorg/** (partial drain for decomposed packages) |
New small files pass strict | Tighten the per-file-ignore from src/synthorg/** to only the residual god-modules / undecomposed packages. (DOC* already lifted by #2065; BLE001/C901/PLR0911-15 remain.) |
check_no_growth_in_god_modules.py allowlist |
PR 3 shrinks api/app.py to <200 LOC and api/state.py to <150 LOC |
Gate flips from "must net-shrink" to "must remain at tier cap"; allowlist drained (mostly empty) |
C. Lifted naturally by PR 4 (#2050)¶
Status: LIFTED in PR 4 (#2050). The six per-entity repository
decompositions (Task / CostRecord / Message repositories, the decision
repos, the two backends slimmed to thin assemblers -- now 179 / 315 LOC,
far under cap) and the three service / recording decompositions
(workers/execution_service, infrastructure/services,
observability/prometheus_recording) landed; the synthorg.persistence.*
mypy override is dropped (the package is disallow_any_explicit-clean). The
three runtime import cycles are broken, so _circular_imports_baseline.txt
drains to 0. The nine decomposed-file _module_size_baseline.json entries
(seven deleted monoliths + the two slimmed backends) are dropped via the
operator's check_module_size_budget.py --update-baseline regeneration
(the gate is green either way once the deleted files are gone). The
synthorg.{communication, engine, observability}.* overrides are NOT dropped
here: PR 4 only decomposed / cycle-broke structurally within those packages
and did not Any-drain them; their per-package disallow_any_explicit drain
is EPIC #2056's charter (Section F), not PR 4's. Import-layering contracts
(ADR-0009) and the architectural feedback loop (ADR-0011) also land in PR 4.
| Exemption | Mechanism | Acceptance criterion in PR 4 |
|---|---|---|
Mypy override for synthorg.persistence.* |
PR 4 decomposes 6 repo factories per-entity; new files strict-clean | Drop persistence override (done) |
Mypy override for synthorg.{communication, engine, observability}.* |
PR 4 decomposes / cycle-breaks structurally but does NOT Any-drain these packages | Overrides retained at PR-4 time; per-package disallow_any_explicit drain is EPIC #2056 (the 22-package ratchet), not PR 4 (RESOLVED since: EPIC #2056 has drained every synthorg.* override) |
_circular_imports_baseline.txt (3 cycles: 2 in synthorg.persistence.*, 1 in synthorg.{memory, observability}.*) |
PR 4 import-linter contracts + decomposition catches these | Baseline drains to 0 (done) |
_module_size_baseline.json entries for the 9 PR-4-named files (persistence backends + decision repos + repositories + workers/execution_service + observability/prometheus_recording + infrastructure/services) |
PR 4 decomposes these | Drop those 9 entries via --update-baseline. The remaining ~22 persistence and engine entries in the baseline are covered by EPIC #2077 (Section F), not PR 4. |
D. Lifted by #2051 (junk-drawer dissolution)¶
| Exemption | Mechanism |
|---|---|
_central_junk_drawer_baseline.json (62 enums + 176 AppState slots) |
#2051 dissolves the remaining two files per-domain |
# module-kind: declarative header on core/enums.py |
File deleted by #2051 |
E. Lifted by #2052 (Group-F audit)¶
| Exemption | Mechanism |
|---|---|
_module_size_baseline.json entries for the Group-F legitimately-complex files |
#2052 tags each file the audit confirms as one cohesive responsibility with # module-kind: complex_service (cap 1100). Files at or below 1100 LOC drop from the baseline on regeneration; files above 1100 stay baselined under the 1100 ceiling; files with two or more responsibilities are reclassified into new decomposition issues. See the "Complex-service tier" subsection above |
F. Requires NEW follow-up issues (no existing PR lifts)¶
These exemptions are not addressed by any existing PR in the EPIC. Each entry below maps to a separate tracking issue that must be filed and closed for the project to reach 100% strict enforcement.
| Exemption | Required follow-up | Estimated size |
|---|---|---|
Ruff BLE001 (1007 sites) on src/synthorg/** |
Issue #2062: "Typed-except remediation: replace blind-except across src/synthorg/" | Large (multi-PR program by package) |
Mypy explicit-override (648 sites at introduction; per-package disabled) |
Issue #2057: "@override decorator backfill across synthorg.*" (RESOLVED: @override from typing added to all 461 remaining override sites across the 25 in-scope packages; every per-package disable_error_code entry for explicit-override dropped, leaving only the global enable_error_code) |
Medium (mechanical) |
Mypy unused-awaitable (108 sites at introduction) |
Issue #2058: "Async cleanup: await or store every Task" (RESOLVED: every per-package disable_error_code = ["unused-awaitable"] override removed from pyproject.toml; the 48 live src/synthorg/** findings fixed with the sanctioned _ = discard on already-tracked tasks (TaskGroup fan-out children + BackgroundTaskRegistry.spawn calls); unused-awaitable is now enforced globally via the top-level enable_error_code alone. The companion tests.* half is #2116, already LIFTED in Section F.1 row 1) |
Medium |
Mypy disallow_any_explicit (4136 sites across 29 synthorg packages at introduction: 22 individual blocks plus a 7-package grouped block; drained per-package) |
EPIC #2056: "Mypy strict++ ratchet" with per-package sub-issues (RESOLVED: every synthorg.* disallow_any_explicit override drained across children #2249-#2259, and #2121 drained the last tests.* surface; the global disallow_any_explicit = true now applies to all of synthorg.* AND tests.*, with the irreducible sites carrying reasoned per-line # type: ignore[explicit-any]. Regression-guarded by scripts/check_no_synthorg_any_override.py (pyproject vector) and scripts/check_no_explicit_any_inline_disable.py (inline-comment vector)) |
Very large (months) |
Mypy possibly-undefined (4 sites) |
Issue #2059: "Mypy possibly-undefined cleanup" | Trivial |
Mypy deprecated (3 sites) |
Issue #2060: "Mypy deprecated-API cleanup" | Trivial |
Mypy strict++ overrides on tests.* |
Issue #2061, sliced into sub-issues #2116, #2117, #2118, #2119, #2120, #2121 (see Section F.1 below for the breakdown by lifted error code). RESOLVED: every sub-issue landed; #2121 dropped the last block, so the entire tests.* override is gone and tests.* inherits the global strict++ bar. |
Small to Very Large per sub-issue (see F.1) |
Ruff ERA001 (13 sites, all false positives) |
Issue #2063: "Remove commented-out code (ERA001)" (RESOLVED: per-file-ignore dropped, code-shaped comments reworded) | Small |
Ruff DOC201/202/501 on src/synthorg/** |
Issue #2065: "Docstring Returns/Raises backfill + interrogate threshold flip" (RESOLVED: brace-expansion per-file-ignore deleted; DOC201/202/501 now enforced across all of src/synthorg/ except tests/ and scripts/) |
Large |
Interrogate fail_under 90 -> 95 |
Same as DOC backfill (RESOLVED: [tool.interrogate] fail-under flipped to 95) |
Medium |
ESLint complexity / max-lines / max-lines-per-function / max-params exempted on src/**/*.{ts,tsx} |
EPIC #2066: "Web component-size ratchet: decompose oversized React components", sliced into 4 sub-issues: #2092 (Foundation: utils + hooks + lib), #2093 (Stores incl. websocket), #2094 (Components + API types/endpoints), #2095 (Pages + override deletion). The override block at web/eslint.config.js:146-217 grows an ignores: list per sub-issue; PR D deletes the block. (RESOLVED: all sub-issues landed; the Pages tranche shipped as #2095 (D1) → D2a → #2141 (D2b), which deleted the src/**/*.{ts,tsx} override block entirely. The four caps now apply globally across web/src/**; the only surviving exemptions are components/ui/** (disables max-lines-per-function for cva variants) and the test/bench globs (disable all four).) |
Large (4 PRs filed) |
Go gocyclo / funlen / gocognit / nestif / revive path-excluded across cli/internal/** + cmd/** |
Issue #2067: "CLI complexity ratchet: per-package lift" | Medium |
| Typeguard activation: a dedicated multi-PR programme, #2182 (WARN) + #2183 (ERROR) | Infrastructure landed under closed #2068: typeguard==4.5.2 in [dependency-groups.test], ruff TC001/2/3 disabled project-wide (the convention shift), the typeguard.install_import_hook(["synthorg"]) line plus the --typeguard-packages=synthorg / --typeguard-forward-ref-policy=ERROR addopts (seeded commented under #2068, now live at ERROR and paired with the checker extensions in tests/_typeguard_checker.py) in tests/conftest.py + pyproject.toml, and @suppress_type_checks on api.app.create_app. #2050 attempted activation and reverted it: an authoritative full pytest -m unit with typeguard live (WARN) surfaced 1,949 failures across 231 test files -- ~1,500 of them resolved-type TypeCheckErrors that forward_ref_policy does NOT skip (AwareDatetime-vs-datetime, test doubles failing Protocol/isinstance checks, DTO generics), the single Clock.now() AwareDatetime return alone cascading to 978 lifespan-fixture failures, plus the ~1,055-module TYPE_CHECKING-guarded-signature class (check_tuple/check_typed_dict/check_protocol/check_callable eager-eval under PEP 649). That is far beyond #2050's scope and file cap, and typeguard is a Section-F follow-up, not a #2046 closure requirement. #2182 (WARN: AwareDatetime->datetime source fixes + a NameError-tolerant checker_lookup + test-double conformance) activated typeguard at WARN across the full synthorg package; #2183 (ERROR) then landed via the EPIC #2303 decomposition, which hoisted the ~1,055-module TYPE_CHECKING-guarded-signature class to runtime imports across file-disjoint children and flipped the policy to ERROR. The WARN-era NameError-tolerant wrapper is gone; only documented cold-import cycle-breakers and third-party guards (litestar's ASGI Scope TypedDicts) survive, handled by the checker extensions in tests/_typeguard_checker.py. |
Large (multi-PR programme) |
knip --no-exit-code (report-only, never blocks) |
Issue #2071: "Knip blocking: eliminate unused exports surfaced by knip" | Medium |
dpdm --skip-imports for stores/auth.ts -> api/client.ts cycle |
Issue #2072: "Fix auth -> client circular dependency" | Small |
_module_size_baseline.json residue: 109 files not covered by PR 3 / PR 4 / #2051 / #2052 (oversized files in persistence/, engine/, api/, meta/, etc. that no existing PR addresses) |
Issue #2077: "EPIC: Drain residual module-size baseline" (RESOLVED: the package-vertical decomposition PRs landed the last entries; locations is now {} and every file is enforced at its tier cap) |
Very large (per-package decomposition program) |
F.1. Issue #2061 partial landing¶
The first #2061 PR narrowed the original tests.* mypy override block
and tightened ~150 test files to the full strict++ bar individually
(foundation helpers, fakes hubs, settings, observability, core,
config, persistence, scripts, monitoring, telemetry, providers,
security, budget, memory, knowledge, communication, hr, integrations,
notifications, templates, workers, a2a, backup, client, plus most of
the meta and tools tiers). The residual override lifted only the error
codes that mapped to specific remaining cleanup work, each tracked by
its own follow-up issue. All of them have now landed: with #2121 the
tests.* override block is removed entirely and tests.* inherits
the global strict++ configuration with no Any-disabling override.
The table below records each lifted error code and the sub-issue that ratcheted it off; every entry is now closed.
| # | Lifted code in tests.* |
Remaining work | Follow-up issue | Size | Blocked on |
|---|---|---|---|---|---|
| 1 | unused-awaitable |
LIFTED. Every fire-and-forget tg.create_task(...) inside an asyncio.TaskGroup block (plus the discarded await reg.register(...) returns in the escalation-registry tests) now carries an explicit _ = ... discard: 49 sites across 15 files. The per-file inline # mypy: disable-error-code unused-awaitable tokens (32 test files; explicit-any retained for #2121) and the tests.* block entry were dropped. |
#2116 | Closed | nothing |
| 2 | method-assign |
LIFTED. The ~17 in-place repository-method reassignments in tests/unit/api/services/test_org_mutations_atomic.py and test_org_mutations_toctou.py were redesigned so both files type strict-clean with method-assign enforced; the tests.* block entry was dropped. |
#2117 | Closed | nothing |
| 3 | explicit-override |
LIFTED. Every overriding method on a test subclass (BaseCompletionProvider, BaseTool, RequestDrainMiddleware, logging.Formatter, logging.Handler, asyncio.Lock, plus internal _Fake* hierarchies) now carries @override; the per-file inline # mypy: disable-error-code comments and the tests.* block entry were dropped. |
#2118 | Closed | nothing |
| 4 | arg-type + redundant-cast + assignment + comparison-overlap + attr-defined (bundled, same root cause) |
LIFTED. FakePersistenceBackend now nominally implements PersistenceBackend; FakeVersionRepository is generic, ~15 concrete Fake*Repository classes had their signatures tightened to protocol parity, the missing idempotency_keys / seen_claims / principle_overrides properties were added, and the residual redundant casts were dropped. |
#2119 | Closed | nothing |
| 5 | deprecated |
LIFTED. pytest-asyncio 1.4.0 shipped the pytest_asyncio_loop_factories pluggy hook; the 9 event_loop_policy fixture overrides under tests/ migrated to per-conftest hook impls. The process-wide asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy()) call in tests/unit/conftest.py was dropped: empirical --count=2 runs showed the previously-feared IOCP teardown SEGV does not actually reproduce, while a separate pre-existing Windows + Python 3.14 + socket._fallback_socketpair deadlock under TestClient anyio-portal concurrency does fire regardless of the policy choice. One residual Pydantic v2 model_fields instance access in tests/unit/api/controllers/test_providers.py switched to class access. The tests.* block entry was dropped. |
#2120 | Closed | nothing |
| 6 | disallow_any_explicit |
LIFTED. The residual annotation-position Any across tests/unit/{api,tools,...}, tests/integration/, tests/conformance/, tests/e2e/, tests/benchmarks/, tests/evals_spine/, and tests/_shared/ was drained: precise types where the shape is known, the confined JsonDict / AsgiDict aliases in tests/_shared/json_types.py for genuinely dynamic JSON, object for genuinely-arbitrary checker / conformance-fake params, and a handful of justified per-line # type: ignore[explicit-any] for irreducible heterogeneous-factory / mock-introspection / model-kwargs-spread signatures. The 74 per-file # mypy: disable-error-code="explicit-any" comments and the tests.* block entry were dropped; scripts/check_no_explicit_any_inline_disable.py now blocks any module-level # mypy: comment that re-lifts the flag (across both src/ and tests/). |
#2121 | Very large | nothing |
| 7 | unused-ignore |
LIFTED. Dropping the tests.* override also removed its disable_error_code = ["unused-ignore"], so stale # type: ignore[...] comments now surface as errors. The redundant comments were removed as part of #2121; tests.* is now fully strict with no error-code suppression, and the inline-disable gate above blocks a file-level unused-ignore lift too. |
(bundled into #2121) | Trivial | Entries 1-6 above |
G. Permanent design decisions (NOT exemptions to lift)¶
- God-module net-shrink rule (
check_no_growth_in_god_modules.py). The allowlist itself is permanent; entries drain as PR 3 decomposes each file. - Generated-glob exemption (
*.gen.*,*_pb2.py): generated code is by definition out of scope for source-level lint. declarativetier exemption: declarative data files don't have a meaningful LOC ceiling; junk-drawer growth is gated separately.
Related¶
- EPIC #2046: the umbrella program this PR opens.
- Sub-issues #2048 (manifest substrate), #2049 (controller decomposition), #2050 (repos/services + import-layering).
- Follow-up #2051 (dissolve
core/enums.py). - Follow-up #2052 (audit Group-F legitimately-complex files).
- Follow-ups required by Section F of the Exemption Ledger above; filed under the EPIC #2046 master ledger.