~ [j] fix cross-module resolution
diff --git a/compiler/fir/fir-jvm/src/org/jetbrains/kotlin/fir/java/deserialization/JvmClassFileBasedSymbolProvider.kt b/compiler/fir/fir-jvm/src/org/jetbrains/kotlin/fir/java/deserialization/JvmClassFileBasedSymbolProvider.kt
index 7ea18c5..bf9d88b 100644
--- a/compiler/fir/fir-jvm/src/org/jetbrains/kotlin/fir/java/deserialization/JvmClassFileBasedSymbolProvider.kt
+++ b/compiler/fir/fir-jvm/src/org/jetbrains/kotlin/fir/java/deserialization/JvmClassFileBasedSymbolProvider.kt
@@ -221,8 +221,26 @@
     override fun isNewPlaceForBodyGeneration(classProto: ProtoBuf.Class): Boolean =
         JvmFlags.IS_COMPILED_IN_JVM_DEFAULT_MODE.get(classProto.getExtension(JvmProtoBuf.jvmClassFlags))
 
-    override fun hasPackage(fqName: FqName): Boolean =
-        binaryClassFinderInputs?.hasBinaryPackage(fqName) ?: javaFacade.hasPackage(fqName)
+    override fun hasPackage(fqName: FqName): Boolean {
+        // On the `java-direct` path (Stage 2 §6.3) the deserializer reads binary-package
+        // presence through the injected [binaryClassFinderInputs] adapter, which only knows
+        // about physical `.class`-bearing directories on the classpath. A Kotlin package
+        // that exists *only* via metadata — i.e. `@file:JvmPackageName("bar")` on top-level
+        // declarations of `package foo` — has NO `foo/` directory on disk; its existence is
+        // recorded exclusively in the consumer's `.kotlin_module` and is enumerated by
+        // [packagePartProvider] via [findPackageParts] / [computePackageSetWithNonClassDeclarations].
+        //
+        // The legacy PSI-backed [javaFacade.hasPackage] returned `true` for such packages
+        // because `KotlinJavaPsiFacade.findPackage` consulted both directory- and
+        // metadata-derived package sources. Mirror that semantics here, otherwise
+        // cross-module resolution of `@file:JvmPackageName`-shifted top-level callables
+        // short-circuits at the package-presence gate (see
+        // `compiler/testData/codegen/boxJvm/compileKotlinAgainstKotlin/jvmPackageName.kt`
+        // and siblings).
+        if (binaryClassFinderInputs == null) return javaFacade.hasPackage(fqName)
+        if (binaryClassFinderInputs.hasBinaryPackage(fqName)) return true
+        return packagePartProvider.findPackageParts(fqName.asString()).isNotEmpty()
+    }
 
     // Stage 2 §6.3 (see [JvmBinaryClassFinderInputs]): on the `java-direct` library-session
     // path the deserializer reads binary class presence / materialization through the
diff --git a/compiler/java-direct/AGENT_INSTRUCTIONS.md b/compiler/java-direct/AGENT_INSTRUCTIONS.md
index 6975abc..7b6812c 100644
--- a/compiler/java-direct/AGENT_INSTRUCTIONS.md
+++ b/compiler/java-direct/AGENT_INSTRUCTIONS.md
@@ -127,6 +127,11 @@
 
 # PSI regression (only after shared FIR file or test data changes)
 ./gradlew :compiler:fir:analysis-tests:test --tests "PhasedJvmDiagnosticLightTreeTestGenerated.*" --stacktrace -q 2>&1 | tee "$JD_TMP/psi_test.txt"
