codeterrain 0.3.0__tar.gz → 0.3.1__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 (101) hide show
  1. {codeterrain-0.3.0 → codeterrain-0.3.1}/AGENTS.md +4 -3
  2. {codeterrain-0.3.0 → codeterrain-0.3.1}/CHANGELOG.md +23 -1
  3. {codeterrain-0.3.0 → codeterrain-0.3.1}/PKG-INFO +88 -59
  4. {codeterrain-0.3.0 → codeterrain-0.3.1}/README.md +87 -58
  5. {codeterrain-0.3.0 → codeterrain-0.3.1}/SECURITY.md +13 -12
  6. codeterrain-0.3.1/docs/charts/make_charts.py +391 -0
  7. {codeterrain-0.3.0 → codeterrain-0.3.1}/docs/commands.md +31 -31
  8. {codeterrain-0.3.0 → codeterrain-0.3.1}/docs/configuration.md +8 -8
  9. {codeterrain-0.3.0 → codeterrain-0.3.1}/docs/design-v0.2.md +3 -3
  10. {codeterrain-0.3.0 → codeterrain-0.3.1}/docs/extractor-protocol.md +5 -5
  11. {codeterrain-0.3.0 → codeterrain-0.3.1}/docs/how-it-works.md +4 -4
  12. codeterrain-0.3.1/docs/img/architecture-dark.svg +43 -0
  13. codeterrain-0.3.1/docs/img/architecture-light.svg +43 -0
  14. {codeterrain-0.3.0 → codeterrain-0.3.1}/docs/img/field-dark.svg +14 -13
  15. {codeterrain-0.3.0 → codeterrain-0.3.1}/docs/img/field-light.svg +14 -13
  16. codeterrain-0.3.1/docs/img/prior-art-dark.svg +165 -0
  17. codeterrain-0.3.1/docs/img/prior-art-light.svg +165 -0
  18. {codeterrain-0.3.0 → codeterrain-0.3.1}/docs/img/speed-dark.svg +7 -7
  19. {codeterrain-0.3.0 → codeterrain-0.3.1}/docs/img/speed-light.svg +7 -7
  20. {codeterrain-0.3.0 → codeterrain-0.3.1}/docs/img/tokens-dark.svg +2 -2
  21. {codeterrain-0.3.0 → codeterrain-0.3.1}/docs/img/tokens-light.svg +2 -2
  22. {codeterrain-0.3.0 → codeterrain-0.3.1}/docs/limits.md +8 -8
  23. codeterrain-0.3.1/docs/prior-art.md +95 -0
  24. {codeterrain-0.3.0 → codeterrain-0.3.1}/docs/why-codeterrain.md +28 -22
  25. {codeterrain-0.3.0 → codeterrain-0.3.1}/packages/codeterrain-terraform/README.md +9 -10
  26. {codeterrain-0.3.0 → codeterrain-0.3.1}/packages/codeterrain-terraform/pyproject.toml +1 -1
  27. {codeterrain-0.3.0 → codeterrain-0.3.1}/packages/codeterrain-typescript/README.md +8 -9
  28. {codeterrain-0.3.0 → codeterrain-0.3.1}/packages/codeterrain-typescript/pyproject.toml +1 -1
  29. {codeterrain-0.3.0 → codeterrain-0.3.1}/pyproject.toml +2 -1
  30. {codeterrain-0.3.0 → codeterrain-0.3.1}/src/codeterrain/__init__.py +1 -1
  31. {codeterrain-0.3.0 → codeterrain-0.3.1}/uv.lock +3 -3
  32. codeterrain-0.3.0/docs/charts/make_charts.py +0 -239
  33. {codeterrain-0.3.0 → codeterrain-0.3.1}/.gitattributes +0 -0
  34. {codeterrain-0.3.0 → codeterrain-0.3.1}/.github/workflows/ci.yml +0 -0
  35. {codeterrain-0.3.0 → codeterrain-0.3.1}/.github/workflows/release.yml +0 -0
  36. {codeterrain-0.3.0 → codeterrain-0.3.1}/.gitignore +0 -0
  37. {codeterrain-0.3.0 → codeterrain-0.3.1}/CLAUDE.md +0 -0
  38. {codeterrain-0.3.0 → codeterrain-0.3.1}/LICENSE +0 -0
  39. {codeterrain-0.3.0 → codeterrain-0.3.1}/NOTICE +0 -0
  40. {codeterrain-0.3.0 → codeterrain-0.3.1}/docs/img/accuracy-dark.svg +0 -0
  41. {codeterrain-0.3.0 → codeterrain-0.3.1}/docs/img/accuracy-light.svg +0 -0
  42. {codeterrain-0.3.0 → codeterrain-0.3.1}/docs/img/logo.svg +0 -0
  43. {codeterrain-0.3.0 → codeterrain-0.3.1}/docs/sandbox.md +0 -0
  44. {codeterrain-0.3.0 → codeterrain-0.3.1}/packages/codeterrain-terraform/src/codeterrain_terraform/__init__.py +0 -0
  45. {codeterrain-0.3.0 → codeterrain-0.3.1}/packages/codeterrain-terraform/tests/test_terraform.py +0 -0
  46. {codeterrain-0.3.0 → codeterrain-0.3.1}/packages/codeterrain-typescript/src/codeterrain_typescript/__init__.py +0 -0
  47. {codeterrain-0.3.0 → codeterrain-0.3.1}/packages/codeterrain-typescript/tests/test_typescript.py +0 -0
  48. {codeterrain-0.3.0 → codeterrain-0.3.1}/src/codeterrain/_guard.py +0 -0
  49. {codeterrain-0.3.0 → codeterrain-0.3.1}/src/codeterrain/_pytest_dump_plugin.py +0 -0
  50. {codeterrain-0.3.0 → codeterrain-0.3.1}/src/codeterrain/_routes_runner.py +0 -0
  51. {codeterrain-0.3.0 → codeterrain-0.3.1}/src/codeterrain/budget.py +0 -0
  52. {codeterrain-0.3.0 → codeterrain-0.3.1}/src/codeterrain/cache.py +0 -0
  53. {codeterrain-0.3.0 → codeterrain-0.3.1}/src/codeterrain/calls_join.py +0 -0
  54. {codeterrain-0.3.0 → codeterrain-0.3.1}/src/codeterrain/calls_static.py +0 -0
  55. {codeterrain-0.3.0 → codeterrain-0.3.1}/src/codeterrain/cli.py +0 -0
  56. {codeterrain-0.3.0 → codeterrain-0.3.1}/src/codeterrain/code_static.py +0 -0
  57. {codeterrain-0.3.0 → codeterrain-0.3.1}/src/codeterrain/config.py +0 -0
  58. {codeterrain-0.3.0 → codeterrain-0.3.1}/src/codeterrain/executables.py +0 -0
  59. {codeterrain-0.3.0 → codeterrain-0.3.1}/src/codeterrain/fixtures_order.py +0 -0
  60. {codeterrain-0.3.0 → codeterrain-0.3.1}/src/codeterrain/fixtures_static.py +0 -0
  61. {codeterrain-0.3.0 → codeterrain-0.3.1}/src/codeterrain/hookspecs.py +0 -0
  62. {codeterrain-0.3.0 → codeterrain-0.3.1}/src/codeterrain/impact.py +0 -0
  63. {codeterrain-0.3.0 → codeterrain-0.3.1}/src/codeterrain/languages.py +0 -0
  64. {codeterrain-0.3.0 → codeterrain-0.3.1}/src/codeterrain/plugins/__init__.py +0 -0
  65. {codeterrain-0.3.0 → codeterrain-0.3.1}/src/codeterrain/plugins/fastapi_routes.py +0 -0
  66. {codeterrain-0.3.0 → codeterrain-0.3.1}/src/codeterrain/plugins/manifest_join.py +0 -0
  67. {codeterrain-0.3.0 → codeterrain-0.3.1}/src/codeterrain/protocol.py +0 -0
  68. {codeterrain-0.3.0 → codeterrain-0.3.1}/src/codeterrain/routes_view.py +0 -0
  69. {codeterrain-0.3.0 → codeterrain-0.3.1}/src/codeterrain/sandbox.py +0 -0
  70. {codeterrain-0.3.0 → codeterrain-0.3.1}/src/codeterrain/views.py +0 -0
  71. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/conftest.py +0 -0
  72. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/sample_repo/docs/route-access.json +0 -0
  73. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/sample_repo/pyproject.toml +0 -0
  74. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/sample_repo/sampleapp/__init__.py +0 -0
  75. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/sample_repo/sampleapp/main.py +0 -0
  76. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/sample_repo/sampleapp/other.py +0 -0
  77. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/sample_repo/tests/api/conftest.py +0 -0
  78. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/sample_repo/tests/api/test_items.py +0 -0
  79. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/sample_repo/tests/conftest.py +0 -0
  80. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/sample_repo/tests/test_root.py +0 -0
  81. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/test_budget.py +0 -0
  82. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/test_cache.py +0 -0
  83. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/test_calls_join.py +0 -0
  84. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/test_calls_static.py +0 -0
  85. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/test_cli.py +0 -0
  86. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/test_code_static.py +0 -0
  87. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/test_config.py +0 -0
  88. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/test_executables.py +0 -0
  89. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/test_fastapi_routes.py +0 -0
  90. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/test_fixtures_order.py +0 -0
  91. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/test_fixtures_static.py +0 -0
  92. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/test_hookspecs.py +0 -0
  93. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/test_impact.py +0 -0
  94. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/test_languages.py +0 -0
  95. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/test_manifest_join.py +0 -0
  96. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/test_protocol.py +0 -0
  97. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/test_pytest_dump_plugin.py +0 -0
  98. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/test_routes_runner.py +0 -0
  99. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/test_routes_view.py +0 -0
  100. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/test_sandbox.py +0 -0
  101. {codeterrain-0.3.0 → codeterrain-0.3.1}/tests/test_views.py +0 -0
