diff --git a/evals/cassettes/ts-0045-kernel-bypass-hallucination/0001-5d5f08dc06f14946.json b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0001-5d5f08dc06f14946.json index 3e52b2f..ccdf854 100644 --- a/evals/cassettes/ts-0045-kernel-bypass-hallucination/0001-5d5f08dc06f14946.json +++ b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0001-5d5f08dc06f14946.json @@ -4,13 +4,13 @@ "model_answered": "deepseek/deepseek-v4-pro-0813", "schema_name": "tinysweeper_rule_extraction", "usage": { - "input_tokens": 1632, - "output_tokens": 3660, + "input_tokens": 3106, + "output_tokens": 5561, "cached_tokens": 1536, "embed_tokens": 0, - "cost_usd": 0.003231528 + "cost_usd": 0.003111033 }, "value": { - "rules_markdown": "- Organize `src/` as single crate: one responsibility per module, core types in module-local `types.rs`, one trait per file in `src/ports/`.\n- Declare every CLI subcommand in `src/bin/tinysweeper.rs` even if milestone not landed; `src/server/` is the only production surface.\n- Store review policy as data in `presets/`: each preset is a folder with `preset.toml`, `README.md`, optional prompt overrides; never add a module for a preset.\n- Treat `vendor/tinyagents` as a git submodule: never edit it here; change upstream and bump the pin.\n- Keep one document per `src/` module at `docs/modules//README.md`.\n- Declare examples in `Cargo.toml` with `required-features` so credential-needing smoke tests never build in CI.\n- Default build is offline; link no HTTP client, test suite never touches network; network features must be gated (`harness`, `github`, `serve`).\n- Follow rustfmt output and Rust 2024 idioms; use `snake_case` for modules/files, `PascalCase` for types.\n- Return `Result` using crate error type from `src/error.rs`.\n- Start every file with a `//!` module doc describing role and any feature gating.\n- Write comments that explain decisions, not code; call out load-bearing flags/ordering and why.\n- Add doc comments to public items; use `///` on Clap fields so it becomes help text.\n- Keep tests in-crate: `#[cfg(test)] mod tests` at bottom, move to sibling `test.rs` or `_test.rs` when large; no `tests/` directory.\n- Provide always-compiled offline mock for every port; `MockForge` records intended writes for exact assertions.\n- Cover lane behaviour with golden tests: fixture diff + canned structured model response, asserting findings after filtering, dedupe, capping.\n- Maintain at least 80% coverage for meaningful library behaviour.\n- Keep every Markdown file, including guidelines, at 500 lines or fewer; split and link from module README.\n- Security: model never holds write token; write credentials minted only in `src/apply/` after all model calls return; leaving a change fails.\n- Security: never execute contributor code; read diff/tree only, no build, install, or running target repo scripts.\n- Security: treat PR bodies/comments/diffs as untrusted input, fence and label as data; model verdict advisory; only `src/apply/` and `src/automerge/` may mutate GitHub.\n- Security: report secrets by type and location only; never include value in comments, check-run summaries, or logs.\n- Use concise imperative commit subjects; keep commits small, one coherent validated slice each.\n- Pull requests state what changed, any behaviour change, and how it was verified." + "rules_markdown": "- src/ is the single crate; one responsibility per module, core types in module-local types.rs, and every port in src/ports/ is one trait in one file.\n- src/bin/tinysweeper.rs is the CLI; every subcommand is declared even if its milestone has not landed.\n- src/server/ is the only production surface; there is no GitHub Actions distribution path.\n- presets/ are review policy as data, not code; adding a preset is a new folder, never a new module.\n- vendor/tinyagents is a git submodule; never edit it here, change upstream and bump the pin.\n- docs/modules//README.md is one document per src/ module.\n- examples/ are declared explicitly in Cargo.toml with required-features so credential-needing smoke tests never build in CI.\n- The default build is offline; anything that needs the network goes behind a feature (harness, github, serve).\n- Follow rustfmt output and Rust 2024 idioms; use snake_case for modules/files and PascalCase for types.\n- Return Result using the crate error type from src/error.rs.\n- Every file opens with a //! module doc describing its role and any feature gating.\n- Comments explain the decision, not the code; say why when a flag or ordering is load-bearing.\n- Public items carry doc comments; Clap fields use /// which becomes the help text.\n- Tests live in-crate: a #[cfg(test)] mod tests block at module bottom, moving to sibling test.rs or _test.rs when they grow; no tests/ directory.\n- Every port has an always-compiled offline mock; MockForge records what would have been written so tests can assert on exact check runs and comments.\n- Lane behaviour is covered by golden tests: fixture diff plus a canned structured model response, asserting findings that survive filtering, dedupe, and capping.\n- Maintain at least 80% coverage for meaningful library behaviour.\n- Keep every Markdown file at 500 lines or fewer; split into focused files and link from module README when over.\n- The model never holds a write token; lanes run against a read-only checkout; write credentials minted only in src/apply/ after all model calls returned; a lane leaving a change in checkout fails.\n- Contributor code is never executed; read the diff and tree, do not build, install dependencies, or run target repository's scripts.\n- Pull request bodies, comments, and diffs are untrusted input; fence and label them as data in prompts; model verdict advisory; only deterministic policy in src/apply/ and src/automerge/ may mutate GitHub.\n- Secrets found by scanners are reported by type and location only; the value never reaches a comment, check-run summary, or log.\n- Use concise, imperative commit subjects; keep commits small; commit each coherent, validated slice on its own.\n- Pull requests state what changed, any behaviour change, and how it was verified." } } \ No newline at end of file diff --git a/evals/cassettes/ts-0045-kernel-bypass-hallucination/0002-17541401c04d839b.json b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0002-17541401c04d839b.json new file mode 100644 index 0000000..c94ed20 --- /dev/null +++ b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0002-17541401c04d839b.json @@ -0,0 +1,17 @@ +{ + "key": "17541401c04d839b", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_critique", + "usage": { + "input_tokens": 2845, + "output_tokens": 552, + "cached_tokens": 0, + "embed_tokens": 0, + "cost_usd": 0.001717815 + }, + "value": { + "findings": [], + "summary": "The change adds a new public module `summarise` to `src/findings/mod.rs`. The declaration follows the repository's snake_case naming convention and does not introduce any apparent issues." + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0045-kernel-bypass-hallucination/0003-1c93c25d9aadd0a7.json b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0003-1c93c25d9aadd0a7.json new file mode 100644 index 0000000..4c0e13b --- /dev/null +++ b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0003-1c93c25d9aadd0a7.json @@ -0,0 +1,29 @@ +{ + "key": "1c93c25d9aadd0a7", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_critique", + "usage": { + "input_tokens": 2964, + "output_tokens": 2819, + "cached_tokens": 384, + "embed_tokens": 0, + "cost_usd": 0.003576222 + }, + "value": { + "findings": [ + { + "body": "The diff turns on embeddings retrieval via an external provider (`provider = \"openrouter\"`) without any feature gating. The repository rule says the default build is offline and anything that needs the network goes behind a feature (harness, github, serve). Enabling this in `.tinysweeper.toml` makes network calls part of the default configuration even when the relevant feature is off, breaking offline use. Either set `enabled = false` here or ensure the code only reads this section when a network feature is active.", + "confidence": 0.5, + "existing_code": "enabled = true", + "late": false, + "path": ".tinysweeper.toml", + "rule": "network-off-by-default", + "severity": "medium", + "suggestion": null, + "title": "Disable embeddings retrieval by default or gate it behind a feature" + } + ], + "summary": "The change adds embeddings configuration; as written it enables network retrieval by default, which appears to violate the offline-default policy. I'd resolve that before merging." + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0045-kernel-bypass-hallucination/0004-3df0bb3a4c9249d9.json b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0004-3df0bb3a4c9249d9.json new file mode 100644 index 0000000..6afc0a3 --- /dev/null +++ b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0004-3df0bb3a4c9249d9.json @@ -0,0 +1,16 @@ +{ + "key": "3df0bb3a4c9249d9", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_falsify", + "usage": { + "input_tokens": 1013, + "output_tokens": 176, + "cached_tokens": 0, + "embed_tokens": 0, + "cost_usd": 0.000593775 + }, + "value": { + "incorrect": [] + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0045-kernel-bypass-hallucination/0005-183c4d7693b78647.json b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0005-183c4d7693b78647.json new file mode 100644 index 0000000..06effef --- /dev/null +++ b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0005-183c4d7693b78647.json @@ -0,0 +1,48 @@ +{ + "key": "183c4d7693b78647", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_critique", + "usage": { + "input_tokens": 3187, + "output_tokens": 4694, + "cached_tokens": 0, + "embed_tokens": 0, + "cost_usd": 0.005470125 + }, + "value": { + "findings": [ + { + "body": "The MongoDB community-server image (especially the UBI9 variant) typically does not include Python 3. Overriding the entrypoint to invoke `python3` will cause the container to fail to start with 'exec: python3: not found' if the interpreter is absent. Use a shell-based entrypoint or install Python before running the script.", + "confidence": 0.8, + "existing_code": " [\"python3\", \"/usr/local/bin/kernel-bypass-entrypoint.py\", \"mongod\"]", + "path": "docker-compose.kernel-bypass.yml", + "rule": "missing-runtime-dependency", + "severity": "high", + "suggestion": null, + "title": "MongoDB container image may lack python3" + }, + { + "body": "The mongo-secrets service inherits the original image entrypoint, which the file's comments say hard-refuses to start `mongod` on affected kernels. Setting `GLIBC_TUNABLES` alone may not bypass that check because the check is separate from the glibc tunable. If the base service uses the same entrypoint, it will fail to start. Override its entrypoint as well or confirm it does not run the kernel check.", + "confidence": 0.7, + "existing_code": " mongo-secrets:\n environment:\n GLIBC_TUNABLES: glibc.pthread.rseq=0", + "path": "docker-compose.kernel-bypass.yml", + "rule": "inconsistent-kernel-bypass", + "severity": "high", + "suggestion": null, + "title": "mongo-secrets still uses the refusing entrypoint" + }, + { + "body": "The MongoDB image's original entrypoint performs important setup (e.g., file permissions, keyfile generation, user/group setup) before executing `mongod`. Replacing it entirely with a custom Python script risks skipping those steps, leading to startup failures or security misconfigurations. Verify that the script replicates necessary initialization or use a wrapper that calls the original entrypoint with a flag to bypass only the kernel check.", + "confidence": 0.3, + "existing_code": " mongod:\n entrypoint:\n [\"python3\", \"/usr/local/bin/kernel-bypass-entrypoint.py\", \"mongod\"]", + "path": "docker-compose.kernel-bypass.yml", + "rule": "entrypoint-replacement", + "severity": "medium", + "suggestion": null, + "title": "Overriding entrypoint drops original setup steps" + } + ], + "summary": "The overlay aims to bypass MongoDB's kernel restriction, but it relies on Python being present in the image and leaves `mongo-secrets` potentially failing, while discarding the original entrypoint's setup logic. These issues need to be addressed before merging." + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0045-kernel-bypass-hallucination/0006-86efd049e0b944eb.json b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0006-86efd049e0b944eb.json new file mode 100644 index 0000000..678f4f7 --- /dev/null +++ b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0006-86efd049e0b944eb.json @@ -0,0 +1,28 @@ +{ + "key": "86efd049e0b944eb", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_critique", + "usage": { + "input_tokens": 3663, + "output_tokens": 5259, + "cached_tokens": 0, + "embed_tokens": 0, + "cost_usd": 0.006168735 + }, + "value": { + "findings": [ + { + "body": "When `rfind(' ')` returns `None`, the code appends an ellipsis to the full `limit`-character `head`, producing a result of `limit + 1` characters. This violates the documented contract (\"at most `limit` characters\") and may exceed downstream limits (e.g., GitHub check-run titles). The fix removes one character from `head` before appending the ellipsis, except when `limit` is 0, where an empty string should be returned.", + "confidence": 0.95, + "existing_code": " let head: String = text.chars().take(limit).collect();\n match head.rfind(' ') {\n Some(space) => format!(\"{}…\", &head[..space]),\n None => format!(\"{head}…\"),\n }", + "path": "src/findings/summarise.rs", + "rule": "off-by-one", + "severity": "medium", + "suggestion": " let head: String = text.chars().take(limit).collect();\n match head.rfind(' ') {\n Some(space) => format!(\"{}…\", &head[..space]),\n None => {\n let mut head = head;\n if head.is_empty() {\n String::new()\n } else {\n head.pop();\n format!(\"{head}…\")\n }\n }\n }", + "title": "Respect the character limit when no space is found" + } + ], + "summary": "The change adds `shorten` and `first_sentence` utilities with tests, but `shorten` can return a string one character longer than the requested limit when the truncated prefix contains no space." + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0045-kernel-bypass-hallucination/0007-abbb7a6b9db04578.json b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0007-abbb7a6b9db04578.json new file mode 100644 index 0000000..2d0176d --- /dev/null +++ b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0007-abbb7a6b9db04578.json @@ -0,0 +1,17 @@ +{ + "key": "abbb7a6b9db04578", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_critique", + "usage": { + "input_tokens": 3420, + "output_tokens": 5319, + "cached_tokens": 0, + "embed_tokens": 0, + "cost_usd": 0.00611523 + }, + "value": { + "findings": [], + "summary": "The change renames GitHub App environment variables to match what the binary reads, adds API key forwarding, and mounts a read-only config file. No issues found." + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0045-kernel-bypass-hallucination/0008-87d1384e18c9d053.json b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0008-87d1384e18c9d053.json new file mode 100644 index 0000000..5159d2f --- /dev/null +++ b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0008-87d1384e18c9d053.json @@ -0,0 +1,16 @@ +{ + "key": "87d1384e18c9d053", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_falsify", + "usage": { + "input_tokens": 1417, + "output_tokens": 456, + "cached_tokens": 384, + "embed_tokens": 0, + "cost_usd": 0.000847467 + }, + "value": { + "incorrect": [] + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0045-kernel-bypass-hallucination/0009-2d53b24abad09d41.json b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0009-2d53b24abad09d41.json new file mode 100644 index 0000000..5998a37 --- /dev/null +++ b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0009-2d53b24abad09d41.json @@ -0,0 +1,16 @@ +{ + "key": "2d53b24abad09d41", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_falsify", + "usage": { + "input_tokens": 1703, + "output_tokens": 1110, + "cached_tokens": 640, + "embed_tokens": 0, + "cost_usd": 0.001430425 + }, + "value": { + "incorrect": [] + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0045-kernel-bypass-hallucination/0010-d6bf343291f4be03.json b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0010-d6bf343291f4be03.json new file mode 100644 index 0000000..5dda22f --- /dev/null +++ b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0010-d6bf343291f4be03.json @@ -0,0 +1,28 @@ +{ + "key": "d6bf343291f4be03", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_critique", + "usage": { + "input_tokens": 3664, + "output_tokens": 6495, + "cached_tokens": 384, + "embed_tokens": 0, + "cost_usd": 0.007078842 + }, + "value": { + "findings": [ + { + "body": "The file handle returned by `open()` is never closed. While the process will eventually close it on exit, the handle remains open during the entire execution of the patched entrypoint, which may run indefinitely if the entrypoint starts a long-lived process. Use a context manager to ensure it is closed immediately after reading.", + "confidence": 0.8, + "existing_code": " source = open(ENTRYPOINT, encoding=\"utf-8\").read()", + "path": "deploy/mongo/kernel-bypass-entrypoint.py", + "rule": "resource-leak", + "severity": "low", + "suggestion": " with open(ENTRYPOINT, encoding=\"utf-8\") as f:\n source = f.read()", + "title": "Close the entrypoint file after reading" + } + ], + "summary": "The change adds a shim to bypass the MongoDB kernel compatibility check; it is generally sound, with one minor resource leak." + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0045-kernel-bypass-hallucination/0011-7c0e6d892204ca20.json b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0011-7c0e6d892204ca20.json new file mode 100644 index 0000000..bad8361 --- /dev/null +++ b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0011-7c0e6d892204ca20.json @@ -0,0 +1,16 @@ +{ + "key": "7c0e6d892204ca20", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_falsify", + "usage": { + "input_tokens": 1663, + "output_tokens": 299, + "cached_tokens": 640, + "embed_tokens": 0, + "cost_usd": 0.000707455 + }, + "value": { + "incorrect": [] + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0045-kernel-bypass-hallucination/0012-b5380fa68e6677d3.json b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0012-b5380fa68e6677d3.json new file mode 100644 index 0000000..394ef5e --- /dev/null +++ b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0012-b5380fa68e6677d3.json @@ -0,0 +1,17 @@ +{ + "key": "b5380fa68e6677d3", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_security", + "usage": { + "input_tokens": 3642, + "output_tokens": 506, + "cached_tokens": 0, + "embed_tokens": 0, + "cost_usd": 0.00202449 + }, + "value": { + "findings": [], + "summary": "The new string truncation and sentence extraction helpers are pure and safe; no security issues introduced." + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0045-kernel-bypass-hallucination/0013-3d5e8e65650775c9.json b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0013-3d5e8e65650775c9.json new file mode 100644 index 0000000..6188fd2 --- /dev/null +++ b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0013-3d5e8e65650775c9.json @@ -0,0 +1,17 @@ +{ + "key": "3d5e8e65650775c9", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_security", + "usage": { + "input_tokens": 2824, + "output_tokens": 508, + "cached_tokens": 0, + "embed_tokens": 0, + "cost_usd": 0.0016704 + }, + "value": { + "findings": [], + "summary": "The change adds a new submodule `summarise` to the findings module. No security-sensitive code is introduced in this file; the change is safe to merge." + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0045-kernel-bypass-hallucination/0014-fb11783005e5e9fd.json b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0014-fb11783005e5e9fd.json new file mode 100644 index 0000000..f0c87fe --- /dev/null +++ b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0014-fb11783005e5e9fd.json @@ -0,0 +1,17 @@ +{ + "key": "fb11783005e5e9fd", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_security", + "usage": { + "input_tokens": 3399, + "output_tokens": 648, + "cached_tokens": 0, + "embed_tokens": 0, + "cost_usd": 0.002042325 + }, + "value": { + "findings": [], + "summary": "The change updates the compose environment to require the GitHub App id/private key and to forward only the relevant API keys, and mounts a read-only config file; no security issues found." + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0045-kernel-bypass-hallucination/0015-3651492c72f6983f.json b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0015-3651492c72f6983f.json new file mode 100644 index 0000000..633ee78 --- /dev/null +++ b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0015-3651492c72f6983f.json @@ -0,0 +1,17 @@ +{ + "key": "3651492c72f6983f", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_security", + "usage": { + "input_tokens": 3166, + "output_tokens": 744, + "cached_tokens": 0, + "embed_tokens": 0, + "cost_usd": 0.00202449 + }, + "value": { + "findings": [], + "summary": "The overlay adds a workaround for running MongoDB on newer Linux kernels by setting GLIBC_TUNABLES and mounting a custom entrypoint script. The script itself is not part of this diff and cannot be reviewed here. The change is explicitly scoped to local development and introduces no new security issues that are visible from the diff." + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0045-kernel-bypass-hallucination/0016-96d6b84e54ee3b43.json b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0016-96d6b84e54ee3b43.json new file mode 100644 index 0000000..5fbf945 --- /dev/null +++ b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0016-96d6b84e54ee3b43.json @@ -0,0 +1,17 @@ +{ + "key": "96d6b84e54ee3b43", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_security", + "usage": { + "input_tokens": 2943, + "output_tokens": 1048, + "cached_tokens": 0, + "embed_tokens": 0, + "cost_usd": 0.002191965 + }, + "value": { + "findings": [], + "summary": "The change adds configuration for embeddings retrieval using an API key from the environment; no security issues introduced." + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0045-kernel-bypass-hallucination/0017-1f308da0fcce4684.json b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0017-1f308da0fcce4684.json new file mode 100644 index 0000000..7ee8c52 --- /dev/null +++ b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0017-1f308da0fcce4684.json @@ -0,0 +1,17 @@ +{ + "key": "1f308da0fcce4684", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_security", + "usage": { + "input_tokens": 3643, + "output_tokens": 2026, + "cached_tokens": 0, + "embed_tokens": 0, + "cost_usd": 0.003347325 + }, + "value": { + "findings": [], + "summary": "The change adds an opt-in compose overlay script that patches and runs the MongoDB image's entrypoint to disable a non-security kernel compatibility check. It reads from a fixed path and executes no untrusted input; no security issues introduced." + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0045-kernel-bypass-hallucination/0018-3e0fe1f1ab7eef77.json b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0018-3e0fe1f1ab7eef77.json new file mode 100644 index 0000000..9d29987 --- /dev/null +++ b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0018-3e0fe1f1ab7eef77.json @@ -0,0 +1,37 @@ +{ + "key": "3e0fe1f1ab7eef77", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_tests", + "usage": { + "input_tokens": 5813, + "output_tokens": 8769, + "cached_tokens": 0, + "embed_tokens": 0, + "cost_usd": 0.010157685 + }, + "value": { + "findings": [ + { + "body": "This test only checks that the output ends with `…` and is not longer than `limit + 1`. An implementation that truncates mid-word (for example `text.chars().take(limit).collect()` plus an ellipsis) would still pass both assertions. Assert the exact expected result, or at least that the character before the ellipsis is a space, so the word-boundary requirement is actually tested.", + "confidence": 0.9, + "existing_code": " assert!(out.ends_with('…'), \"{out}\");\n assert!(out.len() <= 21, \"{out}\");", + "path": "src/findings/summarise.rs", + "rule": "weak-test-assertion", + "severity": "low", + "suggestion": " assert_eq!(out, \"Guard the index…\");\n assert_eq!(out.chars().count(), 16);", + "title": "Assert the actual word-boundary result, not just an ellipsis" + }, + { + "body": "The documented contract is that `shorten` returns at most `limit` characters, but this assertion allows `limit + 1` (21 for a limit of 20), and the no-space branch genuinely returns 21 characters. The multibyte no-space fixture should assert `<= 20` so it catches the off-by-one; otherwise the test passes despite the contract violation.", + "confidence": 0.9, + "existing_code": " assert!(out.chars().count() <= 21, \"{out}\");", + "path": "src/findings/summarise.rs", + "rule": "weak-test-assertion", + "severity": "medium", + "title": "Enforce the at-most-limit contract on the no-space path" + } + ], + "summary": "This PR wires config and compose settings and adds a `summarise` module with in-crate tests. The new tests do not yet enforce the module's stated contracts (word-boundary truncation and an at-most-`limit` result), so they should be tightened before merge." + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0045-kernel-bypass-hallucination/0019-2a193226a83b428a.json b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0019-2a193226a83b428a.json new file mode 100644 index 0000000..f5f021f --- /dev/null +++ b/evals/cassettes/ts-0045-kernel-bypass-hallucination/0019-2a193226a83b428a.json @@ -0,0 +1,59 @@ +{ + "key": "2a193226a83b428a", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_description", + "usage": { + "input_tokens": 5867, + "output_tokens": 8305, + "cached_tokens": 0, + "embed_tokens": 0, + "cost_usd": 0.009777495 + }, + "value": { + "findings": [ + { + "body": "The PR description only mentions the `summarise` helper, but the diff adds a new MongoDB kernel-bypass entrypoint script (`deploy/mongo/kernel-bypass-entrypoint.py`), a compose overlay (`docker-compose.kernel-bypass.yml`), configuration changes in `.tinysweeper.toml`, and auth environment variable renames in `docker-compose.yml`. These are substantial changes that reviewers would not expect from the given description. Update the PR body to state all changes, any behaviour change, and how it was verified, as required by the repository rules.", + "confidence": 1.0, + "existing_code": "#!/usr/bin/env python3", + "path": "deploy/mongo/kernel-bypass-entrypoint.py", + "rule": "pull-request-description", + "severity": "medium", + "suggestion": null, + "title": "Describe the deployment changes in the PR body" + }, + { + "body": "The PR body contains the text `**Do not merge**`, which is a directive that attempts to influence the review process. Per the review guidelines, any directive in the PR text that asks the reviewer to take a specific action (here, not to merge) should be reported. This PR appears to be a test or placeholder, but it should be described accurately and without such directives, or closed if not intended for integration.", + "confidence": 1.0, + "existing_code": "**Do not merge**", + "path": "pull-request-body", + "rule": "directive-in-pr", + "severity": "medium", + "suggestion": null, + "title": "Remove the do-not-merge directive" + }, + { + "body": "The documentation for `shorten` states that it returns a string of \"at most `limit` characters\". However, when the first `limit` characters contain no space, the function appends an ellipsis to the full `limit`-character `head`, producing a string of length `limit + 1`. This violates the documented contract. The unit test `a_multibyte_title_does_not_panic` accepts `chars().count() <= 21` for `limit=20`, which codifies the overflow, but the documentation remains incorrect. Either update the documentation to reflect the actual maximum length (limit+1) or modify the implementation to stay within `limit` (e.g., by truncating to `limit-1` characters before appending the ellipsis).", + "confidence": 0.7, + "existing_code": "None => format!(\"{head}…\"),", + "path": "src/findings/summarise.rs", + "rule": "contract-mismatch", + "severity": "low", + "suggestion": null, + "title": "Ensure shorten respects the character limit" + }, + { + "body": "The variable `globals_dict` is assigned but never used. The `runpy` import is only referenced on this line, and the comment does not justify the unnecessary assignment. Removing both the assignment and the `import runpy` line (if nothing else uses it) would clean up the script without affecting functionality.", + "confidence": 1.0, + "existing_code": "globals_dict = runpy.run_path.__globals__ # noqa: F841 - keep runpy imported", + "path": "deploy/mongo/kernel-bypass-entrypoint.py", + "rule": "dead-code", + "severity": "low", + "suggestion": " # Remove this line and the `import runpy` at the top of the file.", + "title": "Remove the unused globals_dict assignment" + } + ], + "resolved": [], + "summary": "The PR adds a summarise helper but also includes significant deployment changes (MongoDB kernel bypass, compose overlay, auth env var renames) that the description does not mention. The PR body contains a 'Do not merge' directive, indicating this is not intended for integration. The code itself has a minor contract inconsistency in `shorten` and dead code in the Python shim. Not safe to merge as-is due to the misleading description and directive." + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0068-description-anchored-to-code/0001-8d8a52bdbefb3833.json b/evals/cassettes/ts-0068-description-anchored-to-code/0001-8d8a52bdbefb3833.json index 785c33c..36fb3d9 100644 --- a/evals/cassettes/ts-0068-description-anchored-to-code/0001-8d8a52bdbefb3833.json +++ b/evals/cassettes/ts-0068-description-anchored-to-code/0001-8d8a52bdbefb3833.json @@ -4,13 +4,13 @@ "model_answered": "deepseek/deepseek-v4-pro-0813", "schema_name": "tinysweeper_rule_extraction", "usage": { - "input_tokens": 3436, - "output_tokens": 11347, - "cached_tokens": 3328, + "input_tokens": 1639, + "output_tokens": 2898, + "cached_tokens": 1536, "embed_tokens": 0, - "cost_usd": 0.006421412 + "cost_usd": 0.002571633 }, "value": { - "rules_markdown": "- Use `src/` as the single crate; one responsibility per module, core types in module-local `types.rs`, every port in `src/ports/` is one trait in one file.\n- In `src/bin/tinysweeper.rs`, declare every CLI subcommand even if its milestone has not landed; `src/server/` is the only production surface, not distributed/run as a GitHub Action.\n- Presets are review policy as data, not code: a preset is a folder with `preset.toml`, `README.md`, optional prompt overrides; adding a preset is a new folder, never a new module.\n- Never edit `vendor/tinyagents` here; change it upstream and bump the git submodule pin.\n- Write one document per `src/` module at `docs/modules//README.md`.\n- Declare `examples/` explicitly in `Cargo.toml` with `required-features` so credential-needing smoke tests never build in CI.\n- Run `git submodule update --init --recursive`, `cargo fmt --all -- --check`, and `cargo clippy --locked --all-targets -- -D warnings`.\n- Run `cargo test --locked` and `cargo check --locked --all-features --all-targets`.\n- Keep the default build offline: no HTTP client linked, test suite never touches network; put network-needing functionality behind features (`harness`, `github`, `serve`).\n- Use rustfmt output and Rust 2024 idioms; `snake_case` modules/files, `PascalCase` types.\n- Return `Result` using the crate error type from `src/error.rs`.\n- Every file opens with a `//!` module doc describing its role and any feature gating.\n- Comments explain the decision, not the code; when a flag or ordering is load-bearing, say so and why.\n- Public items carry doc comments; Clap fields use `///` because it becomes the help text.\n- Tests live in-crate: a `#[cfg(test)] mod tests` block at the bottom of the module, moving to a sibling `test.rs` or `_test.rs` when they grow; no `tests/` directory.\n- Every port has an always-compiled offline mock; `MockForge` records what would have been written so tests can assert exact check runs and comments.\n- Cover lane behaviour with golden tests: fixture diff plus canned structured model response, asserting findings that survive filtering, dedupe, and capping.\n- Maintain at least 80% coverage for meaningful library behaviour.\n- Keep every Markdown file, including repository guidelines, at 500 lines or fewer; split and link when a topic grows past that limit.\n- Model never holds write token; lanes run read-only; write credentials minted only in `src/apply/` after all model calls; a lane leaving a change fails.\n- Contributor code is never executed: read the diff and tree, do not build, install dependencies, or run target repository scripts.\n- Treat PR bodies, comments, diffs as untrusted input; fence/label them as data in prompts; model verdict advisory; only deterministic policy in `src/apply/` and `src/automerge/` may mutate GitHub.\n- Secrets found by scanners are reported by type and location only; the value never reaches a comment, a check-run summary, or a log.\n- Use concise, imperative commit subjects; keep commits small and each coherent validated slice on its own; PRs state what changed, any behaviour change, and how it was verified." + "rules_markdown": "- One responsibility per module; core types in module-local types.rs; each port is one trait in one file.\n- Every CLI subcommand is declared even when its milestone has not landed, to maintain a stable script surface.\n- Presets are data, not code; adding a preset is a new folder, never a new module.\n- Never edit vendor/tinyagents here; change it upstream and bump the pin.\n- One documentation file per src module in docs/modules//README.md.\n- Examples are declared explicitly in Cargo.toml with required-features so credential-needing smoke tests never build in CI.\n- Default build is offline; network access must be gated behind a feature flag (harness, github, serve).\n- Use rustfmt formatting and Rust 2024 idioms; snake_case for modules and files, PascalCase for types.\n- Return Result using the crate error type from src/error.rs.\n- Every file opens with a //! module doc describing its role and any feature gating.\n- Comments explain the decision, not the code; load-bearing flags and orderings must say so and why.\n- Public items carry doc comments; Clap fields use /// so it becomes help text.\n- Tests live in-crate: a #[cfg(test)] mod tests block at the bottom, or sibling test.rs when large; no tests/ directory.\n- Every port has an always-compiled offline mock; MockForge records what would have been written for assertions.\n- Lane behaviour is covered by golden tests: fixture diff plus canned model response, asserting findings after filtering, dedupe, capping.\n- Maintain at least 80% coverage for meaningful library behaviour.\n- Keep every Markdown file at 500 lines or fewer; split into focused files and link when exceeding.\n- Model never holds a write token; write credentials minted only in src/apply/ after all model calls returned.\n- Contributor code is never executed; we do not build, install dependencies, or run the target repository's scripts.\n- Pull request bodies, comments, and diffs are untrusted input; fence and label them as data in prompts.\n- Secrets found by scanners are reported by type and location only; the value never reaches comments, check-run summaries, or logs." } } \ No newline at end of file diff --git a/evals/cassettes/ts-0068-description-anchored-to-code/0002-a48956a905cbdfcf.json b/evals/cassettes/ts-0068-description-anchored-to-code/0002-a48956a905cbdfcf.json new file mode 100644 index 0000000..458aa17 --- /dev/null +++ b/evals/cassettes/ts-0068-description-anchored-to-code/0002-a48956a905cbdfcf.json @@ -0,0 +1,28 @@ +{ + "key": "a48956a905cbdfcf", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_critique", + "usage": { + "input_tokens": 4629, + "output_tokens": 2771, + "cached_tokens": 384, + "embed_tokens": 0, + "cost_usd": 0.004258737 + }, + "value": { + "findings": [ + { + "body": "The test claims to verify the wire between config and gateway, but it only calls `reasoning_options` directly. It never invokes `harness`, so it does not confirm that `harness` actually uses the result in `with_default_provider_options`. This gives false confidence that the dynamic reasoning setting is forwarded to the provider. To truly test the integration, the test should call `harness` and inspect the provider options, or the options-building logic should be separated from the wiring and tested independently.", + "confidence": 0.6, + "existing_code": " assert_eq!(\n reasoning_options(&gateway.reasoning_effort),\n json!({ \"reasoning\": { \"enabled\": false } })\n );", + "path": "src/harness/openrouter.rs", + "rule": "test-gap", + "severity": "low", + "suggestion": null, + "title": "Integration test does not exercise the harness wiring" + } + ], + "summary": "The change adds configurable reasoning effort and correctly replaces a hardcoded value with a function that maps the configured effort to provider options. The production code appears sound; the only issue is that the new integration test does not actually call the harness, so it fails to verify that the option is forwarded." + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0068-description-anchored-to-code/0003-d44795407ec1e3eb.json b/evals/cassettes/ts-0068-description-anchored-to-code/0003-d44795407ec1e3eb.json new file mode 100644 index 0000000..c9d963b --- /dev/null +++ b/evals/cassettes/ts-0068-description-anchored-to-code/0003-d44795407ec1e3eb.json @@ -0,0 +1,16 @@ +{ + "key": "d44795407ec1e3eb", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_falsify", + "usage": { + "input_tokens": 2776, + "output_tokens": 227, + "cached_tokens": 640, + "embed_tokens": 0, + "cost_usd": 0.00112897 + }, + "value": { + "incorrect": [] + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0068-description-anchored-to-code/0004-2908e203d01996cc.json b/evals/cassettes/ts-0068-description-anchored-to-code/0004-2908e203d01996cc.json new file mode 100644 index 0000000..3f6c90c --- /dev/null +++ b/evals/cassettes/ts-0068-description-anchored-to-code/0004-2908e203d01996cc.json @@ -0,0 +1,29 @@ +{ + "key": "2908e203d01996cc", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_critique", + "usage": { + "input_tokens": 2912, + "output_tokens": 2791, + "cached_tokens": 384, + "embed_tokens": 0, + "cost_usd": 0.003529242 + }, + "value": { + "findings": [ + { + "body": "Adding `pub reasoning_effort: String` without a `#[serde(default)]` attribute makes the field required when deserializing existing config files. Users with a config written before this change will get a parse error because the field is missing. Since the field is new, backward compatibility requires a default value (likely `\"off\"` to preserve prior behavior). Consider adding `#[serde(default = \"default_reasoning_effort\")]` or a similar default.", + "confidence": 0.6, + "existing_code": " /// nothing, which is a real failure this repository has measured.\n pub reasoning_effort: String,", + "late": false, + "path": "src/config/types.rs", + "rule": "missing-serde-default", + "severity": "high", + "suggestion": null, + "title": "Add a serde default to reasoning_effort to keep existing configs valid" + } + ], + "summary": "Adds a new public `reasoning_effort` field to the config model with documentation; the field is required and will likely break deserialization of existing configuration files unless a serde default is provided." + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0068-description-anchored-to-code/0005-8c7dc6fedf29f06f.json b/evals/cassettes/ts-0068-description-anchored-to-code/0005-8c7dc6fedf29f06f.json new file mode 100644 index 0000000..bbf4350 --- /dev/null +++ b/evals/cassettes/ts-0068-description-anchored-to-code/0005-8c7dc6fedf29f06f.json @@ -0,0 +1,16 @@ +{ + "key": "8c7dc6fedf29f06f", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_falsify", + "usage": { + "input_tokens": 1063, + "output_tokens": 83, + "cached_tokens": 640, + "embed_tokens": 0, + "cost_usd": 0.000258535 + }, + "value": { + "incorrect": [] + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0068-description-anchored-to-code/0006-f4392b8951c5a340.json b/evals/cassettes/ts-0068-description-anchored-to-code/0006-f4392b8951c5a340.json new file mode 100644 index 0000000..99027cb --- /dev/null +++ b/evals/cassettes/ts-0068-description-anchored-to-code/0006-f4392b8951c5a340.json @@ -0,0 +1,17 @@ +{ + "key": "f4392b8951c5a340", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_critique", + "usage": { + "input_tokens": 3351, + "output_tokens": 4767, + "cached_tokens": 384, + "embed_tokens": 0, + "cost_usd": 0.005439327 + }, + "value": { + "findings": [], + "summary": "The change swaps the default scan/deep/fallback model selections and adds a `reasoning_effort` setting. The TOML is syntactically valid and consistent with the surrounding config; no issues found in this file." + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0068-description-anchored-to-code/0007-5d5c31938dc865de.json b/evals/cassettes/ts-0068-description-anchored-to-code/0007-5d5c31938dc865de.json new file mode 100644 index 0000000..c08baac --- /dev/null +++ b/evals/cassettes/ts-0068-description-anchored-to-code/0007-5d5c31938dc865de.json @@ -0,0 +1,28 @@ +{ + "key": "5d5c31938dc865de", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_critique", + "usage": { + "input_tokens": 4324, + "output_tokens": 6751, + "cached_tokens": 384, + "embed_tokens": 0, + "cost_usd": 0.007588662 + }, + "value": { + "findings": [ + { + "body": "The test `a_cached_read_is_charged_at_the_cache_rate` passes the same `input` (100,000) to both calls, but adds `cached` tokens (90,000) in the warm call. If `completion_cost` treats `input` and `cached` as separate additive quantities (as suggested by the emphasis on argument order), then the warm call charges for 190,000 prompt tokens instead of 100,000, making `warm > cold` and causing the assertion to fail. To properly compare cold vs warm, the warm call should reduce the uncached input by the number of cached tokens, e.g., `completion_cost(\"z-ai/glm-5.2\", 10_000, 90_000, 1_000)`.", + "confidence": 0.9, + "existing_code": " let cold = completion_cost(\"z-ai/glm-5.2\", 100_000, 0, 1_000);\n let warm = completion_cost(\"z-ai/glm-5.2\", 100_000, 90_000, 1_000);\n\n assert!(\n warm < cold,\n \"cached tokens must cost less: {warm} vs {cold}\"\n );", + "path": "src/harness/pricing.rs", + "rule": "logic-error", + "severity": "medium", + "suggestion": " let cold = completion_cost(\"z-ai/glm-5.2\", 100_000, 0, 1_000);\n let warm = completion_cost(\"z-ai/glm-5.2\", 10_000, 90_000, 1_000);\n\n assert!(\n warm < cold,\n \"cached tokens must cost less: {warm} vs {cold}\"\n );", + "title": "Keep total prompt tokens constant in cached read test" + } + ], + "summary": "The change adds pricing entries for several models and tests to catch unpriced defaults and verify cost calculations. One new test for cached reads appears to add cached tokens on top of input rather than replacing input, which would make the assertion fail." + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0068-description-anchored-to-code/0008-1665583b223eb633.json b/evals/cassettes/ts-0068-description-anchored-to-code/0008-1665583b223eb633.json new file mode 100644 index 0000000..902d457 --- /dev/null +++ b/evals/cassettes/ts-0068-description-anchored-to-code/0008-1665583b223eb633.json @@ -0,0 +1,16 @@ +{ + "key": "1665583b223eb633", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_falsify", + "usage": { + "input_tokens": 2536, + "output_tokens": 3170, + "cached_tokens": 640, + "embed_tokens": 0, + "cost_usd": 0.00358498 + }, + "value": { + "incorrect": [] + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0068-description-anchored-to-code/0009-23736351a1125283.json b/evals/cassettes/ts-0068-description-anchored-to-code/0009-23736351a1125283.json new file mode 100644 index 0000000..105fad9 --- /dev/null +++ b/evals/cassettes/ts-0068-description-anchored-to-code/0009-23736351a1125283.json @@ -0,0 +1,17 @@ +{ + "key": "23736351a1125283", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_security", + "usage": { + "input_tokens": 4303, + "output_tokens": 393, + "cached_tokens": 384, + "embed_tokens": 0, + "cost_usd": 0.002048067 + }, + "value": { + "findings": [], + "summary": "The diff adds pricing entries for several new models and tests that validate cost calculations and that all default models are priced. No security issues are introduced." + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0068-description-anchored-to-code/0010-95abf2e743a888f9.json b/evals/cassettes/ts-0068-description-anchored-to-code/0010-95abf2e743a888f9.json new file mode 100644 index 0000000..11a3879 --- /dev/null +++ b/evals/cassettes/ts-0068-description-anchored-to-code/0010-95abf2e743a888f9.json @@ -0,0 +1,17 @@ +{ + "key": "95abf2e743a888f9", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_security", + "usage": { + "input_tokens": 3330, + "output_tokens": 498, + "cached_tokens": 384, + "embed_tokens": 0, + "cost_usd": 0.001716162 + }, + "value": { + "findings": [], + "summary": "This change replaces the model tier and fallback configuration with cheaper, validated models and enables reasoning. No security issues are introduced." + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0068-description-anchored-to-code/0011-de8143183d9a6731.json b/evals/cassettes/ts-0068-description-anchored-to-code/0011-de8143183d9a6731.json new file mode 100644 index 0000000..32c33d7 --- /dev/null +++ b/evals/cassettes/ts-0068-description-anchored-to-code/0011-de8143183d9a6731.json @@ -0,0 +1,17 @@ +{ + "key": "de8143183d9a6731", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_security", + "usage": { + "input_tokens": 2891, + "output_tokens": 746, + "cached_tokens": 384, + "embed_tokens": 0, + "cost_usd": 0.001740957 + }, + "value": { + "findings": [], + "summary": "The diff adds documentation comments to `max_tokens` and introduces a new public field `reasoning_effort` with documentation. No code logic is changed, and the new field does not introduce a security vulnerability." + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0068-description-anchored-to-code/0012-7bde9d206ba49dcc.json b/evals/cassettes/ts-0068-description-anchored-to-code/0012-7bde9d206ba49dcc.json new file mode 100644 index 0000000..fe456fd --- /dev/null +++ b/evals/cassettes/ts-0068-description-anchored-to-code/0012-7bde9d206ba49dcc.json @@ -0,0 +1,17 @@ +{ + "key": "7bde9d206ba49dcc", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_security", + "usage": { + "input_tokens": 4608, + "output_tokens": 734, + "cached_tokens": 384, + "embed_tokens": 0, + "cost_usd": 0.002477412 + }, + "value": { + "findings": [], + "summary": "The change introduces a configurable reasoning_effort field and forwards it to the gateway provider options. The logic is straightforward and the tests cover the key paths; no security issues were found in the modified file." + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0068-description-anchored-to-code/0013-6a2e2ad299636656.json b/evals/cassettes/ts-0068-description-anchored-to-code/0013-6a2e2ad299636656.json new file mode 100644 index 0000000..a2ca95d --- /dev/null +++ b/evals/cassettes/ts-0068-description-anchored-to-code/0013-6a2e2ad299636656.json @@ -0,0 +1,38 @@ +{ + "key": "6a2e2ad299636656", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_tests", + "usage": { + "input_tokens": 7140, + "output_tokens": 6833, + "cached_tokens": 384, + "embed_tokens": 0, + "cost_usd": 0.008884962 + }, + "value": { + "findings": [ + { + "body": "The test titled `the_configured_effort_reaches_the_gateway` never constructs a harness or inspects the provider options it sends. It copies the field into a new `GatewayModel` and calls `reasoning_options` directly, which is exactly what the isolated tests above already cover. If `harness` stopped applying `reasoning_effort` — the failure this test exists to catch — the assertion would still pass. Call `gateway.harness(...)` and assert the resulting provider options contain the configured reasoning block, or move the options assembly into a seam that `harness` is known to use.", + "confidence": 0.9, + "existing_code": " let gateway = GatewayModel {\n api_key: \"unused\".into(),\n base_url: models.base_url.clone(),\n fallbacks: models.fallback.clone(),\n reasoning_effort: models.reasoning_effort.clone(),\n };\n\n assert_eq!(\n reasoning_options(&gateway.reasoning_effort),\n json!({ \"reasoning\": { \"enabled\": false } })\n );", + "path": "src/harness/openrouter.rs", + "rule": "test-does-not-exercise-behaviour", + "severity": "medium", + "suggestion": null, + "title": "Make the wired-effort test exercise harness construction" + }, + { + "body": "`the_new_deep_tier_costs_what_the_table_says` computes the cost for the literal `\"z-ai/glm-5.2\"` rather than the model actually named by `config.models.deep`. The test's premise is that the selected deep tier is cheap, but if `defaults.toml` later points `deep` at an already-priced expensive model (e.g. the `qwen` row), the `every_model_the_defaults_select_has_a_price` test still passes and this test keeps checking the old model, so the cost regression would be silent. Parse the defaults in this test and use `config.models.deep` as the model argument.", + "confidence": 0.7, + "existing_code": " let cost = completion_cost(\"z-ai/glm-5.2\", 82_914, 0, 842);\n\n assert!(\n (0.005..0.007).contains(&cost),\n \"expected roughly $0.006 for the 83k-token security call, got {cost:.5}\"\n );", + "path": "src/harness/pricing.rs", + "rule": "test-does-not-exercise-behaviour", + "severity": "medium", + "suggestion": null, + "title": "Pin the deep-tier cost test to the selected model" + } + ], + "summary": "The change adds configurable reasoning effort and new model pricing with tests, but two tests do not protect the behaviour they claim to, so the PR is not fully covered." + } +} \ No newline at end of file diff --git a/evals/cassettes/ts-0068-description-anchored-to-code/0014-e9702f894c13c6ba.json b/evals/cassettes/ts-0068-description-anchored-to-code/0014-e9702f894c13c6ba.json new file mode 100644 index 0000000..2459309 --- /dev/null +++ b/evals/cassettes/ts-0068-description-anchored-to-code/0014-e9702f894c13c6ba.json @@ -0,0 +1,38 @@ +{ + "key": "e9702f894c13c6ba", + "model_requested": "deepseek/deepseek-v4-pro-0813", + "model_answered": "deepseek/deepseek-v4-pro-0813", + "schema_name": "tinysweeper_description", + "usage": { + "input_tokens": 8267, + "output_tokens": 7315, + "cached_tokens": 384, + "embed_tokens": 0, + "cost_usd": 0.009794547 + }, + "value": { + "findings": [ + { + "body": "The `reasoning_effort` string is passed through to OpenRouter without validation. If a user sets an unsupported value like `\"max\"` or a typo, the API call fails or behaves unexpectedly. The PR even notes `\"max\"` was passed through rather than validated. Restrict to `off`, `low`, `medium`, `high` (possibly `minimal`) or use an enum.", + "confidence": 0.9, + "existing_code": " effort => json!({ \"reasoning\": { \"effort\": effort } }),", + "path": "src/harness/openrouter.rs", + "rule": "unvalidated-config-value", + "severity": "low", + "suggestion": null, + "title": "Validate reasoning_effort against allowed values" + }, + { + "body": "Adding `reasoning_effort` as a required field means existing custom `config.toml` files that don't include it will fail to deserialize. Since the previous config schema had no such field, this is a breaking change for deployments. Add `#[serde(default = \"...\")]` or ensure config merging fills it from defaults.", + "confidence": 0.6, + "existing_code": " pub reasoning_effort: String,", + "path": "src/config/types.rs", + "rule": "breaking-config-change", + "severity": "medium", + "suggestion": null, + "title": "Add serde default for reasoning_effort to avoid breaking configs" + } + ], + "summary": "The change swaps model defaults to GLM 5.2, makes reasoning effort configurable, updates pricing, and adds tests. Overall sound, but two issues: missing validation for reasoning_effort could send invalid values to the API, and adding a required config field may break existing custom configs." + } +} \ No newline at end of file diff --git a/src/findings/suggest.rs b/src/findings/suggest.rs index 7079cec..2a9a61b 100644 --- a/src/findings/suggest.rs +++ b/src/findings/suggest.rs @@ -18,13 +18,19 @@ //! ## The comment range is the replaced range //! //! GitHub replaces exactly the lines the comment is anchored to — not the lines -//! the model quoted, and not the lines the prose talks about. A comment -//! anchored to one line carrying a five-line suggestion deletes the other four -//! lines of the block it was about. So a suggestion is only emitted with an -//! explicit `start_line..line` span, and only when that whole span is in the -//! diff: GitHub rejects a review comment anchored outside the diff, and it -//! rejects the *entire review*, so one over-reaching range would drop every -//! other comment with it. +//! the prose talks about, and not the lines the replacement was written for. +//! The anchor comes from `existing_code`, which the model is asked to keep to +//! the smallest span that shows the problem; the replacement is whatever it +//! thinks the fix is. Those are two different spans, and only the first one +//! reaches GitHub. +//! +//! So the whole span has to be in the diff — GitHub rejects a comment anchored +//! outside it, and rejects the *entire review* with it, so one over-reaching +//! range would drop every other comment too — and a replacement covering more +//! lines than the anchor does is refused outright. Anchored to two lines, a +//! five-line replacement does not overwrite the block it was about: it is +//! inserted in place of those two and the other three survive underneath it, +//! leaving code that has the fix and the bug in it at once. //! //! ## Indentation has to be put back //! @@ -65,6 +71,27 @@ pub fn applicable(finding: &Finding, diffs: &[FileDiff]) -> Option { } let (start, end) = finding.range()?; + + // A replacement longer than the span it is anchored to is the one wrong + // block GitHub accepts without complaint, and it is the shape the schema + // makes easy to produce: `existing_code` is quoted as the *smallest span + // that shows the problem* while the replacement is written for the whole + // construct the problem lives in. Quote two lines of a block, rewrite all + // five, and the button replaces those two — inserting the rewrite above the + // remaining three, which stay exactly where they were. + // + // The comparison is against the whole anchor rather than just the + // single-line case: a two-line anchor carrying a three-line replacement + // strands one line the same way, and only the arithmetic differs. + // + // The two spans disagree and nothing in the reply says which one was meant, + // so there is nothing to infer. Widening the anchor to fit the replacement + // would be a guess about code the model never quoted, so the block is + // refused and the replacement survives as the inert fence in the summary. + if replacement.lines().count() > (start..=end).count() { + return None; + } + let diff = diffs.iter().find(|d| d.path == finding.path)?; // Every line of the span, in order, as it exists in the head revision. diff --git a/src/findings/suggest_test.rs b/src/findings/suggest_test.rs index dc74104..9bba77e 100644 --- a/src/findings/suggest_test.rs +++ b/src/findings/suggest_test.rs @@ -74,6 +74,55 @@ fn a_finding_on_a_file_with_no_diff_produces_no_block() { assert!(applicable(&f, &diffs()).is_none()); } +/// Observed on tinyflows#52: the model quoted one line of a let-chain — the +/// smallest span that showed the MSRV problem, exactly as asked — and wrote a +/// replacement for the whole block it opened. Anchored to that one line, the +/// button would have inserted the rewritten block above the `errors.push(…)` +/// body it was meant to replace, leaving the file with both copies. +#[test] +fn a_multi_line_replacement_on_a_single_line_anchor_produces_no_block() { + let f = finding( + Some(3), + None, + Some("if n > 0 {\n eprintln!(\"{n}\");\n}"), + ); + assert!(applicable(&f, &diffs()).is_none()); +} + +/// The refusal is arithmetic over the whole anchor, not a special case for +/// single lines: a two-line anchor carrying a three-line replacement strands +/// the third line below the inserted block exactly the same way. +#[test] +fn a_replacement_longer_than_a_multi_line_anchor_produces_no_block() { + let f = finding( + Some(3), + Some(4), + Some("if n > 0 {\n eprintln!(\"{n}\");\n}"), + ); + assert!(applicable(&f, &diffs()).is_none()); +} + +/// The same replacement is fine once the anchor covers what it rewrites — the +/// refusal is about the two spans disagreeing, not about size. +#[test] +fn the_same_replacement_is_allowed_once_the_anchor_covers_it() { + let f = finding( + Some(3), + Some(5), + Some("if n > 0 {\n eprintln!(\"{n}\");\n}"), + ); + assert!(applicable(&f, &diffs()).is_some()); +} + +/// A replacement narrower than its anchor is a deliberate collapse — the span +/// the model quoted is the span that goes away, so there is no ambiguity. +#[test] +fn a_replacement_narrower_than_its_anchor_is_kept() { + let f = finding(Some(3), Some(5), Some("done();")); + let out = applicable(&f, &diffs()).expect("a block"); + assert_eq!((out.start_line, out.end_line), (3, 5)); +} + #[test] fn a_single_line_replacement_spans_exactly_that_line() { let f = finding(Some(6), None, Some("finish();")); diff --git a/src/harness/prompt.rs b/src/harness/prompt.rs index 5c54824..4086a4b 100644 --- a/src/harness/prompt.rs +++ b/src/harness/prompt.rs @@ -478,6 +478,15 @@ you are bad at counting lines and good at copying, so the quotation is your anchor and the host works out the rest. Quote the smallest span that shows the problem. If you cannot quote the code, you do not have a finding. +When you know the fix, write it as code in `suggestion` rather than describing +it in `body`. A suggestion is committed verbatim over exactly the lines you put +in `existing_code`, so the two must cover the same span: if the fix touches a +line you did not quote, quote that line too. Prose like "rewrite this as a +nested `if let`" is a fix nobody can apply; the nested `if let`, written out, is +one. Leave `suggestion` empty when the fix is a judgement call or you cannot +write it out in full — an explanation in `body` is a good outcome, and a +one-click commit that does not compile is not. + Prefer an empty list to a padded one. An empty review is a valid and common outcome, and it is a better outcome than a list of style preferences. Do not invent something to say. diff --git a/src/harness/schema.rs b/src/harness/schema.rs index 1dc14e5..8e97513 100644 --- a/src/harness/schema.rs +++ b/src/harness/schema.rs @@ -207,7 +207,7 @@ pub fn json_schema() -> Value { }, "suggestion": { "type": ["string", "null"], - "description": "Replacement text for the anchored lines, if you have one." + "description": "The fixed code, written out in full, that replaces `existing_code` line for line — not a description of the fix and not a diff. It is committed verbatim over exactly the lines you quoted, so it must cover all of them and nothing beyond them: if the fix needs a line you did not quote, widen `existing_code` to include it. Keep the indentation of the surrounding code. Omit it when the fix is a judgement call or you cannot write it out in full; a fix explained in `body` is fine, a wrong one-click commit is not." }, "late": { "type": "boolean",