Skip to content

fix(python): link constructor calls to the __init__ they run - #4208

Open
krishhgg wants to merge 1 commit into
Graphify-Labs:v8from
krishhgg:fix/python-constructor-init
Open

krishhgg wants to merge 1 commit into
Graphify-Labs:v8from
krishhgg:fix/python-constructor-init

Conversation

@krishhgg

@krishhgg krishhgg commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Related issue: #4209.

Foo(...) reaches the class node but stops before its initializer. affected, query and path traverse calls rather than class-to-method ownership, so a change to something Foo.__init__ calls can miss the constructor's callers and their tests.

This adds an INFERRED calls edge from the caller to the nearest initializer in a supported class chain, with score 0.85 and context: "constructor". The existing edge to the class stays. For example, build() -> CachedStore() now also reaches the inherited Store.__init__, so affected "open_db()" finds build() and test_build().

The pass requires an extracted class call, an unambiguous source binding, and a plain module-level class chain with at most one base per class. It refuses decorated classes or initializers, metaclasses, custom allocation, ambiguous or conditional bindings, stdlib name collisions, initializer ID collisions, and source patterns that can replace construction behavior. Unsupported cases add nothing. Analysis exceptions also add nothing.

Three private node attributes store class-chain and call-binding facts. Incremental merges remove the derived constructor edges and relink them over the merged graph. AST cache schema advances from 17 to 18. Existing nodes and edges keep all other attributes.

Cache coordination: v8 schema 17 stores pytest-fixture facts, salted Python/Rust/Go identities, TypeScript field metadata and Solidity callable facts. This branch uses schema 18 for constructor-entry facts. It preserves grammar-version namespacing, language-override salts and v8's separate sibling-import rules.

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Tests or CI
  • Refactor
  • Security fix

Verification & Invariants

Invariant: only source-backed, unambiguous constructor calls in the supported static subset gain an initializer edge. Existing graph output stays unchanged apart from the derived edges and stored facts.

Rebased onto v0.9.92, 86c57be8204673c2a1b9849a3292a7dd3ab373d8. The branch retains upstream's JavaScript member-ID helper at the insertion conflict and advances the cache schema. Constructor proof rules are unchanged. Collision tests now assert the separated initializer's exact node and keep unrelated helper bodies unreachable through cold, cached and incremental builds. The subclass/base and aliased-binding safeguards remain.

Fresh full builds of seven repositories preserve every baseline node and edge attribute except the three private facts. They add 703 constructor edges. An independent Python ast derivation confirms all 703. SCIP can judge 586, and all 586 agree on the class, inheritance chain and initializer. The existing SCIP indexes refer to unchanged source snapshots; mappings use unique file, label and location anchors in the fresh graphs, with no missing or ambiguous anchors.

  • Read CONTRIBUTING.md.
  • Reproduced the issue and identified the invariant.
  • Kept the change scoped to constructor entry edges.
  • Added regression and boundary tests.
  • Synchronized this description with the final implementation.
  • Documented unsupported cases.

Limitations:

  • Call sites require an actual .py extension, even when .graphifyrc declares another extension as Python. Remapped files can still supply classes and initializers for .py callers.
  • These are static rules. Another module can still replace a class or initializer through mutation or dynamic behavior the rules do not model.
  • Unsupported syntax gives up correct edges. Multiple inheritance, external bases, nested classes, re-exports and overloaded initializers are excluded.
  • The pass preserves v8's incremental graph behavior. Existing stale base or caller edges can prevent an addition until a full rebuild.
  • Undirected builds can collapse a factory method's class-call edge into the class's method-membership edge. A later unrelated or no-change build_merge drops the inferred constructor edge because its class-call evidence no longer exists. Directed graphs retain both directions.
  • Stored facts add about 0.2 to 3.2 percent to the historical corpus graph files. Clustering can assign different communities when new edges appear.
  • Windows was not tested.

How was this tested?

uv run --offline pytest tests/test_python_constructor_init.py -q
  current v8: 95 failed, 176 passed
  this PR: 271 passed

two sibling-import regression cases with the old shared-name collision
  2 failed; both pass with the constructor-only guard name

uv run --offline pytest tests -q -p no:cacheprovider
  current v8: 8208 passed, 149 skipped
  this PR: 8479 passed, 149 skipped, no failures

uv run --offline ruff check .
  clean
uv run --offline pyright
  678 errors, 4 warnings, identical diagnostics to current v8

uv run --offline graphify update <repo> --no-cluster
  added constructor edges: flask 3, requests 3, httpx 56, rich 506,
    langchain-core 100, yt-dlp 17, fastapi 18
  all existing nodes and edges preserved

graphify affected "open_db()" --depth 4
  v8: initializer only
  this PR: initializer, build(), test_build(), importing test file
  after a caller edit: incremental and clean graphs have equal nodes and edges

