Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
111 commits
Select commit Hold shift + click to select a range
9d9e754
fix(resolver): link Go imports of a tracked module across repos
peterkc Oct 23, 2026
57481f1
fix(resolver): bound Go import recovery to other tracked repos
peterkc Oct 23, 2026
681ef69
fix(resolver): cancel Go import recovery and match imports by path
peterkc Oct 23, 2026
a4ddd3d
fix(resolver): keep tracked Go import links across provider changes
peterkc Oct 24, 2026
b66bd87
fix(resolver): keep Go import recovery idempotent and within its own …
peterkc Oct 24, 2026
91d0312
fix(resolver): finish Go import registry seeding before provider reco…
peterkc Oct 24, 2026
1376ff3
fix(resolver): recover Go imports when module identity ties change
peterkc Oct 24, 2026
3fdf715
fix(resolver): preserve Go import state during full resolution and re…
peterkc Oct 24, 2026
b493d9b
fix(resolver): rebind Go imports when local replacements are added
peterkc Oct 24, 2026
b0b30ee
fix(resolver): retain Go import retry metadata across replacement cycles
peterkc Oct 24, 2026
76c8152
test(indexer): adapt Go import restub test to the new return values
peterkc Oct 24, 2026
a4769d0
fix(resolver): bind duplicate Go import aliases
peterkc Oct 24, 2026
a465476
feat(resolver): link Rust imports to a declared tracked crate
peterkc Oct 23, 2026
baee2d8
fix(indexer): preserve tracked Cargo imports across provider replacement
peterkc Oct 24, 2026
ebf503d
fix(indexer): preserve exact Cargo call stubs within eviction scope
peterkc Oct 24, 2026
2446124
fix(indexer): honor Cargo manifest sources and deferred frontiers
peterkc Oct 24, 2026
33dbf35
fix(indexer): retain Cargo call stubs across provider file recovery
peterkc Oct 24, 2026
177b9f3
fix(indexer): carry Cargo provider recovery through warmup
peterkc Oct 24, 2026
d6488c7
feat(resolver): link Python imports to a declared tracked distribution
peterkc Oct 23, 2026
e79898a
fix(resolver): page the Python import retry and keep src packages local
peterkc Oct 23, 2026
c7265fa
fix(indexer): preserve tracked Python import and call lifecycle
peterkc Oct 24, 2026
2845f4f
fix(indexer): keep local Python calls on the native restub path
peterkc Oct 24, 2026
4fb0247
fix(indexer): preserve Python provider discovery across warm restarts
peterkc Oct 24, 2026
d79c100
fix(indexer): retain Python module additions through fallback
peterkc Oct 24, 2026
b31696a
fix(resolver): follow tracked Python package re-exports
peterkc Oct 24, 2026
618f87b
fix(resolver): preserve Python re-export links across cold and mixed …
peterkc Oct 24, 2026
d0193d0
test: keep the popular Go provider over the incoming-row budget
peterkc Oct 25, 2026
d5d2925
style: apply staticcheck QF1001 and QF1003 to the Go import code
peterkc Oct 25, 2026
7885639
style: apply staticcheck to the Cargo and Python import code
peterkc Oct 25, 2026
013ef09
style: use a tagged switch for the Python lifecycle track operation
peterkc Oct 25, 2026
9482631
fix(resolver): bind bridge-linked calls only through the linked impor…
peterkc Oct 25, 2026
f225c32
fix(indexer): close the import scan before the Python package lookup
peterkc Oct 25, 2026
7ea0327
fix(indexer): keep same-repo Go replacement imports on single-file saves
peterkc Oct 25, 2026
2e12eaf
fix(resolver): skip an oversized provider without starving later impo…
peterkc Oct 25, 2026
c3d8498
fix(resolver): let the longest tracked Go module own nested imports
peterkc Oct 25, 2026
a77b5f3
fix(resolver): restore a revisited Cargo import when its target is re…
peterkc Oct 25, 2026
8240ed0
fix(resolver): reopen top-level Python calls parked as stdlib
peterkc Oct 25, 2026
81e7b50
fix(indexer): rewrite bridge call stubs when carrying prior bindings
peterkc Oct 25, 2026
e065aff
fix(indexer): recover Cargo consumers whose import stub is unresolved…
peterkc Oct 25, 2026
60e1dfb
fix(resolver): restore every duplicate-alias Go import statement
peterkc Oct 25, 2026
fc789a9
fix(resolver): bump ResolutionSemanticsVersion for Python bridge meta…
peterkc Oct 25, 2026
76da94b
refactor(daemon): drop the duplicate warm-frontier recording loop
peterkc Oct 25, 2026
e21bb40
test: replace leftover diagnostic logs with assertions
peterkc Oct 25, 2026
37000fc
fix(resolver): count Go import fallback demotions as unresolved
peterkc Oct 25, 2026
1d81a61
docs(multi-repo): document tracked Go, Cargo and Python import links
peterkc Oct 25, 2026
fb865be
perf(indexer): scan imports once and defer Cargo manifest probes
peterkc Oct 25, 2026
4647050
style: use a tagged switch for the replacement save route
peterkc Oct 25, 2026
3a209d1
fix(resolver): bind Python imported methods only through the imported…
peterkc Oct 25, 2026
700d367
fix(resolver): reopen Cargo calls when their import is refused
peterkc Oct 25, 2026
5f042cf
fix(resolver): bind calls through grouped and aliased Rust uses
peterkc Oct 25, 2026
8946c3b
docs(multi-repo): state the Cargo module-call and grouped-use limits
peterkc Oct 25, 2026
1da873d
perf(resolver): hydrate only pending callers' imports on scoped passes
peterkc Oct 25, 2026
f741317
perf(resolver): look up bridge call candidates in provider repos only
peterkc Oct 25, 2026
e5ec71d
perf(indexer): skip Cargo import scans when no repo has a Cargo root
peterkc Oct 25, 2026
1ebb921
docs(multi-repo): state that working Python links are not rechecked
peterkc Oct 25, 2026
113b67a
perf(indexer): gate Python added-file consumers on the repo count
peterkc Oct 25, 2026
3a3eb58
fix(indexer): read configured repos through ReposSnapshot
peterkc Oct 25, 2026
589782c
perf(indexer): filter Python import tops while scanning for consumers
peterkc Oct 25, 2026
d98a5fd
fix(resolver): reject nested grouped-use leaf decoys
peterkc Oct 25, 2026
05bf46e
fix(resolver): scope Python receiver shadow checks to the caller file
peterkc Oct 25, 2026
f6e69a6
perf(resolver): warm caller nodes before scoped import indexing
peterkc Oct 25, 2026
f705302
perf(indexer): skip Cargo recovery without two Cargo roots
peterkc Oct 25, 2026
de1aff8
test(resolver): cover Cargo glob and call-restub rejection paths
peterkc Oct 25, 2026
453195e
docs(indexer): name Python import bindings in the extractor version c…
peterkc Oct 25, 2026
069fa17
docs: state exact Python typed-method bridge conditions
peterkc Oct 25, 2026
364a9e3
fix(resolver): require Cargo members to match the probed module
peterkc Oct 25, 2026
8e1bb82
perf(resolver): cache Python caller-file types for each pass
peterkc Oct 25, 2026
c36f79f
test(resolver): expose Python unmatched-receiver guard regressions
peterkc Oct 25, 2026
23e38a9
docs: state Cargo direct-group and exact-parent call limits
peterkc Oct 25, 2026
5ff9b17
perf(resolver): batch Python caller-file type snapshots
peterkc Oct 25, 2026
9dde603
fix(indexer): clear Cargo module evidence when restubbing imports
peterkc Oct 25, 2026
e84949b
fix(indexer): invalidate removed Go package projections before settle…
peterkc Oct 25, 2026
5b6ba83
perf(indexer): build the untrack resolver only for parked Go references
peterkc Oct 25, 2026
759b16a
test(indexer): list why DeltaWriter lacks the Python import projectio…
peterkc Oct 25, 2026
d47c49b
fix(resolver): restate Python import metadata after file saves
peterkc Oct 25, 2026
4f02233
fix(resolver): batch Python module attribution at save boundaries
peterkc Oct 26, 2026
8a449f3
test(indexer): name Python save-time metadata restaters
peterkc Oct 26, 2026
71f8b06
fix(resolver): preserve same-line Python import identities
peterkc Oct 26, 2026
6a7d9fd
fix(resolver): preserve Python call identities before provider tracking
peterkc Oct 26, 2026
f474677
fix(resolver): normalize Cargo aliases for call workspace policy
peterkc Oct 26, 2026
36c2474
fix(resolver): report deferred Go import recovery
peterkc Oct 26, 2026
e5700b6
fix(resolver): preserve concrete Go imports during restoration
peterkc Oct 26, 2026
de0b617
fix(indexer): park local Go imports on admission skips
peterkc Oct 26, 2026
ae7e7b8
fix(resolver): admit restubbed Cargo imports in the prepass
peterkc Oct 26, 2026
f8856e9
fix(indexer): clear Python re-export evidence when restubbing calls
peterkc Oct 26, 2026
66f696d
fix(resolver): refuse foreign Python test-package bindings
peterkc Oct 26, 2026
ed61cf1
fix(resolver): keep AST evidence for all Cargo imported calls
peterkc Oct 26, 2026
7406b88
perf(resolver): batch Go import ownership and statistics lookups
peterkc Oct 26, 2026
150d23c
fix(indexer): match cold output across tracked-import lifecycles
peterkc Oct 26, 2026
af33f3d
test(indexer): classify tracked-bridge metadata restaters
peterkc Oct 26, 2026
e9d21be
fix(resolver): let a full pass complete deferred Go import recovery
peterkc Oct 26, 2026
22c3dd2
fix(resolver): persist meta-only reindex rows in chunked passes
peterkc Oct 26, 2026
176e84f
perf(indexer): skip the second incoming resolve on Python and Rust saves
peterkc Oct 26, 2026
7ab22ce
perf(resolver): run Go import recovery only for Go inputs
peterkc Oct 26, 2026
0d22d1c
perf(indexer): keep Python and Cargo manifest views across passes
peterkc Oct 26, 2026
3b64f9d
perf(indexer): skip added-file import scans without a declared bridge
peterkc Oct 26, 2026
18cf7e7
perf(indexer): hydrate only foreign bridge sites when parking a repo
peterkc Oct 26, 2026
316c397
perf(resolver): reuse caller imports for Python re-export calls
peterkc Oct 26, 2026
b78de7f
fix(indexer): stop failed registrations from keeping Python warmup open
peterkc Oct 26, 2026
2a998c9
fix(resolver): fill Python re-export imports before the worker fan-out
peterkc Oct 26, 2026
1ce7196
fix(indexer): keep the Cargo lookup nil below two Cargo roots
peterkc Oct 26, 2026
b7ff48a
fix(indexer): mark only master-resolved files as prepared
peterkc Oct 26, 2026
3937c79
fix(indexer): run the full pass after a deferred Go import recovery
peterkc Oct 26, 2026
1d67d5d
fix(indexer): compare the owner's repo prefix for root Cargo crates
peterkc Oct 26, 2026
89cd86b
fix(resolver): drain re-queued Go module work on any save
peterkc Oct 26, 2026
4a2f602
docs(indexer): describe the manifest snapshot invalidation rule
peterkc Oct 26, 2026
7139130
fix(resolver): skip over-budget provider recovery without refusing th…
peterkc Oct 26, 2026
4be92d0
test(resolver): prove the Go deferral continuation without an env switch
peterkc Oct 26, 2026
e78972f
perf(resolver): batch the Python re-export package prefill
peterkc Oct 26, 2026
40cbfba
fix(resolver): skip over-budget providers in a stable order
peterkc Oct 26, 2026
4998f59
test(resolver): count all-repo rechecks by the exact binding kinds
peterkc Oct 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions cmd/gortex/daemon_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,18 +367,20 @@ func (frontier *warmupDeltaFrontier) invalidate() {
func (frontier *warmupDeltaFrontier) record(result *indexer.IndexResult) {
frontier.mu.Lock()
defer frontier.mu.Unlock()
if result != nil {
for _, file := range result.CrossRepoResolveFiles {
if file != "" {
frontier.crossRepoFiles[file] = struct{}{}
}
}
}
if result == nil || result.RepoPrefix == "" || result.FullRetrack || len(result.FailedFiles) > 0 {
frontier.exact = false
return
}
plan := frontier.plans[result.RepoPrefix]
plan.Merge(result.DerivedInvalidation)
frontier.plans[result.RepoPrefix] = plan
for _, file := range result.CrossRepoResolveFiles {
if file != "" {
frontier.crossRepoFiles[file] = struct{}{}
}
}
}

func (frontier *warmupDeltaFrontier) snapshot(changedRepos int, scopeUnknown bool) (map[string]indexer.DerivedInvalidationPlan, []string, bool) {
Expand Down
25 changes: 25 additions & 0 deletions cmd/gortex/daemon_warm_cargo_frontier_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package main

import (
"testing"

"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/indexer"
)

func TestWarmupCargoConsumersStayOutsideDerivedPlan(t *testing.T) {
for _, full := range []bool{false, true} {
frontier := newWarmupDeltaFrontier()
frontier.record(&indexer.IndexResult{
RepoPrefix: "provider", FullRetrack: full,
DerivedInvalidation: indexer.DerivedInvalidationPlan{Files: []string{"provider/src/lib.rs"}},
CrossRepoResolveFiles: []string{"consumer/src/lib.rs"},
})
plans, files, exact := frontier.snapshot(1, false)
require.Contains(t, files, "consumer/src/lib.rs")
require.Equal(t, !full, exact)
for _, plan := range plans {
require.NotContains(t, plan.Files, "consumer/src/lib.rs")
}
}
}
25 changes: 25 additions & 0 deletions cmd/gortex/daemon_warm_python_frontier_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package main

import (
"testing"

"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/indexer"
)

func TestWarmupPythonConsumersStayOutsideDerivedPlan(t *testing.T) {
for _, full := range []bool{false, true} {
frontier := newWarmupDeltaFrontier()
frontier.record(&indexer.IndexResult{
RepoPrefix: "provider", FullRetrack: full,
DerivedInvalidation: indexer.DerivedInvalidationPlan{Files: []string{"provider/src/PIL/image.py"}},
CrossRepoResolveFiles: []string{"consumer/app.py"},
})
plans, files, exact := frontier.snapshot(1, false)
require.Contains(t, files, "consumer/app.py")
require.Equal(t, !full, exact)
for _, plan := range plans {
require.NotContains(t, plan.Files, "consumer/app.py")
}
}
}
33 changes: 32 additions & 1 deletion docs/multi-repo.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,32 @@ the participating repositories' allow-lists. This keeps a repository from
silently losing framework edges requested by a sibling while allowing a
workspace where every repository opts out to avoid the scans entirely.

### Tracked module imports

- Go imports can link to a representative source file in a tracked module.
The longest matching root `go.mod` path selects the package. Ambiguous providers
stay unlinked. Local `replace` paths must point to indexed source files.
- Rust `use` imports can link to a tracked library declared in `Cargo.toml`.
Dependency aliases, workspace dependencies, explicit workspace members and
`[lib]` names and paths are supported. Undeclared crates, ambiguous providers
and reserved names such as `std`, `core` and `crate` stay unlinked.
- Python imports can link to a tracked distribution declared in `pyproject.toml`
or `requirements*.txt`. Root and `src/` layouts support modules and regular
packages. A local top-level package blocks a foreign link. Ambiguous providers,
undeclared distributions and Python stdlib imports stay unlinked.

These import-file links do not make every symbol in the provider reachable.
Calls still need an imported path or type. Versions and build configurations
are not verified.

### Python package re-exports

Calls through a declared, tracked Python distribution can follow one level of
relative `from .module import Name` or `from .module import Name as Alias` in its
package `__init__.py`. The bridge requires an exact imported binding and a unique
function or class in the provider. It does not follow wildcard imports,
conditional or function-local imports, or multi-level re-export chains.

## Daemon tuning (optional)

The background daemon writes JSON lines to `daemon.log` and rotates it while
Expand Down Expand Up @@ -494,9 +520,14 @@ Scoped tool responses carry a `scope_applied` meta field plus a one-line widen h
- Entry lookup respects `exports` keys, wildcard subpaths and `null` blocks. It probes the selected `import`/`default` target first, then a `types` condition when that file is not indexed. Without `exports`, it tries `types`, `main` and directory indexes. Unsupported-condition subpaths never guess a directory target.
- Bare calls use the caller file's named import binding, including its local alias, not another same-named function anywhere in the provider. A per-binding import links to a symbol marked public by the extractor; otherwise it stays at `external::<specifier>::<name>` with its `npm_import` marker and creates no call binding. Linked imports retain their specifier, survive provider eviction as external imports, and are rechecked by whole-graph resolution.
- **Limits:** Build-output-only entries, such as `main` and `types` pointing into excluded `dist/`, stay external unless an indexed target is declared. Gortex does not infer `src/` from `dist/`. The importing repository's own packages, `npm:` aliases and workspace paths that escape the repository are not linked by this pass. Sibling dependencies declared with `file:`, `link:` or `portal:` are not supported. Default-import bare calls have no exact binding evidence yet and stay unresolved. After an upgrade, JavaScript and TypeScript files are re-extracted once (extractor version 2), so existing imports acquire the specifier and alias metadata; their links appear after that pass. Namespace member calls, CommonJS `require` calls and package re-exports are not linked by this npm import pass. Export targets without `./` are accepted leniently, as in workspace resolution, even though Node rejects them. Call binding is file-scoped: a parameter or local callable with the imported name can bind to the import.
- Go source imports can link to a representative indexed package file. The `go.mod` dependency contract remains separate. Linked imports retain their package identity across provider saves.
- Go package-selector calls such as `library.Hello()` use tracked roots' `go.mod` module paths, not checkout names. An import of `example.org/library/sub` can bind to `<provider>/sub/file.go::Hello` even when the provider prefix is `b`.
- Precise own-module, vendor and staging paths take priority. Otherwise the longest matching module path wins. A primary/non-worktree checkout wins a tie with dedicated worktree copies; ties between independent primary checkouts are refused. Cross-workspace calls still require the caller's `cross_workspace_deps` declaration.
- A cross-repo module binding carries confidence `0.6` and origin `ast_inferred`. It is a source hint, not proof that the tracked checkout matches the required version. The edge retains its symbol-bearing import identity so provider edits and full reindexes can rebind it without editing the caller. Module-map changes re-check existing bindings.
- **Limits:** Only root `go.mod` identities are used; nested modules and `go.work` selection are not resolved. Required versions and module/version replacements are not verified. A local `replace` whose destination is untracked prevents module-path binding instead of selecting another checkout with the same module name. Tracked local replacements remain source hints, not full Go build-list resolution. Calls on values typed from another module, such as `client.Do()` for a `library.Client`, can remain `unresolved::*.Do`; receiver-type linking is not implemented here.
- **Limits:** Only each tracked root's `go.mod` identity is used. Separately tracked nested modules can win by longest path; nested manifests within one tracked root and `go.work` selection are not resolved. Required versions and module/version replacements are not verified. A local `replace` whose destination is untracked prevents module-path binding instead of selecting another checkout with the same module name. Tracked local replacements remain source hints, not full Go build-list resolution. Calls on values typed from another module, such as `client.Do()` for a `library.Client`, stay unresolved unless another pass has precise evidence. Importing the module alone does not enable name-only receiver binding.
- Rust `use` imports link to the declared provider's library root or module file. Dependency, dev-dependency, build-dependency and target tables are read. Calls bind only to a unique imported item or receiver in that file.
- **Cargo limits:** This is not Cargo build resolution. Versions, features and target conditions are not evaluated. Absolute or escaping library paths, conflicting aliases and multiple eligible providers are refused. Re-exports and untyped receivers are not inferred. Module-qualified calls such as `use lib::util; util::action()` are not linked by this bridge. Only direct members of a single group bind, such as `use lib::util::{a, b}`. Nested members inside braces are not linked. Grouped and ungrouped member calls require an indexed file for the exact parent module; an import that falls back to an ancestor file does not authorize those calls. Existing links are not always revisited after declaration or provider-set changes; reindex the provider to refresh them.
- Python distribution imports link only after a consumer declaration and a unique eligible provider match. Qualified calls use the linked module file. Typed methods require a receiver name bound by `from X import Name [as Alias]` and a matching provider method in that file. Plain `import pkg.mod` and wildcard imports never link typed methods. A same-named type defined in the caller file refuses the link. Package calls can follow the one-level re-exports described above.
- **Python limits:** Distribution versions and arbitrary source layouts are not verified. Providers must expose indexed root or `src/` modules or regular packages. Namespace-only providers and dynamic imports are not linked. Local top-level names take priority, including namespace directories. A working link is not reconsidered when a second provider appears, its dependency declaration is dropped, or a new local top-level package shadows it. Reindex the consumer to refresh it.
- **Impact analysis** — `explain_change_impact`, `verify_change`, and `get_test_targets` follow cross-repo edges automatically, grouping results by repository.
- **Shared repos** — the same repo can appear in multiple projects with different reference tags. It's indexed once and shared across projects.
156 changes: 156 additions & 0 deletions internal/indexer/added_file_declaration_gate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package indexer

import (
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/config"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser/languages"
"path/filepath"
"strings"
"testing"
)

func TestPythonAddedFileDeclarationGate(t *testing.T) {
provider, consumer := t.TempDir(), t.TempDir()
writeWorkspaceFile(t, provider, "pyproject.toml", "[project]\nname = 'library'\n")
writeWorkspaceFile(t, consumer, "pyproject.toml", "[project]\nname = 'app'\n")
g := &pythonLookupScanStore{Store: graph.New()}
g.AddEdge(&graph.Edge{From: "consumer/app.py", FilePath: "consumer/app.py", To: "external::pkg.extra", Kind: graph.EdgeImports})
mi := &MultiIndexer{graph: g, repos: map[string]*RepoMetadata{"provider": {RootPath: provider}, "consumer": {RootPath: consumer}}}
for range 2 {
require.Empty(t, mi.pythonAddedFileConsumers([]string{"provider/pkg/extra.py"}))
}
require.Zero(t, g.importScans, "no declaration means no import scan")
writeWorkspaceFile(t, consumer, "pyproject.toml", "[project]\ndependencies = ['library']\n")
require.Equal(t, []string{"consumer/app.py"}, mi.pythonAddedFileConsumers([]string{"provider/pkg/extra.py"}))
require.Equal(t, 1, g.importScans)
require.Zero(t, g.nodeReads, "admission must not build the provider lookup")
writeWorkspaceFile(t, consumer, "pyproject.toml", "[project]\nname = 'app'\n")
g.importScans = 0
require.Empty(t, mi.pythonAddedFileConsumers([]string{"provider/pkg/next.py"}))
require.Zero(t, g.importScans)
// Existing but unreadable snapshot evidence must open admission.
prior := readDiskFile
readDiskFile = func(file string) ([]byte, bool) { return nil, false }
t.Cleanup(func() { readDiskFile = prior })
require.Equal(t, []string{"consumer/app.py"}, mi.pythonAddedFileConsumers([]string{"provider/pkg/extra.py"}))
require.Equal(t, 1, g.importScans)
}

func TestCargoAddedFileDeclarationGate(t *testing.T) {
provider, consumer := t.TempDir(), t.TempDir()
writeWorkspaceFile(t, provider, "Cargo.toml", "[package]\nname = 'library'\n")
writeWorkspaceFile(t, consumer, "Cargo.toml", "[package]\nname = 'app'\n")
g := &cargoWarmScanStore{Store: graph.New()}
g.AddEdge(&graph.Edge{From: "consumer/src/lib.rs", FilePath: "consumer/src/lib.rs", To: "external::library/extra", Kind: graph.EdgeImports})
mi := &MultiIndexer{graph: g, repos: map[string]*RepoMetadata{"provider": {RootPath: provider}, "consumer": {RootPath: consumer}}}
for range 2 {
require.Empty(t, mi.cargoAddedFileConsumers("provider"))
}
require.Zero(t, g.lightScans)
writeWorkspaceFile(t, consumer, "Cargo.toml", "[package]\nname = 'app'\n[dependencies]\nlibrary = '1'\n")
require.Equal(t, []string{"consumer/src/lib.rs"}, mi.cargoAddedFileConsumers("provider"))
require.Equal(t, 1, g.lightScans)
writeWorkspaceFile(t, consumer, "Cargo.toml", "[package]\nname = 'app'\n")
g.lightScans = 0
require.Empty(t, mi.cargoAddedFileConsumers("provider"))
require.Zero(t, g.lightScans)
prior := readDiskFile
readDiskFile = func(file string) ([]byte, bool) { return nil, false }
t.Cleanup(func() { readDiskFile = prior })
require.Equal(t, []string{"consumer/src/lib.rs"}, mi.cargoAddedFileConsumers("provider"))
require.Equal(t, 1, g.lightScans)
}

func TestPythonAddedTestPackageDoesNotScan(t *testing.T) {
provider, consumer := t.TempDir(), t.TempDir()
writeWorkspaceFile(t, provider, "pyproject.toml", "[project]\nname = 'library'\n")
writeWorkspaceFile(t, consumer, "pyproject.toml", "[project]\ndependencies = ['library']\n")
g := &pythonLookupScanStore{Store: graph.New()}
mi := &MultiIndexer{graph: g, repos: map[string]*RepoMetadata{"provider": {RootPath: provider}, "consumer": {RootPath: consumer}}}
require.Empty(t, mi.pythonAddedFileConsumers([]string{"provider/tests/__init__.py", "provider/src/test/helpers.py"}))
require.Zero(t, g.importScans)
}

func TestAddedFileAfterDeclarationMatchesCold(t *testing.T) {
for _, backend := range []string{"memory", "sqlite"} {
for _, lang := range []string{"python", "cargo"} {
t.Run(backend+"/"+lang, func(t *testing.T) {
provider, consumer := t.TempDir(), t.TempDir()
manifest, caller, added := "pyproject.toml", "app.py", "pkg/extra.py"
providerManifest, consumerManifest := "[project]\nname = 'library'\n", "[project]\nname = 'app'\n"
declaration := "dependencies = ['library']\n"
providerSource, consumerSource := "", "from pkg.extra import f\ndef run(): return f()\n"
if lang == "cargo" {
manifest, caller, added = "Cargo.toml", "src/lib.rs", "src/extra.rs"
providerManifest, consumerManifest = "[package]\nname = 'library'\n", "[package]\nname = 'app'\n"
declaration = "[dependencies]\nlibrary = '1'\n"
providerSource, consumerSource = "pub mod extra;\n", "use library::extra::f;\npub fn run() { f(); }\n"
writeWorkspaceFile(t, provider, "src/lib.rs", providerSource)
} else {
writeWorkspaceFile(t, provider, "pkg/__init__.py", providerSource)
}
writeWorkspaceFile(t, provider, manifest, providerManifest)
writeWorkspaceFile(t, consumer, manifest, consumerManifest)
writeWorkspaceFile(t, consumer, caller, consumerSource)
entries := []config.RepoEntry{{Path: consumer, Name: "consumer", Workspace: "shared"}, {Path: provider, Name: "provider", Workspace: "shared"}}
mi, g := newPythonBridgeIdentityIndexer(t, backend, entries)
mi.registry.Register(languages.NewRustExtractor())
_, err := mi.IndexAll()
require.NoError(t, err)
for _, target := range revisionCallTargets(g, "consumer/"+caller, graph.EdgeImports) {
require.False(t, strings.HasPrefix(target, "provider/"))
}
// The declaration changes without recreating the MultiIndexer or resolver registry.
writeWorkspaceFile(t, consumer, manifest, consumerManifest+declaration)
if lang == "cargo" {
providerSource = "pub fn f() {}\n"
} else {
providerSource = "def f(): pass\n"
}
writeWorkspaceFile(t, provider, added, providerSource)
_, err = mi.IncrementalReindexRepo("provider", []string{filepath.Join(provider, added)})
require.NoError(t, err)
targets := revisionCallTargets(g, "consumer/"+caller, graph.EdgeImports)
require.Contains(t, targets, "provider/"+added)
cold, coldGraph := newPythonBridgeIdentityIndexer(t, backend, entries)
cold.registry.Register(languages.NewRustExtractor())
_, err = cold.IndexAll()
require.NoError(t, err)
require.ElementsMatch(t, revisionCallTargets(coldGraph, "consumer/"+caller, graph.EdgeImports), targets)
require.ElementsMatch(t, revisionCallTargets(coldGraph, "consumer/"+caller+"::run", graph.EdgeCalls), revisionCallTargets(g, "consumer/"+caller+"::run", graph.EdgeCalls))
// Removal closes admission for the next addition. Full/retrack recovery is separate.
writeWorkspaceFile(t, consumer, manifest, consumerManifest)
if lang == "cargo" {
require.Empty(t, mi.cargoAddedFileConsumers("provider"))
} else {
require.Empty(t, mi.pythonAddedFileConsumers([]string{"provider/pkg/next.py"}))
}
require.NoError(t, mi.GetIndexer("provider").IndexFile(filepath.Join(provider, added)))
})
}
}
}

func TestCargoRootOwnWorkspaceDependencyIsNotBridge(t *testing.T) {
first, second := t.TempDir(), t.TempDir()
own := "[package]\nname='app'\n[workspace]\nmembers=['member']\n[dependencies]\nmember={path='member'}\n"
writeWorkspaceFile(t, first, "Cargo.toml", own)
writeWorkspaceFile(t, first, "member/Cargo.toml", "[package]\nname='member'\n")
writeWorkspaceFile(t, second, "Cargo.toml", "[package]\nname='other'\n")
g := &cargoWarmScanStore{Store: graph.New()}
g.AddEdge(&graph.Edge{From: "first/src/lib.rs", FilePath: "first/src/lib.rs", To: "external::other/extra", Kind: graph.EdgeImports})
mi := &MultiIndexer{graph: g, repos: map[string]*RepoMetadata{"first": {RootPath: first}, "second": {RootPath: second}}}
require.False(t, mi.cargoManifestSnapshot().cargoDeclared)
require.Empty(t, mi.cargoAddedFileConsumers("first"))
require.Zero(t, g.lightScans, "a local workspace dependency is not a tracked bridge")
writeWorkspaceFile(t, first, "Cargo.toml", own+"other='1'\n")
require.True(t, mi.cargoManifestSnapshot().cargoDeclared)
require.Equal(t, []string{"first/src/lib.rs"}, mi.cargoAddedFileConsumers("second"))
require.Equal(t, 1, g.lightScans)
writeWorkspaceFile(t, first, "Cargo.toml", own)
require.False(t, mi.cargoManifestSnapshot().cargoDeclared)
g.lightScans = 0
require.Empty(t, mi.cargoAddedFileConsumers("first"))
require.Zero(t, g.lightScans)
}
2 changes: 1 addition & 1 deletion internal/indexer/affected_by_bound_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ func TestDeletionShapedStagesCarryNoInEdges(t *testing.T) {
stages := []*incrementalBatchStage{{graphPath: restubDefPath, priorNodes: prior}}
view := loadIncrementalPriorView(st, stages)

carried, _ := restubIncomingRefsFromView(st, stages, view)
carried, _, _, _ := restubIncomingRefsFromView(st, stages, view)
require.Empty(t, carried,
"a deletion stage must park every in-edge; a carried edge has no re-state path here")
assert.Equal(t, 1, st.restubWrites("Foo"),
Expand Down
Loading
Loading