verifaied 0.2.0.dev31__tar.gz → 0.3.0.dev32__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 (24) hide show
  1. {verifaied-0.2.0.dev31 → verifaied-0.3.0.dev32}/PKG-INFO +77 -15
  2. {verifaied-0.2.0.dev31 → verifaied-0.3.0.dev32}/README.md +75 -13
  3. {verifaied-0.2.0.dev31 → verifaied-0.3.0.dev32}/pyproject.toml +2 -2
  4. {verifaied-0.2.0.dev31 → verifaied-0.3.0.dev32}/src/verifaied/cli.py +78 -16
  5. {verifaied-0.2.0.dev31 → verifaied-0.3.0.dev32}/src/verifaied/client.py +8 -1
  6. verifaied-0.3.0.dev32/src/verifaied/uploader.py +285 -0
  7. verifaied-0.3.0.dev32/tests/conftest.py +20 -0
  8. {verifaied-0.2.0.dev31 → verifaied-0.3.0.dev32}/tests/test_cli.py +219 -1
  9. {verifaied-0.2.0.dev31 → verifaied-0.3.0.dev32}/tests/test_client.py +30 -3
  10. {verifaied-0.2.0.dev31 → verifaied-0.3.0.dev32}/tests/test_uploader.py +162 -5
  11. {verifaied-0.2.0.dev31 → verifaied-0.3.0.dev32}/uv.lock +1 -1
  12. verifaied-0.2.0.dev31/src/verifaied/uploader.py +0 -175
  13. {verifaied-0.2.0.dev31 → verifaied-0.3.0.dev32}/.gitignore +0 -0
  14. {verifaied-0.2.0.dev31 → verifaied-0.3.0.dev32}/LICENSE +0 -0
  15. {verifaied-0.2.0.dev31 → verifaied-0.3.0.dev32}/src/verifaied/__init__.py +0 -0
  16. {verifaied-0.2.0.dev31 → verifaied-0.3.0.dev32}/src/verifaied/__main__.py +0 -0
  17. {verifaied-0.2.0.dev31 → verifaied-0.3.0.dev32}/src/verifaied/analyzer.py +0 -0
  18. {verifaied-0.2.0.dev31 → verifaied-0.3.0.dev32}/src/verifaied/config.py +0 -0
  19. {verifaied-0.2.0.dev31 → verifaied-0.3.0.dev32}/src/verifaied/prompts.py +0 -0
  20. {verifaied-0.2.0.dev31 → verifaied-0.3.0.dev32}/tests/__init__.py +0 -0
  21. {verifaied-0.2.0.dev31 → verifaied-0.3.0.dev32}/tests/test_check.py +0 -0
  22. {verifaied-0.2.0.dev31 → verifaied-0.3.0.dev32}/tests/test_check_done.py +0 -0
  23. {verifaied-0.2.0.dev31 → verifaied-0.3.0.dev32}/tests/test_config.py +0 -0
  24. {verifaied-0.2.0.dev31 → verifaied-0.3.0.dev32}/tests/test_prompt_parity.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: verifaied
3
- Version: 0.2.0.dev31
4
- Summary: Find what's untested in your Python code — locally, no account required
3
+ Version: 0.3.0.dev32
4
+ Summary: Find what's untested in your code — locally, no account required
5
5
  Project-URL: Homepage, https://pypi.org/project/verifaied/
6
6
  Author: Kyle Richards
7
7
  License: MIT License
@@ -40,17 +40,19 @@ Description-Content-Type: text/markdown
40
40
 
41
41
  # verifaied
42
42
 
43
- Find what's untested in your Python code, so you (or your coding agent) can fix
43
+ Find what's untested in your code, so you (or your coding agent) can fix
44
44
  it without waiting for CI.
45
45
 
46
46
  Two verbs:
47
47
 
48
48
  - **`verifaied check`** — runs entirely on your machine. No account, no API