The full suites use an isolated SQL grammar through PYTHONPATH. GPT-6-Astra at high reasoning approved candidate b1b98048e76de433f209c80183cbf02e546e5347 and reviewed this description. Both final full suites completed without failures. The strict comparison passes 444 paired stages: 316 retained stages, 16 new upstream-interaction stages, and 112 collision stages. It compares every baseline edge attribute and all node attributes except the three private constructor facts. Native language overrides allow no exclusions. Added edges must be .py constructor calls with INFERRED confidence and score 0.85. Hyperedges, labels, pending labels, membership signatures and orphan-sweep accounting also compare exactly.

The new checks retain TS instance/static field maps and private-member identities, Go case-distinct owners, Rust imported names and external file provenance, pytest-fixture receiver typing, and salted Python class/function/method IDs. Direct and inherited collision cases check exact initializer ownership and helper unreachability through extraction, caches and incremental rebuilds. True duplicate or replaced initializers still refuse inference.

Schema 17-to-18 retirement, grammar namespaces, parser salts, semantic-loss guards, opaque identities, provenance checks and standard-library namespace separation pass. The reviewer ran 115 focused upstream tests. Eight Solidity tests skipped because that optional grammar was unavailable; an isolated offline install found no cached package. The report does not claim those cases were exercised. No virtual environment or shared dependency target changed.

The retained label matrix covers watch and cluster-only with and without signatures, strict-majority retention and exact-half rejection. It fixes partitions and fallback names to test saved labels; new edges can still change natural communities and hub names. Windows was not tested.

Graphify-specific checklist

  • Regenerated skill artifacts. No fragments changed.
  • Kept AST extraction deterministic.
  • Reviewed path handling and persistent state.
  • Included no API keys or local graph artifacts.
  • Disclosed AI contributions in commit metadata.

@krishhgg
krishhgg requested a review from safishamsi as a code owner October 7, 2026 23:12
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

Thanks for the pull request, @krishhgg. A maintainer will review it soon.

Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions.

A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic.

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Formal verification. PR-changed functions: 0/7 verified (0 proven, 0 may-equivalent, 0 distinguished) · 7 not verified (3 vacuous, 4 unsupported).

Not verified on this run: build\_merge (vacuous: never exercised), merge\_raw\_extraction (unsupported), \_prune\_graph\_json\_sources (unsupported), dispatch\_command (vacuous: never exercised), extract (vacuous: never exercised), \_extract\_generic (unsupported), \_rebuild\_code (unsupported).


Graphify review — findings

Adds Python constructor linking: when a Foo(...) call provably resolves to a project class, link_python_constructors walks the class's verified single-base chain and emits an INFERRED calls edge (context "constructor") to the nearest __init__, so whatever __init__ calls becomes reachable from the call site. If the chain breaks, a class defines __new__, or no __init__ exists, it adds nothing. build_merge and merge_raw_extraction strip existing constructor edges and relink them over the whole merged graph, _prune_graph_json_sources does the same when given a root, and the bumped _AST_CACHE_SCHEMA forces re-extraction so cached files pick up the new constructor facts.

Review partial — this diff was larger than one review pass covers, so later files were not reviewed; some findings may be missing.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 4549 functions depend on the 1088 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 801 callers, 53 callees
  • new: _rebuild_code() — 164 callers, 57 callees
  • new: build_from_json() — 234 callers, 21 callees
  • new: detect() — 121 callers, 16 callees
  • new: build_merge() — 87 callers, 16 callees
  • new: _extract_generic() — 18 callers, 37 callees
  • new: save_semantic_cache() — 65 callers, 9 callees
  • new: to_obsidian() — 41 callers, 14 callees
  • …and 116 more — each is listed as a finding

Verification — 4549 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 4325 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

