Repository navigation
Deepen: replace the twelve hand-maintained _selected_* rule-name sets with a lazily-memoised analysis context #167
Description
Activity
- addedenhancementNew feature or requestNew feature or requestneeds-triageMaintainer needs to evaluate this issueMaintainer needs to evaluate this issue
on Sep 20, 2026 This was generated by AI during triage.
Agent Brief
Category: enhancement
Summary: Replace eager_selected_*rule precomputations with a lazily-memoized per-file analysis contextCurrent behavior:
Between file parsing and rule execution, twelve hand-maintained functions (_selected_parents,_selected_classes,_selected_design_parents,_selected_design_contexts,_selected_design_bindings, etc.) eagerly precompute AST metadata based on enabled rule names. Each function manually specifies the subsets of rules that require its data. If a rule requiring a precomputed AST property is omitted from a_selected_*function, it silently produces no findings when executed. Additionally, parameters likeparentsandrule_namesare threaded through up to 19 functions.Desired behavior:
Introduce anAnalysisContextobject per analyzed file that lazily computes and caches AST properties (such astree,parents,classes,bindings,import_aliases) upon first request by a rule. Rules receive or query the context for the specific AST properties they need. The twelve eager_selected_*functions and extensive parameter threading are removed. Before removing the precomputation functions, a differential acceptance test verifies that analyzing a file with all rules enabled produces the exact union of findings from running each rule individually with--only.Key interfaces:
AnalysisContext: per-file context exposing lazily memoized AST analysis properties.- Rule functions: updated to retrieve required AST structures from
AnalysisContextrather than receiving threaded parameter tuples.
Acceptance criteria:
- A differential acceptance test is added verifying that running all rules produces the exact union of findings from running each rule individually with
--onlyacross a representative fixture suite. - The twelve eager
_selected_*precomputation functions are removed. - An
AnalysisContextabstraction provides lazily computed, cached access to shared AST attributes (parents,classes, etc.). - Downstream rule functions access needed attributes via the analysis context, reducing signature parameter bloat.
- Finding output across all rules, rulesets, and options remains identical to baseline.
- All existing test suites pass and self-analysis remains clean.
Out of scope:
- Modifying rule finding messages, priorities, or detection logic.
- Introducing cross-file caching or persistent disk caches.
- addedready-for-agentFully specified, ready for an AFK agentFully specified, ready for an AFK agentand removedneeds-triageMaintainer needs to evaluate this issueMaintainer needs to evaluate this issue
on Sep 20, 2026
Recommendation strength: Strong
Modules
src/messpy/cli.py—_findingsand the_selected_*family (_selected_parents,_selected_classes,_selected_design_parents,_selected_design_contexts,_selected_design_bindings, and the rest), plus the 29 rule functions downstream of them.Problem
Between "we parsed a file" and "we ran the rules" sits a layer of twelve functions whose entire job is to decide, from the set of enabled rule names, which precomputations to perform eagerly. Each one hand-lists the rules that need its product. Three of them list different subsets of the same five design rules.
This layer is a pure eagerness optimisation, not a correctness mechanism. Every one of the 29 rule functions already re-checks whether its own rule is enabled — the shape is repeated 29 times:
So the
_selected_*sets never change what is reported; they only change what work is done before the rules ask. That makes them a shallow layer with an expensive failure mode. The interface (twelve functions, twelve rule-name sets to keep in sync, threaded into_findingsas eight precomputed values) is nearly as complex as the implementation, and the penalty for getting a set wrong is silent: add a rule that needsparents, forget to add its name to_selected_parents, and the rule quietly returns nothing for some inputs. No test fails, because no test asserts the invariant.The invariant currently holds — running each rule alone via
--onlyproduces the same findings as the full run. But it holds by hand, and nothing guards it.The plumbing cost shows up downstream too: 19 functions in
cli.pytake aparents: dict[int, ast.AST]parameter and 16 take arule_names: AbstractSet[str]parameter, all so this eager layer's products can reach the rules that need them._design_findingsalone carries seven parameters.Proposed deepening
Introduce an analysis-context module — one object per analysed file — with a small interface of lazily memoised properties:
tree,parents,classes,bindings,import_aliases,class_index, and so on. Rules take the context and ask for what they need; the first ask computes and caches, subsequent asks are free.Then delete all twelve
_selected_*functions. The eagerness decision disappears entirely, because nothing is computed that nobody asks for — which is a stronger guarantee than the current sets provide, and it is enforced by construction rather than by maintenance.Deletion test: deleting this layer concentrates complexity into one memoisation policy rather than spreading it across twelve rule-name sets. Yes.
Benefits
Locality. Adding a rule stops being a two-place edit (write the rule, then remember to amend the right
_selected_*set). The dependency is declared where it is used — at the point the rule asks the context forparents— instead of in a list maintained at a distance.Leverage. The context is one interface that replaces both the twelve selection functions and the parameter threading. The
parentsparameter can come out of 19 signatures andrule_namesout of 16;_design_findingsshrinks from seven parameters toward two. Rule authors learn one object instead of a precomputation protocol.Test impact
The right order of work is: add the guard first, then delete the layer. Specifically, add a differential acceptance test that, for a representative fixture tree, runs the analyzer once with all rules enabled and once per rule with
--only, and asserts the per-rule findings union equals the full-run findings. This is an end-to-end test drivingrun(argv, stdout, stderr)on real source, exactly the default proofCODING_STANDARDS.mdasks for, and it pins the invariant the_selected_*sets currently maintain by hand.That test is valuable on its own even if this ticket is rejected: it converts a silent, class-wide failure mode into a loud one. With it green before and after, the deletion of the twelve functions is verifiable rather than hopeful. No new unit tests and no mocks are needed — the context is exercised through the same CLI surface.