@@ -150,6 +150,114 @@ static const char *pxc_join_pipe(CBMArena *arena, const char *const *items) {
150150 return buf ;
151151}
152152
153+ /* ── Cross-file base-class QN resolution ──────────────────────────
154+ *
155+ * CBMDefinition.base_classes carries the SOURCE SPELLING of each base
156+ * ("Base", "django.db.Model", "React.Component"): extraction strips
157+ * keywords and generic arguments, but it cannot know WHERE the name is
158+ * declared. The Python and TS cross-file registrars, however, consume
159+ * CBMLSPDef.embedded_types as fully-qualified names — py_lookup_attribute
160+ * and ts_lookup_member feed each entry straight into
161+ * cbm_registry_lookup_type. An unqualified spelling therefore matched
162+ * nothing declared in ANOTHER file: `class Child(Base)` in child.py never
163+ * saw Base in base.py, so a call to an inherited method through a typed
164+ * receiver had no member to find and fell through to the weak textual
165+ * cascade (where the #592/#606 guard correctly kills it).
166+ *
167+ * Resolve each base name ONCE per definition here, from exactly the
168+ * inputs pass_semantic uses to draw its INHERITS edge: the project
169+ * registry, the declaring module, and the file's import map. Two
170+ * properties follow. The LSP's inheritance view is the same relation the
171+ * graph records, so the two cannot diverge. And the binding is
172+ * import- or same-module-backed, not a short-name guess, so the CALLS
173+ * edge it enables is a supported fact that the weak-member guard keeps.
174+ *
175+ * Cost: O(defs) hash lookups + ONE import map per file. No per-call-site
176+ * hierarchy walk, no registry scan, no per-file registry rebuild.
177+ */
178+ static bool pxc_lang_resolves_base_qns (CBMLanguage lang ) {
179+ switch (lang ) {
180+ case CBM_LANG_PYTHON :
181+ case CBM_LANG_JAVASCRIPT :
182+ case CBM_LANG_TYPESCRIPT :
183+ case CBM_LANG_TSX :
184+ return true;
185+ default :
186+ /* Go / JVM / C# / C++ / Rust registrars qualify their own embedded
187+ * types already (struct embedding, parent_class chains, impl-trait
188+ * provenance). Re-resolving here would fight those paths, so they
189+ * keep the raw join. */
190+ return false;
191+ }
192+ }
193+
194+ /* True for the registry strategies that are pure short-name guesses.
195+ * EXPLICIT drop-list, mirroring cbm_tsjs_suppress_weak_method_match: a
196+ * base class bound by "some project type happens to share this name" is
197+ * exactly the fabricated relation #606 removed, and inheritance
198+ * multiplies it — every inherited member of the wrong base would become
199+ * a callable target. Import-, module- and suffix-aware strategies are
200+ * kept; anything unresolved simply retains its source spelling and the
201+ * behaviour that predates this resolution. */
202+ static bool pxc_base_strategy_is_weak (const char * strategy ) {
203+ if (!strategy || !strategy [0 ]) {
204+ return true;
205+ }
206+ return strcmp (strategy , "suffix_match" ) == 0 || strcmp (strategy , "unique_name" ) == 0 ||
207+ strcmp (strategy , "field_type_hint" ) == 0 || strcmp (strategy , "fuzzy" ) == 0 ;
208+ }
209+
210+ /* Resolve one base-class spelling to a project QN. Mirrors
211+ * pass_semantic.c::resolve_as_class — same registry, same type-like veto —
212+ * then additionally rejects weak short-name strategies (see above).
213+ * Returns NULL when the base is not a confidently-known project type;
214+ * stdlib and third-party bases land here and keep their raw spelling. */
215+ static const char * pxc_resolve_base_qn (const cbm_registry_t * reg , const char * raw ,
216+ const char * module_qn , const char * * imp_keys ,
217+ const char * * imp_vals , int imp_count ) {
218+ if (!reg || !raw || !raw [0 ]) {
219+ return NULL ;
220+ }
221+ cbm_resolution_t res = cbm_registry_resolve (reg , raw , module_qn , imp_keys , imp_vals , imp_count );
222+ if (!res .qualified_name || !res .qualified_name [0 ]) {
223+ return NULL ;
224+ }
225+ if (pxc_base_strategy_is_weak (res .strategy )) {
226+ return NULL ;
227+ }
228+ if (!cbm_label_is_type_like (cbm_registry_label_of (reg , res .qualified_name ))) {
229+ return NULL ;
230+ }
231+ return res .qualified_name ;
232+ }
233+
234+ /* pxc_join_pipe over base_classes, substituting each resolved QN for its
235+ * source spelling. Unresolved entries pass through verbatim so a base the
236+ * registry does not know keeps working exactly as before. */
237+ static const char * pxc_join_base_qns (CBMArena * arena , const char * const * bases ,
238+ const cbm_registry_t * reg , const char * module_qn ,
239+ const char * * imp_keys , const char * * imp_vals , int imp_count ) {
240+ if (!bases || !bases [0 ]) {
241+ return NULL ;
242+ }
243+ int count = 0 ;
244+ while (bases [count ]) {
245+ count ++ ;
246+ }
247+ const char * * resolved =
248+ (const char * * )cbm_arena_alloc (arena , (size_t )(count + 1 ) * sizeof (const char * ));
249+ if (!resolved ) {
250+ return pxc_join_pipe (arena , bases );
251+ }
252+ for (int i = 0 ; i < count ; i ++ ) {
253+ const char * qn =
254+ pxc_resolve_base_qn (reg , bases [i ], module_qn , imp_keys , imp_vals , imp_count );
255+ resolved [i ] = qn ? qn : bases [i ];
256+ }
257+ resolved [count ] = NULL ;
258+ return pxc_join_pipe (arena , resolved );
259+ }
260+
153261static bool pxc_is_jvm_lang (CBMLanguage lang );
154262
155263static const char * pxc_last_component (const char * qn ) {
@@ -262,7 +370,9 @@ static const char *pxc_qn_leaf(const char *name) {
262370 * to skip (unsupported label or missing required field). dst gets borrowed
263371 * pointers into src and into `arena` for synthesised composites. */
264372static int pxc_build_lsp_def (CBMArena * arena , const CBMDefinition * src , const char * module_qn ,
265- const char * namespace_name , CBMLanguage lang , CBMLSPDef * dst ) {
373+ const char * namespace_name , CBMLanguage lang , CBMLSPDef * dst ,
374+ const cbm_registry_t * reg , const char * * imp_keys ,
375+ const char * * imp_vals , int imp_count ) {
266376 const char * label = pxc_map_label (src -> label );
267377 if (!label || !src -> qualified_name || !src -> name )
268378 return -1 ;
@@ -283,7 +393,13 @@ static int pxc_build_lsp_def(CBMArena *arena, const CBMDefinition *src, const ch
283393 * for multi-return languages (Go); single-return languages just see one
284394 * piece, which is what's already stored. */
285395 dst -> return_types = src -> return_type ;
286- dst -> embedded_types = pxc_join_pipe (arena , src -> base_classes );
396+ /* Languages whose cross registrars read embedded_types as QNs get their
397+ * bases resolved against the project registry; everyone else keeps the
398+ * raw source spelling their own registrar already knows how to handle. */
399+ dst -> embedded_types = (reg && pxc_lang_resolves_base_qns (lang ))
400+ ? pxc_join_base_qns (arena , src -> base_classes , reg , module_qn ,
401+ imp_keys , imp_vals , imp_count )
402+ : pxc_join_pipe (arena , src -> base_classes );
287403 dst -> signature_param_types = src -> signature_param_types ;
288404 dst -> signature_param_count = src -> signature_param_count ;
289405 dst -> lang = lang ;
@@ -323,9 +439,10 @@ static int pxc_build_rust_impl_relation(CBMArena *arena, const CBMImplTrait *imp
323439/* Collect a project-wide CBMLSPDef[] from all cached results. Returns a
324440 * malloc'd array (caller frees) of length *out_count. String fields are
325441 * borrowed from cache[i]->arena and from def_modules[i] (also borrowed). */
326- CBMLSPDef * cbm_pxc_collect_all_defs (CBMFileResult * * cache , const cbm_file_info_t * files ,
327- int file_count , const char * project_name , char * * def_modules ,
328- int * out_count , int * out_def_starts ) {
442+ CBMLSPDef * cbm_pxc_collect_all_defs (const cbm_pipeline_ctx_t * ctx , CBMFileResult * * cache ,
443+ const cbm_file_info_t * files , int file_count ,
444+ const char * project_name , char * * def_modules , int * out_count ,
445+ int * out_def_starts ) {
329446 int total = 0 ;
330447 for (int i = 0 ; i < file_count ; i ++ ) {
331448 if (cache [i ]) {
@@ -369,12 +486,30 @@ CBMLSPDef *cbm_pxc_collect_all_defs(CBMFileResult **cache, const cbm_file_info_t
369486 cache [fi ]-> namespace_name = namespace_name ;
370487 }
371488 }
489+ /* One import map per FILE (not per def, and not per base name): the
490+ * cross-file base-class resolution below needs the same local-name →
491+ * import-QN view pass_semantic uses. Built only for the languages
492+ * that consume resolved base QNs, and only when a caller supplied the
493+ * pipeline context (the surface-probe path passes NULL and keeps the
494+ * raw spelling). */
495+ const cbm_registry_t * base_reg = NULL ;
496+ const char * * imp_keys = NULL ;
497+ const char * * imp_vals = NULL ;
498+ int imp_count = 0 ;
499+ if (ctx && ctx -> registry && pxc_lang_resolves_base_qns (files [fi ].language )) {
500+ base_reg = ctx -> registry ;
501+ cbm_pxc_build_import_map (ctx -> gbuf , project_name , files [fi ].rel_path ,
502+ files [fi ].language , cache [fi ], & imp_keys , & imp_vals ,
503+ & imp_count );
504+ }
372505 for (int di = 0 ; di < cache [fi ]-> defs .count ; di ++ ) {
373506 if (pxc_build_lsp_def (& cache [fi ]-> arena , & cache [fi ]-> defs .items [di ], def_modules [fi ],
374- namespace_name , files [fi ].language , & defs [idx ]) == 0 ) {
507+ namespace_name , files [fi ].language , & defs [idx ], base_reg ,
508+ imp_keys , imp_vals , imp_count ) == 0 ) {
375509 idx ++ ;
376510 }
377511 }
512+ cbm_pxc_free_import_map (imp_keys , imp_vals , imp_count ); /* NULL-safe */
378513 if (files [fi ].language == CBM_LANG_RUST ) {
379514 for (int ii = 0 ; ii < cache [fi ]-> impl_traits .count ; ii ++ ) {
380515 if (pxc_build_rust_impl_relation (
@@ -1284,7 +1419,7 @@ int cbm_pipeline_pass_lsp_cross(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *
12841419
12851420 int def_count = 0 ;
12861421 int * def_starts = (int * )calloc ((size_t )file_count + 1 , sizeof (int ));
1287- CBMLSPDef * all_defs = cbm_pxc_collect_all_defs (cache , files , file_count , ctx -> project_name ,
1422+ CBMLSPDef * all_defs = cbm_pxc_collect_all_defs (ctx , cache , files , file_count , ctx -> project_name ,
12881423 def_modules , & def_count , def_starts );
12891424 /* Same seam as the parallel driver: serialize per-file surfaces while the
12901425 * result cache is alive. Failure only degrades to a full rebuild on the
0 commit comments