262 of 345 test file(s) selected (76%) via static blast radius.

  • tests/test_affected_cli.py — impact
  • tests/test_agents_platform.py — impact
  • tests/test_analyze.py — impact
  • tests/test_astro_extraction.py — impact
  • tests/test_astro_import_ids.py — impact
  • tests/test_atomic_canvas_export.py — impact
  • tests/test_atomic_writes.py — impact
  • tests/test_benchmark.py — impact
  • tests/test_benchmark_raw_graph.py — impact
  • tests/test_blade_extractor.py — impact
  • tests/test_build.py — impact
  • tests/test_build_located_semantic_identity.py — impact
  • tests/test_build_merge_dedup_scope.py — impact
  • tests/test_build_merge_hyperedges_and_prune.py — impact
  • tests/test_build_merge_shrink_guard.py — impact
  • tests/test_builtin_global_type_refs.py — impact
  • tests/test_cache.py — impact
  • tests/test_cache_stale_import_target.py — impact
  • tests/test_carried_hyperedge_remap.py — impact
  • tests/test_case_sensitive_resolution.py — impact
  • tests/test_charmap_encoding.py — impact
  • tests/test_chunking.py — impact
  • tests/test_cjs_module_extension.py — impact
  • tests/test_claude_cli_backend.py — impact
  • tests/test_cli_export.py — impact
  • tests/test_cluster.py — impact
  • tests/test_cobol_extractor.py — impact
  • tests/test_codebuddy.py — impact
  • tests/test_community_labels_skill.py — impact
  • tests/test_confidence.py — impact
  • tests/test_corrupt_graph_json.py — impact
  • tests/test_cpp_method_declarations.py — impact
  • tests/test_cpp_nested_and_cli.py — impact
  • tests/test_cpp_objc_cross_file_calls.py — impact
  • tests/test_cross_extension_reexport_self_cycle.py — impact
  • tests/test_cross_language_call_resolution.py — impact
  • tests/test_cross_repo_external_call_guards.py — impact
  • tests/test_cross_repo_member_calls.py — impact
  • tests/test_csharp_call_site_generic_args.py — impact
  • tests/test_csharp_enum_members.py — impact
  • tests/test_csharp_field_generic_args.py — impact
  • tests/test_csharp_generic_callsites.py — impact
  • tests/test_csharp_interface_dispatch.py — impact
  • tests/test_csharp_member_calls.py — impact
  • tests/test_csharp_member_nodes.py — impact
  • tests/test_csharp_object_creation.py — impact
  • tests/test_csharp_partial_classes.py — impact
  • tests/test_csharp_tuple_type_refs.py — impact
  • tests/test_csharp_type_resolution.py — impact
  • tests/test_dedup.py — impact
  • … and 212 more

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

Docs that may be stale (advisory)

…and 10 more.

Formal verification

Could not verify: Could not verify build\_merge.

The verifier did not have enough to check build\_merge, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: all 224 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify merge\_raw\_extraction.

The verifier did not have enough to check merge\_raw\_extraction, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: parameter `prune_sources` is annotated `'list[str] | None'` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_prune\_graph\_json\_sources.

The verifier did not have enough to check \_prune\_graph\_json\_sources, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: the input domain has 83 values but only 11 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)

Could not verify: Could not verify dispatch\_command.

The verifier did not have enough to check dispatch\_command, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: all 40 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

Could not verify: Could not verify extract.

The verifier did not have enough to check extract, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: all 90 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_extract\_generic.

The verifier did not have enough to check \_extract\_generic, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: parameter `config` is annotated `LanguageConfig` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_rebuild\_code.

The verifier did not have enough to check \_rebuild\_code, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: nondeterministic output (both versions disagreed with themselves on the witness) — not a behaviour change; a function whose output is not a function of its inputs (random / time / uuid / os.urandom, or state carried across calls) cannot be soundly distinguished by execution, so this tier abstains for the whole function

· 4 grounded finding(s) anchored inline below; 120 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/extract.py
)


def link_python_constructors(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — link_python_constructors()

fans out to 7 callees (efferent coupling); 6 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

return False


def _python_poisons_setattr(root_node, source: bytes) -> bool:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — _python_poisons_setattr()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

return ("other", node.start_point[0] + 1, name, "", _python_scope_of(node))


def _python_name_uses(root_node, source: bytes) -> _PythonNameUses:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — _python_name_uses()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

return None
return kind, line, origin, module

def walk_calls(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — walk_calls()

fans out to 29 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@krishhgg
krishhgg force-pushed the fix/python-constructor-init branch 7 times, most recently from eb52397 to 48c7275 Compare October 14, 2026 22:31
`Foo(...)` was linked only to the class node, so nothing `__init__` calls
was reachable from the call site in affected, query or path.

Add an INFERRED `calls` edge (context "constructor") from the caller to the
nearest `__init__` up the class chain, and keep the edge to the class. The
edge is added only when static rules prove that `__init__` runs: the called
name and every base are bound once in their files, by a class statement or
an import that resolves to the defining file (never a stdlib name); each
class in the chain is plain; no module involved patches classes or uses
dynamic namespaces; no other definition shares the `__init__` node id.

graph.json stores each file's facts, and every incremental merge links the
constructor edges again over the merged graph, leaving all other nodes and
edges as v8 builds them. AST cache schema 17 -> 18.
The constructor-only stdlib guard preserves the existing sibling-import
resolver namespace and rules.

Keep v0.9.92's cache facts and salted class/member identities. Update the
collision tests to check the separated initializer exactly, retain runtime
checks, and keep unrelated helper bodies unreachable through cold, cached
and incremental builds. Constructor proof rules are unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>
@krishhgg
krishhgg force-pushed the fix/python-constructor-init branch from 48c7275 to b1b9804 Compare October 19, 2026 23:26

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant