From af70757bbeb097f4779146021e9455e71f6e3f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=9F=E5=9B=BD=E5=BA=86?= Date: Fri, 14 Aug 2026 16:44:37 +0800 Subject: [PATCH 1/2] feat: add python line and branch coverage governance to ci Add scripts/coverage_gate.py and make targets that measure line and branch coverage for the raven package and enforce three gates: a monotonic ratchet against an audited baseline, 90% coverage on the executable lines a pull request changes, and a baseline that cannot be lowered relative to the target branch. The CI unit job runs the suite once with coverage and reuses the report for every gate, uploading xml, json, and html artifacts on every run. Co-authored-by: Claude (claude-opus-4-8) --- .github/coverage-baseline.json | 3576 ++++++++++++++++++++++++++++++++ .github/workflows/ci.yml | 51 +- .gitignore | 2 + AGENTS.md | 3 + Makefile | 53 +- pyproject.toml | 31 + scripts/coverage_gate.py | 352 ++++ tests/README.md | 77 + tests/test_coverage_gate.py | 217 ++ 9 files changed, 4346 insertions(+), 16 deletions(-) create mode 100644 .github/coverage-baseline.json create mode 100644 scripts/coverage_gate.py create mode 100644 tests/test_coverage_gate.py diff --git a/.github/coverage-baseline.json b/.github/coverage-baseline.json new file mode 100644 index 00000000..45be6f2f --- /dev/null +++ b/.github/coverage-baseline.json @@ -0,0 +1,3576 @@ +{ + "files": { + "raven/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 13, + "line_percent": 76.470588, + "statements": 17 + }, + "raven/agent/__init__.py": { + "branch_percent": 0.0, + "branches": 2, + "covered_branches": 0, + "covered_lines": 5, + "line_percent": 45.454545, + "statements": 11 + }, + "raven/agent/context/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 2, + "line_percent": 100.0, + "statements": 2 + }, + "raven/agent/context/builder.py": { + "branch_percent": 50.0, + "branches": 28, + "covered_branches": 14, + "covered_lines": 72, + "line_percent": 72.0, + "statements": 100 + }, + "raven/agent/hook/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 4, + "line_percent": 100.0, + "statements": 4 + }, + "raven/agent/hook/adapters.py": { + "branch_percent": 100.0, + "branches": 10, + "covered_branches": 10, + "covered_lines": 61, + "line_percent": 100.0, + "statements": 61 + }, + "raven/agent/hook/base.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 35, + "line_percent": 100.0, + "statements": 35 + }, + "raven/agent/hook/composite.py": { + "branch_percent": 100.0, + "branches": 10, + "covered_branches": 10, + "covered_lines": 50, + "line_percent": 100.0, + "statements": 50 + }, + "raven/agent/loop/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 2, + "line_percent": 100.0, + "statements": 2 + }, + "raven/agent/loop/checkpoint.py": { + "branch_percent": 90.909091, + "branches": 22, + "covered_branches": 20, + "covered_lines": 81, + "line_percent": 90.0, + "statements": 90 + }, + "raven/agent/loop/main.py": { + "branch_percent": 82.207207, + "branches": 444, + "covered_branches": 365, + "covered_lines": 1037, + "line_percent": 87.069689, + "statements": 1191 + }, + "raven/agent/loop/recovery.py": { + "branch_percent": 100.0, + "branches": 8, + "covered_branches": 8, + "covered_lines": 36, + "line_percent": 100.0, + "statements": 36 + }, + "raven/agent/personalizer/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 2, + "line_percent": 100.0, + "statements": 2 + }, + "raven/agent/personalizer/personalizer.py": { + "branch_percent": 0.0, + "branches": 24, + "covered_branches": 0, + "covered_lines": 26, + "line_percent": 21.138211, + "statements": 123 + }, + "raven/agent/spine_runner.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 9, + "line_percent": 100.0, + "statements": 9 + }, + "raven/agent/subagent/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 2, + "line_percent": 100.0, + "statements": 2 + }, + "raven/agent/subagent/manager.py": { + "branch_percent": 85.714286, + "branches": 28, + "covered_branches": 24, + "covered_lines": 146, + "line_percent": 94.805195, + "statements": 154 + }, + "raven/agent/tools/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 3, + "line_percent": 100.0, + "statements": 3 + }, + "raven/agent/tools/ask_user.py": { + "branch_percent": 94.444444, + "branches": 18, + "covered_branches": 17, + "covered_lines": 53, + "line_percent": 98.148148, + "statements": 54 + }, + "raven/agent/tools/base.py": { + "branch_percent": 62.857143, + "branches": 70, + "covered_branches": 44, + "covered_lines": 100, + "line_percent": 73.529412, + "statements": 136 + }, + "raven/agent/tools/deep_research.py": { + "branch_percent": 89.655172, + "branches": 58, + "covered_branches": 52, + "covered_lines": 248, + "line_percent": 93.939394, + "statements": 264 + }, + "raven/agent/tools/file_search.py": { + "branch_percent": 78.571429, + "branches": 70, + "covered_branches": 55, + "covered_lines": 170, + "line_percent": 82.926829, + "statements": 205 + }, + "raven/agent/tools/filesystem.py": { + "branch_percent": 38.157895, + "branches": 76, + "covered_branches": 29, + "covered_lines": 137, + "line_percent": 61.160714, + "statements": 224 + }, + "raven/agent/tools/mcp.py": { + "branch_percent": 45.833333, + "branches": 24, + "covered_branches": 11, + "covered_lines": 40, + "line_percent": 45.977011, + "statements": 87 + }, + "raven/agent/tools/media.py": { + "branch_percent": 80.0, + "branches": 20, + "covered_branches": 16, + "covered_lines": 73, + "line_percent": 94.805195, + "statements": 77 + }, + "raven/agent/tools/media_gen.py": { + "branch_percent": 0.0, + "branches": 82, + "covered_branches": 0, + "covered_lines": 59, + "line_percent": 22.43346, + "statements": 263 + }, + "raven/agent/tools/message.py": { + "branch_percent": 66.666667, + "branches": 6, + "covered_branches": 4, + "covered_lines": 48, + "line_percent": 94.117647, + "statements": 51 + }, + "raven/agent/tools/registry.py": { + "branch_percent": 90.0, + "branches": 10, + "covered_branches": 9, + "covered_lines": 52, + "line_percent": 94.545455, + "statements": 55 + }, + "raven/agent/tools/shell.py": { + "branch_percent": 75.0, + "branches": 44, + "covered_branches": 33, + "covered_lines": 108, + "line_percent": 84.375, + "statements": 128 + }, + "raven/agent/tools/shell_policy.py": { + "branch_percent": 86.842105, + "branches": 76, + "covered_branches": 66, + "covered_lines": 121, + "line_percent": 93.79845, + "statements": 129 + }, + "raven/agent/tools/skill_hub.py": { + "branch_percent": 96.428571, + "branches": 28, + "covered_branches": 27, + "covered_lines": 92, + "line_percent": 94.845361, + "statements": 97 + }, + "raven/agent/tools/spawn.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 29, + "line_percent": 93.548387, + "statements": 31 + }, + "raven/agent/tools/tool_index.py": { + "branch_percent": 100.0, + "branches": 20, + "covered_branches": 20, + "covered_lines": 71, + "line_percent": 100.0, + "statements": 71 + }, + "raven/agent/tools/tool_search.py": { + "branch_percent": 91.666667, + "branches": 24, + "covered_branches": 22, + "covered_lines": 99, + "line_percent": 99.0, + "statements": 100 + }, + "raven/agent/tools/web.py": { + "branch_percent": 4.166667, + "branches": 24, + "covered_branches": 1, + "covered_lines": 33, + "line_percent": 37.078652, + "statements": 89 + }, + "raven/auth/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 4, + "line_percent": 100.0, + "statements": 4 + }, + "raven/auth/allowlist.py": { + "branch_percent": 100.0, + "branches": 8, + "covered_branches": 8, + "covered_lines": 24, + "line_percent": 100.0, + "statements": 24 + }, + "raven/auth/capability_token.py": { + "branch_percent": 100.0, + "branches": 8, + "covered_branches": 8, + "covered_lines": 52, + "line_percent": 96.296296, + "statements": 54 + }, + "raven/auth/managed_settings.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 9, + "line_percent": 100.0, + "statements": 9 + }, + "raven/channels/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 4, + "line_percent": 100.0, + "statements": 4 + }, + "raven/channels/adapters/dingtalk/api.py": { + "branch_percent": 70.454545, + "branches": 44, + "covered_branches": 31, + "covered_lines": 110, + "line_percent": 72.847682, + "statements": 151 + }, + "raven/channels/adapters/dingtalk/channel.py": { + "branch_percent": 62.5, + "branches": 56, + "covered_branches": 35, + "covered_lines": 112, + "line_percent": 71.794872, + "statements": 156 + }, + "raven/channels/adapters/dingtalk/parsing.py": { + "branch_percent": 88.235294, + "branches": 34, + "covered_branches": 30, + "covered_lines": 81, + "line_percent": 100.0, + "statements": 81 + }, + "raven/channels/adapters/dingtalk/spec.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 4, + "line_percent": 66.666667, + "statements": 6 + }, + "raven/channels/adapters/discord/channel.py": { + "branch_percent": 63.265306, + "branches": 98, + "covered_branches": 62, + "covered_lines": 199, + "line_percent": 75.09434, + "statements": 265 + }, + "raven/channels/adapters/discord/spec.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 4, + "line_percent": 66.666667, + "statements": 6 + }, + "raven/channels/adapters/email/channel.py": { + "branch_percent": 81.818182, + "branches": 44, + "covered_branches": 36, + "covered_lines": 110, + "line_percent": 92.436975, + "statements": 119 + }, + "raven/channels/adapters/email/mailbox.py": { + "branch_percent": 77.777778, + "branches": 18, + "covered_branches": 14, + "covered_lines": 44, + "line_percent": 89.795918, + "statements": 49 + }, + "raven/channels/adapters/email/parsing.py": { + "branch_percent": 86.111111, + "branches": 36, + "covered_branches": 31, + "covered_lines": 75, + "line_percent": 91.463415, + "statements": 82 + }, + "raven/channels/adapters/email/spec.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 4, + "line_percent": 66.666667, + "statements": 6 + }, + "raven/channels/adapters/feishu/cards.py": { + "branch_percent": 70.37037, + "branches": 54, + "covered_branches": 38, + "covered_lines": 97, + "line_percent": 89.814815, + "statements": 108 + }, + "raven/channels/adapters/feishu/channel.py": { + "branch_percent": 25.423729, + "branches": 118, + "covered_branches": 30, + "covered_lines": 123, + "line_percent": 40.728477, + "statements": 302 + }, + "raven/channels/adapters/feishu/content.py": { + "branch_percent": 51.111111, + "branches": 90, + "covered_branches": 46, + "covered_lines": 75, + "line_percent": 64.655172, + "statements": 116 + }, + "raven/channels/adapters/feishu/spec.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 4, + "line_percent": 66.666667, + "statements": 6 + }, + "raven/channels/adapters/matrix/channel.py": { + "branch_percent": 19.166667, + "branches": 120, + "covered_branches": 23, + "covered_lines": 98, + "line_percent": 28.571429, + "statements": 343 + }, + "raven/channels/adapters/matrix/content.py": { + "branch_percent": 86.0, + "branches": 50, + "covered_branches": 43, + "covered_lines": 130, + "line_percent": 94.890511, + "statements": 137 + }, + "raven/channels/adapters/matrix/spec.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 4, + "line_percent": 66.666667, + "statements": 6 + }, + "raven/channels/adapters/mochat/api.py": { + "branch_percent": 68.75, + "branches": 16, + "covered_branches": 11, + "covered_lines": 45, + "line_percent": 86.538462, + "statements": 52 + }, + "raven/channels/adapters/mochat/channel.py": { + "branch_percent": 38.157895, + "branches": 152, + "covered_branches": 58, + "covered_lines": 188, + "line_percent": 52.661064, + "statements": 357 + }, + "raven/channels/adapters/mochat/cursors.py": { + "branch_percent": 78.571429, + "branches": 14, + "covered_branches": 11, + "covered_lines": 48, + "line_percent": 87.272727, + "statements": 55 + }, + "raven/channels/adapters/mochat/parsing.py": { + "branch_percent": 89.0625, + "branches": 64, + "covered_branches": 57, + "covered_lines": 114, + "line_percent": 95.798319, + "statements": 119 + }, + "raven/channels/adapters/mochat/pipeline.py": { + "branch_percent": 87.5, + "branches": 16, + "covered_branches": 14, + "covered_lines": 68, + "line_percent": 100.0, + "statements": 68 + }, + "raven/channels/adapters/mochat/spec.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 4, + "line_percent": 66.666667, + "statements": 6 + }, + "raven/channels/adapters/mochat/transport.py": { + "branch_percent": 100.0, + "branches": 12, + "covered_branches": 12, + "covered_lines": 55, + "line_percent": 85.9375, + "statements": 64 + }, + "raven/channels/adapters/qq/channel.py": { + "branch_percent": 60.0, + "branches": 20, + "covered_branches": 12, + "covered_lines": 53, + "line_percent": 59.550562, + "statements": 89 + }, + "raven/channels/adapters/qq/parsing.py": { + "branch_percent": 100.0, + "branches": 4, + "covered_branches": 4, + "covered_lines": 11, + "line_percent": 100.0, + "statements": 11 + }, + "raven/channels/adapters/qq/spec.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 4, + "line_percent": 66.666667, + "statements": 6 + }, + "raven/channels/adapters/slack/channel.py": { + "branch_percent": 80.952381, + "branches": 42, + "covered_branches": 34, + "covered_lines": 91, + "line_percent": 75.206612, + "statements": 121 + }, + "raven/channels/adapters/slack/parsing.py": { + "branch_percent": 89.285714, + "branches": 28, + "covered_branches": 25, + "covered_lines": 67, + "line_percent": 97.101449, + "statements": 69 + }, + "raven/channels/adapters/slack/spec.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 4, + "line_percent": 66.666667, + "statements": 6 + }, + "raven/channels/adapters/telegram/channel.py": { + "branch_percent": 50.847458, + "branches": 118, + "covered_branches": 60, + "covered_lines": 225, + "line_percent": 64.102564, + "statements": 351 + }, + "raven/channels/adapters/telegram/spec.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 4, + "line_percent": 66.666667, + "statements": 6 + }, + "raven/channels/adapters/wecom/channel.py": { + "branch_percent": 76.666667, + "branches": 60, + "covered_branches": 46, + "covered_lines": 120, + "line_percent": 75.949367, + "statements": 158 + }, + "raven/channels/adapters/wecom/spec.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 4, + "line_percent": 66.666667, + "statements": 6 + }, + "raven/channels/adapters/weixin/channel.py": { + "branch_percent": 33.333333, + "branches": 180, + "covered_branches": 60, + "covered_lines": 203, + "line_percent": 42.647059, + "statements": 476 + }, + "raven/channels/adapters/weixin/crypto.py": { + "branch_percent": 90.0, + "branches": 10, + "covered_branches": 9, + "covered_lines": 36, + "line_percent": 76.595745, + "statements": 47 + }, + "raven/channels/adapters/weixin/protocol.py": { + "branch_percent": 100.0, + "branches": 6, + "covered_branches": 6, + "covered_lines": 58, + "line_percent": 96.666667, + "statements": 60 + }, + "raven/channels/adapters/weixin/spec.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 6, + "line_percent": 100.0, + "statements": 6 + }, + "raven/channels/adapters/weixin/typing_state.py": { + "branch_percent": 70.0, + "branches": 20, + "covered_branches": 14, + "covered_lines": 66, + "line_percent": 90.410959, + "statements": 73 + }, + "raven/channels/adapters/whatsapp/bridge.py": { + "branch_percent": 50.0, + "branches": 14, + "covered_branches": 7, + "covered_lines": 40, + "line_percent": 66.666667, + "statements": 60 + }, + "raven/channels/adapters/whatsapp/channel.py": { + "branch_percent": 57.142857, + "branches": 42, + "covered_branches": 24, + "covered_lines": 91, + "line_percent": 71.09375, + "statements": 128 + }, + "raven/channels/adapters/whatsapp/parsing.py": { + "branch_percent": 100.0, + "branches": 12, + "covered_branches": 12, + "covered_lines": 32, + "line_percent": 100.0, + "statements": 32 + }, + "raven/channels/adapters/whatsapp/spec.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 6, + "line_percent": 100.0, + "statements": 6 + }, + "raven/channels/base.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 19, + "line_percent": 100.0, + "statements": 19 + }, + "raven/channels/contract.py": { + "branch_percent": 100.0, + "branches": 6, + "covered_branches": 6, + "covered_lines": 28, + "line_percent": 100.0, + "statements": 28 + }, + "raven/channels/errors.py": { + "branch_percent": 100.0, + "branches": 4, + "covered_branches": 4, + "covered_lines": 22, + "line_percent": 78.571429, + "statements": 28 + }, + "raven/channels/intake.py": { + "branch_percent": 100.0, + "branches": 6, + "covered_branches": 6, + "covered_lines": 27, + "line_percent": 100.0, + "statements": 27 + }, + "raven/channels/manager.py": { + "branch_percent": 70.0, + "branches": 20, + "covered_branches": 14, + "covered_lines": 57, + "line_percent": 75.0, + "statements": 76 + }, + "raven/channels/media.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 12, + "line_percent": 100.0, + "statements": 12 + }, + "raven/channels/outlet.py": { + "branch_percent": 100.0, + "branches": 4, + "covered_branches": 4, + "covered_lines": 14, + "line_percent": 100.0, + "statements": 14 + }, + "raven/channels/registry.py": { + "branch_percent": 66.666667, + "branches": 6, + "covered_branches": 4, + "covered_lines": 18, + "line_percent": 85.714286, + "statements": 21 + }, + "raven/channels/transcribe.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 11, + "line_percent": 100.0, + "statements": 11 + }, + "raven/cli/_cron_handler.py": { + "branch_percent": 70.0, + "branches": 30, + "covered_branches": 21, + "covered_lines": 78, + "line_percent": 88.636364, + "statements": 88 + }, + "raven/cli/_eval_stack.py": { + "branch_percent": 100.0, + "branches": 2, + "covered_branches": 2, + "covered_lines": 11, + "line_percent": 100.0, + "statements": 11 + }, + "raven/cli/_exit.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 16 + }, + "raven/cli/_gateway_lock.py": { + "branch_percent": 100.0, + "branches": 2, + "covered_branches": 2, + "covered_lines": 52, + "line_percent": 100.0, + "statements": 52 + }, + "raven/cli/_gateway_spine.py": { + "branch_percent": 100.0, + "branches": 12, + "covered_branches": 12, + "covered_lines": 51, + "line_percent": 100.0, + "statements": 51 + }, + "raven/cli/_helpers.py": { + "branch_percent": 75.0, + "branches": 40, + "covered_branches": 30, + "covered_lines": 119, + "line_percent": 87.5, + "statements": 136 + }, + "raven/cli/_hooks_stack.py": { + "branch_percent": 100.0, + "branches": 6, + "covered_branches": 6, + "covered_lines": 16, + "line_percent": 100.0, + "statements": 16 + }, + "raven/cli/_log_file.py": { + "branch_percent": 87.5, + "branches": 16, + "covered_branches": 14, + "covered_lines": 58, + "line_percent": 92.063492, + "statements": 63 + }, + "raven/cli/_log_silence.py": { + "branch_percent": 50.0, + "branches": 2, + "covered_branches": 1, + "covered_lines": 9, + "line_percent": 90.0, + "statements": 10 + }, + "raven/cli/_plugin_stack.py": { + "branch_percent": 91.666667, + "branches": 24, + "covered_branches": 22, + "covered_lines": 65, + "line_percent": 89.041096, + "statements": 73 + }, + "raven/cli/_proactive_stack.py": { + "branch_percent": 26.0, + "branches": 50, + "covered_branches": 13, + "covered_lines": 44, + "line_percent": 26.993865, + "statements": 163 + }, + "raven/cli/_repl_slash.py": { + "branch_percent": 67.741935, + "branches": 62, + "covered_branches": 42, + "covered_lines": 107, + "line_percent": 71.333333, + "statements": 150 + }, + "raven/cli/_repl_spine.py": { + "branch_percent": 90.0, + "branches": 20, + "covered_branches": 18, + "covered_lines": 53, + "line_percent": 96.363636, + "statements": 55 + }, + "raven/cli/_styles.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 4, + "line_percent": 100.0, + "statements": 4 + }, + "raven/cli/_theme.py": { + "branch_percent": 76.315789, + "branches": 38, + "covered_branches": 29, + "covered_lines": 80, + "line_percent": 69.565217, + "statements": 115 + }, + "raven/cli/_token_wise_stack.py": { + "branch_percent": 66.666667, + "branches": 6, + "covered_branches": 4, + "covered_lines": 16, + "line_percent": 100.0, + "statements": 16 + }, + "raven/cli/agent_commands.py": { + "branch_percent": 64.583333, + "branches": 48, + "covered_branches": 31, + "covered_lines": 168, + "line_percent": 81.95122, + "statements": 205 + }, + "raven/cli/channel_commands.py": { + "branch_percent": 82.142857, + "branches": 56, + "covered_branches": 46, + "covered_lines": 201, + "line_percent": 83.057851, + "statements": 242 + }, + "raven/cli/commands.py": { + "branch_percent": 40.0, + "branches": 10, + "covered_branches": 4, + "covered_lines": 46, + "line_percent": 66.666667, + "statements": 69 + }, + "raven/cli/cron_commands.py": { + "branch_percent": 78.125, + "branches": 128, + "covered_branches": 100, + "covered_lines": 337, + "line_percent": 90.106952, + "statements": 374 + }, + "raven/cli/deep_research_commands.py": { + "branch_percent": 89.583333, + "branches": 48, + "covered_branches": 43, + "covered_lines": 132, + "line_percent": 98.507463, + "statements": 134 + }, + "raven/cli/doctor_commands.py": { + "branch_percent": 80.681818, + "branches": 88, + "covered_branches": 71, + "covered_lines": 239, + "line_percent": 95.219124, + "statements": 251 + }, + "raven/cli/gateway_commands.py": { + "branch_percent": 9.090909, + "branches": 66, + "covered_branches": 6, + "covered_lines": 72, + "line_percent": 30.508475, + "statements": 236 + }, + "raven/cli/import_commands.py": { + "branch_percent": 79.054054, + "branches": 148, + "covered_branches": 117, + "covered_lines": 406, + "line_percent": 87.124464, + "statements": 466 + }, + "raven/cli/onboard_channels.py": { + "branch_percent": 34.615385, + "branches": 78, + "covered_branches": 27, + "covered_lines": 122, + "line_percent": 54.464286, + "statements": 224 + }, + "raven/cli/onboard_commands.py": { + "branch_percent": 66.326531, + "branches": 392, + "covered_branches": 260, + "covered_lines": 791, + "line_percent": 78.162055, + "statements": 1012 + }, + "raven/cli/onboard_everos.py": { + "branch_percent": 43.442623, + "branches": 244, + "covered_branches": 106, + "covered_lines": 300, + "line_percent": 54.644809, + "statements": 549 + }, + "raven/cli/plugin_commands.py": { + "branch_percent": 67.857143, + "branches": 28, + "covered_branches": 19, + "covered_lines": 77, + "line_percent": 92.771084, + "statements": 83 + }, + "raven/cli/provider_commands.py": { + "branch_percent": 84.375, + "branches": 96, + "covered_branches": 81, + "covered_lines": 369, + "line_percent": 89.346247, + "statements": 413 + }, + "raven/cli/sandbox_commands.py": { + "branch_percent": 50.0, + "branches": 40, + "covered_branches": 20, + "covered_lines": 147, + "line_percent": 53.068592, + "statements": 277 + }, + "raven/cli/sentinel_commands.py": { + "branch_percent": 55.769231, + "branches": 156, + "covered_branches": 87, + "covered_lines": 298, + "line_percent": 59.719439, + "statements": 499 + }, + "raven/cli/session_commands.py": { + "branch_percent": 94.117647, + "branches": 34, + "covered_branches": 32, + "covered_lines": 133, + "line_percent": 96.376812, + "statements": 138 + }, + "raven/cli/skill_commands.py": { + "branch_percent": 92.857143, + "branches": 14, + "covered_branches": 13, + "covered_lines": 42, + "line_percent": 87.5, + "statements": 48 + }, + "raven/cli/status_commands.py": { + "branch_percent": 100.0, + "branches": 8, + "covered_branches": 8, + "covered_lines": 31, + "line_percent": 100.0, + "statements": 31 + }, + "raven/cli/tracing_commands.py": { + "branch_percent": 18.181818, + "branches": 22, + "covered_branches": 4, + "covered_lines": 38, + "line_percent": 41.758242, + "statements": 91 + }, + "raven/cli/tui_commands.py": { + "branch_percent": 74.637681, + "branches": 138, + "covered_branches": 103, + "covered_lines": 365, + "line_percent": 81.111111, + "statements": 450 + }, + "raven/cli/update_notice.py": { + "branch_percent": 92.307692, + "branches": 26, + "covered_branches": 24, + "covered_lines": 86, + "line_percent": 94.505495, + "statements": 91 + }, + "raven/cli/upgrade_commands.py": { + "branch_percent": 87.254902, + "branches": 102, + "covered_branches": 89, + "covered_lines": 274, + "line_percent": 93.197279, + "statements": 294 + }, + "raven/config/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 5, + "line_percent": 100.0, + "statements": 5 + }, + "raven/config/loader.py": { + "branch_percent": 88.709677, + "branches": 62, + "covered_branches": 55, + "covered_lines": 110, + "line_percent": 94.017094, + "statements": 117 + }, + "raven/config/paths.py": { + "branch_percent": 100.0, + "branches": 6, + "covered_branches": 6, + "covered_lines": 35, + "line_percent": 87.5, + "statements": 40 + }, + "raven/config/raven.py": { + "branch_percent": 77.272727, + "branches": 44, + "covered_branches": 34, + "covered_lines": 432, + "line_percent": 97.737557, + "statements": 442 + }, + "raven/config/schema.py": { + "branch_percent": 86.111111, + "branches": 72, + "covered_branches": 62, + "covered_lines": 461, + "line_percent": 97.876858, + "statements": 471 + }, + "raven/config/update.py": { + "branch_percent": 87.5, + "branches": 24, + "covered_branches": 21, + "covered_lines": 141, + "line_percent": 99.295775, + "statements": 142 + }, + "raven/config/update_channels.py": { + "branch_percent": 84.52381, + "branches": 84, + "covered_branches": 71, + "covered_lines": 206, + "line_percent": 87.288136, + "statements": 236 + }, + "raven/config/update_everos.py": { + "branch_percent": 85.714286, + "branches": 14, + "covered_branches": 12, + "covered_lines": 61, + "line_percent": 91.044776, + "statements": 67 + }, + "raven/config/update_providers.py": { + "branch_percent": 82.038835, + "branches": 206, + "covered_branches": 169, + "covered_lines": 538, + "line_percent": 87.479675, + "statements": 615 + }, + "raven/config/update_tools.py": { + "branch_percent": 100.0, + "branches": 2, + "covered_branches": 2, + "covered_lines": 46, + "line_percent": 100.0, + "statements": 46 + }, + "raven/context_engine/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 6, + "line_percent": 100.0, + "statements": 6 + }, + "raven/context_engine/assembler.py": { + "branch_percent": 91.176471, + "branches": 34, + "covered_branches": 31, + "covered_lines": 85, + "line_percent": 97.701149, + "statements": 87 + }, + "raven/context_engine/base.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 46, + "line_percent": 95.833333, + "statements": 48 + }, + "raven/context_engine/curator.py": { + "branch_percent": 39.285714, + "branches": 84, + "covered_branches": 33, + "covered_lines": 349, + "line_percent": 75.215517, + "statements": 464 + }, + "raven/context_engine/factory.py": { + "branch_percent": 88.888889, + "branches": 18, + "covered_branches": 16, + "covered_lines": 51, + "line_percent": 100.0, + "statements": 51 + }, + "raven/context_engine/history_trimmer.py": { + "branch_percent": 34.615385, + "branches": 52, + "covered_branches": 18, + "covered_lines": 75, + "line_percent": 68.807339, + "statements": 109 + }, + "raven/context_engine/segments/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 6, + "line_percent": 100.0, + "statements": 6 + }, + "raven/context_engine/segments/active_skills.py": { + "branch_percent": 25.0, + "branches": 4, + "covered_branches": 1, + "covered_lines": 15, + "line_percent": 71.428571, + "statements": 21 + }, + "raven/context_engine/segments/bootstrap.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 14, + "line_percent": 100.0, + "statements": 14 + }, + "raven/context_engine/segments/curator.py": { + "branch_percent": 76.666667, + "branches": 30, + "covered_branches": 23, + "covered_lines": 114, + "line_percent": 92.682927, + "statements": 123 + }, + "raven/context_engine/segments/identity.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 12, + "line_percent": 100.0, + "statements": 12 + }, + "raven/context_engine/segments/memory.py": { + "branch_percent": 100.0, + "branches": 4, + "covered_branches": 4, + "covered_lines": 28, + "line_percent": 100.0, + "statements": 28 + }, + "raven/context_engine/segments/render.py": { + "branch_percent": 93.75, + "branches": 48, + "covered_branches": 45, + "covered_lines": 133, + "line_percent": 97.080292, + "statements": 137 + }, + "raven/context_engine/segments/skills.py": { + "branch_percent": 81.578947, + "branches": 38, + "covered_branches": 31, + "covered_lines": 98, + "line_percent": 90.740741, + "statements": 108 + }, + "raven/eval_engine/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 7, + "line_percent": 100.0, + "statements": 7 + }, + "raven/eval_engine/adapter/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 2, + "line_percent": 100.0, + "statements": 2 + }, + "raven/eval_engine/adapter/adapter.py": { + "branch_percent": 100.0, + "branches": 2, + "covered_branches": 2, + "covered_lines": 22, + "line_percent": 100.0, + "statements": 22 + }, + "raven/eval_engine/config.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 22, + "line_percent": 100.0, + "statements": 22 + }, + "raven/eval_engine/engine.py": { + "branch_percent": 50.0, + "branches": 2, + "covered_branches": 1, + "covered_lines": 32, + "line_percent": 91.428571, + "statements": 35 + }, + "raven/eval_engine/hooks/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 4, + "line_percent": 100.0, + "statements": 4 + }, + "raven/eval_engine/hooks/after_iteration_hook.py": { + "branch_percent": 81.818182, + "branches": 22, + "covered_branches": 18, + "covered_lines": 50, + "line_percent": 90.909091, + "statements": 55 + }, + "raven/eval_engine/hooks/before_iteration_hook.py": { + "branch_percent": 100.0, + "branches": 6, + "covered_branches": 6, + "covered_lines": 27, + "line_percent": 90.0, + "statements": 30 + }, + "raven/eval_engine/hooks/tool_audit_hook.py": { + "branch_percent": 87.5, + "branches": 16, + "covered_branches": 14, + "covered_lines": 36, + "line_percent": 94.736842, + "statements": 38 + }, + "raven/eval_engine/judge/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 2, + "line_percent": 100.0, + "statements": 2 + }, + "raven/eval_engine/judge/judge.py": { + "branch_percent": 100.0, + "branches": 4, + "covered_branches": 4, + "covered_lines": 32, + "line_percent": 100.0, + "statements": 32 + }, + "raven/eval_engine/prompts/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 3, + "line_percent": 100.0, + "statements": 3 + }, + "raven/eval_engine/prompts/task_completion.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 2, + "line_percent": 100.0, + "statements": 2 + }, + "raven/eval_engine/prompts/tool_safety.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 2, + "line_percent": 100.0, + "statements": 2 + }, + "raven/evolver/activation/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 6, + "line_percent": 100.0, + "statements": 6 + }, + "raven/evolver/activation/audit.py": { + "branch_percent": 0.0, + "branches": 18, + "covered_branches": 0, + "covered_lines": 6, + "line_percent": 17.647059, + "statements": 34 + }, + "raven/evolver/activation/chamber.py": { + "branch_percent": 0.0, + "branches": 8, + "covered_branches": 0, + "covered_lines": 22, + "line_percent": 51.162791, + "statements": 43 + }, + "raven/evolver/activation/ledger.py": { + "branch_percent": 0.0, + "branches": 18, + "covered_branches": 0, + "covered_lines": 20, + "line_percent": 33.898305, + "statements": 59 + }, + "raven/evolver/activation/predicates.py": { + "branch_percent": 0.0, + "branches": 14, + "covered_branches": 0, + "covered_lines": 10, + "line_percent": 27.027027, + "statements": 37 + }, + "raven/evolver/activation/routing_query.py": { + "branch_percent": 0.0, + "branches": 6, + "covered_branches": 0, + "covered_lines": 6, + "line_percent": 22.222222, + "statements": 27 + }, + "raven/evolver/activation/spec.py": { + "branch_percent": 0.0, + "branches": 94, + "covered_branches": 0, + "covered_lines": 22, + "line_percent": 13.414634, + "statements": 164 + }, + "raven/evolver/analysis/failure_map_builder.py": { + "branch_percent": 0.0, + "branches": 10, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 59 + }, + "raven/evolver/analysis/proxy_features.py": { + "branch_percent": 0.0, + "branches": 52, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 157 + }, + "raven/evolver/analysis/stability_bucket.py": { + "branch_percent": 0.0, + "branches": 30, + "covered_branches": 0, + "covered_lines": 26, + "line_percent": 29.885057, + "statements": 87 + }, + "raven/evolver/analysis/trial_pool.py": { + "branch_percent": 0.0, + "branches": 10, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 28 + }, + "raven/evolver/applier/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 3, + "line_percent": 100.0, + "statements": 3 + }, + "raven/evolver/applier/beacon_guard.py": { + "branch_percent": 0.0, + "branches": 4, + "covered_branches": 0, + "covered_lines": 6, + "line_percent": 60.0, + "statements": 10 + }, + "raven/evolver/applier/path_guard.py": { + "branch_percent": 57.142857, + "branches": 14, + "covered_branches": 8, + "covered_lines": 24, + "line_percent": 75.0, + "statements": 32 + }, + "raven/evolver/cli.py": { + "branch_percent": 0.0, + "branches": 8, + "covered_branches": 0, + "covered_lines": 21, + "line_percent": 65.625, + "statements": 32 + }, + "raven/evolver/compressor/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 2 + }, + "raven/evolver/compressor/trajectory.py": { + "branch_percent": 0.0, + "branches": 108, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 232 + }, + "raven/evolver/judge/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 5, + "line_percent": 100.0, + "statements": 5 + }, + "raven/evolver/judge/llm_client.py": { + "branch_percent": 0.0, + "branches": 46, + "covered_branches": 0, + "covered_lines": 51, + "line_percent": 29.651163, + "statements": 172 + }, + "raven/evolver/judge/parser.py": { + "branch_percent": 0.0, + "branches": 86, + "covered_branches": 0, + "covered_lines": 15, + "line_percent": 9.868421, + "statements": 152 + }, + "raven/evolver/judge/prompts.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 14, + "line_percent": 77.777778, + "statements": 18 + }, + "raven/evolver/judge/schema.py": { + "branch_percent": 0.0, + "branches": 32, + "covered_branches": 0, + "covered_lines": 80, + "line_percent": 68.376068, + "statements": 117 + }, + "raven/evolver/launch/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 3, + "line_percent": 100.0, + "statements": 3 + }, + "raven/evolver/launch/config.py": { + "branch_percent": 79.545455, + "branches": 44, + "covered_branches": 35, + "covered_lines": 95, + "line_percent": 87.962963, + "statements": 108 + }, + "raven/evolver/launch/contract.py": { + "branch_percent": 83.333333, + "branches": 6, + "covered_branches": 5, + "covered_lines": 34, + "line_percent": 94.444444, + "statements": 36 + }, + "raven/evolver/launch/models.py": { + "branch_percent": 21.428571, + "branches": 28, + "covered_branches": 6, + "covered_lines": 35, + "line_percent": 45.454545, + "statements": 77 + }, + "raven/evolver/launch/registry.py": { + "branch_percent": 66.666667, + "branches": 6, + "covered_branches": 4, + "covered_lines": 16, + "line_percent": 94.117647, + "statements": 17 + }, + "raven/evolver/launch/runner.py": { + "branch_percent": 12.121212, + "branches": 66, + "covered_branches": 8, + "covered_lines": 62, + "line_percent": 27.678571, + "statements": 224 + }, + "raven/evolver/launch/state.py": { + "branch_percent": 100.0, + "branches": 2, + "covered_branches": 2, + "covered_lines": 59, + "line_percent": 90.769231, + "statements": 65 + }, + "raven/evolver/orchestrator/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 1, + "line_percent": 100.0, + "statements": 1 + }, + "raven/evolver/orchestrator/archive.py": { + "branch_percent": 0.0, + "branches": 50, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 166 + }, + "raven/evolver/orchestrator/config.py": { + "branch_percent": 66.666667, + "branches": 6, + "covered_branches": 4, + "covered_lines": 50, + "line_percent": 89.285714, + "statements": 56 + }, + "raven/evolver/orchestrator/gates/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 4, + "line_percent": 100.0, + "statements": 4 + }, + "raven/evolver/orchestrator/gates/fisher.py": { + "branch_percent": 91.666667, + "branches": 12, + "covered_branches": 11, + "covered_lines": 32, + "line_percent": 96.969697, + "statements": 33 + }, + "raven/evolver/orchestrator/gates/paired.py": { + "branch_percent": 100.0, + "branches": 4, + "covered_branches": 4, + "covered_lines": 35, + "line_percent": 100.0, + "statements": 35 + }, + "raven/evolver/orchestrator/gates/pipeline.py": { + "branch_percent": 100.0, + "branches": 4, + "covered_branches": 4, + "covered_lines": 25, + "line_percent": 100.0, + "statements": 25 + }, + "raven/evolver/orchestrator/gates/policy.py": { + "branch_percent": 0.0, + "branches": 6, + "covered_branches": 0, + "covered_lines": 55, + "line_percent": 63.218391, + "statements": 87 + }, + "raven/evolver/orchestrator/gates/strategies.py": { + "branch_percent": 83.333333, + "branches": 18, + "covered_branches": 15, + "covered_lines": 87, + "line_percent": 97.752809, + "statements": 89 + }, + "raven/evolver/orchestrator/loop.py": { + "branch_percent": 0.0, + "branches": 102, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 316 + }, + "raven/evolver/orchestrator/nodes/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 1, + "line_percent": 100.0, + "statements": 1 + }, + "raven/evolver/orchestrator/nodes/design.py": { + "branch_percent": 0.0, + "branches": 18, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 62 + }, + "raven/evolver/orchestrator/nodes/diagnose.py": { + "branch_percent": 0.0, + "branches": 20, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 54 + }, + "raven/evolver/orchestrator/nodes/screen.py": { + "branch_percent": 83.333333, + "branches": 6, + "covered_branches": 5, + "covered_lines": 31, + "line_percent": 93.939394, + "statements": 33 + }, + "raven/evolver/orchestrator/nodes/semantic.py": { + "branch_percent": 0.0, + "branches": 4, + "covered_branches": 0, + "covered_lines": 20, + "line_percent": 51.282051, + "statements": 39 + }, + "raven/evolver/orchestrator/nodes/taxonomy.py": { + "branch_percent": 0.0, + "branches": 82, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 229 + }, + "raven/evolver/orchestrator/nodes/verdict.py": { + "branch_percent": 0.0, + "branches": 6, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 28 + }, + "raven/evolver/orchestrator/production.py": { + "branch_percent": 0.0, + "branches": 46, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 234 + }, + "raven/evolver/orchestrator/providers/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 1, + "line_percent": 100.0, + "statements": 1 + }, + "raven/evolver/orchestrator/providers/claude_agentic.py": { + "branch_percent": 0.0, + "branches": 14, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 37 + }, + "raven/evolver/orchestrator/providers/claude_cli.py": { + "branch_percent": 93.75, + "branches": 16, + "covered_branches": 15, + "covered_lines": 56, + "line_percent": 100.0, + "statements": 56 + }, + "raven/evolver/orchestrator/providers/openai_compat.py": { + "branch_percent": 0.0, + "branches": 6, + "covered_branches": 0, + "covered_lines": 13, + "line_percent": 33.333333, + "statements": 39 + }, + "raven/evolver/orchestrator/scoring.py": { + "branch_percent": 100.0, + "branches": 24, + "covered_branches": 24, + "covered_lines": 71, + "line_percent": 100.0, + "statements": 71 + }, + "raven/evolver/orchestrator/sealed/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 1 + }, + "raven/evolver/orchestrator/sealed/runner.py": { + "branch_percent": 0.0, + "branches": 20, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 100 + }, + "raven/evolver/orchestrator/state/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 1 + }, + "raven/evolver/orchestrator/state/journal.py": { + "branch_percent": 0.0, + "branches": 6, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 32 + }, + "raven/evolver/orchestrator/termination.py": { + "branch_percent": 0.0, + "branches": 16, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 35 + }, + "raven/evolver/scheduler/anchor_selection.py": { + "branch_percent": 0.0, + "branches": 18, + "covered_branches": 0, + "covered_lines": 25, + "line_percent": 35.211268, + "statements": 71 + }, + "raven/evolver/scheduler/bandit_tasks.py": { + "branch_percent": 0.0, + "branches": 34, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 86 + }, + "raven/evolver/scheduler/branching_policy.py": { + "branch_percent": 0.0, + "branches": 34, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 79 + }, + "raven/evolver/scheduler/cold_start_bandit.py": { + "branch_percent": 0.0, + "branches": 60, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 157 + }, + "raven/evolver/scheduler/tree_aware_bandit.py": { + "branch_percent": 0.0, + "branches": 60, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 134 + }, + "raven/evolver/scheduler/tree_loader.py": { + "branch_percent": 0.0, + "branches": 74, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 143 + }, + "raven/evolver/tree/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 4, + "line_percent": 100.0, + "statements": 4 + }, + "raven/evolver/tree/git_ops.py": { + "branch_percent": 50.0, + "branches": 26, + "covered_branches": 13, + "covered_lines": 89, + "line_percent": 70.634921, + "statements": 126 + }, + "raven/evolver/tree/node.py": { + "branch_percent": 8.333333, + "branches": 60, + "covered_branches": 5, + "covered_lines": 137, + "line_percent": 56.61157, + "statements": 242 + }, + "raven/evolver/tree/store.py": { + "branch_percent": 0.0, + "branches": 32, + "covered_branches": 0, + "covered_lines": 29, + "line_percent": 28.431373, + "statements": 102 + }, + "raven/importer/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 6, + "line_percent": 100.0, + "statements": 6 + }, + "raven/importer/hermes_user_md.py": { + "branch_percent": 100.0, + "branches": 16, + "covered_branches": 16, + "covered_lines": 60, + "line_percent": 100.0, + "statements": 60 + }, + "raven/importer/orchestrator.py": { + "branch_percent": 96.875, + "branches": 32, + "covered_branches": 31, + "covered_lines": 112, + "line_percent": 100.0, + "statements": 112 + }, + "raven/importer/scanners/__init__.py": { + "branch_percent": 91.666667, + "branches": 12, + "covered_branches": 11, + "covered_lines": 32, + "line_percent": 96.969697, + "statements": 33 + }, + "raven/importer/scanners/claude_code.py": { + "branch_percent": 86.27451, + "branches": 102, + "covered_branches": 88, + "covered_lines": 244, + "line_percent": 89.051095, + "statements": 274 + }, + "raven/importer/scanners/hermes.py": { + "branch_percent": 91.860465, + "branches": 86, + "covered_branches": 79, + "covered_lines": 267, + "line_percent": 96.043165, + "statements": 278 + }, + "raven/importer/skills/__init__.py": { + "branch_percent": 100.0, + "branches": 4, + "covered_branches": 4, + "covered_lines": 34, + "line_percent": 100.0, + "statements": 34 + }, + "raven/importer/skills/hermes.py": { + "branch_percent": 96.875, + "branches": 32, + "covered_branches": 31, + "covered_lines": 100, + "line_percent": 97.087379, + "statements": 103 + }, + "raven/importer/skills/installer.py": { + "branch_percent": 83.333333, + "branches": 18, + "covered_branches": 15, + "covered_lines": 75, + "line_percent": 92.592593, + "statements": 81 + }, + "raven/importer/state.py": { + "branch_percent": 83.333333, + "branches": 6, + "covered_branches": 5, + "covered_lines": 63, + "line_percent": 98.4375, + "statements": 64 + }, + "raven/importer/types.py": { + "branch_percent": 100.0, + "branches": 2, + "covered_branches": 2, + "covered_lines": 46, + "line_percent": 100.0, + "statements": 46 + }, + "raven/memory_engine/__init__.py": { + "branch_percent": 100.0, + "branches": 2, + "covered_branches": 2, + "covered_lines": 9, + "line_percent": 100.0, + "statements": 9 + }, + "raven/memory_engine/backend.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 19, + "line_percent": 100.0, + "statements": 19 + }, + "raven/memory_engine/base.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 23, + "line_percent": 100.0, + "statements": 23 + }, + "raven/memory_engine/consolidate/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 2, + "line_percent": 100.0, + "statements": 2 + }, + "raven/memory_engine/consolidate/attention.py": { + "branch_percent": 90.0, + "branches": 20, + "covered_branches": 18, + "covered_lines": 48, + "line_percent": 97.959184, + "statements": 49 + }, + "raven/memory_engine/consolidate/behaviors.py": { + "branch_percent": 77.777778, + "branches": 54, + "covered_branches": 42, + "covered_lines": 127, + "line_percent": 90.714286, + "statements": 140 + }, + "raven/memory_engine/consolidate/behaviors_extractor.py": { + "branch_percent": 64.772727, + "branches": 88, + "covered_branches": 57, + "covered_lines": 216, + "line_percent": 81.203008, + "statements": 266 + }, + "raven/memory_engine/consolidate/consolidator.py": { + "branch_percent": 80.859375, + "branches": 256, + "covered_branches": 207, + "covered_lines": 573, + "line_percent": 88.69969, + "statements": 646 + }, + "raven/memory_engine/contract_test.py": { + "branch_percent": 50.0, + "branches": 2, + "covered_branches": 1, + "covered_lines": 46, + "line_percent": 88.461538, + "statements": 52 + }, + "raven/memory_engine/skill_forge/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 12, + "line_percent": 100.0, + "statements": 12 + }, + "raven/memory_engine/skill_forge/catalog.py": { + "branch_percent": 61.111111, + "branches": 72, + "covered_branches": 44, + "covered_lines": 131, + "line_percent": 75.287356, + "statements": 174 + }, + "raven/memory_engine/skill_forge/everos_source.py": { + "branch_percent": 66.666667, + "branches": 6, + "covered_branches": 4, + "covered_lines": 29, + "line_percent": 96.666667, + "statements": 30 + }, + "raven/memory_engine/skill_forge/fusion.py": { + "branch_percent": 100.0, + "branches": 8, + "covered_branches": 8, + "covered_lines": 25, + "line_percent": 100.0, + "statements": 25 + }, + "raven/memory_engine/skill_forge/gate.py": { + "branch_percent": 73.529412, + "branches": 34, + "covered_branches": 25, + "covered_lines": 100, + "line_percent": 87.719298, + "statements": 114 + }, + "raven/memory_engine/skill_forge/hub_source.py": { + "branch_percent": 100.0, + "branches": 6, + "covered_branches": 6, + "covered_lines": 28, + "line_percent": 100.0, + "statements": 28 + }, + "raven/memory_engine/skill_forge/local_source.py": { + "branch_percent": 83.333333, + "branches": 6, + "covered_branches": 5, + "covered_lines": 25, + "line_percent": 100.0, + "statements": 25 + }, + "raven/memory_engine/skill_forge/refs.py": { + "branch_percent": 100.0, + "branches": 14, + "covered_branches": 14, + "covered_lines": 43, + "line_percent": 100.0, + "statements": 43 + }, + "raven/memory_engine/skill_forge/rewriter.py": { + "branch_percent": 78.571429, + "branches": 14, + "covered_branches": 11, + "covered_lines": 61, + "line_percent": 96.825397, + "statements": 63 + }, + "raven/memory_engine/skill_forge/router.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 23, + "line_percent": 100.0, + "statements": 23 + }, + "raven/memory_engine/skill_forge/types.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 23, + "line_percent": 100.0, + "statements": 23 + }, + "raven/memory_engine/skill_local/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 4, + "line_percent": 100.0, + "statements": 4 + }, + "raven/memory_engine/skill_local/local_pool.py": { + "branch_percent": 100.0, + "branches": 6, + "covered_branches": 6, + "covered_lines": 41, + "line_percent": 100.0, + "statements": 41 + }, + "raven/memory_engine/skill_local/registry.py": { + "branch_percent": 75.0, + "branches": 128, + "covered_branches": 96, + "covered_lines": 237, + "line_percent": 85.559567, + "statements": 277 + }, + "raven/memory_engine/skill_local/types.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 38, + "line_percent": 100.0, + "statements": 38 + }, + "raven/memory_engine/skill_local/watcher.py": { + "branch_percent": 14.285714, + "branches": 14, + "covered_branches": 2, + "covered_lines": 30, + "line_percent": 56.603774, + "statements": 53 + }, + "raven/plugin/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 7, + "line_percent": 100.0, + "statements": 7 + }, + "raven/plugin/bootstrap.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 10, + "line_percent": 100.0, + "statements": 10 + }, + "raven/plugin/context.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 19, + "line_percent": 100.0, + "statements": 19 + }, + "raven/plugin/discover.py": { + "branch_percent": 83.333333, + "branches": 30, + "covered_branches": 25, + "covered_lines": 75, + "line_percent": 82.417582, + "statements": 91 + }, + "raven/plugin/manifest.py": { + "branch_percent": 100.0, + "branches": 10, + "covered_branches": 10, + "covered_lines": 62, + "line_percent": 100.0, + "statements": 62 + }, + "raven/plugin/memory/everos/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 1, + "line_percent": 100.0, + "statements": 1 + }, + "raven/plugin/memory/everos/_health.py": { + "branch_percent": 83.333333, + "branches": 6, + "covered_branches": 5, + "covered_lines": 34, + "line_percent": 79.069767, + "statements": 43 + }, + "raven/plugin/memory/everos/_server.py": { + "branch_percent": 75.0, + "branches": 8, + "covered_branches": 6, + "covered_lines": 38, + "line_percent": 66.666667, + "statements": 57 + }, + "raven/plugin/memory/everos/backend.py": { + "branch_percent": 86.111111, + "branches": 108, + "covered_branches": 93, + "covered_lines": 254, + "line_percent": 91.366906, + "statements": 278 + }, + "raven/plugin/memory/everos/multimodal.py": { + "branch_percent": 85.714286, + "branches": 14, + "covered_branches": 12, + "covered_lines": 65, + "line_percent": 90.277778, + "statements": 72 + }, + "raven/plugin/memory/everos/tools.py": { + "branch_percent": 90.0, + "branches": 10, + "covered_branches": 9, + "covered_lines": 45, + "line_percent": 91.836735, + "statements": 49 + }, + "raven/plugin/registry.py": { + "branch_percent": 90.909091, + "branches": 22, + "covered_branches": 20, + "covered_lines": 112, + "line_percent": 99.115044, + "statements": 113 + }, + "raven/proactive_engine/schedulers/cron/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 3, + "line_percent": 100.0, + "statements": 3 + }, + "raven/proactive_engine/schedulers/cron/service.py": { + "branch_percent": 67.613636, + "branches": 176, + "covered_branches": 119, + "covered_lines": 332, + "line_percent": 78.859857, + "statements": 421 + }, + "raven/proactive_engine/schedulers/cron/tool.py": { + "branch_percent": 62.5, + "branches": 40, + "covered_branches": 25, + "covered_lines": 83, + "line_percent": 76.851852, + "statements": 108 + }, + "raven/proactive_engine/schedulers/cron/types.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 42, + "line_percent": 100.0, + "statements": 42 + }, + "raven/proactive_engine/schedulers/heartbeat/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 2, + "line_percent": 100.0, + "statements": 2 + }, + "raven/proactive_engine/schedulers/heartbeat/service.py": { + "branch_percent": 72.222222, + "branches": 36, + "covered_branches": 26, + "covered_lines": 106, + "line_percent": 87.603306, + "statements": 121 + }, + "raven/proactive_engine/sentinel/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 13, + "line_percent": 100.0, + "statements": 13 + }, + "raven/proactive_engine/sentinel/attention_producers/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 15, + "line_percent": 100.0, + "statements": 15 + }, + "raven/proactive_engine/sentinel/attention_producers/_base.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 12, + "line_percent": 100.0, + "statements": 12 + }, + "raven/proactive_engine/sentinel/attention_producers/active_threads.py": { + "branch_percent": 66.666667, + "branches": 6, + "covered_branches": 4, + "covered_lines": 24, + "line_percent": 100.0, + "statements": 24 + }, + "raven/proactive_engine/sentinel/attention_producers/archived_patterns.py": { + "branch_percent": 83.333333, + "branches": 6, + "covered_branches": 5, + "covered_lines": 21, + "line_percent": 100.0, + "statements": 21 + }, + "raven/proactive_engine/sentinel/attention_producers/behavior_patterns.py": { + "branch_percent": 83.333333, + "branches": 12, + "covered_branches": 10, + "covered_lines": 29, + "line_percent": 93.548387, + "statements": 31 + }, + "raven/proactive_engine/sentinel/attention_producers/currently_focused.py": { + "branch_percent": 87.5, + "branches": 48, + "covered_branches": 42, + "covered_lines": 99, + "line_percent": 86.842105, + "statements": 114 + }, + "raven/proactive_engine/sentinel/attention_producers/daily_plan.py": { + "branch_percent": 47.916667, + "branches": 48, + "covered_branches": 23, + "covered_lines": 95, + "line_percent": 63.758389, + "statements": 149 + }, + "raven/proactive_engine/sentinel/attention_producers/pending_proposals.py": { + "branch_percent": 100.0, + "branches": 4, + "covered_branches": 4, + "covered_lines": 23, + "line_percent": 100.0, + "statements": 23 + }, + "raven/proactive_engine/sentinel/attention_producers/predicted_3d.py": { + "branch_percent": 83.333333, + "branches": 12, + "covered_branches": 10, + "covered_lines": 32, + "line_percent": 84.210526, + "statements": 38 + }, + "raven/proactive_engine/sentinel/attention_producers/project_rhythm.py": { + "branch_percent": 88.888889, + "branches": 18, + "covered_branches": 16, + "covered_lines": 47, + "line_percent": 92.156863, + "statements": 51 + }, + "raven/proactive_engine/sentinel/attention_producers/recent_decisions.py": { + "branch_percent": 75.0, + "branches": 20, + "covered_branches": 15, + "covered_lines": 50, + "line_percent": 81.967213, + "statements": 61 + }, + "raven/proactive_engine/sentinel/attention_producers/recently_abandoned.py": { + "branch_percent": 75.0, + "branches": 12, + "covered_branches": 9, + "covered_lines": 29, + "line_percent": 85.294118, + "statements": 34 + }, + "raven/proactive_engine/sentinel/attention_producers/rejected_cooldown.py": { + "branch_percent": 100.0, + "branches": 4, + "covered_branches": 4, + "covered_lines": 27, + "line_percent": 100.0, + "statements": 27 + }, + "raven/proactive_engine/sentinel/attention_producers/sentinel_observations.py": { + "branch_percent": 79.6875, + "branches": 64, + "covered_branches": 51, + "covered_lines": 129, + "line_percent": 88.356164, + "statements": 146 + }, + "raven/proactive_engine/sentinel/attention_producers/stance_log.py": { + "branch_percent": 70.588235, + "branches": 34, + "covered_branches": 24, + "covered_lines": 84, + "line_percent": 85.714286, + "statements": 98 + }, + "raven/proactive_engine/sentinel/attention_updater.py": { + "branch_percent": 80.0, + "branches": 10, + "covered_branches": 8, + "covered_lines": 46, + "line_percent": 80.701754, + "statements": 57 + }, + "raven/proactive_engine/sentinel/discover_triggers.py": { + "branch_percent": 100.0, + "branches": 6, + "covered_branches": 6, + "covered_lines": 57, + "line_percent": 91.935484, + "statements": 62 + }, + "raven/proactive_engine/sentinel/executor/action_executor.py": { + "branch_percent": 96.875, + "branches": 32, + "covered_branches": 31, + "covered_lines": 98, + "line_percent": 95.145631, + "statements": 103 + }, + "raven/proactive_engine/sentinel/executor/decision_consumer.py": { + "branch_percent": 60.416667, + "branches": 48, + "covered_branches": 29, + "covered_lines": 91, + "line_percent": 74.590164, + "statements": 122 + }, + "raven/proactive_engine/sentinel/executor/decision_router.py": { + "branch_percent": 76.5625, + "branches": 64, + "covered_branches": 49, + "covered_lines": 138, + "line_percent": 89.032258, + "statements": 155 + }, + "raven/proactive_engine/sentinel/executor/defer_manager.py": { + "branch_percent": 83.333333, + "branches": 48, + "covered_branches": 40, + "covered_lines": 175, + "line_percent": 91.623037, + "statements": 191 + }, + "raven/proactive_engine/sentinel/executor/dispatcher.py": { + "branch_percent": 95.454545, + "branches": 22, + "covered_branches": 21, + "covered_lines": 68, + "line_percent": 97.142857, + "statements": 70 + }, + "raven/proactive_engine/sentinel/executor/injector.py": { + "branch_percent": 87.5, + "branches": 32, + "covered_branches": 28, + "covered_lines": 103, + "line_percent": 96.261682, + "statements": 107 + }, + "raven/proactive_engine/sentinel/executor/pending_decision.py": { + "branch_percent": 94.0, + "branches": 50, + "covered_branches": 47, + "covered_lines": 166, + "line_percent": 91.208791, + "statements": 182 + }, + "raven/proactive_engine/sentinel/executor/runner.py": { + "branch_percent": 65.151515, + "branches": 264, + "covered_branches": 172, + "covered_lines": 525, + "line_percent": 74.89301, + "statements": 701 + }, + "raven/proactive_engine/sentinel/executor/spawn.py": { + "branch_percent": 100.0, + "branches": 6, + "covered_branches": 6, + "covered_lines": 38, + "line_percent": 100.0, + "statements": 38 + }, + "raven/proactive_engine/sentinel/feedback/persistence.py": { + "branch_percent": 50.0, + "branches": 4, + "covered_branches": 2, + "covered_lines": 35, + "line_percent": 92.105263, + "statements": 38 + }, + "raven/proactive_engine/sentinel/feedback/tracker.py": { + "branch_percent": 82.727273, + "branches": 110, + "covered_branches": 91, + "covered_lines": 226, + "line_percent": 88.28125, + "statements": 256 + }, + "raven/proactive_engine/sentinel/planner.py": { + "branch_percent": 95.0, + "branches": 20, + "covered_branches": 19, + "covered_lines": 69, + "line_percent": 94.520548, + "statements": 73 + }, + "raven/proactive_engine/sentinel/predictor/context_assembler.py": { + "branch_percent": 84.782609, + "branches": 92, + "covered_branches": 78, + "covered_lines": 226, + "line_percent": 85.931559, + "statements": 263 + }, + "raven/proactive_engine/sentinel/predictor/daily_analysis.py": { + "branch_percent": 72.826087, + "branches": 92, + "covered_branches": 67, + "covered_lines": 199, + "line_percent": 82.916667, + "statements": 240 + }, + "raven/proactive_engine/sentinel/predictor/prompts.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 11, + "line_percent": 100.0, + "statements": 11 + }, + "raven/proactive_engine/sentinel/predictor/routine_aggregator.py": { + "branch_percent": 79.166667, + "branches": 24, + "covered_branches": 19, + "covered_lines": 71, + "line_percent": 95.945946, + "statements": 74 + }, + "raven/proactive_engine/sentinel/predictor/routine_learner.py": { + "branch_percent": 95.833333, + "branches": 72, + "covered_branches": 69, + "covered_lines": 195, + "line_percent": 97.5, + "statements": 200 + }, + "raven/proactive_engine/sentinel/predictor/routine_store.py": { + "branch_percent": 89.655172, + "branches": 58, + "covered_branches": 52, + "covered_lines": 162, + "line_percent": 94.736842, + "statements": 171 + }, + "raven/proactive_engine/sentinel/predictor/routine_validator.py": { + "branch_percent": 85.714286, + "branches": 14, + "covered_branches": 12, + "covered_lines": 55, + "line_percent": 100.0, + "statements": 55 + }, + "raven/proactive_engine/sentinel/predictor/task_discoverer.py": { + "branch_percent": 75.0, + "branches": 100, + "covered_branches": 75, + "covered_lines": 236, + "line_percent": 78.666667, + "statements": 300 + }, + "raven/proactive_engine/sentinel/tools/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 2 + }, + "raven/proactive_engine/sentinel/tools/nudge_feedback_tool.py": { + "branch_percent": 0.0, + "branches": 4, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 31 + }, + "raven/proactive_engine/sentinel/trigger_policy/derive_dnd.py": { + "branch_percent": 90.0, + "branches": 50, + "covered_branches": 45, + "covered_lines": 107, + "line_percent": 92.241379, + "statements": 116 + }, + "raven/proactive_engine/sentinel/trigger_policy/policy.py": { + "branch_percent": 78.571429, + "branches": 168, + "covered_branches": 132, + "covered_lines": 296, + "line_percent": 87.833828, + "statements": 337 + }, + "raven/proactive_engine/sentinel/trigger_policy/prefs.py": { + "branch_percent": 90.0, + "branches": 20, + "covered_branches": 18, + "covered_lines": 62, + "line_percent": 93.939394, + "statements": 66 + }, + "raven/proactive_engine/sentinel/trigger_policy/prompts.py": { + "branch_percent": 50.0, + "branches": 58, + "covered_branches": 29, + "covered_lines": 58, + "line_percent": 56.31068, + "statements": 103 + }, + "raven/proactive_engine/sentinel/types.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 115, + "line_percent": 100.0, + "statements": 115 + }, + "raven/proactive_engine/system_events.py": { + "branch_percent": 83.333333, + "branches": 6, + "covered_branches": 5, + "covered_lines": 38, + "line_percent": 97.435897, + "statements": 39 + }, + "raven/proactive_engine/wake.py": { + "branch_percent": 91.666667, + "branches": 12, + "covered_branches": 11, + "covered_lines": 44, + "line_percent": 95.652174, + "statements": 46 + }, + "raven/providers/__init__.py": { + "branch_percent": 50.0, + "branches": 2, + "covered_branches": 1, + "covered_lines": 8, + "line_percent": 72.727273, + "statements": 11 + }, + "raven/providers/auth.py": { + "branch_percent": 92.5, + "branches": 40, + "covered_branches": 37, + "covered_lines": 121, + "line_percent": 97.580645, + "statements": 124 + }, + "raven/providers/azure_openai_provider.py": { + "branch_percent": 34.615385, + "branches": 26, + "covered_branches": 9, + "covered_lines": 57, + "line_percent": 67.058824, + "statements": 85 + }, + "raven/providers/base.py": { + "branch_percent": 83.75, + "branches": 80, + "covered_branches": 67, + "covered_lines": 220, + "line_percent": 89.068826, + "statements": 247 + }, + "raven/providers/capabilities.py": { + "branch_percent": 90.625, + "branches": 32, + "covered_branches": 29, + "covered_lines": 80, + "line_percent": 97.560976, + "statements": 82 + }, + "raven/providers/catalog.py": { + "branch_percent": 91.666667, + "branches": 12, + "covered_branches": 11, + "covered_lines": 61, + "line_percent": 98.387097, + "statements": 62 + }, + "raven/providers/chatgpt_token.py": { + "branch_percent": 100.0, + "branches": 6, + "covered_branches": 6, + "covered_lines": 47, + "line_percent": 100.0, + "statements": 47 + }, + "raven/providers/codex_catalog.py": { + "branch_percent": 81.25, + "branches": 16, + "covered_branches": 13, + "covered_lines": 56, + "line_percent": 98.245614, + "statements": 57 + }, + "raven/providers/common_models.py": { + "branch_percent": 95.0, + "branches": 20, + "covered_branches": 19, + "covered_lines": 55, + "line_percent": 100.0, + "statements": 55 + }, + "raven/providers/endpoint_rotor.py": { + "branch_percent": 67.857143, + "branches": 28, + "covered_branches": 19, + "covered_lines": 122, + "line_percent": 89.051095, + "statements": 137 + }, + "raven/providers/endpoints.py": { + "branch_percent": 100.0, + "branches": 4, + "covered_branches": 4, + "covered_lines": 23, + "line_percent": 100.0, + "statements": 23 + }, + "raven/providers/lazy.py": { + "branch_percent": 100.0, + "branches": 12, + "covered_branches": 12, + "covered_lines": 67, + "line_percent": 100.0, + "statements": 67 + }, + "raven/providers/litellm_provider.py": { + "branch_percent": 63.970588, + "branches": 136, + "covered_branches": 87, + "covered_lines": 250, + "line_percent": 76.687117, + "statements": 326 + }, + "raven/providers/litellm_provider_names.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 2, + "line_percent": 100.0, + "statements": 2 + }, + "raven/providers/litellm_setup.py": { + "branch_percent": 100.0, + "branches": 10, + "covered_branches": 10, + "covered_lines": 28, + "line_percent": 100.0, + "statements": 28 + }, + "raven/providers/minimax_oauth.py": { + "branch_percent": 75.0, + "branches": 48, + "covered_branches": 36, + "covered_lines": 187, + "line_percent": 86.175115, + "statements": 217 + }, + "raven/providers/minimax_oauth_provider.py": { + "branch_percent": 0.0, + "branches": 2, + "covered_branches": 0, + "covered_lines": 24, + "line_percent": 88.888889, + "statements": 27 + }, + "raven/providers/model_catalog_cache.py": { + "branch_percent": 80.0, + "branches": 10, + "covered_branches": 8, + "covered_lines": 38, + "line_percent": 86.363636, + "statements": 44 + }, + "raven/providers/openai_codex_provider.py": { + "branch_percent": 46.153846, + "branches": 104, + "covered_branches": 48, + "covered_lines": 150, + "line_percent": 66.666667, + "statements": 225 + }, + "raven/providers/per_model_provider.py": { + "branch_percent": 93.75, + "branches": 16, + "covered_branches": 15, + "covered_lines": 49, + "line_percent": 98.0, + "statements": 50 + }, + "raven/providers/pin.py": { + "branch_percent": 100.0, + "branches": 14, + "covered_branches": 14, + "covered_lines": 35, + "line_percent": 100.0, + "statements": 35 + }, + "raven/providers/prompt_cache.py": { + "branch_percent": 94.444444, + "branches": 18, + "covered_branches": 17, + "covered_lines": 57, + "line_percent": 100.0, + "statements": 57 + }, + "raven/providers/rates.py": { + "branch_percent": 85.897436, + "branches": 78, + "covered_branches": 67, + "covered_lines": 202, + "line_percent": 88.986784, + "statements": 227 + }, + "raven/providers/reasoning.py": { + "branch_percent": 100.0, + "branches": 6, + "covered_branches": 6, + "covered_lines": 15, + "line_percent": 100.0, + "statements": 15 + }, + "raven/providers/registry.py": { + "branch_percent": 98.214286, + "branches": 56, + "covered_branches": 55, + "covered_lines": 140, + "line_percent": 99.29078, + "statements": 141 + }, + "raven/providers/transcription.py": { + "branch_percent": 0.0, + "branches": 4, + "covered_branches": 0, + "covered_lines": 7, + "line_percent": 25.0, + "statements": 28 + }, + "raven/providers/wire.py": { + "branch_percent": 97.222222, + "branches": 36, + "covered_branches": 35, + "covered_lines": 68, + "line_percent": 98.550725, + "statements": 69 + }, + "raven/routing/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 2, + "line_percent": 100.0, + "statements": 2 + }, + "raven/routing/cache.py": { + "branch_percent": 0.0, + "branches": 20, + "covered_branches": 0, + "covered_lines": 26, + "line_percent": 26.262626, + "statements": 99 + }, + "raven/routing/classifier.py": { + "branch_percent": 0.0, + "branches": 12, + "covered_branches": 0, + "covered_lines": 22, + "line_percent": 33.333333, + "statements": 66 + }, + "raven/routing/fetcher.py": { + "branch_percent": 0.0, + "branches": 16, + "covered_branches": 0, + "covered_lines": 15, + "line_percent": 20.27027, + "statements": 74 + }, + "raven/routing/generate_embeddings.py": { + "branch_percent": 0.0, + "branches": 2, + "covered_branches": 0, + "covered_lines": 0, + "line_percent": 0.0, + "statements": 19 + }, + "raven/routing/knn_router.py": { + "branch_percent": 81.25, + "branches": 32, + "covered_branches": 26, + "covered_lines": 115, + "line_percent": 83.333333, + "statements": 138 + }, + "raven/routing/profiles.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 2, + "line_percent": 100.0, + "statements": 2 + }, + "raven/routing/router.py": { + "branch_percent": 60.0, + "branches": 10, + "covered_branches": 6, + "covered_lines": 31, + "line_percent": 54.385965, + "statements": 57 + }, + "raven/routing/selector.py": { + "branch_percent": 0.0, + "branches": 14, + "covered_branches": 0, + "covered_lines": 12, + "line_percent": 24.489796, + "statements": 49 + }, + "raven/routing/types.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 40, + "line_percent": 100.0, + "statements": 40 + }, + "raven/sandbox/__init__.py": { + "branch_percent": 100.0, + "branches": 6, + "covered_branches": 6, + "covered_lines": 21, + "line_percent": 91.304348, + "statements": 23 + }, + "raven/sandbox/_async_utils.py": { + "branch_percent": 50.0, + "branches": 2, + "covered_branches": 1, + "covered_lines": 8, + "line_percent": 72.727273, + "statements": 11 + }, + "raven/sandbox/_runtime.py": { + "branch_percent": 50.0, + "branches": 2, + "covered_branches": 1, + "covered_lines": 13, + "line_percent": 100.0, + "statements": 13 + }, + "raven/sandbox/boxlite_executor.py": { + "branch_percent": 60.0, + "branches": 50, + "covered_branches": 30, + "covered_lines": 183, + "line_percent": 75.0, + "statements": 244 + }, + "raven/sandbox/config.py": { + "branch_percent": 91.666667, + "branches": 12, + "covered_branches": 11, + "covered_lines": 45, + "line_percent": 97.826087, + "statements": 46 + }, + "raven/sandbox/debug_server.py": { + "branch_percent": 78.04878, + "branches": 82, + "covered_branches": 64, + "covered_lines": 256, + "line_percent": 77.108434, + "statements": 332 + }, + "raven/sandbox/direct_executor.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 24, + "line_percent": 92.307692, + "statements": 26 + }, + "raven/sandbox/interfaces.py": { + "branch_percent": 100.0, + "branches": 6, + "covered_branches": 6, + "covered_lines": 39, + "line_percent": 97.5, + "statements": 40 + }, + "raven/security/network.py": { + "branch_percent": 88.888889, + "branches": 18, + "covered_branches": 16, + "covered_lines": 46, + "line_percent": 82.142857, + "statements": 56 + }, + "raven/security/trust.py": { + "branch_percent": 100.0, + "branches": 8, + "covered_branches": 8, + "covered_lines": 18, + "line_percent": 100.0, + "statements": 18 + }, + "raven/session/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 2, + "line_percent": 100.0, + "statements": 2 + }, + "raven/session/export.py": { + "branch_percent": 52.941176, + "branches": 34, + "covered_branches": 18, + "covered_lines": 64, + "line_percent": 79.012346, + "statements": 81 + }, + "raven/session/manager.py": { + "branch_percent": 87.837838, + "branches": 74, + "covered_branches": 65, + "covered_lines": 229, + "line_percent": 93.469388, + "statements": 245 + }, + "raven/skill_hub/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 2, + "line_percent": 100.0, + "statements": 2 + }, + "raven/skill_hub/client.py": { + "branch_percent": 64.285714, + "branches": 28, + "covered_branches": 18, + "covered_lines": 75, + "line_percent": 75.0, + "statements": 100 + }, + "raven/spine/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 6, + "line_percent": 100.0, + "statements": 6 + }, + "raven/spine/delivery.py": { + "branch_percent": 92.5, + "branches": 40, + "covered_branches": 37, + "covered_lines": 122, + "line_percent": 100.0, + "statements": 122 + }, + "raven/spine/events.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 78, + "line_percent": 100.0, + "statements": 78 + }, + "raven/spine/message.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 19, + "line_percent": 100.0, + "statements": 19 + }, + "raven/spine/runner.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 13, + "line_percent": 100.0, + "statements": 13 + }, + "raven/spine/scheduler.py": { + "branch_percent": 93.902439, + "branches": 82, + "covered_branches": 77, + "covered_lines": 233, + "line_percent": 98.312236, + "statements": 237 + }, + "raven/spine/turn.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 27, + "line_percent": 100.0, + "statements": 27 + }, + "raven/token_wise/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 5, + "line_percent": 100.0, + "statements": 5 + }, + "raven/token_wise/base.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 22, + "line_percent": 100.0, + "statements": 22 + }, + "raven/token_wise/cache_optimizer.py": { + "branch_percent": 90.909091, + "branches": 22, + "covered_branches": 20, + "covered_lines": 58, + "line_percent": 96.666667, + "statements": 60 + }, + "raven/token_wise/pricing.py": { + "branch_percent": 100.0, + "branches": 6, + "covered_branches": 6, + "covered_lines": 18, + "line_percent": 100.0, + "statements": 18 + }, + "raven/token_wise/registry.py": { + "branch_percent": 100.0, + "branches": 10, + "covered_branches": 10, + "covered_lines": 32, + "line_percent": 96.969697, + "statements": 33 + }, + "raven/token_wise/system_and_tail_cache.py": { + "branch_percent": 0.0, + "branches": 14, + "covered_branches": 0, + "covered_lines": 14, + "line_percent": 35.897436, + "statements": 39 + }, + "raven/token_wise/usage_tracker.py": { + "branch_percent": 93.75, + "branches": 16, + "covered_branches": 15, + "covered_lines": 73, + "line_percent": 97.333333, + "statements": 75 + }, + "raven/tracing/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 4, + "line_percent": 80.0, + "statements": 5 + }, + "raven/tracing/config.py": { + "branch_percent": 60.0, + "branches": 10, + "covered_branches": 6, + "covered_lines": 31, + "line_percent": 62.0, + "statements": 50 + }, + "raven/tracing/context.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 33, + "line_percent": 76.744186, + "statements": 43 + }, + "raven/tracing/semconv.py": { + "branch_percent": 73.75, + "branches": 80, + "covered_branches": 59, + "covered_lines": 281, + "line_percent": 88.924051, + "statements": 316 + }, + "raven/tracing/spans.py": { + "branch_percent": 100.0, + "branches": 4, + "covered_branches": 4, + "covered_lines": 27, + "line_percent": 87.096774, + "statements": 31 + }, + "raven/tracing/store.py": { + "branch_percent": 72.222222, + "branches": 18, + "covered_branches": 13, + "covered_lines": 81, + "line_percent": 80.19802, + "statements": 101 + }, + "raven/tracing/trace.py": { + "branch_percent": 87.5, + "branches": 24, + "covered_branches": 21, + "covered_lines": 163, + "line_percent": 85.789474, + "statements": 190 + }, + "raven/tracing/usage.py": { + "branch_percent": 50.0, + "branches": 4, + "covered_branches": 2, + "covered_lines": 17, + "line_percent": 80.952381, + "statements": 21 + }, + "raven/tui_rpc/_ansi_filter.py": { + "branch_percent": 100.0, + "branches": 2, + "covered_branches": 2, + "covered_lines": 15, + "line_percent": 100.0, + "statements": 15 + }, + "raven/tui_rpc/_confirm_injection.py": { + "branch_percent": 50.0, + "branches": 2, + "covered_branches": 1, + "covered_lines": 23, + "line_percent": 95.833333, + "statements": 24 + }, + "raven/tui_rpc/_console_injection.py": { + "branch_percent": 100.0, + "branches": 4, + "covered_branches": 4, + "covered_lines": 27, + "line_percent": 100.0, + "statements": 27 + }, + "raven/tui_rpc/approval_broker.py": { + "branch_percent": 80.0, + "branches": 10, + "covered_branches": 8, + "covered_lines": 57, + "line_percent": 95.0, + "statements": 60 + }, + "raven/tui_rpc/confirm_broker.py": { + "branch_percent": 83.333333, + "branches": 6, + "covered_branches": 5, + "covered_lines": 39, + "line_percent": 92.857143, + "statements": 42 + }, + "raven/tui_rpc/dispatcher.py": { + "branch_percent": 65.384615, + "branches": 26, + "covered_branches": 17, + "covered_lines": 57, + "line_percent": 83.823529, + "statements": 68 + }, + "raven/tui_rpc/errors.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 73, + "line_percent": 100.0, + "statements": 73 + }, + "raven/tui_rpc/methods/__init__.py": { + "branch_percent": 100.0, + "branches": 8, + "covered_branches": 8, + "covered_lines": 40, + "line_percent": 100.0, + "statements": 40 + }, + "raven/tui_rpc/methods/_stubs.py": { + "branch_percent": 100.0, + "branches": 2, + "covered_branches": 2, + "covered_lines": 11, + "line_percent": 100.0, + "statements": 11 + }, + "raven/tui_rpc/methods/_typer_reflect.py": { + "branch_percent": 70.0, + "branches": 10, + "covered_branches": 7, + "covered_lines": 16, + "line_percent": 88.888889, + "statements": 18 + }, + "raven/tui_rpc/methods/approval.py": { + "branch_percent": 100.0, + "branches": 2, + "covered_branches": 2, + "covered_lines": 13, + "line_percent": 92.857143, + "statements": 14 + }, + "raven/tui_rpc/methods/cli_dispatch.py": { + "branch_percent": 96.153846, + "branches": 26, + "covered_branches": 25, + "covered_lines": 103, + "line_percent": 95.37037, + "statements": 108 + }, + "raven/tui_rpc/methods/commands.py": { + "branch_percent": 78.26087, + "branches": 46, + "covered_branches": 36, + "covered_lines": 107, + "line_percent": 87.704918, + "statements": 122 + }, + "raven/tui_rpc/methods/config.py": { + "branch_percent": 77.419355, + "branches": 62, + "covered_branches": 48, + "covered_lines": 134, + "line_percent": 90.540541, + "statements": 148 + }, + "raven/tui_rpc/methods/confirm.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 11, + "line_percent": 91.666667, + "statements": 12 + }, + "raven/tui_rpc/methods/model.py": { + "branch_percent": 96.666667, + "branches": 30, + "covered_branches": 29, + "covered_lines": 175, + "line_percent": 91.623037, + "statements": 191 + }, + "raven/tui_rpc/methods/question.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 11, + "line_percent": 91.666667, + "statements": 12 + }, + "raven/tui_rpc/methods/reload.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 7, + "line_percent": 100.0, + "statements": 7 + }, + "raven/tui_rpc/methods/session.py": { + "branch_percent": 87.837838, + "branches": 74, + "covered_branches": 65, + "covered_lines": 247, + "line_percent": 90.145985, + "statements": 274 + }, + "raven/tui_rpc/methods/setup.py": { + "branch_percent": 75.0, + "branches": 16, + "covered_branches": 12, + "covered_lines": 55, + "line_percent": 87.301587, + "statements": 63 + }, + "raven/tui_rpc/methods/slash_routing.py": { + "branch_percent": 85.714286, + "branches": 14, + "covered_branches": 12, + "covered_lines": 55, + "line_percent": 84.615385, + "statements": 65 + }, + "raven/tui_rpc/methods/system.py": { + "branch_percent": 100.0, + "branches": 4, + "covered_branches": 4, + "covered_lines": 33, + "line_percent": 94.285714, + "statements": 35 + }, + "raven/tui_rpc/methods/terminal.py": { + "branch_percent": 90.0, + "branches": 10, + "covered_branches": 9, + "covered_lines": 26, + "line_percent": 100.0, + "statements": 26 + }, + "raven/tui_rpc/methods/turn.py": { + "branch_percent": 76.666667, + "branches": 30, + "covered_branches": 23, + "covered_lines": 108, + "line_percent": 96.428571, + "statements": 112 + }, + "raven/tui_rpc/models.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 389, + "line_percent": 100.0, + "statements": 389 + }, + "raven/tui_rpc/question_broker.py": { + "branch_percent": 78.571429, + "branches": 14, + "covered_branches": 11, + "covered_lines": 55, + "line_percent": 93.220339, + "statements": 59 + }, + "raven/tui_rpc/server.py": { + "branch_percent": 60.714286, + "branches": 28, + "covered_branches": 17, + "covered_lines": 96, + "line_percent": 76.190476, + "statements": 126 + }, + "raven/tui_rpc/spine.py": { + "branch_percent": 92.5, + "branches": 40, + "covered_branches": 37, + "covered_lines": 112, + "line_percent": 99.115044, + "statements": 113 + }, + "raven/tui_rpc/subscriptions.py": { + "branch_percent": 82.352941, + "branches": 34, + "covered_branches": 28, + "covered_lines": 90, + "line_percent": 92.783505, + "statements": 97 + }, + "raven/utils/__init__.py": { + "branch_percent": 100.0, + "branches": 0, + "covered_branches": 0, + "covered_lines": 2, + "line_percent": 100.0, + "statements": 2 + }, + "raven/utils/atomic_io.py": { + "branch_percent": 83.333333, + "branches": 6, + "covered_branches": 5, + "covered_lines": 31, + "line_percent": 96.875, + "statements": 32 + }, + "raven/utils/bm25.py": { + "branch_percent": 100.0, + "branches": 16, + "covered_branches": 16, + "covered_lines": 40, + "line_percent": 100.0, + "statements": 40 + }, + "raven/utils/helpers.py": { + "branch_percent": 75.0, + "branches": 128, + "covered_branches": 96, + "covered_lines": 220, + "line_percent": 83.018868, + "statements": 265 + }, + "raven/utils/portable_lock.py": { + "branch_percent": 50.0, + "branches": 2, + "covered_branches": 1, + "covered_lines": 22, + "line_percent": 81.481481, + "statements": 27 + }, + "raven/utils/text.py": { + "branch_percent": 80.0, + "branches": 10, + "covered_branches": 8, + "covered_lines": 33, + "line_percent": 94.285714, + "statements": 35 + } + }, + "python": "3.12", + "reference_commit": "1cb604aab0bc37471057e029243b17d156aae4c5", + "schema_version": 1, + "source": "raven", + "totals": { + "branch_percent": 64.12266, + "branches": 13142, + "covered_branches": 8427, + "covered_lines": 32521, + "line_percent": 75.624956, + "statements": 43003 + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a90301b1..ac7b33dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,6 +73,8 @@ jobs: name: unit (py${{ matrix.python }} / ${{ matrix.os }}) runs-on: ${{ matrix.os }} timeout-minutes: 20 + env: + PYTHON_VERSION: ${{ matrix.python }} strategy: fail-fast: false matrix: @@ -80,6 +82,8 @@ jobs: python: ["3.12"] steps: - uses: actions/checkout@v7 + with: + fetch-depth: 0 - name: Install uv uses: astral-sh/setup-uv@v8.2.0 @@ -91,15 +95,54 @@ jobs: run: uv python install ${{ matrix.python }} - name: Install dependencies - run: uv sync --locked --all-extras --dev + run: uv sync --frozen --all-extras --dev --python ${{ matrix.python }} - - name: Unit tests (integration + e2e deselected by norecursedirs + addopts) + - name: Unit tests with line and branch coverage # TERM=dumb: typer's rich_utils force-enables colour when GITHUB_ACTIONS is # set, and the highlighter then splits option names with escape codes, so # `"--flag" in result.stdout` assertions fail only on CI. + run: make coverage + + - name: Coverage summary and lowest-covered files + if: ${{ !cancelled() && hashFiles('coverage.json') != '' }} + run: make coverage-summary + + - name: Overall coverage ratchet + if: ${{ !cancelled() && hashFiles('coverage.json') != '' }} + run: make coverage-ratchet + + - name: Coverage baseline cannot decrease + if: ${{ !cancelled() && github.event_name == 'pull_request' }} env: - TERM: dumb - run: uv run --all-extras pytest -q + COVERAGE_BASE_REF: origin/${{ github.base_ref }} + run: make coverage-baseline-check + + - name: PR diff coverage + if: ${{ !cancelled() && github.event_name == 'pull_request' && hashFiles('coverage.json') != '' }} + env: + COVERAGE_BASE_REF: origin/${{ github.base_ref }} + run: make coverage-diff + + - name: Explain skipped diff coverage + if: ${{ !cancelled() && github.event_name != 'pull_request' }} + run: echo "Diff coverage is a PR-only gate; push builds still enforce the overall ratchet." + + - name: Create auditable baseline candidate + if: ${{ !cancelled() && hashFiles('coverage.json') != '' }} + run: make coverage-baseline-candidate + + - name: Upload coverage reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: python-coverage-py${{ matrix.python }}-${{ matrix.os }} + path: | + coverage.xml + coverage.json + htmlcov/ + coverage-baseline-candidate.json + if-no-files-found: warn + retention-days: 30 windows-upgrade: name: Windows self-upgrade diff --git a/.gitignore b/.gitignore index 04df3378..3ca133ab 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,8 @@ poetry.lock .coverage .coverage.* coverage.xml +coverage.json +coverage-baseline-candidate.json htmlcov/ botpy.log nano.*.save diff --git a/AGENTS.md b/AGENTS.md index 09d1efa6..aea6dd75 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,6 +22,9 @@ Hard constraints only (violations get reverted / rejected). Soft suggestions and ### ยง1.1 Top rule: don't add comments unless necessary +- Every new code file must document its purpose in English with an appropriate + module docstring or equivalent file-level documentation. Inline comments + still follow the necessity rules below. - Match the style of surrounding lines. If neighboring code has no comments, **don't** add one to your new line. - Comment **only** when: - the logic is non-obvious; diff --git a/Makefile b/Makefile index aa501989..8b916740 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,15 @@ -.PHONY: help install install-deps lint lint-python lint-tui lint-bridge test test-python test-tui build build-tui build-bridge check-commits check-pr-title check-large-files ci clean +.PHONY: help install install-deps lint lint-python lint-tui lint-bridge test test-python test-tui coverage coverage-summary coverage-diff coverage-ratchet coverage-baseline-check coverage-baseline-candidate build build-tui build-bridge check-commits check-pr-title check-large-files ci clean PYTHON ?= python3 -PYTHON_LINT_TARGETS ?= scripts/check_commit_file.py scripts/check_commit_messages.py scripts/check_pr_title.py scripts/check_large_files.py scripts/commit_lint.py tests/test_commit_lint.py tests/test_large_file_check.py +PYTHON_VERSION ?= 3.12 +PYTHON_LINT_TARGETS ?= scripts/check_commit_file.py scripts/check_commit_messages.py scripts/check_pr_title.py scripts/check_large_files.py scripts/commit_lint.py scripts/coverage_gate.py tests/test_commit_lint.py tests/test_coverage_gate.py tests/test_large_file_check.py COMMIT_RANGE ?= origin/main..HEAD +COVERAGE_BASE_REF ?= origin/main +# Required coverage percentage for executable lines changed by a PR. +COVERAGE_DIFF_THRESHOLD ?= 90 +# Allowed line or branch regression in percentage points to absorb rounding noise. +COVERAGE_RATCHET_TOLERANCE ?= 0.05 +COVERAGE_REPORT_ARGS = --cov=raven --cov-branch --cov-report=term-missing:skip-covered --cov-report=xml --cov-report=json --cov-report=html help: @echo "Targets:" @@ -13,6 +20,10 @@ help: @echo " lint-tui TypeScript lint + RPC drift check" @echo " lint-bridge Bridge package build check" @echo " test Run focused Python checks and TUI tests" + @echo " coverage Run the default Python suite with line and branch coverage" + @echo " coverage-diff Check changed executable lines against COVERAGE_BASE_REF" + @echo " coverage-ratchet Check total line and branch coverage against the baseline" + @echo " coverage-baseline-check Ensure a proposed baseline never lowers the target branch" @echo " check-commits Validate Conventional Commit subjects" @echo " check-pr-title Validate the PR title in PR_TITLE" @echo " check-large-files Validate PR files avoid blocked assets and size bloat" @@ -20,11 +31,11 @@ help: @echo " clean Remove generated caches and build output" install-deps: - uv sync --frozen --extra dev --dev + uv sync --frozen --python $(PYTHON_VERSION) --extra dev --dev install: install-deps - uv run pre-commit install - uv run pre-commit install --hook-type commit-msg + uv run --frozen --python $(PYTHON_VERSION) pre-commit install + uv run --frozen --python $(PYTHON_VERSION) pre-commit install --hook-type commit-msg npm ci npm ci --prefix ui-tui npm ci --prefix bridge @@ -32,8 +43,8 @@ install: install-deps lint: lint-python lint-tui lint-bridge lint-python: - uv run --extra dev ruff check $(PYTHON_LINT_TARGETS) - uv run --extra dev ruff format --check $(PYTHON_LINT_TARGETS) + uv run --frozen --python $(PYTHON_VERSION) --extra dev ruff check $(PYTHON_LINT_TARGETS) + uv run --frozen --python $(PYTHON_VERSION) --extra dev ruff format --check $(PYTHON_LINT_TARGETS) lint-tui: npm run lint --prefix ui-tui @@ -46,7 +57,25 @@ lint-bridge: test: test-python test-tui test-python: - uv run --all-extras pytest -q + uv run --frozen --python $(PYTHON_VERSION) --all-extras pytest -q + +coverage: + TERM=dumb uv run --frozen --python $(PYTHON_VERSION) --all-extras pytest -q $(COVERAGE_REPORT_ARGS) + +coverage-summary: + uv run --frozen --python $(PYTHON_VERSION) python scripts/coverage_gate.py summary + +coverage-diff: + uv run --frozen --python $(PYTHON_VERSION) python scripts/coverage_gate.py diff --base-ref $(COVERAGE_BASE_REF) --threshold $(COVERAGE_DIFF_THRESHOLD) + +coverage-ratchet: + uv run --frozen --python $(PYTHON_VERSION) python scripts/coverage_gate.py ratchet --tolerance $(COVERAGE_RATCHET_TOLERANCE) + +coverage-baseline-check: + uv run --frozen --python $(PYTHON_VERSION) python scripts/coverage_gate.py baseline-check --base-ref $(COVERAGE_BASE_REF) + +coverage-baseline-candidate: + uv run --frozen --python $(PYTHON_VERSION) python scripts/coverage_gate.py baseline test-tui: npm test --prefix ui-tui @@ -61,18 +90,18 @@ build-bridge: check-commits: npx commitlint --from origin/main --to HEAD --config commitlint.config.cjs - PYTHONPATH=. uv run --extra dev python scripts/check_commit_messages.py $(COMMIT_RANGE) + PYTHONPATH=. uv run --frozen --python $(PYTHON_VERSION) --extra dev python scripts/check_commit_messages.py $(COMMIT_RANGE) check-pr-title: - PYTHONPATH=. uv run --extra dev python scripts/check_pr_title.py + PYTHONPATH=. uv run --frozen --python $(PYTHON_VERSION) --extra dev python scripts/check_pr_title.py check-large-files: - PYTHONPATH=. uv run --extra dev python scripts/check_large_files.py $(COMMIT_RANGE) + PYTHONPATH=. uv run --frozen --python $(PYTHON_VERSION) --extra dev python scripts/check_large_files.py $(COMMIT_RANGE) ci: lint test build clean: - rm -rf .pytest_cache .ruff_cache .uv-cache .mypy_cache htmlcov dist build + rm -rf .pytest_cache .ruff_cache .uv-cache .mypy_cache htmlcov coverage.xml coverage.json coverage-baseline-candidate.json dist build rm -rf ui-tui/dist ui-tui/coverage ui-tui/.vitest-cache ui-tui/packages/hermes-ink/dist rm -rf bridge/dist find . -type d -name __pycache__ -prune -exec rm -rf {} + diff --git a/pyproject.toml b/pyproject.toml index 279a57a0..de4eaf70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -270,6 +270,37 @@ markers = [ "llm_judge: uses an LLM as a grader for extraction quality (extra cost / non-determinism); opt-in", ] +[tool.coverage.run] +branch = true +source = ["raven"] +omit = [ + "raven/__main__.py", + "raven/evolver/__main__.py", + "raven/utils/win_fcntl_shim.py", +] + +[tool.coverage.report] +precision = 2 +show_missing = true +omit = [ + "raven/__main__.py", + "raven/evolver/__main__.py", + "raven/utils/win_fcntl_shim.py", +] +exclude_also = [ + "if TYPE_CHECKING:", + "if __name__ == .__main__.:", +] + +[tool.coverage.html] +directory = "htmlcov" + +[tool.coverage.xml] +output = "coverage.xml" + +[tool.coverage.json] +output = "coverage.json" + [dependency-groups] dev = [ "json-repair>=0.59.5", diff --git a/scripts/coverage_gate.py b/scripts/coverage_gate.py new file mode 100644 index 00000000..38a3748a --- /dev/null +++ b/scripts/coverage_gate.py @@ -0,0 +1,352 @@ +"""Coverage reporting and governance gates shared by CI and local development.""" + +from __future__ import annotations + +import argparse +import json +import re +import shutil +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Iterable + +SCHEMA_VERSION = 1 +DEFAULT_TOLERANCE = 0.05 +# Glob magic so ** matches zero-or-more path components; the default pathspec +# raven/**/*.py drops top-level files like raven/__init__.py because plain * spans +# slashes and the middle separator then has nothing to match. +PRODUCTION_PATHSPEC = ":(glob)raven/**/*.py" +DEFAULT_DIFF_THRESHOLD = 90.0 +OMITTED_PATHS = { + "raven/__main__.py", + "raven/evolver/__main__.py", + "raven/utils/win_fcntl_shim.py", +} + + +@dataclass(frozen=True) +class Metrics: + statements: int + covered_lines: int + branches: int + covered_branches: int + + @property + def line_percent(self) -> float: + return _percent(self.covered_lines, self.statements) + + @property + def branch_percent(self) -> float: + return _percent(self.covered_branches, self.branches) + + +@dataclass(frozen=True) +class DiffCoverage: + executable: frozenset[tuple[str, int]] + uncovered: frozenset[tuple[str, int]] + zero_coverage_files: tuple[str, ...] + unmeasured_files: tuple[str, ...] + + @property + def percent(self) -> float: + return _percent(len(self.executable) - len(self.uncovered), len(self.executable)) + + +def _percent(covered: int, total: int) -> float: + return 100.0 if total == 0 else covered * 100.0 / total + + +def _metrics(summary: dict[str, Any]) -> Metrics: + return Metrics( + statements=int(summary["num_statements"]), + covered_lines=int(summary["covered_lines"]), + branches=int(summary["num_branches"]), + covered_branches=int(summary["covered_branches"]), + ) + + +def load_coverage(path: Path) -> dict[str, Any]: + with path.open(encoding="utf-8") as handle: + report = json.load(handle) + if not isinstance(report.get("files"), dict) or not isinstance(report.get("totals"), dict): + raise ValueError(f"{path} is not a coverage.py JSON report") + return report + + +def print_summary(report: dict[str, Any], limit: int) -> None: + totals = _metrics(report["totals"]) + print(f"Line coverage: {totals.line_percent:.2f}% ({totals.covered_lines}/{totals.statements})") + print(f"Branch coverage: {totals.branch_percent:.2f}% ({totals.covered_branches}/{totals.branches})") + print(f"Lowest {limit} production files by combined coverage:") + ranked = sorted( + ( + (float(data["summary"]["percent_covered"]), name, _metrics(data["summary"])) + for name, data in report["files"].items() + if int(data["summary"]["num_statements"]) > 0 + ), + key=lambda item: (item[0], item[1]), + ) + for combined, name, metrics in ranked[:limit]: + print(f" {combined:6.2f}% line {metrics.line_percent:6.2f}% branch {metrics.branch_percent:6.2f}% {name}") + + +def create_baseline(report: dict[str, Any], commit: str) -> dict[str, Any]: + files = { + name: _metrics_payload(_metrics(data["summary"])) + for name, data in sorted(report["files"].items()) + if int(data["summary"]["num_statements"]) > 0 + } + return { + "schema_version": SCHEMA_VERSION, + "source": "raven", + "python": f"{sys.version_info.major}.{sys.version_info.minor}", + "reference_commit": commit, + "totals": _metrics_payload(_metrics(report["totals"])), + "files": files, + } + + +def _metrics_payload(metrics: Metrics) -> dict[str, int | float]: + return { + "statements": metrics.statements, + "covered_lines": metrics.covered_lines, + "line_percent": round(metrics.line_percent, 6), + "branches": metrics.branches, + "covered_branches": metrics.covered_branches, + "branch_percent": round(metrics.branch_percent, 6), + } + + +def write_baseline(report: dict[str, Any], output: Path, commit: str) -> None: + output.write_text(json.dumps(create_baseline(report, commit), indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(f"Wrote coverage baseline candidate to {output}") + + +def check_baseline_update(current: dict[str, Any], previous: dict[str, Any]) -> bool: + if current.get("schema_version") != SCHEMA_VERSION or previous.get("schema_version") != SCHEMA_VERSION: + raise ValueError("current and target-branch baselines must use the supported schema") + passed = True + for metric in ("line_percent", "branch_percent"): + value = float(current["totals"][metric]) + target = float(previous["totals"][metric]) + delta = value - target + print(f"Baseline {metric}: proposed={value:.6f}% target={target:.6f}% delta={delta:+.6f}pp") + if value < target: + passed = False + if passed: + print("Coverage baseline update is monotonic.") + else: + print("Coverage baseline files may not lower line or branch coverage.") + return passed + + +def check_ratchet(report: dict[str, Any], baseline: dict[str, Any], tolerance: float, limit: int) -> bool: + if baseline.get("schema_version") != SCHEMA_VERSION: + raise ValueError(f"unsupported baseline schema: {baseline.get('schema_version')!r}") + current = _metrics(report["totals"]) + expected = baseline["totals"] + checks = ( + ("line", current.line_percent, float(expected["line_percent"])), + ("branch", current.branch_percent, float(expected["branch_percent"])), + ) + failed = [(name, value, target) for name, value, target in checks if value + tolerance < target] + for name, value, target in checks: + delta = value - target + print(f"Ratchet {name}: current={value:.2f}% baseline={target:.2f}% delta={delta:+.2f}pp") + if not failed: + print(f"Coverage ratchet passed with {tolerance:.2f}pp tolerance.") + return True + + print("Largest per-file coverage declines:") + declines = _file_declines(report, baseline) + for decline, name, line_delta, branch_delta in declines[:limit]: + print(f" {decline:6.2f}pp line {line_delta:+7.2f}pp branch {branch_delta:+7.2f}pp {name}") + return False + + +def _file_declines(report: dict[str, Any], baseline: dict[str, Any]) -> list[tuple[float, str, float, float]]: + declines: list[tuple[float, str, float, float]] = [] + current_files = report["files"] + for name, expected in baseline["files"].items(): + data = current_files.get(name) + if data is None: + continue + current = _metrics(data["summary"]) + line_delta = current.line_percent - float(expected["line_percent"]) + branch_delta = current.branch_percent - float(expected["branch_percent"]) + decline = max(-line_delta, -branch_delta, 0.0) + if decline > 0: + declines.append((decline, name, line_delta, branch_delta)) + return sorted(declines, key=lambda item: (-item[0], item[1])) + + +_HUNK_RE = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@") + + +def parse_changed_lines(diff: str) -> dict[str, set[int]]: + """Return new-side hunk lines; deleted lines never enter the denominator.""" + changed: dict[str, set[int]] = {} + path: str | None = None + for line in diff.splitlines(): + if line.startswith("+++ b/"): + path = line[6:] + changed.setdefault(path, set()) + continue + match = _HUNK_RE.match(line) + if path is None or match is None: + continue + start = int(match.group(1)) + count = int(match.group(2) or "1") + changed[path].update(range(start, start + count)) + return changed + + +def calculate_diff_coverage( + report: dict[str, Any], changed: dict[str, set[int]], added_files: Iterable[str] = () +) -> DiffCoverage: + """Intersect changed lines with coverage.py statements to ignore comments and blanks.""" + executable: set[tuple[str, int]] = set() + uncovered: set[tuple[str, int]] = set() + zero_coverage_files: list[str] = [] + unmeasured_files: list[str] = [] + added = set(added_files) + for name, lines in changed.items(): + data = report["files"].get(name) + if data is None: + if name not in OMITTED_PATHS: + unmeasured_files.append(name) + continue + executed = set(data["executed_lines"]) + missing = set(data["missing_lines"]) + statements = executed | missing + for line in lines & statements: + item = (name, line) + executable.add(item) + if line in missing: + uncovered.add(item) + if name in added and statements and not executed: + zero_coverage_files.append(name) + return DiffCoverage( + frozenset(executable), + frozenset(uncovered), + tuple(sorted(zero_coverage_files)), + tuple(sorted(unmeasured_files)), + ) + + +def _git(*args: str) -> str: + executable = shutil.which("git") + if executable is None: + raise OSError("git is required for coverage diff checks") + completed = subprocess.run([executable, *args], check=True, capture_output=True, text=True) + return completed.stdout + + +def load_baseline_from_ref(base_ref: str, path: Path) -> dict[str, Any] | None: + _git("rev-parse", "--verify", f"{base_ref}^{{commit}}") + try: + content = _git("show", f"{base_ref}:{path.as_posix()}") + except subprocess.CalledProcessError: + return None + return json.loads(content) + + +def git_diff_inputs(base_ref: str, head: str | None) -> tuple[dict[str, set[int]], set[str]]: + """Resolve the merge base and collect Python changes for CI or the local worktree.""" + merge_base = _git("merge-base", base_ref, "HEAD").strip() + end = head or "" + range_args = [merge_base, end] if end else [merge_base] + diff = _git("diff", "--unified=0", "--no-color", "--diff-filter=ACMR", *range_args, "--", PRODUCTION_PATHSPEC) + names = _git("diff", "--name-only", "--diff-filter=A", *range_args, "--", PRODUCTION_PATHSPEC) + return parse_changed_lines(diff), {name for name in names.splitlines() if name} + + +def check_diff(report: dict[str, Any], base_ref: str, head: str | None, threshold: float) -> bool: + changed, added = git_diff_inputs(base_ref, head) + result = calculate_diff_coverage(report, changed, added) + covered = len(result.executable) - len(result.uncovered) + print(f"Diff coverage: {result.percent:.2f}% ({covered}/{len(result.executable)} executable changed lines)") + if result.uncovered: + print("Uncovered changed lines:") + for name, line in sorted(result.uncovered): + print(f" {name}:{line}") + if result.zero_coverage_files: + print("New production files with 0% line coverage:") + for name in result.zero_coverage_files: + print(f" {name}") + if result.unmeasured_files: + print("Changed production files missing from the coverage report:") + for name in result.unmeasured_files: + print(f" {name}") + passed = result.percent + 1e-9 >= threshold and not result.zero_coverage_files and not result.unmeasured_files + if passed: + print(f"Diff coverage passed the {threshold:.2f}% threshold.") + else: + print(f"Diff coverage failed the {threshold:.2f}% threshold.") + return passed + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--coverage-json", type=Path, default=Path("coverage.json")) + subparsers = parser.add_subparsers(dest="command", required=True) + + summary = subparsers.add_parser("summary") + summary.add_argument("--limit", type=int, default=10) + + baseline = subparsers.add_parser("baseline") + baseline.add_argument("--output", type=Path, default=Path("coverage-baseline-candidate.json")) + baseline.add_argument("--commit", default="HEAD") + + baseline_check = subparsers.add_parser("baseline-check") + baseline_check.add_argument("--baseline", type=Path, default=Path(".github/coverage-baseline.json")) + baseline_check.add_argument("--base-ref", required=True) + + ratchet = subparsers.add_parser("ratchet") + ratchet.add_argument("--baseline", type=Path, default=Path(".github/coverage-baseline.json")) + ratchet.add_argument("--tolerance", type=float, default=DEFAULT_TOLERANCE) + ratchet.add_argument("--limit", type=int, default=10) + + diff = subparsers.add_parser("diff") + diff.add_argument("--base-ref", required=True) + diff.add_argument("--head", default=None) + diff.add_argument("--threshold", type=float, default=DEFAULT_DIFF_THRESHOLD) + return parser + + +def main(argv: list[str] | None = None) -> int: + args = _parser().parse_args(argv) + try: + if args.command == "baseline-check": + with args.baseline.open(encoding="utf-8") as handle: + current_baseline = json.load(handle) + previous_baseline = load_baseline_from_ref(args.base_ref, args.baseline) + if previous_baseline is None: + print(f"No coverage baseline exists on {args.base_ref}; accepting the initial bootstrap baseline.") + return 0 + return 0 if check_baseline_update(current_baseline, previous_baseline) else 1 + + report = load_coverage(args.coverage_json) + if args.command == "summary": + print_summary(report, args.limit) + return 0 + if args.command == "baseline": + commit = _git("rev-parse", args.commit).strip() + write_baseline(report, args.output, commit) + return 0 + if args.command == "ratchet": + with args.baseline.open(encoding="utf-8") as handle: + baseline = json.load(handle) + return 0 if check_ratchet(report, baseline, args.tolerance, args.limit) else 1 + if args.command == "diff": + return 0 if check_diff(report, args.base_ref, args.head, args.threshold) else 1 + except (OSError, ValueError, KeyError, json.JSONDecodeError, subprocess.CalledProcessError) as exc: + print(f"coverage gate error: {exc}", file=sys.stderr) + return 2 + raise AssertionError(f"unhandled command: {args.command}") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/README.md b/tests/README.md index 08e79263..97997d7c 100644 --- a/tests/README.md +++ b/tests/README.md @@ -21,3 +21,80 @@ uv run pytest -m "not real_llm" Tests should avoid live network calls by default. When a test needs external services or a real model, guard it behind an explicit marker or environment variable so CI and local contributors get deterministic results. + +## Coverage Governance + +Raven uses three related checks: + +- **Coverage** measures line and branch execution across the default Python + suite. It is the broad health signal for the production `raven/` package. +- **Diff coverage** measures executable lines added or modified relative to the + PR target branch. Comments, deleted lines, and non-executable lines are not + part of its denominator. The initial threshold is 90%. +- **The ratchet** compares total line and branch coverage with the audited + baseline in `.github/coverage-baseline.json`. Neither metric may decline by + more than 0.05 percentage points. This protects the established level without + pretending the current project is already near 100%. + +Run the same coverage command used by CI: + +```bash +make coverage +``` + +`PYTHON_VERSION` defaults to the CI version (`3.12`) and is shared by all +Python Make targets. It can be overridden for exploratory compatibility runs, +for example `make coverage PYTHON_VERSION=3.13`; only the configured CI matrix +version may produce an authoritative baseline. + +The command prints missing lines and creates `coverage.xml`, `coverage.json`, +and `htmlcov/`. Open `htmlcov/index.html` in a browser for line-by-line detail. +Generated reports are ignored by git. + +After running coverage, use the local gates: + +```bash +make coverage-summary +make coverage-diff COVERAGE_BASE_REF=origin/main +make coverage-ratchet +make coverage-baseline-check COVERAGE_BASE_REF=origin/main +``` + +The summary prints separate total line and branch percentages plus the ten +lowest-covered production files. A diff failure lists every uncovered changed +line as `path:line`. A ratchet failure prints the current value, baseline, +change in percentage points, and the files with the largest declines. + +The CI unit job runs the default suite once and reuses its data for all gates. +It uploads XML, JSON, and HTML reports even when tests or a gate fail. Diff +coverage runs only for pull requests because a push has no PR target branch; +push builds still enforce the overall ratchet. + +### Baseline updates + +`make coverage-baseline-candidate` creates +`coverage-baseline-candidate.json` from the current report. The tracked +bootstrap baseline is accepted only after the introducing PR reproduces it in +Python 3.12 Ubuntu CI within the ratchet tolerance. After bootstrap, the +baseline must only be replaced with a candidate downloaded from a successful +`main` CI run. Review the reference commit and totals in the candidate before +replacement. Never edit the percentages by hand or lower the baseline to make +a change pass. PR CI compares a proposed baseline with the target branch and +rejects any decrease in either line or branch coverage. + +### Scope and exclusions + +The default coverage suite excludes tests marked `integration` or `e2e`, plus +`tests/integration/`, because they require real services, credentials, +subprocess stacks, VMs, or network access and are not deterministic unit-CI +inputs. Provider cases that require live credentials remain skipped. + +All production Python under `raven/` is measured except: + +- `raven/__main__.py` and `raven/evolver/__main__.py`, which only forward their + module entry points; +- `raven/utils/win_fcntl_shim.py`, which is executable only on Windows while the + canonical coverage job runs on Ubuntu. + +Type-checking-only blocks and `if __name__ == "__main__"` launcher blocks are +excluded as non-runtime paths. No low-coverage feature module is omitted. diff --git a/tests/test_coverage_gate.py b/tests/test_coverage_gate.py new file mode 100644 index 00000000..5301de2c --- /dev/null +++ b/tests/test_coverage_gate.py @@ -0,0 +1,217 @@ +"""Unit tests for coverage summaries, diff selection, and ratchet enforcement.""" + +from __future__ import annotations + +import json +import subprocess +import sys +import tomllib +from pathlib import Path + +import pytest + +from scripts.coverage_gate import ( + OMITTED_PATHS, + calculate_diff_coverage, + check_baseline_update, + check_ratchet, + create_baseline, + git_diff_inputs, + load_coverage, + parse_changed_lines, +) + + +def _file_summary( + *, statements: int, covered_lines: int, branches: int = 0, covered_branches: int = 0 +) -> dict[str, int | float]: + denominator = statements + branches + numerator = covered_lines + covered_branches + return { + "covered_lines": covered_lines, + "num_statements": statements, + "percent_covered": 100.0 if denominator == 0 else numerator * 100.0 / denominator, + "percent_covered_display": "0", + "missing_lines": statements - covered_lines, + "excluded_lines": 0, + "num_branches": branches, + "num_partial_branches": 0, + "covered_branches": covered_branches, + "missing_branches": branches - covered_branches, + } + + +def _report( + *, + statements: int = 10, + covered_lines: int = 8, + branches: int = 4, + covered_branches: int = 3, +) -> dict: + summary = _file_summary( + statements=statements, + covered_lines=covered_lines, + branches=branches, + covered_branches=covered_branches, + ) + return { + "meta": {"branch_coverage": True}, + "files": { + "raven/example.py": { + "executed_lines": list(range(1, covered_lines + 1)), + "missing_lines": list(range(covered_lines + 1, statements + 1)), + "excluded_lines": [], + "executed_branches": [], + "missing_branches": [], + "summary": summary, + } + }, + "totals": summary, + } + + +def test_load_coverage_rejects_non_coverage_json(tmp_path: Path) -> None: + path = tmp_path / "report.json" + path.write_text(json.dumps({"files": []}), encoding="utf-8") + + with pytest.raises(ValueError, match="not a coverage.py JSON report"): + load_coverage(path) + + +def test_documented_omissions_match_run_and_report_configuration() -> None: + with Path("pyproject.toml").open("rb") as handle: + coverage_config = tomllib.load(handle)["tool"]["coverage"] + + assert set(coverage_config["run"]["omit"]) == OMITTED_PATHS + assert set(coverage_config["report"]["omit"]) == OMITTED_PATHS + + +def test_parse_changed_lines_uses_only_new_hunk_ranges() -> None: + diff = """diff --git a/raven/example.py b/raven/example.py +--- a/raven/example.py ++++ b/raven/example.py +@@ -2,3 +2,4 @@ ++replacement +@@ -20,0 +22,2 @@ ++first ++second +diff --git a/tests/test_example.py b/tests/test_example.py +--- a/tests/test_example.py ++++ b/tests/test_example.py +@@ -1 +1 @@ +-old ++new +""" + + assert parse_changed_lines(diff) == { + "raven/example.py": {2, 3, 4, 5, 22, 23}, + "tests/test_example.py": {1}, + } + + +def test_diff_coverage_ignores_comments_deletions_and_non_executable_lines() -> None: + report = _report() + changed = {"raven/example.py": {2, 9, 10, 11, 12}} + + result = calculate_diff_coverage(report, changed) + + assert result.executable == {("raven/example.py", 2), ("raven/example.py", 9), ("raven/example.py", 10)} + assert result.uncovered == {("raven/example.py", 9), ("raven/example.py", 10)} + assert result.percent == pytest.approx(100 / 3) + + +def test_diff_coverage_flags_new_production_file_at_zero_percent() -> None: + report = _report(covered_lines=0) + + result = calculate_diff_coverage(report, {"raven/example.py": set(range(1, 11))}, ["raven/example.py"]) + + assert result.zero_coverage_files == ("raven/example.py",) + assert result.percent == 0.0 + + +def test_diff_coverage_fails_closed_for_unmeasured_production_file() -> None: + result = calculate_diff_coverage(_report(), {"raven/new_module.py": {1}}) + + assert result.unmeasured_files == ("raven/new_module.py",) + + +def test_diff_coverage_allows_documented_omitted_entry_point() -> None: + result = calculate_diff_coverage(_report(), {"raven/__main__.py": {1}}) + + assert result.unmeasured_files == () + + +def test_git_diff_inputs_includes_top_level_raven_files(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + def git(*args: str) -> None: + subprocess.run(["git", *args], cwd=tmp_path, check=True, capture_output=True, text=True) + + git("init") + git("config", "user.email", "test@example.com") + git("config", "user.name", "test") + (tmp_path / "README.md").write_text("base\n", encoding="utf-8") + git("add", "README.md") + git("commit", "-m", "base") + base = subprocess.run( + ["git", "rev-parse", "HEAD"], cwd=tmp_path, check=True, capture_output=True, text=True + ).stdout.strip() + + (tmp_path / "raven").mkdir() + (tmp_path / "raven" / "__init__.py").write_text("value = 1\n", encoding="utf-8") + (tmp_path / "raven" / "pkg").mkdir() + (tmp_path / "raven" / "pkg" / "mod.py").write_text("def run():\n return 1\n", encoding="utf-8") + git("add", "-A") + git("commit", "-m", "add raven sources") + + monkeypatch.chdir(tmp_path) + changed, added = git_diff_inputs(base, "HEAD") + + assert "raven/__init__.py" in changed + assert "raven/pkg/mod.py" in changed + assert "raven/__init__.py" in added + + +def test_create_baseline_records_separate_line_and_branch_metrics() -> None: + baseline = create_baseline(_report(), "abc123") + + assert baseline["reference_commit"] == "abc123" + assert baseline["python"] == f"{sys.version_info.major}.{sys.version_info.minor}" + assert baseline["totals"]["line_percent"] == 80.0 + assert baseline["totals"]["branch_percent"] == 75.0 + assert baseline["files"]["raven/example.py"]["covered_lines"] == 8 + + +def test_baseline_update_cannot_lower_either_metric(capsys: pytest.CaptureFixture[str]) -> None: + previous = create_baseline(_report(), "base") + proposed = create_baseline(_report(covered_lines=9, covered_branches=2), "head") + + assert check_baseline_update(proposed, previous) is False + output = capsys.readouterr().out + assert "line_percent: proposed=90.000000% target=80.000000%" in output + assert "branch_percent: proposed=50.000000% target=75.000000%" in output + assert "may not lower" in output + + +def test_baseline_update_accepts_equal_or_higher_metrics() -> None: + previous = create_baseline(_report(), "base") + proposed = create_baseline(_report(covered_lines=9, covered_branches=4), "head") + + assert check_baseline_update(proposed, previous) is True + + +def test_ratchet_allows_small_tolerance(capsys: pytest.CaptureFixture[str]) -> None: + baseline = create_baseline(_report(statements=10_000, covered_lines=8_001), "abc123") + current = _report(statements=10_000, covered_lines=8_000) + + assert check_ratchet(current, baseline, tolerance=0.05, limit=10) is True + assert "delta=-0.01pp" in capsys.readouterr().out + + +def test_ratchet_reports_total_and_largest_file_decline(capsys: pytest.CaptureFixture[str]) -> None: + baseline = create_baseline(_report(), "abc123") + current = _report(covered_lines=7, covered_branches=2) + + assert check_ratchet(current, baseline, tolerance=0.05, limit=10) is False + output = capsys.readouterr().out + assert "Ratchet line: current=70.00% baseline=80.00% delta=-10.00pp" in output + assert "Ratchet branch: current=50.00% baseline=75.00% delta=-25.00pp" in output + assert "raven/example.py" in output From e6e4b31f04447d4b1e408144cc2cb77ef276c12b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=9F=E5=9B=BD=E5=BA=86?= Date: Fri, 14 Aug 2026 17:17:29 +0800 Subject: [PATCH 2/2] fix: reproduce coverage baseline on ci and restore locked install The bootstrap baseline was generated on macOS; the Ubuntu 3.12 CI run produces slightly lower line and branch coverage, so the ratchet failed by more than its 0.05pp tolerance. Replace it with the candidate emitted by that CI run so the ratchet reproduces at delta 0.00. Also restore uv sync --locked on the unit job, since dropping it for --frozen removed the only CI check that uv.lock matches pyproject.toml, and reset the parser path on a +++ /dev/null target so a deleted file's hunks cannot be misattributed if parse_changed_lines runs on an unfiltered diff. Co-authored-by: Claude (claude-opus-4-8) --- .github/coverage-baseline.json | 94 +++++++++++++++++----------------- .github/workflows/ci.yml | 2 +- scripts/coverage_gate.py | 9 ++-- tests/test_coverage_gate.py | 17 ++++++ 4 files changed, 71 insertions(+), 51 deletions(-) diff --git a/.github/coverage-baseline.json b/.github/coverage-baseline.json index 45be6f2f..3df575a6 100644 --- a/.github/coverage-baseline.json +++ b/.github/coverage-baseline.json @@ -81,11 +81,11 @@ "statements": 90 }, "raven/agent/loop/main.py": { - "branch_percent": 82.207207, + "branch_percent": 81.981982, "branches": 444, - "covered_branches": 365, - "covered_lines": 1037, - "line_percent": 87.069689, + "covered_branches": 364, + "covered_lines": 1036, + "line_percent": 86.985726, "statements": 1191 }, "raven/agent/loop/recovery.py": { @@ -148,8 +148,8 @@ "branch_percent": 94.444444, "branches": 18, "covered_branches": 17, - "covered_lines": 53, - "line_percent": 98.148148, + "covered_lines": 52, + "line_percent": 96.296296, "statements": 54 }, "raven/agent/tools/base.py": { @@ -169,11 +169,11 @@ "statements": 264 }, "raven/agent/tools/file_search.py": { - "branch_percent": 78.571429, + "branch_percent": 64.285714, "branches": 70, - "covered_branches": 55, - "covered_lines": 170, - "line_percent": 82.926829, + "covered_branches": 45, + "covered_lines": 148, + "line_percent": 72.195122, "statements": 205 }, "raven/agent/tools/filesystem.py": { @@ -908,8 +908,8 @@ "branch_percent": 80.681818, "branches": 88, "covered_branches": 71, - "covered_lines": 239, - "line_percent": 95.219124, + "covered_lines": 238, + "line_percent": 94.820717, "statements": 251 }, "raven/cli/gateway_commands.py": { @@ -945,9 +945,9 @@ "statements": 1012 }, "raven/cli/onboard_everos.py": { - "branch_percent": 43.442623, + "branch_percent": 43.032787, "branches": 244, - "covered_branches": 106, + "covered_branches": 105, "covered_lines": 300, "line_percent": 54.644809, "statements": 549 @@ -961,11 +961,11 @@ "statements": 83 }, "raven/cli/provider_commands.py": { - "branch_percent": 84.375, + "branch_percent": 83.333333, "branches": 96, - "covered_branches": 81, - "covered_lines": 369, - "line_percent": 89.346247, + "covered_branches": 80, + "covered_lines": 368, + "line_percent": 89.104116, "statements": 413 }, "raven/cli/sandbox_commands.py": { @@ -1017,11 +1017,11 @@ "statements": 91 }, "raven/cli/tui_commands.py": { - "branch_percent": 74.637681, + "branch_percent": 75.362319, "branches": 138, - "covered_branches": 103, - "covered_lines": 365, - "line_percent": 81.111111, + "covered_branches": 104, + "covered_lines": 363, + "line_percent": 80.666667, "statements": 450 }, "raven/cli/update_notice.py": { @@ -1097,11 +1097,11 @@ "statements": 236 }, "raven/config/update_everos.py": { - "branch_percent": 85.714286, + "branch_percent": 92.857143, "branches": 14, - "covered_branches": 12, - "covered_lines": 61, - "line_percent": 91.044776, + "covered_branches": 13, + "covered_lines": 63, + "line_percent": 94.029851, "statements": 67 }, "raven/config/update_providers.py": { @@ -1217,11 +1217,11 @@ "statements": 28 }, "raven/context_engine/segments/render.py": { - "branch_percent": 93.75, + "branch_percent": 91.666667, "branches": 48, - "covered_branches": 45, - "covered_lines": 133, - "line_percent": 97.080292, + "covered_branches": 44, + "covered_lines": 132, + "line_percent": 96.350365, "statements": 137 }, "raven/context_engine/segments/skills.py": { @@ -2228,16 +2228,16 @@ "branch_percent": 75.0, "branches": 8, "covered_branches": 6, - "covered_lines": 38, - "line_percent": 66.666667, + "covered_lines": 34, + "line_percent": 59.649123, "statements": 57 }, "raven/plugin/memory/everos/backend.py": { "branch_percent": 86.111111, "branches": 108, "covered_branches": 93, - "covered_lines": 254, - "line_percent": 91.366906, + "covered_lines": 253, + "line_percent": 91.007194, "statements": 278 }, "raven/plugin/memory/everos/multimodal.py": { @@ -3028,16 +3028,16 @@ "branch_percent": 78.04878, "branches": 82, "covered_branches": 64, - "covered_lines": 256, - "line_percent": 77.108434, + "covered_lines": 258, + "line_percent": 77.710843, "statements": 332 }, "raven/sandbox/direct_executor.py": { "branch_percent": 100.0, "branches": 0, "covered_branches": 0, - "covered_lines": 24, - "line_percent": 92.307692, + "covered_lines": 26, + "line_percent": 100.0, "statements": 26 }, "raven/sandbox/interfaces.py": { @@ -3225,11 +3225,11 @@ "statements": 5 }, "raven/tracing/config.py": { - "branch_percent": 60.0, + "branch_percent": 70.0, "branches": 10, - "covered_branches": 6, - "covered_lines": 31, - "line_percent": 62.0, + "covered_branches": 7, + "covered_lines": 32, + "line_percent": 64.0, "statements": 50 }, "raven/tracing/context.py": { @@ -3562,15 +3562,15 @@ } }, "python": "3.12", - "reference_commit": "1cb604aab0bc37471057e029243b17d156aae4c5", + "reference_commit": "840d05101b393354e24bba0191b6d9ae40790df6", "schema_version": 1, "source": "raven", "totals": { - "branch_percent": 64.12266, + "branch_percent": 64.038959, "branches": 13142, - "covered_branches": 8427, - "covered_lines": 32521, - "line_percent": 75.624956, + "covered_branches": 8416, + "covered_lines": 32494, + "line_percent": 75.56217, "statements": 43003 } } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac7b33dd..54acb2de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,7 +95,7 @@ jobs: run: uv python install ${{ matrix.python }} - name: Install dependencies - run: uv sync --frozen --all-extras --dev --python ${{ matrix.python }} + run: uv sync --locked --all-extras --dev --python ${{ matrix.python }} - name: Unit tests with line and branch coverage # TERM=dumb: typer's rich_utils force-enables colour when GITHUB_ACTIONS is diff --git a/scripts/coverage_gate.py b/scripts/coverage_gate.py index 38a3748a..52e7d8d7 100644 --- a/scripts/coverage_gate.py +++ b/scripts/coverage_gate.py @@ -190,9 +190,12 @@ def parse_changed_lines(diff: str) -> dict[str, set[int]]: changed: dict[str, set[int]] = {} path: str | None = None for line in diff.splitlines(): - if line.startswith("+++ b/"): - path = line[6:] - changed.setdefault(path, set()) + if line.startswith("+++ "): + # Reset on a non-b/ target (e.g. +++ /dev/null) so a deleted file's + # hunks are not misattributed to the previously seen path. + path = line[6:] if line.startswith("+++ b/") else None + if path is not None: + changed.setdefault(path, set()) continue match = _HUNK_RE.match(line) if path is None or match is None: diff --git a/tests/test_coverage_gate.py b/tests/test_coverage_gate.py index 5301de2c..865d0d09 100644 --- a/tests/test_coverage_gate.py +++ b/tests/test_coverage_gate.py @@ -109,6 +109,23 @@ def test_parse_changed_lines_uses_only_new_hunk_ranges() -> None: } +def test_parse_changed_lines_ignores_deleted_file_hunks() -> None: + diff = """diff --git a/raven/removed.py b/raven/removed.py +--- a/raven/removed.py ++++ /dev/null +@@ -1,2 +0,0 @@ +-gone +-also gone +diff --git a/raven/kept.py b/raven/kept.py +--- a/raven/kept.py ++++ b/raven/kept.py +@@ -3 +3 @@ ++changed +""" + + assert parse_changed_lines(diff) == {"raven/kept.py": {3}} + + def test_diff_coverage_ignores_comments_deletions_and_non_executable_lines() -> None: report = _report() changed = {"raven/example.py": {2, 9, 10, 11, 12}}