@@ -1,8 +1,9 @@
1
1
  # codeterrain: instructions for coding agents and contributors
2
2
 
3
- codeterrain is a local, LLM-free, network-free command line tool (`terrain`) that answers questions about
4
- a repository's code from a cache, in under a second. Python first: pytest fixture order, FastAPI routes,
5
- definitions and references; plus the HTTP calls of a JavaScript or TypeScript front end. See
3
+ codeterrain is a local, LLM-free, network-free command line tool (`codeterrain`, also installed as
4
+ `terrain`) that answers questions about a repository's code from a cache, in under a second. Python
5
+ first: pytest fixture order, FastAPI routes, definitions and references; plus the HTTP calls of a
6
+ JavaScript or TypeScript front end. See
6
7
  [README.md](README.md) for what it does and [docs/extractor-protocol.md](docs/extractor-protocol.md) for
7
8
  the extractor protocol.
8
9
 
@@ -1,6 +1,28 @@
1
1
  # Changelog
2
2
 
3
- ## 0.3.0 (unreleased)
3
+ ## 0.3.1 (2026-11-02)
4
+
5
+ - The README's diagram of how it works is an image, as the charts are: PyPI does not render
6
+ Mermaid, so the 0.3.0 page showed its source.
7
+ - `codeterrain` is the command the docs use; `terrain` stays installed as a shorter name. Another
8
+ tool on PyPI installs a `terrain` command, and with both installed the last one wins.
9
+ - `codeterrain-terraform` 0.2.1 and `codeterrain-typescript` 0.1.1: their READMEs, which are their
10
+ PyPI pages, install from PyPI instead of saying the packs are not published. No code change.
11
+ - The README leads with `uv tool install codeterrain` from PyPI, and has a starting point for an
12
+ agent's instructions. Its claims are narrowed to what is measured: no "exact answers" headline;
13
+ `impact` lists the tests that statically reach a name, not the tests that would catch a change;
14
+ character counts measure an answer, not a session; the import tier's safeguards stop accidents,
15
+ not hostile code, so it is for code you trust. The field figures cover all 160 logged lookups,
16
+ with their period and how they were judged, two marked wrong among them.
17
+ - SECURITY.md and the rationale no longer describe the repository as private or a public release
18
+ as future work. SECURITY.md's third guarantee says what it covers: pytest and its known plugins
19
+ do not write into the repository by default; target code that opens a file by absolute path still
20
+ can.
21
+ - A prior-art page, `docs/prior-art.md`: what twelve comparable tools document answering, read from
22
+ their READMEs, with a chart. None documents pytest fixture resolution, front-end calls matched to
23
+ routes, or routes read from the application. It is a reading of their docs, not a benchmark.
24
+
25
+ ## 0.3.0 (2026-11-02)
4
26
 
5
27
  - `terrain refs` following an alias counts a read after an assignment on the same line
6
28
  (`ALIAS = VALUE; print(ALIAS)`), and still not one inside the assignment's right-hand side.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: codeterrain
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: terrain: answers about your code in under a second. Local, LLM-free structure facts for coding agents; Python first.
5
5
  Project-URL: Homepage, https://github.com/benchli-sports/codeterrain
6
6
  Project-URL: Issues, https://github.com/benchli-sports/codeterrain/issues
@@ -43,7 +43,7 @@ Description-Content-Type: text/markdown
43
43
  <h1 align="center">codeterrain</h1>
44
44
 
45
45
  <p align="center">
46
- <b><code>terrain</code>: exact answers about your code, in under a second.</b><br>
46
+ <b><code>codeterrain</code>: answers about your code, in under a second.</b><br>
47
47
  For coding agents, and the people working beside them.
48
48
  </p>
49
49
 
@@ -63,34 +63,40 @@ Description-Content-Type: text/markdown
63
63
  ---
64
64
 
65
65
  A coding agent spends most of its reading on structure it has to rediscover every session: which
66
- fixtures a test really gets, where a route is handled, who calls a function, which tests would catch a
67
- change to it. The answers are in the code, but finding them takes rounds of grep and file reads, and a
66
+ fixtures a test really gets, where a route is handled, who calls a function, which tests reach
67
+ it. The answers are in the code, but finding them takes rounds of grep and file reads, and a
68
68
  wrong guess costs a review round.
