callix 0.1.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.
Files changed (75) hide show
  1. callix-0.1.0/.github/workflows/CI.yml +53 -0
  2. callix-0.1.0/.github/workflows/bench.yml +167 -0
  3. callix-0.1.0/.github/workflows/release.yml +172 -0
  4. callix-0.1.0/.gitignore +75 -0
  5. callix-0.1.0/CHANGELOG.md +29 -0
  6. callix-0.1.0/CLAUDE.md +172 -0
  7. callix-0.1.0/Cargo.lock +3225 -0
  8. callix-0.1.0/Cargo.toml +69 -0
  9. callix-0.1.0/PKG-INFO +358 -0
  10. callix-0.1.0/README.md +344 -0
  11. callix-0.1.0/benchmarks/README.md +65 -0
  12. callix-0.1.0/benchmarks/Taskfile.yaml +67 -0
  13. callix-0.1.0/benchmarks/projects.json +61 -0
  14. callix-0.1.0/benchmarks/results/gin-gonic__gin.json +23 -0
  15. callix-0.1.0/benchmarks/run_benchmarks.py +581 -0
  16. callix-0.1.0/build.rs +146 -0
  17. callix-0.1.0/cliff.toml +95 -0
  18. callix-0.1.0/example/test.py +132 -0
  19. callix-0.1.0/go/bridge.go +142 -0
  20. callix-0.1.0/go/bridge_go.go +229 -0
  21. callix-0.1.0/go/go.mod +26 -0
  22. callix-0.1.0/go/go.sum +26 -0
  23. callix-0.1.0/pyproject.toml +26 -0
  24. callix-0.1.0/python/callix/__init__.py +184 -0
  25. callix-0.1.0/python/callix/_core/__init__.pyi +994 -0
  26. callix-0.1.0/python/callix/py.typed +0 -0
  27. callix-0.1.0/python/callix/ty.py +50 -0
  28. callix-0.1.0/src/bin/stub_gen.rs +37 -0
  29. callix-0.1.0/src/boundaries.rs +168 -0
  30. callix-0.1.0/src/diffing.rs +146 -0
  31. callix-0.1.0/src/error.rs +32 -0
  32. callix-0.1.0/src/golang/adapter.rs +542 -0
  33. callix-0.1.0/src/golang/boundary.rs +320 -0
  34. callix-0.1.0/src/golang/deps.rs +159 -0
  35. callix-0.1.0/src/golang/mod.rs +39 -0
  36. callix-0.1.0/src/golang/resolver.rs +217 -0
  37. callix-0.1.0/src/golang/visitor.rs +469 -0
  38. callix-0.1.0/src/graph.rs +448 -0
  39. callix-0.1.0/src/ids.rs +34 -0
  40. callix-0.1.0/src/lib.rs +145 -0
  41. callix-0.1.0/src/metrics.rs +93 -0
  42. callix-0.1.0/src/node.rs +165 -0
  43. callix-0.1.0/src/occurrence.rs +113 -0
  44. callix-0.1.0/src/python/adapter.rs +990 -0
  45. callix-0.1.0/src/python/boundary.rs +520 -0
  46. callix-0.1.0/src/python/deps.rs +200 -0
  47. callix-0.1.0/src/python/detector.rs +119 -0
  48. callix-0.1.0/src/python/mod.rs +120 -0
  49. callix-0.1.0/src/python/module_resolver.rs +83 -0
  50. callix-0.1.0/src/python/ty_embedded.rs +195 -0
  51. callix-0.1.0/src/python/visitor.rs +917 -0
  52. callix-0.1.0/src/relation.rs +138 -0
  53. callix-0.1.0/src/roots.rs +220 -0
  54. callix-0.1.0/src/rustlang/adapter.rs +567 -0
  55. callix-0.1.0/src/rustlang/boundary.rs +327 -0
  56. callix-0.1.0/src/rustlang/deps.rs +145 -0
  57. callix-0.1.0/src/rustlang/mod.rs +40 -0
  58. callix-0.1.0/src/rustlang/resolver.rs +378 -0
  59. callix-0.1.0/src/rustlang/scip.rs +175 -0
  60. callix-0.1.0/src/rustlang/visitor.rs +394 -0
  61. callix-0.1.0/src/serde.rs +148 -0
  62. callix-0.1.0/src/serialization.rs +153 -0
  63. callix-0.1.0/src/span.rs +53 -0
  64. callix-0.1.0/src/span_index.rs +120 -0
  65. callix-0.1.0/src/status.rs +94 -0
  66. callix-0.1.0/src/ts.rs +130 -0
  67. callix-0.1.0/src/typescript/adapter.rs +412 -0
  68. callix-0.1.0/src/typescript/boundary.rs +352 -0
  69. callix-0.1.0/src/typescript/detector.rs +214 -0
  70. callix-0.1.0/src/typescript/mod.rs +109 -0
  71. callix-0.1.0/src/typescript/module_resolver.rs +247 -0
  72. callix-0.1.0/src/typescript/resolver.rs +181 -0
  73. callix-0.1.0/src/typescript/visitor.rs +1309 -0
  74. callix-0.1.0/taskfile.dist.yaml +186 -0
  75. callix-0.1.0/uv.lock +7 -0
