diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1bbf2b498..738ca4cd1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -574,6 +574,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- `codegraph sync` and the file watcher now update the files that import through a module that re-exports what it imports, like a package's `__init__.py` or an `index.ts` barrel, when that module is deleted or added, and in Python also the files that reach it through another package re-exporting it. Before, deleting `shop/__init__.py` left every `from shop import Order` linked to the class it re-exported, and deleting `components/index.ts` left every `import { Button } from './components'` linked, until each importing file changed or the project was re-indexed. When such a module came back, the imports stayed linked the way they were while it was missing, and in a large Python project an import of a module whose name is as common as `models` or `test`, like `from django.test import TestCase`, stayed unlinked. Python, JavaScript, TypeScript, Vue, Svelte and Astro.
+- In Kotlin, a call on a typed receiver now links to an extension function declared in a package, like `value.shout()` or the infix `a join b` with `value: Foo` and `fun Foo.shout()` under `package demo`, including one declared in another file of the package. Before, it linked only when the file had no `package` line, so the usual Kotlin project got no edge. On JetBrains Exposed, 1,020 such calls now link (`Table.selectAll()` to `FieldSet.selectAll`, `transaction.throwUnsupportedException(…)`), and none that linked before is lost. A member of the receiver's class still wins over an extension of the same name.
+
#### MCP / indexing
- Full indexes of large projects no longer sometimes run two to three times slower than usual. A timing clash between two background database maintenance steps could switch reference resolution from several threads to one partway through.
diff --git a/README.md b/README.md
index 6da976ad3..224304b82 100644
--- a/README.md
+++ b/README.md
@@ -148,6 +148,7 @@ Call resolution the fork adds:
- Kotlin infix calls keep their call edge when a comment sits inside the expression, and a parenthesized receiver (`(b) put 1`) is typed like a bare one.
- Kotlin receiver inference follows bounded chains of declared returns and verified receiver-preserving methods. Properties initialized by typed factory calls keep compatible imported extensions. Explicit casts, single-type `when` branches, filtered collection elements and bound generic factory arguments also supply receiver types.
- Kotlin chains on instance and extension receivers use the declared return type, including nested and multiline calls; upstream covers class and companion-factory chains. An imported return-type guess for an unknown receiver keeps confidence at most 0.7, and so does a Rust chain match without a proved receiver type.
+- Kotlin calls on a typed receiver link to extension functions declared in a package, infix ones included (`value.shout()`, `a join b`); on JetBrains Exposed that adds 1,020 call links and removes none.
- Kotlin multi-dollar strings keep their interpolation threshold.
- C++ typedef and using aliases follow their declaration scope, including nested owners and inherited aliases. Pointer, array and function members are not supertypes. Visible class-scoped aliases and declared complex receivers keep their method owners; unsubstituted template parameters and ambiguous owners stay unresolved.
- C++ `->` calls on a `std::unique_ptr`, `std::shared_ptr` or `std::optional` reach the type it holds, looked up through the caller's enclosing classes and their bases, its namespaces, `using` directives and aliases. A held type the project does not declare leaves the call unresolved, as a plain pointer to one does; upstream guesses a callee from the receiver's name there. A partial class template specialization is indexed as an anonymous class, so a call to a member only it declares stays unresolved.
diff --git a/__tests__/kotlin-extension-call-typed-receiver.test.ts b/__tests__/kotlin-extension-call-typed-receiver.test.ts
new file mode 100644
index 000000000..2632e6ddb
--- /dev/null
+++ b/__tests__/kotlin-extension-call-typed-receiver.test.ts
@@ -0,0 +1,51 @@
+import { expect, it } from 'vitest';
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+import { CodeGraph } from '../src';
+
+// An extension function's qualified name is `Foo::shout` whatever its package, while the class
+// it extends is `pkg::Foo`: the call on a typed receiver has to find it by the receiver's type.
+it('links a call on a typed receiver to an extension function declared in a package', async () => {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-kotlin-ext-pkg-'));
+ let cg: CodeGraph | undefined;
+ try {
+ fs.writeFileSync(path.join(dir, 'Foo.kt'), `package demo
+
+class Foo
+class Bar
+class Baz { fun shout(): String = "member" }
+`);
+ fs.writeFileSync(path.join(dir, 'Ext.kt'), `package demo
+
+fun Foo.shout(): String = "x"
+infix fun Foo.join(other: Foo): Foo = this
+fun Baz.shout(loud: Boolean): String = "extension"
+fun Bar.whisper(): String = "y"
+`);
+ fs.writeFileSync(path.join(dir, 'Use.kt'), `package demo
+
+fun byParameter(value: Foo): String = value.shout()
+fun byConstructor(): String {
+ val f = Foo()
+ return f.shout()
+}
+fun byInfix(a: Foo, b: Foo): Foo = a join b
+fun memberWins(value: Baz): String = value.shout()
+fun otherType(value: Bar): String = value.shout()
+fun unrelatedExtension(value: Foo): String = value.whisper()
+`);
+ cg = await CodeGraph.init(dir, { index: true });
+ const targets = (name: string) => cg!.getOutgoingEdges(cg!.getNodesInFile('Use.kt').find((n) => n.name === name)!.id)
+ .filter((e) => e.kind === 'calls').map((e) => cg!.getNode(e.target)!.qualifiedName + '@' + cg!.getNode(e.target)!.filePath);
+ expect(targets('byParameter')).toEqual(['Foo::shout@Ext.kt']);
+ expect(targets('byConstructor')).toEqual(['Foo::shout@Ext.kt']);
+ expect(targets('byInfix')).toEqual(['Foo::join@Ext.kt']);
+ expect(targets('memberWins')).toEqual(['demo::Baz::shout@Foo.kt']);
+ expect(targets('otherType')).toEqual([]);
+ expect(targets('unrelatedExtension')).toEqual([]);
+ } finally {
+ cg?.close();
+ fs.rmSync(dir, { recursive: true, force: true });
+ }
+});
diff --git a/__tests__/kotlin-infix-calls.test.ts b/__tests__/kotlin-infix-calls.test.ts
index e95df7d31..c78a13695 100644
--- a/__tests__/kotlin-infix-calls.test.ts
+++ b/__tests__/kotlin-infix-calls.test.ts
@@ -128,13 +128,13 @@ infix fun Int.and(other: Int): Int = this
fun numeric(value: Int, mask: Int) = (value) and mask
`, 'numeric', []],
// A parenthesized receiver is typed like a bare one, and an extension on a
- // project class is not linked through a typed receiver (`value and mask`
- // gives the same result), so no edge is better than a guess by name.
+ // project class links through the typed receiver (`value and mask` gives
+ // the same result): the extension is named `Int::and` whatever its package.
['project numeric extension', `package project
class Int
infix fun Int.and(other: Int): Int = this
fun custom(value: Int, mask: Int) = (value) and mask
-`, 'custom', []],
+`, 'custom', ['Int::and']],
['nested-comment this receiver', `package app
class Spec {
infix fun apply(other: Spec): Spec = this
diff --git a/codegraph-kernel/src/resolve/kotlin_receiver.rs b/codegraph-kernel/src/resolve/kotlin_receiver.rs
index 99596c525..bae796fc1 100644
--- a/codegraph-kernel/src/resolve/kotlin_receiver.rs
+++ b/codegraph-kernel/src/resolve/kotlin_receiver.rs
@@ -133,6 +133,13 @@ impl KernelResolver {
fn kotlin_receiver_member(&mut self, hit: &KotlinReceiverHit, name: &str, r: &ResolveRefIn) -> Res