scopia 1.0.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 (53) hide show
  1. scopia-1.0.0/.gitignore +10 -0
  2. scopia-1.0.0/CHANGELOG.md +37 -0
  3. scopia-1.0.0/CONTRIBUTING.md +72 -0
  4. scopia-1.0.0/LICENSE +21 -0
  5. scopia-1.0.0/PKG-INFO +378 -0
  6. scopia-1.0.0/README.md +344 -0
  7. scopia-1.0.0/pyproject.toml +73 -0
  8. scopia-1.0.0/scopia/__init__.py +3 -0
  9. scopia-1.0.0/scopia/cli.py +363 -0
  10. scopia-1.0.0/scopia/graph/__init__.py +0 -0
  11. scopia-1.0.0/scopia/graph/build.py +748 -0
  12. scopia-1.0.0/scopia/graph/model.py +172 -0
  13. scopia-1.0.0/scopia/index/__init__.py +0 -0
  14. scopia-1.0.0/scopia/index/repo.py +232 -0
  15. scopia-1.0.0/scopia/index/store.py +70 -0
  16. scopia-1.0.0/scopia/lang/__init__.py +69 -0
  17. scopia-1.0.0/scopia/lang/_ts.py +71 -0
  18. scopia-1.0.0/scopia/lang/base.py +110 -0
  19. scopia-1.0.0/scopia/lang/java.py +290 -0
  20. scopia-1.0.0/scopia/lang/php.py +244 -0
  21. scopia-1.0.0/scopia/lang/python.py +280 -0
  22. scopia-1.0.0/scopia/lang/react.py +590 -0
  23. scopia-1.0.0/scopia/lsp/__init__.py +6 -0
  24. scopia-1.0.0/scopia/lsp/client.py +218 -0
  25. scopia-1.0.0/scopia/lsp/resolve.py +310 -0
  26. scopia-1.0.0/scopia/lsp/session.py +193 -0
  27. scopia-1.0.0/scopia/render/__init__.py +0 -0
  28. scopia-1.0.0/scopia/render/html.py +1000 -0
  29. scopia-1.0.0/scopia/render/layout.py +190 -0
  30. scopia-1.0.0/scopia/render/mermaid.py +66 -0
  31. scopia-1.0.0/scopia/render/text.py +313 -0
  32. scopia-1.0.0/scopia/vcs/__init__.py +0 -0
  33. scopia-1.0.0/scopia/vcs/diff.py +64 -0
  34. scopia-1.0.0/scopia/vcs/git.py +223 -0
  35. scopia-1.0.0/scopia/watch/__init__.py +1 -0
  36. scopia-1.0.0/scopia/watch/frames.py +79 -0
  37. scopia-1.0.0/scopia/watch/fresh.py +46 -0
  38. scopia-1.0.0/scopia/watch/live.py +62 -0
  39. scopia-1.0.0/scopia/watch/loop.py +136 -0
  40. scopia-1.0.0/scopia/watch/server.py +83 -0
  41. scopia-1.0.0/scopia/watch/terminal.py +82 -0
  42. scopia-1.0.0/tests/__init__.py +0 -0
  43. scopia-1.0.0/tests/conftest.py +236 -0
  44. scopia-1.0.0/tests/fake_lsp_server.py +80 -0
  45. scopia-1.0.0/tests/fixtures/__init__.py +0 -0
  46. scopia-1.0.0/tests/fixtures/php_repo.py +104 -0
  47. scopia-1.0.0/tests/fixtures/py_repo.py +165 -0
  48. scopia-1.0.0/tests/fixtures/react_repo.py +279 -0
  49. scopia-1.0.0/tests/test_cli.py +167 -0
  50. scopia-1.0.0/tests/test_graph.py +920 -0
  51. scopia-1.0.0/tests/test_lsp.py +269 -0
  52. scopia-1.0.0/tests/test_scale.py +105 -0
  53. scopia-1.0.0/tests/test_watch.py +338 -0
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .DS_Store
10
+ .idea/
@@ -0,0 +1,37 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0 — 2026-10-02
4
+
5
+ First release.
6
+
7
+ - Diff → touched-symbol graph for Python, PHP and React
8
+ - One-line changes are first-class: the unit of analysis is the enclosing symbol, and
9
+ nodes are weighted by fan-in rather than diff size
10
+ - Every edge carries provenance; a call through an unknown receiver is never presented
11
+ as certain, and uncertain edges state why
12
+ - PHP resolves the class a call site names — `Service::class`, `new Service()`,
13
+ `Service::method()`, typed properties, and inherited methods
14
+ - Python resolves aliased imports (`from x import run as v3`)
15
+ - React reads `.jsx`, `.tsx`, `.js` and `.ts` as one language: `<Button />` is a call
16
+ edge, `const`/`memo`/`forwardRef`/`useCallback` components are definitions, a default
17
+ import under another local name still resolves, and a declared TypeScript type
18
+ narrows a call to the class it names
19
+ - Untracked files are reviewed; `git diff` cannot see them
20
+ - Files in unsupported languages are listed rather than silently omitted
21
+ - Repo index cached per git blob SHA inside `.git/`, so warm runs re-parse nothing
22
+ - `--utility` hides widely-called plumbing; `--exclude` skips paths
23
+ - Text, mermaid, JSON, and a self-contained clickable HTML report
24
+ - A gate that declines to draw a graph when the diff is too small to need one
25
+ - `--watch`: redraws as files settle (debounced, polled from `git status` with no
26
+ file-watching dependency), holds the last good graph while a file does not parse,
27
+ skips redraws that change nothing, and marks what arrived since the last redraw
28
+ - `--watch --html`: a live page served on 127.0.0.1 that patches itself in place,
29
+ keeping the selected node and scroll position across redraws
30
+ - Java: `.java`, parsed through tree-sitter with no JDK; receivers resolve through
31
+ declared parameter, field and local types
32
+ - `--lsp`: optional language-server resolution (Python via `jedi-language-server`).
33
+ Confirmed edges are marked `resolved`; a definition outside the repo drops the
34
+ name-matched edges. Never required, never an error when absent
35
+ - `--to-entry`: climbs callers until each chain reaches something nothing in the repo
36
+ calls — the route, job or command that triggers the changed code
37
+ - Fixed: a long call chain in a large uncapped graph exhausted the recursion limit
@@ -0,0 +1,72 @@
1
+ # Contributing
2
+
3
+ ## Adding a language
4
+
5
+ This is the contribution that matters most, and it is deliberately small: **one file**.
6
+
7
+ Everything outside `scopia/lang/` is language-neutral. The index, the graph builder
8
+ and all four renderers only ever see `FileFacts`, so a new language never requires a
9
+ change above the seam.
10
+
11
+ An adapter implements one method:
12
+
13
+ ```python
14
+ class MyLangAdapter:
15
+ name = "mylang"
16
+ extensions = (".ml",)
17
+
18
+ def extract(self, source: bytes) -> FileFacts:
19
+ ...
20
+ ```
21
+
22
+ `FileFacts` holds three things, all defined in `scopia/lang/base.py`:
23
+
24
+ | Field | What it is |
25
+ |---|---|
26
+ | `defs` | every named definition, with its line range and a path-independent `qualname` |
27
+ | `calls` | every call site, recorded by the **name** being called |
28
+ | `bases` | classes naming a base class or implemented interface |
29
+
30
+ Two rules matter:
31
+
32
+ 1. **Record calls by name, don't resolve them.** Resolution happens in the index, where
33
+ the whole repo is visible. An adapter that guesses which definition a call refers to
34
+ is doing the wrong job in the wrong place.
35
+ 2. **Keep `qualname` path-independent** (`Class.method`, not `src/a.py::Class.method`).
36
+ The fact cache is keyed by git blob SHA, so identical content anywhere in the repo
37
+ reuses the same entry.
38
+
39
+ Then register it in `scopia/lang/__init__.py::_registry`, and add a fixture repo under
40
+ `tests/fixtures/` covering the four shapes (`py_repo.py` is the reference).
41
+
42
+ A language that needs more than one grammar registers one instance per grammar and
43
+ gives them all the same `name` — `react.py` does this for `.js`, `.ts` and `.tsx`.
44
+ `name` is what stops a call crossing a language boundary, so two grammars parsing one
45
+ language must agree on it, or every import between them is dropped as a false edge.
46
+
47
+ Give the adapter a `grammar` attribute naming its tree-sitter grammar. Watch mode uses it
48
+ to tell a finished file from a half-written one: tree-sitter never raises on bad input, so
49
+ without it a syntax error looks identical to "no symbols".
50
+
51
+ Grammars should ship pre-built abi3 wheels. A dependency that needs a C toolchain at
52
+ install time breaks the "download and run" promise for exactly the people this tool is
53
+ for.
54
+
55
+ ## Running things
56
+
57
+ ```bash
58
+ python -m venv .venv && .venv/bin/pip install -e ".[dev]"
59
+ .venv/bin/python -m pytest tests/ -q
60
+ .venv/bin/ruff check .
61
+ ```
62
+
63
+ ## What this tool is not
64
+
65
+ Pull requests that turn scopia into a linter, a bug finder, or a code-quality scorer
66
+ will be declined. It maps what changed so a human reads less; it does not pass judgement
67
+ on the code. Output wording follows from that — "differs from the other five", never
68
+ "flagged" or "suspicious".
69
+
70
+ Similarly, an edge whose target is uncertain must fan out to every candidate and say so.
71
+ Picking the likeliest one produces a graph that looks confident and is sometimes wrong,
72
+ which is worse than one that is visibly unsure.
scopia-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 diffgraph contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
scopia-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,378 @@
1
+ Metadata-Version: 2.5
2
+ Name: scopia
3
+ Version: 1.0.0
4
+ Summary: See the shape of a code change before reading its lines.
5
+ Project-URL: Homepage, https://github.com/printSamarth/scopia
6
+ Project-URL: Issues, https://github.com/printSamarth/scopia/issues
7
+ Author: Samarth Patel
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: call-graph,code-review,diff,git,java,react,static-analysis
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Quality Assurance
20
+ Classifier: Topic :: Software Development :: Version Control :: Git
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: tree-sitter-java>=0.23
23
+ Requires-Dist: tree-sitter-javascript>=0.23
24
+ Requires-Dist: tree-sitter-php>=0.23
25
+ Requires-Dist: tree-sitter-python>=0.23
26
+ Requires-Dist: tree-sitter-typescript>=0.23
27
+ Requires-Dist: tree-sitter<0.27,>=0.23
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=8; extra == 'dev'
30
+ Requires-Dist: ruff>=0.6; extra == 'dev'
31
+ Provides-Extra: lsp
32
+ Requires-Dist: jedi-language-server>=0.40; extra == 'lsp'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # scopia
36
+
37
+ **σκοπιά — the watchtower. See what your AI is building, while it builds it.**
38
+
39
+ <p align="center">
40
+ <img src="https://raw.githubusercontent.com/printSamarth/scopia/main/docs/watch.gif" alt="scopia --watch --html drawing a call graph live as an AI agent writes code" width="900">
41
+ </p>
42
+
43
+ AI agents write code at a speed and volume no reviewer can match. By the time the agent
44
+ says *done*, you are staring at a twenty-file diff in alphabetical order, with no idea
45
+ which piece matters or how the pieces connect. The hard part of review is no longer
46
+ reading the code — it is getting **the lay of the land**.
47
+
48
+ `scopia` is a visualisation layer for code changes. It draws **one graph** — what
49
+ changed, what calls it, and what it calls — and it does so two ways:
50
+
51
+ - **Live** — watch the graph grow in real time *as the agent writes*, so you already
52
+ understand the change before it finishes.
53
+ - **Static** — point it at any commit, branch or range and get the same graph for code
54
+ that was already committed.
55
+
56
+ No config, no init step, no language servers required. Just git and Python.
57
+
58
+ ---
59
+
60
+ ## Quick start
61
+
62
+ ```bash
63
+ pip install scopia # or: uv tool install scopia / pipx install scopia
64
+ ```
65
+
66
+ Run these from anywhere inside a git repository:
67
+
68
+ | I want to… | Command |
69
+ |---|---|
70
+ | **Watch the graph build live** while an agent works | `scopia --watch --html` |
71
+ | Watch live in the terminal instead | `scopia --watch` |
72
+ | Review my uncommitted work | `scopia` |
73
+ | Analyse a past commit | `scopia <sha>~1..<sha>` |
74
+ | Review a branch (the pull-request case) | `scopia main..` |
75
+ | Analyse the last 5 commits | `scopia HEAD~5..HEAD` |
76
+ | Save a clickable report to share | `scopia main.. --html review.html` |
77
+ | Paste a diagram into a PR comment | `scopia --format mermaid` |
78
+ | Feed it to other tools | `scopia --format json` |
79
+ | Trace each change back to the route/job/command that triggers it | `scopia --to-entry` |
80
+ | Sharpen uncertain edges with a language server | `scopia --lsp` |
81
+
82
+ Requires **Python 3.10+** and **git**. Every dependency ships as a pre-built wheel, so no
83
+ compiler is needed. Check with `scopia --version`.
84
+
85
+ ---
86
+
87
+ ## Live mode: review while the agent is still typing
88
+
89
+ ```bash
90
+ scopia --watch --html
91
+ ```
92
+
93
+ This opens a live page in your browser. Start your agent in another window and watch the
94
+ graph fill in as files land: changed symbols in green, the code they touch around them,
95
+ new arrivals marked `● just now`.
96
+
97
+ - **The page patches itself in place.** The node you are reading stays selected and the
98
+ view does not jump when the agent saves.
99
+ - **Half-written code is held, not thrashed.** A file that does not parse yet keeps the
100
+ last good graph on screen, with a note.
101
+ - **No-op rewrites are ignored.** A formatter or a `touch` that leaves the graph
102
+ unchanged causes no redraw.
103
+ - **Private by default.** It is served on `127.0.0.1` only (it serves your source), on a
104
+ random port. `--no-open` prints the URL instead of opening a browser.
105
+
106
+ ```bash
107
+ scopia --watch # same idea, rendered in the terminal
108
+ scopia --watch --html r.html # live page, and keep r.html up to date too
109
+ ```
110
+
111
+ Ctrl-C leaves your terminal as it found it.
112
+
113
+ ---
114
+
115
+ ## Static mode: understand a commit after the fact
116
+
117
+ The same graph works on history. Give it a commit and it shows what that commit changed
118
+ and what the change reaches — useful for reviewing a teammate's (or an agent's) PR, for
119
+ auditing something that already merged, or for onboarding onto unfamiliar code.
120
+
121
+ ```bash
122
+ scopia HEAD~1..HEAD --html commit.html && open commit.html
123
+ ```
124
+
125
+ <p align="center">
126
+ <img src="https://raw.githubusercontent.com/printSamarth/scopia/main/docs/commit-graph.png" alt="scopia HTML report for a 16-file commit adding refunds to a payments codebase" width="900">
127
+ </p>
128
+
129
+ A real 16-file commit adding refunds to a payments codebase. Left to right: seven provider
130
+ classes gained a `calculate_refund`, and a new checkout path runs four layers deep to
131
+ land on `util.clamp` — a **one-line edit to existing code that 13 places call**, drawn
132
+ with an amber border. Dashed boxes are unchanged context; the five files at the bottom are
133
+ changed but untraceable (`NO CONNECTIONS FOUND`). Click any node to focus its
134
+ neighbourhood; the rest recedes rather than disappearing. One self-contained file — no
135
+ CDN, no network, opens from disk years from now.
136
+
137
+ Pass `<sha>~1..<sha>` for "what this commit changed":
138
+
139
+ ```bash
140
+ scopia 3036712be90~1..3036712be90
141
+ ```
142
+
143
+ `~1` has no meaning on a repo's first commit; use `scopia <sha>` there. Skip merge
144
+ commits — `~1` follows only the first parent and reports something misleading.
145
+
146
+ ---
147
+
148
+ ## Reading the output
149
+
150
+ The same commit as plain text:
151
+
152
+ ```
153
+ $ scopia HEAD~1..HEAD
154
+
155
+ 16 symbols touched across 16 files (10 shown for context)
156
+
157
+ CALL CHAINS — indented → means the line above calls it
158
+ checkout_form.CheckoutForm.submit 1 line changed · existing code app/
159
+ └→ checkout_controller.CheckoutController.apply_promo_code 3 lines changed app/
160
+ └→ discount_service.DiscountService.validate 4 lines changed app/
161
+ └→ pricing_engine.PricingEngine.recalculate 3 lines changed app/
162
+ └→ util.clamp 1 line changed · called from 13 places · existing code app/
163
+
164
+ stripe.StripeProvider.calculate_refund 3 lines changed app/providers/
165
+ └→ stripe.StripeProvider.fees called from ~6 places · context app/providers/
166
+ └→ util.clamp 1 line changed · called from 13 places · existing code app/
167
+ ┈ implements refundable.Refundable.calculate_refund
168
+
169
+ NO CONNECTIONS FOUND — scopia could not trace these; not a statement that they are safe
170
+ billing.py 1 line changed config/
171
+ inventory.py 1 line changed config/
172
+ ```
173
+
174
+ Three things a file list cannot tell you, all visible at a glance:
175
+
176
+ 1. **A new checkout path runs four layers deep** and bottoms out in `util.clamp` — a
177
+ one-line edit to existing code that **13 places call**. The riskiest change in the
178
+ diff is one line long.
179
+ 2. **Several providers implement the same new interface method**, grouped by their
180
+ `implements` edges — read the pattern once instead of seven times.
181
+ 3. **`ApplepayProvider` never calls `clamp`**, unlike its siblings. scopia does not
182
+ claim that is wrong, only that it differs — which is where attention belongs.
183
+
184
+ | Section | What it means |
185
+ |---|---|
186
+ | `CALL CHAINS` | What changed and what it reaches. Indentation is a call: the line above calls the line below. Heaviest chain first. |
187
+ | `CHANGED, REFERENCED FROM` | Changed things that call nothing themselves, shown with who calls *them*. Small edits with wide reach land here. |
188
+ | `NO CONNECTIONS FOUND` | scopia could not trace these. **Not** a claim that they are safe. |
189
+ | `NOT ANALYSED` | Changed files in unsupported languages. The graph says nothing about them. |
190
+ | `NOT CERTAIN` | Edges matched by name that may be wrong, each with its reason. Leads, not facts. |
191
+
192
+ | Annotation | Meaning |
193
+ |---|---|
194
+ | `3 lines changed` | Lines changed inside that symbol |
195
+ | `new` | The file did not exist before |
196
+ | `existing code` | Edits something that was already there — other code may depend on it |
197
+ | `called from 12 places` | Repo-wide caller count |
198
+ | `~12` | Count is shared across several same-named definitions; approximate |
199
+ | `context` | Unchanged, shown only to orient you |
200
+ | `+3 hidden` | Neighbours cut by `--max-nodes` |
201
+
202
+ **The number that matters is callers, not lines.** `1 line changed · called from 40
203
+ places` deserves more attention than a fifty-line change nothing calls.
204
+
205
+ ### Mermaid and JSON
206
+
207
+ `--format mermaid` prints a diagram GitHub renders natively — paste it straight into a
208
+ pull-request comment:
209
+
210
+ ```mermaid
211
+ flowchart LR
212
+ n0["CheckoutForm.submit"] -->|"1"| n1["CheckoutController.apply_promo_code"]
213
+ n1 -->|"2"| n2["DiscountService.validate"]
214
+ n2 -->|"3"| n3["PricingEngine.recalculate"]
215
+ n3 -->|"4"| n4["util.clamp<br/>13 callers"]
216
+ n5["StripeProvider.calculate_refund"] --> n4
217
+ n5 -.->|"implements"| n6["Refundable.calculate_refund"]
218
+ class n4 hot
219
+ classDef hot stroke-width:3px,stroke:#b45309
220
+ ```
221
+
222
+ `--format json` is for anything downstream. Every edge carries its provenance
223
+ (`static`, `inferred`, …), so a consumer can tell a resolved call from a guess. Nodes
224
+ carry `touched`, `added`, `fan_in`, `changed_lines`; the top level reports `truncated`,
225
+ `utilities_hidden`, `files_changed` and `unparsed`.
226
+
227
+ ---
228
+
229
+ ## Options
230
+
231
+ ```
232
+ scopia [revisions] [options]
233
+
234
+ --html [PATH] write a self-contained, clickable HTML report
235
+ (with --watch and no PATH: serve a live page)
236
+ --watch follow the working tree and redraw as files settle
237
+ --to-entry climb callers until each chain reaches an entry point
238
+ --lsp confirm name-matched calls with a language server, if installed
239
+ --no-open with --watch --html, print the URL instead of opening it
240
+ --format FORMAT text (default), mermaid, or json
241
+ --hops N neighbour hops to expand (default: 1)
242
+ --max-nodes N cap on drawn nodes (default: 60)
243
+ --utility N hide unchanged helpers called from more than N places
244
+ (default: 12; 0 keeps everything)
245
+ --exclude GLOB skip paths matching GLOB (repeatable)
246
+ --always draw even when the diff is below the gate
247
+ --jobs N parser processes used when indexing
248
+ --no-color disable ANSI colour
249
+ --version print the version
250
+ ```
251
+
252
+ **Too noisy?** Hide shared plumbing — a logger every handler calls says nothing about
253
+ *this* change — or exclude paths (`vendor/`, `node_modules/`, `.venv/`, `migrations/`,
254
+ `dist/`, `build/` and similar are already skipped):
255
+
256
+ ```bash
257
+ scopia --utility 6
258
+ scopia --exclude 'tests/*' --exclude 'generated/*'
259
+ ```
260
+
261
+ **Too small?** Raise the node cap if the report says nodes were hidden (edges to cut
262
+ nodes are not drawn, which can make a symbol look unreferenced), look further out, or
263
+ force a graph for a tiny diff:
264
+
265
+ ```bash
266
+ scopia --max-nodes 300
267
+ scopia --hops 2
268
+ scopia --always
269
+ ```
270
+
271
+ ---
272
+
273
+ ## Languages
274
+
275
+ | Language | Extensions | Status |
276
+ |---|---|---|
277
+ | Python | `.py` `.pyi` | supported — including aliased imports (`from x import run as v3`) |
278
+ | PHP | `.php` `.phtml` | supported — no PHP runtime needed, parsed via tree-sitter |
279
+ | Java | `.java` | supported — no JDK needed; reads the type each parameter, field and local declares |
280
+ | React | `.jsx` `.tsx` `.js` `.ts` `.mjs` `.cjs` `.mts` `.cts` | supported — no Node, no `tsc`, no `node_modules` |
281
+
282
+ **PHP** resolves the class a call site names — `app(Service::class)->show()`,
283
+ `new Service()`, `Service::show()`, typed properties — including inherited methods. On a
284
+ Laravel-shaped app that removed roughly a third of all edges as false.
285
+
286
+ **React:** `<Button />` is a call (composition *is* the call graph; `<div>` is left
287
+ alone); `const Panel = () => …`, `memo(forwardRef(…))`, `useCallback(…)` and
288
+ `styled.div\`…\`` are definitions, so a one-line edit inside a handler is attributed to
289
+ the handler rather than the 200-line component around it; hooks are ordinary calls;
290
+ handlers wired through props are followed and listed as references. TypeScript is read
291
+ where it states a type, and `.js`/`.ts`/`.tsx` are one language, so a call between them
292
+ is an ordinary edge.
293
+
294
+ ### Sharper edges with a language server
295
+
296
+ ```bash
297
+ pip install "scopia[lsp]" # adds jedi-language-server — pure Python, no Node
298
+ scopia --lsp # or: scopia --watch --lsp
299
+ ```
300
+
301
+ Where scopia could only match a call by name, `--lsp` asks a language server where the
302
+ call really goes. One confirmed target becomes a solid edge; a definition outside your
303
+ repo (`dict.get`, the standard library) removes the name-matched edges outright. If no
304
+ server is installed, or it fails or times out, you get exactly the graph you would have
305
+ had without the flag — plus a line saying so. In watch mode the graph is drawn from
306
+ tree-sitter immediately and upgraded when answers arrive, so a slow server never stalls
307
+ a redraw.
308
+
309
+ Measured on Python (uncertain edges as a share of all edges, before → after):
310
+
311
+ | Repo | Before | After |
312
+ |---|---|---|
313
+ | scopia | 30% | 4% |
314
+ | pygls | 67% | 10% |
315
+ | parso | 64% | 21% |
316
+ | jedi (190,000 edges, uncapped) | 67% | 64% |
317
+
318
+ The last row is the honest limit: in heavily dynamic code a language server sharpens what
319
+ types can settle, and does not conjure types that are not there. Python is the only
320
+ language wired up so far.
321
+
322
+ Adding a language means writing **one adapter**. See [CONTRIBUTING.md](CONTRIBUTING.md).
323
+
324
+ ---
325
+
326
+ ## The ideas it is built on
327
+
328
+ - **A one-line change is a first-class change.** The unit of analysis is the enclosing
329
+ function, never the size of the hunk. A one-line sign flip in a helper called from forty
330
+ places outranks a two-hundred-line rewrite of something nobody calls.
331
+ - **Uncertainty is shown, never disguised.** Every edge carries its provenance; a call
332
+ through an unknown receiver is marked `inferred` even when only one candidate exists.
333
+ - **It does not claim knowledge it lacks.** Unsupported files are listed, not omitted.
334
+ Untraceable symbols are "no connections found", not "no edges" — a reviewer reads the
335
+ second as "safe to skip".
336
+ - **It is not a correctness checker.** scopia does not judge code or hunt for bugs; it
337
+ reduces how much you must read. The metric it optimises is hunks a reviewer *didn't*
338
+ have to open.
339
+ - **Small diffs get no graph.** Touch fewer than three files with nothing connecting them
340
+ and scopia tells you to just read the diff.
341
+
342
+ ---
343
+
344
+ ## How it works
345
+
346
+ 1. `git diff --unified=0` gives exact changed line ranges; context lines would overstate
347
+ the blast radius. Untracked files are added separately.
348
+ 2. Each changed line maps to its **smallest enclosing definition**.
349
+ 3. A symbol and reference index over the repo, cached per git blob SHA in `.git/scopia/`,
350
+ so only files whose content actually changed are ever re-parsed.
351
+ 4. One hop of expansion to callers and callees — never a transitive closure, so graph
352
+ size scales with the diff rather than the repo.
353
+ 5. Edges resolved and tagged by how the call named its target.
354
+
355
+ A 1,200-file repo with a 200-file diff: **1.7s cold, 0.9s warm.** To force a clean
356
+ re-index, `rm -rf .git/scopia`.
357
+
358
+ ---
359
+
360
+ ## Development
361
+
362
+ ```bash
363
+ git clone https://github.com/printSamarth/scopia && cd scopia
364
+ python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
365
+ .venv/bin/python -m pytest -q
366
+ .venv/bin/ruff check .
367
+ ```
368
+
369
+ ## Status
370
+
371
+ First stable release. The graph, live watch mode and all four language adapters work. Planned: hunk
372
+ clustering (read a repeated pattern once, then only what differs), framework/DI edge
373
+ recognition, and runtime-trace ingestion for the dynamic dispatch static analysis cannot
374
+ see.
375
+
376
+ ## License
377
+
378
+ MIT