49
- token, no network call. Tells you which functions are untested or only
50
- partially covered, and can hand you a ready-to-paste prompt for each one.
49
+ token, no network call. Tells you which Python functions are untested or
50
+ only partially covered, and can hand you a ready-to-paste prompt for each
51
+ one.
51
52
  - **`verifaied upload`** — syncs the same coverage to the [verifAIed](https://verifaied.app)
52
53
  app, which adds history, branch diffing against CI, a web UI, and AI-written
53
- test prompts. Needs a free account.
54
+ test prompts. Handles both Python (pytest-cov) and TypeScript/JavaScript
55
+ (vitest/Istanbul). Needs a free account.
54
56
 
55
57
  ## Install
56
58
 
@@ -78,6 +80,10 @@ verifaied check
78
80
  Nothing leaves your machine — the whole analysis is a local AST pass over the
79
81
  source `coverage.json` already points at.
80
82
 
83
+ `check` is Python-only: it reads pytest-cov's `coverage.json`. Point it at a
84
+ vitest/Istanbul report and it exits with a note to use `verifaied upload`,
85
+ which analyzes TypeScript/JavaScript on the hosted side.
86
+
81
87
  Add `--prompt` to get a concrete, ready-to-paste instruction for each one,
82
88
  generated from the function's signature and its uncovered lines (no LLM, no
83
89
  cost, unlimited):
@@ -133,17 +139,37 @@ verifaied upload --repo kyle/verifaied # owner/name slug
133
139
  verifaied upload --repo <UUID> # raw UUID, no lookup