69
69
 
70
- `terrain` answers those questions from a cache, the way the framework itself would answer them.
70
+ `codeterrain` answers those questions from a cache, the way the framework itself would answer them.
71
71
 
72
72
  | | |
73
73
  |:---|:---|
74
- | **Exact** | Setup order equals `pytest --setup-plan` on every node tested. `refs` follows imports and re-exports, so an alias counts and a same-named function elsewhere does not. |
74
+ | **Accurate where it counts** | Setup order and routes come from pytest and FastAPI themselves: setup order equals `pytest --setup-plan` on every node tested. `refs`, `def` and `impact` read the code statically: they follow imports and re-exports, so an alias counts and a same-named function elsewhere does not, and a name whose meaning depends on run time is left unresolved rather than guessed ([limits](docs/limits.md)). |
75
75
  | **Fast** | Most warm answers take 0.3 to 0.5 s on a large backend, Python start-up included. |
76
- | **Cheaper to read** | 4.4x to 8.8x fewer characters than the grep-and-read it replaces, on code bases with a real fixture graph. |
77
- | **Safe to point at a repo** | The static tier never runs the target's code. The tier that has to import it runs in a sandbox with no credentials, and by default it cannot write into the repository. |
76
+ | **Shorter to read** | 4.4x to 8.8x fewer characters than the grep-and-read it replaces, on code bases with a real fixture graph. That measures the answer, not a whole session's tokens, cost or time. |
77
+ | **Careful with your repo** | The static tier never runs the target's code. Setup order and routes have to import it: they run in a subprocess with an allowlisted environment, dummy cloud credentials and a best-effort network guard, which stop accidents, not hostile code. Run those two on code you trust ([SECURITY.md](SECURITY.md)). |
78
78
 
79
79
  ## Quick start
80
80
 
81
81
  ```sh
82
- uv tool install <path-to-this-checkout> # Python 3.12+; the only dependency is pluggy
83
- # or "<path-to-this-checkout>[terraform]" to add the Terraform language pack, "[typescript]"
84
- # to read front-end calls with tree-sitter, "[terraform,typescript]" for both
82
+ uv tool install codeterrain # Python 3.12+; the only dependency is pluggy
83
+ # or "codeterrain[terraform]" to add the Terraform language pack, "codeterrain[typescript]"
84
+ # to read front-end calls with tree-sitter, "codeterrain[terraform,typescript]" for both
85
85
  cd your-repo
86
- terrain doctor # what it found, and which config it used
86
+ codeterrain doctor # what it found, and which config it used
87
+ codeterrain fixtures # every pytest fixture, and which definition wins where
87
88
  ```
88
89
 
90
+ The same command is also installed as `terrain`, a shorter name. Another tool on PyPI installs a
91
+ `terrain` command too; with both installed, the last one wins, while `codeterrain` is always this
92
+ one. To work on codeterrain itself, install from a checkout instead:
93
+ `uv tool install --editable <path-to-checkout>`.
94
+
89
95
  Ask about a function: where it is called, how each call passes its arguments, and which tests patch it.
90
- This is `terrain` run on its own source:
96
+ This is `codeterrain` run on its own source:
91
97
 
92
98
  ```console
93
- $ terrain refs is_test_module
99
+ $ codeterrain refs is_test_module
94
100
  function codeterrain.fixtures_static.is_test_module(rel) src/codeterrain/fixtures_static.py:327
95
101
  6 calls (6 in code, 0 in tests), 0 with a shape problem, 0 used as a value, 0 patched
96
102
  file:line in shape note
@@ -102,11 +108,12 @@ src/codeterrain/impact.py:272 _Fixtures.__init__ 1 posit
102
108
  src/codeterrain/impact.py:424 _inherited_tests 1 positional
103
109
  ```
104
110
 
105
- Ask which tests would catch a change to it, through its callers and the fixtures that call them
106
- (abridged):
111
+ Ask which tests statically reach it, through its callers and the fixtures that call them
112
+ (abridged). A test that reaches a function does not necessarily check what you change, and an
113
+ empty list does not prove the change is untested:
107
114
 
108
115
  ```console
109
- $ terrain impact is_test_module --depth 2
116
+ $ codeterrain impact is_test_module --depth 2
110
117
  17 tests reach codeterrain.fixtures_static.is_test_module (0 directly), through 15 callers within 2 levels; 2 tests patch it …
111
118
  test depth how via
112
119
  tests/test_cli.py::test_a_handler_is_matched_by_… 2 call at tests/test_cli.py:1186 …update <- …is_test_module
@@ -117,7 +124,7 @@ tests/test_cli.py::test_a_route_text_that_matches_… 2 call at tests/test
117
124
  Ask what a fixture is and what depends on it (from the sample repository in `tests/sample_repo`):
118
125
 
119
126
  ```console
120
- $ terrain fixture db
127
+ $ codeterrain fixture db
121
128
  name: db
122
129
  scope: session
123
130
  autouse: no
@@ -137,21 +144,44 @@ The commands that import your application (`setup-order`, `routes`) run in the *
137
144
  interpreter: pass `--python .venv/bin/python` or set `CODETERRAIN_PYTHON`. Add `--json` to any command
138
145
  for the full data.
139
146
 
147
+ ## Telling your agent
148
+
149
+ An agent uses `codeterrain` when its instructions say when to. A starting point for `AGENTS.md` or
150
+ `CLAUDE.md`:
151
+
152
+ ```markdown
153
+ ## Code structure: ask codeterrain first
154
+
155
+ Before grepping or reading files for one of these, run `codeterrain` in the repository root
156
+ (local, under a second when warm; add `--json` for the full data):
157
+
158
+ - where a name is defined: `codeterrain def NAME`
159
+ - who calls, passes or patches it: `codeterrain refs NAME`
160
+ - which tests statically reach it: `codeterrain impact NAME` (a starting point, not proof of coverage)
161
+ - which fixtures a test gets, in order: `codeterrain setup-order NODEID`
162
+ - a fixture and what depends on it: `codeterrain fixture NAME`
163
+ - where a route is handled: `codeterrain route METHOD PATH`
164
+ - a front end's HTTP calls, and the backend routes they hit: `codeterrain calls`, `codeterrain callers`
165
+
166
+ When codeterrain says it cannot resolve something, fall back to grep. `setup-order` and the route
167
+ commands import the code: use them only on code you trust.
168
+ ```
169
+
140
170
  ## What it answers
141
171
 
142
172
  | Question | Command | What grep can't tell you |
143
173
  |:---|:---|:---|
