server/ is a Kotlin Spring Boot service with SQLite: POST /api/usage stores the usage record of a run,
GET /api/stats returns costs per period with details for a read token, and the app embeds <werkrevisor-dashboard>.
The command-line tool reports each run when WERKREVISOR_STATS_URL is set; a failure never changes the review.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-code
marked the pull request as ready for review 2026-10-06 09:40:37 +02:00
YELLOW possible secrets in added lines: server/src/test/kotlin/de/hoennig/werkrevisor/server/UsageApiIntegrationTest.kt:154 (generic_secret)
YELLOW PR too large for an AI-only approval: 1280 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM skipped (possible secrets in the diff), duration 0.3 s, no model call, cost 0 USD, config: defaults + host + repository (.werkrevisor.toml at base 7aee874).
Reviewed head `15bf85ace864733a70786416b896843d984285b8` against base `7aee874ec7ceeb3e564d92d9fb68795b7188a084`, werkrevisor 0.17.0.
<!-- werkrevisor:15bf85ace864733a70786416b896843d984285b8 -->
<!-- werkrevisor-verdict:YELLOW -->
<!-- werkrevisor-folded -->
<details><summary>🟡 werkrevisor YELLOW for <code>15bf85a</code>, superseded by <a href="http://git.javagil.de/mi/werkrevisor/pulls/21#issuecomment-1054">the result for <code>2322b76</code></a></summary>
## 🟡 werkrevisor: YELLOW / HUMAN_REVIEW
Change kind: `mixed`.
### Reasons
- **YELLOW** risky paths changed: .werkrevisor.toml (built in)
- **YELLOW** possible secrets in added lines: server/src/test/kotlin/de/hoennig/werkrevisor/server/UsageApiIntegrationTest.kt:154 (generic_secret)
- **YELLOW** PR too large for an AI-only approval: 1280 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself
<sub>
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM skipped (possible secrets in the diff), duration 0.3 s, no model call, cost 0 USD, config: defaults + host + repository (.werkrevisor.toml at base 7aee874).<br>
Reviewed head `15bf85ace864733a70786416b896843d984285b8` against base `7aee874ec7ceeb3e564d92d9fb68795b7188a084`, werkrevisor 0.17.0.
</sub>
</details>
ai-reviewer
scheduled this pull request to auto merge when all checks succeed 2026-10-06 09:40:49 +02:00
ai-reviewer
requested review from mi 2026-10-06 09:40:49 +02:00
ai-review/assessment AI findings; a developer approval after checking them unlocks the merge; a human review request is pending; merge already scheduled
YELLOW PR too large for an AI-only approval: 1282 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself
YELLOW 1 changed file was given to the AI only as diff, not in full, so it could not check it against the rest of its file (each larger than [review] max_full_file_chars = 40000): werkrevisor/cli.py
YELLOW files the model cannot review: .gitignore, server/gradle/wrapper/gradle-wrapper.jar
YELLOW 2 finding(s) possibly severe, but Jev doubts that they are real: werkrevisor/cli.py:543-550, server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageStore.kt:26
hint 1 finding(s) of low severity or rated minor by Jev: .werkrevisor.toml:28
hint model recommends merge with caution
Findings
[medium] Untrusted record fields are unvalidated and stored at server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:45-70, ORANGE, as a line comment
[medium] Stats failure message is not redacted before printing at werkrevisor/cli.py:543-550, YELLOW, as a line comment
[medium] Dedup key uses the timestamp, so retries with a new timestamp double count at server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageStore.kt:26, YELLOW, as a line comment
1 more finding(s) not shown.
Classifier
15 of 15 units passed all questions (1 PR document(s)).
3 answers ignored because of a confidence below [classifier] min_confidence.
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 72638, calls 1, tokens in 72139 / out 1183 (reasoning included in out), duration 18.6 s, cost 0.1561 USD (reported), classifier `jev-1.13.0` calls 15, tokens in 12675, cost 0.0005 USD, finding judge `jev-1.13.0` calls 3, tokens in 4348, cost 0.0002 USD, config: defaults + host + repository (.werkrevisor.toml at base 7aee874).
Reviewed head `2322b763c030af942b91d366009dc3112bc3e628` against base `7aee874ec7ceeb3e564d92d9fb68795b7188a084`, werkrevisor 0.17.0.
<!-- werkrevisor:2322b763c030af942b91d366009dc3112bc3e628 -->
<!-- werkrevisor-verdict:ORANGE -->
<!-- werkrevisor-folded -->
<details><summary>🟠 werkrevisor ORANGE for <code>2322b76</code>, superseded by <a href="http://git.javagil.de/mi/werkrevisor/pulls/21#issuecomment-1061">the result for <code>53584ec</code></a></summary>
## 🟠 werkrevisor: ORANGE / FINDINGS
Change kind: `mixed`.
### Reasons
- **ORANGE** 1 finding(s) that Jev rates as probably real and severe: server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:45-70
- **YELLOW** risky paths changed: .werkrevisor.toml (built in)
- **YELLOW** PR too large for an AI-only approval: 1282 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself
- **YELLOW** 1 changed file was given to the AI only as diff, not in full, so it could not check it against the rest of its file (each larger than [review] max_full_file_chars = 40000): werkrevisor/cli.py
- **YELLOW** files the model cannot review: .gitignore, server/gradle/wrapper/gradle-wrapper.jar
- **YELLOW** 2 finding(s) possibly severe, but Jev doubts that they are real: werkrevisor/cli.py:543-550, server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageStore.kt:26
- **hint** 1 finding(s) of low severity or rated minor by Jev: .werkrevisor.toml:28
- **hint** model recommends merge with caution
### Findings
1. **[medium] Untrusted record fields are unvalidated and stored** at `server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:45-70`, ORANGE, as a line comment
2. **[medium] Stats failure message is not redacted before printing** at `werkrevisor/cli.py:543-550`, YELLOW, as a line comment
3. **[medium] Dedup key uses the timestamp, so retries with a new timestamp double count** at `server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageStore.kt:26`, YELLOW, as a line comment
1 more finding(s) not shown.
### Classifier
15 of 15 units passed all questions (1 PR document(s)).
3 answers ignored because of a confidence below [classifier] min_confidence.
<sub>
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 72638, calls 1, tokens in 72139 / out 1183 (reasoning included in out), duration 18.6 s, cost 0.1561 USD (reported), classifier `jev-1.13.0` calls 15, tokens in 12675, cost 0.0005 USD, finding judge `jev-1.13.0` calls 3, tokens in 4348, cost 0.0002 USD, config: defaults + host + repository (.werkrevisor.toml at base 7aee874).<br>
Reviewed head `2322b763c030af942b91d366009dc3112bc3e628` against base `7aee874ec7ceeb3e564d92d9fb68795b7188a084`, werkrevisor 0.17.0.
</sub>
</details>
werkrevisor: ORANGE / FINDINGS for 2322b763c030af942b91d366009dc3112bc3e628.
The result comment of werkrevisor holds the details.
werkrevisor: ORANGE / FINDINGS for `2322b763c030af942b91d366009dc3112bc3e628`.
The result comment of werkrevisor holds the details.
<!-- werkrevisor-review:2322b763c030af942b91d366009dc3112bc3e628 -->
[low] hard_yellow glob for the server build does not cover subdirectories (werkrevisor)
hint
server/*.gradle.kts matches only the top-level build files. If the glob semantics are fnmatch-like, this is fine for server/build.gradle.kts and server/settings.gradle.kts. server/gradle/wrapper/gradle-wrapper.jar is covered by server/gradle/**. The wrapper jar itself is not shown in the diff, so it is unclear whether it is committed. If it is, it is a binary in the trust boundary and should be verified.
**[low] hard_yellow glob for the server build does not cover subdirectories** (werkrevisor)
hint
`server/*.gradle.kts` matches only the top-level build files. If the glob semantics are fnmatch-like, this is fine for `server/build.gradle.kts` and `server/settings.gradle.kts`. `server/gradle/wrapper/gradle-wrapper.jar` is covered by `server/gradle/**`. The wrapper jar itself is not shown in the diff, so it is unclear whether it is committed. If it is, it is a binary in the trust boundary and should be verified.
[medium] Untrusted record fields are unvalidated and stored (werkrevisor)
ORANGE; Jev: real 0.90, severity 3.0 of 3
toRecord accepts any repo, pr_url and verdict strings from any source token holder and stores them. There is no length limit and no URL scheme check on pr_url. The dashboard filters the link scheme client-side, but other embedders of GET /api/stats get raw values. Also asDouble() on a non-numeric cost_usd yields 0.0, so a malformed record is silently stored as free. A negative cost is also accepted and lowers the sums. Validate scheme, length and cost range server-side.
**[medium] Untrusted record fields are unvalidated and stored** (werkrevisor)
ORANGE; Jev: real 0.90, severity 3.0 of 3
`toRecord` accepts any `repo`, `pr_url` and `verdict` strings from any source token holder and stores them. There is no length limit and no URL scheme check on `pr_url`. The dashboard filters the link scheme client-side, but other embedders of `GET /api/stats` get raw values. Also `asDouble()` on a non-numeric `cost_usd` yields 0.0, so a malformed record is silently stored as free. A negative cost is also accepted and lowers the sums. Validate scheme, length and cost range server-side.
[medium] Dedup key uses the timestamp, so retries with a new timestamp double count (werkrevisor)
YELLOW; Jev: real 0.56, severity 2.8 of 3
runKey includes timestamp. Scenario 21.04 says a retried report counts once, which holds only for the identical record. A rerun of the same head commit, for example a manual re-trigger, produces a different timestamp and is counted again. That is arguably correct because each run costs money, but the PR document's wording suggests deduplication by run. Clarify the intent, because it determines whether costs are over- or under-counted.
**[medium] Dedup key uses the timestamp, so retries with a new timestamp double count** (werkrevisor)
YELLOW; Jev: real 0.56, severity 2.8 of 3
`runKey` includes `timestamp`. Scenario 21.04 says a retried report counts once, which holds only for the identical record. A rerun of the same head commit, for example a manual re-trigger, produces a different timestamp and is counted again. That is arguably correct because each run costs money, but the PR document's wording suggests deduplication by run. Clarify the intent, because it determines whether costs are over- or under-counted.
[medium] Stats failure message is not redacted before printing (werkrevisor)
YELLOW; Jev: real 0.47, severity 2.6 of 3
The stats client builds its error message from an exception, and redactor(...) is applied to the whole line, which is fine. But the StatsError raised in main for a missing token or an invalid URL is printed without redactor. WERKREVISOR_STATS_URL can contain userinfo or a secret query string, but the message only contains fixed text, so there is no leak today. The real risk is that execute() now runs the stats report after the usage record is written, with no timeout guard beyond the 5 s client timeout. A slow or unreachable service adds up to 5 s to every run, and a DNS stall can exceed it because urllib's timeout does not cover name resolution. Consider whether this latency is acceptable for the merge gate.
**[medium] Stats failure message is not redacted before printing** (werkrevisor)
YELLOW; Jev: real 0.47, severity 2.6 of 3
The stats client builds its error message from an exception, and `redactor(...)` is applied to the whole line, which is fine. But the `StatsError` raised in `main` for a missing token or an invalid URL is printed without `redactor`. `WERKREVISOR_STATS_URL` can contain userinfo or a secret query string, but the message only contains fixed text, so there is no leak today. The real risk is that `execute()` now runs the stats report after the usage record is written, with no timeout guard beyond the 5 s client timeout. A slow or unreachable service adds up to 5 s to every run, and a DNS stall can exceed it because urllib's timeout does not cover name resolution. Consider whether this latency is acceptable for the merge gate.
Resolves the ORANGE finding, the two YELLOW findings and the hint of the werkrevisor review on PR#21.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
YELLOW PR too large for an AI-only approval: 1326 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself
YELLOW 1 changed file was given to the AI only as diff, not in full, so it could not check it against the rest of its file (each larger than [review] max_full_file_chars = 40000): werkrevisor/cli.py
YELLOW files the model cannot review: .gitignore, server/gradle/wrapper/gradle-wrapper.jar
hint 4 finding(s) of low severity or rated minor by Jev: werkrevisor/cli.py:543-550, server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:54-97, server/src/main/kotlin/de/hoennig/werkrevisor/server/Statistics.kt:77-123 and 1 more
hint model recommends changes before merge
Findings
[high] Usage record written before stats report, but exit code can still change at werkrevisor/cli.py:543-550, hint, still open in an earlier line comment
[medium] Unbounded request body and non-finite numbers at server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:54-97, hint, as a line comment
[medium] Reads all records of 12 months on every stats request at server/src/main/kotlin/de/hoennig/werkrevisor/server/Statistics.kt:77-123, hint, as a line comment
14 of 15 units passed all questions (1 PR document(s)).
1 answer ignored because of a confidence below [classifier] min_confidence.
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 75095, calls 1, tokens in 74449 / out 987 (reasoning included in out), duration 16.0 s, cost 0.1588 USD (reported), classifier `jev-1.13.0` calls 15, tokens in 12839, cost 0.0005 USD, finding judge `jev-1.13.0` calls 3, tokens in 5434, cost 0.0002 USD, config: defaults + host + repository (.werkrevisor.toml at base 7aee874).
Reviewed head `53584ecdb19ac15f194f8d70a2a1000b6885e4e9` against base `7aee874ec7ceeb3e564d92d9fb68795b7188a084`, werkrevisor 0.17.0.
<!-- werkrevisor:53584ecdb19ac15f194f8d70a2a1000b6885e4e9 -->
<!-- werkrevisor-verdict:YELLOW -->
<!-- werkrevisor-folded -->
<details><summary>🟡 werkrevisor YELLOW for <code>53584ec</code>, superseded by <a href="http://git.javagil.de/mi/werkrevisor/pulls/21#issuecomment-1067">the result for <code>cc63405</code></a></summary>
## 🟡 werkrevisor: YELLOW / HUMAN_REVIEW
Change kind: `mixed`.
### Reasons
- **YELLOW** risky paths changed: .werkrevisor.toml (built in)
- **YELLOW** PR too large for an AI-only approval: 1326 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself
- **YELLOW** spec: docs/prs/2026-10-06-PR#21-stats-service-and-dashboard.md Scenario#21.04 more_than_one_aspect (0.67 > yellow_above = 0.6)
- **YELLOW** 1 changed file was given to the AI only as diff, not in full, so it could not check it against the rest of its file (each larger than [review] max_full_file_chars = 40000): werkrevisor/cli.py
- **YELLOW** files the model cannot review: .gitignore, server/gradle/wrapper/gradle-wrapper.jar
- **hint** 4 finding(s) of low severity or rated minor by Jev: werkrevisor/cli.py:543-550, server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:54-97, server/src/main/kotlin/de/hoennig/werkrevisor/server/Statistics.kt:77-123 and 1 more
- **hint** model recommends changes before merge
### Findings
1. **[high] Usage record written before stats report, but exit code can still change** at `werkrevisor/cli.py:543-550`, hint, still open in [an earlier line comment](http://git.javagil.de/mi/werkrevisor/pulls/21#issuecomment-1055)
2. **[medium] Unbounded request body and non-finite numbers** at `server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:54-97`, hint, as a line comment
3. **[medium] Reads all records of 12 months on every stats request** at `server/src/main/kotlin/de/hoennig/werkrevisor/server/Statistics.kt:77-123`, hint, as a line comment
1 more finding(s) not shown.
### Classifier
| Unit | Question | Problem | Confidence | Verdict |
|---|---|---|---|---|
| docs/prs/2026-10-06-PR#21-stats-service-and-dashboard.md Scenario#21.04 | more_than_one_aspect | 0.67 | – | YELLOW |
14 of 15 units passed all questions (1 PR document(s)).
1 answer ignored because of a confidence below [classifier] min_confidence.
<sub>
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 75095, calls 1, tokens in 74449 / out 987 (reasoning included in out), duration 16.0 s, cost 0.1588 USD (reported), classifier `jev-1.13.0` calls 15, tokens in 12839, cost 0.0005 USD, finding judge `jev-1.13.0` calls 3, tokens in 5434, cost 0.0002 USD, config: defaults + host + repository (.werkrevisor.toml at base 7aee874).<br>
Reviewed head `53584ecdb19ac15f194f8d70a2a1000b6885e4e9` against base `7aee874ec7ceeb3e564d92d9fb68795b7188a084`, werkrevisor 0.17.0.
</sub>
</details>
werkrevisor: YELLOW / HUMAN_REVIEW for 53584ecdb19ac15f194f8d70a2a1000b6885e4e9.
The result comment of werkrevisor holds the details.
werkrevisor: YELLOW / HUMAN_REVIEW for `53584ecdb19ac15f194f8d70a2a1000b6885e4e9`.
The result comment of werkrevisor holds the details.
<!-- werkrevisor-review:53584ecdb19ac15f194f8d70a2a1000b6885e4e9 -->
[low] Test secret scan exclusion widened for a new directory (werkrevisor)
hint
secret_scan_exclude now also covers server/src/test/**. This is consistent with tests/**, but real secrets in Kotlin tests would no longer be flagged. The change is intentional, so only a reviewer note.
**[low] Test secret scan exclusion widened for a new directory** (werkrevisor)
hint
`secret_scan_exclude` now also covers `server/src/test/**`. This is consistent with `tests/**`, but real secrets in Kotlin tests would no longer be flagged. The change is intentional, so only a reviewer note.
[medium] Reads all records of 12 months on every stats request (werkrevisor)
hint; Jev: real 0.67, severity 2.3 of 3
statistics() loads every record since the start of the month 11 months back into memory and filters it several times, on an unauthenticated endpoint. A client that polls /api/stats repeatedly costs one full table scan per request. The volume of a few thousand records per year makes this tolerable today, but the endpoint has no caching or rate limit.
**[medium] Reads all records of 12 months on every stats request** (werkrevisor)
hint; Jev: real 0.67, severity 2.3 of 3
`statistics()` loads every record since the start of the month 11 months back into memory and filters it several times, on an unauthenticated endpoint. A client that polls `/api/stats` repeatedly costs one full table scan per request. The volume of a few thousand records per year makes this tolerable today, but the endpoint has no caching or rate limit.
[medium] Unbounded request body and non-finite numbers (werkrevisor)
hint; Jev: real 0.57, severity 2.5 of 3
POST /api/usage accepts a JsonNode of any size from any holder of a source token, and stores run_key from repo, pr, head_sha and timestamp. head_sha is limited to 200 characters, but cost_status and model are stored without a value check. cost_status values other than the known ones (reported, unknown, none) are stored as is and make costKnown false for a costUsd of null only. This is a limited defect.
**[medium] Unbounded request body and non-finite numbers** (werkrevisor)
hint; Jev: real 0.57, severity 2.5 of 3
`POST /api/usage` accepts a `JsonNode` of any size from any holder of a source token, and stores `run_key` from `repo`, `pr`, `head_sha` and `timestamp`. `head_sha` is limited to 200 characters, but `cost_status` and `model` are stored without a value check. `cost_status` values other than the known ones (`reported`, `unknown`, `none`) are stored as is and make `costKnown` false for a `costUsd` of null only. This is a limited defect.
ai-review/assessment AI findings; a developer approval after checking them unlocks the merge; a human review request is pending; merge already scheduled
YELLOW PR too large for an AI-only approval: 1326 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself
YELLOW 1 changed file was given to the AI only as diff, not in full, so it could not check it against the rest of its file (each larger than [review] max_full_file_chars = 40000): werkrevisor/cli.py
YELLOW files the model cannot review: .gitignore, server/gradle/wrapper/gradle-wrapper.jar
hint 2 finding(s) of low severity or rated minor by Jev: werkrevisor/cli.py:543-550, server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:93
hint model recommends merge with caution
Findings
[medium] Dedup key allows duplicates when head_sha is absent at server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageStore.kt:26, ORANGE, still open in an earlier line comment
[medium] Stats report failure can crash the run at werkrevisor/cli.py:543-550, hint, still open in an earlier line comment
[medium] NaN or infinite cost handling is untested at server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:93, hint, as a line comment
Classifier
15 of 15 units passed all questions (1 PR document(s)).
2 answers ignored because of a confidence below [classifier] min_confidence.
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 75079, calls 1, tokens in 74438 / out 878 (reasoning included in out), duration 16.0 s, cost 0.1577 USD (reported), classifier `jev-1.13.0` calls 15, tokens in 12800, cost 0.0005 USD, finding judge `jev-1.13.0` calls 3, tokens in 3976, cost 0.0002 USD, config: defaults + host + repository (.werkrevisor.toml at base 7aee874).
Reviewed head `cc63405f7cb7a24e0356b15de72dc193b1265ed0` against base `7aee874ec7ceeb3e564d92d9fb68795b7188a084`, werkrevisor 0.17.0.
<!-- werkrevisor:cc63405f7cb7a24e0356b15de72dc193b1265ed0 -->
<!-- werkrevisor-verdict:ORANGE -->
<!-- werkrevisor-folded -->
<details><summary>🟠 werkrevisor ORANGE for <code>cc63405</code>, superseded by <a href="http://git.javagil.de/mi/werkrevisor/pulls/21#issuecomment-1071">the result for <code>0a55535</code></a></summary>
## 🟠 werkrevisor: ORANGE / FINDINGS
Change kind: `mixed`.
### Reasons
- **ORANGE** 1 finding(s) that Jev rates as probably real and severe: server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageStore.kt:26
- **YELLOW** risky paths changed: .werkrevisor.toml (built in)
- **YELLOW** PR too large for an AI-only approval: 1326 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself
- **YELLOW** 1 changed file was given to the AI only as diff, not in full, so it could not check it against the rest of its file (each larger than [review] max_full_file_chars = 40000): werkrevisor/cli.py
- **YELLOW** files the model cannot review: .gitignore, server/gradle/wrapper/gradle-wrapper.jar
- **hint** 2 finding(s) of low severity or rated minor by Jev: werkrevisor/cli.py:543-550, server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:93
- **hint** model recommends merge with caution
### Findings
1. **[medium] Dedup key allows duplicates when `head_sha` is absent** at `server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageStore.kt:26`, ORANGE, still open in [an earlier line comment](http://git.javagil.de/mi/werkrevisor/pulls/21#issuecomment-1057)
2. **[medium] Stats report failure can crash the run** at `werkrevisor/cli.py:543-550`, hint, still open in [an earlier line comment](http://git.javagil.de/mi/werkrevisor/pulls/21#issuecomment-1055)
3. **[medium] NaN or infinite cost handling is untested** at `server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:93`, hint, as a line comment
### Classifier
15 of 15 units passed all questions (1 PR document(s)).
2 answers ignored because of a confidence below [classifier] min_confidence.
<sub>
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 75079, calls 1, tokens in 74438 / out 878 (reasoning included in out), duration 16.0 s, cost 0.1577 USD (reported), classifier `jev-1.13.0` calls 15, tokens in 12800, cost 0.0005 USD, finding judge `jev-1.13.0` calls 3, tokens in 3976, cost 0.0002 USD, config: defaults + host + repository (.werkrevisor.toml at base 7aee874).<br>
Reviewed head `cc63405f7cb7a24e0356b15de72dc193b1265ed0` against base `7aee874ec7ceeb3e564d92d9fb68795b7188a084`, werkrevisor 0.17.0.
</sub>
</details>
werkrevisor: ORANGE / FINDINGS for cc63405f7cb7a24e0356b15de72dc193b1265ed0.
The result comment of werkrevisor holds the details.
werkrevisor: ORANGE / FINDINGS for `cc63405f7cb7a24e0356b15de72dc193b1265ed0`.
The result comment of werkrevisor holds the details.
<!-- werkrevisor-review:cc63405f7cb7a24e0356b15de72dc193b1265ed0 -->
[medium] NaN or infinite cost handling is untested (werkrevisor)
hint; Jev: real 0.74, severity 2.3 of 3
number() checks it in 0.0..MAX_COST_USD, which rejects NaN. The costStatus text is not validated against a known set. A source with a valid token can send arbitrary cost_status strings, and costKnown treats anything other than "unknown" as known when costUsd is present. This is minor, but unrecognized values silently count toward cost sums.
**[medium] NaN or infinite cost handling is untested** (werkrevisor)
hint; Jev: real 0.74, severity 2.3 of 3
`number()` checks `it in 0.0..MAX_COST_USD`, which rejects NaN. The `costStatus` text is not validated against a known set. A source with a valid token can send arbitrary `cost_status` strings, and `costKnown` treats anything other than "unknown" as known when `costUsd` is present. This is minor, but unrecognized values silently count toward cost sums.
ai-review/assessment AI findings; a developer approval after checking them unlocks the merge; a human review request is pending; merge already scheduled
ORANGE 2 finding(s) that Jev rates as probably real and severe: werkrevisor/cli.py:543-550, server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:53
YELLOW PR too large for an AI-only approval: 1336 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself
YELLOW 1 changed file was given to the AI only as diff, not in full, so it could not check it against the rest of its file (each larger than [review] max_full_file_chars = 40000): werkrevisor/cli.py
YELLOW files the model cannot review: .gitignore, server/gradle/wrapper/gradle-wrapper.jar
hint 1 finding(s) of low severity or rated minor by Jev: server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:26
hint model recommends changes before merge
Findings
[high] Stats report can fail the review at werkrevisor/cli.py:543-550, ORANGE, still open in an earlier line comment
[medium] Unlimited request body size and sloppy bearer parsing at server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:53, ORANGE, still open in an earlier line comment
[medium] Dedup key allows double counting at server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:26, hint, as a line comment
Classifier
15 of 15 units passed all questions (1 PR document(s)).
1 answer ignored because of a confidence below [classifier] min_confidence.
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 75647, calls 1, tokens in 75002 / out 886 (reasoning included in out), duration 16.6 s, cost 0.1589 USD (reported), classifier `jev-1.13.0` calls 15, tokens in 12809, cost 0.0005 USD, finding judge `jev-1.13.0` calls 3, tokens in 4539, cost 0.0002 USD, config: defaults + host + repository (.werkrevisor.toml at base 7aee874).
Reviewed head `0a55535718b7a40c158df1b90ab270de4cb8489b` against base `7aee874ec7ceeb3e564d92d9fb68795b7188a084`, werkrevisor 0.17.0.
<!-- werkrevisor:0a55535718b7a40c158df1b90ab270de4cb8489b -->
<!-- werkrevisor-verdict:ORANGE -->
<!-- werkrevisor-folded -->
<details><summary>🟠 werkrevisor ORANGE for <code>0a55535</code>, superseded by <a href="http://git.javagil.de/mi/werkrevisor/pulls/21#issuecomment-1075">the result for <code>6c141ca</code></a></summary>
## 🟠 werkrevisor: ORANGE / FINDINGS
Change kind: `mixed`.
### Reasons
- **ORANGE** 2 finding(s) that Jev rates as probably real and severe: werkrevisor/cli.py:543-550, server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:53
- **YELLOW** risky paths changed: .werkrevisor.toml (built in)
- **YELLOW** PR too large for an AI-only approval: 1336 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself
- **YELLOW** 1 changed file was given to the AI only as diff, not in full, so it could not check it against the rest of its file (each larger than [review] max_full_file_chars = 40000): werkrevisor/cli.py
- **YELLOW** files the model cannot review: .gitignore, server/gradle/wrapper/gradle-wrapper.jar
- **hint** 1 finding(s) of low severity or rated minor by Jev: server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:26
- **hint** model recommends changes before merge
### Findings
1. **[high] Stats report can fail the review** at `werkrevisor/cli.py:543-550`, ORANGE, still open in [an earlier line comment](http://git.javagil.de/mi/werkrevisor/pulls/21#issuecomment-1055)
2. **[medium] Unlimited request body size and sloppy bearer parsing** at `server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:53`, ORANGE, still open in [an earlier line comment](http://git.javagil.de/mi/werkrevisor/pulls/21#issuecomment-1062)
3. **[medium] Dedup key allows double counting** at `server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:26`, hint, as a line comment
### Classifier
15 of 15 units passed all questions (1 PR document(s)).
1 answer ignored because of a confidence below [classifier] min_confidence.
<sub>
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 75647, calls 1, tokens in 75002 / out 886 (reasoning included in out), duration 16.6 s, cost 0.1589 USD (reported), classifier `jev-1.13.0` calls 15, tokens in 12809, cost 0.0005 USD, finding judge `jev-1.13.0` calls 3, tokens in 4539, cost 0.0002 USD, config: defaults + host + repository (.werkrevisor.toml at base 7aee874).<br>
Reviewed head `0a55535718b7a40c158df1b90ab270de4cb8489b` against base `7aee874ec7ceeb3e564d92d9fb68795b7188a084`, werkrevisor 0.17.0.
</sub>
</details>
werkrevisor: ORANGE / FINDINGS for 0a55535718b7a40c158df1b90ab270de4cb8489b.
The result comment of werkrevisor holds the details.
werkrevisor: ORANGE / FINDINGS for `0a55535718b7a40c158df1b90ab270de4cb8489b`.
The result comment of werkrevisor holds the details.
<!-- werkrevisor-review:0a55535718b7a40c158df1b90ab270de4cb8489b -->
The runKey includes the record timestamp, which is set per run by the client. A retried report does produce the same timestamp, but the docs say a second run on the same commit counts again. The key relies on exact string equality of Instant.toString(). Two equal instants with different fraction precision in the JSON input, such as ...:00Z and ...:00.000Z, parse to the same Instant, so this is fine. However, the uniqueness constraint is only on the key, and the since() query parses ts back with Instant.parse. This is acceptable, but a missing test for the retry with a changed head_sha is a gap.
**[medium] Dedup key allows double counting** (werkrevisor)
hint; Jev: real 0.35, severity 2.3 of 3
The `runKey` includes the record `timestamp`, which is set per run by the client. A retried report does produce the same timestamp, but the docs say a second run on the same commit counts again. The key relies on exact string equality of `Instant.toString()`. Two equal instants with different fraction precision in the JSON input, such as `...:00Z` and `...:00.000Z`, parse to the same Instant, so this is fine. However, the uniqueness constraint is only on the key, and the `since()` query parses `ts` back with `Instant.parse`. This is acceptable, but a missing test for the retry with a changed `head_sha` is a gap.
ai-review/assessment AI findings; a developer approval after checking them unlocks the merge; a human review request is pending; merge already scheduled
ORANGE 2 finding(s) that Jev rates as probably real and severe: werkrevisor/cli.py:544-547, server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:99-102
YELLOW PR too large for an AI-only approval: 1410 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself
YELLOW 1 changed file was given to the AI only as diff, not in full, so it could not check it against the rest of its file (each larger than [review] max_full_file_chars = 40000): werkrevisor/cli.py
YELLOW files the model cannot review: .gitignore, server/gradle/wrapper/gradle-wrapper.jar
YELLOW 1 finding(s) possibly severe, but Jev doubts that they are real: .werkrevisor.toml:28
hint model recommends changes before merge
Findings
[medium] Stats failure before the report can crash the run at werkrevisor/cli.py:544-547, ORANGE, still open in an earlier line comment
[medium] Non-finite or oversized numbers cause 500 instead of 400 at server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:99-102, ORANGE, as a line comment
[medium] Gradle wrapper jar not covered by hard_yellow at .werkrevisor.toml:28, YELLOW, still open in an earlier line comment
Classifier
15 of 15 units passed all questions (1 PR document(s)).
3 answers ignored because of a confidence below [classifier] min_confidence.
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 78530, calls 1, tokens in 78092 / out 788 (reasoning included in out), duration 16.1 s, cost 0.1641 USD (reported), classifier `jev-1.13.0` calls 15, tokens in 12846, cost 0.0005 USD, finding judge `jev-1.13.0` calls 3, tokens in 4087, cost 0.0002 USD, config: defaults + host + repository (.werkrevisor.toml at base 7aee874).
Reviewed head `6c141ca5eed6afc917957d1726c61b9e5a5c1a25` against base `7aee874ec7ceeb3e564d92d9fb68795b7188a084`, werkrevisor 0.17.0.
<!-- werkrevisor:6c141ca5eed6afc917957d1726c61b9e5a5c1a25 -->
<!-- werkrevisor-verdict:ORANGE -->
## 🟠 werkrevisor: ORANGE / FINDINGS
Change kind: `mixed`.
### Reasons
- **ORANGE** 2 finding(s) that Jev rates as probably real and severe: werkrevisor/cli.py:544-547, server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:99-102
- **YELLOW** risky paths changed: .werkrevisor.toml (built in)
- **YELLOW** PR too large for an AI-only approval: 1410 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself
- **YELLOW** 1 changed file was given to the AI only as diff, not in full, so it could not check it against the rest of its file (each larger than [review] max_full_file_chars = 40000): werkrevisor/cli.py
- **YELLOW** files the model cannot review: .gitignore, server/gradle/wrapper/gradle-wrapper.jar
- **YELLOW** 1 finding(s) possibly severe, but Jev doubts that they are real: .werkrevisor.toml:28
- **hint** model recommends changes before merge
### Findings
1. **[medium] Stats failure before the report can crash the run** at `werkrevisor/cli.py:544-547`, ORANGE, still open in [an earlier line comment](http://git.javagil.de/mi/werkrevisor/pulls/21#issuecomment-1055)
2. **[medium] Non-finite or oversized numbers cause 500 instead of 400** at `server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:99-102`, ORANGE, as a line comment
3. **[medium] Gradle wrapper jar not covered by hard_yellow** at `.werkrevisor.toml:28`, YELLOW, still open in [an earlier line comment](http://git.javagil.de/mi/werkrevisor/pulls/21#issuecomment-1058)
### Classifier
15 of 15 units passed all questions (1 PR document(s)).
3 answers ignored because of a confidence below [classifier] min_confidence.
<sub>
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 78530, calls 1, tokens in 78092 / out 788 (reasoning included in out), duration 16.1 s, cost 0.1641 USD (reported), classifier `jev-1.13.0` calls 15, tokens in 12846, cost 0.0005 USD, finding judge `jev-1.13.0` calls 3, tokens in 4087, cost 0.0002 USD, config: defaults + host + repository (.werkrevisor.toml at base 7aee874).<br>
Reviewed head `6c141ca5eed6afc917957d1726c61b9e5a5c1a25` against base `7aee874ec7ceeb3e564d92d9fb68795b7188a084`, werkrevisor 0.17.0.
</sub>
werkrevisor: ORANGE / FINDINGS for 6c141ca5eed6afc917957d1726c61b9e5a5c1a25.
The result comment of werkrevisor holds the details.
werkrevisor: ORANGE / FINDINGS for `6c141ca5eed6afc917957d1726c61b9e5a5c1a25`.
The result comment of werkrevisor holds the details.
<!-- werkrevisor-review:6c141ca5eed6afc917957d1726c61b9e5a5c1a25 -->
[medium] Non-finite or oversized numbers cause 500 instead of 400 (werkrevisor)
ORANGE; Jev: real 0.78, severity 3.0 of 3
pr is checked with canConvertToInt, but timestamp parsing catches only DateTimeParseException. store.add can also throw on malformed input, e.g. a repo or model value containing odd characters is fine, but a JSON body that is not an object (an array or string) makes body.get(name) return null and pr is reported as "missing" only. A non-JSON body yields Spring's default 400. The main gap: jevCostUsd is the sum of three values each up to 100 and costUsd is checked on its own, so total cost per record can reach 400 USD, contradicting the documented 0 to 100 USD bound.
**[medium] Non-finite or oversized numbers cause 500 instead of 400** (werkrevisor)
ORANGE; Jev: real 0.78, severity 3.0 of 3
`pr` is checked with `canConvertToInt`, but `timestamp` parsing catches only `DateTimeParseException`. `store.add` can also throw on malformed input, e.g. a `repo` or `model` value containing odd characters is fine, but a JSON body that is not an object (an array or string) makes `body.get(name)` return null and `pr` is reported as "missing" only. A non-JSON body yields Spring's default 400. The main gap: `jevCostUsd` is the sum of three values each up to 100 and `costUsd` is checked on its own, so total cost per record can reach 400 USD, contradicting the documented 0 to 100 USD bound.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
See docs/prs/2026-10-06-PR#21-stats-service-and-dashboard.md.
🤖 Generated with Claude Code
🟡 werkrevisor YELLOW for
15bf85a, superseded by the result for2322b76🟡 werkrevisor: YELLOW / HUMAN_REVIEW
Change kind:
mixed.Reasons
- YELLOW risky paths changed: .werkrevisor.toml (built in)
- YELLOW possible secrets in added lines: server/src/test/kotlin/de/hoennig/werkrevisor/server/UsageApiIntegrationTest.kt:154 (generic_secret)
- YELLOW PR too large for an AI-only approval: 1280 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM skipped (possible secrets in the diff), duration 0.3 s, no model call, cost 0 USD, config: defaults + host + repository (.werkrevisor.toml at base7aee874).Reviewed head `15bf85ace864733a70786416b896843d984285b8` against base `7aee874ec7ceeb3e564d92d9fb68795b7188a084`, werkrevisor 0.17.0.
🟠 werkrevisor ORANGE for
2322b76, superseded by the result for53584ec🟠 werkrevisor: ORANGE / FINDINGS
Change kind:
mixed.Reasons
Findings
[medium] Untrusted record fields are unvalidated and stored at
server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:45-70, ORANGE, as a line comment[medium] Stats failure message is not redacted before printing at
werkrevisor/cli.py:543-550, YELLOW, as a line comment[medium] Dedup key uses the timestamp, so retries with a new timestamp double count at
server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageStore.kt:26, YELLOW, as a line comment1 more finding(s) not shown.
Classifier
15 of 15 units passed all questions (1 PR document(s)).
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 72638, calls 1, tokens in 72139 / out 1183 (reasoning included in out), duration 18.6 s, cost 0.1561 USD (reported), classifier `jev-1.13.0` calls 15, tokens in 12675, cost 0.0005 USD, finding judge `jev-1.13.0` calls 3, tokens in 4348, cost 0.0002 USD, config: defaults + host + repository (.werkrevisor.toml at base3 answers ignored because of a confidence below [classifier] min_confidence.
7aee874).Reviewed head `2322b763c030af942b91d366009dc3112bc3e628` against base `7aee874ec7ceeb3e564d92d9fb68795b7188a084`, werkrevisor 0.17.0.
werkrevisor: ORANGE / FINDINGS for
2322b763c030af942b91d366009dc3112bc3e628.The result comment of werkrevisor holds the details.
@@ -26,3 +26,3 @@# These modules hold the trust boundary: tokens, redaction, the HOME guard and the Gitea writes."werkrevisor/isolation.py", "werkrevisor/redact.py", "werkrevisor/gitea.py", "werkrevisor/config.py","pyproject.toml", "requirements*.txt", "**/*secret*", "**/*.pem", "**/*.key", ".env*","pyproject.toml", "requirements*.txt", "server/*.gradle.kts", "server/gradle/**", "server/gradlew*", "**/*secret*", "**/*.pem", "**/*.key", ".env*",[low] hard_yellow glob for the server build does not cover subdirectories (werkrevisor)
hint
server/*.gradle.ktsmatches only the top-level build files. If the glob semantics are fnmatch-like, this is fine forserver/build.gradle.ktsandserver/settings.gradle.kts.server/gradle/wrapper/gradle-wrapper.jaris covered byserver/gradle/**. The wrapper jar itself is not shown in the diff, so it is unclear whether it is committed. If it is, it is a binary in the trust boundary and should be verified.@@ -0,0 +42,4 @@private fun bearer(authorization: String?) = authorization?.removePrefix("Bearer ")?.trim()private fun toRecord(body: JsonNode): UsageRecord {[medium] Untrusted record fields are unvalidated and stored (werkrevisor)
ORANGE; Jev: real 0.90, severity 3.0 of 3
toRecordaccepts anyrepo,pr_urlandverdictstrings from any source token holder and stores them. There is no length limit and no URL scheme check onpr_url. The dashboard filters the link scheme client-side, but other embedders ofGET /api/statsget raw values. AlsoasDouble()on a non-numericcost_usdyields 0.0, so a malformed record is silently stored as free. A negative cost is also accepted and lowers the sums. Validate scheme, length and cost range server-side.@@ -0,0 +23,4 @@val totalCostUsd get() = (costUsd ?: 0.0) + jevCostUsdval runKey get() = "$repo#$pr@${headSha ?: ""}@$timestamp"[medium] Dedup key uses the timestamp, so retries with a new timestamp double count (werkrevisor)
YELLOW; Jev: real 0.56, severity 2.8 of 3
runKeyincludestimestamp. Scenario 21.04 says a retried report counts once, which holds only for the identical record. A rerun of the same head commit, for example a manual re-trigger, produces a different timestamp and is counted again. That is arguably correct because each run costs money, but the PR document's wording suggests deduplication by run. Clarify the intent, because it determines whether costs are over- or under-counted.@@ -539,3 +541,3 @@print(report.render_comment(result, pr, pr.base_sha, config.review.max_findings, redactor),file=deps.out, flush=True)report.write_usage(report.usage_record(result, pr, _now()), redactor, deps.out, deps.usage_path)record = report.usage_record(result, pr, _now())[medium] Stats failure message is not redacted before printing (werkrevisor)
YELLOW; Jev: real 0.47, severity 2.6 of 3
The stats client builds its error message from an exception, and
redactor(...)is applied to the whole line, which is fine. But theStatsErrorraised inmainfor a missing token or an invalid URL is printed withoutredactor.WERKREVISOR_STATS_URLcan contain userinfo or a secret query string, but the message only contains fixed text, so there is no leak today. The real risk is thatexecute()now runs the stats report after the usage record is written, with no timeout guard beyond the 5 s client timeout. A slow or unreachable service adds up to 5 s to every run, and a DNS stall can exceed it because urllib's timeout does not cover name resolution. Consider whether this latency is acceptable for the merge gate.🟡 werkrevisor YELLOW for
53584ec, superseded by the result forcc63405🟡 werkrevisor: YELLOW / HUMAN_REVIEW
Change kind:
mixed.Reasons
Findings
[high] Usage record written before stats report, but exit code can still change at
werkrevisor/cli.py:543-550, hint, still open in an earlier line comment[medium] Unbounded request body and non-finite numbers at
server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:54-97, hint, as a line comment[medium] Reads all records of 12 months on every stats request at
server/src/main/kotlin/de/hoennig/werkrevisor/server/Statistics.kt:77-123, hint, as a line comment1 more finding(s) not shown.
Classifier
14 of 15 units passed all questions (1 PR document(s)).
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 75095, calls 1, tokens in 74449 / out 987 (reasoning included in out), duration 16.0 s, cost 0.1588 USD (reported), classifier `jev-1.13.0` calls 15, tokens in 12839, cost 0.0005 USD, finding judge `jev-1.13.0` calls 3, tokens in 5434, cost 0.0002 USD, config: defaults + host + repository (.werkrevisor.toml at base1 answer ignored because of a confidence below [classifier] min_confidence.
7aee874).Reviewed head `53584ecdb19ac15f194f8d70a2a1000b6885e4e9` against base `7aee874ec7ceeb3e564d92d9fb68795b7188a084`, werkrevisor 0.17.0.
werkrevisor: YELLOW / HUMAN_REVIEW for
53584ecdb19ac15f194f8d70a2a1000b6885e4e9.The result comment of werkrevisor holds the details.
@@ -30,3 +30,3 @@secret_scan = true# Test data holds fake passwords; private keys and the real key formats stay flagged there too.secret_scan_exclude = ["tests/**"]secret_scan_exclude = ["tests/**", "server/src/test/**"][low] Test secret scan exclusion widened for a new directory (werkrevisor)
hint
secret_scan_excludenow also coversserver/src/test/**. This is consistent withtests/**, but real secrets in Kotlin tests would no longer be flagged. The change is intentional, so only a reviewer note.@@ -0,0 +74,4 @@private val store: UsageStore,private val clock: Clock,) {fun statistics(withDetails: Boolean): Statistics {[medium] Reads all records of 12 months on every stats request (werkrevisor)
hint; Jev: real 0.67, severity 2.3 of 3
statistics()loads every record since the start of the month 11 months back into memory and filters it several times, on an unauthenticated endpoint. A client that polls/api/statsrepeatedly costs one full table scan per request. The volume of a few thousand records per year makes this tolerable today, but the endpoint has no caching or rate limit.@@ -0,0 +51,4 @@private fun bearer(authorization: String?) = authorization?.removePrefix("Bearer ")?.trim()// Every embedder of /api/stats shows these values, so the service accepts only what a real run can report.private fun toRecord(body: JsonNode): UsageRecord {[medium] Unbounded request body and non-finite numbers (werkrevisor)
hint; Jev: real 0.57, severity 2.5 of 3
POST /api/usageaccepts aJsonNodeof any size from any holder of a source token, and storesrun_keyfromrepo,pr,head_shaandtimestamp.head_shais limited to 200 characters, butcost_statusandmodelare stored without a value check.cost_statusvalues other than the known ones (reported,unknown,none) are stored as is and makecostKnownfalse for acostUsdof null only. This is a limited defect.🟠 werkrevisor ORANGE for
cc63405, superseded by the result for0a55535🟠 werkrevisor: ORANGE / FINDINGS
Change kind:
mixed.Reasons
Findings
[medium] Dedup key allows duplicates when
head_shais absent atserver/src/main/kotlin/de/hoennig/werkrevisor/server/UsageStore.kt:26, ORANGE, still open in an earlier line comment[medium] Stats report failure can crash the run at
werkrevisor/cli.py:543-550, hint, still open in an earlier line comment[medium] NaN or infinite cost handling is untested at
server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:93, hint, as a line commentClassifier
15 of 15 units passed all questions (1 PR document(s)).
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 75079, calls 1, tokens in 74438 / out 878 (reasoning included in out), duration 16.0 s, cost 0.1577 USD (reported), classifier `jev-1.13.0` calls 15, tokens in 12800, cost 0.0005 USD, finding judge `jev-1.13.0` calls 3, tokens in 3976, cost 0.0002 USD, config: defaults + host + repository (.werkrevisor.toml at base2 answers ignored because of a confidence below [classifier] min_confidence.
7aee874).Reviewed head `cc63405f7cb7a24e0356b15de72dc193b1265ed0` against base `7aee874ec7ceeb3e564d92d9fb68795b7188a084`, werkrevisor 0.17.0.
werkrevisor: ORANGE / FINDINGS for
cc63405f7cb7a24e0356b15de72dc193b1265ed0.The result comment of werkrevisor holds the details.
@@ -0,0 +90,4 @@verdict = verdict,model = text("model"),costUsd = number("cost_usd"),costStatus = text("cost_status"),[medium] NaN or infinite cost handling is untested (werkrevisor)
hint; Jev: real 0.74, severity 2.3 of 3
number()checksit in 0.0..MAX_COST_USD, which rejects NaN. ThecostStatustext is not validated against a known set. A source with a valid token can send arbitrarycost_statusstrings, andcostKnowntreats anything other than "unknown" as known whencostUsdis present. This is minor, but unrecognized values silently count toward cost sums.🟠 werkrevisor ORANGE for
0a55535, superseded by the result for6c141ca🟠 werkrevisor: ORANGE / FINDINGS
Change kind:
mixed.Reasons
Findings
[high] Stats report can fail the review at
werkrevisor/cli.py:543-550, ORANGE, still open in an earlier line comment[medium] Unlimited request body size and sloppy bearer parsing at
server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:53, ORANGE, still open in an earlier line comment[medium] Dedup key allows double counting at
server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:26, hint, as a line commentClassifier
15 of 15 units passed all questions (1 PR document(s)).
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 75647, calls 1, tokens in 75002 / out 886 (reasoning included in out), duration 16.6 s, cost 0.1589 USD (reported), classifier `jev-1.13.0` calls 15, tokens in 12809, cost 0.0005 USD, finding judge `jev-1.13.0` calls 3, tokens in 4539, cost 0.0002 USD, config: defaults + host + repository (.werkrevisor.toml at base1 answer ignored because of a confidence below [classifier] min_confidence.
7aee874).Reviewed head `0a55535718b7a40c158df1b90ab270de4cb8489b` against base `7aee874ec7ceeb3e564d92d9fb68795b7188a084`, werkrevisor 0.17.0.
werkrevisor: ORANGE / FINDINGS for
0a55535718b7a40c158df1b90ab270de4cb8489b.The result comment of werkrevisor holds the details.
@@ -0,0 +23,4 @@@RestControllerclass UsageApiController(private val properties: ServerProperties,[medium] Dedup key allows double counting (werkrevisor)
hint; Jev: real 0.35, severity 2.3 of 3
The
runKeyincludes the recordtimestamp, which is set per run by the client. A retried report does produce the same timestamp, but the docs say a second run on the same commit counts again. The key relies on exact string equality ofInstant.toString(). Two equal instants with different fraction precision in the JSON input, such as...:00Zand...:00.000Z, parse to the same Instant, so this is fine. However, the uniqueness constraint is only on the key, and thesince()query parsestsback withInstant.parse. This is acceptable, but a missing test for the retry with a changedhead_shais a gap.🟠 werkrevisor: ORANGE / FINDINGS
Change kind:
mixed.Reasons
Findings
[medium] Stats failure before the report can crash the run at
werkrevisor/cli.py:544-547, ORANGE, still open in an earlier line comment[medium] Non-finite or oversized numbers cause 500 instead of 400 at
server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:99-102, ORANGE, as a line comment[medium] Gradle wrapper jar not covered by hard_yellow at
.werkrevisor.toml:28, YELLOW, still open in an earlier line commentClassifier
15 of 15 units passed all questions (1 PR document(s)).
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 78530, calls 1, tokens in 78092 / out 788 (reasoning included in out), duration 16.1 s, cost 0.1641 USD (reported), classifier `jev-1.13.0` calls 15, tokens in 12846, cost 0.0005 USD, finding judge `jev-1.13.0` calls 3, tokens in 4087, cost 0.0002 USD, config: defaults + host + repository (.werkrevisor.toml at base3 answers ignored because of a confidence below [classifier] min_confidence.
7aee874).Reviewed head `6c141ca5eed6afc917957d1726c61b9e5a5c1a25` against base `7aee874ec7ceeb3e564d92d9fb68795b7188a084`, werkrevisor 0.17.0.
werkrevisor: ORANGE / FINDINGS for
6c141ca5eed6afc917957d1726c61b9e5a5c1a25.The result comment of werkrevisor holds the details.
@@ -0,0 +96,4 @@headSha = headSha,verdict = verdict,model = text("model"),costUsd = number("cost_usd"),[medium] Non-finite or oversized numbers cause 500 instead of 400 (werkrevisor)
ORANGE; Jev: real 0.78, severity 3.0 of 3
pris checked withcanConvertToInt, buttimestampparsing catches onlyDateTimeParseException.store.addcan also throw on malformed input, e.g. arepoormodelvalue containing odd characters is fine, but a JSON body that is not an object (an array or string) makesbody.get(name)return null andpris reported as "missing" only. A non-JSON body yields Spring's default 400. The main gap:jevCostUsdis the sum of three values each up to 100 andcostUsdis checked on its own, so total cost per record can reach 400 USD, contradicting the documented 0 to 100 USD bound.edgecases later