134
140
  ```
135
141
 
136
- The CLI reads `coverage.json`, pulls the source for every file it
137
- references from your working tree, and posts everything to
138
- `/repositories/<id>/local-coverage`. The response prints a summary of
139
- untested / partial / failing functions so you (or your LLM) can fix
140
- them on the next iteration.
142
+ The CLI reads your coverage report(s), pulls the source for every file
143
+ they reference from your working tree, and posts everything to
144
+ `/repositories/<id>/local-coverage`. With no `--coverage` flag it
145
+ auto-discovers the report, looking in order for `./coverage.json`
146
+ (pytest-cov), then `./coverage/coverage-final.json`, then a bare
147
+ `./coverage-final.json` (vitest/Istanbul). Pass `--coverage` one or more
148
+ times to name reports explicitly — repeat it to push Python and
149
+ TypeScript coverage in a single upload; multiple reports merge per
150
+ language on the server. The response prints a summary of untested /
151
+ partial / failing functions so you (or your LLM) can fix them on the
152
+ next iteration.
153
+
154
+ ### What the upload reports
155
+
156
+ The summary leads with counts so an LLM (or you) sees the deltas first:
157
+
158
+ - **functions matched** — functions in the uploaded report the analyzer
159
+ resolved against your source.
160
+ - **functions preserved** — functions retained from a previous upload in
161
+ another language for the same branch (only shown when non-zero). A
162
+ Python-only upload never wipes the TypeScript functions a vitest upload
163
+ recorded on the same branch, and vice versa — the branch's total tracked
164
+ functions is _matched + preserved_.
165
+ - **untested / partial / failing tests** — what to fix next.
141
166
 
142
167
  ### What gets uploaded
143
168
 
144
- The CLI only sends files that appear in `coverage.json`'s `"files"`
145
- map — i.e. exactly the files `pytest --cov` instrumented. There is no
146
- directory walk and no glob:
169
+ The CLI only sends files that appear in your coverage report — i.e.
170
+ exactly the files your test run instrumented (pytest-cov's `"files"`
171
+ map, or the top-level file keys in an Istanbul `coverage-final.json`).
172
+ There is no directory walk and no glob:
147
173
 
148
174
  - `.env`, build artefacts, vendored libraries, and anything outside
149
175
  your coverage scope are never read.
@@ -169,13 +195,49 @@ expand **Recently deleted** under the branches grid, and use
169
195
  to the uploaded source — the per-card **Delete** is a soft-delete that
170
196
  keeps the data around for accidental-deletion recovery.
171
197
 
198
+ ### TypeScript / JavaScript
199
+
200
+ `upload` ingests vitest/Istanbul coverage the same way it ingests
201
+ pytest-cov's. Generate it with vitest's **Istanbul** provider:
202
+
203
+ ```bash
204
+ npm i -D @vitest/coverage-istanbul
205
+ vitest run --coverage --coverage.provider=istanbul --coverage.reporter=json \
206
+ --reporter=junit --outputFile.junit=junit.xml
207
+ verifaied upload --junit junit.xml
208
+ ```
209
+
210
+ The Istanbul provider is required. Istanbul applies your source maps, so
211
+ `coverage/coverage-final.json` reports the original `.ts` / `.tsx`
212
+ sources — which is what verifAIed matches function definitions against.
213
+ vitest's default **v8** provider emits a different report shape that
214
+ verifAIed rejects, so `--coverage.provider=istanbul` is not optional. The
215
+ `--reporter=junit --outputFile.junit=junit.xml` flags produce the JUnit
216
+ XML that feeds the failing-tests panel; hand it to the upload with
217
+ `--junit`.
218
+
219
+ If a report references only compiled output — paths under `dist/`,
220
+ `build/`, `out/`, or `.next/` with no `.ts` / `.tsx` / `.jsx` entries —
221
+ the upload is refused. That's the source-maps-not-applied case: coverage
222
+ landed on bundled JS instead of your sources, and verifAIed can't map it
223
+ back. Switching to the Istanbul provider is the fix.
224
+
225
+ **Supported formats.** pytest-cov `coverage.json` and Istanbul
226
+ `coverage-final.json` only. LCOV, Cobertura XML, jest's own format, and
227
+ c8 / v8-style JSON are rejected with a clear error naming the two
228
+ supported shapes.
229
+
172
230
  ### `upload` flags
173
231
 
174
232
  - `--repo / -r <UUID|owner/name>` — repository to upload to. If omitted,
175
233
  the CLI auto-detects from the GitHub `origin` remote.
176
234
  - `--branch / -b <name>` — branch to attach the upload to (default:
177
235
  `git branch --show-current`, then `local`)
178
- - `--coverage <path>` — path to coverage.json (default: `./coverage.json`)
236
+ - `--coverage <path>` — coverage report to upload. **Repeatable** — pass
237
+ it once per report to combine Python and TypeScript coverage in one
238
+ upload. Accepts pytest-cov `coverage.json` and Istanbul
239
+ `coverage-final.json`. Default: auto-detect `./coverage.json`, then
240
+ `./coverage/coverage-final.json` (or a bare `./coverage-final.json`).
179
241
  - `--junit <path>` — optional JUnit XML for failing-test detail
180
242
  - `--commit-sha <sha>` — commit sha for display (default: `git rev-parse HEAD`)
181
243
  - `--root <path>` — root the coverage paths are relative to (default: cwd)
@@ -1,16 +1,18 @@
1
1
  # verifaied
2
2
 
3
- Find what's untested in your Python code, so you (or your coding agent) can fix
3
+ Find what's untested in your code, so you (or your coding agent) can fix
4
4
  it without waiting for CI.
5
5
 
6
6
  Two verbs:
7
7
 
8
8
  - **`verifaied check`** — runs entirely on your machine. No account, no API
9
- token, no network call. Tells you which functions are untested or only
10
- partially covered, and can hand you a ready-to-paste prompt for each one.
9
+ token, no network call. Tells you which Python functions are untested or
10
+ only partially covered, and can hand you a ready-to-paste prompt for each
11
+ one.
11
12
  - **`verifaied upload`** — syncs the same coverage to the [verifAIed](https://verifaied.app)
12
13
  app, which adds history, branch diffing against CI, a web UI, and AI-written
13
- test prompts. Needs a free account.
14
+ test prompts. Handles both Python (pytest-cov) and TypeScript/JavaScript
15
+ (vitest/Istanbul). Needs a free account.
14
16
 
15
17
  ## Install
16
18
 
@@ -38,6 +40,10 @@ verifaied check
38
40
  Nothing leaves your machine — the whole analysis is a local AST pass over the
39
41
  source `coverage.json` already points at.
40
42
 
43
+ `check` is Python-only: it reads pytest-cov's `coverage.json`. Point it at a
44
+ vitest/Istanbul report and it exits with a note to use `verifaied upload`,
45
+ which analyzes TypeScript/JavaScript on the hosted side.
46
+
41
47
  Add `--prompt` to get a concrete, ready-to-paste instruction for each one,
42
48
  generated from the function's signature and its uncovered lines (no LLM, no
43
49
  cost, unlimited):
@@ -93,17 +99,37 @@ verifaied upload --repo kyle/verifaied # owner/name slug
93
99
  verifaied upload --repo <UUID> # raw UUID, no lookup
94
100
  ```
95
101
 
