-
-
Notifications
You must be signed in to change notification settings - Fork 11.2k
Park TS/JS member calls whose receiver type lives in another repo (#3152) - #3387
Park TS/JS member calls whose receiver type lives in another repo (#3152) #3387xiongjianxu wants to merge 3 commits into
Conversation
`_DISPATCH` routes `.mjs`/`.cjs` to `extract_js` and the cross-file call gate already lists them, but `typescript_member_calls` claimed only six suffixes, so a repo written entirely in `.mjs` was the one shape where the resolver never ran and `const s = new Svc(); s.doThing()` produced no edge. Both the suffix set and the resolver now read one `_JS_TS_SUFFIXES` constant.
...po (Graphify-Labs#3152) `_resolve_typescript_member_calls` binds `this.repo.save()` only when the receiver's type is declared in the same build. With the type in another repository the resolver held the receiver type and dropped the call, so `graph.json` — the only artifact `merge-graphs` and `global add` read — recorded nothing and no merge-time pass could recover it. Those calls are now parked on the caller as `metadata.unresolved_calls`, and `link_cross_repo_member_calls` finishes them after the merge. One `_LANG_SUFFIXES` key covers all eight TS/JS suffixes: a TS class legitimately answers a JS call site, and two keys would drop every TS<->JS cross-repo call. Only a table-typed receiver parks. The uppercase-spelling arm also matches namespace aliases and default imports (`React.createElement()`), and `node_modules` is never scanned, so parking those would send npm names to the merge to be matched by bare name. The park sits strictly upstream of the Graphify-Labs#2553 origin gate, which only ever vets a name that did match locally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Extends cross-repo member-call parking to TypeScript/JavaScript by giving both a single typescript suffix key (.ts/.tsx/.mts/.cts/.js/.jsx/.mjs/.cjs) so a TS class can answer a JS call site and pure-ESM repos still run the resolver. When a receiver's annotated type is declared nowhere in the corpus, _resolve_typescript_member_calls parks the call on the caller for merge-graphs to finish — but only for a bare, unqualified name from a JS/TS source, so namespace/default-import-style uppercase receivers and types the local origin gate already rejected are left unparked.
Worth a look
- Parked call re-emitted when a local same-name type exists but the gate rejected it —
graphify/extract.py:3508· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1889 functions depend on the 306 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 544 callers, 43 callees - new:
_rebuild_code()— 115 callers, 51 callees - new:
extract_js()— 85 callers, 4 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
dispatch_command()— 2 callers, 124 callees - new:
_get_extractor()— 26 callers, 6 callees - new:
link_cross_repo_member_calls()— 20 callers, 7 callees - new:
run_pipeline()— 8 callers, 13 callees - ...and 35 more — each is listed as a finding
Verification — 1889 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: 1724 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify \_resolve\_typescript\_member\_calls.
The verifier did not have enough to check \_resolve\_typescript\_member\_calls, 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: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
· 2 grounded finding(s) anchored inline below; 41 more finding(s) on lines outside this diff (see the check run).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test_a_typescript_call_binds_across_the_whole_js_family()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test_the_definition_answers_before_a_same_named_declaration()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
A corpus-wide type index let a same-named class in another language stand in for the receiver's type, which hid from the parking branch that nothing in the JS/TS family declares it. SFC script blocks are in the family, so a class exported from a .vue file still answers — here and at merge time.
xiongjianxu
commented
Sep 7, 2026
Self-audit pass over the bot review. One fix pushed; the review's finding does not reproduce.
Fixed — the declaration index was corpus-wide. _is_type_like_definition is language-blind, so a same-named class in another language (a PHP class Greeter, say) made type_defs non-empty for a TS receiver typed Greeter. Resolution then fell to the origin gate, which correctly refused it — but the parking branch had already been skipped, so the call was dropped instead of handed to the merge. The index is now gated with _lang_family(...) == "jsts", which is wider than the suffix tuple on purpose: .vue/.svelte/.astro script blocks are extracted with the TS/JS grammars, so a class exported from an SFC is in the same module graph and must keep answering. test_a_class_declared_in_an_sfc_script_block_still_answers pins that half, test_a_class_from_another_language_neither_answers_nor_blocks_parking the other. _LANG_SUFFIXES["typescript"] in cross_repo_calls gains the same three suffixes so the merge side agrees.
Does not reproduce — a parked call being re-emitted after the origin gate rejects it. Parking sits under if not type_defs: and returns immediately, so a name that matched anything locally never reaches it; the origin gate runs strictly later, on the branch where a local definition was found. test_a_type_the_origin_gate_rejected_is_not_parked pins exactly that case and passes. The reasoning behind it is deliberate: if a local declaration exists and the gate judged the evidence too weak, parking would ask the merge to accept a remote match on weaker evidence still.
Verification: ruff check graphify tests clean; full suite 5321 passed / 93 skipped, with only the four pre-existing tests/test_ollama_retry_cap.py env failures that v8 also shows here.
There was a problem hiding this comment.
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. No changes could be formally verified in this run.
Graphify review — findings
Extends TS/JS receiver-typed member-call resolution to park calls whose receiver type is declared nowhere in the corpus onto the caller node, so the cross-repo pass can finish them after a merge — but only for table-typed (annotated, non-qualified) receivers in JS/TS files, leaving uppercase namespace-alias/default-import shapes and gate-rejected local names unparked. Scopes the local type-definition index to the JS/TS family so a same-named class in another language neither answers a call nor makes the receiver look locally declared. Registers the resolver and its cross-repo suffixes across the full ESM/CJS/.vue/.svelte/.astro family and treats TS and JS as one key so a TS class answers a JS call site.
No blocking issues surfaced. 5 lower-confidence candidates did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1891 functions depend on the 308 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 544 callers, 43 callees - new:
_rebuild_code()— 115 callers, 51 callees - new:
extract_js()— 85 callers, 4 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
dispatch_command()— 2 callers, 124 callees - new:
_get_extractor()— 26 callers, 6 callees - new:
link_cross_repo_member_calls()— 20 callers, 7 callees - new:
run_pipeline()— 8 callers, 13 callees - ...and 35 more — each is listed as a finding
Verification — 1891 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: 1726 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify \_resolve\_typescript\_member\_calls.
The verifier did not have enough to check \_resolve\_typescript\_member\_calls, 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: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
· 2 grounded finding(s) anchored inline below; 41 more finding(s) on lines outside this diff (see the check run).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test_a_typescript_call_binds_across_the_whole_js_family()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test_the_definition_answers_before_a_same_named_declaration()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
Closes #3386. Continues #3152 (#3385 did Objective-C).
What changes
_resolve_typescript_member_callsnow parks a member call whose receiver type isdeclared nowhere in this build, instead of dropping it, and
link_cross_repo_member_callsfinishes it once the graphs are composed.Two commits, separable:
.mjs/.cjsactivation. The resolver claimed.ts .tsx .mts .cts .js .jsx, so apure-ESM corpus never ran type-aware member-call resolution at all. The suffix set is
now one constant the resolver both activates on and parks by. Single-repo bug, has its
own test that fails without the fix.
metadata.unresolved_callson the caller (names only — no node ids,which the import edges silently dropped on Next.js codebase — short IDs vs full-path node IDs (regression of #256) #1529 remap and repo prefixing both rewrite), plus the
"typescript"entryin
_LANG_SUFFIXES.The two precision calls, and why
Only table-typed receivers park. The resolver types a receiver either from the
per-file table (constructor parameter properties, local
newbindings, baretype_identifierannotations) or from spelling — an initial capital is taken to name thetype. That second arm is satisfied by
import * as React, by default imports and byplain const objects, and since
node_modulesis not scanned, most receivers that reachthe zero-declarations point through it are npm names. Parking them would send bare names
to the merge with nothing behind them, so the park is gated on
not type_qualified.test_a_namespace_style_receiver_is_not_parkedpins it.One
_LANG_SUFFIXESkey over all eight suffixes. A TS class legitimately answers a JScall site, so splitting TS from JS would drop every TS↔JS cross-repo call.
test_a_typescript_call_binds_across_the_whole_js_familyruns the four representativesuffixes.
The park is gated on a JS/TS suffix rather than a
langtag because TSraw_callscarryno tag and this resolver iterates every language's — the same shape the Swift resolver
uses.
Interaction with the origin gate (#2553)
The gate runs after the single-definition check, so it only ever vets a name that did
match locally; the park sits at the zero-declarations point, strictly upstream and
disjoint.
test_a_type_the_origin_gate_rejected_is_not_parkedbuilds the gate's own case(a local
class Repoplusimport type { Repo } from 'external-pkg') and asserts nothingis parked, so a gate rejection cannot be laundered through the merge.
Known recall cost
interface_declaration,enum_declarationandtype_alias_declarationall carry_callable_class, so atype Greeter = ...alias in a third repo makes a genuine classmatch ambiguous and the merge emits nothing. That fails closed, which is the bar the
existing resolvers set, and narrowing it belongs with whatever decides that a type alias
is not a call target.
Verification
tests/test_cross_repo_member_calls.py+tests/test_ts_receiver_member_calls.py: 38 passed.tests/test_ollama_retry_cap.pydeselected —its 4 failures reproduce unchanged on
v8atc9f9901.ruff check graphify tests: clean. Added lines are within 100 code points.