From a201861c71c04da1a9101548d3a387acd91d118d Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Mon, 20 Apr 2026 11:07:58 +0100 Subject: [PATCH] feat(python-scope): collapse free-call edges per (caller, target) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Free calls (no explicit receiver) now emit a single CALLS edge per (caller, target) pair regardless of how many call sites the caller contains. Mirrors the legacy DAG's per-pair dedup contract — what the `default-params`, `variadic`, and `overload` fixtures expect. Member calls keep position-based dedup so distinct resolved targets (e.g. UserService.find_user vs AdminService.find_user from the same caller) still produce distinct edges. Implementation: bypass `tryEmitEdge` (which dedupes positionally) and hand-roll the relationship with a position-independent rel.id (`rel:CALLS:->`). Site handling is now unconditional — even when the dedup-collapse skips the actual emit, we mark the site handled so the shared `emit-references` doesn't fight us with its fallback. Verification: - Flag-off: 191/191 (identical baseline). - Flag-on: 10 fail / 181 pass (was 12/179; +2 — both `default parameter arity` tests now pass). - tsc --noEmit clean. --- .../src/core/ingestion/python-scope-emit.ts | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/gitnexus/src/core/ingestion/python-scope-emit.ts b/gitnexus/src/core/ingestion/python-scope-emit.ts index 889e852de..c0fefdd9e 100644 --- a/gitnexus/src/core/ingestion/python-scope-emit.ts +++ b/gitnexus/src/core/ingestion/python-scope-emit.ts @@ -617,19 +617,31 @@ function emitFreeCallFallback( const fnDef = findCallableBindingInScope(site.inScope, site.name, scopes); if (fnDef === undefined) continue; - const ok = tryEmitEdge( - graph, - scopes, - nodeLookup, - site, - fnDef, - 'python-scope: free-call-import', - seen, - ); - if (ok) { - emitted++; - handledSites.add(`${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`); - } + // Free calls collapse to one CALLS edge per (caller, target) + // pair. Multiple call sites in the same caller body should not + // emit multiple edges (legacy DAG semantics — what + // `default-params` / `variadic` / `overload` tests expect). + // Member calls keep positional dedup elsewhere. + const callerGraphId = resolveCallerGraphId(site.inScope, scopes, nodeLookup); + if (callerGraphId === undefined) continue; + const tgtGraphId = resolveDefGraphId(fnDef.filePath, fnDef, nodeLookup); + if (tgtGraphId === undefined) continue; + // Always mark the site as handled — even when the dedup-collapse + // means we don't add a new edge — so `emit-references` skips its + // potentially-wrong fallback for the same site. + handledSites.add(`${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`); + const relId = `rel:CALLS:${callerGraphId}->${tgtGraphId}`; + if (seen.has(relId)) continue; + seen.add(relId); + graph.addRelationship({ + id: relId, + sourceId: callerGraphId, + targetId: tgtGraphId, + type: 'CALLS', + confidence: 0.85, + reason: 'python-scope: free-call-import', + }); + emitted++; } } return emitted;