144
- | Where is this defined? | `terrain def NAME` | Resolves a re-export to the real definition |
145
- | Who calls it, passes it, patches it? | `terrain refs NAME` | Follows imports and aliases, flags calls that don't fit the signature, and separates tests that patch it out |
146
- | Who imports this module, and what does it import? | `terrain importers MODULE`, `terrain imports MODULE` | Every import of it or of a name from it, relative imports resolved |
147
- | Which tests would catch a change? | `terrain impact NAME` | Walks callers and fixtures the way pytest resolves them |
148
- | Which fixtures does a test get, in what order? | `terrain setup-order NODEID` | pytest's own resolution, overrides and scopes included |
149
- | What fixtures exist, and who uses one? | `terrain fixtures`, `terrain fixture NAME` | Which definition wins where, and every dependent |
150
- | Where is a route handled, with what access? | `terrain routes`, `terrain route METHOD PATH` | Prefixes from router calls in other files, joined to your access manifest |
151
- | Which HTTP calls does a front end make? | `terrain calls` | Paths built from templates, constants and concatenation |
152
- | Which backend route does each call hit? | `terrain callers` | Front-end calls matched to backend routes across repositories |
153
- | Where is a Terraform resource, variable or module, and who reads it? | `terrain def NAME`, `terrain refs NAME` (Terraform pack) | Terraform's own names (`aws_s3_bucket.logs`, `var.region`, `module.vpc`) and the block each reference sits in |
154
- | What is the module tree, and which providers are pinned? | `terrain def --kind module`, `terrain def --kind required_provider` | Every module call with its source and version; provider aliases and what selects them |
174
+ | Where is this defined? | `codeterrain def NAME` | Resolves a re-export to the real definition |
175
+ | Who calls it, passes it, patches it? | `codeterrain refs NAME` | Follows imports and aliases, flags calls that don't fit the signature, and separates tests that patch it out |
176
+ | Who imports this module, and what does it import? | `codeterrain importers MODULE`, `codeterrain imports MODULE` | Every import of it or of a name from it, relative imports resolved |
177
+ | Which tests statically reach it? | `codeterrain impact NAME` | Walks callers and fixtures the way pytest resolves them; what it cannot follow statically is listed, not dropped |
178
+ | Which fixtures does a test get, in what order? | `codeterrain setup-order NODEID` | pytest's own resolution, overrides and scopes included |
179
+ | What fixtures exist, and who uses one? | `codeterrain fixtures`, `codeterrain fixture NAME` | Which definition wins where, and every dependent |
180
+ | Where is a route handled, with what access? | `codeterrain routes`, `codeterrain route METHOD PATH` | Prefixes from router calls in other files, joined to your access manifest |
181
+ | Which HTTP calls does a front end make? | `codeterrain calls` | Paths built from templates, constants and concatenation |
182
+ | Which backend route does each call hit? | `codeterrain callers` | Front-end calls matched to backend routes across repositories |
183
+ | Where is a Terraform resource, variable or module, and who reads it? | `codeterrain def NAME`, `codeterrain refs NAME` (Terraform pack) | Terraform's own names (`aws_s3_bucket.logs`, `var.region`, `module.vpc`) and the block each reference sits in |
184
+ | What is the module tree, and which providers are pinned? | `codeterrain def --kind module`, `codeterrain def --kind required_provider` | Every module call with its source and version; provider aliases and what selects them |
155
185
 
156
186
  Every option is in **[docs/commands.md](docs/commands.md)**.
157
187
 
@@ -165,7 +195,7 @@ smaller Python code bases, an analytics project with a large fixture graph, and
165
195
  <td align="center" width="25%"><h2>11 / 11</h2>test nodes where setup order equals <code>pytest --setup-plan</code></td>
166
196
  <td align="center" width="25%"><h2>0.35 s</h2>median warm <code>fixtures</code> answer on the large backend</td>
167
197
  <td align="center" width="25%"><h2>up to 8.8x</h2>fewer characters for an agent to read</td>
168
- <td align="center" width="25%"><h2>0</h2>answers marked wrong in agents' logged lookups</td>
198
+ <td align="center" width="25%"><h2>160</h2>field lookups logged by coding agents; agent-assessed, not independently validated</td>
169
199
  </tr>
170
200
  </table>
171
201
 
@@ -195,18 +225,27 @@ than the grep; there the gain is the exact order, which grep can't show.
195
225
 
196
226
  ### In the field
197
227
 
198
- Coding agents working on real tasks logged each `terrain` lookup: what they asked, whether the answer
199
- was right, and what they would have done instead.
228
+ Coding agents working on real tasks logged each `codeterrain` lookup: what they asked, whether the answer
229
+ was right, and what they would have done instead. 160 lookups, 26 October to 2 November 2026, across
230
+ the code bases above. Each agent judged its own answer against what it then read or ran; nobody
231
+ checked the judgements independently. Every lookup has one outcome; the shares are rounded, so they
232
+ can add up to 101%.
200
233
 
201
234
  <picture>
202
235
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/benchli-sports/codeterrain/main/docs/img/field-dark.svg">
203
- <img src="https://raw.githubusercontent.com/benchli-sports/codeterrain/main/docs/img/field-light.svg" alt="Outcomes of logged lookups: answered 79%, partly answered 12%, no help 2%, error 7% (mostly the wrong command), wrong answer 0%" width="760">
236
+ <img src="https://raw.githubusercontent.com/benchli-sports/codeterrain/main/docs/img/field-light.svg" alt="Outcomes of 160 lookups agents logged from 26 October to 2 November 2026, one outcome each, as they judged them: answered 118 (74%), partly answered 16 (10%), no help 12 (8%), error 12 (8%, mostly the wrong command), wrong answer 2 (1%)" width="760">
204
237
  </picture>
205
238
 
206
- - Each fully answered lookup saved the agent **about 1.3 tool calls**, by its own count.
207
- - **No answer was marked wrong.** The partial answers came from output cut by the token budget, a cache
208
- built from a different checkout than the one being edited, and a value read through a module alias.
209
- - Most errors were the wrong command: `callers`, which is for routes, where `refs` was meant.
239
+ - Each fully answered lookup saved the agent **about 1.8 tool calls**, by its own count.
240
+ - **Two answers were marked wrong.** In one, `fixture` answered a fixture of a live-test folder it
241
+ did not read with a unit-test fixture of the same name; `fixtures`, `fixture` and `doctor` now name
242
+ such a folder. The other, `routes` missing an app's support routes, was misjudged: a later lookup
243
+ found them in the view, below the token budget's cut, whose "more" notice a `grep` had removed.
244
+ - The partial answers came mostly from output cut by the token budget; the rest from a cache built
245
+ from a different checkout than the one being edited, reads `refs` does not see (through a module
246
+ alias, as a dict key), duck-typed calls, and route filters that matched less than expected.
247
+ - Most errors were the wrong command or arguments: `callers`, which is for routes, where `refs` was
248
+ meant.
210
249
 
211
250
  ## Inferred token savings
212
251
 
@@ -214,41 +253,30 @@ An estimate from the two measurements above, not an A/B experiment:
214
253
 
215
254
  | Step | Figure |
216
255
  |:---|---:|
217
- | Reading saved on a question `terrain` answers, large backend (4.4x to 6.9x fewer characters) | 77% to 86% |
218
- | Lookups answered fully, in the field | 79% |
219
- | Lookups answered partly (counted at half the saving) | 12% |
220
- | **Inferred share of lookup reading saved on a large Python backend** | **about 65% to 73%** |
256
+ | Reading saved on a question `codeterrain` answers, large backend (4.4x to 6.9x fewer characters) | 77% to 86% |
257
+ | Lookups answered fully, in the field | 74% |
258
+ | Lookups answered partly (counted at half the saving) | 10% |
259
+ | **Inferred share of lookup reading saved on a large Python backend** | **about 61% to 68%** |
221
260
 