96
- The CLI reads `coverage.json`, pulls the source for every file it
97
- references from your working tree, and posts everything to
98
- `/repositories/<id>/local-coverage`. The response prints a summary of
99
- untested / partial / failing functions so you (or your LLM) can fix
100
- them on the next iteration.
102
+ The CLI reads your coverage report(s), pulls the source for every file
103
+ they reference from your working tree, and posts everything to
104
+ `/repositories/<id>/local-coverage`. With no `--coverage` flag it
105
+ auto-discovers the report, looking in order for `./coverage.json`
106
+ (pytest-cov), then `./coverage/coverage-final.json`, then a bare
107
+ `./coverage-final.json` (vitest/Istanbul). Pass `--coverage` one or more
108
+ times to name reports explicitly — repeat it to push Python and
109
+ TypeScript coverage in a single upload; multiple reports merge per
110
+ language on the server. The response prints a summary of untested /
111
+ partial / failing functions so you (or your LLM) can fix them on the
112
+ next iteration.
113
+
114
+ ### What the upload reports
115
+
116
+ The summary leads with counts so an LLM (or you) sees the deltas first:
117
+
118
+ - **functions matched** — functions in the uploaded report the analyzer
119
+ resolved against your source.
120
+ - **functions preserved** — functions retained from a previous upload in
121
+ another language for the same branch (only shown when non-zero). A
122
+ Python-only upload never wipes the TypeScript functions a vitest upload
123
+ recorded on the same branch, and vice versa — the branch's total tracked
124
+ functions is _matched + preserved_.
125
+ - **untested / partial / failing tests** — what to fix next.
101
126
 
102
127
  ### What gets uploaded
103
128
 
104
- The CLI only sends files that appear in `coverage.json`'s `"files"`
105
- map — i.e. exactly the files `pytest --cov` instrumented. There is no
106
- directory walk and no glob:
129
+ The CLI only sends files that appear in your coverage report — i.e.
130
+ exactly the files your test run instrumented (pytest-cov's `"files"`
131
+ map, or the top-level file keys in an Istanbul `coverage-final.json`).
132
+ There is no directory walk and no glob:
107
133
 
108
134
  - `.env`, build artefacts, vendored libraries, and anything outside
109
135
  your coverage scope are never read.
@@ -129,13 +155,49 @@ expand **Recently deleted** under the branches grid, and use
129
155
  to the uploaded source — the per-card **Delete** is a soft-delete that
130
156
  keeps the data around for accidental-deletion recovery.
131
157
 
158
+ ### TypeScript / JavaScript
159
+
160
+ `upload` ingests vitest/Istanbul coverage the same way it ingests
161
+ pytest-cov's. Generate it with vitest's **Istanbul** provider:
162
+
163
+ ```bash
164
+ npm i -D @vitest/coverage-istanbul
165
+ vitest run --coverage --coverage.provider=istanbul --coverage.reporter=json \
166
+ --reporter=junit --outputFile.junit=junit.xml
167
+ verifaied upload --junit junit.xml
168
+ ```
169
+
170
+ The Istanbul provider is required. Istanbul applies your source maps, so
171
+ `coverage/coverage-final.json` reports the original `.ts` / `.tsx`
172
+ sources — which is what verifAIed matches function definitions against.
173
+ vitest's default **v8** provider emits a different report shape that
174
+ verifAIed rejects, so `--coverage.provider=istanbul` is not optional. The
175
+ `--reporter=junit --outputFile.junit=junit.xml` flags produce the JUnit
176
+ XML that feeds the failing-tests panel; hand it to the upload with
177
+ `--junit`.
178
+
179
+ If a report references only compiled output — paths under `dist/`,
180
+ `build/`, `out/`, or `.next/` with no `.ts` / `.tsx` / `.jsx` entries —
181
+ the upload is refused. That's the source-maps-not-applied case: coverage
182
+ landed on bundled JS instead of your sources, and verifAIed can't map it
183
+ back. Switching to the Istanbul provider is the fix.
184
+
185
+ **Supported formats.** pytest-cov `coverage.json` and Istanbul
186
+ `coverage-final.json` only. LCOV, Cobertura XML, jest's own format, and
187
+ c8 / v8-style JSON are rejected with a clear error naming the two
188
+ supported shapes.
189
+
132
190
  ### `upload` flags
133
191
 
134
192
  - `--repo / -r <UUID|owner/name>` — repository to upload to. If omitted,
135
193
  the CLI auto-detects from the GitHub `origin` remote.