@@ -0,0 +1,53 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ env:
13
+ CARGO_TERM_COLOR: always
14
+
15
+ jobs:
16
+ test:
17
+ runs-on: ubuntu-latest
18
+ # The ty/ruff tree is about 200 crates; a cold build takes a long time,
19
+ # a warm one finishes in a few minutes.
20
+ timeout-minutes: 60
21
+ steps:
22
+ - uses: actions/checkout@v6
23
+ - uses: dtolnay/rust-toolchain@stable
24
+ # Go is only needed at build time: the typescript-go bridge is linked
25
+ # statically, and users get a prebuilt wheel.
26
+ - uses: actions/setup-go@v5
27
+ with:
28
+ go-version: "1.26"
29
+ cache-dependency-path: go/go.sum
30
+ - uses: astral-sh/setup-uv@v7
31
+ with:
32
+ python-version: "3.13"
33
+ - uses: arduino/setup-task@v2
34
+
35
+ # Without the cache every run would rebuild all of ty from scratch.
36
+ - uses: Swatinem/rust-cache@v2
37
+ with:
38
+ cache-all-crates: true
39
+
40
+ - name: Install
41
+ run: task install
42
+
43
+ - name: Lint
44
+ run: task lint
45
+
46
+ - name: Build
47
+ run: task build
48
+
49
+ - name: Stubs are up to date
50
+ run: task stubs:check
51
+
52
+ - name: Example
53
+ run: task example
@@ -0,0 +1,167 @@
1
+ name: Benchmark
2
+
3
+ # A load benchmark on real-world projects — report only, it never gates a
4
+ # release. The matrix runs per language: one hung resolver neither blocks the
5
+ # others nor eats their time.
6
+ #
7
+ # - workflow_dispatch — every language runs, and the README is refreshed and
8
+ # committed; run it by hand after a release when all the numbers are wanted.
9
+ # - pull_request — only the languages whose adapter the PR touches. The result
10
+ # goes to the job summary; the README is left alone.
11
+ on:
12
+ workflow_dispatch:
13
+ pull_request:
14
+ branches: ["*"]
15
+
16
+ permissions:
17
+ contents: read
18
+
19
+ jobs:
20
+ changes:
21
+ if: github.event_name == 'pull_request'
22
+ runs-on: ubuntu-latest
23
+ outputs:
24
+ # paths-filter's `changes` output is already a JSON array of the filters
25
+ # that matched — exactly what the matrix below needs.
26
+ langs: ${{ steps.filter.outputs.changes }}
27
+ steps:
28
+ - uses: actions/checkout@v6
29
+ - uses: dorny/paths-filter@v3
30
+ id: filter
31
+ with:
32
+ filters: |
33
+ python:
34
+ - "src/python/**"
35
+ - "benchmarks/**"
36
+ - ".github/workflows/bench.yml"
37
+ typescript:
38
+ - "src/typescript/**"
39
+ - "go/bridge.go"
40
+ - "benchmarks/**"
41
+ - ".github/workflows/bench.yml"
42
+ go:
43
+ - "src/golang/**"
44
+ - "go/bridge_go.go"
45
+ - "benchmarks/**"
46
+ - ".github/workflows/bench.yml"
47
+ rust:
48
+ - "src/rustlang/**"
49
+ - "benchmarks/**"
50
+ - ".github/workflows/bench.yml"
51
+
52
+ benchmark:
53
+ if: >-
54
+ github.event_name == 'workflow_dispatch' ||
55
+ (needs.changes.result == 'success' && needs.changes.outputs.langs != '[]')
56
+ needs: [changes]
57
+ runs-on: ubuntu-latest
58
+ timeout-minutes: 120
59
+ strategy:
60
+ fail-fast: false
61
+ matrix:
62
+ lang: >-
63
+ ${{ github.event_name == 'workflow_dispatch'
64
+ && fromJSON('["python","typescript","go","rust"]')
65
+ || fromJSON(needs.changes.outputs.langs) }}
66
+ defaults:
67
+ run:
68
+ shell: bash
69
+ steps:
70
+ - uses: actions/checkout@v6
71
+
72
+ - uses: dtolnay/rust-toolchain@stable
73
+ # Go is needed both to build the bridge and as the toolchain the Go
74
+ # resolver drives.
75
+ - uses: actions/setup-go@v5
76
+ with:
77
+ go-version: "1.26"
78
+ cache-dependency-path: go/go.sum
79
+ - uses: astral-sh/setup-uv@v7
80
+ with:
81
+ python-version: "3.13"
82
+ - uses: arduino/setup-task@v2
83
+ with:
84
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
85
+ - uses: Swatinem/rust-cache@v2
86
+ with:
87
+ cache-all-crates: true
88
+
89
+ - name: Install rust-analyzer (the Rust resolver)
90
+ if: matrix.lang == 'rust'
91
+ run: |
92
+ rustup component add rust-analyzer
93
+ echo "$(dirname "$(rustup which rust-analyzer)")" >> "$GITHUB_PATH"
94
+
95
+ - name: Build
96
+ run: |
97
+ task install
98
+ task build
99
+
100
+ - name: Benchmark ${{ matrix.lang }}
101
+ run: task bench:run BENCH_LANG=${{ matrix.lang }}
102
+
103
+ - name: Report
104
+ if: always()
105
+ run: |
106
+ {
107
+ echo "## callix benchmark — ${{ matrix.lang }}"
108
+ echo
109
+ task bench:show VERSION="${{ github.sha }}"
110
+ } >> "$GITHUB_STEP_SUMMARY"
111
+
112
+ - uses: actions/upload-artifact@v4
113
+ if: always()
114
+ with:
115
+ name: benchmark-results-${{ matrix.lang }}
116
+ path: benchmarks/results/
117
+
118
+ # Aggregates every language's results and refreshes the README. Manual runs
119
+ # only: a PR never commits anything.
120
+ render:
121
+ if: always() && github.event_name == 'workflow_dispatch'
122
+ needs: benchmark
123
+ runs-on: ubuntu-latest
124
+ permissions:
125
+ contents: write
126
+ concurrency:
127
+ group: benchmark-render
128
+ cancel-in-progress: false
129
+ steps:
130
+ - uses: actions/checkout@v6
131
+ with:
132
+ fetch-depth: 0
133
+ token: ${{ secrets.GITHUB_TOKEN }}
134
+ - uses: arduino/setup-task@v2
135
+ with:
136
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
137
+
138
+ - name: Download results
139
+ uses: actions/download-artifact@v4
140
+ with:
141
+ pattern: benchmark-results-*
142
+ path: benchmarks/results
143
+ merge-multiple: true
144
+
145
+ - name: Render into README
146
+ # benchmarks/results/ holds the committed results of the previous run
147
+ # overlaid with the fresh ones: languages that did not run this time
148
+ # keep their last known numbers.
149
+ run: task bench:render VERSION="${{ github.ref_name }}"
150
+
151
+ - name: Commit
152
+ run: |
153
+ if git diff --quiet -- README.md benchmarks/results; then
154
+ echo "Nothing changed"
155
+ exit 0
156
+ fi
157
+ git config user.name "github-actions[bot]"
158
+ git config user.email "github-actions[bot]@users.noreply.github.com"
159
+ git add README.md benchmarks/results
160
+ git commit -m "docs(bench): refresh benchmark metrics [skip ci]"
161
+ for i in 1 2 3 4; do
162
+ git push origin HEAD:main && exit 0
163
+ git pull --rebase origin main || true
164
+ sleep "$((2 ** i))"
165
+ done
166
+ echo "::error::could not push the README update"
167
+ exit 1
@@ -0,0 +1,172 @@
1
+ name: Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ permissions:
7
+ contents: read
8
+
9
+ jobs:
10
+ # ---------------------------------------------------------------------------
11
+ # 1. Version, changelog, tag. Wheels are built from that tag.
12
+ # ---------------------------------------------------------------------------
13
+ version:
14
+ runs-on: ubuntu-latest
15
+ permissions:
16
+ contents: write
17
+ env:
18
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
19
+ outputs:
20
+ tag: ${{ steps.version.outputs.tag }}
21
+ version: ${{ steps.version.outputs.version }}
22
+ steps:
23
+ - uses: actions/checkout@v6
24
+ with:
25
+ fetch-depth: 0
26
+ fetch-tags: true
27
+ token: ${{ secrets.GITHUB_TOKEN }}
28
+
29
+ - name: Configure git
30
+ run: |
31
+ git config user.name "github-actions[bot]"
32
+ git config user.email "github-actions[bot]@users.noreply.github.com"
33
+
34
+ - uses: arduino/setup-task@v2
35
+ with:
36
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
37
+ - uses: kenji-miyake/setup-git-cliff@v2
38
+
39
+ - name: Determine next version
40
+ id: version
41
+ run: task release:version
42
+
43
+ - name: Bump version
44
+ run: task release:bump VERSION=${{ steps.version.outputs.version }}
45
+
46
+ - name: Generate changelog
47
+ run: task release:changelog TAG=${{ steps.version.outputs.tag }}
48
+
49
+ - name: Commit and tag
50
+ run: task release:commit TAG=${{ steps.version.outputs.tag }}
51
+
52
+ - name: Push
53
+ run: task release:push TAG=${{ steps.version.outputs.tag }}
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # 2. Wheels. One wheel cannot cover every platform: ty and the Go bridge to
57
+ # typescript-go are linked inside. Thanks to abi3-py313 the matrix runs
58
+ # over platforms only, not over Python versions.
59
+ # ---------------------------------------------------------------------------
60
+ wheels:
61
+ needs: version
62
+ runs-on: ${{ matrix.os }}
63
+ timeout-minutes: 90
64
+ strategy:
65
+ fail-fast: false
66
+ matrix:
67
+ include:
68
+ - os: ubuntu-latest
69
+ target: x86_64
70
+ # A native ARM runner rather than QEMU inside the container: the ty
71
+ # tree alone is ~200 crates, and emulating that would not fit the
72
+ # timeout.
73
+ - os: ubuntu-24.04-arm
74
+ target: aarch64
75
+ - os: macos-latest
76
+ target: aarch64
77
+ # macos-latest is Apple Silicon, so the Intel wheel is cross-built;
78
+ # build.rs passes the architecture on to Go and clang.
79
+ - os: macos-latest
80
+ target: x86_64
81
+ steps:
82
+ - uses: actions/checkout@v6
83
+ with:
84
+ ref: ${{ needs.version.outputs.tag }}
85
+
86
+ - uses: actions/setup-go@v5
87
+ with:
88
+ go-version: "1.26"
89
+ cache-dependency-path: go/go.sum
90
+
91
+ - name: Build wheel
92
+ uses: PyO3/maturin-action@v1
93
+ with:
94
+ target: ${{ matrix.target }}
95
+ args: --release --out dist
96
+ # The manylinux container carries no Go, and build.rs cannot build
97
+ # the bridge without it, so Go is installed into the container first.
98
+ manylinux: ${{ startsWith(matrix.os, 'ubuntu') && '2_28' || 'auto' }}
99
+ before-script-linux: |
100
+ case "$(uname -m)" in
101
+ x86_64) goarch=amd64 ;;
102
+ aarch64) goarch=arm64 ;;
103
+ *) echo "unsupported container arch $(uname -m)" >&2; exit 1 ;;
104
+ esac
105
+ curl -sSL "https://go.dev/dl/go1.26.0.linux-${goarch}.tar.gz" \
106
+ | tar -C /usr/local -xz
107
+ export PATH=$PATH:/usr/local/go/bin
108
+ go version
109
+
110
+ - uses: actions/upload-artifact@v4
111
+ with:
112
+ name: wheel-${{ matrix.os }}-${{ matrix.target }}
113
+ path: dist/
114
+
115
+ # ---------------------------------------------------------------------------
116
+ # 3. The source distribution, so platforms outside the matrix can build.
117
+ # ---------------------------------------------------------------------------
118
+ sdist:
119
+ needs: version
120
+ runs-on: ubuntu-latest
121
+ steps:
122
+ - uses: actions/checkout@v6
123
+ with:
124
+ ref: ${{ needs.version.outputs.tag }}
125
+ - uses: PyO3/maturin-action@v1
126
+ with:
127
+ command: sdist
128
+ args: --out dist
129
+ - uses: actions/upload-artifact@v4
130
+ with:
131
+ name: sdist
132
+ path: dist/
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # 4. Publishing and the GitHub Release.
136
+ # ---------------------------------------------------------------------------
137
+ publish:
138
+ needs: [version, wheels, sdist]
139
+ runs-on: ubuntu-latest
140
+ environment: release
141
+ permissions:
142
+ contents: write
143
+ env:
144
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
145
+ steps:
146
+ - uses: actions/checkout@v6
147
+ with:
148
+ ref: ${{ needs.version.outputs.tag }}
149
+ fetch-depth: 0
150
+ fetch-tags: true
151
+
152
+ - uses: arduino/setup-task@v2
153
+ with:
154
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
155
+ - uses: astral-sh/setup-uv@v7
156
+ - uses: kenji-miyake/setup-git-cliff@v2
157
+
158
+ - name: Collect artifacts
159
+ uses: actions/download-artifact@v4
160
+ with:
161
+ path: dist
162
+ merge-multiple: true
163
+
164
+ - name: Publish to PyPI
165
+ env:
166
+ UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
167
+ run: task release:publish
168
+
169
+ - name: Create GitHub Release
170
+ env:
171
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
172
+ run: task release:github TAG=${{ needs.version.outputs.tag }}
@@ -0,0 +1,75 @@
1
+ /target
2
+
3
+ # The typescript-go clone for the Go bridge: fetched by build.rs, ~370 MB
4
+ /.ts-go
5
+
6
+ # Byte-compiled / optimized / DLL files
7
+ __pycache__/
8
+ .pytest_cache/
9
+ *.py[cod]
10
+
11
+ # C extensions
12
+ *.so
13
+
14
+ # Distribution / packaging
15
+ .Python
16
+ .venv/
17
+ env/
18
+ /bin/
19
+ /build/
20
+ develop-eggs/
21
+ dist/
22
+ eggs/
23
+ /lib/
24
+ /lib64/
25
+ /parts/
26
+ sdist/
27
+ /var/
28
+ /include/
29
+ /man/
30
+ venv/
31
+ *.egg-info/
32
+ .installed.cfg
33
+ *.egg
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+ pip-selfcheck.json
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .coverage
44
+ .cache
45
+ nosetests.xml
46
+ coverage.xml
47
+
48
+ # Translations
49
+ *.mo
50
+
51
+ # Mr Developer
52
+ .mr.developer.cfg
53
+ .project
54
+ .pydevproject
55
+
56
+ # Rope
57
+ .ropeproject
58
+
59
+ # Django stuff:
60
+ *.log
61
+ *.pot
62
+
63
+ .DS_Store
64
+
65
+ # Sphinx documentation
66
+ docs/_build/
67
+
68
+ # PyCharm
69
+ .idea/
70
+
71
+ # VSCode
72
+ .vscode/
73
+
74
+ # Pyenv
75
+ .python-version
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [0.1.0] - 2026-07-29
6
+ ### Bug Fixes
7
+
8
+ - ci
9
+ - ci release
10
+ - **build**: unbreak the linux wheel and widen the platform matrix
11
+
12
+ ### Documentation
13
+
14
+ - **bench**: refresh benchmark metrics [skip ci] by @github-actions[bot]
15
+ - add CLAUDE.md
16
+
17
+ ### Features
18
+
19
+ - reinit
20
+ - update docs
21
+
22
+
23
+ ### New Contributors
24
+
25
+ - @ made their first contribution
26
+ - @github-actions[bot] made their first contribution
27
+
28
+
29
+ <!-- generated by git-cliff -->
callix-0.1.0/CLAUDE.md ADDED
@@ -0,0 +1,172 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Language
6
+
7
+ All code, comments, docs, commit messages and error strings in this repository
8
+ are **English**. Keep it that way when adding code.
9
+
10
+ ## Commands
11
+
12
+ Everything goes through [Task](https://taskfile.dev) — `taskfile.dist.yaml`.
13
+ Run cargo through the tasks rather than directly (see *Build gotchas* below).
14
+
15
+ ```bash
16
+ task # build → regenerate .pyi → run example/test.py
17
+ task install # create .venv (Python 3.13) + maturin
18
+ task build # maturin develop, release profile
19
+ task lint # clippy with -D warnings (CI gate)
20
+ task check # cargo check, no module build
21
+ task test # cargo test + example smoke run
22
+ task stubs / task stubs:check # regenerate / verify committed .pyi
23
+ task example -- example/foo.py # run a different script
24
+ DEBUG=1 task ... # switch every cargo task to the debug profile
25
+ ```
26
+
27
+ There is no test suite yet: `cargo test` compiles but has no tests, and
28
+ correctness is currently established by parity runs against graphlens (below).
29
+
30
+ Benchmarks (`benchmarks/Taskfile.yaml`, included as `bench:`):
31
+
32
+ ```bash
33
+ task bench:list [BENCH_LANG=rust]
34
+ task bench:one PROJECT=gin-gonic/gin
35
+ task bench:run [BENCH_LANG=go]
36
+ task bench:render VERSION=0.1.0 # splices the table into README.md
37
+ ```
38
+
39
+ ## Architecture
40
+
41
+ callix is a Rust rewrite of [graphlens](https://github.com/Neko1313/graphlens).
42
+ Everything of substance lives in the Rust extension module `callix._core`;
43
+ `python/callix/` is a facade of re-exports plus a thin `TyResolver` wrapper.
44
+
45
+ ### The graph contract
46
+
47
+ `src/node.rs`, `src/relation.rs`, `src/graph.rs`, `src/ids.rs`, `src/span.rs`.
48
+ 14 `NodeKind`s, 12 `RelationKind`s, 1-based spans, and IDs that must stay
49
+ deterministic: `sha256("{project}::{kind}::{qualified_name}")[:16]`. Boundary
50
+ IDs deliberately depend only on `mechanism` + normalized key, which is what
51
+ makes a server in one language and a client in another collapse into one node.
52
+
53
+ Two ordering constraints are load-bearing and easy to break:
54
+
55
+ - `Graph` stores nodes in an `IndexMap`, not a `HashMap` — insertion order is
56
+ part of the output and a `HashMap` silently reshuffles `CONTAINS` edges.
57
+ - `src/roots.rs::sort_paths` compares paths **component by component**, the
58
+ way Python's `sorted()` over `pathlib.Path` does. Sorting by the whole
59
+ string diverges on neighbours like `src/v4-mini/…` vs `src/v4/…`.
60
+
61
+ ### Anatomy of a language adapter
62
+
63
+ Each of `src/python/`, `src/typescript/`, `src/golang/`, `src/rustlang/`
64
+ follows the same layout:
65
+
66
+ | File | Role |
67
+ |---|---|
68
+ | `mod.rs` | thread-local tree-sitter parser, re-exports |
69
+ | `detector.rs` / `deps.rs` | project roots, manifest parsing, import classification |
70
+ | `visitor.rs` | CST walk → nodes, `DECLARES`/`CONTAINS`/`RESOLVES_TO` edges, occurrences |
71
+ | `boundary.rs` | cross-language ports via tree-sitter queries |
72
+ | `resolver.rs` | symbol resolution backend |
73
+ | `adapter.rs` | orchestration of `analyze()` |
74
+
75
+ **Visitors never create CALLS / REFERENCES / HAS_TYPE / INHERITS_FROM edges.**
76
+ They emit `OccurrenceRef`s (`src/occurrence.rs`) with a role — `call`, `read`,
77
+ `write`, `annotation`, `base` — which the resolution pass turns into edges.
78
+
79
+ `analyze()` always runs three phases, and the order is not cosmetic:
80
+
81
+ 1. **Structure** for every sub-root, no resolution. The `SpanIndex` built next
82
+ has to span the whole workspace, or cross-root definitions have nothing to
83
+ bind to.
84
+ 2. **One resolver for the whole call.** Per-root resolvers would both lose
85
+ cross-root references and re-index the workspace once per root.
86
+ 3. **Boundaries**, so BOUNDARY nodes land in the graph after resolver edges.
87
+
88
+ Resolution maps a definition position back to a node through
89
+ `SpanIndex::lookup_name` (name spans, not full extents). Anything that misses
90
+ falls through to an `EXTERNAL_SYMBOL` so the edge is never lost.
91
+
92
+ ### Node schemes differ per language
93
+
94
+ Do not assume Python's shape elsewhere. `MODULE` is a dotted name in Python and
95
+ TypeScript, a **package directory** in Go (its `qualified_name` equals the
96
+ import path, so internal imports bind by direct lookup), and a Rust module path
97
+ in `rustlang`. Rust dispatches `impl` blocks in a second pass because a type may
98
+ be declared below its `impl`.
99
+
100
+ ### Resolvers: what is linked in and what is not
101
+
102
+ | Language | Backend | External requirement |
103
+ |---|---|---|
104
+ | Python | `ty` linked as a library (`src/python/ty_embedded.rs`) | none |
105
+ | TypeScript | typescript-go via a Go c-archive (`go/bridge.go`) | none |
106
+ | Go | `go/packages` + `go/types` via the same c-archive (`go/bridge_go.go`) | Go toolchain |
107
+ | Rust | `rust-analyzer scip` subprocess + own SCIP decoder (`src/rustlang/scip.rs`) | rust-analyzer + Cargo |
108
+
109
+ Go and Rust cannot be compiled in: both need their standard library's sources
110
+ and a real workspace load. `build.rs` clones typescript-go into `.ts-go/`
111
+ (~370 MB, survives `cargo clean`) and builds `go/` into a c-archive. That
112
+ c-archive is why **Windows is unsupported** — cgo emits a mingw-w64 GNU
113
+ archive that will not link into the MSVC target; `build.rs` refuses up front.
114
+ It also derives GOOS/GOARCH from the Rust target so an Apple Intel wheel can
115
+ be cross-built on an Apple Silicon runner, and passes `-buildvcs=false`
116
+ because `go build` otherwise fails where git will not read the checkout (a
117
+ manylinux container mounts the workspace under another uid).
118
+
119
+ Resolvers are swappable from Python — any object with `prepare`/`resolve_all`/
120
+ `status` — as are dependency parsers (`can_parse`/`parse`). Rust calls them
121
+ through the ordinary Python protocol.
122
+
123
+ ## Parity with graphlens
124
+
125
+ This is the project's correctness harness. graphlens lives at
126
+ `~/project/graphlens` with its own venv at `~/project/graphlens/.venv/bin/python`
127
+ (callix's venv does not have graphlens's tree-sitter grammars installed).
128
+
129
+ Method: build the same graph both ways → `to_dict()` →
130
+ `json.dumps(sort_keys=True)` → `diff`. Every subtle bug found so far (key
131
+ ordering, HashMap vs IndexMap, path sorting) surfaced only this way. When
132
+ comparing structure only, use `Adapter(resolve=False)` on the callix side and a
133
+ stub resolver on graphlens's; the metrics block will differ because callix skips
134
+ the pass entirely, and that is expected.
135
+
136
+ Re-snapshot graphlens baselines before trusting a diff — a stale dump once
137
+ produced a fake 448k-line regression.
138
+
139
+ **One intentional divergence, applied to all four languages.** When the resolver
140
+ finds a definition it cannot name, the synthetic `EXTERNAL_SYMBOL` key includes
141
+ the project-relative file path (`{role}@{path}:{line}:{col}`). graphlens omits
142
+ the path, which merges distinct sites that share coordinates — 98% of external
143
+ nodes on apache/superset. Expect diffs confined to those nodes' ids/names.
144
+
145
+ ## Build gotchas
146
+
147
+ - **`PYO3_PYTHON`** is set globally in the Taskfile to `.venv/bin/python`.
148
+ `abi3-py313` refuses anything older than 3.13, and bare `cargo` otherwise
149
+ picks the first `python3` on PATH.
150
+ - **All cargo tasks share one profile**, release by default. The ty/ruff tree is
151
+ ~200 crates; linting in debug while building in release compiles it twice.
152
+ - **`get-size2` is pinned to `=0.10.0`** with a hand-written feature list.
153
+ Outside ruff's workspace the features are not inherited, and 0.10.3 moved to
154
+ `compact_str` 0.10 while ruff is on 0.9 — without the pin `ruff_python_ast`
155
+ does not compile at all.
156
+ - **ty comes from a git `rev`** pinned across several `Cargo.toml` entries;
157
+ updating means changing all of them together and re-verifying.
158
+ - Large SCIP indexes and Go builds respect `TMPDIR`; a small `/tmp` will fill.
159
+ - `cargo fmt` is deliberately not a task — formatting is hand-tuned in places.
160
+
161
+ ## Stubs and release
162
+
163
+ `python/callix/_core/__init__.pyi` is generated by `src/bin/stub_gen.rs` from
164
+ the Rust doc comments and **is committed**; CI fails on drift. Module constants
165
+ and an exception-base-class fixup are appended manually there because
166
+ pyo3-stub-gen 0.23 does not emit them correctly.
167
+
168
+ Versions live **only** in `Cargo.toml` (`pyproject.toml` declares it `dynamic`).
169
+ `cliff.toml` drives both conventional-commit parsing and the changelog template;
170
+ its `[remote.github]` owner/repo must match the real repository or git-cliff
171
+ panics on a 404. Wheels are built by a per-platform matrix in `release.yml` —
172
+ one wheel cannot cover every platform with ty and the Go bridge linked inside.