222
261
  Lookups are only part of a session: reasoning, editing and test runs are untouched, and small code
223
262
  bases save little. Tokens here are characters divided by four. A session-level A/B, the same lane run
224
- with and without `terrain`, is the next measurement. The method and the per-repository results are in
263
+ with and without `codeterrain`, is the next measurement. The method and the per-repository results are in
225
264
  [docs/why-codeterrain.md](docs/why-codeterrain.md).
226
265
 
227
266
  ## How it works
228
267
 
229
- ```mermaid
230
- flowchart LR
231
- ask(["agent or you: terrain refs NAME"]) --> cache[("cache per repository,<br/>keyed on content")]
232
- cache -->|warm| answer(["answer in under a second"])
233
- subgraph static ["Static tier: reads files, never runs them"]
234
- fixtures["fixtures and tests<br/>(ast)"]
235
- code["code index<br/>(ast, SQLite)"]
236
- calls["front-end HTTP calls<br/>(tree-sitter, or the JS/TS tokenizer)"]
237
- end
238
- subgraph sandbox ["Import tier: sandboxed subprocess"]
239
- order["setup order<br/>(pytest --collect-only)"]
240
- routes["routes<br/>(imports the apps)"]
241
- end
242
- static -->|incremental, changed files only| cache
243
- sandbox -->|on refresh; served stamped stale| cache
244
- ```
268
+ <picture>
269
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/benchli-sports/codeterrain/main/docs/img/architecture-dark.svg">
270
+ <img src="https://raw.githubusercontent.com/benchli-sports/codeterrain/main/docs/img/architecture-light.svg" alt="How it works: a codeterrain question is answered from a cache per repository, keyed on content, in under a second when warm. The static tier (fixtures and tests and the code index with Python's ast, front-end calls with tree-sitter or a tokenizer) fills it from changed files only and never runs the code. The import tier runs the code in a guarded subprocess and fills it on refresh with the setup order and the routes." width="760">
271
+ </picture>
245
272
 
246
273
  - **The static tier** reads source with Python's `ast`, and front-end files with tree-sitter when the
247
274
  TypeScript pack is installed (chosen for speed and accuracy) or a tokenizer written in Python when
248
275
  it is not. It never imports or runs the target, and re-reads only the files that changed.
249
276
  - **The import tier** needs the real framework: pytest's fixture resolution, FastAPI's route table. It
250
277
  runs in a subprocess using the target's own interpreter, with an allowlisted environment, dummy cloud
251
- credentials, no proxy and a best-effort network guard.
278
+ credentials, no proxy and a best-effort network guard. Those stop accidents, not hostile code: it
279
+ imports and runs the target, so use it on code you trust.
252
280
  - **The cache** lives outside the checkout, in the git common directory or your user cache, and is
253
281
  keyed on content hashes.
254
282
 
@@ -279,6 +307,7 @@ knowledge-graph tool, are in [docs/why-codeterrain.md](docs/why-codeterrain.md).
279
307
  | [TypeScript pack](packages/codeterrain-typescript/README.md) | Front-end calls read with tree-sitter, and why it is the reader to install |
280
308
  | [Design notes](docs/design-v0.2.md) | The decisions behind the front-end calls and the language packs |
281
309
  | [Why codeterrain](docs/why-codeterrain.md) | The origin, the measurements, what is not proven yet |
310
+ | [Prior art](docs/prior-art.md) | What twelve comparable tools document answering, read from their docs |
282
311
  | [Changelog](CHANGELOG.md) | What changed, release by release |
283
312
 
284
313
  ## Development
@@ -5,7 +5,7 @@
5
5
  <h1 align="center">codeterrain</h1>
6
6
 
7
7
  <p align="center">
8
- <b><code>terrain</code>: exact answers about your code, in under a second.</b><br>
8
+ <b><code>codeterrain</code>: answers about your code, in under a second.</b><br>
9
9
  For coding agents, and the people working beside them.
10
10
  </p>
11
11
 
@@ -25,34 +25,40 @@
25
25
  ---
26
26
 
27
27
  A coding agent spends most of its reading on structure it has to rediscover every session: which
28
- fixtures a test really gets, where a route is handled, who calls a function, which tests would catch a
29
- change to it. The answers are in the code, but finding them takes rounds of grep and file reads, and a
28
+ fixtures a test really gets, where a route is handled, who calls a function, which tests reach
29
+ it. The answers are in the code, but finding them takes rounds of grep and file reads, and a
30
30
  wrong guess costs a review round.
31
31
 
32
- `terrain` answers those questions from a cache, the way the framework itself would answer them.
32
+ `codeterrain` answers those questions from a cache, the way the framework itself would answer them.
33
33
 
34
34
  | | |
35
35
  |:---|:---|
36
- | **Exact** | Setup order equals `pytest --setup-plan` on every node tested. `refs` follows imports and re-exports, so an alias counts and a same-named function elsewhere does not. |
36
+ | **Accurate where it counts** | Setup order and routes come from pytest and FastAPI themselves: setup order equals `pytest --setup-plan` on every node tested. `refs`, `def` and `impact` read the code statically: they follow imports and re-exports, so an alias counts and a same-named function elsewhere does not, and a name whose meaning depends on run time is left unresolved rather than guessed ([limits](docs/limits.md)). |
37
37
  | **Fast** | Most warm answers take 0.3 to 0.5 s on a large backend, Python start-up included. |
38
- | **Cheaper to read** | 4.4x to 8.8x fewer characters than the grep-and-read it replaces, on code bases with a real fixture graph. |
39
- | **Safe to point at a repo** | The static tier never runs the target's code. The tier that has to import it runs in a sandbox with no credentials, and by default it cannot write into the repository. |
38
+ | **Shorter to read** | 4.4x to 8.8x fewer characters than the grep-and-read it replaces, on code bases with a real fixture graph. That measures the answer, not a whole session's tokens, cost or time. |
39
+ | **Careful with your repo** | The static tier never runs the target's code. Setup order and routes have to import it: they run in a subprocess with an allowlisted environment, dummy cloud credentials and a best-effort network guard, which stop accidents, not hostile code. Run those two on code you trust ([SECURITY.md](SECURITY.md)). |
40
40
 
41
41
  ## Quick start
42
42
 
43
43
  ```sh
44
- uv tool install <path-to-this-checkout> # Python 3.12+; the only dependency is pluggy
45
- # or "<path-to-this-checkout>[terraform]" to add the Terraform language pack, "[typescript]"
46
- # to read front-end calls with tree-sitter, "[terraform,typescript]" for both
44
+ uv tool install codeterrain # Python 3.12+; the only dependency is pluggy
45
+ # or "codeterrain[terraform]" to add the Terraform language pack, "codeterrain[typescript]"
46
+ # to read front-end calls with tree-sitter, "codeterrain[terraform,typescript]" for both
47
47
  cd your-repo
48
- terrain doctor # what it found, and which config it used
48
+ codeterrain doctor # what it found, and which config it used
49
+ codeterrain fixtures # every pytest fixture, and which definition wins where
49
50
  ```
