ADR-020: Impact plan and ladder¶
Status¶
Accepted; amended by ADR-021 (D9, D11) and ADR-031 (D10) and ADR-050.
Decision¶
D1. impact/dataflow.py decides the group; impact/planner.py decides the file¶
dataflow.py derives a binding group's code from one Receiver record (binding
kind, assignments, escape_lines) and takes no ScanSpec. planner.py adds
the client rung and atomicity, which need a whole file's findings and the pack.
A group code that needed the pack would be a rule about the surface, not the
binding.
D2. The client rung applies to a module's call rewrites, not to its findings¶
client_source_unresolved withholds only what the new SDK routes through a
client: the configure call, a constructor, and a read of a declared receiver.
A type import, an annotation through it and an identity-mapped enum member need
no client, so a type-only module with no configure stays eligible. The rule
is public as planner.needs_client(finding, spec), because two of its answers
cannot be observed through a plan.
D3. The module's client is counted over resolved call findings¶
A configure counts only when the finding's kind is call and its
symbol is the pack's client_symbol; a pack with none (a library with no client) counts no
configure, so no row waits for one. A mock.patch("google.generativeai.configure")
target is a string and introduces no client; counting it would give a test file
a client and a real module a spurious multiple_configure_calls.
D4. A binding row names the group's own defect; a finding row names the ladder's¶
A withheld finding names SCAN_VOCABULARY.md §6's most
specific code. A bindings row is the record of the group: it names the
group's own code where it has one, and otherwise whatever withheld the group's
own lines. It never names file_not_fully_migrated (Binding refuses it), so
a closed-world group in a file withheld only by atomicity is eligible.
D5. The three group codes have a declared order, and the oracle pins none of it¶
class_attr_binding, then multiple_assignments, then model_object_escapes,
because each makes the next moot: a constructor site v0 does not rewrite, then
an unknown referent (so the escape set is unknowable), then an escape, which
presupposes a resolved group. No fixture carries two, and the order changes no
outcome: all three are rung 5 and needs_review.
D6. Atomicity reaches every otherwise-eligible row, and its causes are the graded ones¶
file_not_fully_migrated lands on every row that would otherwise be eligible,
not only the import, because
ADR-010 F-1 leaves the file exactly
as it was. caused_by is read off the graded findings after every other
rung has run, never off the binding table, so it names only rules that withheld
something.
D7. A group claims its constructor and its uses, and never a line-mate¶
A finding belongs to a group when its line is the constructor's or a use's
and its symbol is the group's receiver or a member of it; a line match
alone would withhold a line-mate such as the GenerationConfig(...) handed to
the constructor. Where two calls of one method share a line, one through the
bound name and one through a function result, ORDER prefers the group's code
over receiver_unresolved; both are rung 5 and needs_review.
D8. local_import is scan/analysis.py's, at rung 3, and the row is one word wider¶
local_import fires when a legacy import is not a module-level statement,
class bodies included, and is raised by analysis.py (analysis.BAILS, rung
3), which holds the scope mapping. An import under if TYPE_CHECKING: is in
module scope (C-26) and does not fire. multiple_assignments is dataflow.py's
and is len(scope[name]) > 1 exactly, never a count of legacy constructors,
because a use of a name assigned twice may refer to neither.
D9. ImpactPlan refuses a plan F-1 would leave half-applied¶
ImpactPlan refuses an eligible row beside a withheld one; the error names the
lines. A plan is one file's: every row names that file, findings are in document
order and bindings in constructor order. A manifest row is refused, because a
manifest legitimately carries an applied edit beside a withheld one; it belongs
to ManifestPlan (ADR-021 D6).
D10. §6's ladder is one total order, in the module that compares them¶
planner.ORDER is the ladder most specific first, and planner.LADDER gives
each code's rung, reproducing analysis.RUNG, dataflow.RUNG and
planner.RUNG; a test asserts that ORDER covers exactly LADDER with rungs
that never decrease. input_does_not_parse is ranked here because
scan/parse.py compares no codes. Only scan-time codes are ranked: §4's
rewrite-time bails get no rung, and neither does output_does_not_parse, whose
gate is asked only of a file nothing else withheld (ADR-031 D5).
D11. tests/oracle/ asserts equality, and the deferred table is down to one row¶
The oracle asserts status, bail and caused_by (in order) for equality on every
graded row, and grades the binding table: kind, name, scope, constructor line,
use lines, verdict and bail. It has no ladder tolerance and no deferral table,
so a row moving to another code must change the answer key.
Consequences¶
- A report that prints bindings beside findings must say that D4's two codes
answer different questions;
REPORT.mdhas no binding table yet, so the sentence is owed when one appears. - Two same-method calls on one line are told apart by neither line nor symbol, so the group's code wins (D7).