ADR-019: Resolution and detection rules¶
Status¶
Accepted; amended by ADR-020 (D8), and in D5 for a call on the new SDK's client.
Decision¶
D1 — Analysis resolves; the planner decides. BAILS is the seam.¶
scan/analysis.py emits Finding rows and Receiver records and never
decides whether a file may be rewritten. analysis.BAILS holds the eight codes
a scan can see (§6's rungs 1 to 3 and receiver_unresolved), ranked by
analysis.RUNG; impact/ raises the client rung and atomicity, which close
over verdicts not yet known. tests/oracle/test_scan_against_the_oracle.py
holds raised and graded codes, and each row's status and bail, equal.
D2 — One finding per resolution site, and the arguments are still walked.¶
A site resolves to a legacy IMPORT name and its parent is neither a legacy
Attribute it links into nor the Call it is the callee of. Arguments are
still walked, so basic/app.py:9 reports the constructor and the
GenerationConfig inside it. A Call site is kind: call, any other
kind: attribute.
D3 — A mention is anchored to the line it occurs on, and no tokenizer reads it.¶
The sweep walks Comment, SimpleString and FormattedString nodes and
reports each match at the node's start line plus the newlines before it, since
tokenize cannot read the latin-1 and bare-CR fixtures. A line with another
finding carries no mention (§1).
D4 — The hyphenated distribution name is never a source finding.¶
A mention is a dotted run that starts at a legacy module after a non-word,
non-dot character and continues in identifiers: google-generativeai is no
importable path, and a non-identifier segment would fail Finding.symbol.
D5 — An unresolvable receiver is reported only where the file builds a legacy object, and not for the new SDK's client.¶
<expr>.<method>(...) is receiver_unresolved when <method> is the supported
method of exactly one receiver in the spec, <expr> resolves to no legacy
binding, and at least one call in the file resolves to a constructor symbol.
Otherwise there is no symbol to name, as with a method two receivers share.
A half-migrated file holds a legacy model beside client = genai.Client(...), and
client.models.generate_content(...) names the same method. It is no finding
when the name at the root of <expr> (client in client.chats.create(...))
has every one of its assignments built from ScanSpec.new_client_symbol, the
pack's configure_to_client client class, resolved through
QualifiedNameProvider, so genai.Client, an alias of it and a from import
of Client all count. It is no finding at all, not a not_a_usage row: that
status is context only (SCAN_VOCABULARY §10), and a row for every call on the new
SDK would be noise. A name assigned anything else too, a parameter, an import, a
with target and an attribute such as self.client stay receiver_unresolved,
because a wrong guess that a receiver is the new SDK's would hide a legacy call.
A pack with no configure_to_client rule has no such class and reports as
before. The field moves spec_digest for a pack that sets it, so Gate 1 waits
for a re-measurement (COVERAGE.md gap 38).
D6 — A patch target is a legacy path that is a call argument, whole.¶
mock_patch_target fires on a string literal in argument position whose whole
value is a legacy-prefixed qualified name. Argument position, because a list of
patch callees would be behaviour a pack must carry (ADR-006); whole, because a
log message that starts with the prefix is prose.
D7 — The pattern id covers four shapes; the confidence reason covers three.¶
The dynamic_access pattern id enables all four shapes that reach the module
without a resolvable import; the dynamic_access reason covers the three that
reach it by name (importlib.import_module, __import__, an assignment into
sys.modules). getattr(<alias>, "X") goes through a resolved name, so its
reason and bail are module_alias_rebound and the whole file is rebound.
D8 — method_returns joins the pack, and a receiver nothing produces is a sixth incoherence.¶
generative_model_calls.params.method_returns maps a supported method to the
receiver it returns, because ChatSession has no constructor and only
GenerativeModel.start_chat(...) produces one. Each key must be a method of its
receiver, each value must have its own method list, no method may return its
own receiver, and every receiver in methods must be the ctor_symbol or a
method_returns value (ADR-018 D4); ScanSpec re-checks all but the third.
D9 — A comparison operand is not an escape; everything else that is not a receiver is.¶
An escape lets the object reach code the rewrite cannot see: stored, returned,
passed, exported, or read for an attribute that is not a supported method. A
comparison operand captures nothing, so the scan grades its group eligible; a
bare truth test is an escape. Because the comparison outlives a deleted
constructor, generative_model_calls refuses the group as
model_object_escapes when a use line has fewer rewritten uses than references.
D10 — A use's confidence reason follows the import form, by spelling.¶
The first segment as written decides: an alias bound with as is
alias_resolved, a name a from statement bound (from google import
generativeai too) is from_import_resolved, and anything else is
direct_import_resolved. It is total, and right in a file with two spellings.
D11 — conditional_binding is a property of the import statement.¶
It fires on an import whose bound name has more than one ImportAssignment in
its scope, the try/except double import. When the second binding is an
ordinary assignment elsewhere, the import resolved and stays alias_resolved,
and the file takes the file-wide code instead. An import under
if TYPE_CHECKING: binds once and is ordinary.
D12 — A conditional constructor still binds; a comprehension scope does not.¶
The assigned value is unwrapped through IfExp and BooleanOperation, since
X = genai.GenerativeModel(...) if flag else None still holds a model. A
ComprehensionScope binds nothing, as Binding.scope cannot spell one; its
constructor call is still reported.
D13 — The corpus exclusions live beside the harness, never in the answer key.¶
tests/oracle/harness.yaml excludes every *.after.* answer key and
encoding/_build.py, whose byte literals hold the legacy import, with one why
each and a test that each matches a file, because a case that could exclude its
own files could pass by grading less. The harness also unquotes a mock target's
symbol, ignores symbol on a parse_error, and reads verdict: auto as
scan_status: eligible.
Consequences¶
- Analysis has no dataflow through returns, by design, so D5 can report a false
positive (
Anything().run(x)beside a legacy constructor), asneeds_reviewand never an edit. - D5 covers method calls, not attribute reads (
COVERAGE.mdgap 20), and sees the new SDK's client only as a plain name: one held onselfor passed in is stillreceiver_unresolved. - D6 reports a patch target passed through a variable only as a mention (gap 17).
- D8's field defaults to empty; chaining deeper than one level is unexercised.