50
51
 
52
+ The same command is also installed as `terrain`, a shorter name. Another tool on PyPI installs a
53
+ `terrain` command too; with both installed, the last one wins, while `codeterrain` is always this
54
+ one. To work on codeterrain itself, install from a checkout instead:
55
+ `uv tool install --editable <path-to-checkout>`.
56
+
51
57
  Ask about a function: where it is called, how each call passes its arguments, and which tests patch it.
52
- This is `terrain` run on its own source:
58
+ This is `codeterrain` run on its own source:
53
59
 
54
60
  ```console
55
- $ terrain refs is_test_module
61
+ $ codeterrain refs is_test_module
56
62
  function codeterrain.fixtures_static.is_test_module(rel) src/codeterrain/fixtures_static.py:327
57
63
  6 calls (6 in code, 0 in tests), 0 with a shape problem, 0 used as a value, 0 patched
58
64
  file:line in shape note
@@ -64,11 +70,12 @@ src/codeterrain/impact.py:272 _Fixtures.__init__ 1 posit
64
70
  src/codeterrain/impact.py:424 _inherited_tests 1 positional
65
71
  ```
66
72
 
67
- Ask which tests would catch a change to it, through its callers and the fixtures that call them
68
- (abridged):
73
+ Ask which tests statically reach it, through its callers and the fixtures that call them
74
+ (abridged). A test that reaches a function does not necessarily check what you change, and an
75
+ empty list does not prove the change is untested:
69
76
 
70
77
  ```console
71
- $ terrain impact is_test_module --depth 2
78
+ $ codeterrain impact is_test_module --depth 2
72
79
  17 tests reach codeterrain.fixtures_static.is_test_module (0 directly), through 15 callers within 2 levels; 2 tests patch it …
73
80
  test depth how via
74
81
  tests/test_cli.py::test_a_handler_is_matched_by_… 2 call at tests/test_cli.py:1186 …update <- …is_test_module
@@ -79,7 +86,7 @@ tests/test_cli.py::test_a_route_text_that_matches_… 2 call at tests/test
79
86
  Ask what a fixture is and what depends on it (from the sample repository in `tests/sample_repo`):
80
87
 
81
88
  ```console
82
- $ terrain fixture db
89
+ $ codeterrain fixture db
83
90
  name: db
84
91
  scope: session
85
92
  autouse: no
@@ -99,21 +106,44 @@ The commands that import your application (`setup-order`, `routes`) run in the *
99
106
  interpreter: pass `--python .venv/bin/python` or set `CODETERRAIN_PYTHON`. Add `--json` to any command
100
107
  for the full data.
101
108
 
109
+ ## Telling your agent
110
+
111
+ An agent uses `codeterrain` when its instructions say when to. A starting point for `AGENTS.md` or
112
+ `CLAUDE.md`:
113
+
114
+ ```markdown
115
+ ## Code structure: ask codeterrain first
116
+
117
+ Before grepping or reading files for one of these, run `codeterrain` in the repository root
118
+ (local, under a second when warm; add `--json` for the full data):
119
+
120
+ - where a name is defined: `codeterrain def NAME`
121
+ - who calls, passes or patches it: `codeterrain refs NAME`
122
+ - which tests statically reach it: `codeterrain impact NAME` (a starting point, not proof of coverage)
123
+ - which fixtures a test gets, in order: `codeterrain setup-order NODEID`
124
+ - a fixture and what depends on it: `codeterrain fixture NAME`
125
+ - where a route is handled: `codeterrain route METHOD PATH`
126
+ - a front end's HTTP calls, and the backend routes they hit: `codeterrain calls`, `codeterrain callers`
127
+
128
+ When codeterrain says it cannot resolve something, fall back to grep. `setup-order` and the route
129
+ commands import the code: use them only on code you trust.
130
+ ```
131
+
102
132
  ## What it answers
103
133
 
104
134
  | Question | Command | What grep can't tell you |
105
135
  |:---|:---|:---|
106
- | Where is this defined? | `terrain def NAME` | Resolves a re-export to the real definition |
107
- | Who calls it, passes it, patches it? | `terrain refs NAME` | Follows imports and aliases, flags calls that don't fit the signature, and separates tests that patch it out |
108
- | Who imports this module, and what does it import? | `terrain importers MODULE`, `terrain imports MODULE` | Every import of it or of a name from it, relative imports resolved |
109
- | Which tests would catch a change? | `terrain impact NAME` | Walks callers and fixtures the way pytest resolves them |
110
- | Which fixtures does a test get, in what order? | `terrain setup-order NODEID` | pytest's own resolution, overrides and scopes included |
111
- | What fixtures exist, and who uses one? | `terrain fixtures`, `terrain fixture NAME` | Which definition wins where, and every dependent |
112
- | Where is a route handled, with what access? | `terrain routes`, `terrain route METHOD PATH` | Prefixes from router calls in other files, joined to your access manifest |
113
- | Which HTTP calls does a front end make? | `terrain calls` | Paths built from templates, constants and concatenation |
114
- | Which backend route does each call hit? | `terrain callers` | Front-end calls matched to backend routes across repositories |
115
- | Where is a Terraform resource, variable or module, and who reads it? | `terrain def NAME`, `terrain refs NAME` (Terraform pack) | Terraform's own names (`aws_s3_bucket.logs`, `var.region`, `module.vpc`) and the block each reference sits in |
116
- | What is the module tree, and which providers are pinned? | `terrain def --kind module`, `terrain def --kind required_provider` | Every module call with its source and version; provider aliases and what selects them |
136
+ | Where is this defined? | `codeterrain def NAME` | Resolves a re-export to the real definition |
137
+ | Who calls it, passes it, patches it? | `codeterrain refs NAME` | Follows imports and aliases, flags calls that don't fit the signature, and separates tests that patch it out |
138
+ | Who imports this module, and what does it import? | `codeterrain importers MODULE`, `codeterrain imports MODULE` | Every import of it or of a name from it, relative imports resolved |
139
+ | Which tests statically reach it? | `codeterrain impact NAME` | Walks callers and fixtures the way pytest resolves them; what it cannot follow statically is listed, not dropped |
140
+ | Which fixtures does a test get, in what order? | `codeterrain setup-order NODEID` | pytest's own resolution, overrides and scopes included |
141
+ | What fixtures exist, and who uses one? | `codeterrain fixtures`, `codeterrain fixture NAME` | Which definition wins where, and every dependent |
142
+ | Where is a route handled, with what access? | `codeterrain routes`, `codeterrain route METHOD PATH` | Prefixes from router calls in other files, joined to your access manifest |
143
+ | Which HTTP calls does a front end make? | `codeterrain calls` | Paths built from templates, constants and concatenation |
144
+ | Which backend route does each call hit? | `codeterrain callers` | Front-end calls matched to backend routes across repositories |
145
+ | Where is a Terraform resource, variable or module, and who reads it? | `codeterrain def NAME`, `codeterrain refs NAME` (Terraform pack) | Terraform's own names (`aws_s3_bucket.logs`, `var.region`, `module.vpc`) and the block each reference sits in |
146
+ | What is the module tree, and which providers are pinned? | `codeterrain def --kind module`, `codeterrain def --kind required_provider` | Every module call with its source and version; provider aliases and what selects them |
117
147
 
