@@ -635,25 +635,30 @@ int64_t cbm_gbuf_upsert_node(cbm_gbuf_t *gb, const char *label, const char *name
635635 /* Check if node already exists */
636636 cbm_gbuf_node_t * existing = cbm_ht_get (gb -> node_by_qn , qualified_name );
637637 if (existing ) {
638- /* Update in-place. name/properties are strdup'd BEFORE freeing old ones
639- * (callers may pass existing->name as an argument). label/file_path are
640- * interned: gb_intern returns a stable pool pointer (idempotent even when
641- * label == existing->label), so the old value is replaced, never freed. */
642- char * new_name = heap_strdup (name );
643- char * new_props = properties_json ? heap_strdup (properties_json ) : NULL ;
644- /* Don't let a per-file "Module" def downgrade a structural directory node
638+ /* Don't let a per-file "Module" def touch a structural directory node
645639 * ("Project" root or "Folder"). In a directory-based-module language
646640 * (Go/Java) a file's module_qn equals its directory QN: a root file →
647641 * the project name (== the "Project" node's QN); a file in pkg/ →
648642 * proj.pkg (== the "pkg/" Folder node's QN). Its always-emitted Module
649643 * def collides here; the directory node is the package/module container
650- * and must keep its structural label. (Both the sequential upsert and the
651- * parallel local-gbuf merge route through this function.) */
652- if (!(existing -> label && label && strcmp (label , "Module" ) == 0 &&
653- (strcmp (existing -> label , "Project" ) == 0 ||
654- strcmp (existing -> label , "Folder" ) == 0 ))) {
655- existing -> label = (char * )gb_intern (gb , label );
644+ * and must keep its structural label AND its own name/file_path/range.
645+ * Updating those in place set the shared node's file_path to whichever
646+ * same-package file happened to be processed LAST (worker-order
647+ * dependent) — the nondeterministic file attribution behind #787 — and
648+ * left the Folder node exposed to delete-nodes-by-file on incremental
649+ * reindex of that file. Skip the update entirely. (Both the sequential
650+ * upsert and the parallel local-gbuf merge route through this function.) */
651+ if (existing -> label && label && strcmp (label , "Module" ) == 0 &&
652+ (strcmp (existing -> label , "Project" ) == 0 || strcmp (existing -> label , "Folder" ) == 0 )) {
653+ return existing -> id ;
656654 }
655+ /* Update in-place. name/properties are strdup'd BEFORE freeing old ones
656+ * (callers may pass existing->name as an argument). label/file_path are
657+ * interned: gb_intern returns a stable pool pointer (idempotent even when
658+ * label == existing->label), so the old value is replaced, never freed. */
659+ char * new_name = heap_strdup (name );
660+ char * new_props = properties_json ? heap_strdup (properties_json ) : NULL ;
661+ existing -> label = (char * )gb_intern (gb , label );
657662 free (existing -> name );
658663 existing -> name = new_name ;
659664 existing -> file_path = (char * )gb_intern (gb , file_path );
@@ -1112,15 +1117,30 @@ static void free_remap_entry(const char *key, void *val, void *ud) {
11121117 * label/file_path are re-interned into dst's pool (sn's pointers belong to src). */
11131118static void merge_update_existing (cbm_gbuf_t * dst , cbm_gbuf_node_t * existing ,
11141119 const cbm_gbuf_node_t * sn , CBMHashTable * * remap ) {
1115- existing -> label = (char * )gb_intern (dst , sn -> label );
1116- free (existing -> name );
1117- existing -> name = heap_strdup (sn -> name );
1118- existing -> file_path = (char * )gb_intern (dst , sn -> file_path );
1119- existing -> start_line = sn -> start_line ;
1120- existing -> end_line = sn -> end_line ;
1121- if (sn -> properties_json ) {
1122- free (existing -> properties_json );
1123- existing -> properties_json = heap_strdup (sn -> properties_json );
1120+ /* Same guard as cbm_gbuf_upsert_node: a per-file "Module" def coming from a
1121+ * worker-local gbuf must not touch the structural directory node ("Project"
1122+ * root or "Folder") that shares its QN in a directory-based-module language
1123+ * (Java/Go). pass_structure seeds Folder/Project nodes on the MAIN gbuf
1124+ * before the parallel extract, so every worker's always-emitted Module def
1125+ * for that package collides here; unconditional "src wins" relabelled the
1126+ * directory node to Module and set its file_path to whichever worker merged
1127+ * LAST — the nondeterministic USAGE-source misattribution of #787. Keep the
1128+ * structural node intact; the ID remap below still redirects the worker's
1129+ * edges onto the canonical node. */
1130+ bool module_on_container =
1131+ existing -> label && sn -> label && strcmp (sn -> label , "Module" ) == 0 &&
1132+ (strcmp (existing -> label , "Project" ) == 0 || strcmp (existing -> label , "Folder" ) == 0 );
1133+ if (!module_on_container ) {
1134+ existing -> label = (char * )gb_intern (dst , sn -> label );
1135+ free (existing -> name );
1136+ existing -> name = heap_strdup (sn -> name );
1137+ existing -> file_path = (char * )gb_intern (dst , sn -> file_path );
1138+ existing -> start_line = sn -> start_line ;
1139+ existing -> end_line = sn -> end_line ;
1140+ if (sn -> properties_json ) {
1141+ free (existing -> properties_json );
1142+ existing -> properties_json = heap_strdup (sn -> properties_json );
1143+ }
11241144 }
11251145
11261146 if (sn -> id != existing -> id ) {
0 commit comments