diffctx 1.14.0__tar.gz → 1.15.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {diffctx-1.14.0 → diffctx-1.15.0}/CHANGELOG.md +44 -1
- {diffctx-1.14.0 → diffctx-1.15.0}/Cargo.lock +1 -1
- {diffctx-1.14.0 → diffctx-1.15.0}/PKG-INFO +1 -1
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/Cargo.toml +1 -1
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/base.rs +64 -11
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/generic.rs +45 -1
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/c_family.rs +5 -4
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/jvm.rs +73 -4
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/rust_lang.rs +59 -12
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/structural/containment.rs +16 -8
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/structural/testing.rs +2 -2
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/locate.rs +19 -3
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/parsers/tree_sitter_strategy.rs +51 -7
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/render.rs +50 -8
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/select.rs +72 -1
- {diffctx-1.14.0 → diffctx-1.15.0}/pyproject.toml +1 -1
- diffctx-1.15.0/src/diffctx/version.py +1 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/writer.py +20 -0
- diffctx-1.14.0/src/diffctx/version.py +0 -1
- {diffctx-1.14.0 → diffctx-1.15.0}/Cargo.toml +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/LICENSE +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/README.md +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/README.md +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/analytics.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/candidate_files.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/analytics.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/bm25.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/budget.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/category_weights.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/edge_weights.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/env_overrides.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/extensions.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/filtering.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/fragmentation.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/git.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/graph_filtering.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/importance.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/limits.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/mod.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/mode.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/needs.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/parsers.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/render.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/scoring.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/selection.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/tokenization.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/weights.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/core.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/deadline.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/discovery.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/build_system.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/cicd.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/docker.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/helm.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/kubernetes.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/mod.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/document/mod.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/history/cochange.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/history/mod.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/mod.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/ansible.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/bazel.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/cargo_edges.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/clojure.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/css.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/dart.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/dbt.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/dotnet.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/elixir.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/erlang.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/go.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/graphql.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/haskell.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/javascript.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/julia.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/latex.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/lua.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/mod.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/nim.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/nix.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/ocaml.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/openapi.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/perl.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/php.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/prisma.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/protobuf.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/python.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/r_lang.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/ruby.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/shell.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/sql.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/swift.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/tags.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/terraform.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/zig.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/similarity/lexical.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/similarity/mod.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/structural/mod.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/structural/sibling.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/excerpt.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/filtering.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/fragmentation.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/git.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/graph.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/graph_export.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/interval.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/languages.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/lib.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/main.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/memory_pipeline.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/mode.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/parsers/config_parser.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/parsers/generic.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/parsers/markdown.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/parsers/mod.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/paths.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/peak_rss.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/pipeline.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/postpass.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/ppr.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/project_graph.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/provenance.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/pybridge.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/scoring.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/signatures.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/stopwords.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/test_harness.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/testfiles.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/token_corpus.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/tokenizer.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/types.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/utility/boltzmann.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/utility/importance.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/utility/mod.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/utility/needs.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/utility/scoring.rs +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/rust-toolchain.toml +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/__init__.py +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/__main__.py +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/_app.py +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/_diffctx.pyi +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/_native/__init__.py +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/_native/graph_analytics.py +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/_native/graph_export.py +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/_native/pipeline.py +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/_native/project_graph.py +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/cli.py +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/clipboard.py +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/ignore.py +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/logger.py +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/mcp/README.md +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/mcp/__init__.py +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/mcp/__main__.py +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/mcp/fetch.py +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/mcp/formatting.py +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/mcp/security.py +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/mcp/server.py +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/py.typed +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/tokens.py +0 -0
- {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/tree.py +0 -0
|
@@ -7,6 +7,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Changed files that produced no fragments are disclosed in the md/text
|
|
13
|
+
output** ("Changed files not represented in the output"): a range whose
|
|
14
|
+
selection dropped some changed files no longer hides the gap — the footer
|
|
15
|
+
names exactly the changed files absent from the fragment list. Derived
|
|
16
|
+
from the already policy-clean `changed_files`, so withheld paths can never
|
|
17
|
+
appear in it.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **Path-reference ambiguity cap counted duplicate index keys** (#207): every
|
|
22
|
+
file was indexed under its absolute AND repo-relative path, so
|
|
23
|
+
`MAX_FILES_PER_PATH_REF` (8) effectively halved, and host directory
|
|
24
|
+
components (`/home/runner/work/...`) matched references they had nothing
|
|
25
|
+
to do with. The index now carries one repo-relative posix entry per file.
|
|
26
|
+
- **`<script>` type detection** (#213): `data-type="json"` no longer opts a
|
|
27
|
+
real JavaScript block out of parsing, and a legal MIME parameter
|
|
28
|
+
(`type="text/javascript; charset=utf-8"`) no longer degrades the block to
|
|
29
|
+
raw line windows.
|
|
30
|
+
- **Scala import/package extraction gaps** (#215): Scala 3 renames
|
|
31
|
+
(`import a.b.C as D`, `a.{B as C}`) keep the original name, multi-line
|
|
32
|
+
brace imports keep their selectors, and nested package blocks no longer
|
|
33
|
+
concatenate sibling scopes into a nonexistent package.
|
|
34
|
+
- **Rust `pub use` re-export registration no longer depends on file
|
|
35
|
+
iteration order** (#199): registration now runs after the definition maps
|
|
36
|
+
are complete, so a re-export defined in a later-iterated file still
|
|
37
|
+
resolves (`rust_027_arc_mutex` left the corpus baseline on this fix).
|
|
38
|
+
- **The per-file budget ceiling now also gates signature substitution**
|
|
39
|
+
(#212): a file at its ceiling can no longer keep placing signature stubs
|
|
40
|
+
ahead of other files' cores.
|
|
41
|
+
- **A signature stub substituted for a changed core renders as `changed`**
|
|
42
|
+
(#209): the stub shares the core's location, and both surfaces (pack render
|
|
43
|
+
and locate) now agree it carries the change, same as an excerpt. Ordinary
|
|
44
|
+
context signatures stay context.
|
|
45
|
+
- **Config-key word boundaries treat non-ASCII bytes as word bytes** (#216):
|
|
46
|
+
a config key ending exactly where a non-ASCII letter begins (e.g. inside
|
|
47
|
+
`tokenización`) is no longer a match, restoring the Unicode-`\b` rejection
|
|
48
|
+
the Aho-Corasick rewrite lost.
|
|
49
|
+
|
|
50
|
+
## [1.14.0] - 2026-08-16
|
|
51
|
+
|
|
10
52
|
### Fixed
|
|
11
53
|
|
|
12
54
|
- **Changed secret-classified files (`id_rsa`, `*.key`, `*.pem`, …) are now
|
|
@@ -991,7 +1033,8 @@ Earlier releases shipped as `treemapper`; see
|
|
|
991
1033
|
<https://github.com/nikolay-e/diffctx/releases> for the corresponding GitHub
|
|
992
1034
|
release notes (`1.0.0` through `1.6.1`).
|
|
993
1035
|
|
|
994
|
-
[Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.
|
|
1036
|
+
[Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.14.0...HEAD
|
|
1037
|
+
[1.14.0]: https://github.com/nikolay-e/diffctx/compare/v1.13.0...v1.14.0
|
|
995
1038
|
[1.13.0]: https://github.com/nikolay-e/diffctx/compare/v1.12.3...v1.13.0
|
|
996
1039
|
[1.12.3]: https://github.com/nikolay-e/diffctx/compare/v1.12.2...v1.12.3
|
|
997
1040
|
[1.12.2]: https://github.com/nikolay-e/diffctx/compare/v1.12.1...v1.12.2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "diffctx"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.15.0"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
rust-version = "1.85"
|
|
6
6
|
description = "Selects the minimum code an LLM needs to review a git diff: walks the dependency graph outward from changed lines and stops when extra context stops paying for itself"
|
|
@@ -97,8 +97,8 @@ pub struct FragmentIndex {
|
|
|
97
97
|
/// interior) requires each reference component to appear as a whole path
|
|
98
98
|
/// component, so the last component is a complete candidate filter. Each
|
|
99
99
|
/// entry carries the file's representative fragment: a path reference is a
|
|
100
|
-
/// file-level relation
|
|
101
|
-
/// the
|
|
100
|
+
/// file-level relation and endorses the file, not each sibling (see
|
|
101
|
+
/// `file_representatives` on the accepted reachability trade, #208).
|
|
102
102
|
lower_paths: Vec<(String, FragmentId)>,
|
|
103
103
|
component_to_paths: FxHashMap<String, Vec<u32>>,
|
|
104
104
|
}
|
|
@@ -136,18 +136,29 @@ impl FragmentIndex {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
// One entry per FILE, keyed on the repo-relative posix path (the form
|
|
140
|
+
// path references are written in). Indexing every `by_path` variant put
|
|
141
|
+
// the same file under 2-3 keys, so `MAX_FILES_PER_PATH_REF` counted
|
|
142
|
+
// duplicate keys and silently halved (#207) — and the absolute keys'
|
|
143
|
+
// host directory components (`/home/runner/work/...`) matched
|
|
144
|
+
// references they had nothing to do with.
|
|
139
145
|
let reps = file_representatives(fragments);
|
|
140
|
-
let mut lower_paths: Vec<(String, FragmentId)> = Vec::with_capacity(
|
|
146
|
+
let mut lower_paths: Vec<(String, FragmentId)> = Vec::with_capacity(reps.len());
|
|
141
147
|
let mut component_to_paths: FxHashMap<String, Vec<u32>> = FxHashMap::default();
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
148
|
+
let mut indexed_files: FxHashSet<&str> = FxHashSet::default();
|
|
149
|
+
for f in fragments {
|
|
150
|
+
if !indexed_files.insert(f.path()) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
let Some(rep) = reps.get(f.path()) else {
|
|
147
154
|
continue;
|
|
148
155
|
};
|
|
156
|
+
let matchable = repo_root
|
|
157
|
+
.and_then(|root| Path::new(f.path()).strip_prefix(root).ok())
|
|
158
|
+
.map(|rel| rel.to_string_lossy().replace('\\', "/"))
|
|
159
|
+
.unwrap_or_else(|| f.path().replace('\\', "/"));
|
|
149
160
|
let idx = lower_paths.len() as u32;
|
|
150
|
-
let lower =
|
|
161
|
+
let lower = matchable.to_lowercase();
|
|
151
162
|
for comp in lower.split('/').filter(|c| !c.is_empty()) {
|
|
152
163
|
let posting = component_to_paths.entry(comp.to_string()).or_default();
|
|
153
164
|
if posting.last() != Some(&idx) {
|
|
@@ -174,8 +185,11 @@ impl FragmentIndex {
|
|
|
174
185
|
/// change endorsing a file diffusely must not outweigh a call edge naming one
|
|
175
186
|
/// symbol — the fragment-pair encoding gave both the same weight, which is
|
|
176
187
|
/// simultaneously the quadratic edge blow-up (envoy: 22M c-family edges) and
|
|
177
|
-
/// the file-level noise mechanism of #65.
|
|
178
|
-
///
|
|
188
|
+
/// the file-level noise mechanism of #65. The file's other fragments are
|
|
189
|
+
/// reachable only through nesting and naming edges — the intra-file star is
|
|
190
|
+
/// OFF by default and measured net-negative on the corpus (#208) — an
|
|
191
|
+
/// accepted trade: a file-level relation endorses the file, not each of its
|
|
192
|
+
/// siblings.
|
|
179
193
|
pub fn file_representatives(fragments: &[Fragment]) -> FxHashMap<String, FragmentId> {
|
|
180
194
|
let mut file_to_rep: FxHashMap<String, FragmentId> = FxHashMap::default();
|
|
181
195
|
let mut file_to_token_count: FxHashMap<String, u32> = FxHashMap::default();
|
|
@@ -498,4 +512,43 @@ mod tests {
|
|
|
498
512
|
let hit = linked_paths("pkg/api", &paths);
|
|
499
513
|
assert_eq!(hit, vec!["pkg/api/server.go".to_string()]);
|
|
500
514
|
}
|
|
515
|
+
|
|
516
|
+
fn linked_paths_rooted(reference: &str, root: &str, paths: &[&str]) -> Vec<String> {
|
|
517
|
+
let frags: Vec<Fragment> = paths.iter().map(|p| frag(p)).collect();
|
|
518
|
+
let idx = FragmentIndex::new(&frags, Some(Path::new(root)));
|
|
519
|
+
let src = frag("/ci/work/repo/src/origin.yml");
|
|
520
|
+
let mut edges: EdgeDict = FxHashMap::default();
|
|
521
|
+
link_by_path_match(&src.id, reference, &idx, &mut edges, 0.5, 0.5);
|
|
522
|
+
let mut out: Vec<String> = edges
|
|
523
|
+
.keys()
|
|
524
|
+
.map(|(_, dst)| dst.path.to_string())
|
|
525
|
+
.filter(|p| p != src.id.path.as_ref())
|
|
526
|
+
.collect();
|
|
527
|
+
out.sort();
|
|
528
|
+
out.dedup();
|
|
529
|
+
out
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
#[test]
|
|
533
|
+
fn the_ambiguity_cap_counts_distinct_files_not_index_keys() {
|
|
534
|
+
// 5 distinct files under a rooted index: the pre-#207 code indexed
|
|
535
|
+
// each under its absolute AND relative key, so matched.len() was 10
|
|
536
|
+
// and the cap (8) dropped a reference naming only 5 files.
|
|
537
|
+
let paths: Vec<String> = (0..5)
|
|
538
|
+
.map(|i| format!("/ci/work/repo/lib/util/mod_{i}.py"))
|
|
539
|
+
.collect();
|
|
540
|
+
let refs: Vec<&str> = paths.iter().map(String::as_str).collect();
|
|
541
|
+
let hit = linked_paths_rooted("lib/util", "/ci/work/repo", &refs);
|
|
542
|
+
assert_eq!(hit.len(), 5, "cap must count files, not duplicate keys");
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
#[test]
|
|
546
|
+
fn host_directory_components_are_not_matchable() {
|
|
547
|
+
let paths = ["/ci/work/repo/src/app.rs", "/ci/work/repo/src/db.rs"];
|
|
548
|
+
let hit = linked_paths_rooted("work", "/ci/work/repo", &paths);
|
|
549
|
+
assert!(
|
|
550
|
+
hit.is_empty(),
|
|
551
|
+
"components outside the repo root must not resolve a reference"
|
|
552
|
+
);
|
|
553
|
+
}
|
|
501
554
|
}
|
|
@@ -170,7 +170,13 @@ fn extract_config_keys(suffix: &str, content: &str) -> FxHashSet<String> {
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
fn is_word_byte(b: u8) -> bool {
|
|
173
|
-
|
|
173
|
+
// Non-ASCII bytes count as word bytes: the regex `\b` this check replaced
|
|
174
|
+
// treats a Unicode letter as a word character, so `token` inside
|
|
175
|
+
// `tokenización` was NOT a match (#216). Treating every non-ASCII byte as
|
|
176
|
+
// a word byte is the conservative direction — it can only reject matches
|
|
177
|
+
// adjacent to non-ASCII punctuation that `\b` would have accepted, never
|
|
178
|
+
// admit ones it rejected.
|
|
179
|
+
b.is_ascii_alphanumeric() || b == b'_' || !b.is_ascii()
|
|
174
180
|
}
|
|
175
181
|
|
|
176
182
|
fn build_key_patterns(keys: &FxHashSet<String>) -> Vec<Regex> {
|
|
@@ -403,3 +409,41 @@ impl EdgeBuilder for ConfigToCodeEdgeBuilder {
|
|
|
403
409
|
discovered
|
|
404
410
|
}
|
|
405
411
|
}
|
|
412
|
+
|
|
413
|
+
#[cfg(test)]
|
|
414
|
+
mod tests {
|
|
415
|
+
use super::*;
|
|
416
|
+
use crate::types::{FragmentId, FragmentKind};
|
|
417
|
+
use std::sync::Arc;
|
|
418
|
+
|
|
419
|
+
fn frag(path: &str, content: &str) -> Fragment {
|
|
420
|
+
Fragment {
|
|
421
|
+
id: FragmentId::new(Arc::from(path), 1, 5),
|
|
422
|
+
kind: FragmentKind::Chunk,
|
|
423
|
+
content: Arc::from(content),
|
|
424
|
+
identifiers: FxHashSet::default(),
|
|
425
|
+
token_count: 10,
|
|
426
|
+
symbol_name: None,
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/// #216: the manual boundary check replaced the regex crate's Unicode
|
|
431
|
+
/// `\b`, which treats a non-ASCII letter as a word character. A key ending
|
|
432
|
+
/// exactly where a non-ASCII letter begins is NOT a whole-word match.
|
|
433
|
+
#[test]
|
|
434
|
+
fn a_key_inside_a_non_ascii_word_is_not_a_match() {
|
|
435
|
+
let cfg = frag("app.yaml", "sesion_timeout: 30\n");
|
|
436
|
+
let hit = frag("b.py", "sesion = abrir()\n");
|
|
437
|
+
let miss = frag("a.py", "valor = sesion\u{00e9}s\n");
|
|
438
|
+
let frags = vec![cfg.clone(), hit.clone(), miss.clone()];
|
|
439
|
+
let edges = ConfigToCodeEdgeBuilder.build(&frags, None);
|
|
440
|
+
assert!(
|
|
441
|
+
edges.contains_key(&(cfg.id.clone(), hit.id.clone())),
|
|
442
|
+
"a standalone key occurrence must still link"
|
|
443
|
+
);
|
|
444
|
+
assert!(
|
|
445
|
+
!edges.contains_key(&(cfg.id.clone(), miss.id.clone())),
|
|
446
|
+
"`sesion` inside `sesion\u{00e9}s` is not a word match"
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
@@ -172,8 +172,8 @@ impl EdgeBuilder for CFamilyEdgeBuilder {
|
|
|
172
172
|
|
|
173
173
|
// An include names a file, so its edge lands on the file's
|
|
174
174
|
// representative fragment (base::file_representatives — the sibling
|
|
175
|
-
// builder's long-standing semantics
|
|
176
|
-
//
|
|
175
|
+
// builder's long-standing semantics; see its doc on the accepted
|
|
176
|
+
// reachability trade, #208). Buckets therefore hold file paths, and
|
|
177
177
|
// ambiguity is measured in files: a basename repeated across the tree
|
|
178
178
|
// is what makes an include unresolvable.
|
|
179
179
|
// Representatives keyed off the full slice: is_c_family is per-path, so
|
|
@@ -362,8 +362,9 @@ impl EdgeBuilder for CFamilyEdgeBuilder {
|
|
|
362
362
|
}
|
|
363
363
|
|
|
364
364
|
// A header/impl pair is a relation between two files; representatives
|
|
365
|
-
// carry it
|
|
366
|
-
//
|
|
365
|
+
// carry it (see base::file_representatives on the accepted
|
|
366
|
+
// reachability trade, #208). Pairing every fragment with every
|
|
367
|
+
// fragment restated the same fact quadratically.
|
|
367
368
|
for (i, (_key, (files, _))) in by_stem.iter().enumerate() {
|
|
368
369
|
crate::deadline::check_current_every(i, 256, "edge construction (c_family pairing)");
|
|
369
370
|
if files.len() < 2 {
|
|
@@ -187,10 +187,15 @@ fn parse_scala_import_clause(clause: &str) -> Option<ScalaImport> {
|
|
|
187
187
|
let mut selectors = Vec::new();
|
|
188
188
|
let mut wildcard = false;
|
|
189
189
|
for sel in inner.split(',') {
|
|
190
|
+
// A rename keeps the ORIGINAL name — that is what the target file
|
|
191
|
+
// defines. Scala 2 spells it `C => D`, Scala 3 `C as D`.
|
|
190
192
|
let name = sel
|
|
191
193
|
.split("=>")
|
|
192
194
|
.next()
|
|
193
195
|
.unwrap_or("")
|
|
196
|
+
.split(" as ")
|
|
197
|
+
.next()
|
|
198
|
+
.unwrap_or("")
|
|
194
199
|
.trim()
|
|
195
200
|
.trim_matches('`');
|
|
196
201
|
match name {
|
|
@@ -211,6 +216,8 @@ fn parse_scala_import_clause(clause: &str) -> Option<ScalaImport> {
|
|
|
211
216
|
wildcard,
|
|
212
217
|
});
|
|
213
218
|
}
|
|
219
|
+
// Scala 3 top-level rename: `import a.b.c as d` imports `a.b.c`.
|
|
220
|
+
let clause = clause.split(" as ").next().unwrap_or(clause).trim();
|
|
214
221
|
if let Some(p) = clause
|
|
215
222
|
.strip_suffix("._")
|
|
216
223
|
.or_else(|| clause.strip_suffix(".*"))
|
|
@@ -248,9 +255,30 @@ fn parse_scala_import_clause(clause: &str) -> Option<ScalaImport> {
|
|
|
248
255
|
|
|
249
256
|
fn parse_scala_imports(content: &str) -> Vec<ScalaImport> {
|
|
250
257
|
let mut out = Vec::new();
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
258
|
+
let mut lines = content.lines();
|
|
259
|
+
while let Some(line) = lines.next() {
|
|
260
|
+
let Some(cap) = SCALA_IMPORT_LINE_RE.captures(line) else {
|
|
261
|
+
continue;
|
|
262
|
+
};
|
|
263
|
+
let mut clause_text = cap[1].split("//").next().unwrap_or("").trim().to_string();
|
|
264
|
+
// A brace group may span lines; join until the braces balance so
|
|
265
|
+
// `import scala.collection.{\n mutable,\n immutable\n}` keeps its
|
|
266
|
+
// selectors instead of yielding an empty list.
|
|
267
|
+
let mut open = clause_text.matches('{').count();
|
|
268
|
+
let mut close = clause_text.matches('}').count();
|
|
269
|
+
let mut joined = 0;
|
|
270
|
+
// The join is bounded so an unbalanced brace (a file mid-edit) cannot
|
|
271
|
+
// swallow the rest of the file into one clause.
|
|
272
|
+
while open > close && joined < 32 {
|
|
273
|
+
joined += 1;
|
|
274
|
+
let Some(next) = lines.next() else { break };
|
|
275
|
+
let next = next.split("//").next().unwrap_or("").trim();
|
|
276
|
+
open += next.matches('{').count();
|
|
277
|
+
close += next.matches('}').count();
|
|
278
|
+
clause_text.push(' ');
|
|
279
|
+
clause_text.push_str(next);
|
|
280
|
+
}
|
|
281
|
+
for clause in split_top_level_commas(&clause_text) {
|
|
254
282
|
if let Some(imp) = parse_scala_import_clause(clause) {
|
|
255
283
|
out.push(imp);
|
|
256
284
|
}
|
|
@@ -315,7 +343,10 @@ fn extract_package(content: &str, path: &Path) -> Option<String> {
|
|
|
315
343
|
if is_scala(path) {
|
|
316
344
|
// Scala chains package clauses (`package com.acme` then `package svc`
|
|
317
345
|
// means com.acme.svc); `package object x` is a member of the enclosing
|
|
318
|
-
// package, not a chain segment.
|
|
346
|
+
// package, not a chain segment. A block-form clause (`package util {`)
|
|
347
|
+
// opens a scope whose SIBLINGS are not chain segments — concatenating
|
|
348
|
+
// past it produced nonexistent packages (`com.example.util.data`), so
|
|
349
|
+
// the chain stops at the first block.
|
|
319
350
|
let mut parts: Vec<String> = Vec::new();
|
|
320
351
|
for cap in SCALA_PACKAGE_RE.captures_iter(content) {
|
|
321
352
|
let seg = &cap[1];
|
|
@@ -323,6 +354,10 @@ fn extract_package(content: &str, path: &Path) -> Option<String> {
|
|
|
323
354
|
continue;
|
|
324
355
|
}
|
|
325
356
|
parts.push(seg.to_string());
|
|
357
|
+
let after = content[cap.get(0).map_or(0, |m| m.end())..].trim_start();
|
|
358
|
+
if after.starts_with('{') {
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
326
361
|
}
|
|
327
362
|
if parts.is_empty() {
|
|
328
363
|
None
|
|
@@ -875,6 +910,40 @@ mod tests {
|
|
|
875
910
|
assert!(object_rooted.wildcard);
|
|
876
911
|
}
|
|
877
912
|
|
|
913
|
+
#[test]
|
|
914
|
+
fn scala3_renames_keep_the_original_name() {
|
|
915
|
+
let top = parse_one("a.b.Conf as Config");
|
|
916
|
+
assert_eq!(top.prefix, "a.b");
|
|
917
|
+
assert_eq!(top.selectors, vec!["Conf".to_string()]);
|
|
918
|
+
|
|
919
|
+
let braced = parse_one("a.{B as C, D}");
|
|
920
|
+
assert_eq!(braced.prefix, "a");
|
|
921
|
+
assert_eq!(braced.selectors, vec!["B".to_string(), "D".to_string()]);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
#[test]
|
|
925
|
+
fn scala_multiline_brace_import_keeps_its_selectors() {
|
|
926
|
+
let content = "import scala.collection.{\n mutable,\n immutable\n}\nimport a.B\n";
|
|
927
|
+
let imports = parse_scala_imports(content);
|
|
928
|
+
assert_eq!(imports.len(), 2);
|
|
929
|
+
assert_eq!(imports[0].prefix, "scala.collection");
|
|
930
|
+
assert_eq!(
|
|
931
|
+
imports[0].selectors,
|
|
932
|
+
vec!["mutable".to_string(), "immutable".to_string()]
|
|
933
|
+
);
|
|
934
|
+
assert_eq!(imports[1].selectors, vec!["B".to_string()]);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
#[test]
|
|
938
|
+
fn scala_nested_package_blocks_do_not_concatenate_siblings() {
|
|
939
|
+
let content =
|
|
940
|
+
"package com.example\npackage util {\n class A\n}\npackage data {\n class B\n}\n";
|
|
941
|
+
assert_eq!(
|
|
942
|
+
extract_package(content, Path::new("a.scala")),
|
|
943
|
+
Some("com.example.util".to_string())
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
|
|
878
947
|
#[test]
|
|
879
948
|
fn scala_chained_packages_join_and_package_object_is_skipped() {
|
|
880
949
|
let content = "package com.acme\npackage service\n\nclass X {}\n";
|
|
@@ -259,25 +259,34 @@ impl EdgeBuilder for RustEdgeBuilder {
|
|
|
259
259
|
if !impls.is_empty() {
|
|
260
260
|
trait_impls.insert(f.id.clone(), impls);
|
|
261
261
|
}
|
|
262
|
+
}
|
|
262
263
|
|
|
264
|
+
// Re-export registration runs over the COMPLETE definition maps:
|
|
265
|
+
// inline in the loop above it saw only files iterated before the
|
|
266
|
+
// re-exporting one, so which `pub use` leaves resolved depended on
|
|
267
|
+
// file iteration order (#199). Registrations are collected before
|
|
268
|
+
// insertion so re-exporters do not shadow each other either.
|
|
269
|
+
let mut pub_use_regs: Vec<(String, FragmentId)> = Vec::new();
|
|
270
|
+
for f in &rust_frags {
|
|
263
271
|
for pub_use_path in extract_pub_uses(&f.content) {
|
|
264
272
|
let leaf_lower = pub_use_path.split("::").last().unwrap_or("").to_lowercase();
|
|
265
|
-
if
|
|
266
|
-
|
|
273
|
+
if leaf_lower.is_empty() || name_to_frags.contains_key(&leaf_lower) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
let has_target = type_defs
|
|
277
|
+
.get(&leaf_lower)
|
|
278
|
+
.map_or(false, |v| v.iter().any(|fid| fid != &f.id))
|
|
279
|
+
|| fn_defs
|
|
267
280
|
.get(&leaf_lower)
|
|
268
|
-
.map_or(false, |v| v.iter().any(|fid| fid != &f.id))
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
.map_or(false, |v| v.iter().any(|fid| fid != &f.id));
|
|
272
|
-
if has_target {
|
|
273
|
-
name_to_frags
|
|
274
|
-
.entry(leaf_lower)
|
|
275
|
-
.or_default()
|
|
276
|
-
.push(f.id.clone());
|
|
277
|
-
}
|
|
281
|
+
.map_or(false, |v| v.iter().any(|fid| fid != &f.id));
|
|
282
|
+
if has_target {
|
|
283
|
+
pub_use_regs.push((leaf_lower, f.id.clone()));
|
|
278
284
|
}
|
|
279
285
|
}
|
|
280
286
|
}
|
|
287
|
+
for (leaf_lower, fid) in pub_use_regs {
|
|
288
|
+
name_to_frags.entry(leaf_lower).or_default().push(fid);
|
|
289
|
+
}
|
|
281
290
|
|
|
282
291
|
let mut edges: EdgeDict = FxHashMap::default();
|
|
283
292
|
// Same ambiguity bar as CFamilySemanticWeights::max_files_per_name: a
|
|
@@ -520,3 +529,41 @@ impl EdgeBuilder for RustEdgeBuilder {
|
|
|
520
529
|
result
|
|
521
530
|
}
|
|
522
531
|
}
|
|
532
|
+
|
|
533
|
+
#[cfg(test)]
|
|
534
|
+
mod tests {
|
|
535
|
+
use super::*;
|
|
536
|
+
use crate::types::FragmentKind;
|
|
537
|
+
|
|
538
|
+
fn frag(path: &str, content: &str) -> Fragment {
|
|
539
|
+
Fragment {
|
|
540
|
+
id: FragmentId::new(std::sync::Arc::from(path), 1, 20),
|
|
541
|
+
kind: FragmentKind::Function,
|
|
542
|
+
content: std::sync::Arc::from(content),
|
|
543
|
+
identifiers: FxHashSet::default(),
|
|
544
|
+
token_count: 20,
|
|
545
|
+
symbol_name: None,
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
#[test]
|
|
550
|
+
fn pub_use_registration_does_not_depend_on_file_iteration_order() {
|
|
551
|
+
// The re-exporting file iterates FIRST, the defining file after it —
|
|
552
|
+
// exactly the order that dropped the registration before #199.
|
|
553
|
+
let facade = frag("src/facade.rs", "pub use detail::WidgetFactory;\n");
|
|
554
|
+
let detail = frag(
|
|
555
|
+
"src/z_detail.rs",
|
|
556
|
+
"pub struct WidgetFactory {\n size: u32,\n}\n",
|
|
557
|
+
);
|
|
558
|
+
let consumer = frag(
|
|
559
|
+
"src/consumer.rs",
|
|
560
|
+
"use crate::api::WidgetFactory;\n\nfn run() {}\n",
|
|
561
|
+
);
|
|
562
|
+
let frags = vec![facade.clone(), detail, consumer.clone()];
|
|
563
|
+
let edges = RustEdgeBuilder.build(&frags, None);
|
|
564
|
+
assert!(
|
|
565
|
+
edges.contains_key(&(consumer.id.clone(), facade.id.clone())),
|
|
566
|
+
"a use of the re-exported leaf must resolve to the re-exporting file"
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
@@ -22,7 +22,15 @@ impl EdgeBuilder for ContainmentEdgeBuilder {
|
|
|
22
22
|
|
|
23
23
|
let mut edges: EdgeDict = FxHashMap::default();
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
// Computed only for the experimental star below — OFF by default, and
|
|
26
|
+
// measured net-negative when ON (#208): 21 corpus regressions vs 14
|
|
27
|
+
// improvements, 2026-08-16 A/B at shipped defaults.
|
|
28
|
+
let star_on = std::env::var_os("DIFFCTX_FILE_STAR").is_some();
|
|
29
|
+
let reps = if star_on {
|
|
30
|
+
super::super::base::file_representatives(fragments)
|
|
31
|
+
} else {
|
|
32
|
+
FxHashMap::default()
|
|
33
|
+
};
|
|
26
34
|
|
|
27
35
|
for (path, frags) in &by_path {
|
|
28
36
|
if frags.len() < 2 {
|
|
@@ -58,13 +66,13 @@ impl EdgeBuilder for ContainmentEdgeBuilder {
|
|
|
58
66
|
stack.push(f);
|
|
59
67
|
}
|
|
60
68
|
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
if
|
|
69
|
+
// Experimental intra-file spine: connects every fragment to the
|
|
70
|
+
// file representative so top-level siblings become mutually
|
|
71
|
+
// reachable. Ships OFF — the 2026-08-16 corpus A/B measured it
|
|
72
|
+
// net-negative (#208) — so by default a flat file's siblings are
|
|
73
|
+
// reachable only through nesting and naming edges, and the
|
|
74
|
+
// representative-only builders accept that.
|
|
75
|
+
if star_on {
|
|
68
76
|
if let Some(rep) = reps.get(*path) {
|
|
69
77
|
for f in &sorted {
|
|
70
78
|
if f.id != *rep {
|
|
@@ -120,8 +120,8 @@ impl EdgeBuilder for TestEdgeBuilder {
|
|
|
120
120
|
let mut edges: EdgeDict = FxHashMap::default();
|
|
121
121
|
|
|
122
122
|
// The naming convention names a FILE, so the relation is carried by
|
|
123
|
-
// the two files' representatives (base::file_representatives
|
|
124
|
-
//
|
|
123
|
+
// the two files' representatives (base::file_representatives; see its
|
|
124
|
+
// doc on the accepted reachability trade, #208). One pair per
|
|
125
125
|
// (test file, source file), not per fragment pair.
|
|
126
126
|
let mut linked_file_pairs: FxHashSet<(&str, &str)> = FxHashSet::default();
|
|
127
127
|
|
|
@@ -226,13 +226,28 @@ fn rel_path(state: &ScoredState, path: &str) -> String {
|
|
|
226
226
|
.unwrap_or_else(|| crate::paths::to_posix_display(std::borrow::Cow::Borrowed(path)))
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
/// Same predicate on both surfaces (pack render agrees via
|
|
230
|
+
/// `render::carries_changed_role`): the fragment is a core, an excerpt
|
|
231
|
+
/// stand-in, or a signature stub substituted at a core's location (#209).
|
|
232
|
+
fn carries_change(
|
|
233
|
+
state: &ScoredState,
|
|
234
|
+
frag: &Fragment,
|
|
235
|
+
core_locs: &rustc_hash::FxHashSet<(std::sync::Arc<str>, u32)>,
|
|
236
|
+
) -> bool {
|
|
237
|
+
state.core_ids.contains(&frag.id)
|
|
238
|
+
|| frag.kind == crate::types::FragmentKind::Excerpt
|
|
239
|
+
|| (frag.kind.is_signature()
|
|
240
|
+
&& core_locs.contains(&(frag.id.path.clone(), frag.id.start_line)))
|
|
241
|
+
}
|
|
242
|
+
|
|
229
243
|
fn reasons_for(
|
|
230
244
|
state: &ScoredState,
|
|
231
245
|
frag: &Fragment,
|
|
246
|
+
core_locs: &rustc_hash::FxHashSet<(std::sync::Arc<str>, u32)>,
|
|
232
247
|
hops: Option<u32>,
|
|
233
248
|
attribution: Option<&Vec<(String, String, f64)>>,
|
|
234
249
|
) -> Vec<Reason> {
|
|
235
|
-
if state
|
|
250
|
+
if carries_change(state, frag, core_locs) {
|
|
236
251
|
return vec![Reason::Changed];
|
|
237
252
|
}
|
|
238
253
|
let mut reasons: Vec<Reason> = Vec::new();
|
|
@@ -456,12 +471,12 @@ pub fn build_locate(state: &ScoredState, outcome: &SelectionOutcome) -> LocateOu
|
|
|
456
471
|
let attribution = incoming_attribution(state);
|
|
457
472
|
let (overflow, overflow_count, next_up) = build_overflow(state, outcome, &hops, &attribution);
|
|
458
473
|
|
|
474
|
+
let core_locs = crate::render::core_substitute_locs(&state.core_ids);
|
|
459
475
|
let items: Vec<LocateItem> = outcome
|
|
460
476
|
.selected
|
|
461
477
|
.iter()
|
|
462
478
|
.map(|frag| {
|
|
463
|
-
let is_changed = state
|
|
464
|
-
|| frag.kind == crate::types::FragmentKind::Excerpt;
|
|
479
|
+
let is_changed = carries_change(state, frag, &core_locs);
|
|
465
480
|
let path = rel_path(state, frag.id.path.as_ref());
|
|
466
481
|
let group = group_of(&path, frag.kind);
|
|
467
482
|
LocateItem {
|
|
@@ -479,6 +494,7 @@ pub fn build_locate(state: &ScoredState, outcome: &SelectionOutcome) -> LocateOu
|
|
|
479
494
|
reasons: reasons_for(
|
|
480
495
|
state,
|
|
481
496
|
frag,
|
|
497
|
+
&core_locs,
|
|
482
498
|
hops.get(&frag.id).copied(),
|
|
483
499
|
attribution.get(&frag.id),
|
|
484
500
|
),
|