118
148
  Every option is in **[docs/commands.md](docs/commands.md)**.
119
149
 
@@ -127,7 +157,7 @@ smaller Python code bases, an analytics project with a large fixture graph, and
127
157
  <td align="center" width="25%"><h2>11 / 11</h2>test nodes where setup order equals <code>pytest --setup-plan</code></td>
128
158
  <td align="center" width="25%"><h2>0.35 s</h2>median warm <code>fixtures</code> answer on the large backend</td>
129
159
  <td align="center" width="25%"><h2>up to 8.8x</h2>fewer characters for an agent to read</td>
130
- <td align="center" width="25%"><h2>0</h2>answers marked wrong in agents' logged lookups</td>
160
+ <td align="center" width="25%"><h2>160</h2>field lookups logged by coding agents; agent-assessed, not independently validated</td>
131
161
  </tr>
132
162
  </table>
133
163
 
@@ -157,18 +187,27 @@ than the grep; there the gain is the exact order, which grep can't show.
157
187
 
158
188
  ### In the field
159
189
 
160
- Coding agents working on real tasks logged each `terrain` lookup: what they asked, whether the answer
161
- was right, and what they would have done instead.
190
+ Coding agents working on real tasks logged each `codeterrain` lookup: what they asked, whether the answer
191
+ was right, and what they would have done instead. 160 lookups, 26 October to 2 November 2026, across
192
+ the code bases above. Each agent judged its own answer against what it then read or ran; nobody
193
+ checked the judgements independently. Every lookup has one outcome; the shares are rounded, so they
194
+ can add up to 101%.
162
195
 
163
196
  <picture>
164
197
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/benchli-sports/codeterrain/main/docs/img/field-dark.svg">
165
- <img src="https://raw.githubusercontent.com/benchli-sports/codeterrain/main/docs/img/field-light.svg" alt="Outcomes of logged lookups: answered 79%, partly answered 12%, no help 2%, error 7% (mostly the wrong command), wrong answer 0%" width="760">
198
+ <img src="https://raw.githubusercontent.com/benchli-sports/codeterrain/main/docs/img/field-light.svg" alt="Outcomes of 160 lookups agents logged from 26 October to 2 November 2026, one outcome each, as they judged them: answered 118 (74%), partly answered 16 (10%), no help 12 (8%), error 12 (8%, mostly the wrong command), wrong answer 2 (1%)" width="760">
166
199
  </picture>
167
200
 
168
- - Each fully answered lookup saved the agent **about 1.3 tool calls**, by its own count.
169
- - **No answer was marked wrong.** The partial answers came from output cut by the token budget, a cache
170
- built from a different checkout than the one being edited, and a value read through a module alias.
171
- - Most errors were the wrong command: `callers`, which is for routes, where `refs` was meant.
201
+ - Each fully answered lookup saved the agent **about 1.8 tool calls**, by its own count.
202
+ - **Two answers were marked wrong.** In one, `fixture` answered a fixture of a live-test folder it
203
+ did not read with a unit-test fixture of the same name; `fixtures`, `fixture` and `doctor` now name
204
+ such a folder. The other, `routes` missing an app's support routes, was misjudged: a later lookup
205
+ found them in the view, below the token budget's cut, whose "more" notice a `grep` had removed.
206
+ - The partial answers came mostly from output cut by the token budget; the rest from a cache built
207
+ from a different checkout than the one being edited, reads `refs` does not see (through a module
208
+ alias, as a dict key), duck-typed calls, and route filters that matched less than expected.
209
+ - Most errors were the wrong command or arguments: `callers`, which is for routes, where `refs` was
210
+ meant.
172
211
 
173
212
  ## Inferred token savings
174
213
 
@@ -176,41 +215,30 @@ An estimate from the two measurements above, not an A/B experiment:
176
215
 
177
216
  | Step | Figure |
178
217
  |:---|---:|
179
- | Reading saved on a question `terrain` answers, large backend (4.4x to 6.9x fewer characters) | 77% to 86% |
180
- | Lookups answered fully, in the field | 79% |
181
- | Lookups answered partly (counted at half the saving) | 12% |
182
- | **Inferred share of lookup reading saved on a large Python backend** | **about 65% to 73%** |
218
+ | Reading saved on a question `codeterrain` answers, large backend (4.4x to 6.9x fewer characters) | 77% to 86% |
219
+ | Lookups answered fully, in the field | 74% |
220
+ | Lookups answered partly (counted at half the saving) | 10% |
221
+ | **Inferred share of lookup reading saved on a large Python backend** | **about 61% to 68%** |
183
222
 
184
223
  Lookups are only part of a session: reasoning, editing and test runs are untouched, and small code
185
224
  bases save little. Tokens here are characters divided by four. A session-level A/B, the same lane run
186
- with and without `terrain`, is the next measurement. The method and the per-repository results are in
225
+ with and without `codeterrain`, is the next measurement. The method and the per-repository results are in
187
226
  [docs/why-codeterrain.md](docs/why-codeterrain.md).
188
227
 
189
228
  ## How it works
190
229
 
191
- ```mermaid
192
- flowchart LR
193
- ask(["agent or you: terrain refs NAME"]) --> cache[("cache per repository,<br/>keyed on content")]
194
- cache -->|warm| answer(["answer in under a second"])
195
- subgraph static ["Static tier: reads files, never runs them"]
196
- fixtures["fixtures and tests<br/>(ast)"]
197
- code["code index<br/>(ast, SQLite)"]
198
- calls["front-end HTTP calls<br/>(tree-sitter, or the JS/TS tokenizer)"]
199
- end
200
- subgraph sandbox ["Import tier: sandboxed subprocess"]
201
- order["setup order<br/>(pytest --collect-only)"]
202
- routes["routes<br/>(imports the apps)"]
203
- end
204
- static -->|incremental, changed files only| cache
205
- sandbox -->|on refresh; served stamped stale| cache
206
- ```
230
+ <picture>
231
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/benchli-sports/codeterrain/main/docs/img/architecture-dark.svg">
232
+ <img src="https://raw.githubusercontent.com/benchli-sports/codeterrain/main/docs/img/architecture-light.svg" alt="How it works: a codeterrain question is answered from a cache per repository, keyed on content, in under a second when warm. The static tier (fixtures and tests and the code index with Python's ast, front-end calls with tree-sitter or a tokenizer) fills it from changed files only and never runs the code. The import tier runs the code in a guarded subprocess and fills it on refresh with the setup order and the routes." width="760">
233
+ </picture>
207
234
 
208
235
  - **The static tier** reads source with Python's `ast`, and front-end files with tree-sitter when the
209
236
  TypeScript pack is installed (chosen for speed and accuracy) or a tokenizer written in Python when
