Status: draft implementation-proposal open — PR #1946 (marked ready on maintainer design ack). Minimal scope as proposed here: per-file τ folded into func/method QNs, #495 #-suffix style; τ-aware resolution is the follow-up. Sole-constrained-variant callers keep their edges; twin-pair callers from unconstrained files resolve as ambiguous (honest) instead of binding the lexicographically-smallest stub. Receiver qualification (#1913) does not separate twins — generated-twin evidence below.
Version
codebase-memory-mcp 0.10.8 (also reproduced on a source build @ d706c33)
Platform
macOS (Apple Silicon)
Install channel
Built from source
Binary variant
standard
What happened, and what did you expect?
Go build-constrained twin files (//go:build unix / //go:build !unix, GOOS/GOARCH filename suffixes) legally define the same symbols in one package. The indexer ignores the constraints; the upsert keeps one node per QN by the deterministic smallest-file-path rule. Measured:
hostmirror_other.go (2-line stub) beat hostmirror_unix.go (63-line real implementation) because o < u: get_code_snippet returns the stub and all 25 inbound CALLS landed on the stub;
- same for a
nofollow_unix.go/nofollow_other.go constant pair;
- one generated GOARCH twin pair loses 39 of 40 nodes — 100% of the losing file;
- the dropped twin leaves no trace: both files parse clean, so
index_status/check_index_coverage report nothing.
Expected: both build variants exist as nodes — exactly what #495 already does for Rust #[cfg(...)] twins.
Reproduction
// mirror_unix.go
//go:build unix
package mirror
func MirrorConfig(path string) (string, error) {
dst := path + ".bak"
// ... real work ...
return dst, nil
}
// mirror_other.go
//go:build !unix
package mirror
func MirrorConfig(path string) (string, error) { return "", nil }
Actual: one node, file_path = mirror_other.go (stub wins); the real implementation has no node. Expected: two nodes, one per constraint.
Proposed design (Go analog of #495)
Per-file τ, resolved once: the compacted //go:build expression when present, else the GOOS/GOARCH filename suffix. Fold τ into that file's def QNs (proj.pkg.MirrorConfig#unix), mirrored on the call-scope side. Same-config cross-file calls stay exact by trying module.name#τ (caller's τ) before the plain key; calls from unconstrained files degrade to the multi-candidate path — the honest answer, and strictly better than today's silent bind-everyone-to-the-stub.
Generated-twin evidence (bpf2go, measured with #1913/#1915 applied)
bpf2go emits one Go file per (program, target) pair, gated by //go:build GOARCH/endianness lines. Two files in the measured repo are GOARCH twins with an identical set of 21 top-level declarations: the arm64 twin kept 43 nodes, the x86 twin kept 1 (its __file__ node) — 39 symbol nodes lost, 100% of the losing file, invisible to parse_partial. Directory-level: 66 Close methods in source, 63 in the graph — the missing 3 are exactly the x86 twin's, and receiver-qualified QNs cannot separate them because the twins are twins all the way down; only a build-constraint discriminator in the QN can. Scale note: adding a second build target — a routine change — multiplies the loss by the number of programs, which argues for a cheap, automatic discriminator rather than an opt-in.
Related: #495, #1909, #1910 (same silent-upsert-collapse family); tracked in #1932.
Confirmations
Status: draft implementation-proposal open — PR #1946 (marked ready on maintainer design ack). Minimal scope as proposed here: per-file τ folded into func/method QNs, #495
#-suffix style; τ-aware resolution is the follow-up. Sole-constrained-variant callers keep their edges; twin-pair callers from unconstrained files resolve as ambiguous (honest) instead of binding the lexicographically-smallest stub. Receiver qualification (#1913) does not separate twins — generated-twin evidence below.Version
codebase-memory-mcp 0.10.8 (also reproduced on a source build @ d706c33)
Platform
macOS (Apple Silicon)
Install channel
Built from source
Binary variant
standard
What happened, and what did you expect?
Go build-constrained twin files (
//go:build unix///go:build !unix, GOOS/GOARCH filename suffixes) legally define the same symbols in one package. The indexer ignores the constraints; the upsert keeps one node per QN by the deterministic smallest-file-path rule. Measured:hostmirror_other.go(2-line stub) beathostmirror_unix.go(63-line real implementation) becauseo<u:get_code_snippetreturns the stub and all 25 inbound CALLS landed on the stub;nofollow_unix.go/nofollow_other.goconstant pair;index_status/check_index_coveragereport nothing.Expected: both build variants exist as nodes — exactly what #495 already does for Rust
#[cfg(...)]twins.Reproduction
Actual: one node,
file_path = mirror_other.go(stub wins); the real implementation has no node. Expected: two nodes, one per constraint.Proposed design (Go analog of #495)
Per-file τ, resolved once: the compacted
//go:buildexpression when present, else the GOOS/GOARCH filename suffix. Fold τ into that file's def QNs (proj.pkg.MirrorConfig#unix), mirrored on the call-scope side. Same-config cross-file calls stay exact by tryingmodule.name#τ(caller's τ) before the plain key; calls from unconstrained files degrade to the multi-candidate path — the honest answer, and strictly better than today's silent bind-everyone-to-the-stub.Generated-twin evidence (bpf2go, measured with #1913/#1915 applied)
bpf2goemits one Go file per (program, target) pair, gated by//go:buildGOARCH/endianness lines. Two files in the measured repo are GOARCH twins with an identical set of 21 top-level declarations: the arm64 twin kept 43 nodes, the x86 twin kept 1 (its__file__node) — 39 symbol nodes lost, 100% of the losing file, invisible toparse_partial. Directory-level: 66Closemethods in source, 63 in the graph — the missing 3 are exactly the x86 twin's, and receiver-qualified QNs cannot separate them because the twins are twins all the way down; only a build-constraint discriminator in the QN can. Scale note: adding a second build target — a routine change — multiplies the loss by the number of programs, which argues for a cheap, automatic discriminator rather than an opt-in.Related: #495, #1909, #1910 (same silent-upsert-collapse family); tracked in #1932.
Confirmations