Summary
On the AST track, symbols declared in a sibling file of the same Swift module cannot be resolved, so cross-file IMPLEMENTS and REFERENCES edges are dropped silently.
Swift imports are module-level, not file-level, and every file in one Swift target is one module — symbols declared in one file are visible in its siblings with no import statement at all. The AST resolver, however, resolves a relation target only through (a) a symbol declared in the same file, or (b) an imported binding:
src/wiki-engine/code-knowledge/ast/index.ts — IMPLEMENTS sites: localInterfaces.find(…), then bindings.localToFile.get(ifaceName), otherwise continue (it never reaches a third fallback).
src/wiki-engine/code-knowledge/ast/call-resolver.ts — resolveOneCall() for a bare callee: symbolsByFile.get(site.fromFile) first, then bindings.localToSymbolId / bindings.localToFile, otherwise returns the site unresolved.
That model is correct for TypeScript / TSX / Python / Go, where reaching a symbol in another file always requires an import or a relative path. It does not hold for Swift.
Minimal reproduction
Sources/P1/A.swift
import Foundation
public protocol LocalProto {
func ping() -> String
}
public func helper() -> Int {
return 42
}
Sources/P1/B.swift
import Foundation
public struct S: LocalProto {
public func ping() -> String {
return "s"
}
}
public func caller() -> Int {
return helper()
}
teamai codebase --extract <pkg> --project p1:
[AST: ast: 6 symbols, 3 imports (0 resolved), 2 calls (0 resolved), 0 edges]
[extract] p1 complete
Files: 3
Facts: 9 (relation:3, interface:1, component:5)
Graph: 10 nodes, 4 edges
Neither struct S: LocalProto nor caller() → helper() yields a relation:
facts-cache.json holds component facts for Sources/P1/B.swift:3 and Sources/P1/B.swift:9, but no IMPLEMENTS relation for line 3 and nothing resolving the call at line 9.
gaps/detected.md reports high-orphan-ratio | 6/6 nodes have no graph connections, dependencies may not be fully extracted.
- The 4 graph edges are all
CONTAINS edges generated by the heuristic track; every code-ast edge is absent.
The missing dimension is specifically "another file in the same module"
A fixture that declares one conformance per shape — protocol Refined: LocalProto, struct S: LocalProto, enum E: LocalProto, actor A: LocalProto, class C: LocalProto, extension ExtSite: LocalProto — resolves 8/8 IMPLEMENTS facts when all declarations live in one file. So struct, enum, actor, class, a protocol refinement and an extension conformance are all captured; the capture is not class-specific and is not the problem. Splitting the declaration and the conformance across two files drops it entirely.
Impact
Any Swift package with more than one source file that declares a conformance or calls across files — i.e. practically every real package. Because the edges are simply absent rather than recorded as a gap, a consumer cannot distinguish "there is no dependency" from "this was not resolved".
Relationship to #842
Not introduced by that PR: before it, .swift files were not collected on either track, so the resolver never received a Swift specifier. Registering Swift is what exposes the limitation — which is also why that PR registers Swift straight into the EXTERNAL_IMPORT gap path for module imports rather than attempting module resolution.
Possible direction
Swift Package Manager places each target's sources under Sources/<Target>/ (tests under Tests/<Target>/), and all files in one target form one module.
- Derive the module root for a
.swift file by walking up to the first directory under Sources/ or Tests/.
- When resolving a call site or an
IMPLEMENTS target from a .swift file, fall back to a symbol declared anywhere inside that module root before giving up.
- Handle ambiguity explicitly — two targets in one package can declare the same symbol name. Pick deterministically or record a gap rather than emitting an arbitrary edge.
- Gate the fallback on the importer being Swift so the TS / Go / Python semantics are untouched.
Adjacent but distinct from the above: import MyLib is unresolvable today for the same underlying reason (the resolver has no directory listing), which is why #842 reports it as an EXTERNAL_IMPORT gap.
Summary
On the AST track, symbols declared in a sibling file of the same Swift module cannot be resolved, so cross-file
IMPLEMENTSandREFERENCESedges are dropped silently.Swift imports are module-level, not file-level, and every file in one Swift target is one module — symbols declared in one file are visible in its siblings with no import statement at all. The AST resolver, however, resolves a relation target only through (a) a symbol declared in the same file, or (b) an imported binding:
src/wiki-engine/code-knowledge/ast/index.ts—IMPLEMENTSsites:localInterfaces.find(…), thenbindings.localToFile.get(ifaceName), otherwisecontinue(it never reaches a third fallback).src/wiki-engine/code-knowledge/ast/call-resolver.ts—resolveOneCall()for a bare callee:symbolsByFile.get(site.fromFile)first, thenbindings.localToSymbolId/bindings.localToFile, otherwise returns the site unresolved.That model is correct for TypeScript / TSX / Python / Go, where reaching a symbol in another file always requires an import or a relative path. It does not hold for Swift.
Minimal reproduction
Sources/P1/A.swiftSources/P1/B.swiftteamai codebase --extract <pkg> --project p1:Neither
struct S: LocalProtonorcaller() → helper()yields a relation:facts-cache.jsonholdscomponentfacts forSources/P1/B.swift:3andSources/P1/B.swift:9, but noIMPLEMENTSrelation for line 3 and nothing resolving the call at line 9.gaps/detected.mdreportshigh-orphan-ratio | 6/6 nodes have no graph connections, dependencies may not be fully extracted.CONTAINSedges generated by the heuristic track; everycode-astedge is absent.The missing dimension is specifically "another file in the same module"
A fixture that declares one conformance per shape —
protocol Refined: LocalProto,struct S: LocalProto,enum E: LocalProto,actor A: LocalProto,class C: LocalProto,extension ExtSite: LocalProto— resolves 8/8IMPLEMENTSfacts when all declarations live in one file. Sostruct,enum,actor,class, a protocol refinement and anextensionconformance are all captured; the capture is notclass-specific and is not the problem. Splitting the declaration and the conformance across two files drops it entirely.Impact
Any Swift package with more than one source file that declares a conformance or calls across files — i.e. practically every real package. Because the edges are simply absent rather than recorded as a gap, a consumer cannot distinguish "there is no dependency" from "this was not resolved".
Relationship to #842
Not introduced by that PR: before it,
.swiftfiles were not collected on either track, so the resolver never received a Swift specifier. Registering Swift is what exposes the limitation — which is also why that PR registers Swift straight into theEXTERNAL_IMPORTgap path for module imports rather than attempting module resolution.Possible direction
Swift Package Manager places each target's sources under
Sources/<Target>/(tests underTests/<Target>/), and all files in one target form one module..swiftfile by walking up to the first directory underSources/orTests/.IMPLEMENTStarget from a.swiftfile, fall back to a symbol declared anywhere inside that module root before giving up.Adjacent but distinct from the above:
import MyLibis unresolvable today for the same underlying reason (the resolver has no directory listing), which is why #842 reports it as anEXTERNAL_IMPORTgap.