+
+# Cross-module bytecode-vs-source regression (MUST run after any edit to shared FIR JVM files
+# below — covers cases the PSI gate above does not, e.g. `@file:JvmPackageName`-shifted
+# top-level callables read across modules. See ITERATION_RESULTS.md 2026-06-02 entry).
+./gradlew :compiler:fir:fir2ir:test --tests "*FirLightTreeBlackBoxCodegenTestGenerated*CompileKotlinAgainstKotlin*" --stacktrace -q 2>&1 | tee "$JD_TMP/ckk_test.txt"
 ```
 
 ### Extracting failures
@@ -146,14 +151,23 @@
 - `compiler/fir/fir-jvm/src/.../FirJavaFacade.kt`
 - `compiler/fir/fir-jvm/src/.../JavaTypeConversion.kt`
 - `compiler/fir/fir-jvm/src/.../javaAnnotationsMapping.kt`
+- `compiler/fir/fir-jvm/src/.../JavaSymbolProvider.kt`
+- `compiler/fir/fir-jvm/src/.../deserialization/JvmClassFileBasedSymbolProvider.kt`
+- `compiler/fir/fir-jvm/src/.../deserialization/JvmBinaryClassFinderInputs.kt`
+- `compiler/fir/entrypoint/src/.../session/FirJvmSessionFactory.kt`
+- `compiler/cli/cli-jvm/src/.../pipeline/jvm/JvmFrontendPipelinePhase.kt`
 - `core/compiler.common.jvm/src/.../load/java/structure/*.kt`
 
 For any edit to these: always compare with upstream first —
 ```bash
 git show origin/master:<path> | grep -A10 "relevantFunction"
 ```
-Then run PSI regression BEFORE and AFTER the change. If new PSI failures appear,
-**revert immediately**.
+Then run BOTH regression gates (PSI **and** `CompileKotlinAgainstKotlin`) BEFORE and AFTER
+the change. If new failures appear in either gate, **revert immediately**. The
+`CompileKotlinAgainstKotlin` gate is the only routine suite that exercises
+cross-module bytecode-vs-source resolution (`@file:JvmPackageName` shifting,
+`MULTIFILE_CLASS_PART`, etc.) — the PSI gate alone is insufficient for any edit
+to the deserializer / library-session wiring path.
 
 ---
 
diff --git a/compiler/java-direct/ITERATION_RESULTS.md b/compiler/java-direct/ITERATION_RESULTS.md
index a47820b..481a169 100644
--- a/compiler/java-direct/ITERATION_RESULTS.md
+++ b/compiler/java-direct/ITERATION_RESULTS.md
@@ -4213,6 +4213,140 @@
 
 ---
 
+## 2026-06-02 — `@file:JvmPackageName` cross-module regression (`CompileKotlinAgainstKotlin.testJvmPackageName*`)
+
+### Symptom
+
+Three sibling tests of
+`FirLightTreeBlackBoxCodegenTestGenerated.BoxJvm.CompileKotlinAgainstKotlin`:
+
+- `testJvmPackageName`
+- `testJvmPackageNameWithJvmName`
+- `testJvmPackageNameMultifileClass`
+
+failed with `UNRESOLVED_REFERENCE 'v'` / `'f'` on the main module while compiling
+`B.kt` against lib's pre-built `.class` output. The fourth sibling,
+`testJvmPackageNameInRootPackage`, was green throughout. The shared distinguishing
+factor: the failing testdata places lib's top-level callables in `package foo`
+but ships the bytecode under `bar/AKt.class` via `@file:JvmPackageName("bar")`;
+the passing test has no source package, hence no source-vs-bytecode home shift.
+
+### Investigation path
+
+The previous session falsified the working hypothesis that the §6.4/§6.5
+binary-class-finder series caused the regression — a whole-tree revert of all
+`[j]` deltas left the three failures unchanged. After the user's redirection
+("investigate from the other side; the tests pass in master, so the cause is
+inside the java-direct implementation"), instrumentation in
+`JvmClassFileBasedSymbolProvider` made the actual gate visible:
+
+1. **Test routing**: `setupJvmPipelineSteps` registers `FirCliJvmFacade`, **not**
+   `FirFrontendFacade`, so every JVM black-box codegen test runs through
+   `JvmCliPipeline` → `JvmFrontendPipelinePhase`. The `[j]` series wired
+   `JvmFrontendPipelinePhase` to unconditionally install
+   `createJavaDirectBinaryClassFinderInputsBuilder`, so the deserializer of every
+   such test ends up with `binaryClassFinderInputs = JvmBinaryClassFinderInputsOverIndex`
+   regardless of whether `JavaDirectFacadeBuilderProvider` is registered at the
+   test fixture level.
+2. **Metadata visibility**: `JvmClassFileBasedSymbolProvider.computePackageSetWithNonClassDeclarations()`
+   on main's library session returned a set containing `"foo"` (lib's
+   `.kotlin_module` is properly read).
+3. **Suppressed call**: yet `computePackagePartsInfos("foo")` was **never**
+   invoked. The gate sits upstream of the deserializer:
+   `JvmClassFileBasedSymbolProvider.hasPackage("foo")` returned `false`, causing
+   every callable-presence probe through the composite to skip the `foo` package
+   entirely.
+
+### Root cause
+
+`JvmClassFileBasedSymbolProvider.hasPackage` on the `java-direct` path delegated
+**solely** to `binaryClassFinderInputs.hasBinaryPackage(fqName)`:
+
+```kotlin
+override fun hasPackage(fqName: FqName): Boolean =
+    binaryClassFinderInputs?.hasBinaryPackage(fqName) ?: javaFacade.hasPackage(fqName)
+```
+
+`JvmBinaryClassFinderInputsOverIndex.hasBinaryPackage(fqName)` only asks the
+`JvmDependenciesIndex` whether a physical `.class`-bearing directory `fqName/`
+exists on the classpath. For `@file:JvmPackageName("bar")` on
+`package foo` lib code, the only physical directory is `bar/`; `foo/` does not
+exist on disk and never will. The legacy PSI-backed `FirJavaFacade.hasPackage`
+returned `true` for such metadata-only packages because
+`KotlinJavaPsiFacade.findPackage` consulted both directory- and metadata-derived
+package sources. The `[j]` adapter dropped that second source.
+
+### Fix
+
+Smallest scope: `JvmClassFileBasedSymbolProvider.hasPackage`. When the
+binary-only check returns `false`, fall back to
+`packagePartProvider.findPackageParts(fqName.asString()).isNotEmpty()`, which is
+metadata-aware (reads `.kotlin_module`). This mirrors the legacy PSI semantics
+without touching the binary adapter contract:
+
+```kotlin
+override fun hasPackage(fqName: FqName): Boolean {
+    if (binaryClassFinderInputs == null) return javaFacade.hasPackage(fqName)
+    if (binaryClassFinderInputs.hasBinaryPackage(fqName)) return true
+    return packagePartProvider.findPackageParts(fqName.asString()).isNotEmpty()
+}
+```
+
+This leaves `JvmBinaryClassFinderInputs` itself purely directory-oriented (its
+documented role) and keeps the metadata source where it belongs — on
+`PackagePartProvider`, which the deserializer already holds a reference to.
+Non-`java-direct` paths (PSI / LL / IDE / scripting / IC / jklib) take the early
+return through `javaFacade.hasPackage` and are completely unaffected.
+
+### Verification
+
+| Suite | Tests | Failures |
+|-------|------:|---------:|
+| `*FirLightTreeBlackBoxCodegenTestGenerated*CompileKotlinAgainstKotlin*testJvmPackageName*` | 4 | 0 |
+| `*FirLightTreeBlackBoxCodegenTestGenerated*CompileKotlinAgainstKotlin*` (full sub-suite) | n | 0 |
+| `:compiler:fir:analysis-tests:test --tests PhasedJvmDiagnosticLightTreeTestGenerated.*` (PSI regression gate) | n | 0 |
+| `:compiler:java-direct:test` (full java-direct suite) | n | 0 |
+
+### Files Modified
+
+| File | Change |
+|------|--------|
+| `compiler/fir/fir-jvm/src/.../deserialization/JvmClassFileBasedSymbolProvider.kt` | `hasPackage`: on the `java-direct` path, combine binary-directory presence (`binaryClassFinderInputs.hasBinaryPackage`) with metadata-declared presence (`packagePartProvider.findPackageParts(...).isNotEmpty()`). |
+
+### Key Learnings
+
+- **`JvmBinaryClassFinderInputs.hasBinaryPackage` ≠ `FirSymbolProvider.hasPackage`.**
+  The former is purely about physical binary directories on the classpath (its
+  documented job). The latter answers "does this Kotlin/Java package exist in
+  this provider's view of the world", which is a strictly broader question:
+  `@file:JvmPackageName`-shifted Kotlin source packages exist as `.kotlin_module`
+  entries only. Conflating the two collapses cross-module resolution of
+  bytecode-home-shifted top-level callables.
+
+- **Coverage gap of the routine regression gate.** The pre-existing "PSI
+  regression" command (`PhasedJvmDiagnosticLightTreeTestGenerated.*`) is a
+  single-compilation-unit diagnostic suite. It cannot catch cross-module
+  bytecode-against-source bugs. The `*CompileKotlinAgainstKotlin*` suite is the
+  canonical regression gate for such bugs and has been added to
+  `AGENT_INSTRUCTIONS.md` (§4 Routine regression suites).
+
+- **The CLI pipeline is where `java-direct` is actually exercised by codegen
+  tests.** Repeating the lesson from the Lombok iteration (above): every black-box
+  codegen test goes through `FirCliJvmFacade` → `JvmFrontendPipelinePhase`, so
+  `binaryClassFinderInputs` is always non-null in those tests regardless of any
+  fixture-level `useAdditionalService` registration. That is why this bug
+  surfaced through `CompileKotlinAgainstKotlin` (a CLI-pipeline-driven codegen
+  suite) and not through any `FirFrontendFacade`-driven analysis-tests path.
+
+- **Debug-the-gate, not the symptom.** Three earlier rounds of investigation
+  hunted the symptom (`UNRESOLVED 'v' at B.kt`) and bisected commits with no
+  result. A 4-line `File.appendText` instrumentation inside
+  `computePackagePartsInfos` revealed in one test run that the call was never
+  reached for `pkg=foo`, which immediately pointed at the upstream gate and
+  shortened the investigation by an order of magnitude.
+
+---
+
 ## Archived Iteration History
 
 Earlier entries have been moved to dated archives under `implDocs/archive/`: