codingest 0.1.2__tar.gz → 0.1.3__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.
Files changed (76) hide show
  1. {codingest-0.1.2 → codingest-0.1.3}/Cargo.lock +5 -4
  2. {codingest-0.1.2 → codingest-0.1.3}/Cargo.toml +2 -2
  3. {codingest-0.1.2 → codingest-0.1.3}/PKG-INFO +14 -7
  4. {codingest-0.1.2 → codingest-0.1.3}/README.md +13 -6
  5. codingest-0.1.3/codingest/mcp_server.py +38 -0
  6. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/builder/call_edges.rs +22 -0
  7. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/builder/load/edge_frames.rs +176 -2
  8. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/builder/load/entity_frames.rs +58 -4
  9. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/builder/load.rs +71 -2
  10. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/builder/mod.rs +46 -0
  11. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/builder/other_edges.rs +16 -0
  12. codingest-0.1.3/crates/codingest/src/builder/semantic_edges.rs +530 -0
  13. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/models.rs +91 -0
  14. codingest-0.1.3/crates/codingest/src/parsers/agc.rs +984 -0
  15. codingest-0.1.3/crates/codingest/tests/agc_apollo.rs +116 -0
  16. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/tests/goldens/README.md +3 -2
  17. codingest-0.1.3/crates/codingest/tests/goldens/agc_basic.sha256 +1 -0
  18. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest-cli/skills/codingest-code-review/SKILL.md +2 -2
  19. codingest-0.1.3/crates/codingest-mcp/Cargo.toml +19 -0
  20. codingest-0.1.3/crates/codingest-mcp/src/lib.rs +99 -0
  21. codingest-0.1.3/crates/codingest-mcp/src/main.rs +5 -0
  22. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest-py/Cargo.toml +5 -0
  23. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest-py/src/lib.rs +15 -0
  24. {codingest-0.1.2 → codingest-0.1.3}/pyproject.toml +6 -5
  25. codingest-0.1.2/crates/codingest/src/parsers/agc.rs +0 -522
  26. codingest-0.1.2/crates/codingest/tests/goldens/agc_basic.sha256 +0 -1
  27. {codingest-0.1.2 → codingest-0.1.3}/LICENSE +0 -0
  28. {codingest-0.1.2 → codingest-0.1.3}/codingest/__init__.py +0 -0
  29. {codingest-0.1.2 → codingest-0.1.3}/codingest/__init__.pyi +0 -0
  30. {codingest-0.1.2 → codingest-0.1.3}/codingest/cli.py +0 -0
  31. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/Cargo.toml +0 -0
  32. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/bin/codingest_bench.rs +0 -0
  33. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/bin/codingest_stats.rs +0 -0
  34. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/builder/routes/django.rs +0 -0
  35. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/builder/routes/fastapi.rs +0 -0
  36. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/builder/routes/flask.rs +0 -0
  37. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/builder/routes/mod.rs +0 -0
  38. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/builder/type_edges.rs +0 -0
  39. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/cross_lang.rs +0 -0
  40. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/docs/mod.rs +0 -0
  41. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/docs/rst.rs +0 -0
  42. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/lib.rs +0 -0
  43. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/manifest/mod.rs +0 -0
  44. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/parsers/cpp.rs +0 -0
  45. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/parsers/csharp.rs +0 -0
  46. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/parsers/css.rs +0 -0
  47. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/parsers/dart.rs +0 -0
  48. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/parsers/go.rs +0 -0
  49. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/parsers/html.rs +0 -0
  50. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/parsers/java.rs +0 -0
  51. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/parsers/mod.rs +0 -0
  52. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/parsers/php.rs +0 -0
  53. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/parsers/python.rs +0 -0
  54. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/parsers/registry.rs +0 -0
  55. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/parsers/rust_lang.rs +0 -0
  56. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/parsers/shared.rs +0 -0
  57. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/parsers/swift.rs +0 -0
  58. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/parsers/typescript.rs +0 -0
  59. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/repo.rs +0 -0
  60. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/src/rev.rs +0 -0
  61. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/tests/goldens/cross_ts_py.sha256 +0 -0
  62. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/tests/goldens/dup_minified_assets.sha256 +0 -0
  63. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/tests/goldens/py_basic.sha256 +0 -0
  64. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/tests/goldens/py_inheritance.sha256 +0 -0
  65. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/tests/goldens/rust_xfile.sha256 +0 -0
  66. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/tests/goldens/ts_callback.sha256 +0 -0
  67. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest/tests/parity.rs +0 -0
  68. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest-cli/Cargo.toml +0 -0
  69. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest-cli/skills/codingest-code-review/references/mcp-upgrade.md +0 -0
  70. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest-cli/skills/codingest-code-review/references/public-repositories.md +0 -0
  71. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest-cli/skills/codingest-code-review/references/queries.md +0 -0
  72. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest-cli/src/code_tree_cli.rs +0 -0
  73. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest-cli/src/lib.rs +0 -0
  74. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest-cli/src/main.rs +0 -0
  75. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest-cli/src/skill.rs +0 -0
  76. {codingest-0.1.2 → codingest-0.1.3}/crates/codingest-cli/src/skill_assets.rs +0 -0