210
237
  it is not. It never imports or runs the target, and re-reads only the files that changed.
211
238
  - **The import tier** needs the real framework: pytest's fixture resolution, FastAPI's route table. It
212
239
  runs in a subprocess using the target's own interpreter, with an allowlisted environment, dummy cloud
213
- credentials, no proxy and a best-effort network guard.
240
+ credentials, no proxy and a best-effort network guard. Those stop accidents, not hostile code: it
241
+ imports and runs the target, so use it on code you trust.
214
242
  - **The cache** lives outside the checkout, in the git common directory or your user cache, and is
215
243
  keyed on content hashes.
216
244
 
@@ -241,6 +269,7 @@ knowledge-graph tool, are in [docs/why-codeterrain.md](docs/why-codeterrain.md).
241
269
  | [TypeScript pack](packages/codeterrain-typescript/README.md) | Front-end calls read with tree-sitter, and why it is the reader to install |
242
270
  | [Design notes](docs/design-v0.2.md) | The decisions behind the front-end calls and the language packs |
243
271
  | [Why codeterrain](docs/why-codeterrain.md) | The origin, the measurements, what is not proven yet |
272
+ | [Prior art](docs/prior-art.md) | What twelve comparable tools document answering, read from their docs |
244
273
  | [Changelog](CHANGELOG.md) | What changed, release by release |
245
274
 
246
275
  ## Development
@@ -11,16 +11,16 @@ What codeterrain does guarantee is about its own, parent process:
11
11
  1. **It never executes target code.** The static tier (`fixtures`, `fixture`) reads files with `ast`
12
12
  only. Every program it starts (`git`, the target interpreter) is resolved to an absolute path from
13
13
  the absolute `PATH` entries, never from the current directory, never a `.bat` or `.cmd` on Windows,
14
- and never a program a bare name finds inside the target repository or, for `terrain callers`,
14
+ and never a program a bare name finds inside the target repository or, for `codeterrain callers`,
15
15
  inside any front-end checkout it reads, whichever checkout it is asking about. The `git status` that
16
- `terrain callers` runs in each checkout switches `core.fsmonitor` off, so a command named in that
16
+ `codeterrain callers` runs in each checkout switches `core.fsmonitor` off, so a command named in that
17
17
  checkout's `.git/config` is not run, and takes no optional locks, so it writes nothing there.
18
- A language pack runs in terrain's own process. Before any pack is looked up, every import-path
18
+ A language pack runs in codeterrain's own process. Before any pack is looked up, every import-path
19
19
  entry inside the repository being read (and an empty or relative one) is removed, so neither a
20
20
  pack, the parts of a namespace package nor anything a pack imports later resolves there; a pack
21
- whose distribution or module still lies inside it is refused before it is imported. What terrain
21
+ whose distribution or module still lies inside it is refused before it is imported. What codeterrain
22
22
  itself runs from is the exception, when it lies inside the checkout: its virtual environment and
23
- the folder its own code is imported from. A checkout inside terrain's environment is not trusted.
23
+ the folder its own code is imported from. A checkout inside codeterrain's environment is not trusted.
24
24
  2. **It never hands the child anything you did not opt into.** The child's environment is an allowlist;
25
25
  proxies, `PYTEST_ADDOPTS` and `PYTEST_PLUGINS` are dropped; AWS credentials are dummies; the home and
26
26
  per-user data directories point at an empty directory; git reads no system or global config and has
@@ -28,10 +28,12 @@ What codeterrain does guarantee is about its own, parent process:
28
28
  `pytest_args` and `respect_addopts` are honoured only from `--config` or your user config, and
29
29
  ignored, with a warning, when the repository's `pyproject.toml` sets them. A user-config `pass_env`
30
30
  applies to every repository you run on, trusted or not; a `--config` file scopes it to one run.
31
- 3. **It never lets the child write into the repository by default.** `addopts` are switched off,
31
+ 3. **It does not have pytest write into the repository by default.** `addopts` are switched off,
32
32
  `log_file` is blanked, and the known file-writing options of pytest and common plugins are refused,
33
33
  from the project's options and from `pytest_args`. The child runs in a private temp directory,
34
- outside the cache, which is removed afterwards.
34
+ outside the cache, which is removed afterwards. This stops reports and logs landing in the
35
+ checkout; it does not stop target code that opens a file by absolute path, which can write
36
+ anywhere you can.
35
37
  4. **It ends the child and what it started.** A timeout kills the whole process tree. On Windows a job
36
38
  object also kills what the child left behind after a normal exit; on POSIX its process group is.
37
39
 
@@ -76,12 +78,11 @@ passes the check.
76
78
 
77
79
  | | |
78
80
  |---|---|
79
- | **Guaranteed** (each has a regression test) | the parent never runs target code or a repository-shipped executable; the environment allowlist; repository config cannot widen it; git in the child has no credential helper; no default writes into the target; tree kill on timeout and of stragglers after exit (except a POSIX grandchild that started its own session); redaction of the values codeterrain knows; terminal escaping; path confinement |
81
+ | **Guaranteed** (each has a regression test) | the parent never runs target code or a repository-shipped executable; the environment allowlist; repository config cannot widen it; git in the child has no credential helper; no default writes into the target by pytest or its known plugins; tree kill on timeout and of stragglers after exit (except a POSIX grandchild that started its own session); redaction of the values codeterrain knows; terminal escaping; path confinement |
80
82
  | **Best-effort** | the network guard. It blocks accidental network calls from Python code in the child, and is not a boundary against hostile code: non-Python subprocesses, isolated-mode Python children, a freshly loaded `_socket`, target code that undoes the patch, `getfqdn`, the C socket class and C extensions are not covered. [docs/sandbox.md](docs/sandbox.md) lists every gap. |
81
- | **Not covered** | secrets codeterrain does not know (a value the target reads from a file and echoes); loopback services, which are reachable by design; a POSIX grandchild that started its own session; hostile code reading the real home by absolute path or an OS credential store; a child that rewrites the cache it worked out from the root; a crash of a native parser a language pack uses (the TypeScript pack's tree-sitter parses front-end source in terrain's process: it runs nothing from that source, and a file over 2,000,000 bytes is not read, but a parser fault would end the command rather than report the file) |
83
+ | **Not covered** | secrets codeterrain does not know (a value the target reads from a file and echoes); loopback services, which are reachable by design; a POSIX grandchild that started its own session; hostile code reading the real home by absolute path or an OS credential store; a child that rewrites the cache it worked out from the root; a crash of a native parser a language pack uses (the TypeScript pack's tree-sitter parses front-end source in codeterrain's process: it runs nothing from that source, and a file over 2,000,000 bytes is not read, but a parser fault would end the command rather than report the file) |
82
84
 
83
85
  ## Reporting a vulnerability
84
86
 
85
- The repository is private for now. Open a private security advisory on the repository
86
- (Security, then Advisories, then "Report a vulnerability"), with a reproduction if you have one. Do not
87
- open a public issue.
87
+ Report it privately on GitHub: on the repository's Security tab, choose "Report a vulnerability", and
88
+ include a reproduction if you have one. Do not open a public issue.