136
194
  - `--branch / -b <name>` — branch to attach the upload to (default:
137
195
  `git branch --show-current`, then `local`)
138
- - `--coverage <path>` — path to coverage.json (default: `./coverage.json`)
196
+ - `--coverage <path>` — coverage report to upload. **Repeatable** — pass
197
+ it once per report to combine Python and TypeScript coverage in one
198
+ upload. Accepts pytest-cov `coverage.json` and Istanbul
199
+ `coverage-final.json`. Default: auto-detect `./coverage.json`, then
200
+ `./coverage/coverage-final.json` (or a bare `./coverage-final.json`).
139
201
  - `--junit <path>` — optional JUnit XML for failing-test detail
140
202
  - `--commit-sha <sha>` — commit sha for display (default: `git rev-parse HEAD`)
141
203
  - `--root <path>` — root the coverage paths are relative to (default: cwd)
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "verifaied"
3
- version = "0.2.0.dev31"
4
- description = "Find what's untested in your Python code — locally, no account required"
3
+ version = "0.3.0.dev32"
4
+ description = "Find what's untested in your code — locally, no account required"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
7
7
  license = { file = "LICENSE" }
@@ -41,6 +41,7 @@ from verifaied.uploader import (
41
41
  UploaderError,
42
42
  build_payload,
43
43
  detect_branch,
44
+ detect_coverage_kind,
44
45
  detect_repo_slug,
45
46
  )
46
47
 
@@ -91,10 +92,14 @@ def upload_command(
91
92
  "-b",
92
93
  help="Branch name. Defaults to `git branch --show-current`.",
93
94
  ),