@@ -313,7 +313,7 @@ dependencies = [
313
313
 
314
314
  [[package]]
315
315
  name = "codingest"
316
- version = "0.1.2"
316
+ version = "0.1.3"
317
317
  dependencies = [
318
318
  "aho-corasick",
319
319
  "base64",
@@ -345,7 +345,7 @@ dependencies = [
345
345
 
346
346
  [[package]]
347
347
  name = "codingest-cli"
348
- version = "0.1.2"
348
+ version = "0.1.3"
349
349
  dependencies = [
350
350
  "anyhow",
351
351
  "clap",
@@ -357,7 +357,7 @@ dependencies = [
357
357
 
358
358
  [[package]]
359
359
  name = "codingest-mcp"
360
- version = "0.1.2"
360
+ version = "0.1.3"
361
361
  dependencies = [
362
362
  "anyhow",
363
363
  "codingest",
@@ -366,10 +366,11 @@ dependencies = [
366
366
 
367
367
  [[package]]
368
368
  name = "codingest-py"
369
- version = "0.1.2"
369
+ version = "0.1.3"
370
370
  dependencies = [
371
371
  "codingest",
372
372
  "codingest-cli",
373
+ "codingest-mcp",
373
374
  "kglite",
374
375
  "pyo3",
375
376
  "tempfile",
@@ -1,9 +1,9 @@
1
1
  [workspace]
2
- members = ["crates/codingest", "crates/codingest-cli", "crates/codingest-py"]
2
+ members = ["crates/codingest", "crates/codingest-cli", "crates/codingest-mcp", "crates/codingest-py"]
3
3
  resolver = "2"
4
4
 
5
5
  [workspace.package]
6
- version = "0.1.2"
6
+ version = "0.1.3"
7
7
  edition = "2021"
8
8
  rust-version = "1.88"
9
9
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codingest
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Programming Language :: Python :: 3
@@ -70,10 +70,13 @@ a real `kglite.KnowledgeGraph`, ready for Cypher queries.
70
70
  - **Revision-aware:** analyse a tag, branch, commit, or multiple revisions in
71
71
  one graph and query what changed.
72
72
 
73
+ AGC assembly receives architecture-aware control and data relationships rather
74
+ than a generic text-level call graph; see the [AGC graph model](https://codingest.readthedocs.io/en/latest/agc-assembly.html).
75
+
73
76
  ## Give your agent a code-review MCP server
74
77
 
75
78
  ```bash
76
- cargo install codingest-mcp
79
+ pip install codingest
77
80
  codingest-mcp --watch /absolute/path/to/repo
78
81
  ```
79
82
 
@@ -172,14 +175,18 @@ for direct Cypher analysis.
172
175
  ## Install options
173
176
 
174
177
  ```bash
175
- pip install codingest # Python API + codingest CLI + KGLite runtime
176
- cargo install codingest-cli # pure-Rust builder CLI
177
- cargo install codingest-mcp # MCP server for agents
178
+ pip install codingest # Python API + CLI + MCP server + KGLite runtime
178
179
  codingest skill install # code-review Agent Skill
179
180
  ```
180
181
 
181
- The wheel bundles every grammar and the `codingest` command. KGLite ≥0.14.5 is
182
- installed automatically as the query/storage engine.
182
+ The wheel bundles every grammar plus the `codingest` and `codingest-mcp`
183
+ commands. KGLite ≥0.14.5 is installed automatically as the query/storage
184
+ engine; its MCP server and the transitive `mcp-methods` framework power the
185
+ builder-aware Codingest server. Nothing else needs to be installed.
186
+
187
+ Rust-only environments can instead use `cargo install codingest-cli
188
+ codingest-mcp`; these are alternative distributions of the same commands, not
189
+ Python prerequisites.
183
190
 
184
191
  ### Rust crate
185
192
 
@@ -43,10 +43,13 @@ a real `kglite.KnowledgeGraph`, ready for Cypher queries.
43
43
  - **Revision-aware:** analyse a tag, branch, commit, or multiple revisions in
44
44
  one graph and query what changed.
45
45
 
46
+ AGC assembly receives architecture-aware control and data relationships rather
47
+ than a generic text-level call graph; see the [AGC graph model](https://codingest.readthedocs.io/en/latest/agc-assembly.html).
48
+
46
49
  ## Give your agent a code-review MCP server
47
50
 
48
51
  ```bash
49
- cargo install codingest-mcp
52
+ pip install codingest
50
53
  codingest-mcp --watch /absolute/path/to/repo
51
54
  ```
52
55
 
@@ -145,14 +148,18 @@ for direct Cypher analysis.
145
148
  ## Install options
146
149
 
147
150
  ```bash
148
- pip install codingest # Python API + codingest CLI + KGLite runtime
149
- cargo install codingest-cli # pure-Rust builder CLI
150
- cargo install codingest-mcp # MCP server for agents
151
+ pip install codingest # Python API + CLI + MCP server + KGLite runtime
151
152
  codingest skill install # code-review Agent Skill
152
153
  ```
153
154
 
154
- The wheel bundles every grammar and the `codingest` command. KGLite ≥0.14.5 is
155
- installed automatically as the query/storage engine.
155
+ The wheel bundles every grammar plus the `codingest` and `codingest-mcp`
156
+ commands. KGLite ≥0.14.5 is installed automatically as the query/storage
157
+ engine; its MCP server and the transitive `mcp-methods` framework power the
158
+ builder-aware Codingest server. Nothing else needs to be installed.
159
+
160
+ Rust-only environments can instead use `cargo install codingest-cli
161
+ codingest-mcp`; these are alternative distributions of the same commands, not
162
+ Python prerequisites.
156
163
 
157
164
  ### Rust crate
158
165
 
@@ -0,0 +1,38 @@
1
+ """Console-script shim for the Rust ``codingest-mcp`` server in the wheel.
2
+
3
+ Codingest supplies the source-builder hooks; KGlite supplies graph/Cypher
4
+ serving and pulls the generic MCP lifecycle from ``mcp-methods``. All three are
5
+ linked into the wheel's existing native extension, so users need only
6
+ ``pip install codingest``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import sys
14
+
15
+
16
+ def main(argv: list[str] | None = None) -> int:
17
+ """Run the bundled MCP server with ``argv`` or ``sys.argv[1:]``."""
18
+ from codingest.codingest import _run_mcp_server
19
+
20
+ args = list(sys.argv[1:] if argv is None else argv)
21
+ # In a wheel console script, current_exe() is Python rather than the Rust
22
+ # server. Give KGlite's selftest the exact command needed to re-enter this
23
+ # module with the same interpreter and environment.
24
+ os.environ["KGLITE_MCP_RESPAWN"] = json.dumps(
25
+ [sys.executable, "-m", "codingest.mcp_server"]
26
+ )
27
+ try:
28
+ _run_mcp_server(args)
29
+ except KeyboardInterrupt:
30
+ return 130
31
+ except RuntimeError as exc:
32
+ print(f"codingest-mcp: {exc}", file=sys.stderr)
33
+ return 1
34
+ return 0
35
+
36
+
37
+ if __name__ == "__main__":
38
+ raise SystemExit(main())
@@ -19,6 +19,12 @@ pub struct CallEdge {
19
19
  /// Comma-separated sorted unique line numbers.
20
20
  pub call_lines: String,
21
21
  pub call_count: i64,
22
+ /// Optional parser-preserved transfer spelling/mechanism. Empty for the
23
+ /// ordinary language call resolver, populated by typed semantic sites.
24
+ pub raw_targets: Option<String>,
25
+ pub offsets: Option<String>,
26
+ pub via: Option<String>,
27
+ pub address_lines: Option<String>,
22
28
  }
23
29
 
24
30
  /// Aggregate counters describing how the resolver classified every call
@@ -48,6 +54,18 @@ pub struct CallResolutionStats {
48
54
  pub resolved_via_inheritance: u64,
49
55
  }
50
56
 
57
+ impl CallResolutionStats {
58
+ pub(crate) fn merge(&mut self, other: Self) {
59
+ self.total_calls += other.total_calls;
60
+ self.excluded_noise += other.excluded_noise;
61
+ self.no_candidate += other.no_candidate;
62
+ self.ambiguous_dropped += other.ambiguous_dropped;
63
+ self.resolved_call_sites += other.resolved_call_sites;
64
+ self.resolved_edges += other.resolved_edges;
65
+ self.resolved_via_inheritance += other.resolved_via_inheritance;
66
+ }
67
+ }
68
+
51
69
  /// Per-function scratch counters, summed into [`CallResolutionStats`] after
52
70
  /// the parallel match loop. Kept `Copy` so the rayon reduce stays alloc-free.
53
71
  #[derive(Debug, Clone, Copy, Default)]
@@ -495,6 +513,10 @@ pub fn build_call_edges(
495
513
  callee: callee.to_string(),
496
514
  call_lines,
497
515
  call_count: count,
516
+ raw_targets: None,
517
+ offsets: None,
518
+ via: None,
519
+ address_lines: None,
498
520
  }
499
521
  })
500
522
  .collect();
@@ -88,11 +88,100 @@ pub(super) fn call_edges_df(edges: &[super::super::call_edges::CallEdge]) -> Dat
88
88
  let callee: Vec<Option<String>> = edges.iter().map(|e| Some(e.callee.clone())).collect();
89
89
  let lines: Vec<Option<String>> = edges.iter().map(|e| Some(e.call_lines.clone())).collect();
90
90
  let count: Vec<Option<i64>> = edges.iter().map(|e| Some(e.call_count)).collect();
91
- build_df(vec![
91
+ let mut columns = vec![
92
92
  ("caller", ColumnType::String, str_col(caller)),
93
93
  ("callee", ColumnType::String, str_col(callee)),
94
94
  ("call_lines", ColumnType::String, str_col(lines)),
95
95
  ("call_count", ColumnType::Int64, int_col(count)),
96
+ ];
97
+ if edges.iter().any(|edge| edge.raw_targets.is_some()) {
98
+ columns.push((
99
+ "raw_targets",
100
+ ColumnType::String,
101
+ str_col(edges.iter().map(|edge| edge.raw_targets.clone()).collect()),
102
+ ));
103
+ }
104
+ if edges.iter().any(|edge| edge.offsets.is_some()) {
105
+ columns.push((
106
+ "offsets",
107
+ ColumnType::String,
108
+ str_col(edges.iter().map(|edge| edge.offsets.clone()).collect()),
109
+ ));
110
+ }
111
+ if edges.iter().any(|edge| edge.via.is_some()) {
112
+ columns.push((
113
+ "via",
114
+ ColumnType::String,
115
+ str_col(edges.iter().map(|edge| edge.via.clone()).collect()),
116
+ ));
117
+ }
118
+ if edges.iter().any(|edge| edge.address_lines.is_some()) {
119
+ columns.push((
120
+ "address_lines",
121
+ ColumnType::String,
122
+ str_col(
123
+ edges
124
+ .iter()
125
+ .map(|edge| edge.address_lines.clone())
126
+ .collect(),
127
+ ),
128
+ ));
129
+ }
130
+ build_df(columns)
131
+ }
132
+
133
+ pub(super) fn control_edges_df(edges: &[super::super::semantic_edges::ControlEdge]) -> DataFrame {
134
+ build_df(vec![
135
+ (
136
+ "caller",
137
+ ColumnType::String,
138
+ str_col(edges.iter().map(|edge| Some(edge.caller.clone())).collect()),
139
+ ),
140
+ (
141
+ "target",
142
+ ColumnType::String,
143
+ str_col(edges.iter().map(|edge| Some(edge.target.clone())).collect()),
144
+ ),
145
+ (
146
+ "transfer_lines",
147
+ ColumnType::String,
148
+ str_col(
149
+ edges
150
+ .iter()
151
+ .map(|edge| Some(edge.transfer_lines.clone()))
152
+ .collect(),
153
+ ),
154
+ ),
155
+ (
156
+ "transfer_count",
157
+ ColumnType::Int64,
158
+ int_col(edges.iter().map(|edge| Some(edge.transfer_count)).collect()),
159
+ ),
160
+ (
161
+ "raw_targets",
162
+ ColumnType::String,
163
+ str_col(edges.iter().map(|edge| edge.raw_targets.clone()).collect()),
164
+ ),
165
+ (
166
+ "offsets",
167
+ ColumnType::String,
168
+ str_col(edges.iter().map(|edge| edge.offsets.clone()).collect()),
169
+ ),
170
+ (
171
+ "via",
172
+ ColumnType::String,
173
+ str_col(edges.iter().map(|edge| edge.via.clone()).collect()),
174
+ ),
175
+ (
176
+ "address_lines",
177
+ ColumnType::String,
178
+ str_col(
179
+ edges
180
+ .iter()
181
+ .map(|edge| edge.address_lines.clone())
182
+ .collect(),
183
+ ),
184
+ ),
96
185
  ])
97
186
  }
98
187
 
@@ -144,10 +233,95 @@ pub(super) fn references_edges_df(
144
233
  let fns: Vec<Option<String>> = edges.iter().map(|e| Some(e.function.clone())).collect();
145
234
  let consts: Vec<Option<String>> = edges.iter().map(|e| Some(e.constant.clone())).collect();
146
235
  let lines: Vec<Option<i64>> = edges.iter().map(|e| Some(e.line as i64)).collect();
147
- build_df(vec![
236
+ let mut columns = vec![
148
237
  ("function", ColumnType::String, str_col(fns)),
149
238
  ("constant", ColumnType::String, str_col(consts)),
150
239
  ("line", ColumnType::Int64, int_col(lines)),
240
+ ];
241
+ if edges.iter().any(|edge| edge.reference_lines.is_some()) {
242
+ columns.push((
243
+ "reference_lines",
244
+ ColumnType::String,
245
+ str_col(
246
+ edges
247
+ .iter()
248
+ .map(|edge| edge.reference_lines.clone())
249
+ .collect(),
250
+ ),
251
+ ));
252
+ }
253
+ if edges.iter().any(|edge| edge.reference_count.is_some()) {
254
+ columns.push((
255
+ "reference_count",
256
+ ColumnType::Int64,
257
+ int_col(edges.iter().map(|edge| edge.reference_count).collect()),
258
+ ));
259
+ }
260
+ if edges.iter().any(|edge| edge.opcodes.is_some()) {
261
+ columns.push((
262
+ "opcodes",
263
+ ColumnType::String,
264
+ str_col(edges.iter().map(|edge| edge.opcodes.clone()).collect()),
265
+ ));
266
+ }
267
+ if edges.iter().any(|edge| edge.accesses.is_some()) {
268
+ columns.push((
269
+ "accesses",
270
+ ColumnType::String,
271
+ str_col(edges.iter().map(|edge| edge.accesses.clone()).collect()),
272
+ ));
273
+ }
274
+ if edges.iter().any(|edge| edge.has_read.is_some()) {
275
+ columns.push((
276
+ "has_read",
277
+ ColumnType::Boolean,
278
+ bool_col(edges.iter().map(|edge| edge.has_read).collect()),
279
+ ));
280
+ }
281
+ if edges.iter().any(|edge| edge.has_write.is_some()) {
282
+ columns.push((
283
+ "has_write",
284
+ ColumnType::Boolean,
285
+ bool_col(edges.iter().map(|edge| edge.has_write).collect()),
286
+ ));
287
+ }
288
+ if edges.iter().any(|edge| edge.has_address.is_some()) {
289
+ columns.push((
290
+ "has_address",
291
+ ColumnType::Boolean,
292
+ bool_col(edges.iter().map(|edge| edge.has_address).collect()),
293
+ ));
294
+ }
295
+ build_df(columns)
296
+ }
297
+
298
+ pub(super) fn symbol_edges_df(edges: &[super::super::semantic_edges::SymbolEdge]) -> DataFrame {
299
+ build_df(vec![
300
+ (
301
+ "source",
302
+ ColumnType::String,
303
+ str_col(edges.iter().map(|edge| Some(edge.source.clone())).collect()),
304
+ ),
305
+ (
306
+ "target",
307
+ ColumnType::String,
308
+ str_col(edges.iter().map(|edge| Some(edge.target.clone())).collect()),
309
+ ),
310
+ (
311
+ "line",
312
+ ColumnType::Int64,
313
+ int_col(edges.iter().map(|edge| Some(edge.line as i64)).collect()),
314
+ ),
315
+ (
316
+ "raw_target",
317
+ ColumnType::String,
318
+ str_col(
319
+ edges
320
+ .iter()
321
+ .map(|edge| Some(edge.raw_target.clone()))
322
+ .collect(),
323
+ ),
324
+ ),
151
325
  ])
152
326
  }
153
327
 
@@ -169,7 +169,7 @@ pub(super) fn functions_df(
169
169
  file_is_test: &HashMap<&str, bool>,
170
170
  file_to_module: &HashMap<&str, &str>,
171
171
  ) -> DataFrame {
172
- build_df(vec![
172
+ let mut columns = vec![
173
173
  (
174
174
  "qualified_name",
175
175
  ColumnType::String,
@@ -462,7 +462,33 @@ pub(super) fn functions_df(
462
462
  .collect(),
463
463
  ),
464
464
  ),
465
- ])
465
+ ];
466
+ for key in ["symbol_kind", "role_hint"] {
467
+ if fns.iter().any(|function| {
468
+ function
469
+ .metadata
470
+ .get(key)
471
+ .and_then(|value| value.as_str())
472
+ .is_some()
473
+ }) {
474
+ columns.push((
475
+ key,
476
+ ColumnType::String,
477
+ str_col(
478
+ fns.iter()
479
+ .map(|function| {
480
+ function
481
+ .metadata
482
+ .get(key)
483
+ .and_then(|value| value.as_str())
484
+ .map(str::to_string)
485
+ })
486
+ .collect(),
487
+ ),
488
+ ));
489
+ }
490
+ }
491
+ build_df(columns)
466
492
  }
467
493
 
468
494
  pub(super) fn classes_df(
@@ -814,7 +840,7 @@ pub(super) fn constants_df(
814
840
  consts: &[ConstantInfo],
815
841
  file_to_module: &HashMap<&str, &str>,
816
842
  ) -> DataFrame {
817
- build_df(vec![
843
+ let mut columns = vec![
818
844
  (
819
845
  "qualified_name",
820
846
  ColumnType::String,
@@ -875,7 +901,35 @@ pub(super) fn constants_df(
875
901
  ColumnType::Int64,
876
902
  int_col(consts.iter().map(|c| Some(c.line_number as i64)).collect()),
877
903
  ),
878
- ])
904
+ ];
905
+ if consts.iter().any(|constant| constant.kind == "agc_erase") {
906
+ columns.push((
907
+ "is_mutable",
908
+ ColumnType::Boolean,
909
+ bool_col(
910
+ consts
911
+ .iter()
912
+ .map(|constant| {
913
+ constant
914
+ .kind
915
+ .starts_with("agc_")
916
+ .then_some(constant.kind == "agc_erase")
917
+ })
918
+ .collect(),
919
+ ),
920
+ ));
921
+ columns.push((
922
+ "storage",
923
+ ColumnType::String,
924
+ str_col(
925
+ consts
926
+ .iter()
927
+ .map(|constant| (constant.kind == "agc_erase").then(|| "erasable".to_string()))
928
+ .collect(),
929
+ ),
930
+ ));
931
+ }
932
+ build_df(columns)
879
933
  }
880
934
 
881
935
  pub(super) fn elements_df(elements: &[crate::models::ElementInfo]) -> DataFrame {
@@ -120,6 +120,7 @@ pub fn load_into_graph(
120
120
  run_stage!(type_relationship_edges, "implements+extends+has_method");
121
121
  run_stage!(uses_type_edges, "uses_type");
122
122
  run_stage!(reference_edges, "references");
123
+ run_stage!(symbol_relationship_edges, "alias_of+points_to");
123
124
  run_stage!(function_reference_edges, "references_fn");
124
125
  run_stage!(decorator_edges, "decorates");
125
126
  run_stage!(ffi_edges, "ffi_exposes");
@@ -826,13 +827,19 @@ impl<'a> LoadPipeline<'a> {
826
827
  let graph = &mut self.graph;
827
828
  // Function CALLS Function (5-tier resolution).
828
829
  let noise = super::call_edges::noise_names_for_files(&result.files);
829
- let (call_edges, call_stats) = super::call_edges::build_call_edges(
830
+ let (mut call_edges, mut call_stats) = super::call_edges::build_call_edges(
830
831
  &result.functions,
831
832
  &result.files,
832
833
  &noise,
833
834
  5,
834
835
  &result.type_relationships,
835
836
  );
837
+ let semantic = super::semantic_edges::build_control_edges(
838
+ &result.control_transfers,
839
+ &result.functions,
840
+ );
841
+ call_edges.extend(semantic.calls);
842
+ call_stats.merge(semantic.call_stats);
836
843
  if !call_edges.is_empty() {
837
844
  maintain::add_connections(
838
845
  graph,
@@ -848,6 +855,36 @@ impl<'a> LoadPipeline<'a> {
848
855
  )
849
856
  .map_err(py_err)?;
850
857
  }
858
+ if !semantic.jumps.is_empty() {
859
+ maintain::add_connections(
860
+ graph,
861
+ control_edges_df(&semantic.jumps),
862
+ "JUMPS_TO".into(),
863
+ "Function".into(),
864
+ "caller".into(),
865
+ "Function".into(),
866
+ "target".into(),
867
+ None,
868
+ None,
869
+ None,
870
+ )
871
+ .map_err(py_err)?;
872
+ }
873
+ if !semantic.branches.is_empty() {
874
+ maintain::add_connections(
875
+ graph,
876
+ control_edges_df(&semantic.branches),
877
+ "BRANCHES_TO".into(),
878
+ "Function".into(),
879
+ "caller".into(),
880
+ "Function".into(),
881
+ "target".into(),
882
+ None,
883
+ None,
884
+ None,
885
+ )
886
+ .map_err(py_err)?;
887
+ }
851
888
 
852
889
  self.call_stats = call_stats;
853
890
  Ok(())
@@ -1099,7 +1136,13 @@ impl<'a> LoadPipeline<'a> {
1099
1136
  let result = self.result;
1100
1137
  let graph = &mut self.graph;
1101
1138
  // REFERENCES (Function → Constant) — name-keyed identifier resolution.
1102
- let refs = super::other_edges::build_references_edges(&result.functions, &result.constants);
1139
+ let mut refs =
1140
+ super::other_edges::build_references_edges(&result.functions, &result.constants);
1141
+ refs.extend(super::semantic_edges::build_reference_site_edges(
1142
+ &result.reference_sites,
1143
+ &result.functions,
1144
+ &result.constants,
1145
+ ));
1103
1146
  if !refs.is_empty() {
1104
1147
  maintain::add_connections(
1105
1148
  graph,
@@ -1119,6 +1162,32 @@ impl<'a> LoadPipeline<'a> {
1119
1162
  Ok(())
1120
1163
  }
1121
1164
 
1165
+ fn symbol_relationship_edges(&mut self) -> Result<(), String> {
1166
+ let result = self.result;
1167
+ let graph = &mut self.graph;
1168
+ let batches = super::semantic_edges::build_symbol_edges(
1169
+ &result.symbol_relationships,
1170
+ &result.functions,
1171
+ &result.constants,
1172
+ );
1173
+ for batch in batches {
1174
+ maintain::add_connections(
1175
+ graph,
1176
+ symbol_edges_df(&batch.edges),
1177
+ batch.relationship.into(),
1178
+ "Constant".into(),
1179
+ "source".into(),
1180
+ batch.target_node_type.into(),
1181
+ "target".into(),
1182
+ None,
1183
+ None,
1184
+ None,
1185
+ )
1186
+ .map_err(py_err)?;
1187
+ }
1188
+ Ok(())
1189
+ }
1190
+
1122
1191
  fn function_reference_edges(&mut self) -> Result<(), String> {
1123
1192
  let result = self.result;
1124
1193
  let graph = &mut self.graph;
@@ -4,6 +4,7 @@ pub mod call_edges;
4
4
  pub mod load;
5
5
  pub mod other_edges;
6
6
  pub mod routes;
7
+ pub mod semantic_edges;
7
8
  pub mod type_edges;
8
9
 
9
10
  use crate::models::{ParseResult, ProjectInfo, SourceRoot};
@@ -257,6 +258,39 @@ fn retain_non_test_entities(result: &mut ParseResult) {
257
258
  .selectors
258
259
  .retain(|item| !test_paths.contains(&item.file_path));
259
260
 
261
+ let retained_functions: HashSet<&str> = result
262
+ .functions
263
+ .iter()
264
+ .map(|item| item.qualified_name.as_str())
265
+ .collect();
266
+ let retained_constants: HashSet<&str> = result
267
+ .constants
268
+ .iter()
269
+ .map(|item| item.qualified_name.as_str())
270
+ .collect();
271
+ result.control_transfers.retain(|site| {
272
+ retained_functions.contains(site.caller.as_str())
273
+ && site
274
+ .target
275
+ .as_deref()
276
+ .is_none_or(|target| retained_functions.contains(target))
277
+ });
278
+ result.reference_sites.retain(|site| {
279
+ retained_functions.contains(site.caller.as_str())
280
+ && retained_constants.contains(site.target.as_str())
281
+ });
282
+ result.symbol_relationships.retain(|relationship| {
283
+ retained_constants.contains(relationship.source.as_str())
284
+ && match relationship.target_kind {
285
+ crate::models::SymbolTargetKind::Constant => {
286
+ retained_constants.contains(relationship.target.as_str())
287
+ }
288
+ crate::models::SymbolTargetKind::Function => {
289
+ retained_functions.contains(relationship.target.as_str())
290
+ }
291
+ }
292
+ });
293
+
260
294
  let retained_types: HashSet<&str> = result
261
295
  .classes
262
296
  .iter()
@@ -524,6 +558,12 @@ fn finalize_and_load(
524
558
  dedup_by_key(&mut combined.enums, |e| e.qualified_name.clone());
525
559
  dedup_by_key(&mut combined.interfaces, |i| i.qualified_name.clone());
526
560
  dedup_by_key(&mut combined.constants, |c| c.qualified_name.clone());
561
+ combined.control_transfers.sort();
562
+ combined.control_transfers.dedup();
563
+ combined.reference_sites.sort();
564
+ combined.reference_sites.dedup();
565
+ combined.symbol_relationships.sort();
566
+ combined.symbol_relationships.dedup();
527
567
  if verbose {
528
568
  eprintln!("[timing] dedup: {:.3}s", t_dedup.elapsed().as_secs_f64());
529
569
  }
@@ -615,6 +655,12 @@ pub fn run(src_dir: &Path, verbose: bool) -> Result<Arc<DirGraph>, String> {
615
655
  dedup_by_key(&mut combined.enums, |e| e.qualified_name.clone());
616
656
  dedup_by_key(&mut combined.interfaces, |i| i.qualified_name.clone());
617
657
  dedup_by_key(&mut combined.constants, |c| c.qualified_name.clone());
658
+ combined.control_transfers.sort();
659
+ combined.control_transfers.dedup();
660
+ combined.reference_sites.sort();
661
+ combined.reference_sites.dedup();
662
+ combined.symbol_relationships.sort();
663
+ combined.symbol_relationships.dedup();
618
664
 
619
665
  if verbose {
620
666
  eprintln!(