RIPR-SPEC-0028: Python Preview Static Facts
August 25, 2026 · View on GitHub
Status: proposed
Problem
The Python preview adapter must emit RIPR static facts from Python source
without depending on mypy, pyright, an import graph, or any runtime
tooling. Syntax-first facts are the contract; semantic enrichment is
explicitly deferred.
This spec defines the per-language behavior the adapter must produce. The language-neutral boundary, the router, the output metadata, the opt-in posture, and the cross-language non-goals live in RIPR-SPEC-0026: Language adapter contract. The proposal context is RIPR-PROP-0001: Multi-Language Adapter Preview.
Behavior
The Python preview adapter is enabled by repo configuration:
[languages]
enabled = ["rust", "python"]
When enabled, it routes *.py files. It emits the same RIPR fact
families as the Rust adapter and tags each finding with
language = "python" and language_status = "preview".
When ripr.toml is absent, Python project detection may also enable the
adapter for roots with markers such as pyproject.toml, setup.py,
setup.cfg, requirements.txt, pytest.ini, tox.ini, noxfile.py, or
Python files under src/ or tests/. An explicit ripr.toml remains
authoritative and can keep Python disabled.
The adapter is syntax-first. It must not depend on mypy, pyright, a
runtime test runner, or an import graph. When syntax-first analysis
cannot classify, the adapter emits an explicit static_limit_kind
instead of silently coercing to no_static_path.
Inputs
- Python source files routed to this adapter.
- Diff spans inside those files.
- Repo configuration including
[languages] enabledand any future Python-specific options layered on top of this spec. - Python project marker filenames used only to decide whether missing-config roots should select Python preview analysis.
The adapter may observe project marker filenames, but it does not parse
pyproject.toml build metadata, install requirements, read virtualenv contents,
read generated stubs, or consume runtime test output.
Owner Facts
Owners the adapter must recognise:
- top-level
deffunctions andasync deffunctions - nested functions only when they participate in changed behavior
classdefinitions and their methods (regular,@staticmethod,@classmethod)- decorated functions and methods; the decoration is preserved as syntactic context, not resolved semantically
- module-scope expressions whose right-hand side participates in changed behavior (when a probe attaches to the expression)
Owner kinds emitted in output (per RIPR-SPEC-0026):
function,method,class_method,module_function.
The stable Python probe.owner identifier is language-qualified and
path-qualified:
python:<normalized/path.py>::<qualified_owner>
Examples include python:app/pricing.py::calculate_discount,
python:app/cart.py::Cart.apply_discount,
python:app/models.py::Invoice, and
python:app/settings.py::<module>. Class owner findings may carry a
class-shaped probe.owner while omitting owner_kind until the shared
RIPR-SPEC-0026 owner-kind vocabulary explicitly adds a class value.
Test and Assertion Facts
Test discovery:
pytesttest functions namedtest_*at module level- pytest test methods under
class Test* unittest.TestCasesubclasses and theirtest_*methods- parametrized tests via
@pytest.mark.parametrize(recognised syntactically) - pytest fixture and parameter names captured from test function signatures
- fixture files matched by configured patterns (default:
test_*.pyand*_test.py; the configured pattern is part of the repo config cross-spec contract) - framework-shaped verify commands for related tests when the static selector
is known:
pytest path::nodefor pytest andpython -m unittest module.Class.test_methodfor unittest
Assertions / oracles the adapter must recognise:
The repair-routing lane preserves a conservative internal oracle shape for
pytest and unittest facts without expanding the shared public OracleKind
vocabulary. That shape distinguishes exact assertions, boundary comparisons,
exception assertions, dict/object field assertions, output assertions through
caplog.text / capsys.readouterr().out / stdout-stderr-output attributes,
status-code and exit-code assertions, broad smoke assertions, reach-only tests,
mock expectations, and custom assert_* helpers.
- bare
assert expr→ smoke oracle assert a == bandassert a != b→ exact-value oracle (for==) or smoke-style negative oracle (for!=, recorded as broad)assert isinstance(value, SomeType)→ broad-type oraclepytest.raises(...)context manager → error-path oracleself.assertEqual(a, b)andassertNotEqual→ exact-value oracleself.assertRaises(...)→ error-path oracleself.assertTrue(...)/assertFalse(...)→ smoke oraclemock.assert_called*family (assert_called_once_with,assert_called_with,assert_called,assert_not_called) → side-effect/call oracleunittest.mockpatches recognised syntactically as call-context only
Related-test heuristics mirror the Rust and TypeScript adapters: changed
owner name match, import-reference match, file-path proximity, and
syntactic call proximity. Direct owner calls must be token-aware. Module
import aliases may match attribute calls such as pricing.apply_discount(...);
arbitrary object method calls must not be treated as related to a top-level
function owner unless the changed owner is itself a method or class method.
Test-name and fixture-name proximity may provide a suggested repair location,
but these links must be marked uncertain, must keep weak reachability, and must
not promote unrelated assertions to strong revealability.
Probe Facts
Probes the adapter must generate (syntax-first):
- predicate probes for changed
if/elif/conditional-expression boundary conditions - return-value probes for changed
returnand final-expression shapes - error-path probes for changed
raisestatements andtry/exceptshapes - field probes for changed attribute assignments
- call probes for changed function and method calls including argument changes
- mock-interaction probes for call surfaces resolved through a syntactic
mock.Mock()/MagicMock()initializer
The adapter must not emit a probe for a change that both source versions parse as an edit inside a real module, class, function, or async-function docstring. It must derive this from AST string-expression spans in both source versions, not from triple-quote text alone. Assigned strings, f-strings, and a behavioral line replaced by a newly introduced docstring remain analyzable.
When the adapter cannot classify, it emits one of the static_limit_kind
values defined in RIPR-SPEC-0026:
dynamic_dispatch(e.g.,getattr(obj, name)(...)or mapping lookups such asdict[key]followed by invocation)metaprogramming(e.g.,class ...(metaclass=...),type(...), or__getattr__indirection)missing_import_graph(the symbol is imported from a module the adapter cannot resolve syntactically, or the changed line uses dynamic import syntax such asimportlib.import_module(...)/__import__(...))decorator_indirection(the decorator changes the call semantics in a way the syntax-first adapter cannot follow; simple route decorators such as@app.get(...),@api.post(...), or@router.api_route(...)may be treated as static route metadata when the changed behavior itself is a supported repair shape)mocked_module(e.g.,@patch(...)ormonkeypatch.setattr(...)observed at the related-test call site)opaque_custom_assertion_helper(e.g., a related test observes the changed owner only through anassert_*(...)helper body the adapter does not inspect)property_based_test(e.g., a related test uses Hypothesis@given(...)generated inputs whose concrete discriminator coverage is not statically known)unresolved_pytest_fixture(e.g., a related pytest test uses fixture-sourced input or expected values whose concrete discriminator coverage is not statically known)unsupported_syntax
Canonical Gap Identity
For non-static-limit Python preview findings, the adapter emits an optional canonical gap identity that avoids line-number-only matching:
gap:python:<normalized/path.py>:<owner_path>:<behavior_kind>:<probe_kind>:<normalized_discriminator>
The identity parts are also available as a structured canonical_gap object in
JSON output. behavior_kind is derived from the Python probe family, such as
predicate_boundary, return_value, exception_path, field_value, or
call_or_output_effect. normalized_discriminator is syntax-derived from the
changed predicate, return expression, raised exception, field assignment, or
call/output text after whitespace and punctuation normalization.
Static-limit findings keep their named static_limit_kind and may omit
canonical_gap_id until the repair-routing lane adds typed non-actionable
gap-state projection. This prevents dynamic or unsupported Python cases from
being mistaken for bounded repair work before repair cards and stop reasons
exist.
Python RIPR Evidence
For non-static-limit findings, the adapter must express the same RIPR evidence spine as other languages:
- reachability: related Python tests, direct calls, import-alias calls, or conservative proximity links
- infection: the changed Python behavior family, such as predicate, return value, exception path, field/object state, or call/output effect
- propagation: whether the changed behavior is already at an output boundary, can flow through an exception/control boundary, or can only weakly propagate through unresolved control, object, or side-effect flow
- revealability: the strongest extracted pytest or unittest oracle and whether
it discriminates the changed behavior. A strong oracle is credited as
discriminating — and the finding classified
exposed— only when its assertion observes the changed sink: it must reference the changed owner (by name or import alias) or a changed-sink identifier/literal from the changed line. A strong oracle that reaches the owner but observes a different value (for example a wrapper's return value rather than the changed boundary) downgrades toweakly_exposedwith a typed reason. Reach plus a strong oracle alone must not creditexposed, or the classification degrades into coverage.
Static-limit findings must fail closed. They keep any observed reachability and
oracle facts, but their infection and propagation stages remain unknown, the
finding class is static_unknown, a typed stop reason is emitted, and no
canonical repair-gap ID or repair recommendation is emitted.
Direct weak findings may also carry activation-level missing discriminator
facts for the first preview repair classes. For example, a changed
if amount >= threshold: predicate can emit amount == threshold; a changed
return amount >= 100 expression can emit return value == amount >= 100; a
changed raise ValueError("positive required") path can emit
raises ValueError matching "positive required"; a changed
self.status = "paid" assignment can emit self.status == "paid"; and a
changed logger.warning("coupon expired") call can emit
log contains "coupon expired". These facts are evidence only until a later
repair-card contract supplies the test shape, verify command, receipt command,
and edit boundaries. Heuristic-only links, no related-test paths, and static
limits must not emit repair guidance.
Sink alignment visibility
The adapter already decides exposed vs weakly_exposed by whether a strong
oracle observes the changed sink (the revealability rule above). It must also
surface that decision so a consumer can see why a strong oracle did or did
not credit exposed, via four additive optional output fields. They are a pure
read-out: the boolean the classifier uses is derived from the surfaced
oracle_alignment, so the visible value can never disagree with the decision.
changed_sink— the comma-joined significant tokens of the changed line.observed_sink— the strongest related oracle's assertion text.oracle_alignment— a controlled enum mapping to the existing branches:direct/alias/changed_sink_tokenappear only onexposedfindings (strong oracle observes the owner name, an import alias, or a changed-sink token);orthogonalappears only on the fail-closedweakly_exposedbranch (strong oracle observes a different sink);unknowncovers every other finding (no strong oracle, or a<module>owner with no usable token).alignment_reason— a stable snake_case token explaining the value (e.g.strong_oracle_observes_different_sink).
These fields are advisory preview evidence; they do not change the
classification and do not claim runtime maturity. The contract does not bump the
JSON schema_version. Python's support tier remains governed by
Support tiers.
Required Evidence
The Python preview contract is supported only when the implementation can show:
- a fixture corpus pinning at least one example per owner kind above
- a fixture corpus pinning at least one example per oracle kind above
- a fixture corpus pinning at least one example per probe kind above
- a fixture corpus pinning at least one example per
static_limit_kind - fixtures cover plain
def,async def, classes, methods, decorated methods, and module-level fixtures - fixtures proving broad
pytest.raises/self.assertRaisesare recognised as weak error-path oracles whilepytest.raises(..., match=...)/self.assertRaisesRegex(...)are recognised as exact exception observers - a fixture proving unittest assertion argument shapes can identify field, output, and status-code oracles
- a fixture proving pytest and unittest related tests produce framework-shaped verify commands
- fixtures proving test-name and fixture-name proximity are related-test heuristics but remain explicitly uncertain
- fixtures proving Python preview findings carry stable canonical gap IDs across human, JSON, GitHub annotation, and SARIF output while static-limit findings remain limitation evidence rather than repair gaps
- fixtures proving non-static-limit Python findings carry RIPR infection and propagation evidence instead of placeholder unknowns
- fixtures proving static-limit Python findings fail closed as
static_unknownwith typed stop reasons and no repair recommendation or canonical repair-gap ID - fixtures proving the first repair classes carry activation-level missing discriminators for predicate boundaries, return values, exception paths, field/object values, and output/log/call effects
- fixtures proving strong-oracle, no-path, heuristic-only, and static-limit cases suppress repair guidance rather than becoming repair-ready work
- a check-output gap-ledger fixture proving Python repair cards can become bounded agent packets with verify and receipt commands while remaining preview/advisory
- outcome fixtures proving Python canonical gaps can close, remain unchanged, reopen, strengthen without closing, and weaken across check-output snapshots while preserving static/advisory receipt language
- a field/object fixture proving a returned constructor keyword such as
return User(active=True)can route to a syntax-only object-field repair card with a discriminator such asresult.active == True, a direct object-field assertion shape, and a stop condition when the returned object does not expose that keyword as a public field or attribute - a non-boundary return-value outcome fixture proving a weak broad assertion can become an exact return assertion and close the canonical Python gap
- a non-boundary exception-path outcome fixture proving a weak broad exception observer can become exact message evidence and close the canonical Python gap
- a non-boundary field/object outcome fixture proving a broad object truthiness assertion can become an exact field assertion and close the canonical Python gap
- a non-boundary output/log outcome fixture proving a broad output observer can
become exact output text evidence and close the canonical Python gap, while
containment checks such as
"..." in caplog.textremain conservative weak evidence - fixtures proving direct weak related pytest and unittest tests are preferred
as
strengthen_existing_testrepair targets instead of redundant new tests - output tests proving eligible Python repair cards are projected into GitHub annotations and diff-scoped SARIF as advisory repair context, and proving no-action/static-limit Python findings project explicit no-repair-card and no-agent-packet annotation context, not gate or receipt authority
- a fixture proving
mock.assert_called*is recognised as a side-effect oracle - a fixture covering parametrized
pytestcases - a fixture covering pytest fixture parameters and a non-exact output/log oracle shape
- generated CI fixtures cover Python preview output visible only when
[languages]declarespython - LSP protocol smoke covers a Python seam diagnostic, hover, code action, and evidence packet
- VS Code e2e smoke covers opening a Python file when the adapter is enabled
cargo xtask dogfoodrecords a checked Python preview receipt- the capability matrix gains
Python preview static factsatalpha, marked preview, with metrics from RIPR-SPEC-0026 plus Python-specific counts where the language adds vocabulary
Non-Goals
- No type checking (
mypy,pyright,pytype). - No
pyproject.toml,setup.py, orrequirements.txtparsing. - No virtualenv resolution or installed-package introspection.
- No runtime test runner integration beyond syntax pattern recognition.
- No
@dataclasssemantic expansion beyond detecting the syntactic shape. - No mocking framework introspection beyond syntactic recognition.
- No automatic source edits, generated tests, or provider calls.
- No claims of parity with Rust evidence.
Acceptance Examples
Function with boundary gap:
def apply_discount(amount: float, threshold: float) -> float:
if amount >= threshold:
return amount * 0.9
return amount
Existing tests:
def test_discount_applies_above_threshold():
assert apply_discount(100, 50) == 90
def test_no_discount_below_threshold():
assert apply_discount(10, 50) == 10
Expected static evidence:
- owner:
apply_discount(function) - predicate probe:
amount >= threshold - oracle:
exact-valueviaassert ... == 90andassert ... == 10 - missing discriminator:
amount == threshold - finding emits
language = "python",language_status = "preview",owner_kind = "function"
Error path with pytest.raises:
def test_rejects_negative_amount():
with pytest.raises(ValueError):
apply_discount(-1, 50)
Expected static evidence:
- oracle:
error-path - the
pytest.raisescontext manager is recorded as the discriminator surface
Decorator indirection limit:
@retry(times=3)
def fetch_total(client):
return client.get_total()
Expected static evidence:
- probe emits
static_limit_kind = "decorator_indirection"; finding stays conservative.
Simple route decorator repair:
@api.post("/checkout")
def checkout(coupon_expired):
response.status_code = 422
Expected static evidence:
- route decorator is preserved as owner context rather than treated as arbitrary decorator indirection;
- probe can emit a field/object repair card with missing discriminator
response.status_code == 422; - finding remains
preview/ advisory and does not execute route registration.
Test Mapping
Follow-up fixtures and tests cover the owner, test, assertion, related
test, probe, and static-limit cases listed under Required Evidence, plus
generated CI behavior and LSP smoke coverage. The CLI first-use path also
checks that ripr pilot can surface a top Python repair card from diff-scoped
preview evidence without requiring a Cargo workspace, and that ripr first-pr
can route an existing Python preview GapRecord into a preview-limited
start-here packet for a Python project root. The first-PR mapping also covers
the direct --check-output <check.json> bridge that materializes the
check-output-derived gap decision ledger before selecting the same preview
Python repair card. The repo-ops PR summary also projects the top eligible
Python preview repair card from actionable-gaps.json so local reviewer
packets preserve the same canonical gap, missing discriminator, verify command,
receipt command, and advisory boundary. Editor projection accepts bounded
pytest ... and python -m unittest ... verify commands from Python
GapRecords, can copy a bounded Python agent packet from current actionable
GapRecords, can copy a full repair card with a current validated GapRecord
freshness cue, can copy a fail-fast pytest skeleton, and can open the
suggested test file when the repair route carries a bare test name.
Implementation Mapping
Follow-up implementation belongs to Campaign 27 work item
analysis/python-preview-adapter. The boundary, router, repo config,
and additive output metadata land first under RIPR-SPEC-0026 work items.
This spec PR records the per-language contract; no analyzer behavior
changes in the spec PR.
Metrics
In addition to the cross-language metrics in RIPR-SPEC-0026, the Python adapter contributes:
language_adapter_python_findings_previewlanguage_adapter_python_owner_functionlanguage_adapter_python_owner_methodlanguage_adapter_python_owner_class_methodlanguage_adapter_python_owner_module_functionlanguage_adapter_python_oracle_exact_valuelanguage_adapter_python_oracle_error_pathlanguage_adapter_python_oracle_side_effectlanguage_adapter_python_oracle_smokelanguage_adapter_python_oracle_broad_typelanguage_adapter_python_canonical_gap_identitylanguage_adapter_python_ripr_evidence_modellanguage_adapter_python_missing_discriminator_boundarylanguage_adapter_python_first_pr_start_here_pathlanguage_adapter_python_repair_class_predicate_boundarylanguage_adapter_python_repair_class_return_valuelanguage_adapter_python_repair_class_exception_pathlanguage_adapter_python_repair_class_field_valuelanguage_adapter_python_repair_class_output_or_call_effectlanguage_adapter_python_repair_guidance_suppressed_non_actionablelanguage_adapter_python_repair_ranking_noise_controllanguage_adapter_python_test_placement_verifylanguage_adapter_python_repair_card_v1language_adapter_python_existing_test_strengtheninglanguage_adapter_python_route_decorator_repair_cardlanguage_adapter_python_agent_packet_v1language_adapter_python_swarm_queue_stale_packetslanguage_adapter_python_gap_receipt_from_check_outputlanguage_adapter_python_real_repo_eval_receiptlanguage_adapter_python_repair_routing_quality_metricslanguage_adapter_python_pilot_first_use_pathlanguage_adapter_python_pr_summary_repair_card_projectionlanguage_adapter_python_lsp_agent_packet_actionlanguage_adapter_python_lsp_repair_card_actionlanguage_adapter_python_lsp_pytest_skeleton_actionlanguage_adapter_python_static_limit_stop_reasonslanguage_adapter_python_static_limit_dynamic_dispatchlanguage_adapter_python_static_limit_decorator_indirectionlanguage_adapter_python_static_limit_missing_import_graphlanguage_adapter_python_static_limit_metaprogramminglanguage_adapter_python_static_limit_mocked_modulelanguage_adapter_python_static_limit_opaque_custom_assertion_helperlanguage_adapter_python_static_limit_property_based_testlanguage_adapter_python_static_limit_unresolved_pytest_fixturelanguage_adapter_python_static_limit_unsupported_syntax