94
- coverage: Path | None = typer.Option(
95
- None,
95
+ coverage: list[Path] = typer.Option(
96
+ [],
96
97
  "--coverage",
97
- help="Path to the pytest-cov json report (default: ./coverage.json).",
98
+ help=(
99
+ "Coverage report path(s). Repeatable. Accepts pytest-cov "
100
+ "coverage.json and Istanbul coverage-final.json. Default: "
101
+ "auto-detect coverage.json and/or coverage/coverage-final.json."
102
+ ),
98
103
  ),
99
104
  junit: Path | None = typer.Option(
100
105
  None,
@@ -109,7 +114,11 @@ def upload_command(
109
114
  root: Path | None = typer.Option(
110
115
  None,
111
116
  "--root",
112
- help="Root the coverage paths are relative to (default: cwd).",
117
+ help=(
118
+ "Root the coverage paths are relative to (default: cwd). Its "
119
+ "position inside the git repo is the prefix every stored path "
120
+ "is rooted by, so paths display repo-rooted."
121
+ ),
113
122
  ),
114
123
  api_url: str | None = typer.Option(
115
124
  None,
@@ -138,23 +147,31 @@ def upload_command(
138
147
  # block the preview. Skip the token check for that path.
139
148
  if not dry_run and not resolved_token:
140
149
  err_console.print(
141
- f"[red]error[/red]: no API token (set ${ENV_API_TOKEN} or pass --token)"
150
+ "[red]VerifAIed error[/red]: no API token "
151
+ f"(set ${ENV_API_TOKEN} or pass --token)"
142
152
  )
143
153
  raise typer.Exit(code=1)
144
154
 
145
- coverage_path = coverage or Path("coverage.json")
146
155
  root_path = root or Path.cwd()
156
+ coverage_paths = list(coverage) if coverage else _discover_coverage_files()
157
+ if not coverage_paths:
158
+ err_console.print(
159
+ "[red]VerifAIed error[/red]: no coverage file found (looked for "
160
+ "coverage.json, coverage/coverage-final.json, coverage-final.json). "
161
+ "Run `pytest --cov --cov-report=json` or `vitest run --coverage` first."
162
+ )
163
+ raise typer.Exit(code=1)
147
164
 
148
165
  try:
149
166
  payload = build_payload(
150
- coverage_path=coverage_path,
167
+ coverage_paths=coverage_paths,
151
168
  root=root_path,
152
169
  branch=branch,
153
170
  commit_sha=commit_sha,
154
171
  junit_path=junit,
155
172
  )
156
173
  except UploaderError as e:
157
- err_console.print(f"[red]error[/red]: {e}")
174
+ err_console.print(f"[red]VerifAIed error[/red]: {e}")
158
175
  raise typer.Exit(code=1) from e
159
176
 
160
177
  if dry_run:
@@ -169,7 +186,7 @@ def upload_command(
169
186
  repo_input=repo, api_url=resolved_url, token=resolved_token
170
187
  )
171
188
  except (UploaderError, ApiError, RepoNotFoundError) as e:
172
- err_console.print(f"[red]error[/red]: {e}")
189
+ err_console.print(f"[red]VerifAIed error[/red]: {e}")
173
190
  raise typer.Exit(code=2 if isinstance(e, ApiError) else 1) from e
174
191
 
175
192
  try:
@@ -180,7 +197,7 @@ def upload_command(
180
197
  payload=payload,
181
198
  )
182
199
  except ApiError as e:
183
- err_console.print(f"[red]error[/red]: {e.message}")
200
+ err_console.print(f"[red]VerifAIed error[/red]: {e.message}")
184
201
  raise typer.Exit(code=2) from e
185
202
 
186
203
  _render(result, api_url=resolved_url, repo_id=repo_id)
@@ -191,7 +208,11 @@ def check_command(
191
208
  coverage: Path | None = typer.Option(
192
209
  None,
193
210
  "--coverage",
194
- help="Path to the pytest-cov json report (default: ./coverage.json).",
211
+ help=(
212
+ "Path to the coverage report (default: ./coverage.json). "
213
+ "check is Python-only; Istanbul/vitest reports are refused — "
214
+ "use `verifaied upload` for TS/JS."
215
+ ),
195
216
  ),
196
217
  root: Path | None = typer.Option(
197
218
  None,
@@ -245,7 +266,7 @@ def check_command(
245
266
  raw = coverage_path.read_text(encoding="utf-8")
246
267
  except OSError as e:
247
268
  err_console.print(
248
- f"[red]error[/red]: cannot read {coverage_path} — run "
269
+ f"[red]VerifAIed error[/red]: cannot read {coverage_path} — run "
249
270
  f"`pytest --cov --cov-report=json` first"
250
271
  )
251
272
  raise typer.Exit(code=1) from e
@@ -253,9 +274,23 @@ def check_command(
253
274
  try:
254
275
  coverage_json = json.loads(raw)
255
276
  except json.JSONDecodeError as e:
256
- err_console.print(f"[red]error[/red]: {coverage_path} is not valid JSON: {e}")
277
+ err_console.print(
278
+ f"[red]VerifAIed error[/red]: {coverage_path} is not valid JSON: {e}"
279
+ )
257
280
  raise typer.Exit(code=1) from e
258
281
 
282
+ if isinstance(coverage_json, dict):
283
+ try:
284
+ kind = detect_coverage_kind(coverage_json)
285
+ except UploaderError:
286
+ kind = None
287
+ if kind == "istanbul":
288
+ err_console.print(
289
+ "[red]VerifAIed error[/red]: verifaied check is Python-only "
290
+ "today — for TS/JS use `verifaied upload` (hosted analysis)."
291
+ )
292
+ raise typer.Exit(code=1)
293
+
259
294
  summary = analyze_coverage(coverage_json, root=root_path)
260
295
 
261
296
  if summary.files_missing:
@@ -357,7 +392,8 @@ def check_done_command(
357
392
  resolved_token = resolved_api_token(token)
358
393
  if not resolved_token:
359
394
  err_console.print(
360
- f"[red]error[/red]: no API token (set ${ENV_API_TOKEN} or pass --token)"
395
+ "[red]VerifAIed error[/red]: no API token "
396
+ f"(set ${ENV_API_TOKEN} or pass --token)"
361
397
  )
362
398
  raise typer.Exit(code=1)
363
399
 
@@ -366,7 +402,7 @@ def check_done_command(
366
402
  repo_input=repo, api_url=resolved_url, token=resolved_token
367
403
  )
368
404
  except (UploaderError, ApiError, RepoNotFoundError) as e:
369
- err_console.print(f"[red]error[/red]: {e}")
405
+ err_console.print(f"[red]VerifAIed error[/red]: {e}")
370
406
  raise typer.Exit(code=2 if isinstance(e, ApiError) else 1) from e
371
407
 
372
408
  resolved_branch = branch or detect_branch()
@@ -378,7 +414,7 @@ def check_done_command(
378
414
  branch=resolved_branch,
379
415
  )
380
416
  except ApiError as e:
381
- err_console.print(f"[red]error[/red]: {e.message}")
417
+ err_console.print(f"[red]VerifAIed error[/red]: {e.message}")
382
418
  raise typer.Exit(code=2) from e
383
419
 
384
420
  if as_json:
@@ -530,6 +566,21 @@ def _resolve_repo(*, repo_input: str | None, api_url: str, token: str) -> UUID:
530
566
  return resolve_repo_id(api_url=api_url, token=token, owner=slug[0], name=slug[1])
531
567
 
532
568
 
569
+ def _discover_coverage_files() -> list[Path]:
570
+ """Default coverage discovery when no ``--coverage`` is passed: pytest's
571
+ ``coverage.json`` and/or vitest's ``coverage/coverage-final.json``
572
+ (falling back to a bare ``coverage-final.json``). Order preserved so a
573
+ same-file collision resolves later-wins consistently with the backend."""
574
+ found: list[Path] = []
575
+ if Path("coverage.json").is_file():
576
+ found.append(Path("coverage.json"))
577
+ if Path("coverage/coverage-final.json").is_file():
578
+ found.append(Path("coverage/coverage-final.json"))
579
+ elif Path("coverage-final.json").is_file():
580
+ found.append(Path("coverage-final.json"))
581
+ return found
582
+
583
+
533
584
  def _render(result: UploadResult, *, api_url: str, repo_id: UUID) -> None:
534
585
  """Print a compact, scannable summary. Designed for LLM tail-reads
535
586
  too: the counts come first so the model sees the deltas without
@@ -538,6 +589,8 @@ def _render(result: UploadResult, *, api_url: str, repo_id: UUID) -> None:
538
589
  table.add_row("branch", result.branch)
539
590
  table.add_row("files received", str(result.files_received))
540
591
  table.add_row("functions matched", str(result.functions_matched))
592
+ if result.functions_preserved > 0:
593
+ table.add_row("functions preserved", str(result.functions_preserved))
541
594
  table.add_row("untested", str(len(result.untested)))
542
595
  table.add_row("partial", str(len(result.partial)))
543
596
  table.add_row("failing tests", str(len(result.failing_tests)))
@@ -576,6 +629,15 @@ def _render_dry_run(payload, *, repo_input: str | None) -> None:
576
629
  header.add_row("repo", repo_display)
577
630
  header.add_row("branch", payload.branch)
578
631
  header.add_row("commit", payload.commit_sha or "-")
632
+ if payload.path_prefix is None:
633
+ prefix_display = "(none — server resolves against the repo tree)"
634
+ elif payload.path_prefix == "":
635
+ prefix_display = '"" (repo root)'
636
+ else:
637
+ prefix_display = (
638
+ f"{payload.path_prefix} — stored as {payload.path_prefix}/<path>"
639
+ )
640
+ header.add_row("path prefix", prefix_display)
579
641
  header.add_row("files", str(len(payload.files)))
580
642
  total_bytes = sum(
581
643
  len(content.encode("utf-8")) for content in payload.files.values()
@@ -45,6 +45,9 @@ class UploadResult:
45
45
  failing_tests: list[dict[str, Any]]
46
46
  message: str
47
47
  summary: dict[str, Any]
48
+ # Functions retained from earlier uploads of other languages (0 on
49
+ # older backends that don't return the field).
50
+ functions_preserved: int = 0
48
51
 
49
52
 
50
53
  def upload(
@@ -65,7 +68,10 @@ def upload(
65
68
  "branch": payload.branch,
66
69
  "commit_sha": payload.commit_sha,
67
70
  "files": payload.files,
68
- "coverage_json": payload.coverage_json,
71
+ "coverage_reports": payload.coverage_reports,
72
+ # Sent unconditionally; older backends ignore the extra field.
73
+ # ``None`` tells the server to resolve the prefix itself.
74
+ "path_prefix": payload.path_prefix,
69
75
  }
70
76
  if payload.junit_xml is not None:
71
77
  body["junit_xml"] = payload.junit_xml
@@ -92,6 +98,7 @@ def upload(
92
98
  failing_tests=data.get("failing_tests") or [],
93
99
  message=data.get("message") or "",
94
100
  summary=data.get("summary") or {},
101
+ functions_preserved=data.get("functions_preserved") or 0,
95
102
  )
96
103
 
97
104